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