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

Source Code for Module Gnumed.pycommon.gmDateTime

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