44
55from __future__ import annotations
66
7- import calendar
87import datetime
98import operator
109from functools import total_ordering
1110from typing import TYPE_CHECKING , Any , Final , final , override
1211
1312from babel import dates
1413
14+ from betty .calendars .gregorian import Gregorian
1515from betty .data import Data
1616from betty .datas .aggregate .record import FieldDefinition
1717from betty .datas .aggregate .record .object import ObjectDefinition
2828 from collections .abc import Callable , Mapping
2929 from types import NotImplementedType
3030
31+ from betty .calendar import Calendar
3132 from betty .localizer import Localizer
3233
3334
5758 ),
5859 },
5960 samples = [
60- lambda : Sample (Date (), label = "Minimal" , size = Size .MINIMAL ),
61+ lambda : Sample (Date (1 ), label = "Minimal" , size = Size .MINIMAL ),
6162 lambda : Sample (Date (1970 , 1 , 1 , fuzzy = True ), label = "Full" , size = Size .FULL ),
6263 ],
6364)
6465class Date (Localizable , Data ):
6566 """
66- A (Gregorian) date.
67+ A date.
6768 """
6869
6970 __slots__ = ("year" , "month" , "day" , "fuzzy" , "normalized" )
7071
71- _localize_formats : Final [dict [tuple [bool , bool , bool ], Localizable ]] = {
72+ _format_date_patterns : Final [dict [tuple [bool , bool , bool ], Localizable ]] = {
7273 (True , True , True ): pgettext ("date" , "MMMM d, y" ),
7374 (True , True , False ): pgettext ("date" , "MMMM, y" ),
7475 (True , False , True ): pgettext ("date" , "'day' d 'of the month,' y" ),
@@ -85,11 +86,17 @@ def __init__(
8586 month : int | None = None ,
8687 day : int | None = None ,
8788 * ,
89+ calendar : Calendar = Gregorian (), # noqa: B008
8890 fuzzy : bool = False ,
8991 ):
9092 if year is None and month is None and day is None :
9193 raise ValueError ("Dates require a year, and/or a month, and/or a day." )
9294 super ().__init__ ()
95+ self .calendar : Final [Calendar ] = calendar
96+ """
97+ The calendar on which the date is expressed.
98+ """
99+
93100 self .year : Final [int | None ] = year
94101 """
95102 The year.
@@ -110,34 +117,38 @@ def __init__(
110117 Whether the year, month, and/or day are fuzzy, e.g. not exactly known.
111118 """
112119
113- self .normalized : Final [AnyDate | None ] = self ._normalize ()
114- """
115- The normalized, expanded, comparable variant of this date, if at least a year is available,
116- """
117-
118- def _normalize (self ) -> AnyDate | None :
120+ # @todo Do we use this still?
121+ def _get_comparable (self ) -> AnyDate | None :
119122 if self .year is None :
120123 return None
121124 if self .month is not None and self .day is not None :
122125 return self
123126 if self .month is None :
124127 month_start = 1
125- month_end = 12
128+ month_end = self . calendar . months ( self . year )
126129 else :
127130 month_start = month_end = self .month
128131 if self .day is None :
129132 day_start = 1
130- day_end = calendar .monthrange (self .year , month_end )[ 1 ]
133+ day_end = self . calendar .days (self .year , month_end )
131134 else :
132135 day_start = day_end = self .day
133136 return DateRange (
134- Date (self .year , month_start , day_start , fuzzy = self .fuzzy ),
135- Date (self .year , month_end , day_end , fuzzy = self .fuzzy ),
137+ Date (
138+ * self .calendar .to_gregorian (self .year , month_start , day_start ),
139+ calendar = Gregorian (),
140+ fuzzy = self .fuzzy ,
141+ ),
142+ Date (
143+ * self .calendar .to_gregorian (self .year , month_end , day_end ),
144+ calendar = Gregorian (),
145+ fuzzy = self .fuzzy ,
146+ ),
136147 )
137148
138149 @override
139150 def localize (self , localizer : Localizer , / ) -> LocalizedStr :
140- pattern = self ._localize_formats [
151+ pattern = self ._format_date_patterns [
141152 (self .year is not None , self .month is not None , self .day is not None )
142153 ].localize (localizer )
143154 localized = LocalizedStr (
@@ -159,25 +170,15 @@ def localize(self, localizer: Localizer, /) -> LocalizedStr:
159170 def _compare (
160171 self , other : Any , comparator : Callable [[Any , Any ], bool ], /
161172 ) -> bool | NotImplementedType :
162- if not isinstance ( other , Date ) :
173+ if not self . year :
163174 return NotImplemented
164- if self . normalized is self and other . normalized is other :
175+ if not isinstance ( other , Date ) :
165176 return NotImplemented
166- if not self . normalized or not other .normalized :
177+ if not other .year :
167178 return NotImplemented
168179 return comparator (
169- (
170- self .normalized .year ,
171- self .normalized .month ,
172- self .normalized .day ,
173- self .normalized .fuzzy ,
174- ),
175- (
176- other .normalized .year ,
177- other .normalized .month ,
178- other .normalized .day ,
179- other .normalized .fuzzy ,
180- ),
180+ (self .year , self .month , self .day , self .fuzzy ),
181+ (other .year , other .month , other .day , other .fuzzy ),
181182 )
182183
183184 def __contains__ (self , other : AnyDate ) -> bool :
@@ -241,7 +242,7 @@ class DateRange(Localizable, Data):
241242
242243 __slots__ = ("start" , "start_is_boundary" , "end" , "end_is_boundary" , "normalized" )
243244
244- _localize_formats : Final [Mapping [tuple [bool | None , bool | None ], Localizable ]] = {
245+ _localizables : Final [Mapping [tuple [bool | None , bool | None ], Localizable ]] = {
245246 (False , False ): _ ("from {start_date} until {end_date}" ),
246247 (False , True ): _ ("from {start_date} until sometime before {end_date}" ),
247248 (True , False ): _ ("from sometime after {start_date} until {end_date}" ),
@@ -268,57 +269,43 @@ def __init__(
268269 self .start_is_boundary : Final [bool ] = start_is_boundary
269270 self .end : Final [Date | None ] = end
270271 self .end_is_boundary : Final [bool ] = end_is_boundary
271- self .normalized : Final [DateRange | None ] = self ._normalize ()
272- """
273- The normalized, expanded, comparable variant of this date range, if at least a start and/or end year is available,
274- """
272+ self ._comparable : Final [DateRange | None ] = self ._get_comparable ()
275273
276- def _normalize (self ) -> DateRange | None :
277- start = self .start .normalized if self .start else None
278- end = self .end .normalized if self .end else None
279- if start is None and end is None :
280- return None
281- if self .start is start and self .end is end :
274+ def _get_comparable (self ) -> DateRange | None :
275+ if self .start and self .start .year is not None :
282276 return self
283- return DateRange (
284- None
285- if start is None
286- else start
287- if isinstance (start , Date )
288- else start .start ,
289- None if end is None else end if isinstance (end , Date ) else end .end ,
290- start_is_boundary = self .start_is_boundary ,
291- end_is_boundary = self .end_is_boundary ,
292- )
277+ if self .end and self .end .year is not None :
278+ return self
279+ return None
293280
294281 @override
295282 def localize (self , localizer : Localizer , / ) -> LocalizedStr :
296- formatter_configuration : tuple [bool | None , bool | None ] = (None , None )
297- formatter_arguments = {}
283+ localizable_key : tuple [bool | None , bool | None ] = (None , None )
284+ localizable_args = {}
298285
299286 if self .start :
300- formatter_arguments ["start_date" ] = self .start .localize (localizer )
301- formatter_configuration = (
287+ localizable_args ["start_date" ] = self .start .localize (localizer )
288+ localizable_key = (
302289 self .start_is_boundary ,
303- formatter_configuration [1 ],
290+ localizable_key [1 ],
304291 )
305292
306293 if self .end :
307- formatter_arguments ["end_date" ] = self .end .localize (localizer )
308- formatter_configuration = (
309- formatter_configuration [0 ],
294+ localizable_args ["end_date" ] = self .end .localize (localizer )
295+ localizable_key = (
296+ localizable_key [0 ],
310297 self .end_is_boundary ,
311298 )
312299
313300 return (
314301 self
315- ._localize_formats [ formatter_configuration ]
316- .format (** formatter_arguments )
302+ ._localizables [ localizable_key ]
303+ .format (** localizable_args )
317304 .localize (localizer )
318305 )
319306
320307 def __contains__ (self , other : AnyDate ) -> bool :
321- if not self .normalized :
308+ if not self ._comparable :
322309 return False
323310
324311 if isinstance (other , Date ):
0 commit comments