Package Gnumed :: Package pycommon :: Module gmDateTime
[frames] | no frames]

Source Code for Module Gnumed.pycommon.gmDateTime

   1  # -*- coding: utf-8 -*- 
   2   
   3  __doc__ = """ 
   4  GNUmed date/time handling. 
   5   
   6  This modules provides access to date/time handling 
   7  and offers an fuzzy timestamp implementation 
   8   
   9  It utilizes 
  10   
  11          - Python time 
  12          - Python datetime 
  13          - mxDateTime 
  14   
  15  Note that if you want locale-aware formatting you need to call 
  16   
  17          locale.setlocale(locale.LC_ALL, '') 
  18   
  19  somewhere before importing this script. 
  20   
  21  Note regarding UTC offsets 
  22  -------------------------- 
  23   
  24  Looking from Greenwich: 
  25          WEST (IOW "behind"): negative values 
  26          EAST (IOW "ahead"):  positive values 
  27   
  28  This is in compliance with what datetime.tzinfo.utcoffset() 
  29  does but NOT what time.altzone/time.timezone do ! 
  30   
  31  This module also implements a class which allows the 
  32  programmer to define the degree of fuzziness, uncertainty 
  33  or imprecision of the timestamp contained within. 
  34   
  35  This is useful in fields such as medicine where only partial 
  36  timestamps may be known for certain events. 
  37   
  38  Other useful links: 
  39   
  40          http://joda-time.sourceforge.net/key_instant.html 
  41  """ 
  42  #=========================================================================== 
  43  __author__ = "K. Hilbert <Karsten.Hilbert@gmx.net>" 
  44  __license__ = "GPL v2 or later (details at http://www.gnu.org)" 
  45   
  46  # stdlib 
  47  import sys, datetime as pyDT, time, os, re as regex, locale, logging 
  48   
  49   
  50  # 3rd party 
  51  #import mx.DateTime as mxDT 
  52   
  53   
  54  if __name__ == '__main__': 
  55          sys.path.insert(0, '../../') 
  56  #from Gnumed.pycommon import gmI18N 
  57   
  58   
  59  _log = logging.getLogger('gm.datetime') 
  60  #_log.info(u'mx.DateTime version: %s', mxDT.__version__) 
  61   
  62  dst_locally_in_use = None 
  63  dst_currently_in_effect = None 
  64   
  65  py_timezone_name = None 
  66  py_dst_timezone_name = None 
  67  current_local_utc_offset_in_seconds = None 
  68  #current_local_timezone_interval = None 
  69  current_local_iso_numeric_timezone_string = None 
  70  current_local_timezone_name = None 
  71   
  72  gmCurrentLocalTimezone = 'gmCurrentLocalTimezone not initialized' 
  73   
  74   
  75  (       acc_years, 
  76          acc_months, 
  77          acc_weeks, 
  78          acc_days, 
  79          acc_hours, 
  80          acc_minutes, 
  81          acc_seconds, 
  82          acc_subseconds 
  83  ) = range(1,9) 
  84   
  85  _accuracy_strings = { 
  86          1: 'years', 
  87          2: 'months', 
  88          3: 'weeks', 
  89          4: 'days', 
  90          5: 'hours', 
  91          6: 'minutes', 
  92          7: 'seconds', 
  93          8: 'subseconds' 
  94  } 
  95   
  96  gregorian_month_length = { 
  97          1: 31, 
  98          2: 28,          # FIXME: make leap year aware 
  99          3: 31, 
 100          4: 30, 
 101          5: 31, 
 102          6: 30, 
 103          7: 31, 
 104          8: 31, 
 105          9: 30, 
 106          10: 31, 
 107          11: 30, 
 108          12: 31 
 109  } 
 110   
 111  avg_days_per_gregorian_year = 365 
 112  avg_days_per_gregorian_month = 30 
 113  avg_seconds_per_day = 24 * 60 * 60 
 114  days_per_week = 7 
 115   
 116  #=========================================================================== 
 117  # module init 
 118  #--------------------------------------------------------------------------- 
119 -def init():
120 121 # _log.debug('mx.DateTime.now(): [%s]' % mxDT.now()) 122 _log.debug('datetime.now() : [%s]' % pyDT.datetime.now()) 123 _log.debug('time.localtime() : [%s]' % str(time.localtime())) 124 _log.debug('time.gmtime() : [%s]' % str(time.gmtime())) 125 126 try: 127 _log.debug('$TZ: [%s]' % os.environ['TZ']) 128 except KeyError: 129 _log.debug('$TZ not defined') 130 131 _log.debug('time.daylight : [%s] (whether or not DST is locally used at all)', time.daylight) 132 _log.debug('time.timezone : [%s] seconds (+/-: WEST/EAST of Greenwich)', time.timezone) 133 _log.debug('time.altzone : [%s] seconds (+/-: WEST/EAST of Greenwich)', time.altzone) 134 _log.debug('time.tzname : [%s / %s] (non-DST / DST)' % time.tzname) 135 _log.debug('time.localtime.tm_zone : [%s]', time.localtime().tm_zone) 136 _log.debug('time.localtime.tm_gmtoff: [%s]', time.localtime().tm_gmtoff) 137 # _log.debug('mx.DateTime.now().gmtoffset(): [%s]' % mxDT.now().gmtoffset()) 138 139 global py_timezone_name 140 py_timezone_name = time.tzname[0] 141 142 global py_dst_timezone_name 143 py_dst_timezone_name = time.tzname[1] 144 145 global dst_locally_in_use 146 dst_locally_in_use = (time.daylight != 0) 147 148 global dst_currently_in_effect 149 dst_currently_in_effect = bool(time.localtime()[8]) 150 _log.debug('DST currently in effect: [%s]' % dst_currently_in_effect) 151 152 if (not dst_locally_in_use) and dst_currently_in_effect: 153 _log.error('system inconsistency: DST not in use - but DST currently in effect ?') 154 155 global current_local_utc_offset_in_seconds 156 msg = 'DST currently%sin effect: using UTC offset of [%s] seconds instead of [%s] seconds' 157 if dst_currently_in_effect: 158 current_local_utc_offset_in_seconds = time.altzone * -1 159 _log.debug(msg % (' ', time.altzone * -1, time.timezone * -1)) 160 else: 161 current_local_utc_offset_in_seconds = time.timezone * -1 162 _log.debug(msg % (' not ', time.timezone * -1, time.altzone * -1)) 163 164 if current_local_utc_offset_in_seconds < 0: 165 _log.debug('UTC offset is negative, assuming WEST of Greenwich (clock is "behind")') 166 elif current_local_utc_offset_in_seconds > 0: 167 _log.debug('UTC offset is positive, assuming EAST of Greenwich (clock is "ahead")') 168 else: 169 _log.debug('UTC offset is ZERO, assuming Greenwich Time') 170 171 # global current_local_timezone_interval 172 # current_local_timezone_interval = mxDT.now().gmtoffset() 173 # _log.debug('ISO timezone: [%s] (taken from mx.DateTime.now().gmtoffset())' % current_local_timezone_interval) 174 175 global current_local_iso_numeric_timezone_string 176 # current_local_iso_numeric_timezone_string = str(current_local_timezone_interval).replace(',', '.') 177 current_local_iso_numeric_timezone_string = '%s' % current_local_utc_offset_in_seconds 178 _log.debug('ISO numeric timezone string: [%s]' % current_local_iso_numeric_timezone_string) 179 180 global current_local_timezone_name 181 try: 182 current_local_timezone_name = os.environ['TZ'] 183 except KeyError: 184 if dst_currently_in_effect: 185 current_local_timezone_name = time.tzname[1] 186 else: 187 current_local_timezone_name = time.tzname[0] 188 189 global gmCurrentLocalTimezone 190 gmCurrentLocalTimezone = cPlatformLocalTimezone() 191 _log.debug('local-timezone class: %s', cPlatformLocalTimezone) 192 _log.debug('local-timezone instance: %s', gmCurrentLocalTimezone)
193 # _log.debug('') 194 # print (" (total) UTC offset:", gmCurrentLocalTimezone.utcoffset(pyDT.datetime.now())) 195 # print (" DST adjustment:", gmCurrentLocalTimezone.dst(pyDT.datetime.now())) 196 # print (" timezone name:", gmCurrentLocalTimezone.tzname(pyDT.datetime.now())) 197 198 #=========================================================================== 199 # local timezone implementation (lifted from the docs) 200 # 201 # A class capturing the platform's idea of local time. 202 # (May result in wrong values on historical times in 203 # timezones where UTC offset and/or the DST rules had 204 # changed in the past.) 205 #---------------------------------------------------------------------------
206 -class cPlatformLocalTimezone(pyDT.tzinfo):
207 208 #-----------------------------------------------------------------------
209 - def __init__(self):
210 self._SECOND = pyDT.timedelta(seconds = 1) 211 self._nonDST_OFFSET_FROM_UTC = pyDT.timedelta(seconds = -time.timezone) 212 if time.daylight: 213 self._DST_OFFSET_FROM_UTC = pyDT.timedelta(seconds = -time.altzone) 214 else: 215 self._DST_OFFSET_FROM_UTC = self._nonDST_OFFSET_FROM_UTC 216 self._DST_SHIFT = self._DST_OFFSET_FROM_UTC - self._nonDST_OFFSET_FROM_UTC 217 _log.debug('[%s]: UTC->non-DST offset [%s], UTC->DST offset [%s], DST shift [%s]', self.__class__.__name__, self._nonDST_OFFSET_FROM_UTC, self._DST_OFFSET_FROM_UTC, self._DST_SHIFT)
218 219 #-----------------------------------------------------------------------
220 - def fromutc(self, dt):
221 assert dt.tzinfo is self 222 stamp = (dt - pyDT.datetime(1970, 1, 1, tzinfo = self)) // self._SECOND 223 args = time.localtime(stamp)[:6] 224 dst_diff = self._DST_SHIFT // self._SECOND 225 # Detect fold 226 fold = (args == time.localtime(stamp - dst_diff)) 227 return pyDT.datetime(*args, microsecond = dt.microsecond, tzinfo = self, fold = fold)
228 229 #-----------------------------------------------------------------------
230 - def utcoffset(self, dt):
231 if self._isdst(dt): 232 return self._DST_OFFSET_FROM_UTC 233 return self._nonDST_OFFSET_FROM_UTC
234 235 #-----------------------------------------------------------------------
236 - def dst(self, dt):
237 if self._isdst(dt): 238 return self._DST_SHIFT 239 return pyDT.timedelta(0)
240 241 #-----------------------------------------------------------------------
242 - def tzname(self, dt):
243 return time.tzname[self._isdst(dt)]
244 245 #-----------------------------------------------------------------------
246 - def _isdst(self, dt):
247 tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.weekday(), 0, 0) 248 try: 249 stamp = time.mktime(tt) 250 except (OverflowError, ValueError): 251 _log.exception('overflow in time.mktime(%s)', tt) 252 return False 253 254 tt = time.localtime(stamp) 255 return tt.tm_isdst > 0
256 257 #=========================================================================== 258 # convenience functions 259 #---------------------------------------------------------------------------
260 -def get_next_month(dt):
261 next_month = dt.month + 1 262 if next_month == 13: 263 return 1 264 return next_month
265 266 #---------------------------------------------------------------------------
267 -def get_last_month(dt):
268 last_month = dt.month - 1 269 if last_month == 0: 270 return 12 271 return last_month
272 273 #---------------------------------------------------------------------------
274 -def get_date_of_weekday_in_week_of_date(weekday, base_dt=None):
275 # weekday: 276 # 0 = Sunday 277 # 1 = Monday ... 278 if weekday not in [0,1,2,3,4,5,6,7]: 279 raise ValueError('weekday must be in 0 (Sunday) to 7 (Sunday, again)') 280 if base_dt is None: 281 base_dt = pydt_now_here() 282 dt_weekday = base_dt.isoweekday() # 1 = Mon 283 day_diff = dt_weekday - weekday 284 days2add = (-1 * day_diff) 285 return pydt_add(base_dt, days = days2add)
286 287 #---------------------------------------------------------------------------
288 -def get_date_of_weekday_following_date(weekday, base_dt=None):
289 # weekday: 290 # 0 = Sunday # will be wrapped to 7 291 # 1 = Monday ... 292 if weekday not in [0,1,2,3,4,5,6,7]: 293 raise ValueError('weekday must be in 0 (Sunday) to 7 (Sunday, again)') 294 if weekday == 0: 295 weekday = 7 296 if base_dt is None: 297 base_dt = pydt_now_here() 298 dt_weekday = base_dt.isoweekday() # 1 = Mon 299 days2add = weekday - dt_weekday 300 if days2add == 0: 301 days2add = 7 302 elif days2add < 0: 303 days2add += 7 304 return pydt_add(base_dt, days = days2add)
305 306 #=========================================================================== 307 # mxDateTime conversions 308 #---------------------------------------------------------------------------
309 -def mxdt2py_dt(mxDateTime):
310 311 if isinstance(mxDateTime, pyDT.datetime): 312 return mxDateTime 313 314 try: 315 tz_name = str(mxDateTime.gmtoffset()).replace(',', '.') 316 except mxDT.Error: 317 _log.debug('mx.DateTime cannot gmtoffset() this timestamp, assuming local time') 318 #tz_name = current_local_iso_numeric_timezone_string 319 tz_name = current_local_timezone_name 320 321 if dst_currently_in_effect: 322 # convert 323 tz = cFixedOffsetTimezone ( 324 offset = ((time.altzone * -1) // 60), 325 name = tz_name 326 ) 327 else: 328 # convert 329 tz = cFixedOffsetTimezone ( 330 offset = ((time.timezone * -1) // 60), 331 name = tz_name 332 ) 333 334 try: 335 return pyDT.datetime ( 336 year = mxDateTime.year, 337 month = mxDateTime.month, 338 day = mxDateTime.day, 339 tzinfo = tz 340 ) 341 except Exception: 342 _log.debug ('error converting mx.DateTime.DateTime to Python: %s-%s-%s %s:%s %s.%s', 343 mxDateTime.year, 344 mxDateTime.month, 345 mxDateTime.day, 346 mxDateTime.hour, 347 mxDateTime.minute, 348 mxDateTime.second, 349 mxDateTime.tz 350 ) 351 raise
352 353 #===========================================================================
354 -def format_dob(dob, format='%Y %b %d', none_string=None, dob_is_estimated=False):
355 if dob is None: 356 if none_string is None: 357 return _('** DOB unknown **') 358 return none_string 359 360 dob_txt = pydt_strftime(dob, format = format, accuracy = acc_days) 361 if dob_is_estimated: 362 return '%s%s' % ('\u2248', dob_txt) 363 364 return dob_txt
365 366 #---------------------------------------------------------------------------
367 -def pydt_strftime(dt=None, format='%Y %b %d %H:%M.%S', accuracy=None, none_str=None):
368 369 if dt is None: 370 if none_str is not None: 371 return none_str 372 raise ValueError('must provide <none_str> if <dt>=None is to be dealt with') 373 374 try: 375 return dt.strftime(format) 376 except ValueError: 377 _log.exception() 378 return 'strftime() error'
379 #_log.exception('Python cannot strftime() this <datetime>, trying ourselves') 380 381 # if isinstance(dt, pyDT.date): 382 # accuracy = acc_days 383 # 384 # if accuracy == acc_days: 385 # return '%04d-%02d-%02d' % ( 386 # dt.year, 387 # dt.month, 388 # dt.day 389 # ) 390 # 391 # if accuracy == acc_minutes: 392 # return '%04d-%02d-%02d %02d:%02d' % ( 393 # dt.year, 394 # dt.month, 395 # dt.day, 396 # dt.hour, 397 # dt.minute 398 # ) 399 # 400 # return '%04d-%02d-%02d %02d:%02d:%02d' % ( 401 # dt.year, 402 # dt.month, 403 # dt.day, 404 # dt.hour, 405 # dt.minute, 406 # dt.second 407 # ) 408 409 #---------------------------------------------------------------------------
410 -def pydt_add(dt, years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0, milliseconds=0, microseconds=0):
411 if months > 11 or months < -11: 412 raise ValueError('pydt_add(): months must be within [-11..11]') 413 414 dt = dt + pyDT.timedelta ( 415 weeks = weeks, 416 days = days, 417 hours = hours, 418 minutes = minutes, 419 seconds = seconds, 420 milliseconds = milliseconds, 421 microseconds = microseconds 422 ) 423 if (years == 0) and (months == 0): 424 return dt 425 target_year = dt.year + years 426 target_month = dt.month + months 427 if target_month > 12: 428 target_year += 1 429 target_month -= 12 430 elif target_month < 1: 431 target_year -= 1 432 target_month += 12 433 return pydt_replace(dt, year = target_year, month = target_month, strict = False)
434 435 #---------------------------------------------------------------------------
436 -def pydt_replace(dt, strict=True, year=None, month=None, day=None, hour=None, minute=None, second=None, microsecond=None, tzinfo=None):
437 # normalization required because .replace() does not 438 # deal with keyword arguments being None ... 439 if year is None: 440 year = dt.year 441 if month is None: 442 month = dt.month 443 if day is None: 444 day = dt.day 445 if hour is None: 446 hour = dt.hour 447 if minute is None: 448 minute = dt.minute 449 if second is None: 450 second = dt.second 451 if microsecond is None: 452 microsecond = dt.microsecond 453 if tzinfo is None: 454 tzinfo = dt.tzinfo # can fail on naive dt's 455 456 if strict: 457 return dt.replace(year = year, month = month, day = day, hour = hour, minute = minute, second = second, microsecond = microsecond, tzinfo = tzinfo) 458 459 try: 460 return dt.replace(year = year, month = month, day = day, hour = hour, minute = minute, second = second, microsecond = microsecond, tzinfo = tzinfo) 461 except ValueError: 462 _log.debug('error replacing datetime member(s): %s', locals()) 463 464 # (target/existing) day did not exist in target month (which raised the exception) 465 if month == 2: 466 if day > 28: 467 if is_leap_year(year): 468 day = 29 469 else: 470 day = 28 471 else: 472 if day == 31: 473 day = 30 474 475 return dt.replace(year = year, month = month, day = day, hour = hour, minute = minute, second = second, microsecond = microsecond, tzinfo = tzinfo)
476 477 #---------------------------------------------------------------------------
478 -def pydt_is_today(dt):
479 now = pyDT.datetime.now(gmCurrentLocalTimezone) 480 if dt.day != now.day: 481 return False 482 if dt.month != now.month: 483 return False 484 if dt.year != now.year: 485 return False 486 return True
487 488 #---------------------------------------------------------------------------
489 -def pydt_now_here():
490 """Returns NOW @ HERE (IOW, in the local timezone.""" 491 return pyDT.datetime.now(gmCurrentLocalTimezone)
492 493 #---------------------------------------------------------------------------
494 -def pydt_max_here():
495 return pyDT.datetime.max.replace(tzinfo = gmCurrentLocalTimezone)
496 497 #=========================================================================== 498 # wxPython conversions 499 #---------------------------------------------------------------------------
500 -def wxDate2py_dt(wxDate=None):
501 if not wxDate.IsValid(): 502 raise ValueError ('invalid wxDate: %s-%s-%s %s:%s %s.%s', 503 wxDate.GetYear(), 504 wxDate.GetMonth(), 505 wxDate.GetDay(), 506 wxDate.GetHour(), 507 wxDate.GetMinute(), 508 wxDate.GetSecond(), 509 wxDate.GetMillisecond() 510 ) 511 512 try: 513 return pyDT.datetime ( 514 year = wxDate.GetYear(), 515 month = wxDate.GetMonth() + 1, 516 day = wxDate.GetDay(), 517 tzinfo = gmCurrentLocalTimezone 518 ) 519 except Exception: 520 _log.debug ('error converting wxDateTime to Python: %s-%s-%s %s:%s %s.%s', 521 wxDate.GetYear(), 522 wxDate.GetMonth(), 523 wxDate.GetDay(), 524 wxDate.GetHour(), 525 wxDate.GetMinute(), 526 wxDate.GetSecond(), 527 wxDate.GetMillisecond() 528 ) 529 raise
530 531 #=========================================================================== 532 # interval related 533 #---------------------------------------------------------------------------
534 -def format_interval(interval=None, accuracy_wanted=None, none_string=None, verbose=False):
535 536 if accuracy_wanted is None: 537 accuracy_wanted = acc_seconds 538 539 if interval is None: 540 if none_string is not None: 541 return none_string 542 543 years, days = divmod(interval.days, avg_days_per_gregorian_year) 544 months, days = divmod(days, avg_days_per_gregorian_month) 545 weeks, days = divmod(days, days_per_week) 546 days, secs = divmod((days * avg_seconds_per_day) + interval.seconds, avg_seconds_per_day) 547 hours, secs = divmod(secs, 3600) 548 mins, secs = divmod(secs, 60) 549 550 tmp = '' 551 552 if years > 0: 553 if verbose: 554 if years > 1: 555 tag = ' ' + _('years') 556 else: 557 tag = ' ' + _('year') 558 else: 559 tag = _('interval_format_tag::years::y')[-1:] 560 tmp += '%s%s' % (int(years), tag) 561 562 if accuracy_wanted < acc_months: 563 if tmp == '': 564 if verbose: 565 return _('0 years') 566 return '0%s' % _('interval_format_tag::years::y')[-1:] 567 return tmp.strip() 568 569 if months > 0: 570 if verbose: 571 if months > 1: 572 tag = ' ' + _('months') 573 else: 574 tag = ' ' + _('month') 575 else: 576 tag = _('interval_format_tag::months::m')[-1:] 577 tmp += ' %s%s' % (int(months), tag) 578 579 if accuracy_wanted < acc_weeks: 580 if tmp == '': 581 if verbose: 582 return _('0 months') 583 return '0%s' % _('interval_format_tag::months::m')[-1:] 584 return tmp.strip() 585 586 if weeks > 0: 587 if verbose: 588 if weeks > 1: 589 tag = ' ' + _('weeks') 590 else: 591 tag = ' ' + _('week') 592 else: 593 tag = _('interval_format_tag::weeks::w')[-1:] 594 tmp += ' %s%s' % (int(weeks), tag) 595 596 if accuracy_wanted < acc_days: 597 if tmp == '': 598 if verbose: 599 return _('0 weeks') 600 return '0%s' % _('interval_format_tag::weeks::w')[-1:] 601 return tmp.strip() 602 603 if days > 0: 604 if verbose: 605 if days > 1: 606 tag = ' ' + _('days') 607 else: 608 tag = ' ' + _('day') 609 else: 610 tag = _('interval_format_tag::days::d')[-1:] 611 tmp += ' %s%s' % (int(days), tag) 612 613 if accuracy_wanted < acc_hours: 614 if tmp == '': 615 if verbose: 616 return _('0 days') 617 return '0%s' % _('interval_format_tag::days::d')[-1:] 618 return tmp.strip() 619 620 if hours > 0: 621 if verbose: 622 if hours > 1: 623 tag = ' ' + _('hours') 624 else: 625 tag = ' ' + _('hour') 626 else: 627 tag = '/24' 628 tmp += ' %s%s' % (int(hours), tag) 629 630 if accuracy_wanted < acc_minutes: 631 if tmp == '': 632 if verbose: 633 return _('0 hours') 634 return '0/24' 635 return tmp.strip() 636 637 if mins > 0: 638 if verbose: 639 if mins > 1: 640 tag = ' ' + _('minutes') 641 else: 642 tag = ' ' + _('minute') 643 else: 644 tag = '/60' 645 tmp += ' %s%s' % (int(mins), tag) 646 647 if accuracy_wanted < acc_seconds: 648 if tmp == '': 649 if verbose: 650 return _('0 minutes') 651 return '0/60' 652 return tmp.strip() 653 654 if secs > 0: 655 if verbose: 656 if secs > 1: 657 tag = ' ' + _('seconds') 658 else: 659 tag = ' ' + _('second') 660 else: 661 tag = 's' 662 tmp += ' %s%s' % (int(secs), tag) 663 664 if tmp == '': 665 if verbose: 666 return _('0 seconds') 667 return '0s' 668 669 return tmp.strip()
670 671 #---------------------------------------------------------------------------
672 -def format_interval_medically(interval=None):
673 """Formats an interval. 674 675 This isn't mathematically correct but close enough for display. 676 """ 677 # more than 1 year ? 678 if interval.days > 363: 679 years, days = divmod(interval.days, 364) 680 leap_days, tmp = divmod(years, 4) 681 months, day = divmod((days + leap_days), 30.33) 682 if int(months) == 0: 683 return "%s%s" % (int(years), _('interval_format_tag::years::y')[-1:]) 684 return "%s%s %s%s" % (int(years), _('interval_format_tag::years::y')[-1:], int(months), _('interval_format_tag::months::m')[-1:]) 685 686 # more than 30 days / 1 month ? 687 if interval.days > 30: 688 months, days = divmod(interval.days, 30.33) 689 weeks, days = divmod(days, 7) 690 if int(weeks + days) == 0: 691 result = '%smo' % int(months) 692 else: 693 result = '%s%s' % (int(months), _('interval_format_tag::months::m')[-1:]) 694 if int(weeks) != 0: 695 result += ' %s%s' % (int(weeks), _('interval_format_tag::weeks::w')[-1:]) 696 if int(days) != 0: 697 result += ' %s%s' % (int(days), _('interval_format_tag::days::d')[-1:]) 698 return result 699 700 # between 7 and 30 days ? 701 if interval.days > 7: 702 return "%s%s" % (interval.days, _('interval_format_tag::days::d')[-1:]) 703 704 # between 1 and 7 days ? 705 if interval.days > 0: 706 hours, seconds = divmod(interval.seconds, 3600) 707 if hours == 0: 708 return '%s%s' % (interval.days, _('interval_format_tag::days::d')[-1:]) 709 return "%s%s (%sh)" % (interval.days, _('interval_format_tag::days::d')[-1:], int(hours)) 710 711 # between 5 hours and 1 day 712 if interval.seconds > (5*3600): 713 return "%sh" % int(interval.seconds // 3600) 714 715 # between 1 and 5 hours 716 if interval.seconds > 3600: 717 hours, seconds = divmod(interval.seconds, 3600) 718 minutes = seconds // 60 719 if minutes == 0: 720 return '%sh' % int(hours) 721 return "%s:%02d" % (int(hours), int(minutes)) 722 723 # minutes only 724 if interval.seconds > (5*60): 725 return "0:%02d" % (int(interval.seconds // 60)) 726 727 # seconds 728 minutes, seconds = divmod(interval.seconds, 60) 729 if minutes == 0: 730 return '%ss' % int(seconds) 731 if seconds == 0: 732 return '0:%02d' % int(minutes) 733 return "%s.%ss" % (int(minutes), int(seconds))
734 735 #---------------------------------------------------------------------------
736 -def format_pregnancy_weeks(age):
737 weeks, days = divmod(age.days, 7) 738 return '%s%s%s%s' % ( 739 int(weeks), 740 _('interval_format_tag::weeks::w')[-1:], 741 interval.days, 742 _('interval_format_tag::days::d')[-1:] 743 )
744 745 #---------------------------------------------------------------------------
746 -def format_pregnancy_months(age):
747 months, remainder = divmod(age.days, 28) 748 return '%s%s' % ( 749 int(months) + 1, 750 _('interval_format_tag::months::m')[-1:] 751 )
752 753 #---------------------------------------------------------------------------
754 -def is_leap_year(year):
755 # year is multiple of 4 ? 756 div, remainder = divmod(year, 4) 757 # no -> not a leap year 758 if remainder > 0: 759 return False 760 761 # year is a multiple of 100 ? 762 div, remainder = divmod(year, 100) 763 # no -> IS a leap year 764 if remainder > 0: 765 return True 766 767 # year is a multiple of 400 ? 768 div, remainder = divmod(year, 400) 769 # yes -> IS a leap year 770 if remainder == 0: 771 return True 772 773 return False
774 775 #---------------------------------------------------------------------------
776 -def calculate_apparent_age(start=None, end=None):
777 """The result of this is a tuple (years, ..., seconds) as one would 778 'expect' an age to look like, that is, simple differences between 779 the fields: 780 781 (years, months, days, hours, minutes, seconds) 782 783 This does not take into account time zones which may 784 shift the result by one day. 785 786 <start> and <end> must by python datetime instances 787 <end> is assumed to be "now" if not given 788 """ 789 if end is None: 790 end = pyDT.datetime.now(gmCurrentLocalTimezone) 791 792 if end < start: 793 raise ValueError('calculate_apparent_age(): <end> (%s) before <start> (%s)' % (end, start)) 794 795 if end == start: 796 return (0, 0, 0, 0, 0, 0) 797 798 # steer clear of leap years 799 if end.month == 2: 800 if end.day == 29: 801 if not is_leap_year(start.year): 802 end = end.replace(day = 28) 803 804 # years 805 years = end.year - start.year 806 end = end.replace(year = start.year) 807 if end < start: 808 years = years - 1 809 810 # months 811 if end.month == start.month: 812 if end < start: 813 months = 11 814 else: 815 months = 0 816 else: 817 months = end.month - start.month 818 if months < 0: 819 months = months + 12 820 if end.day > gregorian_month_length[start.month]: 821 end = end.replace(month = start.month, day = gregorian_month_length[start.month]) 822 else: 823 end = end.replace(month = start.month) 824 if end < start: 825 months = months - 1 826 827 # days 828 if end.day == start.day: 829 if end < start: 830 days = gregorian_month_length[start.month] - 1 831 else: 832 days = 0 833 else: 834 days = end.day - start.day 835 if days < 0: 836 days = days + gregorian_month_length[start.month] 837 end = end.replace(day = start.day) 838 if end < start: 839 days = days - 1 840 841 # hours 842 if end.hour == start.hour: 843 hours = 0 844 else: 845 hours = end.hour - start.hour 846 if hours < 0: 847 hours = hours + 24 848 end = end.replace(hour = start.hour) 849 if end < start: 850 hours = hours - 1 851 852 # minutes 853 if end.minute == start.minute: 854 minutes = 0 855 else: 856 minutes = end.minute - start.minute 857 if minutes < 0: 858 minutes = minutes + 60 859 end = end.replace(minute = start.minute) 860 if end < start: 861 minutes = minutes - 1 862 863 # seconds 864 if end.second == start.second: 865 seconds = 0 866 else: 867 seconds = end.second - start.second 868 if seconds < 0: 869 seconds = seconds + 60 870 end = end.replace(second = start.second) 871 if end < start: 872 seconds = seconds - 1 873 874 return (years, months, days, hours, minutes, seconds)
875 876 #---------------------------------------------------------------------------
877 -def format_apparent_age_medically(age=None):
878 """<age> must be a tuple as created by calculate_apparent_age()""" 879 880 (years, months, days, hours, minutes, seconds) = age 881 882 # at least 1 year ? 883 if years > 0: 884 if months == 0: 885 return '%s%s' % ( 886 years, 887 _('y::year_abbreviation').replace('::year_abbreviation', '') 888 ) 889 return '%s%s %s%s' % ( 890 years, 891 _('y::year_abbreviation').replace('::year_abbreviation', ''), 892 months, 893 _('m::month_abbreviation').replace('::month_abbreviation', '') 894 ) 895 896 # at least 1 month ? 897 if months > 0: 898 if days == 0: 899 return '%s%s' % ( 900 months, 901 _('mo::month_only_abbreviation').replace('::month_only_abbreviation', '') 902 ) 903 904 result = '%s%s' % ( 905 months, 906 _('m::month_abbreviation').replace('::month_abbreviation', '') 907 ) 908 909 weeks, days = divmod(days, 7) 910 if int(weeks) != 0: 911 result += '%s%s' % ( 912 int(weeks), 913 _('w::week_abbreviation').replace('::week_abbreviation', '') 914 ) 915 if int(days) != 0: 916 result += '%s%s' % ( 917 int(days), 918 _('d::day_abbreviation').replace('::day_abbreviation', '') 919 ) 920 921 return result 922 923 # between 7 days and 1 month 924 if days > 7: 925 return "%s%s" % ( 926 days, 927 _('d::day_abbreviation').replace('::day_abbreviation', '') 928 ) 929 930 # between 1 and 7 days ? 931 if days > 0: 932 if hours == 0: 933 return '%s%s' % ( 934 days, 935 _('d::day_abbreviation').replace('::day_abbreviation', '') 936 ) 937 return '%s%s (%s%s)' % ( 938 days, 939 _('d::day_abbreviation').replace('::day_abbreviation', ''), 940 hours, 941 _('h::hour_abbreviation').replace('::hour_abbreviation', '') 942 ) 943 944 # between 5 hours and 1 day 945 if hours > 5: 946 return '%s%s' % ( 947 hours, 948 _('h::hour_abbreviation').replace('::hour_abbreviation', '') 949 ) 950 951 # between 1 and 5 hours 952 if hours > 1: 953 if minutes == 0: 954 return '%s%s' % ( 955 hours, 956 _('h::hour_abbreviation').replace('::hour_abbreviation', '') 957 ) 958 return '%s:%02d' % ( 959 hours, 960 minutes 961 ) 962 963 # between 5 and 60 minutes 964 if minutes > 5: 965 return "0:%02d" % minutes 966 967 # less than 5 minutes 968 if minutes == 0: 969 return '%s%s' % ( 970 seconds, 971 _('s::second_abbreviation').replace('::second_abbreviation', '') 972 ) 973 if seconds == 0: 974 return "0:%02d" % minutes 975 return "%s.%s%s" % ( 976 minutes, 977 seconds, 978 _('s::second_abbreviation').replace('::second_abbreviation', '') 979 )
980 #---------------------------------------------------------------------------
981 -def str2interval(str_interval=None):
982 983 unit_keys = { 984 'year': _('yYaA_keys_year'), 985 'month': _('mM_keys_month'), 986 'week': _('wW_keys_week'), 987 'day': _('dD_keys_day'), 988 'hour': _('hH_keys_hour') 989 } 990 991 str_interval = str_interval.strip() 992 993 # "(~)35(yY)" - at age 35 years 994 keys = '|'.join(list(unit_keys['year'].replace('_keys_year', ''))) 995 if regex.match('^~*(\s|\t)*\d+(%s)*$' % keys, str_interval, flags = regex.UNICODE): 996 return pyDT.timedelta(days = (int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0]) * avg_days_per_gregorian_year)) 997 998 # "(~)12mM" - at age 12 months 999 keys = '|'.join(list(unit_keys['month'].replace('_keys_month', ''))) 1000 if regex.match('^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE): 1001 years, months = divmod ( 1002 int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0]), 1003 12 1004 ) 1005 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month))) 1006 1007 # weeks 1008 keys = '|'.join(list(unit_keys['week'].replace('_keys_week', ''))) 1009 if regex.match('^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE): 1010 return pyDT.timedelta(weeks = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0])) 1011 1012 # days 1013 keys = '|'.join(list(unit_keys['day'].replace('_keys_day', ''))) 1014 if regex.match('^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE): 1015 return pyDT.timedelta(days = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0])) 1016 1017 # hours 1018 keys = '|'.join(list(unit_keys['hour'].replace('_keys_hour', ''))) 1019 if regex.match('^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE): 1020 return pyDT.timedelta(hours = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0])) 1021 1022 # x/12 - months 1023 if regex.match('^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*12$', str_interval, flags = regex.UNICODE): 1024 years, months = divmod ( 1025 int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0]), 1026 12 1027 ) 1028 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month))) 1029 1030 # x/52 - weeks 1031 if regex.match('^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*52$', str_interval, flags = regex.UNICODE): 1032 return pyDT.timedelta(weeks = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0])) 1033 1034 # x/7 - days 1035 if regex.match('^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*7$', str_interval, flags = regex.UNICODE): 1036 return pyDT.timedelta(days = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0])) 1037 1038 # x/24 - hours 1039 if regex.match('^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*24$', str_interval, flags = regex.UNICODE): 1040 return pyDT.timedelta(hours = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0])) 1041 1042 # x/60 - minutes 1043 if regex.match('^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*60$', str_interval, flags = regex.UNICODE): 1044 return pyDT.timedelta(minutes = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0])) 1045 1046 # nYnM - years, months 1047 keys_year = '|'.join(list(unit_keys['year'].replace('_keys_year', ''))) 1048 keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', ''))) 1049 if regex.match('^~*(\s|\t)*\d+(%s|\s|\t)+\d+(\s|\t)*(%s)+$' % (keys_year, keys_month), str_interval, flags = regex.UNICODE): 1050 parts = regex.findall('\d+', str_interval, flags = regex.UNICODE) 1051 years, months = divmod(int(parts[1]), 12) 1052 years += int(parts[0]) 1053 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month))) 1054 1055 # nMnW - months, weeks 1056 keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', ''))) 1057 keys_week = '|'.join(list(unit_keys['week'].replace('_keys_week', ''))) 1058 if regex.match('^~*(\s|\t)*\d+(%s|\s|\t)+\d+(\s|\t)*(%s)+$' % (keys_month, keys_week), str_interval, flags = regex.UNICODE): 1059 parts = regex.findall('\d+', str_interval, flags = regex.UNICODE) 1060 months, weeks = divmod(int(parts[1]), 4) 1061 months += int(parts[0]) 1062 return pyDT.timedelta(days = ((months * avg_days_per_gregorian_month) + (weeks * days_per_week))) 1063 1064 return None
1065 1066 #=========================================================================== 1067 # string -> python datetime parser 1068 #---------------------------------------------------------------------------
1069 -def __single_char2py_dt(str2parse, trigger_chars=None):
1070 """This matches on single characters. 1071 1072 Spaces and tabs are discarded. 1073 1074 Default is 'ndmy': 1075 n - _N_ow 1076 d - to_D_ay 1077 m - to_M_orrow Someone please suggest a synonym ! ("2" does not cut it ...) 1078 y - _Y_esterday 1079 1080 This also defines the significance of the order of the characters. 1081 """ 1082 str2parse = str2parse.strip().lower() 1083 if len(str2parse) != 1: 1084 return [] 1085 1086 if trigger_chars is None: 1087 trigger_chars = _('ndmy (single character date triggers)')[:4].lower() 1088 1089 if str2parse not in trigger_chars: 1090 return [] 1091 1092 now = pydt_now_here() 1093 1094 # FIXME: handle uebermorgen/vorgestern ? 1095 1096 # right now 1097 if str2parse == trigger_chars[0]: 1098 return [{ 1099 'data': now, 1100 'label': _('right now (%s, %s)') % (now.strftime('%A'), now) 1101 }] 1102 # today 1103 if str2parse == trigger_chars[1]: 1104 return [{ 1105 'data': now, 1106 'label': _('today (%s)') % now.strftime('%A, %Y-%m-%d') 1107 }] 1108 # tomorrow 1109 if str2parse == trigger_chars[2]: 1110 ts = pydt_add(now, days = 1) 1111 return [{ 1112 'data': ts, 1113 'label': _('tomorrow (%s)') % ts.strftime('%A, %Y-%m-%d') 1114 }] 1115 # yesterday 1116 if str2parse == trigger_chars[3]: 1117 ts = pydt_add(now, days = -1) 1118 return [{ 1119 'data': ts, 1120 'label': _('yesterday (%s)') % ts.strftime('%A, %Y-%m-%d') 1121 }] 1122 return []
1123 1124 #---------------------------------------------------------------------------
1125 -def __single_dot2py_dt(str2parse):
1126 """Expand fragments containing a single dot. 1127 1128 Standard colloquial date format in Germany: day.month.year 1129 1130 "14." 1131 - the 14th of the current month 1132 - the 14th of next month 1133 "-14." 1134 - the 14th of last month 1135 """ 1136 str2parse = str2parse.replace(' ', '').replace('\t', '') 1137 1138 if not str2parse.endswith('.'): 1139 return [] 1140 try: 1141 day_val = int(str2parse[:-1]) 1142 except ValueError: 1143 return [] 1144 if (day_val < -31) or (day_val > 31) or (day_val == 0): 1145 return [] 1146 1147 now = pydt_now_here() 1148 matches = [] 1149 1150 # day X of last month only 1151 if day_val < 0: 1152 ts = pydt_replace(pydt_add(now, months = -1), day = abs(day_val), strict = False) 1153 if ts.day == day_val: 1154 matches.append ({ 1155 'data': ts, 1156 'label': _('%s-%s-%s: a %s last month') % (ts.year, ts.month, ts.day, ts.strftime('%A')) 1157 }) 1158 1159 # day X of ... 1160 if day_val > 0: 1161 # ... this month 1162 try: 1163 ts = pydt_replace(now, day = day_val, strict = False) 1164 matches.append ({ 1165 'data': ts, 1166 'label': _('%s-%s-%s: a %s this month') % (ts.year, ts.month, ts.day, ts.strftime('%A')) 1167 }) 1168 except ValueError: 1169 pass 1170 # ... next month 1171 try: 1172 ts = pydt_replace(pydt_add(now, months = 1), day = day_val, strict = False) 1173 if ts.day == day_val: 1174 matches.append ({ 1175 'data': ts, 1176 'label': _('%s-%s-%s: a %s next month') % (ts.year, ts.month, ts.day, ts.strftime('%A')) 1177 }) 1178 except ValueError: 1179 pass 1180 # ... last month 1181 try: 1182 ts = pydt_replace(pydt_add(now, months = -1), day = day_val, strict = False) 1183 if ts.day == day_val: 1184 matches.append ({ 1185 'data': ts, 1186 'label': _('%s-%s-%s: a %s last month') % (ts.year, ts.month, ts.day, ts.strftime('%A')) 1187 }) 1188 except ValueError: 1189 pass 1190 1191 return matches
1192 1193 #---------------------------------------------------------------------------
1194 -def __single_slash2py_dt(str2parse):
1195 """Expand fragments containing a single slash. 1196 1197 "5/" 1198 - 2005/ (2000 - 2025) 1199 - 1995/ (1990 - 1999) 1200 - Mai/current year 1201 - Mai/next year 1202 - Mai/last year 1203 - Mai/200x 1204 - Mai/20xx 1205 - Mai/199x 1206 - Mai/198x 1207 - Mai/197x 1208 - Mai/19xx 1209 1210 5/1999 1211 6/2004 1212 """ 1213 str2parse = str2parse.strip() 1214 1215 now = pydt_now_here() 1216 1217 # 5/1999 1218 if regex.match(r"^\d{1,2}(\s|\t)*/+(\s|\t)*\d{4}$", str2parse, flags = regex.UNICODE): 1219 month, year = regex.findall(r'\d+', str2parse, flags = regex.UNICODE) 1220 ts = pydt_replace(now, year = int(year), month = int(month), strict = False) 1221 return [{ 1222 'data': ts, 1223 'label': ts.strftime('%Y-%m-%d') 1224 }] 1225 1226 matches = [] 1227 # 5/ 1228 if regex.match(r"^\d{1,2}(\s|\t)*/+$", str2parse, flags = regex.UNICODE): 1229 val = int(str2parse.rstrip('/').strip()) 1230 1231 # "55/" -> "1955" 1232 if val < 100 and val >= 0: 1233 matches.append ({ 1234 'data': None, 1235 'label': '%s-' % (val + 1900) 1236 }) 1237 # "11/" -> "2011" 1238 if val < 26 and val >= 0: 1239 matches.append ({ 1240 'data': None, 1241 'label': '%s-' % (val + 2000) 1242 }) 1243 # "5/" -> "1995" 1244 if val < 10 and val >= 0: 1245 matches.append ({ 1246 'data': None, 1247 'label': '%s-' % (val + 1990) 1248 }) 1249 if val < 13 and val > 0: 1250 # "11/" -> "11/this year" 1251 matches.append ({ 1252 'data': None, 1253 'label': '%s-%.2d-' % (now.year, val) 1254 }) 1255 # "11/" -> "11/next year" 1256 ts = pydt_add(now, years = 1) 1257 matches.append ({ 1258 'data': None, 1259 'label': '%s-%.2d-' % (ts.year, val) 1260 }) 1261 # "11/" -> "11/last year" 1262 ts = pydt_add(now, years = -1) 1263 matches.append ({ 1264 'data': None, 1265 'label': '%s-%.2d-' % (ts.year, val) 1266 }) 1267 # "11/" -> "201?-11-" 1268 matches.append ({ 1269 'data': None, 1270 'label': '201?-%.2d-' % val 1271 }) 1272 # "11/" -> "200?-11-" 1273 matches.append ({ 1274 'data': None, 1275 'label': '200?-%.2d-' % val 1276 }) 1277 # "11/" -> "20??-11-" 1278 matches.append ({ 1279 'data': None, 1280 'label': '20??-%.2d-' % val 1281 }) 1282 # "11/" -> "199?-11-" 1283 matches.append ({ 1284 'data': None, 1285 'label': '199?-%.2d-' % val 1286 }) 1287 # "11/" -> "198?-11-" 1288 matches.append ({ 1289 'data': None, 1290 'label': '198?-%.2d-' % val 1291 }) 1292 # "11/" -> "198?-11-" 1293 matches.append ({ 1294 'data': None, 1295 'label': '197?-%.2d-' % val 1296 }) 1297 # "11/" -> "19??-11-" 1298 matches.append ({ 1299 'data': None, 1300 'label': '19??-%.2d-' % val 1301 }) 1302 1303 return matches
1304 1305 #---------------------------------------------------------------------------
1306 -def __numbers_only2py_dt(str2parse):
1307 """This matches on single numbers. 1308 1309 Spaces or tabs are discarded. 1310 """ 1311 try: 1312 val = int(str2parse.strip()) 1313 except ValueError: 1314 return [] 1315 1316 now = pydt_now_here() 1317 1318 matches = [] 1319 1320 # that year 1321 if (1850 < val) and (val < 2100): 1322 ts = pydt_replace(now, year = val, strict = False) 1323 matches.append ({ 1324 'data': ts, 1325 'label': ts.strftime('%Y-%m-%d') 1326 }) 1327 # day X of this month 1328 if (val > 0) and (val <= gregorian_month_length[now.month]): 1329 ts = pydt_replace(now, day = val, strict = False) 1330 matches.append ({ 1331 'data': ts, 1332 'label': _('%d. of %s (this month): a %s') % (val, ts.strftime('%B'), ts.strftime('%A')) 1333 }) 1334 # day X of ... 1335 if (val > 0) and (val < 32): 1336 # ... next month 1337 ts = pydt_replace(pydt_add(now, months = 1), day = val, strict = False) 1338 matches.append ({ 1339 'data': ts, 1340 'label': _('%d. of %s (next month): a %s') % (val, ts.strftime('%B'), ts.strftime('%A')) 1341 }) 1342 # ... last month 1343 ts = pydt_replace(pydt_add(now, months = -1), day = val, strict = False) 1344 matches.append ({ 1345 'data': ts, 1346 'label': _('%d. of %s (last month): a %s') % (val, ts.strftime('%B'), ts.strftime('%A')) 1347 }) 1348 # X days from now 1349 if (val > 0) and (val <= 400): # more than a year ahead in days ?? nah ! 1350 ts = pydt_add(now, days = val) 1351 matches.append ({ 1352 'data': ts, 1353 'label': _('in %d day(s): %s') % (val, ts.strftime('%A, %Y-%m-%d')) 1354 }) 1355 if (val < 0) and (val >= -400): # more than a year back in days ?? nah ! 1356 ts = pydt_add(now, days = val) 1357 matches.append ({ 1358 'data': ts, 1359 'label': _('%d day(s) ago: %s') % (abs(val), ts.strftime('%A, %Y-%m-%d')) 1360 }) 1361 # X weeks from now 1362 if (val > 0) and (val <= 50): # pregnancy takes about 40 weeks :-) 1363 ts = pydt_add(now, weeks = val) 1364 matches.append ({ 1365 'data': ts, 1366 'label': _('in %d week(s): %s') % (val, ts.strftime('%A, %Y-%m-%d')) 1367 }) 1368 if (val < 0) and (val >= -50): # pregnancy takes about 40 weeks :-) 1369 ts = pydt_add(now, weeks = val) 1370 matches.append ({ 1371 'data': ts, 1372 'label': _('%d week(s) ago: %s') % (abs(val), ts.strftime('%A, %Y-%m-%d')) 1373 }) 1374 1375 # month X of ... 1376 if (val < 13) and (val > 0): 1377 # ... this year 1378 ts = pydt_replace(now, month = val, strict = False) 1379 matches.append ({ 1380 'data': ts, 1381 'label': _('%s (%s this year)') % (ts.strftime('%Y-%m-%d'), ts.strftime('%B')) 1382 }) 1383 # ... next year 1384 ts = pydt_replace(pydt_add(now, years = 1), month = val, strict = False) 1385 matches.append ({ 1386 'data': ts, 1387 'label': _('%s (%s next year)') % (ts.strftime('%Y-%m-%d'), ts.strftime('%B')) 1388 }) 1389 # ... last year 1390 ts = pydt_replace(pydt_add(now, years = -1), month = val, strict = False) 1391 matches.append ({ 1392 'data': ts, 1393 'label': _('%s (%s last year)') % (ts.strftime('%Y-%m-%d'), ts.strftime('%B')) 1394 }) 1395 # fragment expansion 1396 matches.append ({ 1397 'data': None, 1398 'label': '200?-%s' % val 1399 }) 1400 matches.append ({ 1401 'data': None, 1402 'label': '199?-%s' % val 1403 }) 1404 matches.append ({ 1405 'data': None, 1406 'label': '198?-%s' % val 1407 }) 1408 matches.append ({ 1409 'data': None, 1410 'label': '19??-%s' % val 1411 }) 1412 1413 # needs mxDT 1414 # # day X of ... 1415 # if (val < 8) and (val > 0): 1416 # # ... this week 1417 # ts = now + mxDT.RelativeDateTime(weekday = (val-1, 0)) 1418 # matches.append ({ 1419 # 'data': mxdt2py_dt(ts), 1420 # 'label': _('%s this week (%s of %s)') % (ts.strftime('%A'), ts.day, ts.strftime('%B')) 1421 # }) 1422 # # ... next week 1423 # ts = now + mxDT.RelativeDateTime(weeks = +1, weekday = (val-1, 0)) 1424 # matches.append ({ 1425 # 'data': mxdt2py_dt(ts), 1426 # 'label': _('%s next week (%s of %s)') % (ts.strftime('%A'), ts.day, ts.strftime('%B')) 1427 # }) 1428 # # ... last week 1429 # ts = now + mxDT.RelativeDateTime(weeks = -1, weekday = (val-1, 0)) 1430 # matches.append ({ 1431 # 'data': mxdt2py_dt(ts), 1432 # 'label': _('%s last week (%s of %s)') % (ts.strftime('%A'), ts.day, ts.strftime('%B')) 1433 # }) 1434 1435 if (val < 100) and (val > 0): 1436 matches.append ({ 1437 'data': None, 1438 'label': '%s-' % (1900 + val) 1439 }) 1440 1441 if val == 201: 1442 matches.append ({ 1443 'data': now, 1444 'label': now.strftime('%Y-%m-%d') 1445 }) 1446 matches.append ({ 1447 'data': None, 1448 'label': now.strftime('%Y-%m') 1449 }) 1450 matches.append ({ 1451 'data': None, 1452 'label': now.strftime('%Y') 1453 }) 1454 matches.append ({ 1455 'data': None, 1456 'label': '%s-' % (now.year + 1) 1457 }) 1458 matches.append ({ 1459 'data': None, 1460 'label': '%s-' % (now.year - 1) 1461 }) 1462 1463 if val < 200 and val >= 190: 1464 for i in range(10): 1465 matches.append ({ 1466 'data': None, 1467 'label': '%s%s-' % (val, i) 1468 }) 1469 1470 return matches
1471 1472 #---------------------------------------------------------------------------
1473 -def __explicit_offset2py_dt(str2parse, offset_chars=None):
1474 """Default is 'hdwmy': 1475 h - hours 1476 d - days 1477 w - weeks 1478 m - months 1479 y - years 1480 1481 This also defines the significance of the order of the characters. 1482 """ 1483 if offset_chars is None: 1484 offset_chars = _('hdwmy (single character date offset triggers)')[:5].lower() 1485 1486 str2parse = str2parse.replace(' ', '').replace('\t', '') 1487 # "+/-XXXh/d/w/m/t" 1488 if regex.fullmatch(r"(\+|-){,1}\d{1,3}[%s]" % offset_chars, str2parse) is None: 1489 return [] 1490 1491 offset_val = int(str2parse[:-1]) 1492 offset_char = str2parse[-1:] 1493 is_past = str2parse.startswith('-') 1494 now = pydt_now_here() 1495 ts = None 1496 1497 # hours 1498 if offset_char == offset_chars[0]: 1499 ts = pydt_add(now, hours = offset_val) 1500 if is_past: 1501 label = _('%d hour(s) ago: %s') % (abs(offset_val), ts.strftime('%H:%M')) 1502 else: 1503 label = _('in %d hour(s): %s') % (offset_val, ts.strftime('%H:%M')) 1504 # days 1505 elif offset_char == offset_chars[1]: 1506 ts = pydt_add(now, days = offset_val) 1507 if is_past: 1508 label = _('%d day(s) ago: %s') % (abs(offset_val), ts.strftime('%A, %Y-%m-%d')) 1509 else: 1510 label = _('in %d day(s): %s') % (offset_val, ts.strftime('%A, %Y-%m-%d')) 1511 # weeks 1512 elif offset_char == offset_chars[2]: 1513 ts = pydt_add(now, weeks = offset_val) 1514 if is_past: 1515 label = _('%d week(s) ago: %s') % (abs(offset_val), ts.strftime('%A, %Y-%m-%d')) 1516 else: 1517 label = _('in %d week(s): %s') % (offset_val, ts.strftime('%A, %Y-%m-%d')) 1518 # months 1519 elif offset_char == offset_chars[3]: 1520 ts = pydt_add(now, months = offset_val) 1521 if is_past: 1522 label = _('%d month(s) ago: %s') % (abs(offset_val), ts.strftime('%A, %Y-%m-%d')) 1523 else: 1524 label = _('in %d month(s): %s') % (offset_val, ts.strftime('%A, %Y-%m-%d')) 1525 # years 1526 elif offset_char == offset_chars[4]: 1527 ts = pydt_add(now, years = offset_val) 1528 if is_past: 1529 label = _('%d year(s) ago: %s') % (abs(offset_val), ts.strftime('%A, %Y-%m-%d')) 1530 else: 1531 label = _('in %d year(s): %s') % (offset_val, ts.strftime('%A, %Y-%m-%d')) 1532 1533 if ts is None: 1534 return [] 1535 1536 return [{'data': ts, 'label': label}]
1537 1538 #---------------------------------------------------------------------------
1539 -def str2pydt_matches(str2parse=None, patterns=None):
1540 """Turn a string into candidate dates and auto-completions the user is likely to type. 1541 1542 You MUST have called locale.setlocale(locale.LC_ALL, '') 1543 somewhere in your code previously. 1544 1545 @param patterns: list of time.strptime compatible date pattern 1546 @type patterns: list 1547 """ 1548 matches = [] 1549 matches.extend(__single_dot2py_dt(str2parse)) 1550 matches.extend(__numbers_only2py_dt(str2parse)) 1551 matches.extend(__single_slash2py_dt(str2parse)) 1552 matches.extend(__single_char2py_dt(str2parse)) 1553 matches.extend(__explicit_offset2py_dt(str2parse)) 1554 1555 # no more with Python3 1556 # # try mxDT parsers 1557 # try: 1558 # date = mxDT.Parser.DateFromString ( 1559 # text = str2parse, 1560 # formats = ('euro', 'iso', 'us', 'altus', 'altiso', 'lit', 'altlit', 'eurlit') 1561 # ) 1562 # matches.append ({ 1563 # 'data': mxdt2py_dt(date), 1564 # 'label': date.strftime('%Y-%m-%d') 1565 # }) 1566 # except (ValueError, OverflowError): 1567 # pass 1568 # except mxDT.RangeError: 1569 # pass 1570 1571 # apply explicit patterns 1572 if patterns is None: 1573 patterns = [] 1574 1575 patterns.append('%Y-%m-%d') 1576 patterns.append('%y-%m-%d') 1577 patterns.append('%Y/%m/%d') 1578 patterns.append('%y/%m/%d') 1579 1580 patterns.append('%d-%m-%Y') 1581 patterns.append('%d-%m-%y') 1582 patterns.append('%d/%m/%Y') 1583 patterns.append('%d/%m/%y') 1584 patterns.append('%d.%m.%Y') 1585 1586 patterns.append('%m-%d-%Y') 1587 patterns.append('%m-%d-%y') 1588 patterns.append('%m/%d/%Y') 1589 patterns.append('%m/%d/%y') 1590 1591 patterns.append('%Y.%m.%d') 1592 1593 for pattern in patterns: 1594 try: 1595 date = pyDT.datetime.strptime(str2parse, pattern).replace ( 1596 hour = 11, 1597 minute = 11, 1598 second = 11, 1599 tzinfo = gmCurrentLocalTimezone 1600 ) 1601 matches.append ({ 1602 'data': date, 1603 'label': pydt_strftime(date, format = '%Y-%m-%d', accuracy = acc_days) 1604 }) 1605 except ValueError: 1606 # C-level overflow 1607 continue 1608 1609 return matches
1610 1611 #=========================================================================== 1612 # string -> fuzzy timestamp parser 1613 #---------------------------------------------------------------------------
1614 -def __single_slash(str2parse):
1615 """Expand fragments containing a single slash. 1616 1617 "5/" 1618 - 2005/ (2000 - 2025) 1619 - 1995/ (1990 - 1999) 1620 - Mai/current year 1621 - Mai/next year 1622 - Mai/last year 1623 - Mai/200x 1624 - Mai/20xx 1625 - Mai/199x 1626 - Mai/198x 1627 - Mai/197x 1628 - Mai/19xx 1629 """ 1630 matches = [] 1631 now = pydt_now_here() 1632 # "xx/yyyy" 1633 if regex.match("^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*\d{4}(\s|\t)*$", str2parse, flags = regex.UNICODE): 1634 parts = regex.findall('\d+', str2parse, flags = regex.UNICODE) 1635 month = int(parts[0]) 1636 if month in range(1, 13): 1637 fts = cFuzzyTimestamp ( 1638 timestamp = now.replace(year = int(parts[1], month = month)), 1639 accuracy = acc_months 1640 ) 1641 matches.append ({ 1642 'data': fts, 1643 'label': fts.format_accurately() 1644 }) 1645 # "xx/" 1646 elif regex.match("^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*$", str2parse, flags = regex.UNICODE): 1647 val = int(regex.findall('\d+', str2parse, flags = regex.UNICODE)[0]) 1648 1649 if val < 100 and val >= 0: 1650 matches.append ({ 1651 'data': None, 1652 'label': '%s/' % (val + 1900) 1653 }) 1654 1655 if val < 26 and val >= 0: 1656 matches.append ({ 1657 'data': None, 1658 'label': '%s/' % (val + 2000) 1659 }) 1660 1661 if val < 10 and val >= 0: 1662 matches.append ({ 1663 'data': None, 1664 'label': '%s/' % (val + 1990) 1665 }) 1666 1667 if val < 13 and val > 0: 1668 matches.append ({ 1669 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months), 1670 'label': '%.2d/%s' % (val, now.year) 1671 }) 1672 ts = now.replace(year = now.year + 1) 1673 matches.append ({ 1674 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months), 1675 'label': '%.2d/%s' % (val, ts.year) 1676 }) 1677 ts = now.replace(year = now.year - 1) 1678 matches.append ({ 1679 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months), 1680 'label': '%.2d/%s' % (val, ts.year) 1681 }) 1682 matches.append ({ 1683 'data': None, 1684 'label': '%.2d/200' % val 1685 }) 1686 matches.append ({ 1687 'data': None, 1688 'label': '%.2d/20' % val 1689 }) 1690 matches.append ({ 1691 'data': None, 1692 'label': '%.2d/199' % val 1693 }) 1694 matches.append ({ 1695 'data': None, 1696 'label': '%.2d/198' % val 1697 }) 1698 matches.append ({ 1699 'data': None, 1700 'label': '%.2d/197' % val 1701 }) 1702 matches.append ({ 1703 'data': None, 1704 'label': '%.2d/19' % val 1705 }) 1706 1707 return matches
1708 1709 #---------------------------------------------------------------------------
1710 -def __numbers_only(str2parse):
1711 """This matches on single numbers. 1712 1713 Spaces or tabs are discarded. 1714 """ 1715 if not regex.match("^(\s|\t)*\d{1,4}(\s|\t)*$", str2parse, flags = regex.UNICODE): 1716 return [] 1717 1718 now = pydt_now_here() 1719 val = int(regex.findall('\d{1,4}', str2parse, flags = regex.UNICODE)[0]) 1720 1721 matches = [] 1722 1723 # today in that year 1724 if (1850 < val) and (val < 2100): 1725 target_date = cFuzzyTimestamp ( 1726 timestamp = now.replace(year = val), 1727 accuracy = acc_years 1728 ) 1729 tmp = { 1730 'data': target_date, 1731 'label': '%s' % target_date 1732 } 1733 matches.append(tmp) 1734 1735 # day X of this month 1736 if val <= gregorian_month_length[now.month]: 1737 ts = now.replace(day = val) 1738 target_date = cFuzzyTimestamp ( 1739 timestamp = ts, 1740 accuracy = acc_days 1741 ) 1742 tmp = { 1743 'data': target_date, 1744 'label': _('%d. of %s (this month) - a %s') % (val, ts.strftime('%B'), ts.strftime('%A')) 1745 } 1746 matches.append(tmp) 1747 1748 # day X of next month 1749 next_month = get_next_month(now) 1750 if val <= gregorian_month_length[next_month]: 1751 ts = now.replace(day = val, month = next_month) 1752 target_date = cFuzzyTimestamp ( 1753 timestamp = ts, 1754 accuracy = acc_days 1755 ) 1756 tmp = { 1757 'data': target_date, 1758 'label': _('%d. of %s (next month) - a %s') % (val, ts.strftime('%B'), ts.strftime('%A')) 1759 } 1760 matches.append(tmp) 1761 1762 # day X of last month 1763 last_month = get_last_month(now) 1764 if val <= gregorian_month_length[last_month]: 1765 ts = now.replace(day = val, month = last_month) 1766 target_date = cFuzzyTimestamp ( 1767 timestamp = ts, 1768 accuracy = acc_days 1769 ) 1770 tmp = { 1771 'data': target_date, 1772 'label': _('%d. of %s (last month) - a %s') % (val, ts.strftime('%B'), ts.strftime('%A')) 1773 } 1774 matches.append(tmp) 1775 1776 # X days from now 1777 if val <= 400: # more than a year ahead in days ?? nah ! 1778 target_date = cFuzzyTimestamp(timestamp = now + pyDT.timedelta(days = val)) 1779 tmp = { 1780 'data': target_date, 1781 'label': _('in %d day(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d')) 1782 } 1783 matches.append(tmp) 1784 1785 # X weeks from now 1786 if val <= 50: # pregnancy takes about 40 weeks :-) 1787 target_date = cFuzzyTimestamp(timestamp = now + pyDT.timedelta(weeks = val)) 1788 tmp = { 1789 'data': target_date, 1790 'label': _('in %d week(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d')) 1791 } 1792 matches.append(tmp) 1793 1794 # month X of ... 1795 if val < 13: 1796 # ... this year 1797 target_date = cFuzzyTimestamp ( 1798 timestamp = pydt_replace(now, month = val, strict = False), 1799 accuracy = acc_months 1800 ) 1801 tmp = { 1802 'data': target_date, 1803 'label': _('%s (%s this year)') % (target_date, ts.strftime('%B')) 1804 } 1805 matches.append(tmp) 1806 1807 # ... next year 1808 target_date = cFuzzyTimestamp ( 1809 timestamp = pydt_add(pydt_replace(now, month = val, strict = False), years = 1), 1810 accuracy = acc_months 1811 ) 1812 tmp = { 1813 'data': target_date, 1814 'label': _('%s (%s next year)') % (target_date, ts.strftime('%B')) 1815 } 1816 matches.append(tmp) 1817 1818 # ... last year 1819 target_date = cFuzzyTimestamp ( 1820 timestamp = pydt_add(pydt_replace(now, month = val, strict = False), years = -1), 1821 accuracy = acc_months 1822 ) 1823 tmp = { 1824 'data': target_date, 1825 'label': _('%s (%s last year)') % (target_date, ts.strftime('%B')) 1826 } 1827 matches.append(tmp) 1828 1829 # fragment expansion 1830 matches.append ({ 1831 'data': None, 1832 'label': '%s/200' % val 1833 }) 1834 matches.append ({ 1835 'data': None, 1836 'label': '%s/199' % val 1837 }) 1838 matches.append ({ 1839 'data': None, 1840 'label': '%s/198' % val 1841 }) 1842 matches.append ({ 1843 'data': None, 1844 'label': '%s/19' % val 1845 }) 1846 1847 # reactivate when mxDT becomes available on py3k 1848 # # day X of ... 1849 # if val < 8: 1850 # # ... this week 1851 # ts = now + mxDT.RelativeDateTime(weekday = (val-1, 0)) 1852 # target_date = cFuzzyTimestamp ( 1853 # timestamp = ts, 1854 # accuracy = acc_days 1855 # ) 1856 # tmp = { 1857 # 'data': target_date, 1858 # 'label': _('%s this week (%s of %s)') % (ts.strftime('%A'), ts.day, ts.strftime('%B')) 1859 # } 1860 # matches.append(tmp) 1861 # 1862 # # ... next week 1863 # ts = now + mxDT.RelativeDateTime(weeks = +1, weekday = (val-1, 0)) 1864 # target_date = cFuzzyTimestamp ( 1865 # timestamp = ts, 1866 # accuracy = acc_days 1867 # ) 1868 # tmp = { 1869 # 'data': target_date, 1870 # 'label': _('%s next week (%s of %s)') % (ts.strftime('%A'), ts.day, ts.strftime('%B')) 1871 # } 1872 # matches.append(tmp) 1873 1874 # # ... last week 1875 # ts = now + mxDT.RelativeDateTime(weeks = -1, weekday = (val-1, 0)) 1876 # target_date = cFuzzyTimestamp ( 1877 # timestamp = ts, 1878 # accuracy = acc_days 1879 # ) 1880 # tmp = { 1881 # 'data': target_date, 1882 # 'label': _('%s last week (%s of %s)') % (ts.strftime('%A'), ts.day, ts.strftime('%B')) 1883 # } 1884 # matches.append(tmp) 1885 1886 if val < 100: 1887 matches.append ({ 1888 'data': None, 1889 'label': '%s/' % (1900 + val) 1890 }) 1891 1892 # year 2k 1893 if val == 200: 1894 tmp = { 1895 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_days), 1896 'label': '%s' % target_date 1897 } 1898 matches.append(tmp) 1899 matches.append ({ 1900 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months), 1901 'label': '%.2d/%s' % (now.month, now.year) 1902 }) 1903 matches.append ({ 1904 'data': None, 1905 'label': '%s/' % now.year 1906 }) 1907 matches.append ({ 1908 'data': None, 1909 'label': '%s/' % (now.year + 1) 1910 }) 1911 matches.append ({ 1912 'data': None, 1913 'label': '%s/' % (now.year - 1) 1914 }) 1915 1916 if val < 200 and val >= 190: 1917 for i in range(10): 1918 matches.append ({ 1919 'data': None, 1920 'label': '%s%s/' % (val, i) 1921 }) 1922 1923 return matches
1924 1925 #---------------------------------------------------------------------------
1926 -def str2fuzzy_timestamp_matches(str2parse=None, default_time=None, patterns=None):
1927 """ 1928 Turn a string into candidate fuzzy timestamps and auto-completions the user is likely to type. 1929 1930 You MUST have called locale.setlocale(locale.LC_ALL, '') 1931 somewhere in your code previously. 1932 1933 @param default_time: if you want to force the time part of the time 1934 stamp to a given value and the user doesn't type any time part 1935 this value will be used 1936 @type default_time: an mx.DateTime.DateTimeDelta instance 1937 1938 @param patterns: list of [time.strptime compatible date/time pattern, accuracy] 1939 @type patterns: list 1940 """ 1941 matches = [] 1942 1943 matches.extend(__numbers_only(str2parse)) 1944 matches.extend(__single_slash(str2parse)) 1945 1946 matches.extend ([ 1947 { 'data': cFuzzyTimestamp(timestamp = m['data'], accuracy = acc_days), 1948 'label': m['label'] 1949 } for m in __single_dot2py_dt(str2parse) 1950 ]) 1951 matches.extend ([ 1952 { 'data': cFuzzyTimestamp(timestamp = m['data'], accuracy = acc_days), 1953 'label': m['label'] 1954 } for m in __single_char2py_dt(str2parse) 1955 ]) 1956 matches.extend ([ 1957 { 'data': cFuzzyTimestamp(timestamp = m['data'], accuracy = acc_days), 1958 'label': m['label'] 1959 } for m in __explicit_offset2py_dt(str2parse) 1960 ]) 1961 1962 # reactivate, once mxDT becomes available on Py3k 1963 # # try mxDT parsers 1964 # try: 1965 # # date ? 1966 # date_only = mxDT.Parser.DateFromString ( 1967 # text = str2parse, 1968 # formats = ('euro', 'iso', 'us', 'altus', 'altiso', 'lit', 'altlit', 'eurlit') 1969 # ) 1970 # # time, too ? 1971 # time_part = mxDT.Parser.TimeFromString(text = str2parse) 1972 # datetime = date_only + time_part 1973 # if datetime == date_only: 1974 # accuracy = acc_days 1975 # if isinstance(default_time, mxDT.DateTimeDeltaType): 1976 # datetime = date_only + default_time 1977 # accuracy = acc_minutes 1978 # else: 1979 # accuracy = acc_subseconds 1980 # fts = cFuzzyTimestamp ( 1981 # timestamp = datetime, 1982 # accuracy = accuracy 1983 # ) 1984 # matches.append ({ 1985 # 'data': fts, 1986 # 'label': fts.format_accurately() 1987 # }) 1988 # except ValueError: 1989 # pass 1990 # except mxDT.RangeError: 1991 # pass 1992 1993 if patterns is None: 1994 patterns = [] 1995 patterns.extend([ 1996 ['%Y-%m-%d', acc_days], 1997 ['%y-%m-%d', acc_days], 1998 ['%Y/%m/%d', acc_days], 1999 ['%y/%m/%d', acc_days], 2000 2001 ['%d-%m-%Y', acc_days], 2002 ['%d-%m-%y', acc_days], 2003 ['%d/%m/%Y', acc_days], 2004 ['%d/%m/%y', acc_days], 2005 ['%d.%m.%Y', acc_days], 2006 2007 ['%m-%d-%Y', acc_days], 2008 ['%m-%d-%y', acc_days], 2009 ['%m/%d/%Y', acc_days], 2010 ['%m/%d/%y', acc_days] 2011 ]) 2012 for pattern in patterns: 2013 try: 2014 ts = pyDT.datetime.strptime(str2parse, pattern[0]).replace ( 2015 hour = 11, 2016 minute = 11, 2017 second = 11, 2018 tzinfo = gmCurrentLocalTimezone 2019 ) 2020 fts = cFuzzyTimestamp(timestamp = ts, accuracy = pattern[1]) 2021 matches.append ({ 2022 'data': fts, 2023 'label': fts.format_accurately() 2024 }) 2025 except ValueError: 2026 # C-level overflow 2027 continue 2028 2029 return matches
2030 2031 #=========================================================================== 2032 # fuzzy timestamp class 2033 #---------------------------------------------------------------------------
2034 -class cFuzzyTimestamp:
2035 2036 # FIXME: add properties for year, month, ... 2037 2038 """A timestamp implementation with definable inaccuracy. 2039 2040 This class contains an datetime.datetime instance to 2041 hold the actual timestamp. It adds an accuracy attribute 2042 to allow the programmer to set the precision of the 2043 timestamp. 2044 2045 The timestamp will have to be initialzed with a fully 2046 precise value (which may, of course, contain partially 2047 fake data to make up for missing values). One can then 2048 set the accuracy value to indicate up to which part of 2049 the timestamp the data is valid. Optionally a modifier 2050 can be set to indicate further specification of the 2051 value (such as "summer", "afternoon", etc). 2052 2053 accuracy values: 2054 1: year only 2055 ... 2056 7: everything including milliseconds value 2057 2058 Unfortunately, one cannot directly derive a class from mx.DateTime.DateTime :-( 2059 """ 2060 #-----------------------------------------------------------------------
2061 - def __init__(self, timestamp=None, accuracy=acc_subseconds, modifier=''):
2062 2063 if timestamp is None: 2064 timestamp = pydt_now_here() 2065 accuracy = acc_subseconds 2066 modifier = '' 2067 2068 if (accuracy < 1) or (accuracy > 8): 2069 raise ValueError('%s.__init__(): <accuracy> must be between 1 and 8' % self.__class__.__name__) 2070 2071 if not isinstance(timestamp, pyDT.datetime): 2072 raise TypeError('%s.__init__(): <timestamp> must be of datetime.datetime type, but is %s' % self.__class__.__name__, type(timestamp)) 2073 2074 if timestamp.tzinfo is None: 2075 raise ValueError('%s.__init__(): <tzinfo> must be defined' % self.__class__.__name__) 2076 2077 self.timestamp = timestamp 2078 self.accuracy = accuracy 2079 self.modifier = modifier
2080 2081 #----------------------------------------------------------------------- 2082 # magic API 2083 #-----------------------------------------------------------------------
2084 - def __str__(self):
2085 """Return string representation meaningful to a user, also for %s formatting.""" 2086 return self.format_accurately()
2087 2088 #-----------------------------------------------------------------------
2089 - def __repr__(self):
2090 """Return string meaningful to a programmer to aid in debugging.""" 2091 tmp = '<[%s]: timestamp [%s], accuracy [%s] (%s), modifier [%s] at %s>' % ( 2092 self.__class__.__name__, 2093 repr(self.timestamp), 2094 self.accuracy, 2095 _accuracy_strings[self.accuracy], 2096 self.modifier, 2097 id(self) 2098 ) 2099 return tmp
2100 2101 #----------------------------------------------------------------------- 2102 # external API 2103 #-----------------------------------------------------------------------
2104 - def strftime(self, format_string):
2105 if self.accuracy == 7: 2106 return self.timestamp.strftime(format_string) 2107 return self.format_accurately()
2108 2109 #-----------------------------------------------------------------------
2110 - def Format(self, format_string):
2111 return self.strftime(format_string)
2112 2113 #-----------------------------------------------------------------------
2114 - def format_accurately(self, accuracy=None):
2115 if accuracy is None: 2116 accuracy = self.accuracy 2117 2118 if accuracy == acc_years: 2119 return str(self.timestamp.year) 2120 2121 if accuracy == acc_months: 2122 return self.timestamp.strftime('%m/%Y') # FIXME: use 3-letter month ? 2123 2124 if accuracy == acc_weeks: 2125 return self.timestamp.strftime('%m/%Y') # FIXME: use 3-letter month ? 2126 2127 if accuracy == acc_days: 2128 return self.timestamp.strftime('%Y-%m-%d') 2129 2130 if accuracy == acc_hours: 2131 return self.timestamp.strftime("%Y-%m-%d %I%p") 2132 2133 if accuracy == acc_minutes: 2134 return self.timestamp.strftime("%Y-%m-%d %H:%M") 2135 2136 if accuracy == acc_seconds: 2137 return self.timestamp.strftime("%Y-%m-%d %H:%M:%S") 2138 2139 if accuracy == acc_subseconds: 2140 return self.timestamp.strftime("%Y-%m-%d %H:%M:%S.%f") 2141 2142 raise ValueError('%s.format_accurately(): <accuracy> (%s) must be between 1 and 7' % ( 2143 self.__class__.__name__, 2144 accuracy 2145 ))
2146 2147 #-----------------------------------------------------------------------
2148 - def get_pydt(self):
2149 return self.timestamp
2150 2151 #=========================================================================== 2152 # main 2153 #--------------------------------------------------------------------------- 2154 if __name__ == '__main__': 2155 2156 if len(sys.argv) < 2: 2157 sys.exit() 2158 2159 if sys.argv[1] != "test": 2160 sys.exit() 2161 2162 from Gnumed.pycommon import gmI18N 2163 from Gnumed.pycommon import gmLog2 2164 2165 #----------------------------------------------------------------------- 2166 intervals_as_str = [ 2167 '7', '12', ' 12', '12 ', ' 12 ', ' 12 ', '0', '~12', '~ 12', ' ~ 12', ' ~ 12 ', 2168 '12a', '12 a', '12 a', '12j', '12J', '12y', '12Y', ' ~ 12 a ', '~0a', 2169 '12m', '17 m', '12 m', '17M', ' ~ 17 m ', ' ~ 3 / 12 ', '7/12', '0/12', 2170 '12w', '17 w', '12 w', '17W', ' ~ 17 w ', ' ~ 15 / 52', '2/52', '0/52', 2171 '12d', '17 d', '12 t', '17D', ' ~ 17 T ', ' ~ 12 / 7', '3/7', '0/7', 2172 '12h', '17 h', '12 H', '17H', ' ~ 17 h ', ' ~ 36 / 24', '7/24', '0/24', 2173 ' ~ 36 / 60', '7/60', '190/60', '0/60', 2174 '12a1m', '12 a 1 M', '12 a17m', '12j 12m', '12J7m', '12y7m', '12Y7M', ' ~ 12 a 37 m ', '~0a0m', 2175 '10m1w', 2176 'invalid interval input' 2177 ] 2178 #-----------------------------------------------------------------------
2179 - def test_format_interval():
2180 intv = pyDT.timedelta(minutes=1, seconds=2) 2181 for acc in _accuracy_strings.keys(): 2182 print ('[%s]: "%s" -> "%s"' % (acc, intv, format_interval(intv, acc))) 2183 return 2184 2185 for tmp in intervals_as_str: 2186 intv = str2interval(str_interval = tmp) 2187 if intv is None: 2188 print(tmp, '->', intv) 2189 continue 2190 for acc in _accuracy_strings.keys(): 2191 print ('[%s]: "%s" -> "%s"' % (acc, tmp, format_interval(intv, acc)))
2192 2193 #-----------------------------------------------------------------------
2194 - def test_format_interval_medically():
2195 2196 intervals = [ 2197 pyDT.timedelta(seconds = 1), 2198 pyDT.timedelta(seconds = 5), 2199 pyDT.timedelta(seconds = 30), 2200 pyDT.timedelta(seconds = 60), 2201 pyDT.timedelta(seconds = 94), 2202 pyDT.timedelta(seconds = 120), 2203 pyDT.timedelta(minutes = 5), 2204 pyDT.timedelta(minutes = 30), 2205 pyDT.timedelta(minutes = 60), 2206 pyDT.timedelta(minutes = 90), 2207 pyDT.timedelta(minutes = 120), 2208 pyDT.timedelta(minutes = 200), 2209 pyDT.timedelta(minutes = 400), 2210 pyDT.timedelta(minutes = 600), 2211 pyDT.timedelta(minutes = 800), 2212 pyDT.timedelta(minutes = 1100), 2213 pyDT.timedelta(minutes = 2000), 2214 pyDT.timedelta(minutes = 3500), 2215 pyDT.timedelta(minutes = 4000), 2216 pyDT.timedelta(hours = 1), 2217 pyDT.timedelta(hours = 2), 2218 pyDT.timedelta(hours = 4), 2219 pyDT.timedelta(hours = 8), 2220 pyDT.timedelta(hours = 12), 2221 pyDT.timedelta(hours = 20), 2222 pyDT.timedelta(hours = 23), 2223 pyDT.timedelta(hours = 24), 2224 pyDT.timedelta(hours = 25), 2225 pyDT.timedelta(hours = 30), 2226 pyDT.timedelta(hours = 48), 2227 pyDT.timedelta(hours = 98), 2228 pyDT.timedelta(hours = 120), 2229 pyDT.timedelta(days = 1), 2230 pyDT.timedelta(days = 2), 2231 pyDT.timedelta(days = 4), 2232 pyDT.timedelta(days = 16), 2233 pyDT.timedelta(days = 29), 2234 pyDT.timedelta(days = 30), 2235 pyDT.timedelta(days = 31), 2236 pyDT.timedelta(days = 37), 2237 pyDT.timedelta(days = 40), 2238 pyDT.timedelta(days = 47), 2239 pyDT.timedelta(days = 126), 2240 pyDT.timedelta(days = 127), 2241 pyDT.timedelta(days = 128), 2242 pyDT.timedelta(days = 300), 2243 pyDT.timedelta(days = 359), 2244 pyDT.timedelta(days = 360), 2245 pyDT.timedelta(days = 361), 2246 pyDT.timedelta(days = 362), 2247 pyDT.timedelta(days = 363), 2248 pyDT.timedelta(days = 364), 2249 pyDT.timedelta(days = 365), 2250 pyDT.timedelta(days = 366), 2251 pyDT.timedelta(days = 367), 2252 pyDT.timedelta(days = 400), 2253 pyDT.timedelta(weeks = 52 * 30), 2254 pyDT.timedelta(weeks = 52 * 79, days = 33) 2255 ] 2256 2257 idx = 1 2258 for intv in intervals: 2259 print ('%s) %s -> %s' % (idx, intv, format_interval_medically(intv))) 2260 idx += 1
2261 #-----------------------------------------------------------------------
2262 - def test_str2interval():
2263 print ("testing str2interval()") 2264 print ("----------------------") 2265 2266 for interval_as_str in intervals_as_str: 2267 print ("input: <%s>" % interval_as_str) 2268 print (" ==>", str2interval(str_interval=interval_as_str)) 2269 2270 return True
2271 #-------------------------------------------------
2272 - def test_date_time():
2273 print ("DST currently in effect:", dst_currently_in_effect) 2274 print ("current UTC offset:", current_local_utc_offset_in_seconds, "seconds") 2275 #print ("current timezone (interval):", current_local_timezone_interval) 2276 print ("current timezone (ISO conformant numeric string):", current_local_iso_numeric_timezone_string) 2277 print ("local timezone class:", cPlatformLocalTimezone) 2278 print ("") 2279 tz = cPlatformLocalTimezone() 2280 print ("local timezone instance:", tz) 2281 print (" (total) UTC offset:", tz.utcoffset(pyDT.datetime.now())) 2282 print (" DST adjustment:", tz.dst(pyDT.datetime.now())) 2283 print (" timezone name:", tz.tzname(pyDT.datetime.now())) 2284 print ("") 2285 print ("current local timezone:", gmCurrentLocalTimezone) 2286 print (" (total) UTC offset:", gmCurrentLocalTimezone.utcoffset(pyDT.datetime.now())) 2287 print (" DST adjustment:", gmCurrentLocalTimezone.dst(pyDT.datetime.now())) 2288 print (" timezone name:", gmCurrentLocalTimezone.tzname(pyDT.datetime.now())) 2289 print ("") 2290 print ("now here:", pydt_now_here()) 2291 print ("")
2292 2293 #-------------------------------------------------
2294 - def test_str2fuzzy_timestamp_matches():
2295 print ("testing function str2fuzzy_timestamp_matches") 2296 print ("--------------------------------------------") 2297 2298 val = None 2299 while val != 'exit': 2300 val = input('Enter date fragment ("exit" quits): ') 2301 matches = str2fuzzy_timestamp_matches(str2parse = val) 2302 for match in matches: 2303 print ('label shown :', match['label']) 2304 print ('data attached:', match['data'], match['data'].timestamp) 2305 print ("") 2306 print ("---------------")
2307 2308 #-------------------------------------------------
2309 - def test_cFuzzyTimeStamp():
2310 print ("testing fuzzy timestamp class") 2311 print ("-----------------------------") 2312 2313 fts = cFuzzyTimestamp() 2314 print ("\nfuzzy timestamp <%s '%s'>" % ('class', fts.__class__.__name__)) 2315 for accuracy in range(1,8): 2316 fts.accuracy = accuracy 2317 print (" accuracy : %s (%s)" % (accuracy, _accuracy_strings[accuracy])) 2318 print (" format_accurately:", fts.format_accurately()) 2319 print (" strftime() :", fts.strftime('%Y %b %d %H:%M:%S')) 2320 print (" print ... :", fts) 2321 print (" print '%%s' %% ... : %s" % fts) 2322 print (" str() :", str(fts)) 2323 print (" repr() :", repr(fts)) 2324 input('press ENTER to continue')
2325 2326 #-------------------------------------------------
2327 - def test_get_pydt():
2328 print ("testing platform for handling dates before 1970") 2329 print ("-----------------------------------------------") 2330 ts = mxDT.DateTime(1935, 4, 2) 2331 fts = cFuzzyTimestamp(timestamp=ts) 2332 print ("fts :", fts) 2333 print ("fts.get_pydt():", fts.get_pydt())
2334 #-------------------------------------------------
2335 - def test_calculate_apparent_age():
2336 # test leap year glitches 2337 start = pydt_now_here().replace(year = 2000).replace(month = 2).replace(day = 29) 2338 end = pydt_now_here().replace(year = 2012).replace(month = 2).replace(day = 27) 2339 print ("start is leap year: 29.2.2000") 2340 print (" ", calculate_apparent_age(start = start, end = end)) 2341 print (" ", format_apparent_age_medically(calculate_apparent_age(start = start))) 2342 2343 start = pydt_now_here().replace(month = 10).replace(day = 23).replace(year = 1974) 2344 end = pydt_now_here().replace(year = 2012).replace(month = 2).replace(day = 29) 2345 print ("end is leap year: 29.2.2012") 2346 print (" ", calculate_apparent_age(start = start, end = end)) 2347 print (" ", format_apparent_age_medically(calculate_apparent_age(start = start))) 2348 2349 start = pydt_now_here().replace(year = 2000).replace(month = 2).replace(day = 29) 2350 end = pydt_now_here().replace(year = 2012).replace(month = 2).replace(day = 29) 2351 print ("start is leap year: 29.2.2000") 2352 print ("end is leap year: 29.2.2012") 2353 print (" ", calculate_apparent_age(start = start, end = end)) 2354 print (" ", format_apparent_age_medically(calculate_apparent_age(start = start))) 2355 2356 print ("leap year tests worked") 2357 2358 start = pydt_now_here().replace(month = 10).replace(day = 23).replace(year = 1974) 2359 print (calculate_apparent_age(start = start)) 2360 print (format_apparent_age_medically(calculate_apparent_age(start = start))) 2361 2362 start = pydt_now_here().replace(month = 3).replace(day = 13).replace(year = 1979) 2363 print (calculate_apparent_age(start = start)) 2364 print (format_apparent_age_medically(calculate_apparent_age(start = start))) 2365 2366 start = pydt_now_here().replace(month = 2, day = 2).replace(year = 1979) 2367 end = pydt_now_here().replace(month = 3).replace(day = 31).replace(year = 1979) 2368 print (calculate_apparent_age(start = start, end = end)) 2369 2370 start = pydt_now_here().replace(month = 7, day = 21).replace(year = 2009) 2371 print (format_apparent_age_medically(calculate_apparent_age(start = start))) 2372 2373 print ("-------") 2374 start = pydt_now_here().replace(month = 1).replace(day = 23).replace(hour = 12).replace(minute = 11).replace(year = 2011) 2375 print (calculate_apparent_age(start = start)) 2376 print (format_apparent_age_medically(calculate_apparent_age(start = start)))
2377 #-------------------------------------------------
2378 - def test_str2pydt():
2379 print ("testing function str2pydt_matches") 2380 print ("---------------------------------") 2381 2382 val = None 2383 while val != 'exit': 2384 val = input('Enter date fragment ("exit" quits): ') 2385 matches = str2pydt_matches(str2parse = val) 2386 for match in matches: 2387 print ('label shown :', match['label']) 2388 print ('data attached:', match['data']) 2389 print ("") 2390 print ("---------------")
2391 2392 #-------------------------------------------------
2393 - def test_pydt_strftime():
2394 dt = pydt_now_here() 2395 print (pydt_strftime(dt, '-(%Y %b %d)-')) 2396 print (pydt_strftime(dt)) 2397 print (pydt_strftime(dt, accuracy = acc_days)) 2398 print (pydt_strftime(dt, accuracy = acc_minutes)) 2399 print (pydt_strftime(dt, accuracy = acc_seconds)) 2400 dt = dt.replace(year = 1899) 2401 print (pydt_strftime(dt)) 2402 print (pydt_strftime(dt, accuracy = acc_days)) 2403 print (pydt_strftime(dt, accuracy = acc_minutes)) 2404 print (pydt_strftime(dt, accuracy = acc_seconds)) 2405 dt = dt.replace(year = 198) 2406 print (pydt_strftime(dt, accuracy = acc_seconds))
2407 #-------------------------------------------------
2408 - def test_is_leap_year():
2409 for year in range(1995, 2017): 2410 print (year, "leaps:", is_leap_year(year))
2411 2412 #-------------------------------------------------
2413 - def test_get_date_of_weekday_in_week_of_date():
2414 dt = pydt_now_here() 2415 print('weekday', base_dt.isoweekday(), '(2day):', dt) 2416 for weekday in range(8): 2417 dt = get_date_of_weekday_in_week_of_date(weekday) 2418 print('weekday', weekday, '(same):', dt) 2419 dt = get_date_of_weekday_following_date(weekday) 2420 print('weekday', weekday, '(next):', dt) 2421 try: 2422 get_date_of_weekday_in_week_of_date(8) 2423 except ValueError as exc: 2424 print(exc) 2425 try: 2426 get_date_of_weekday_following_date(8) 2427 except ValueError as exc: 2428 print(exc)
2429 2430 #------------------------------------------------- 2431 # GNUmed libs 2432 gmI18N.activate_locale() 2433 gmI18N.install_domain('gnumed') 2434 2435 init() 2436 2437 #test_date_time() 2438 #test_str2fuzzy_timestamp_matches() 2439 #test_get_date_of_weekday_in_week_of_date() 2440 #test_cFuzzyTimeStamp() 2441 #test_get_pydt() 2442 #test_str2interval() 2443 #test_format_interval() 2444 #test_format_interval_medically() 2445 #test_str2pydt() 2446 #test_pydt_strftime() 2447 #test_calculate_apparent_age() 2448 #test_is_leap_year() 2449 2450 #=========================================================================== 2451