Emfin2429 commited on
Commit
caaee0b
·
verified ·
1 Parent(s): 9185181

Update app.py

Browse files

Second attempt at calculating number of leave days acrued.

Files changed (1) hide show
  1. app.py +179 -61
app.py CHANGED
@@ -1,14 +1,36 @@
1
  """
2
  Per Diem / Leave Start Date Calculator (Form e49)
3
 
4
- Reserve members accrue leave (typically 15 days for a 6-month deployment)
5
- and take it during the last 15 days of the deployment, so the last day of
6
- leave equals the end date on their orders. Given that end date, this app
7
- tells them when their leave starts.
8
-
9
- Rule: leave is 15 days inclusive of start and end date.
10
- If the end date falls on a weekend or federal holiday, the member gets
11
- one extra day of leave (so the start date shifts one day earlier).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  Usage tracking: every time someone clicks "Calculate", a timestamped
14
  row is appended to a log file in a separate Hugging Face dataset repo
@@ -83,84 +105,180 @@ def log_usage():
83
  print(f"Usage logging failed: {e}")
84
 
85
 
86
- def is_holiday(end_date: date) -> bool:
87
- """Return True if end_date is a US federal holiday."""
88
- return end_date in US_HOLIDAYS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
 
91
- def is_weekend(end_date: date) -> bool:
92
- """Return True if end_date is a Saturday or Sunday."""
93
- return end_date.weekday() >= 5
 
 
 
 
 
 
 
94
 
95
 
96
- def fifteen_days_prior(end_date: date, exception: bool) -> date:
97
  """
98
- Return the date 15 days (inclusive) before end_date.
99
- If `exception` is True (end date is a weekend/holiday), the member
100
- gets one extra day of leave, so we go back 15 days instead of 14.
101
  """
102
- if exception:
103
- return end_date - timedelta(days=15)
104
- return end_date - timedelta(days=14)
105
-
106
-
107
- def calc_leave_dates(end_date: date) -> tuple[date, date]:
108
- """Determine the leave start date given the leave end date."""
109
- exception = is_weekend(end_date) or is_holiday(end_date)
110
- start_date = fifteen_days_prior(end_date, exception)
111
- return start_date, end_date
112
-
113
-
114
- def submit_leave_date(end_of_leave):
115
- if end_of_leave is None:
116
- return "Please select a date."
117
-
118
- # gr.DateTime can hand back either a datetime object or a string
119
- # depending on how the value was set, so handle both defensively.
120
- if isinstance(end_of_leave, str):
121
- end_of_leave = datetime.strptime(
122
- end_of_leave.split(" ")[0], "%Y-%m-%d"
123
- ).date()
124
- elif isinstance(end_of_leave, (int, float)):
125
- end_of_leave = datetime.fromtimestamp(end_of_leave).date()
126
- else:
127
- end_of_leave = end_of_leave.date()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
- start_date, end_date = calc_leave_dates(end_of_leave)
 
 
 
 
 
130
 
131
  reason = []
132
- if is_weekend(end_date):
133
  reason.append("your end date falls on a weekend")
134
- if is_holiday(end_date):
135
  reason.append("your end date is a federal holiday")
136
- extra_note = f" (You get an extra day of leave because {' and '.join(reason)}.)" if reason else ""
 
 
 
 
 
137
 
138
  # Log this use (best-effort, won't block or break the result).
139
  log_usage()
140
 
141
  return (
142
- f"Your leave starts on {start_date:%B %d, %Y} "
143
- f"and ends on {end_date:%B %d, %Y}.{extra_note}"
 
 
 
 
144
  )
145
 
146
 
147
  with gr.Blocks(title="Leave Start Date Calculator") as app:
148
  gr.Markdown(
149
  "# Leave Start Date Calculator\n"
150
- "Enter the **end date of your deployment (Form e49 end date)** "
151
- "to find out when your 15 days of leave will start."
152
- )
153
- date_input = gr.DateTime(
154
- label="When is the end of your leave?",
155
- include_time=False,
156
- type="datetime",
157
  )
 
 
 
 
 
 
 
 
 
 
 
158
  submit_btn = gr.Button("Calculate", variant="primary")
159
- output = gr.Textbox(label="Result", interactive=False)
160
 
161
- submit_btn.click(fn=submit_leave_date, inputs=date_input, outputs=output)
162
- date_input.submit(fn=submit_leave_date, inputs=date_input, outputs=output)
 
 
 
 
163
 
164
 
165
  if __name__ == "__main__":
166
- app.launch()
 
1
  """
2
  Per Diem / Leave Start Date Calculator (Form e49)
3
 
4
+ Reserve members accrue leave at 2.5 days per month of service (per the
5
+ official "Leave Accrual to Date of Separation" chart), and take that
6
+ leave during the final days of the deployment, so the last day of leave
7
+ equals the end date on their orders.
8
+
9
+ Given the FIRST and LAST day of the deployment, this app:
10
+ 1. Looks up how much leave was earned over that span, using the same
11
+ day-of-month / month accrual logic as the official chart (in 0.5-day
12
+ increments).
13
+ 2. Truncates that to a whole number of usable days (leave can only
14
+ actually be taken in whole days — e.g. 1.5 earned days means only
15
+ 1 day can be taken).
16
+ 3. Adds one bonus day if the deployment end date falls on a weekend or
17
+ federal holiday (existing rule, unchanged from the original app).
18
+ 4. Reports the leave start date, so the last day of leave still equals
19
+ the deployment end date.
20
+
21
+ --- IMPORTANT ASSUMPTION ABOUT THE ACCRUAL CHART ---
22
+ The official chart is expressed relative to the start of the fiscal/leave
23
+ year (Oct 1) and its values reset to ~0 every October. Real leave accrual
24
+ does NOT actually reset mid-deployment — it just keeps accumulating at a
25
+ constant 2.5 days/month. Since 6-month deployments routinely cross an
26
+ Oct 1 boundary, this app reproduces the chart's exact day-of-month /
27
+ month-of-year logic (verified to match the chart's printed values
28
+ exactly within any single fiscal year), but does NOT hard-reset the
29
+ counter at each Oct 1 — leave earned is simply
30
+ (accrual value at end date) - (accrual value at start date).
31
+ If your unit's actual policy is that leave literally zeroes out every
32
+ Oct 1 even mid-deployment, this calculation will need to change —
33
+ flag it and it can be adjusted to hard-reset instead.
34
 
35
  Usage tracking: every time someone clicks "Calculate", a timestamped
36
  row is appended to a log file in a separate Hugging Face dataset repo
 
105
  print(f"Usage logging failed: {e}")
106
 
107
 
108
+ def is_holiday(d: date) -> bool:
109
+ """Return True if d is a US federal holiday."""
110
+ return d in US_HOLIDAYS
111
+
112
+
113
+ def is_weekend(d: date) -> bool:
114
+ """Return True if d is a Saturday or Sunday."""
115
+ return d.weekday() >= 5
116
+
117
+
118
+ # --- Leave accrual chart logic ---
119
+ #
120
+ # The official "Leave Accrual to Date of Separation" table earns 2.5 days
121
+ # per calendar month, subdivided into five 0.5-day steps per month based
122
+ # on which 6-day bucket the day-of-month falls into:
123
+ # days 1-6 -> +0.5 (bucket 1)
124
+ # days 7-12 -> +1.0 (bucket 2)
125
+ # days 13-18 -> +1.5 (bucket 3)
126
+ # days 19-24 -> +2.0 (bucket 4)
127
+ # days 25-31 -> +2.5 (bucket 5, i.e. the full month)
128
+ #
129
+ # So within any month, accrued-since-start-of-month = bucket * 0.5, and
130
+ # each full elapsed calendar month is worth a flat 2.5 days on top of that.
131
+
132
+
133
+ def _day_bucket(day_of_month: int) -> int:
134
+ """Map a day-of-month (1-31) to its 6-day accrual bucket (1-5)."""
135
+ if day_of_month <= 6:
136
+ return 1
137
+ elif day_of_month <= 12:
138
+ return 2
139
+ elif day_of_month <= 18:
140
+ return 3
141
+ elif day_of_month <= 24:
142
+ return 4
143
+ else:
144
+ return 5 # 25-31 (or 25-28/29/30 for shorter months)
145
 
146
 
147
+ def _accrual_value(d: date) -> float:
148
+ """
149
+ A running accrual "odometer" value for date d: increases by 2.5 for
150
+ every full calendar month, plus the partial-month bucket amount.
151
+ This is anchored to an arbitrary fixed point (not Oct 1 of any
152
+ particular year), which is fine because only *differences* between
153
+ two of these values are ever used — the anchor cancels out.
154
+ """
155
+ absolute_month_index = d.year * 12 + (d.month - 1)
156
+ return absolute_month_index * 2.5 + _day_bucket(d.day) * 0.5
157
 
158
 
159
+ def earned_leave_days(deployment_start: date, deployment_end: date) -> float:
160
  """
161
+ Days of leave earned over the deployment, per the accrual chart,
162
+ rounded to the nearest 0.5 to avoid floating point drift.
 
163
  """
164
+ raw = _accrual_value(deployment_end) - _accrual_value(deployment_start)
165
+ return round(raw * 2) / 2
166
+
167
+
168
+ def calc_leave_plan(deployment_start: date, deployment_end: date) -> dict:
169
+ """
170
+ Given the deployment's first and last day, compute:
171
+ - earned: raw leave earned (chart lookup, in 0.5-day increments)
172
+ - usable: whole days actually usable (earned, truncated down)
173
+ - exception: whether the end date is a weekend/holiday bonus day
174
+ - total_days: usable days actually taken (usable + bonus day if any)
175
+ - leave_start: first day of leave (last day is always deployment_end)
176
+ """
177
+ earned = earned_leave_days(deployment_start, deployment_end)
178
+ usable = int(earned) # whole days only; e.g. 1.5 -> 1 (earned is always >= 0)
179
+
180
+ exception = is_weekend(deployment_end) or is_holiday(deployment_end)
181
+ total_days = usable + (1 if exception else 0)
182
+
183
+ leave_start = deployment_end - timedelta(days=max(total_days - 1, 0))
184
+
185
+ return {
186
+ "earned": earned,
187
+ "usable": usable,
188
+ "exception": exception,
189
+ "total_days": total_days,
190
+ "leave_start": leave_start,
191
+ "leave_end": deployment_end,
192
+ }
193
+
194
+
195
+ def _parse_date_input(value):
196
+ """
197
+ gr.DateTime can hand back either a datetime object, a string, or a
198
+ numeric timestamp depending on how the value was set, so handle all
199
+ three defensively.
200
+ """
201
+ if value is None:
202
+ return None
203
+ if isinstance(value, str):
204
+ return datetime.strptime(value.split(" ")[0], "%Y-%m-%d").date()
205
+ if isinstance(value, (int, float)):
206
+ return datetime.fromtimestamp(value).date()
207
+ return value.date()
208
+
209
+
210
+ def submit_leave_dates(deployment_start_input, deployment_end_input):
211
+ deployment_start = _parse_date_input(deployment_start_input)
212
+ deployment_end = _parse_date_input(deployment_end_input)
213
+
214
+ if deployment_start is None or deployment_end is None:
215
+ return "Please select both a deployment start date and end date."
216
+
217
+ if deployment_end < deployment_start:
218
+ return "The deployment end date can't be before the start date."
219
+
220
+ plan = calc_leave_plan(deployment_start, deployment_end)
221
 
222
+ if plan["total_days"] <= 0:
223
+ return (
224
+ f"Based on a deployment from {deployment_start:%B %d, %Y} to "
225
+ f"{deployment_end:%B %d, %Y}, you earned {plan['earned']:g} days "
226
+ "of leave — not enough to take a full day off."
227
+ )
228
 
229
  reason = []
230
+ if is_weekend(deployment_end):
231
  reason.append("your end date falls on a weekend")
232
+ if is_holiday(deployment_end):
233
  reason.append("your end date is a federal holiday")
234
+ extra_note = (
235
+ f" You get one extra day of leave because {' and '.join(reason)}, "
236
+ f"for {plan['total_days']} total days off."
237
+ if plan["exception"]
238
+ else ""
239
+ )
240
 
241
  # Log this use (best-effort, won't block or break the result).
242
  log_usage()
243
 
244
  return (
245
+ f"Based on a deployment from {deployment_start:%B %d, %Y} to "
246
+ f"{deployment_end:%B %d, %Y}, you earned {plan['earned']:g} days of "
247
+ f"leave. Since leave can only be taken in whole days, you can use "
248
+ f"{plan['usable']} day(s).{extra_note}\n\n"
249
+ f"Your leave starts on {plan['leave_start']:%B %d, %Y} "
250
+ f"and ends on {plan['leave_end']:%B %d, %Y}."
251
  )
252
 
253
 
254
  with gr.Blocks(title="Leave Start Date Calculator") as app:
255
  gr.Markdown(
256
  "# Leave Start Date Calculator\n"
257
+ "Enter the **first and last day of your deployment** to see how "
258
+ "much leave you earned and when it starts. Your last day of leave "
259
+ "will always be your deployment's end date."
 
 
 
 
260
  )
261
+ with gr.Row():
262
+ start_input = gr.DateTime(
263
+ label="First day of deployment",
264
+ include_time=False,
265
+ type="datetime",
266
+ )
267
+ end_input = gr.DateTime(
268
+ label="Last day of deployment (Form e49 end date)",
269
+ include_time=False,
270
+ type="datetime",
271
+ )
272
  submit_btn = gr.Button("Calculate", variant="primary")
273
+ output = gr.Textbox(label="Result", interactive=False, lines=4)
274
 
275
+ submit_btn.click(
276
+ fn=submit_leave_dates, inputs=[start_input, end_input], outputs=output
277
+ )
278
+ end_input.submit(
279
+ fn=submit_leave_dates, inputs=[start_input, end_input], outputs=output
280
+ )
281
 
282
 
283
  if __name__ == "__main__":
284
+ app.launch()