1
1
import calendar
2
2
import re
3
+ from time import struct_time
3
4
from datetime import date , datetime
4
- from dateutil .parser import parse
5
+ from operator import add , sub
6
+ from exceptions import OverflowError
7
+
5
8
from dateutil .relativedelta import relativedelta
9
+
6
10
from edtf import appsettings
7
11
8
12
EARLIEST = 'earliest'
17
21
PRECISION_DAY = "day"
18
22
19
23
24
+ TIME_EMPTY_TIME = [0 , 0 , 0 ] # tm_hour, tm_min, tm_sec
25
+ TIME_EMPTY_EXTRAS = [0 , 0 , - 1 ] # tm_wday, tm_yday, tm_isdst
26
+
27
+
28
+ def days_in_month (year , month ):
29
+ """
30
+ Return the number of days in the given year and month, where month is
31
+ 1=January to 12=December, and respecting leap years as identified by
32
+ `calendar.isleap()`
33
+ """
34
+ return {
35
+ 1 : 31 ,
36
+ 2 : 29 if calendar .isleap (year ) else 28 ,
37
+ 3 : 31 ,
38
+ 4 : 30 ,
39
+ 5 : 31 ,
40
+ 6 : 30 ,
41
+ 7 : 31 ,
42
+ 8 : 31 ,
43
+ 9 : 30 ,
44
+ 10 : 31 ,
45
+ 11 : 30 ,
46
+ 12 : 31 ,
47
+ }[month ]
48
+
49
+
50
+ def apply_relativedelta (op , time_struct , delta ):
51
+ """
52
+ Apply `relativedelta` to `struct_time` data structure.
53
+
54
+ This function is required because we cannot use standard `datetime` module
55
+ objects for conversion when the date/time is, or will become, outside the
56
+ boundary years 1 AD to 9999 AD.
57
+ """
58
+ if not delta :
59
+ return time_struct # No work to do
60
+
61
+ try :
62
+ dt_result = op (datetime (* time_struct [:6 ]), delta )
63
+ return dt_to_struct_time (dt_result )
64
+ except (OverflowError , ValueError ):
65
+ # Year is not within supported 1 to 9999 AD range
66
+ pass
67
+
68
+ # Here we fake the year to one in the acceptable range to avoid having to
69
+ # write our own date rolling logic
70
+
71
+ # Adjust the year to be close to the 2000 millenium in 1,000 year
72
+ # increments to try and retain accurate relative leap years
73
+ actual_year = time_struct .tm_year
74
+ millenium = int (float (actual_year ) / 1000 )
75
+ millenium_diff = (2 - millenium ) * 1000
76
+ adjusted_year = actual_year + millenium_diff
77
+ # Apply delta to the date/time with adjusted year
78
+ dt = datetime (* (adjusted_year ,) + time_struct [1 :6 ])
79
+ dt_result = op (dt , delta )
80
+ # Convert result year back to its original millenium
81
+ final_year = dt_result .year - millenium_diff
82
+ return struct_time (
83
+ (final_year ,) + dt_result .timetuple ()[1 :6 ] + tuple (TIME_EMPTY_EXTRAS ))
84
+
85
+
86
+ def dt_to_struct_time (dt ):
87
+ """
88
+ Convert a `datetime.date` or `datetime.datetime` to a `struct_time`
89
+ representation *with zero values* for data fields that we cannot always
90
+ rely on for ancient or far-future dates: tm_wday, tm_yday, tm_isdst
91
+
92
+ NOTE: If it wasn't for the requirement that the extra fields are unset
93
+ we could use the `timetuple()` method instead of this function.
94
+ """
95
+ if isinstance (dt , date ):
96
+ return struct_time (
97
+ [dt .year , dt .month , dt .day ] + TIME_EMPTY_TIME + TIME_EMPTY_EXTRAS
98
+ )
99
+ elif isinstance (dt , datetime ):
100
+ return struct_time (
101
+ [dt .year , dt .month , dt .day , dt .hour , dt .minute , dt .second ] +
102
+ TIME_EMPTY_EXTRAS
103
+ )
104
+ else :
105
+ raise NotImplementedError (
106
+ "Cannot convert %s to `struct_time`" % type (dt ))
107
+
108
+
20
109
class EDTFObject (object ):
21
110
"""
22
111
Object to attact to a parser to become instantiated when the parser
@@ -86,57 +175,63 @@ def set_is_uncertain(self, val):
86
175
87
176
def lower_fuzzy (self ):
88
177
strict_val = self .lower_strict ()
89
- # Do not exceed or adjust boundary datetimes
90
- if strict_val in (date .min , date .max ):
91
- return strict_val
92
- return strict_val - self ._get_fuzzy_padding (EARLIEST )
178
+ return apply_relativedelta (sub , strict_val , self ._get_fuzzy_padding (EARLIEST ))
93
179
94
180
def upper_fuzzy (self ):
95
181
strict_val = self .upper_strict ()
96
- # Do not exceed or adjust boundary datetimes
97
- if strict_val in (date .min , date .max ):
98
- return strict_val
99
- return strict_val + self ._get_fuzzy_padding (LATEST )
182
+ return apply_relativedelta (add , strict_val , self ._get_fuzzy_padding (LATEST ))
100
183
101
184
def __eq__ (self , other ):
102
185
if isinstance (other , EDTFObject ):
103
186
return str (self ) == str (other )
104
187
elif isinstance (other , date ):
105
188
return str (self ) == other .isoformat ()
189
+ elif isinstance (other , struct_time ):
190
+ return self ._strict_date () == other
106
191
return False
107
192
108
193
def __ne__ (self , other ):
109
194
if isinstance (other , EDTFObject ):
110
195
return str (self ) != str (other )
111
196
elif isinstance (other , date ):
112
197
return str (self ) != other .isoformat ()
198
+ elif isinstance (other , struct_time ):
199
+ return self ._strict_date () != other
113
200
return True
114
201
115
202
def __gt__ (self , other ):
116
203
if isinstance (other , EDTFObject ):
117
204
return self .lower_strict () > other .lower_strict ()
118
205
elif isinstance (other , date ):
206
+ return self .lower_strict () > dt_to_struct_time (other )
207
+ elif isinstance (other , struct_time ):
119
208
return self .lower_strict () > other
120
209
raise TypeError ("can't compare %s with %s" % (type (self ).__name__ , type (other ).__name__ ))
121
210
122
211
def __ge__ (self , other ):
123
212
if isinstance (other , EDTFObject ):
124
213
return self .lower_strict () >= other .lower_strict ()
125
214
elif isinstance (other , date ):
215
+ return self .lower_strict () >= dt_to_struct_time (other )
216
+ elif isinstance (other , struct_time ):
126
217
return self .lower_strict () >= other
127
218
raise TypeError ("can't compare %s with %s" % (type (self ).__name__ , type (other ).__name__ ))
128
219
129
220
def __lt__ (self , other ):
130
221
if isinstance (other , EDTFObject ):
131
222
return self .lower_strict () < other .lower_strict ()
132
223
elif isinstance (other , date ):
224
+ return self .lower_strict () < dt_to_struct_time (other )
225
+ elif isinstance (other , struct_time ):
133
226
return self .lower_strict () < other
134
227
raise TypeError ("can't compare %s with %s" % (type (self ).__name__ , type (other ).__name__ ))
135
228
136
229
def __le__ (self , other ):
137
230
if isinstance (other , EDTFObject ):
138
231
return self .lower_strict () <= other .lower_strict ()
139
232
elif isinstance (other , date ):
233
+ return self .lower_strict () <= dt_to_struct_time (other )
234
+ elif isinstance (other , struct_time ):
140
235
return self .lower_strict () <= other
141
236
raise TypeError ("can't compare %s with %s" % (type (self ).__name__ , type (other ).__name__ ))
142
237
@@ -204,49 +299,28 @@ def _precise_month(self, lean):
204
299
else :
205
300
return 1 if lean == EARLIEST else 12
206
301
207
- @staticmethod
208
- def _days_in_month (yr , month ):
209
- return calendar .monthrange (int (yr ), int (month ))[1 ]
210
-
211
302
def _precise_day (self , lean ):
212
303
if not self .day or self .day == 'uu' :
213
304
if lean == EARLIEST :
214
305
return 1
215
306
else :
216
- return self . _days_in_month (
307
+ return days_in_month (
217
308
self ._precise_year (LATEST ), self ._precise_month (LATEST )
218
309
)
219
310
else :
220
311
return int (self .day )
221
312
222
313
def _strict_date (self , lean ):
223
- py = self ._precise_year (lean )
224
- if py < 1 : # year is not positive
225
- return date .min
226
-
227
- parts = {
228
- 'year' : py ,
229
- 'month' : self ._precise_month (lean ),
230
- 'day' : self ._precise_day (lean ),
231
- }
232
-
233
- isoish = "%(year)s-%(month)02d-%(day)02d" % parts
234
-
235
- try :
236
- dt = parse (
237
- isoish ,
238
- fuzzy = True ,
239
- yearfirst = True ,
240
- dayfirst = False ,
241
- default = date .max if lean == LATEST else date .min
242
- )
243
- return dt
244
-
245
- except ValueError : # year is out of range
246
- if isoish < date .min .isoformat ():
247
- return date .min
248
- else :
249
- return date .max
314
+ """
315
+ Return a `time.struct_time` representation of the date.
316
+ """
317
+ return struct_time (
318
+ (
319
+ self ._precise_year (lean ),
320
+ self ._precise_month (lean ),
321
+ self ._precise_day (lean ),
322
+ ) + tuple (TIME_EMPTY_TIME ) + tuple (TIME_EMPTY_EXTRAS )
323
+ )
250
324
251
325
@property
252
326
def precision (self ):
@@ -274,11 +348,15 @@ def _strict_date(self, lean):
274
348
def __eq__ (self , other ):
275
349
if isinstance (other , datetime ):
276
350
return self .isoformat () == other .isoformat ()
351
+ elif isinstance (other , struct_time ):
352
+ return self ._strict_date () == other
277
353
return super (DateAndTime , self ).__eq__ (other )
278
354
279
355
def __ne__ (self , other ):
280
356
if isinstance (other , datetime ):
281
357
return self .isoformat () != other .isoformat ()
358
+ elif isinstance (other , struct_time ):
359
+ return self ._strict_date () != other
282
360
return super (DateAndTime , self ).__ne__ (other )
283
361
284
362
@@ -299,7 +377,7 @@ def _strict_date(self, lean):
299
377
return r
300
378
except AttributeError : # it's a string, or no date. Result depends on the upper date
301
379
upper = self .upper ._strict_date (LATEST )
302
- return upper - appsettings .DELTA_IF_UNKNOWN
380
+ return apply_relativedelta ( sub , upper , appsettings .DELTA_IF_UNKNOWN )
303
381
else :
304
382
try :
305
383
r = self .upper ._strict_date (lean )
@@ -308,10 +386,10 @@ def _strict_date(self, lean):
308
386
return r
309
387
except AttributeError : # an 'unknown' or 'open' string - depends on the lower date
310
388
if self .upper and (self .upper == "open" or self .upper .date == "open" ):
311
- return date .today () # it's still happening
389
+ return dt_to_struct_time ( date .today ()) # it's still happening
312
390
else :
313
391
lower = self .lower ._strict_date (EARLIEST )
314
- return lower + appsettings .DELTA_IF_UNKNOWN
392
+ return apply_relativedelta ( add , lower , appsettings .DELTA_IF_UNKNOWN )
315
393
316
394
317
395
# (* ************************** Level 1 *************************** *)
@@ -360,7 +438,7 @@ def __str__(self):
360
438
361
439
def _strict_date (self , lean ):
362
440
if self .date == "open" :
363
- return date .today ()
441
+ return dt_to_struct_time ( date .today () )
364
442
if self .date == "unknown" :
365
443
return None # depends on the other date
366
444
return self .date ._strict_date (lean )
@@ -406,15 +484,12 @@ def _precise_year(self):
406
484
407
485
def _strict_date (self , lean ):
408
486
py = self ._precise_year ()
409
- if py >= date .max .year :
410
- return date .max
411
- if py <= date .min .year :
412
- return date .min
413
-
414
487
if lean == EARLIEST :
415
- return date (py , 1 , 1 )
488
+ return struct_time (
489
+ [py , 1 , 1 ] + TIME_EMPTY_TIME + TIME_EMPTY_EXTRAS )
416
490
else :
417
- return date (py , 12 , 31 )
491
+ return struct_time (
492
+ [py , 12 , 31 ] + TIME_EMPTY_TIME + TIME_EMPTY_EXTRAS )
418
493
419
494
420
495
class Season (Date ):
0 commit comments