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