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