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