@@ -185,3 +185,100 @@ def isoformat(self):
185
185
def archive_ts_now ():
186
186
"""return tz-aware datetime obj for current time for usage as archive timestamp"""
187
187
return datetime .now (timezone .utc ) # utc time / utc timezone
188
+
189
+ class DatePatternError (ValueError ):
190
+ """Raised when a date: archive pattern cannot be parsed."""
191
+
192
+
193
+ def local (dt : datetime ) -> datetime :
194
+ """Attach the system local timezone to naive dt without converting."""
195
+ if dt .tzinfo is None :
196
+ dt = dt .replace (tzinfo = datetime .now ().astimezone ().tzinfo )
197
+ return dt
198
+
199
+
200
+ def exact_predicate (dt : datetime ):
201
+ """Return predicate matching archives whose ts equals dt (UTC)."""
202
+ dt_utc = local (dt ).astimezone (timezone .utc )
203
+ return lambda ts : ts == dt_utc
204
+
205
+
206
+ def interval_predicate (start : datetime , end : datetime ):
207
+ start_utc = local (start ).astimezone (timezone .utc )
208
+ end_utc = local (end ).astimezone (timezone .utc )
209
+ return lambda ts : start_utc <= ts < end_utc
210
+
211
+
212
+ def compile_date_pattern (expr : str ):
213
+ """
214
+ Turn a date: expression into a predicate ts->bool.
215
+ Supports:
216
+ 1) Full ISO‑8601 timestamps with minute (and optional seconds/fraction)
217
+ 2) Hour-only: YYYY‑MM‑DDTHH -> interval of 1 hour
218
+ 3) Minute-only: YYYY‑MM‑DDTHH:MM -> interval of 1 minute
219
+ 4) YYYY, YYYY‑MM, YYYY‑MM‑DD -> day/month/year intervals
220
+ 5) Unix epoch (@123456789) -> exact match
221
+ Naive inputs are assumed local, then converted into UTC.
222
+ TODO: verify working for fractional seconds; add timezone support.
223
+ """
224
+ expr = expr .strip ()
225
+
226
+ # 1) Full timestamp (with fraction)
227
+ full_re = re .compile (r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+" )
228
+ if full_re .match (expr ):
229
+ dt = parse_local_timestamp (expr , tzinfo = timezone .utc )
230
+ return exact_predicate (dt ) # no interval, since we have a fractional timestamp
231
+
232
+ # 2) Seconds-only
233
+ second_re = re .compile (r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$" )
234
+ if second_re .match (expr ):
235
+ start = parse_local_timestamp (expr , tzinfo = timezone .utc )
236
+ return interval_predicate (start , start + timedelta (seconds = 1 ))
237
+
238
+ # 2) Minute-only
239
+ minute_re = re .compile (r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$" )
240
+ if minute_re .match (expr ):
241
+ start = parse_local_timestamp (expr + ":00" , tzinfo = timezone .utc )
242
+ return interval_predicate (start , start + timedelta (minutes = 1 ))
243
+
244
+ # 3) Hour-only
245
+ hour_re = re .compile (r"^\d{4}-\d{2}-\d{2}T\d{2}$" )
246
+ if hour_re .match (expr ):
247
+ start = parse_local_timestamp (expr + ":00:00" , tzinfo = timezone .utc )
248
+ return interval_predicate (start , start + timedelta (hours = 1 ))
249
+
250
+
251
+ # Unix epoch (@123456789) - Note: We don't support fractional seconds here, since Unix epochs are almost always whole numbers.
252
+ if expr .startswith ("@" ):
253
+ try :
254
+ epoch = int (expr [1 :])
255
+ except ValueError :
256
+ raise DatePatternError (f"invalid epoch: { expr !r} " )
257
+ start = datetime .fromtimestamp (epoch , tz = timezone .utc )
258
+ return interval_predicate (start , start + timedelta (seconds = 1 )) # match within the second
259
+
260
+ # Year/Year-month/Year-month-day
261
+ parts = expr .split ("-" )
262
+ try :
263
+ if len (parts ) == 1 : # YYYY
264
+ year = int (parts [0 ])
265
+ start = datetime (year , 1 , 1 )
266
+ end = datetime (year + 1 , 1 , 1 )
267
+
268
+ elif len (parts ) == 2 : # YYYY‑MM
269
+ year , month = map (int , parts )
270
+ start = datetime (year , month , 1 )
271
+ end = offset_n_months (start , 1 )
272
+
273
+ elif len (parts ) == 3 : # YYYY‑MM‑DD
274
+ year , month , day = map (int , parts )
275
+ start = datetime (year , month , day )
276
+ end = start + timedelta (days = 1 )
277
+
278
+ else :
279
+ raise DatePatternError (f"unrecognised date: { expr !r} " )
280
+
281
+ except ValueError as e :
282
+ raise DatePatternError (str (e )) from None
283
+
284
+ return interval_predicate (start , end )
0 commit comments