1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2000-2007  Donald N. Allingham
5# Copyright (C) 2009-2013  Douglas S. Blank
6# Copyright (C) 2013       Paul Franklin
7# Copyright (C) 2013-2014  Vassilii Khachaturov
8# Copyright (C) 2017       Nick Hall
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation; either version 2 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program; if not, write to the Free Software
22# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23#
24
25"""Support for dates."""
26
27#------------------------------------------------------------------------
28#
29# Set up logging
30#
31#------------------------------------------------------------------------
32import logging
33
34#-------------------------------------------------------------------------
35#
36# Gnome/GTK modules
37#
38#-------------------------------------------------------------------------
39
40
41#------------------------------------------------------------------------
42#
43# Gramps modules
44#
45#------------------------------------------------------------------------
46from .gcalendar import (gregorian_sdn, julian_sdn, hebrew_sdn,
47                        french_sdn, persian_sdn, islamic_sdn, swedish_sdn,
48                        gregorian_ymd, julian_ymd, hebrew_ymd,
49                        french_ymd, persian_ymd, islamic_ymd,
50                        swedish_ymd)
51from ..config import config
52from ..errors import DateError
53from ..const import GRAMPS_LOCALE as glocale
54_ = glocale.translation.sgettext
55
56LOG = logging.getLogger(".Date")
57
58class Span:
59    """
60    Span is used to represent the difference between two dates for three
61    main purposes: sorting, ranking, and describing.
62
63    sort = (base day count, offset)
64    minmax = (min days, max days)
65
66    """
67    BEFORE = config.get('behavior.date-before-range')
68    AFTER = config.get('behavior.date-after-range')
69    ABOUT = config.get('behavior.date-about-range')
70    ALIVE = config.get('behavior.max-age-prob-alive')
71    def __init__(self, date1, date2):
72        self.valid = (date1.sortval != 0 and date2.sortval != 0)
73        self.date1 = date1
74        self.date2 = date2
75        self.sort = (-9999, -9999)
76        self.minmax = (9999, -9999)
77        self.precision = 2
78        self.negative = False
79        if self.valid:
80            if self.date1.calendar != Date.CAL_GREGORIAN:
81                self.date1 = self.date1.to_calendar("gregorian")
82            if self.date2.calendar != Date.CAL_GREGORIAN:
83                self.date2 = self.date2.to_calendar("gregorian")
84            if self.date1.sortval < self.date2.sortval:
85                self.date1 = date2
86                self.date2 = date1
87                self.negative = True
88            if self.date1.get_modifier() == Date.MOD_NONE:
89                if   self.date2.get_modifier() == Date.MOD_NONE:
90                    val = self.date1.sortval - self.date2.sortval
91                    self.sort = (val, 0)
92                    self.minmax = (val, val)
93                elif self.date2.get_modifier() == Date.MOD_BEFORE:
94                    val = self.date1.sortval - self.date2.sortval
95                    self.sort = (val, -Span.BEFORE)
96                    self.minmax = (val - Span.BEFORE, val)
97                elif self.date2.get_modifier() == Date.MOD_AFTER:
98                    val = self.date1.sortval - self.date2.sortval
99                    self.sort = (val, Span.AFTER)
100                    self.minmax = (val, val + Span.AFTER)
101                elif self.date2.get_modifier() == Date.MOD_ABOUT:
102                    val = self.date1.sortval - self.date2.sortval
103                    self.sort = (val, -Span.ABOUT)
104                    self.minmax = (val - Span.ABOUT, val + Span.ABOUT)
105                elif self.date2.is_compound():
106                    start, stop = self.date2.get_start_stop_range()
107                    start = Date(*start)
108                    stop = Date(*stop)
109                    val1 = self.date1.sortval - stop.sortval  # min
110                    val2 = self.date1.sortval - start.sortval # max
111                    self.sort = (val1, val2 - val1)
112                    self.minmax = (val1, val2)
113            elif self.date1.get_modifier() == Date.MOD_BEFORE:
114                if   self.date2.get_modifier() == Date.MOD_NONE:
115                    val = self.date1.sortval - self.date2.sortval
116                    self.sort = (val, 0)
117                    self.minmax = (0, val)
118                elif self.date2.get_modifier() == Date.MOD_BEFORE:
119                    val = self.date1.sortval - self.date2.sortval
120                    self.sort = (val, -Span.BEFORE)
121                    self.minmax = (val, val + Span.BEFORE)
122                elif self.date2.get_modifier() == Date.MOD_AFTER:
123                    val = self.date1.sortval - self.date2.sortval
124                    self.sort = (val, -Span.AFTER)
125                    self.minmax = (0, val)
126                elif self.date2.get_modifier() == Date.MOD_ABOUT:
127                    val = self.date1.sortval - self.date2.sortval
128                    self.sort = (val, -Span.ABOUT)
129                    self.minmax = (val - Span.ABOUT, val + Span.ABOUT)
130                elif self.date2.is_compound():
131                    val = self.date1.sortval - self.date2.sortval
132                    self.sort = (val, -Span.ABOUT)
133                    self.minmax = (val - Span.ABOUT, val + Span.ABOUT)
134            elif self.date1.get_modifier() == Date.MOD_AFTER:
135                if self.date2.get_modifier() == Date.MOD_NONE:
136                    val = self.date1.sortval - self.date2.sortval
137                    self.sort = (val, Span.AFTER)
138                    self.minmax = (val, val + Span.AFTER)
139                elif self.date2.get_modifier() == Date.MOD_BEFORE:
140                    val = self.date1.sortval - self.date2.sortval
141                    self.sort = (val, Span.AFTER)
142                    self.minmax = (val - Span.BEFORE, val + Span.AFTER)
143                elif self.date2.get_modifier() == Date.MOD_AFTER:
144                    val = self.date1.sortval - self.date2.sortval
145                    self.sort = (val, Span.AFTER)
146                    self.minmax = (val, val + Span.AFTER)
147                elif self.date2.get_modifier() == Date.MOD_ABOUT:
148                    val = self.date1.sortval - self.date2.sortval
149                    self.sort = (val, -Span.ABOUT)
150                    self.minmax = (val - Span.ABOUT, val + Span.AFTER)
151                elif self.date2.is_compound():
152                    val = self.date1.sortval - self.date2.sortval
153                    self.sort = (val, -Span.ABOUT)
154                    self.minmax = (val - Span.ABOUT, val + Span.ABOUT)
155            elif self.date1.get_modifier() == Date.MOD_ABOUT:
156                if self.date2.get_modifier() == Date.MOD_NONE:
157                    val = self.date1.sortval - self.date2.sortval
158                    self.sort = (val, -Span.ABOUT)
159                    self.minmax = (val - Span.ABOUT, val + Span.ABOUT)
160                elif self.date2.get_modifier() == Date.MOD_BEFORE:
161                    val = self.date1.sortval - self.date2.sortval
162                    self.sort = (val, -Span.BEFORE)
163                    self.minmax = (val - Span.BEFORE, val + Span.ABOUT)
164                elif self.date2.get_modifier() == Date.MOD_AFTER:
165                    val = self.date1.sortval - self.date2.sortval
166                    self.sort = (val, Span.AFTER)
167                    self.minmax = (val - Span.ABOUT, val + Span.ABOUT)
168                elif self.date2.get_modifier() == Date.MOD_ABOUT:
169                    val = self.date1.sortval - self.date2.sortval
170                    self.sort = (val, -Span.ABOUT)
171                    self.minmax = (val - Span.ABOUT, val + Span.ABOUT)
172                elif self.date2.is_compound():
173                    val = self.date1.sortval - self.date2.sortval
174                    self.sort = (val, -Span.ABOUT)
175                    self.minmax = (val - Span.ABOUT, val + Span.ABOUT)
176            elif self.date1.is_compound():
177                if self.date2.get_modifier() == Date.MOD_NONE:
178                    start, stop = self.date1.get_start_stop_range()
179                    start = Date(*start)
180                    stop = Date(*stop)
181                    val1 = start.sortval - self.date2.sortval # min
182                    val2 = stop.sortval - self.date2.sortval # max
183                    self.sort = (val1, val2 - val1)
184                    self.minmax = (val1, val2)
185                elif self.date2.get_modifier() == Date.MOD_BEFORE:
186                    val = self.date1.sortval - self.date2.sortval
187                    self.sort = (val, Span.BEFORE)
188                    self.minmax = (val - Span.BEFORE, val + Span.BEFORE)
189                elif self.date2.get_modifier() == Date.MOD_AFTER:
190                    val = self.date1.sortval - self.date2.sortval
191                    self.sort = (val, -Span.AFTER)
192                    self.minmax = (val - Span.AFTER, val + Span.AFTER)
193                elif self.date2.get_modifier() == Date.MOD_ABOUT:
194                    val = self.date1.sortval - self.date2.sortval
195                    self.sort = (val, -Span.ABOUT)
196                    self.minmax = (val - Span.ABOUT, val + Span.ABOUT)
197                elif self.date2.is_compound():
198                    start1, stop1 = self.date1.get_start_stop_range()
199                    start2, stop2 = self.date2.get_start_stop_range()
200                    start1 = Date(*start1)
201                    start2 = Date(*start2)
202                    stop1 = Date(*stop1)
203                    stop2 = Date(*stop2)
204                    val1 = start1.sortval - stop2.sortval  # min
205                    val2 = stop1.sortval - start2.sortval # max
206                    self.sort = (val1, val2 - val1)
207                    self.minmax = (val1, val2)
208
209    def is_valid(self):
210        return self.valid
211
212    def tuple(self):
213        return self._diff(self.date1, self.date2)
214
215    def __getitem__(self, pos):
216        # Depricated!
217        return self._diff(self.date1, self.date2)[pos]
218
219    def __int__(self):
220        """
221        Returns the number of days of span.
222        """
223        if self.negative:
224            return -(self.sort[0] + self.sort[1])
225        else:
226            return self.sort[0] + self.sort[1]
227
228##    def __cmp__(self, other):
229##        """
230##        DEPRECATED - not available in python 3
231##
232##        Comparing two Spans for SORTING purposes.
233##        Use cmp(abs(int(span1)), abs(int(span2))) for comparing
234##        actual spans of times, as spans have directionality
235##        as indicated by negative values.
236##        """
237##        raise NotImplementedError
238##        if other is None:
239##            return cmp(int(self), -9999)
240##        else:
241##            return cmp(int(self), int(other))
242
243    def as_age(self):
244        """
245        Get Span as an age (will not return more than Span.ALIVE).
246        """
247        return self.get_repr(as_age=True)
248
249    def as_time(self):
250        """
251        Get Span as a time (can be greater than Span.ALIVE).
252        """
253        return self.get_repr(as_age=False)
254
255    def __repr__(self):
256        """
257        Get the Span as an age. Use Span.as_time() to get as a textual
258        description of time greater than Span.ALIVE.
259        """
260        return self.get_repr(as_age=True)
261
262    def get_repr(self, as_age=False, dlocale=glocale):
263        """
264        Get the representation as a time or age.
265
266        If dlocale is passed in (a :class:`.GrampsLocale`) then
267        the translated value will be returned instead.
268
269        :param dlocale: allow deferred translation of strings
270        :type dlocale: a :class:`.GrampsLocale` instance
271        """
272        # trans_text is a defined keyword (see po/update_po.py, po/genpot.sh)
273        trans_text = dlocale.translation.sgettext
274        _repr = trans_text("unknown")
275        # FIXME all this concatenation will fail for RTL languages -- really??
276        if self.valid:
277            fdate12 = self._format(self._diff(self.date1, self.date2), dlocale)
278            fdate12p1 = self._format(self._diff(self.date1, self.date2),
279                                     dlocale).format(precision=1)
280            if as_age and self._diff(self.date1, self.date2)[0] > Span.ALIVE:
281                _repr = trans_text("greater than %s years") % Span.ALIVE
282            elif self.date1.get_modifier() == Date.MOD_NONE:
283                if self.date2.get_modifier() == Date.MOD_NONE:
284                    _repr = fdate12
285                elif self.date2.get_modifier() == Date.MOD_BEFORE:
286                    _repr = trans_text("more than") + " " + fdate12
287                elif self.date2.get_modifier() == Date.MOD_AFTER:
288                    _repr = trans_text("less than") + " " + fdate12
289                elif self.date2.get_modifier() == Date.MOD_ABOUT:
290                    _repr = trans_text("age|about") + " " + fdate12p1
291                elif self.date2.is_compound():
292                    start, stop = self.date2.get_start_stop_range()
293                    start = Date(*start)
294                    stop = Date(*stop)
295                    _repr = (trans_text("between") + " " +
296                             self._format(self._diff(self.date1, stop),
297                                          dlocale) + " " +
298                             trans_text("and") + " " +
299                             self._format(self._diff(self.date1, start),
300                                          dlocale))
301            elif self.date1.get_modifier() == Date.MOD_BEFORE:
302                if   self.date2.get_modifier() == Date.MOD_NONE:
303                    _repr = trans_text("less than") + " " + fdate12
304                elif self.date2.get_modifier() == Date.MOD_BEFORE:
305                    _repr = self._format((-1, -1, -1))
306                elif self.date2.get_modifier() == Date.MOD_AFTER:
307                    _repr = trans_text("less than") + " " + fdate12
308                elif self.date2.get_modifier() == Date.MOD_ABOUT:
309                    _repr = trans_text("less than about") + " " + fdate12
310                elif self.date2.is_compound():
311                    _repr = trans_text("less than") + " " + fdate12
312            elif self.date1.get_modifier() == Date.MOD_AFTER:
313                if   self.date2.get_modifier() == Date.MOD_NONE:
314                    _repr = trans_text("more than") + " " + fdate12
315                elif self.date2.get_modifier() == Date.MOD_BEFORE:
316                    _repr = trans_text("more than") + " " + fdate12
317                elif self.date2.get_modifier() == Date.MOD_AFTER:
318                    _repr = self._format((-1, -1, -1))
319                elif self.date2.get_modifier() == Date.MOD_ABOUT:
320                    _repr = trans_text("more than about") + " " + fdate12p1
321                elif self.date2.is_compound():
322                    _repr = trans_text("more than") + " " + fdate12
323            elif self.date1.get_modifier() == Date.MOD_ABOUT:
324                if   self.date2.get_modifier() == Date.MOD_NONE:
325                    _repr = trans_text("age|about") + " " + fdate12p1
326                elif self.date2.get_modifier() == Date.MOD_BEFORE:
327                    _repr = trans_text("more than about") + " " + fdate12p1
328                elif self.date2.get_modifier() == Date.MOD_AFTER:
329                    _repr = trans_text("less than about") + " " + fdate12p1
330                elif self.date2.get_modifier() == Date.MOD_ABOUT:
331                    _repr = trans_text("age|about") + " " + fdate12p1
332                elif self.date2.is_compound():
333                    _repr = trans_text("age|about") + " " + fdate12p1
334            elif self.date1.is_compound():
335                if   self.date2.get_modifier() == Date.MOD_NONE:
336                    start, stop = self.date1.get_start_stop_range()
337                    start = Date(*start)
338                    stop = Date(*stop)
339                    _repr = (trans_text("between") + " " +
340                             self._format(self._diff(start, self.date2),
341                                          dlocale) + " " +
342                             trans_text("and") + " " +
343                             self._format(self._diff(stop, self.date2),
344                                          dlocale))
345                elif self.date2.get_modifier() == Date.MOD_BEFORE:
346                    _repr = trans_text("more than") + " " + fdate12
347                elif self.date2.get_modifier() == Date.MOD_AFTER:
348                    _repr = trans_text("less than") + " " + fdate12
349                elif self.date2.get_modifier() == Date.MOD_ABOUT:
350                    _repr = trans_text("age|about") + " " + fdate12p1
351                elif self.date2.is_compound():
352                    start1, stop1 = self.date1.get_start_stop_range()
353                    start2, stop2 = self.date2.get_start_stop_range()
354                    start1 = Date(*start1)
355                    start2 = Date(*start2)
356                    stop1 = Date(*stop1)
357                    stop2 = Date(*stop2)
358                    _repr = (trans_text("between") + " " +
359                             self._format(self._diff(start1, stop2), dlocale) +
360                             " " + trans_text("and") + " " +
361                             self._format(self._diff(stop1, start2), dlocale))
362        if _repr.find('-') == -1: # we don't have a negative value to return.
363            return _repr
364        else:
365            return '(' + _repr.replace('-', '') + ')'
366
367    def __eq__(self, other):
368        """
369        For comparing of Spans. Uses the integer representation.
370        """
371        if other is None:
372            return False
373        return int(self) == int(other)
374
375    def __lt__(self, other):
376        """
377        For less-than comparing of Spans. Uses the integer representation.
378        """
379        if other is None:
380            return False
381        return int(self) < int(other)
382
383    def __gt__(self, other):
384        """
385        For greater-than comparing of Spans. Uses the integer representation.
386        """
387        if other is None:
388            return True
389        return int(self) > int(other)
390
391    def format(self, precision=2, as_age=True, dlocale=glocale):
392        """
393        Force a string representation at a level of precision.
394
395        ==  ====================================================
396        1   only most significant level (year, month, day)
397        2   only most two significant levels (year, month, day)
398        3   at most three items of signifance (year, month, day)
399        ==  ====================================================
400
401        If dlocale is passed in (a :class:`.GrampsLocale`) then
402        the translated value will be returned instead.
403
404        :param dlocale: allow deferred translation of strings
405        :type dlocale: a :class:`.GrampsLocale` instance
406        """
407        self.precision = precision
408        return self.get_repr(as_age, dlocale=dlocale)
409
410    def _format(self, diff_tuple, dlocale=glocale):
411        """
412        If dlocale is passed in (a :class:`.GrampsLocale`) then
413        the translated value will be returned instead.
414
415        :param dlocale: allow deferred translation of strings
416        :type dlocale: a :class:`.GrampsLocale` instance
417        """
418        ngettext = dlocale.translation.ngettext # to see "nearby" comments
419        # trans_text is a defined keyword (see po/update_po.py, po/genpot.sh)
420        trans_text = dlocale.translation.sgettext
421        if diff_tuple == (-1, -1, -1):
422            return trans_text("unknown")
423        retval = ""
424        detail = 0
425        if diff_tuple[0] != 0:
426            # translators: leave all/any {...} untranslated
427            retval += ngettext("{number_of} year", "{number_of} years",
428                               diff_tuple[0]
429                              ).format(number_of=diff_tuple[0])
430            detail += 1
431        if self.precision == detail:
432            if diff_tuple[1] >= 6: # round up years
433                # translators: leave all/any {...} untranslated
434                retval = ngettext("{number_of} year", "{number_of} years",
435                                  diff_tuple[0] + 1
436                                 ).format(number_of=diff_tuple[0] + 1)
437            return retval
438        if diff_tuple[1] != 0:
439            if retval != "":
440                # translators: needed for Arabic, ignore otherwise
441                retval += trans_text(", ")
442            # translators: leave all/any {...} untranslated
443            retval += ngettext("{number_of} month", "{number_of} months",
444                               diff_tuple[1]
445                              ).format(number_of=diff_tuple[1])
446            detail += 1
447        if self.precision == detail:
448            return retval
449        if diff_tuple[2] != 0:
450            if retval != "":
451                # translators: needed for Arabic, ignore otherwise
452                retval += trans_text(", ")
453            # translators: leave all/any {...} untranslated
454            retval += ngettext("{number_of} day", "{number_of} days",
455                               diff_tuple[2]
456                              ).format(number_of=diff_tuple[2])
457            detail += 1
458        if self.precision == detail:
459            return retval
460        if retval == "":
461            retval = trans_text("0 days")
462        return retval
463
464    def _diff(self, date1, date2):
465        # We should make sure that Date2 + tuple -> Date1 and
466        #                          Date1 - tuple -> Date2
467        if date1.get_new_year() or date2.get_new_year():
468            days = date1.sortval - date2.sortval
469            years = days // 365
470            months = (days - years * 365) // 30
471            days = (days - years * 365) - months * 30
472            if self.negative:
473                return (-years, -months, -days)
474            else:
475                return (years, months, days)
476        ymd1 = [i or 1 for i in date1.get_ymd()]
477        ymd2 = [i or 1 for i in date2.get_ymd()]
478        # ymd1 - ymd2 (1998, 12, 32) - (1982, 12, 15)
479        # days:
480        if ymd2[2] > ymd1[2]:
481            # months:
482            if ymd2[1] > ymd1[1]:
483                ymd1[0] -= 1
484                ymd1[1] += 12
485            ymd1[1] -= 1
486            ymd1[2] += 31
487        # months:
488        if ymd2[1] > ymd1[1]:
489            ymd1[0] -= 1  # from years
490            ymd1[1] += 12 # to months
491        days = ymd1[2] - ymd2[2]
492        months = ymd1[1] - ymd2[1]
493        years = ymd1[0] - ymd2[0]
494        if days > 31:
495            months += days // 31
496            days = days % 31
497        if months > 12:
498            years += months // 12
499            months = months % 12
500        # estimate: (years, months, days)
501        # Check transitivity:
502        if date1.is_full() and date2.is_full():
503            edate = date1 - (years, months, days)
504            if edate < date2: # too small, strictly less than
505                diff = 0
506                while edate << date2 and diff < 60:
507                    diff += 1
508                    edate = edate + (0, 0, diff)
509                if diff == 60:
510                    return (-1, -1, -1)
511                if self.negative:
512                    return (-years, -months, -(days - diff))
513                else:
514                    return (years, months, days - diff)
515            elif edate > date2:
516                diff = 0
517                while edate >> date2 and diff > -60:
518                    diff -= 1
519                    edate -= (0, 0, abs(diff))
520                if diff == -60:
521                    return (-1, -1, -1)
522                if self.negative:
523                    return (-years, -months, -(days + diff))
524                else:
525                    return (years, months, days + diff)
526        if self.negative:
527            return (-years, -months, -days)
528        else:
529            return (years, months, days)
530
531#-------------------------------------------------------------------------
532#
533# Date class
534#
535#-------------------------------------------------------------------------
536class Date:
537    """
538    The core date handling class for Gramps.
539
540    Supports partial dates, compound dates and alternate calendars.
541    """
542    MOD_NONE = 0  # CODE
543    MOD_BEFORE = 1
544    MOD_AFTER = 2
545    MOD_ABOUT = 3
546    MOD_RANGE = 4
547    MOD_SPAN = 5
548    MOD_TEXTONLY = 6
549
550    QUAL_NONE = 0 # BITWISE
551    QUAL_ESTIMATED = 1
552    QUAL_CALCULATED = 2
553    #QUAL_INTERPRETED = 4 unused in source!!
554
555    CAL_GREGORIAN = 0 # CODE
556    CAL_JULIAN = 1
557    CAL_HEBREW = 2
558    CAL_FRENCH = 3
559    CAL_PERSIAN = 4
560    CAL_ISLAMIC = 5
561    CAL_SWEDISH = 6
562    CALENDARS = range(7)
563
564    NEWYEAR_JAN1 = 0 # CODE
565    NEWYEAR_MAR1 = 1
566    NEWYEAR_MAR25 = 2
567    NEWYEAR_SEP1 = 3
568
569    EMPTY = (0, 0, 0, False)
570
571    _POS_DAY = 0
572    _POS_MON = 1
573    _POS_YR = 2
574    _POS_SL = 3
575    _POS_RDAY = 4
576    _POS_RMON = 5
577    _POS_RYR = 6
578    _POS_RSL = 7
579
580    _calendar_convert = [
581        gregorian_sdn,
582        julian_sdn,
583        hebrew_sdn,
584        french_sdn,
585        persian_sdn,
586        islamic_sdn,
587        swedish_sdn,
588        ]
589
590    _calendar_change = [
591        gregorian_ymd,
592        julian_ymd,
593        hebrew_ymd,
594        french_ymd,
595        persian_ymd,
596        islamic_ymd,
597        swedish_ymd,
598        ]
599
600    calendar_names = ["Gregorian",
601                      "Julian",
602                      "Hebrew",
603                      "French Republican",
604                      "Persian",
605                      "Islamic",
606                      "Swedish"]
607
608
609    ui_calendar_names = [_("calendar|Gregorian"),
610                         _("calendar|Julian"),
611                         _("calendar|Hebrew"),
612                         _("calendar|French Republican"),
613                         _("calendar|Persian"),
614                         _("calendar|Islamic"),
615                         _("calendar|Swedish")]
616
617    def __init__(self, *source):
618        """
619        Create a new Date instance.
620        """
621        #### setup None, Date, or numbers
622        if len(source) == 0:
623            source = None
624        elif len(source) == 1:
625            if isinstance(source[0], int):
626                source = (source[0], 0, 0)
627            else:
628                source = source[0]
629        elif len(source) == 2:
630            source = (source[0], source[1], 0)
631        elif len(source) == 3:
632            pass # source is ok
633        else:
634            raise AttributeError("invalid args to Date: %s" % source)
635        self.format = None
636        #### ok, process either date or tuple
637        if isinstance(source, tuple):
638            self.calendar = Date.CAL_GREGORIAN
639            self.modifier = Date.MOD_NONE
640            self.quality = Date.QUAL_NONE
641            self.dateval = Date.EMPTY
642            self.text = ""
643            self.sortval = 0
644            self.newyear = 0
645            self.set_yr_mon_day(*source)
646        elif source:
647            self.calendar = source.calendar
648            self.modifier = source.modifier
649            self.quality = source.quality
650            self.dateval = source.dateval
651            self.text = source.text
652            self.sortval = source.sortval
653            self.newyear = source.newyear
654        else:
655            self.calendar = Date.CAL_GREGORIAN
656            self.modifier = Date.MOD_NONE
657            self.quality = Date.QUAL_NONE
658            self.dateval = Date.EMPTY
659            self.text = ""
660            self.sortval = 0
661            self.newyear = Date.NEWYEAR_JAN1
662
663    def serialize(self, no_text_date=False):
664        """
665        Convert to a series of tuples for data storage.
666        """
667        if no_text_date:
668            text = ''
669        else:
670            text = self.text
671
672        return (self.calendar, self.modifier, self.quality,
673                self.dateval, text, self.sortval, self.newyear)
674
675    def unserialize(self, data):
676        """
677        Load from the format created by serialize.
678        """
679        #FIXME: work around 3.1.0 error:
680        #2792: Dates in sourcereferences in person_ref_list not upgraded
681        #Added 2009/03/09
682        if len(data) == 7:
683            # This is correct:
684            (self.calendar, self.modifier, self.quality,
685             self.dateval, self.text, self.sortval, self.newyear) = data
686        elif len(data) == 6:
687            # This is necessary to fix 3.1.0 bug:
688            (self.calendar, self.modifier, self.quality,
689             self.dateval, self.text, self.sortval) = data
690            self.newyear = 0
691            # Remove all except if-part after 3.1.1
692        else:
693            raise DateError("Invalid date to unserialize")
694        return self
695
696    @classmethod
697    def get_schema(cls):
698        """
699        Returns the JSON Schema for this class.
700
701        :returns: Returns a dict containing the schema.
702        :rtype: dict
703        """
704        return {
705            "type": "object",
706            "title": _("Date"),
707            "properties": {
708                "_class": {"enum": [cls.__name__]},
709                "calendar": {"type": "integer",
710                             "title": _("Calendar")},
711                "modifier": {"type": "integer",
712                             "title": _("Modifier")},
713                "quality": {"type": "integer",
714                            "title": _("Quality")},
715                "dateval": {"type": "array",
716                            "title": _("Values"),
717                            "items": {"type": ["integer", "boolean"]}},
718                "text": {"type": "string",
719                         "title": _("Text")},
720                "sortval": {"type": "integer",
721                            "title": _("Sort value")},
722                "newyear": {"type": "integer",
723                            "title": _("New year begins")}
724            }
725        }
726
727    def copy(self, source):
728        """
729        Copy all the attributes of the given Date instance to the present
730        instance, without creating a new object.
731        """
732        self.calendar = source.calendar
733        self.modifier = source.modifier
734        self.quality = source.quality
735        self.dateval = source.dateval
736        self.text = source.text
737        self.sortval = source.sortval
738        self.newyear = source.newyear
739
740##   PYTHON 3 no __cmp__
741##    def __cmp__(self, other):
742##        """
743##        Compare two dates.
744##
745##        Comparison function. Allows the usage of equality tests.
746##        This allows you do run statements like 'date1 <= date2'
747##        """
748##        if isinstance(other, Date):
749##            return cmp(self.sortval, other.sortval)
750##        else:
751##            return -1
752
753    # Can't use this (as is) as this breaks comparing dates to None
754    #def __eq__(self, other):
755    #    return self.sortval == other.sortval
756
757    def __eq__(self, other):
758        """
759        Equality based on sort value, use is_equal/match instead if needed
760        """
761        if isinstance(other, Date):
762            return self.sortval == other.sortval
763        else:
764            #indicate this is not supported
765            return False
766
767    def __ne__(self, other):
768        """
769        Equality based on sort value, use is_equal/match instead if needed
770        """
771        if isinstance(other, Date):
772            return self.sortval != other.sortval
773        else:
774            #indicate this is not supported
775            return True
776
777    def __le__(self, other):
778        """
779        <= based on sort value, use match instead if needed
780        So this is different from using < which uses match!
781        """
782        if isinstance(other, Date):
783            return self.sortval <= other.sortval
784        else:
785            #indicate this is not supported
786            return NotImplemented
787
788    def __ge__(self, other):
789        """
790        >= based on sort value, use match instead if needed
791        So this is different from using > which uses match!
792        """
793        if isinstance(other, Date):
794            return self.sortval >= other.sortval
795        else:
796            #indicate this is not supported
797            return NotImplemented
798
799    def __add__(self, other):
800        """
801        Date arithmetic: Date() + years, or Date() + (years, [months, [days]]).
802        """
803        if isinstance(other, int):
804            return self.copy_offset_ymd(other)
805        elif isinstance(other, (tuple, list)):
806            return self.copy_offset_ymd(*other)
807        else:
808            raise AttributeError("unknown date add type: %s " % type(other))
809
810    def __radd__(self, other):
811        """
812        Add a number + Date() or (years, months, days) + Date().
813        """
814        return self + other
815
816    def __sub__(self, other):
817        """
818        Date arithmetic: Date() - years, Date - (y,m,d), or Date() - Date().
819        """
820        if isinstance(other, int):                # Date - value -> Date
821            return self.copy_offset_ymd(-other)
822        elif isinstance(other, (tuple, list)):    # Date - (y, m, d) -> Date
823            return self.copy_offset_ymd(*[-i for i in other])
824        elif isinstance(other, type(self)):       # Date1 - Date2 -> tuple
825            return Span(self, other)
826        else:
827            raise AttributeError("unknown date sub type: %s " % type(other))
828
829    def __contains__(self, string):
830        """
831        For use with "x in Date" syntax.
832        """
833        return str(string) in self.text
834
835    def __lshift__(self, other):
836        """
837        Comparison for strictly less than.
838        """
839        return self.match(other, comparison="<<")
840
841    def __lt__(self, other):
842        """
843        Comparison for less than using match, use sortval instead if needed.
844        """
845        return self.match(other, comparison="<")
846
847    def __rshift__(self, other):
848        """
849        Comparison for strictly greater than.
850        """
851        return self.match(other, comparison=">>")
852
853    def __gt__(self, other):
854        """
855        Comparison for greater than using match, use sortval instead if needed.
856        """
857        return self.match(other, comparison=">")
858
859    def is_equal(self, other):
860        """
861        Return 1 if the given Date instance is the same as the present
862        instance IN ALL REGARDS.
863
864        Needed, because the __cmp__ only looks at the sorting value, and
865        ignores the modifiers/comments.
866        """
867        if self.modifier == other.modifier \
868               and self.modifier == Date.MOD_TEXTONLY:
869            value = self.text == other.text
870        else:
871            value = (self.calendar == other.calendar and
872                     self.modifier == other.modifier and
873                     self.quality == other.quality and
874                     self.dateval == other.dateval)
875        return value
876
877    def get_start_stop_range(self):
878        """
879        Return the minimal start_date, and a maximal stop_date corresponding
880        to this date, given in Gregorian calendar.
881
882        Useful in doing range overlap comparisons between different dates.
883
884        Note that we stay in (YR,MON,DAY)
885        """
886
887        def yr_mon_day(dateval):
888            """
889            Local function to swap order for easy comparisons, and correct
890            year of slash date.
891
892            Slash date is given as year1/year2, where year1 is Julian year,
893            and year2=year1+1 the Gregorian year.
894
895            Slash date is already taken care of.
896            """
897            return (dateval[Date._POS_YR], dateval[Date._POS_MON],
898                    dateval[Date._POS_DAY])
899        def date_offset(dateval, offset):
900            """
901            Local function to do date arithmetic: add the offset, return
902            (year,month,day) in the Gregorian calendar.
903            """
904            new_date = Date()
905            new_date.set_yr_mon_day(*dateval[:3])
906            return new_date.offset(offset)
907
908        datecopy = Date(self)
909        #we do all calculation in Gregorian calendar
910        datecopy.convert_calendar(Date.CAL_GREGORIAN)
911
912        start = yr_mon_day(datecopy.get_start_date())
913        stop = yr_mon_day(datecopy.get_stop_date())
914
915        if stop == (0, 0, 0):
916            stop = start
917
918        stopmax = list(stop)
919        if stopmax[0] == 0: # then use start_year, if one
920            stopmax[0] = start[Date._POS_YR]
921        if stopmax[1] == 0:
922            stopmax[1] = 12
923        if stopmax[2] == 0:
924            stopmax[2] = 31
925        startmin = list(start)
926        if startmin[1] == 0:
927            startmin[1] = 1
928        if startmin[2] == 0:
929            startmin[2] = 1
930        # if BEFORE, AFTER, or ABOUT/EST, adjust:
931        if self.modifier == Date.MOD_BEFORE:
932            stopmax = date_offset(startmin, -1)
933            fdiff = config.get('behavior.date-before-range')
934            startmin = (stopmax[0] - fdiff, stopmax[1], stopmax[2])
935        elif self.modifier == Date.MOD_AFTER:
936            startmin = date_offset(stopmax, 1)
937            fdiff = config.get('behavior.date-after-range')
938            stopmax = (startmin[0] + fdiff, startmin[1], startmin[2])
939        elif (self.modifier == Date.MOD_ABOUT or
940              self.quality == Date.QUAL_ESTIMATED):
941            fdiff = config.get('behavior.date-about-range')
942            startmin = (startmin[0] - fdiff, startmin[1], startmin[2])
943            stopmax = (stopmax[0] + fdiff, stopmax[1], stopmax[2])
944        # return tuples not lists, for comparisons
945        return (tuple(startmin), tuple(stopmax))
946
947    def match_exact(self, other_date):
948        """
949        Perform an extact match between two dates. The dates are not treated
950        as being person-centric. This is used to match date ranges in places.
951        """
952        if other_date.modifier == Date.MOD_NONE:
953            return other_date.sortval == self.sortval
954        elif other_date.modifier == Date.MOD_BEFORE:
955            return other_date.sortval > self.sortval
956        elif other_date.modifier == Date.MOD_AFTER:
957            return other_date.sortval < self.sortval
958        elif other_date.is_compound():
959            start, stop = other_date.get_start_stop_range()
960            start = Date(*start)
961            stop = Date(*stop)
962            return start.sortval <= self.sortval <= stop.sortval
963        else:
964            return False
965
966    def match(self, other_date, comparison="="):
967        """
968        Compare two dates using sophisticated techniques looking for any match
969        between two possible dates, date spans and qualities.
970
971        The other comparisons for Date (is_equal() and __cmp() don't actually
972        look for anything other than a straight match, or a simple comparison
973        of the sortval.
974
975        ==========  =======================================================
976        Comparison  Returns
977        ==========  =======================================================
978        =,==        True if any part of other_date matches any part of self
979        <           True if any part of other_date < any part of self
980        <<          True if all parts of other_date < all parts of self
981        >           True if any part of other_date > any part of self
982        >>          True if all parts of other_date > all parts of self
983        ==========  =======================================================
984        """
985        if (other_date.modifier == Date.MOD_TEXTONLY or
986                self.modifier == Date.MOD_TEXTONLY):
987            if comparison == "=":
988                return self.text.upper().find(other_date.text.upper()) != -1
989            elif comparison == "==":
990                return self.text == other_date.text
991            else:
992                return False
993        if self.sortval == 0 or other_date.sortval == 0:
994            return False
995
996        # Obtain minimal start and maximal stop in Gregorian calendar
997        other_start, other_stop = other_date.get_start_stop_range()
998        self_start, self_stop = self.get_start_stop_range()
999
1000        if comparison == "=":
1001            # If some overlap then match is True, otherwise False.
1002            return ((self_start <= other_start <= self_stop) or
1003                    (self_start <= other_stop <= self_stop) or
1004                    (other_start <= self_start <= other_stop) or
1005                    (other_start <= self_stop <= other_stop))
1006        elif comparison == "==":
1007            # If they match exactly on start and stop
1008            return ((self_start == other_start) and
1009                    (other_stop == other_stop))
1010        elif comparison == "<":
1011            # If any < any
1012            return self_start < other_stop
1013        elif comparison == "<=":
1014            # If any < any
1015            return self_start <= other_stop
1016        elif comparison == "<<":
1017            # If all < all
1018            return self_stop < other_start
1019        elif comparison == ">":
1020            # If any > any
1021            return self_stop > other_start
1022        elif comparison == ">=":
1023            # If any > any
1024            return self_stop >= other_start
1025        elif comparison == ">>":
1026            # If all > all
1027            return self_start > other_stop
1028        else:
1029            raise AttributeError("invalid match comparison operator: '%s'" %
1030                                 comparison)
1031
1032    def __str__(self):
1033        """
1034        Produce a string representation of the Date object.
1035
1036        If the date is not valid, the text representation is displayed. If
1037        the date is a range or a span, a string in the form of
1038        'YYYY-MM-DD - YYYY-MM-DD' is returned. Otherwise, a string in
1039        the form of 'YYYY-MM-DD' is returned.
1040        """
1041        if self.quality == Date.QUAL_ESTIMATED:
1042            qual = "est "
1043        elif self.quality == Date.QUAL_CALCULATED:
1044            qual = "calc "
1045        else:
1046            qual = ""
1047
1048        if self.modifier == Date.MOD_BEFORE:
1049            pref = "bef "
1050        elif self.modifier == Date.MOD_AFTER:
1051            pref = "aft "
1052        elif self.modifier == Date.MOD_ABOUT:
1053            pref = "abt "
1054        else:
1055            pref = ""
1056
1057        nyear = self.newyear_to_str()
1058
1059        if self.calendar != Date.CAL_GREGORIAN:
1060            if nyear:
1061                cal = " (%s,%s)" % (Date.calendar_names[self.calendar], nyear)
1062            else:
1063                cal = " (%s)" % Date.calendar_names[self.calendar]
1064        else:
1065            if nyear:
1066                cal = " (%s)" % nyear
1067            else:
1068                cal = ""
1069
1070        if self.modifier == Date.MOD_TEXTONLY:
1071            val = self.text
1072        elif self.get_slash():
1073            val = "%04d/%d-%02d-%02d" % (
1074                self.dateval[Date._POS_YR] - 1,
1075                (self.dateval[Date._POS_YR]) % 10,
1076                self.dateval[Date._POS_MON],
1077                self.dateval[Date._POS_DAY])
1078        elif self.is_compound():
1079            val = "%04d-%02d-%02d - %04d-%02d-%02d" % (
1080                self.dateval[Date._POS_YR], self.dateval[Date._POS_MON],
1081                self.dateval[Date._POS_DAY], self.dateval[Date._POS_RYR],
1082                self.dateval[Date._POS_RMON], self.dateval[Date._POS_RDAY])
1083        else:
1084            val = "%04d-%02d-%02d" % (
1085                self.dateval[Date._POS_YR], self.dateval[Date._POS_MON],
1086                self.dateval[Date._POS_DAY])
1087        return "%s%s%s%s" % (qual, pref, val, cal)
1088
1089    def newyear_to_str(self):
1090        """
1091        Return the string representation of the newyear.
1092        """
1093        if self.newyear == Date.NEWYEAR_JAN1:
1094            nyear = ""
1095        elif self.newyear == Date.NEWYEAR_MAR1:
1096            nyear = "Mar1"
1097        elif self.newyear == Date.NEWYEAR_MAR25:
1098            nyear = "Mar25"
1099        elif self.newyear == Date.NEWYEAR_SEP1:
1100            nyear = "Sep1"
1101        elif isinstance(self.newyear, (list, tuple)):
1102            nyear = "%s-%s" % (self.newyear[0], self.newyear[1])
1103        else:
1104            nyear = "Err"
1105        return nyear
1106
1107    @staticmethod
1108    def newyear_to_code(string):
1109        """
1110        Return newyear code of string, where string is:
1111           '', 'Jan1', 'Mar1', '3-25', '9-1', etc.
1112        """
1113        string = string.strip().lower()
1114        if string == "" or string == "jan1":
1115            code = Date.NEWYEAR_JAN1
1116        elif string == "mar1":
1117            code = Date.NEWYEAR_MAR1
1118        elif string == "mar25":
1119            code = Date.NEWYEAR_MAR25
1120        elif string == "sep1":
1121            code = Date.NEWYEAR_SEP1
1122        elif "-" in string:
1123            try:
1124                code = tuple(map(int, string.split("-")))
1125            except:
1126                code = 0
1127        else:
1128            code = 0
1129        return code
1130
1131    def get_sort_value(self):
1132        """
1133        Return the sort value of Date object.
1134
1135        If the value is a text string, 0 is returned. Otherwise, the
1136        calculated sort date is returned. The sort date is rebuilt on every
1137        assignment.
1138
1139        The sort value is an integer representing the value. The sortval is
1140        the integer number of days that have elapsed since Monday, January 1,
1141        4713 BC in the proleptic Julian calendar.
1142
1143        .. seealso:: http://en.wikipedia.org/wiki/Julian_day
1144        """
1145        return self.sortval
1146
1147    def get_modifier(self):
1148        """
1149        Return an integer indicating the calendar selected.
1150
1151        The valid values are:
1152
1153        ============  =====================
1154        MOD_NONE      no modifier (default)
1155        MOD_BEFORE    before
1156        MOD_AFTER     after
1157        MOD_ABOUT     about
1158        MOD_RANGE     date range
1159        MOD_SPAN      date span
1160        MOD_TEXTONLY  text only
1161        ============  =====================
1162        """
1163        return self.modifier
1164
1165    def set_modifier(self, val):
1166        """
1167        Set the modifier for the date.
1168        """
1169        if val not in (Date.MOD_NONE, Date.MOD_BEFORE, Date.MOD_AFTER,
1170                       Date.MOD_ABOUT, Date.MOD_RANGE, Date.MOD_SPAN,
1171                       Date.MOD_TEXTONLY):
1172            raise DateError("Invalid modifier")
1173        self.modifier = val
1174
1175    def get_quality(self):
1176        """
1177        Return an integer indicating the calendar selected.
1178
1179        The valid values are:
1180
1181        ===============  ================
1182        QUAL_NONE        normal (default)
1183        QUAL_ESTIMATED   estimated
1184        QUAL_CALCULATED  calculated
1185        ===============  ================
1186        """
1187        return self.quality
1188
1189    def set_quality(self, val):
1190        """
1191        Set the quality selected for the date.
1192        """
1193        if val not in (Date.QUAL_NONE, Date.QUAL_ESTIMATED,
1194                       Date.QUAL_CALCULATED):
1195            raise DateError("Invalid quality")
1196        self.quality = val
1197
1198    def get_calendar(self):
1199        """
1200        Return an integer indicating the calendar selected.
1201
1202        The valid values are:
1203
1204        =============  ==========================================
1205        CAL_GREGORIAN  Gregorian calendar
1206        CAL_JULIAN     Julian calendar
1207        CAL_HEBREW     Hebrew (Jewish) calendar
1208        CAL_FRENCH     French Republican calendar
1209        CAL_PERSIAN    Persian calendar
1210        CAL_ISLAMIC    Islamic calendar
1211        CAL_SWEDISH    Swedish calendar 1700-03-01 -> 1712-02-30!
1212        =============  ==========================================
1213        """
1214        return self.calendar
1215
1216    def set_calendar(self, val):
1217        """
1218        Set the calendar selected for the date.
1219        """
1220        if val not in Date.CALENDARS:
1221            raise DateError("Invalid calendar")
1222        self.calendar = val
1223
1224    def get_start_date(self):
1225        """
1226        Return a tuple representing the start date.
1227
1228        If the date is a compound date (range or a span), it is the first part
1229        of the compound date. If the date is a text string, a tuple of
1230        (0, 0, 0, False) is returned. Otherwise, a date of (DD, MM, YY, slash)
1231        is returned. If slash is True, then the date is in the form of 1530/1.
1232        """
1233        if self.modifier == Date.MOD_TEXTONLY:
1234            val = Date.EMPTY
1235        else:
1236            val = self.dateval[0:4]
1237        return val
1238
1239    def get_stop_date(self):
1240        """
1241        Return a tuple representing the second half of a compound date.
1242
1243        If the date is not a compound date, (including text strings) a tuple
1244        of (0, 0, 0, False) is returned. Otherwise, a date of (DD, MM, YY, slash)
1245        is returned. If slash is True, then the date is in the form of 1530/1.
1246        """
1247        if self.is_compound():
1248            val = self.dateval[4:8]
1249        else:
1250            val = Date.EMPTY
1251        return val
1252
1253    def _get_low_item(self, index):
1254        """
1255        Return the item specified.
1256        """
1257        if self.modifier == Date.MOD_TEXTONLY:
1258            val = 0
1259        else:
1260            val = self.dateval[index]
1261        return val
1262
1263    def _get_low_item_valid(self, index):
1264        """
1265        Determine if the item specified is valid.
1266        """
1267        if self.modifier == Date.MOD_TEXTONLY:
1268            val = False
1269        else:
1270            val = self.dateval[index] != 0
1271        return val
1272
1273    def _get_high_item(self, index):
1274        """
1275        Return the item specified.
1276        """
1277        if self.is_compound():
1278            val = self.dateval[index]
1279        else:
1280            val = 0
1281        return val
1282
1283    def get_year(self):
1284        """
1285        Return the year associated with the date.
1286
1287        If the year is not defined, a zero is returned. If the date is a
1288        compound date, the lower date year is returned.
1289        """
1290        return self._get_low_item(Date._POS_YR)
1291
1292    def get_year_calendar(self, calendar_name=None):
1293        """
1294        Return the year of this date in the calendar name given.
1295
1296        Defaults to self's calendar if one is not given.
1297
1298        >>> Date(2009, 12, 8).to_calendar("hebrew").get_year_calendar()
1299        5770
1300        """
1301        if calendar_name:
1302            cal = lookup_calendar(calendar_name)
1303        else:
1304            cal = self.calendar
1305        if cal == self.calendar:
1306            return self.get_year()
1307        else:
1308            retval = Date(self)
1309            retval.convert_calendar(cal)
1310            return retval.get_year()
1311
1312    def get_new_year(self):
1313        """
1314        Return the new year code associated with the date.
1315        """
1316        return self.newyear
1317
1318    def set_new_year(self, value):
1319        """
1320        Set the new year code associated with the date.
1321        """
1322        self.newyear = value
1323
1324    def __set_yr_mon_day(self, year, month, day, pos_yr, pos_mon, pos_day):
1325        dlist = list(self.dateval)
1326        dlist[pos_yr] = year
1327        dlist[pos_mon] = month
1328        dlist[pos_day] = day
1329        self.dateval = tuple(dlist)
1330
1331    def set_yr_mon_day(self, year, month, day, remove_stop_date=None):
1332        """
1333        Set the year, month, and day values.
1334
1335        :param remove_stop_date:
1336            Required parameter for a compound date.
1337            When True, the stop date is changed to the same date as well.
1338            When False, the stop date is not changed.
1339        """
1340        if self.is_compound() and remove_stop_date is None:
1341            raise DateError("Required parameter remove_stop_date not set!")
1342
1343        self.__set_yr_mon_day(year, month, day,
1344                              Date._POS_YR, Date._POS_MON, Date._POS_DAY)
1345        self._calc_sort_value()
1346        if remove_stop_date and self.is_compound():
1347            self.set2_yr_mon_day(year, month, day)
1348
1349    def _assert_compound(self):
1350        if not self.is_compound():
1351            raise DateError("Operation allowed for compound dates only!")
1352
1353    def set2_yr_mon_day(self, year, month, day):
1354        """
1355        Set the year, month, and day values in the 2nd part of
1356        a compound date (range or span).
1357        """
1358        self._assert_compound()
1359        self.__set_yr_mon_day(year, month, day,
1360                              Date._POS_RYR, Date._POS_RMON, Date._POS_RDAY)
1361
1362    def __set_yr_mon_day_offset(self, year, month, day,
1363                                pos_yr, pos_mon, pos_day):
1364        dlist = list(self.dateval)
1365        if dlist[pos_yr]:
1366            dlist[pos_yr] += year
1367        elif year:
1368            dlist[pos_yr] = year
1369        if dlist[pos_mon]:
1370            dlist[pos_mon] += month
1371        elif month:
1372            if month < 0:
1373                dlist[pos_mon] = 1 + month
1374            else:
1375                dlist[pos_mon] = month
1376        # Fix if month out of bounds:
1377        if month != 0: # only check if changed
1378            if dlist[pos_mon] == 0: # subtraction
1379                dlist[pos_mon] = 12
1380                dlist[pos_yr] -= 1
1381            elif dlist[pos_mon] < 0: # subtraction
1382                dlist[pos_yr] -= int((-dlist[pos_mon]) // 12) + 1
1383                dlist[pos_mon] = (dlist[pos_mon] % 12)
1384            elif dlist[pos_mon] > 12 or dlist[pos_mon] < 1:
1385                dlist[pos_yr] += int(dlist[pos_mon] // 12)
1386                dlist[pos_mon] = dlist[pos_mon] % 12
1387        self.dateval = tuple(dlist)
1388        self._calc_sort_value()
1389        return day != 0 or dlist[pos_day] > 28
1390
1391    def set_yr_mon_day_offset(self, year=0, month=0, day=0):
1392        """
1393        Offset the date by the given year, month, and day values.
1394        """
1395        if self.__set_yr_mon_day_offset(year, month, day, Date._POS_YR,
1396                                        Date._POS_MON, Date._POS_DAY):
1397            self.set_yr_mon_day(*self.offset(day), remove_stop_date=False)
1398        if self.is_compound():
1399            self.set2_yr_mon_day_offset(year, month, day)
1400
1401    def set2_yr_mon_day_offset(self, year=0, month=0, day=0):
1402        """
1403        Set the year, month, and day values by offset in the 2nd part
1404        of a compound date (range or span).
1405        """
1406        self._assert_compound()
1407        if self.__set_yr_mon_day_offset(year, month, day, Date._POS_RYR,
1408                                        Date._POS_RMON, Date._POS_RDAY):
1409            stop = Date(self.get_stop_ymd())
1410            self.set2_yr_mon_day(*stop.offset(day))
1411
1412    def copy_offset_ymd(self, year=0, month=0, day=0):
1413        """
1414        Return a Date copy based on year, month, and day offset.
1415        """
1416        orig_cal = self.calendar
1417        if self.calendar != 0:
1418            new_date = self.to_calendar("gregorian")
1419        else:
1420            new_date = self
1421        retval = Date(new_date)
1422        retval.set_yr_mon_day_offset(year, month, day)
1423        if orig_cal == 0:
1424            return retval
1425        else:
1426            retval.convert_calendar(orig_cal)
1427            return retval
1428
1429    def copy_ymd(self, year=0, month=0, day=0, remove_stop_date=None):
1430        """
1431        Return a Date copy with year, month, and day set.
1432
1433        :param remove_stop_date: Same as in set_yr_mon_day.
1434        """
1435        retval = Date(self)
1436        retval.set_yr_mon_day(year, month, day, remove_stop_date)
1437        return retval
1438
1439    def set_year(self, year):
1440        """
1441        Set the year value.
1442        """
1443        self.dateval = self.dateval[0:2] + (year, ) + self.dateval[3:]
1444        self._calc_sort_value()
1445
1446    def get_year_valid(self):
1447        """
1448        Return true if the year is valid.
1449        """
1450        return self._get_low_item_valid(Date._POS_YR)
1451
1452    def get_month(self):
1453        """
1454        Return the month associated with the date.
1455
1456        If the month is not defined, a zero is returned. If the date is a
1457        compound date, the lower date month is returned.
1458        """
1459        return self._get_low_item(Date._POS_MON)
1460
1461    def get_month_valid(self):
1462        """
1463        Return true if the month is valid
1464        """
1465        return self._get_low_item_valid(Date._POS_MON)
1466
1467    def get_day(self):
1468        """
1469        Return the day of the month associated with the date.
1470
1471        If the day is not defined, a zero is returned. If the date is a
1472        compound date, the lower date day is returned.
1473        """
1474        return self._get_low_item(Date._POS_DAY)
1475
1476    def get_day_valid(self):
1477        """
1478        Return true if the day is valid.
1479        """
1480        return self._get_low_item_valid(Date._POS_DAY)
1481
1482    def get_valid(self):
1483        """
1484        Return true if any part of the date is valid.
1485        """
1486        return self.modifier != Date.MOD_TEXTONLY
1487
1488    def is_valid(self):
1489        """
1490        Return true if any part of the date is valid.
1491        """
1492        return self.modifier != Date.MOD_TEXTONLY and self.sortval != 0
1493
1494    def get_stop_year(self):
1495        """
1496        Return the day of the year associated with the second part of a
1497        compound date.
1498
1499        If the year is not defined, a zero is returned.
1500        """
1501        return self._get_high_item(Date._POS_RYR)
1502
1503    def get_stop_month(self):
1504        """
1505        Return the month of the month associated with the second part of a
1506        compound date.
1507
1508        If the month is not defined, a zero is returned.
1509        """
1510        return self._get_high_item(Date._POS_RMON)
1511
1512    def get_stop_day(self):
1513        """
1514        Return the day of the month associated with the second part of a
1515        compound date.
1516
1517        If the day is not defined, a zero is returned.
1518        """
1519        return self._get_high_item(Date._POS_RDAY)
1520
1521    def get_high_year(self):
1522        """
1523        Return the high year estimate.
1524
1525        For compound dates with non-zero stop year, the stop year is returned.
1526        Otherwise, the start year is returned.
1527        """
1528        if self.is_compound():
1529            ret = self.get_stop_year()
1530            if ret:
1531                return ret
1532        else:
1533            return self.get_year()
1534
1535    def get_text(self):
1536        """
1537        Return the text value associated with an invalid date.
1538        """
1539        return self.text
1540
1541    def get_dow(self):
1542        """
1543        Return an integer representing the day of the week associated with the
1544        date (Monday=0).
1545
1546        If the day is not defined, a None is returned. If the date is a
1547        compound date, the lower date day is returned.
1548        """
1549        return self.sortval % 7 if self.is_regular() else None
1550
1551    def _zero_adjust_ymd(self, year, month, day):
1552        year = year if year != 0 else 1
1553        month = max(month, 1)
1554        day = max(day, 1)
1555        return (year, month, day)
1556
1557    def _adjust_newyear(self):
1558        """
1559        Returns year adjustment performed (0 or -1).
1560        """
1561        nyear = self.get_new_year()
1562        year_delta = 0
1563        if nyear: # new year offset?
1564            if nyear == Date.NEWYEAR_MAR1:
1565                split = (3, 1)
1566            elif nyear == Date.NEWYEAR_MAR25:
1567                split = (3, 25)
1568            elif nyear == Date.NEWYEAR_SEP1:
1569                split = (9, 1)
1570            elif isinstance(nyear, (list, tuple)):
1571                split = nyear
1572            else:
1573                split = (0, 0)
1574            if (self.get_month(), self.get_day()) >= split and split != (0, 0):
1575                year_delta = -1
1576                new_date = Date(self.get_year() + year_delta, self.get_month(),
1577                                self.get_day())
1578                new_date.set_calendar(self.calendar)
1579                new_date.recalc_sort_value()
1580                self.sortval = new_date.sortval
1581        return year_delta
1582
1583    def set(self, quality=None, modifier=None, calendar=None,
1584            value=None, text=None, newyear=0):
1585        """
1586        Set the date to the specified value.
1587
1588        :param quality: The date quality for the date (see :meth:`get_quality`
1589                        for more information).
1590                        Defaults to the previous value for the date.
1591        :param modified: The date modifier for the date (see
1592                         :meth:`get_modifier` for more information)
1593                         Defaults to the previous value for the date.
1594        :param calendar: The calendar associated with the date (see
1595                         :meth:`get_calendar` for more information).
1596                         Defaults to the previous value for the date.
1597        :param value: A tuple representing the date information. For a
1598                      non-compound date, the format is (DD, MM, YY, slash)
1599                      and for a compound date the tuple stores data as
1600                      (DD, MM, YY, slash1, DD, MM, YY, slash2)
1601                      Defaults to the previous value for the date.
1602        :param text: A text string holding either the verbatim user input
1603                     or a comment relating to the date.
1604                     Defaults to the previous value for the date.
1605        :param newyear: The newyear code, or tuple representing (month, day)
1606                        of newyear day.
1607                        Defaults to 0.
1608
1609        The sort value is recalculated.
1610        """
1611
1612        if quality is None:
1613            quality = self.quality
1614        if modifier is None:
1615            modifier = self.modifier
1616        if calendar is None:
1617            calendar = self.calendar
1618        if value is None:
1619            value = self.dateval
1620
1621        if modifier in (Date.MOD_NONE, Date.MOD_BEFORE,
1622                        Date.MOD_AFTER, Date.MOD_ABOUT) and len(value) < 4:
1623            raise DateError("Invalid value. Should be: (DD, MM, YY, slash)")
1624        if modifier in (Date.MOD_RANGE, Date.MOD_SPAN) and len(value) < 8:
1625            raise DateError("Invalid value. Should be: (DD, MM, "
1626                            "YY, slash1, DD, MM, YY, slash2)")
1627        if modifier not in (Date.MOD_NONE, Date.MOD_BEFORE, Date.MOD_AFTER,
1628                            Date.MOD_ABOUT, Date.MOD_RANGE, Date.MOD_SPAN,
1629                            Date.MOD_TEXTONLY):
1630            raise DateError("Invalid modifier")
1631        if quality not in (Date.QUAL_NONE, Date.QUAL_ESTIMATED,
1632                           Date.QUAL_CALCULATED):
1633            raise DateError("Invalid quality")
1634        if calendar not in Date.CALENDARS:
1635            raise DateError("Invalid calendar")
1636        if newyear != 0 and calendar_has_fixed_newyear(calendar):
1637            raise DateError(
1638                "May not adjust newyear to {ny} for calendar {cal}".format(
1639                    ny=newyear, cal=calendar))
1640
1641        self.quality = quality
1642        self.modifier = modifier
1643        self.calendar = calendar
1644        self.dateval = value
1645        self.set_new_year(newyear)
1646        year, month, day = self._zero_adjust_ymd(
1647            value[Date._POS_YR],
1648            value[Date._POS_MON],
1649            value[Date._POS_DAY])
1650
1651        if year == month == day == 0:
1652            self.sortval = 0
1653        else:
1654            func = Date._calendar_convert[calendar]
1655            self.sortval = func(year, month, day)
1656
1657        if self.get_slash() and self.get_calendar() != Date.CAL_JULIAN:
1658            self.set_calendar(Date.CAL_JULIAN)
1659            self.recalc_sort_value()
1660
1661        year_delta = self._adjust_newyear()
1662
1663        if text:
1664            self.text = text
1665
1666        if modifier != Date.MOD_TEXTONLY:
1667            sanity = Date(self)
1668            sanity.convert_calendar(self.calendar, known_valid=False)
1669            # convert_calendar resets slash and new year, restore these as needed
1670            if sanity.get_slash() != self.get_slash():
1671                sanity.set_slash(self.get_slash())
1672            if self.is_compound() and sanity.get_slash2() != self.get_slash2():
1673                sanity.set_slash2(self.get_slash2())
1674            if sanity.get_new_year() != self.get_new_year():
1675                sanity.set_new_year(self.get_new_year())
1676                sanity._adjust_newyear()
1677
1678            # We don't do the roundtrip conversion on self, becaue
1679            # it would remove uncertainty on day/month expressed with zeros
1680
1681            # Did the roundtrip change the date value?!
1682            if sanity.dateval != value:
1683                try:
1684                    self.__compare(sanity.dateval, value, year_delta)
1685                except DateError as err:
1686                    LOG.debug("Sanity check failed - self: {}, sanity: {}".
1687                              format(self.__dict__, sanity.__dict__))
1688                    err.date = self
1689                    raise
1690
1691    def __compare(self, sanity, value, year_delta):
1692        ziplist = zip(sanity, value)
1693        # Loop over all values present, whether compound or not
1694        for day, month, year, slash in zip(*[iter(ziplist)]*4):
1695            # each of d,m,y,sl is a pair from dateval and value, to compare
1696            adjusted, original = slash
1697            if adjusted != original:
1698                raise DateError("Invalid date value {}".
1699                                format(value))
1700
1701            for adjusted, original in day, month:
1702                if adjusted != original and not(original == 0 and
1703                                                adjusted == 1):
1704                    raise DateError("Invalid day/month {} passed in value {}".
1705                                    format(original, value))
1706
1707            adjusted, original = year
1708            adjusted -= year_delta
1709            if adjusted != original and not(original == 0 and adjusted == 1):
1710                raise DateError("Invalid year {} passed in value {}".
1711                                format(original, value))
1712
1713    def recalc_sort_value(self):
1714        """
1715        Recalculates the numerical sort value associated with the date
1716        and returns it. Public method.
1717        """
1718        self._calc_sort_value()
1719        return self.sortval
1720
1721    def _calc_sort_value(self):
1722        """
1723        Calculate the numerical sort value associated with the date.
1724        """
1725        year, month, day = self._zero_adjust_ymd(
1726            self.dateval[Date._POS_YR],
1727            self.dateval[Date._POS_MON],
1728            self.dateval[Date._POS_DAY])
1729        if year == month == 0 and day == 0:
1730            self.sortval = 0
1731        else:
1732            func = Date._calendar_convert[self.calendar]
1733            self.sortval = func(year, month, day)
1734
1735    def convert_calendar(self, calendar, known_valid=True):
1736        """
1737        Convert the date from the current calendar to the specified calendar.
1738        """
1739        if (known_valid  # if not known valid, round-trip convert anyway
1740                and calendar == self.calendar
1741                and self.newyear == Date.NEWYEAR_JAN1):
1742            return
1743        (year, month, day) = Date._calendar_change[calendar](self.sortval)
1744        if self.is_compound():
1745            ryear, rmonth, rday = self._zero_adjust_ymd(
1746                self.dateval[Date._POS_RYR],
1747                self.dateval[Date._POS_RMON],
1748                self.dateval[Date._POS_RDAY])
1749            sdn = Date._calendar_convert[self.calendar](ryear, rmonth, rday)
1750            (nyear, nmonth, nday) = Date._calendar_change[calendar](sdn)
1751            self.dateval = (day, month, year, False,
1752                            nday, nmonth, nyear, False)
1753        else:
1754            self.dateval = (day, month, year, False)
1755        self.calendar = calendar
1756        self.newyear = Date.NEWYEAR_JAN1
1757
1758    def set_as_text(self, text):
1759        """
1760        Set the day to a text string, and assign the sort value to zero.
1761        """
1762        self.modifier = Date.MOD_TEXTONLY
1763        self.text = text
1764        self.sortval = 0
1765
1766    def set_text_value(self, text):
1767        """
1768        Set the text string to a given text.
1769        """
1770        self.text = text
1771
1772    def is_empty(self):
1773        """
1774        Return True if the date contains no information (empty text).
1775        """
1776        return not((self.modifier == Date.MOD_TEXTONLY and self.text)
1777               or self.get_start_date() != Date.EMPTY
1778                or self.get_stop_date() != Date.EMPTY)
1779
1780    def is_compound(self):
1781        """
1782        Return True if the date is a date range or a date span.
1783        """
1784        return self.modifier == Date.MOD_RANGE \
1785               or self.modifier == Date.MOD_SPAN
1786
1787    def is_regular(self):
1788        """
1789        Return True if the date is a regular date.
1790
1791        The regular date is a single exact date, i.e. not text-only, not
1792        a range or a span, not estimated/calculated, not about/before/after
1793        date, and having year, month, and day all non-zero.
1794        """
1795        return self.modifier == Date.MOD_NONE \
1796               and self.quality == Date.QUAL_NONE \
1797               and self.get_year_valid() and self.get_month_valid() \
1798               and self.get_day_valid()
1799
1800    def is_full(self):
1801        """
1802        Return True if the date is fully specified.
1803        """
1804        return (self.get_year_valid() and
1805                self.get_month_valid() and
1806                self.get_day_valid())
1807
1808    def get_ymd(self):
1809        """
1810        Return (year, month, day).
1811        """
1812        return (self.get_year(), self.get_month(), self.get_day())
1813
1814    def get_dmy(self, get_slash=False):
1815        """
1816        Return (day, month, year, [slash]).
1817        """
1818        if get_slash:
1819            return (self.get_day(), self.get_month(), self.get_year(),
1820                    self.get_slash())
1821        else:
1822            return (self.get_day(), self.get_month(), self.get_year())
1823
1824    def get_stop_ymd(self):
1825        """
1826        Return (year, month, day) of the stop date, or all-zeros if it's not
1827        defined.
1828        """
1829        return (self.get_stop_year(), self.get_stop_month(),
1830                self.get_stop_day())
1831
1832    def offset(self, value):
1833        """
1834        Return (year, month, day) of this date +- value.
1835        """
1836        return Date._calendar_change[Date.CAL_GREGORIAN](self.sortval + value)
1837
1838    def offset_date(self, value):
1839        """
1840        Return (year, month, day) of this date +- value.
1841        """
1842        return Date(Date._calendar_change[Date.CAL_GREGORIAN](self.sortval +
1843                                                              value))
1844
1845    def lookup_calendar(self, calendar):
1846        """
1847        Lookup calendar name in the list of known calendars, even if translated.
1848        """
1849        return lookup_calendar(calendar)
1850
1851    def lookup_quality(self, quality):
1852        """
1853        Lookup date quality keyword, even if translated.
1854        """
1855        qualities = ["none", "estimated", "calculated"]
1856        ui_qualities = [_("date-quality|none"),
1857                        _("estimated"), _("calculated")]
1858        if quality.lower() in qualities:
1859            return qualities.index(quality.lower())
1860        elif quality.lower() in ui_qualities:
1861            return ui_qualities.index(quality.lower())
1862        else:
1863            raise AttributeError("invalid quality: '%s'" % quality)
1864
1865    def lookup_modifier(self, modifier):
1866        """
1867        Lookup date modifier keyword, even if translated.
1868        """
1869        mods = ["none", "before", "after", "about",
1870                "range", "span", "textonly"]
1871        ui_mods = [_("date-modifier|none"),
1872                   _("before"), _("after"), _("about"),
1873                   _("range"), _("span"), _("textonly")]
1874        if modifier.lower() in mods:
1875            return mods.index(modifier.lower())
1876        elif modifier.lower() in ui_mods:
1877            return ui_mods.index(modifier.lower())
1878        else:
1879            raise AttributeError("invalid modifier: '%s'" % modifier)
1880
1881    def to_calendar(self, calendar_name):
1882        """
1883        Return a new Date object in the calendar calendar_name.
1884
1885        >>> Date(1591, 1, 1).to_calendar("julian")
1886        1590-12-22 (Julian)
1887        """
1888        cal = lookup_calendar(calendar_name)
1889        retval = Date(self)
1890        retval.convert_calendar(cal)
1891        return retval
1892
1893    def get_slash(self):
1894        """
1895        Return true if the date is a slash-date (dual dated).
1896        """
1897        return self._get_low_item_valid(Date._POS_SL)
1898
1899    def set_slash(self, value):
1900        """
1901        Set to 1 if the date is a slash-date (dual dated).
1902        """
1903        temp = list(self.dateval)
1904        temp[Date._POS_SL] = value
1905        self.dateval = tuple(temp)
1906
1907    def get_slash2(self):
1908        """
1909        Return true if the ending date is a slash-date (dual dated).
1910        """
1911        return self._get_low_item_valid(Date._POS_RSL)
1912
1913    def set_slash2(self, value):
1914        """
1915        Set to 1 if the ending date is a slash-date (dual dated).
1916        """
1917        temp = list(self.dateval)
1918        temp[Date._POS_RSL] = value
1919        self.dateval = tuple(temp)
1920
1921    def make_vague(self):
1922        """
1923        Remove month and day details to make the date approximate.
1924        """
1925        dlist = list(self.dateval)
1926        dlist[Date._POS_MON] = 0
1927        dlist[Date._POS_DAY] = 0
1928        if Date._POS_RDAY < len(dlist):
1929            dlist[Date._POS_RDAY] = 0
1930            dlist[Date._POS_RMON] = 0
1931        self.dateval = tuple(dlist)
1932        self._calc_sort_value()
1933
1934    year = property(get_year, set_year)
1935
1936def Today():
1937    """
1938    Returns a Date object set to the current date.
1939    """
1940    import time
1941    current_date = Date()
1942    current_date.set_yr_mon_day(*time.localtime(time.time())[0:3])
1943    return current_date
1944
1945def NextYear():
1946    """
1947    Returns a Date object set to next year
1948    """
1949    return Today() + 1
1950
1951#-------------------------------------------------------------------------
1952#
1953# Date Functions
1954#
1955#-------------------------------------------------------------------------
1956
1957
1958def lookup_calendar(calendar):
1959    """
1960    Find the ID associated with the calendar name.
1961
1962    >>> lookup_calendar("hebrew")
1963    2
1964    """
1965    if calendar is None: return Date.CAL_GREGORIAN
1966    if isinstance(calendar, int): return calendar
1967    for pos, calendar_name in enumerate(Date.calendar_names):
1968        if calendar.lower() == calendar_name.lower():
1969            return pos
1970    for pos, calendar_name in enumerate(Date.ui_calendar_names):
1971        if calendar.lower() == calendar_name.lower():
1972            return pos
1973    raise AttributeError("invalid calendar: '%s'" % calendar)
1974
1975def gregorian(date):
1976    """Convert given date to gregorian. Doesn't modify the original object."""
1977    if date.get_calendar() != Date.CAL_GREGORIAN:
1978        date = Date(date)
1979        date.convert_calendar(Date.CAL_GREGORIAN)
1980    return date
1981
1982def calendar_has_fixed_newyear(cal):
1983    """Does the given calendar have a fixed new year, or may it be reset?"""
1984    return cal not in (Date.CAL_GREGORIAN, Date.CAL_JULIAN, Date.CAL_SWEDISH)
1985