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(weeks = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0])) 793 794 # x/7 - days 795 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*7$', str_interval, flags = regex.LOCALE | regex.UNICODE): 796 return pyDT.timedelta(days = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0])) 797 798 # x/24 - hours 799 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*24$', str_interval, flags = regex.LOCALE | regex.UNICODE): 800 return pyDT.timedelta(hours = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0])) 801 802 # x/60 - minutes 803 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*60$', str_interval, flags = regex.LOCALE | regex.UNICODE): 804 return pyDT.timedelta(minutes = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0])) 805 806 # nYnM - years, months 807 keys_year = '|'.join(list(unit_keys['year'].replace('_keys_year', u''))) 808 keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', u''))) 809 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): 810 parts = regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE) 811 years, months = divmod(int(parts[1]), 12) 812 years += int(parts[0]) 813 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month))) 814 815 # nMnW - months, weeks 816 keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', u''))) 817 keys_week = '|'.join(list(unit_keys['week'].replace('_keys_week', u''))) 818 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): 819 parts = regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE) 820 months, weeks = divmod(int(parts[1]), 4) 821 months += int(parts[0]) 822 return pyDT.timedelta(days = ((months * avg_days_per_gregorian_month) + (weeks * days_per_week))) 823 824 return None
825 #=========================================================================== 826 # string -> date parser 827 #---------------------------------------------------------------------------
828 -def __single_char2py_dt(str2parse, trigger_chars=None):
829 """This matches on single characters. 830 831 Spaces and tabs are discarded. 832 833 Default is 'ndmy': 834 n - _N_ow 835 d - to_D_ay 836 m - to_M_orrow Someone please suggest a synonym ! ("2" does not cut it ...) 837 y - _Y_esterday 838 839 This also defines the significance of the order of the characters. 840 """ 841 if trigger_chars is None: 842 trigger_chars = _('ndmy (single character date triggers)')[:4].lower() 843 844 str2parse = str2parse.strip().lower() 845 846 if len(str2parse) != 1: 847 return [] 848 849 if str2parse not in trigger_chars: 850 return [] 851 852 now = mxDT.now() 853 enc = gmI18N.get_encoding() 854 855 # FIXME: handle uebermorgen/vorgestern ? 856 857 # right now 858 if str2parse == trigger_chars[0]: 859 return [{ 860 'data': mxdt2py_dt(now), 861 'label': _('right now (%s, %s)') % (now.strftime('%A').decode(enc), now) 862 }] 863 864 # today 865 if str2parse == trigger_chars[1]: 866 return [{ 867 'data': mxdt2py_dt(now), 868 'label': _('today (%s)') % now.strftime('%A, %Y-%m-%d').decode(enc) 869 }] 870 871 # tomorrow 872 if str2parse == trigger_chars[2]: 873 ts = now + mxDT.RelativeDateTime(days = +1) 874 return [{ 875 'data': mxdt2py_dt(ts), 876 'label': _('tomorrow (%s)') % ts.strftime('%A, %Y-%m-%d').decode(enc) 877 }] 878 879 # yesterday 880 if str2parse == trigger_chars[3]: 881 ts = now + mxDT.RelativeDateTime(days = -1) 882 return [{ 883 'data': mxdt2py_dt(ts), 884 'label': _('yesterday (%s)') % ts.strftime('%A, %Y-%m-%d').decode(enc) 885 }] 886 887 return []
888 #---------------------------------------------------------------------------
889 -def __single_dot2py_dt(str2parse):
890 """Expand fragments containing a single dot. 891 892 Standard colloquial date format in Germany: day.month.year 893 894 "14." 895 - the 14th of the current month 896 - the 14th of next month 897 "-14." 898 - the 14th of last month 899 """ 900 str2parse = str2parse.strip() 901 902 if not str2parse.endswith(u'.'): 903 return [] 904 905 str2parse = str2parse[:-1] 906 try: 907 day_val = int(str2parse) 908 except ValueError: 909 return [] 910 911 if (day_val < -31) or (day_val > 31) or (day_val == 0): 912 return [] 913 914 now = mxDT.now() 915 enc = gmI18N.get_encoding() 916 matches = [] 917 918 # day X of last month only 919 if day_val < 0: 920 ts = now + mxDT.RelativeDateTime(day = abs(day_val), months = -1) 921 if abs(day_val) <= gregorian_month_length[ts.month]: 922 matches.append ({ 923 'data': mxdt2py_dt(ts), 924 'label': _('%s-%s-%s: a %s last month') % (ts.year, ts.month, ts.day, ts.strftime('%A').decode(enc)) 925 }) 926 927 # day X of this month 928 if day_val > 0: 929 ts = now + mxDT.RelativeDateTime(day = day_val) 930 if day_val <= gregorian_month_length[ts.month]: 931 matches.append ({ 932 'data': mxdt2py_dt(ts), 933 'label': _('%s-%s-%s: a %s this month') % (ts.year, ts.month, ts.day, ts.strftime('%A').decode(enc)) 934 }) 935 936 # day X of next month 937 if day_val > 0: 938 ts = now + mxDT.RelativeDateTime(day = day_val, months = +1) 939 if day_val <= gregorian_month_length[ts.month]: 940 matches.append ({ 941 'data': mxdt2py_dt(ts), 942 'label': _('%s-%s-%s: a %s next month') % (ts.year, ts.month, ts.day, ts.strftime('%A').decode(enc)) 943 }) 944 945 # day X of last month 946 if day_val > 0: 947 ts = now + mxDT.RelativeDateTime(day = day_val, months = -1) 948 if day_val <= gregorian_month_length[ts.month]: 949 matches.append ({ 950 'data': mxdt2py_dt(ts), 951 'label': _('%s-%s-%s: a %s last month') % (ts.year, ts.month, ts.day, ts.strftime('%A').decode(enc)) 952 }) 953 954 return matches
955 #---------------------------------------------------------------------------
956 -def __single_slash2py_dt(str2parse):
957 """Expand fragments containing a single slash. 958 959 "5/" 960 - 2005/ (2000 - 2025) 961 - 1995/ (1990 - 1999) 962 - Mai/current year 963 - Mai/next year 964 - Mai/last year 965 - Mai/200x 966 - Mai/20xx 967 - Mai/199x 968 - Mai/198x 969 - Mai/197x 970 - Mai/19xx 971 972 5/1999 973 6/2004 974 """ 975 str2parse = str2parse.strip() 976 977 now = mxDT.now() 978 enc = gmI18N.get_encoding() 979 980 # 5/1999 981 if regex.match(r"^\d{1,2}(\s|\t)*/+(\s|\t)*\d{4}$", str2parse, flags = regex.LOCALE | regex.UNICODE): 982 parts = regex.findall(r'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE) 983 ts = now + mxDT.RelativeDateTime(year = int(parts[1]), month = int(parts[0])) 984 return [{ 985 'data': mxdt2py_dt(ts), 986 'label': ts.strftime('%Y-%m-%d').decode(enc) 987 }] 988 989 matches = [] 990 # 5/ 991 if regex.match(r"^\d{1,2}(\s|\t)*/+$", str2parse, flags = regex.LOCALE | regex.UNICODE): 992 val = int(str2parse.rstrip(u'/').strip()) 993 994 # "55/" -> "1955" 995 if val < 100 and val >= 0: 996 matches.append ({ 997 'data': None, 998 'label': '%s-' % (val + 1900) 999 }) 1000 1001 # "11/" -> "2011" 1002 if val < 26 and val >= 0: 1003 matches.append ({ 1004 'data': None, 1005 'label': '%s-' % (val + 2000) 1006 }) 1007 1008 # "5/" -> "1995" 1009 if val < 10 and val >= 0: 1010 matches.append ({ 1011 'data': None, 1012 'label': '%s-' % (val + 1990) 1013 }) 1014 1015 if val < 13 and val > 0: 1016 # "11/" -> "11/this year" 1017 matches.append ({ 1018 'data': None, 1019 'label': '%s-%.2d-' % (now.year, val) 1020 }) 1021 # "11/" -> "11/next year" 1022 ts = now + mxDT.RelativeDateTime(years = 1) 1023 matches.append ({ 1024 'data': None, 1025 'label': '%s-%.2d-' % (ts.year, val) 1026 }) 1027 # "11/" -> "11/last year" 1028 ts = now + mxDT.RelativeDateTime(years = -1) 1029 matches.append ({ 1030 'data': None, 1031 'label': '%s-%.2d-' % (ts.year, val) 1032 }) 1033 # "11/" -> "201?-11-" 1034 matches.append ({ 1035 'data': None, 1036 'label': '201?-%.2d-' % val 1037 }) 1038 # "11/" -> "200?-11-" 1039 matches.append ({ 1040 'data': None, 1041 'label': '200?-%.2d-' % val 1042 }) 1043 # "11/" -> "20??-11-" 1044 matches.append ({ 1045 'data': None, 1046 'label': '20??-%.2d-' % val 1047 }) 1048 # "11/" -> "199?-11-" 1049 matches.append ({ 1050 'data': None, 1051 'label': '199?-%.2d-' % val 1052 }) 1053 # "11/" -> "198?-11-" 1054 matches.append ({ 1055 'data': None, 1056 'label': '198?-%.2d-' % val 1057 }) 1058 # "11/" -> "198?-11-" 1059 matches.append ({ 1060 'data': None, 1061 'label': '197?-%.2d-' % val 1062 }) 1063 # "11/" -> "19??-11-" 1064 matches.append ({ 1065 'data': None, 1066 'label': '19??-%.2d-' % val 1067 }) 1068 1069 return matches
1070 #---------------------------------------------------------------------------
1071 -def __numbers_only2py_dt(str2parse):
1072 """This matches on single numbers. 1073 1074 Spaces or tabs are discarded. 1075 """ 1076 try: 1077 val = int(str2parse.strip()) 1078 except ValueError: 1079 return [] 1080 1081 # strftime() returns str but in the localized encoding, 1082 # so we may need to decode that to unicode 1083 enc = gmI18N.get_encoding() 1084 now = mxDT.now() 1085 1086 matches = [] 1087 1088 # that year 1089 if (1850 < val) and (val < 2100): 1090 ts = now + mxDT.RelativeDateTime(year = val) 1091 matches.append ({ 1092 'data': mxdt2py_dt(ts), 1093 'label': ts.strftime('%Y-%m-%d') 1094 }) 1095 1096 # day X of this month 1097 if (val > 0) and (val <= gregorian_month_length[now.month]): 1098 ts = now + mxDT.RelativeDateTime(day = val) 1099 matches.append ({ 1100 'data': mxdt2py_dt(ts), 1101 'label': _('%d. of %s (this month): a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc)) 1102 }) 1103 1104 # day X of next month 1105 if (val > 0) and (val < 32): 1106 ts = now + mxDT.RelativeDateTime(months = 1, day = val) 1107 matches.append ({ 1108 'data': mxdt2py_dt(ts), 1109 'label': _('%d. of %s (next month): a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc)) 1110 }) 1111 1112 # day X of last month 1113 if (val > 0) and (val < 32): 1114 ts = now + mxDT.RelativeDateTime(months = -1, day = val) 1115 matches.append ({ 1116 'data': mxdt2py_dt(ts), 1117 'label': _('%d. of %s (last month): a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc)) 1118 }) 1119 1120 # X days from now 1121 if (val > 0) and (val <= 400): # more than a year ahead in days ?? nah ! 1122 ts = now + mxDT.RelativeDateTime(days = val) 1123 matches.append ({ 1124 'data': mxdt2py_dt(ts), 1125 'label': _('in %d day(s): %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1126 }) 1127 if (val < 0) and (val >= -400): # more than a year back in days ?? nah ! 1128 ts = now - mxDT.RelativeDateTime(days = abs(val)) 1129 matches.append ({ 1130 'data': mxdt2py_dt(ts), 1131 'label': _('%d day(s) ago: %s') % (abs(val), ts.strftime('%A, %Y-%m-%d').decode(enc)) 1132 }) 1133 1134 # X weeks from now 1135 if (val > 0) and (val <= 50): # pregnancy takes about 40 weeks :-) 1136 ts = now + mxDT.RelativeDateTime(weeks = val) 1137 matches.append ({ 1138 'data': mxdt2py_dt(ts), 1139 'label': _('in %d week(s): %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1140 }) 1141 if (val < 0) and (val >= -50): # pregnancy takes about 40 weeks :-) 1142 ts = now - mxDT.RelativeDateTime(weeks = abs(val)) 1143 matches.append ({ 1144 'data': mxdt2py_dt(ts), 1145 'label': _('%d week(s) ago: %s') % (abs(val), ts.strftime('%A, %Y-%m-%d').decode(enc)) 1146 }) 1147 1148 # month X of ... 1149 if (val < 13) and (val > 0): 1150 # ... this year 1151 ts = now + mxDT.RelativeDateTime(month = val) 1152 matches.append ({ 1153 'data': mxdt2py_dt(ts), 1154 'label': _('%s (%s this year)') % (ts.strftime('%Y-%m-%d'), ts.strftime('%B').decode(enc)) 1155 }) 1156 1157 # ... next year 1158 ts = now + mxDT.RelativeDateTime(years = 1, month = val) 1159 matches.append ({ 1160 'data': mxdt2py_dt(ts), 1161 'label': _('%s (%s next year)') % (ts.strftime('%Y-%m-%d'), ts.strftime('%B').decode(enc)) 1162 }) 1163 1164 # ... last year 1165 ts = now + mxDT.RelativeDateTime(years = -1, month = val) 1166 matches.append ({ 1167 'data': mxdt2py_dt(ts), 1168 'label': _('%s (%s last year)') % (ts.strftime('%Y-%m-%d'), ts.strftime('%B').decode(enc)) 1169 }) 1170 1171 # fragment expansion 1172 matches.append ({ 1173 'data': None, 1174 'label': '200?-%s' % val 1175 }) 1176 matches.append ({ 1177 'data': None, 1178 'label': '199?-%s' % val 1179 }) 1180 matches.append ({ 1181 'data': None, 1182 'label': '198?-%s' % val 1183 }) 1184 matches.append ({ 1185 'data': None, 1186 'label': '19??-%s' % val 1187 }) 1188 1189 # day X of ... 1190 if (val < 8) and (val > 0): 1191 # ... this week 1192 ts = now + mxDT.RelativeDateTime(weekday = (val-1, 0)) 1193 matches.append ({ 1194 'data': mxdt2py_dt(ts), 1195 'label': _('%s this week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc)) 1196 }) 1197 1198 # ... next week 1199 ts = now + mxDT.RelativeDateTime(weeks = +1, weekday = (val-1, 0)) 1200 matches.append ({ 1201 'data': mxdt2py_dt(ts), 1202 'label': _('%s next week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc)) 1203 }) 1204 1205 # ... last week 1206 ts = now + mxDT.RelativeDateTime(weeks = -1, weekday = (val-1, 0)) 1207 matches.append ({ 1208 'data': mxdt2py_dt(ts), 1209 'label': _('%s last week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc)) 1210 }) 1211 1212 if (val < 100) and (val > 0): 1213 matches.append ({ 1214 'data': None, 1215 'label': '%s-' % (1900 + val) 1216 }) 1217 1218 if val == 201: 1219 tmp = { 1220 'data': mxdt2py_dt(now), 1221 'label': now.strftime('%Y-%m-%d') 1222 } 1223 matches.append(tmp) 1224 matches.append ({ 1225 'data': None, 1226 'label': now.strftime('%Y-%m') 1227 }) 1228 matches.append ({ 1229 'data': None, 1230 'label': now.strftime('%Y') 1231 }) 1232 matches.append ({ 1233 'data': None, 1234 'label': '%s-' % (now.year + 1) 1235 }) 1236 matches.append ({ 1237 'data': None, 1238 'label': '%s-' % (now.year - 1) 1239 }) 1240 1241 if val < 200 and val >= 190: 1242 for i in range(10): 1243 matches.append ({ 1244 'data': None, 1245 'label': '%s%s-' % (val, i) 1246 }) 1247 1248 return matches
1249 #---------------------------------------------------------------------------
1250 -def __explicit_offset2py_dt(str2parse, offset_chars=None):
1251 """ 1252 Default is 'hdwmy': 1253 h - hours 1254 d - days 1255 w - weeks 1256 m - months 1257 y - years 1258 1259 This also defines the significance of the order of the characters. 1260 """ 1261 if offset_chars is None: 1262 offset_chars = _('hdwmy (single character date offset triggers)')[:5].lower() 1263 1264 str2parse = str2parse.strip() 1265 1266 # "+/-XXd/w/m/t" 1267 if not regex.match(r"^(\+|-)?(\s|\t)*\d{1,3}(\s|\t)*[%s]$" % offset_chars, str2parse, flags = regex.LOCALE | regex.UNICODE): 1268 return [] 1269 1270 # into the past ? 1271 if str2parse.startswith(u'-'): 1272 is_future = False 1273 str2parse = str2parse[1:].strip() 1274 else: 1275 is_future = True 1276 str2parse = str2parse.replace(u'+', u'').strip() 1277 1278 val = int(regex.findall(u'\d{1,3}', str2parse, flags = regex.LOCALE | regex.UNICODE)[0]) 1279 offset_char = regex.findall(u'[%s]' % offset_chars, str2parse, flags = regex.LOCALE | regex.UNICODE)[0].lower() 1280 1281 now = mxDT.now() 1282 enc = gmI18N.get_encoding() 1283 1284 ts = None 1285 # hours 1286 if offset_char == offset_chars[0]: 1287 if is_future: 1288 ts = now + mxDT.RelativeDateTime(hours = val) 1289 label = _('in %d hour(s): %s') % (val, ts.strftime('%H:%M')) 1290 else: 1291 ts = now - mxDT.RelativeDateTime(hours = val) 1292 label = _('%d hour(s) ago: %s') % (val, ts.strftime('%H:%M')) 1293 # days 1294 elif offset_char == offset_chars[1]: 1295 if is_future: 1296 ts = now + mxDT.RelativeDateTime(days = val) 1297 label = _('in %d day(s): %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1298 else: 1299 ts = now - mxDT.RelativeDateTime(days = val) 1300 label = _('%d day(s) ago: %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1301 # weeks 1302 elif offset_char == offset_chars[2]: 1303 if is_future: 1304 ts = now + mxDT.RelativeDateTime(weeks = val) 1305 label = _('in %d week(s): %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1306 else: 1307 ts = now - mxDT.RelativeDateTime(weeks = val) 1308 label = _('%d week(s) ago: %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1309 # months 1310 elif offset_char == offset_chars[3]: 1311 if is_future: 1312 ts = now + mxDT.RelativeDateTime(months = val) 1313 label = _('in %d month(s): %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1314 else: 1315 ts = now - mxDT.RelativeDateTime(months = val) 1316 label = _('%d month(s) ago: %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1317 # years 1318 elif offset_char == offset_chars[4]: 1319 if is_future: 1320 ts = now + mxDT.RelativeDateTime(years = val) 1321 label = _('in %d year(s): %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1322 else: 1323 ts = now - mxDT.RelativeDateTime(years = val) 1324 label = _('%d year(s) ago: %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1325 1326 if ts is None: 1327 return [] 1328 1329 return [{ 1330 'data': mxdt2py_dt(ts), 1331 'label': label 1332 }]
1333 #---------------------------------------------------------------------------
1334 -def str2pydt_matches(str2parse=None, patterns=None):
1335 """Turn a string into candidate dates and auto-completions the user is likely to type. 1336 1337 You MUST have called locale.setlocale(locale.LC_ALL, '') 1338 somewhere in your code previously. 1339 1340 @param patterns: list of time.strptime compatible date pattern 1341 @type patterns: list 1342 """ 1343 matches = [] 1344 matches.extend(__single_dot2py_dt(str2parse)) 1345 matches.extend(__numbers_only2py_dt(str2parse)) 1346 matches.extend(__single_slash2py_dt(str2parse)) 1347 matches.extend(__single_char2py_dt(str2parse)) 1348 matches.extend(__explicit_offset2py_dt(str2parse)) 1349 1350 # try mxDT parsers 1351 try: 1352 date = mxDT.Parser.DateFromString ( 1353 text = str2parse, 1354 formats = ('euro', 'iso', 'us', 'altus', 'altiso', 'lit', 'altlit', 'eurlit') 1355 ) 1356 matches.append ({ 1357 'data': mxdt2py_dt(date), 1358 'label': date.strftime('%Y-%m-%d') 1359 }) 1360 except (ValueError, OverflowError, mxDT.RangeError): 1361 pass 1362 1363 # apply explicit patterns 1364 if patterns is None: 1365 patterns = [] 1366 1367 patterns.append('%Y-%m-%d') 1368 patterns.append('%y-%m-%d') 1369 patterns.append('%Y/%m/%d') 1370 patterns.append('%y/%m/%d') 1371 1372 patterns.append('%d-%m-%Y') 1373 patterns.append('%d-%m-%y') 1374 patterns.append('%d/%m/%Y') 1375 patterns.append('%d/%m/%y') 1376 1377 patterns.append('%m-%d-%Y') 1378 patterns.append('%m-%d-%y') 1379 patterns.append('%m/%d/%Y') 1380 patterns.append('%m/%d/%y') 1381 1382 patterns.append('%Y.%m.%d') 1383 patterns.append('%y.%m.%d') 1384 1385 for pattern in patterns: 1386 try: 1387 date = pyDT.datetime.strptime(str2parse, pattern).replace ( 1388 hour = 11, 1389 minute = 11, 1390 second = 11, 1391 tzinfo = gmCurrentLocalTimezone 1392 ) 1393 matches.append ({ 1394 'data': date, 1395 'label': pydt_strftime(date, format = '%Y-%m-%d', accuracy = acc_days) 1396 }) 1397 except AttributeError: 1398 # strptime() only available starting with Python 2.5 1399 break 1400 except OverflowError: 1401 # time.mktime() cannot handle dates older than a platform-dependant limit :-( 1402 continue 1403 except ValueError: 1404 # C-level overflow 1405 continue 1406 1407 return matches
1408 #=========================================================================== 1409 # string -> fuzzy timestamp parser 1410 #---------------------------------------------------------------------------
1411 -def __explicit_offset(str2parse, offset_chars=None):
1412 """ 1413 Default is 'hdwm': 1414 h - hours 1415 d - days 1416 w - weeks 1417 m - months 1418 y - years 1419 1420 This also defines the significance of the order of the characters. 1421 """ 1422 if offset_chars is None: 1423 offset_chars = _('hdwmy (single character date offset triggers)')[:5].lower() 1424 1425 # "+/-XXd/w/m/t" 1426 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): 1427 return [] 1428 val = int(regex.findall(u'\d{1,2}', str2parse, flags = regex.LOCALE | regex.UNICODE)[0]) 1429 offset_char = regex.findall(u'[%s]' % offset_chars, str2parse, flags = regex.LOCALE | regex.UNICODE)[0].lower() 1430 1431 now = mxDT.now() 1432 enc = gmI18N.get_encoding() 1433 1434 # allow past ? 1435 is_future = True 1436 if str2parse.find('-') > -1: 1437 is_future = False 1438 1439 ts = None 1440 # hours 1441 if offset_char == offset_chars[0]: 1442 if is_future: 1443 ts = now + mxDT.RelativeDateTime(hours = val) 1444 label = _('in %d hour(s) - %s') % (val, ts.strftime('%H:%M')) 1445 else: 1446 ts = now - mxDT.RelativeDateTime(hours = val) 1447 label = _('%d hour(s) ago - %s') % (val, ts.strftime('%H:%M')) 1448 accuracy = acc_subseconds 1449 # days 1450 elif offset_char == offset_chars[1]: 1451 if is_future: 1452 ts = now + mxDT.RelativeDateTime(days = val) 1453 label = _('in %d day(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1454 else: 1455 ts = now - mxDT.RelativeDateTime(days = val) 1456 label = _('%d day(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1457 accuracy = acc_days 1458 # weeks 1459 elif offset_char == offset_chars[2]: 1460 if is_future: 1461 ts = now + mxDT.RelativeDateTime(weeks = val) 1462 label = _('in %d week(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1463 else: 1464 ts = now - mxDT.RelativeDateTime(weeks = val) 1465 label = _('%d week(s) ago - %s)') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1466 accuracy = acc_days 1467 # months 1468 elif offset_char == offset_chars[3]: 1469 if is_future: 1470 ts = now + mxDT.RelativeDateTime(months = val) 1471 label = _('in %d month(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1472 else: 1473 ts = now - mxDT.RelativeDateTime(months = val) 1474 label = _('%d month(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1475 accuracy = acc_days 1476 # years 1477 elif offset_char == offset_chars[4]: 1478 if is_future: 1479 ts = now + mxDT.RelativeDateTime(years = val) 1480 label = _('in %d year(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1481 else: 1482 ts = now - mxDT.RelativeDateTime(years = val) 1483 label = _('%d year(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 1484 accuracy = acc_months 1485 1486 if ts is None: 1487 return [] 1488 1489 tmp = { 1490 'data': cFuzzyTimestamp(timestamp = ts, accuracy = accuracy), 1491 'label': label 1492 } 1493 return [tmp]
1494 #---------------------------------------------------------------------------
1495 -def __single_slash(str2parse):
1496 """Expand fragments containing a single slash. 1497 1498 "5/" 1499 - 2005/ (2000 - 2025) 1500 - 1995/ (1990 - 1999) 1501 - Mai/current year 1502 - Mai/next year 1503 - Mai/last year 1504 - Mai/200x 1505 - Mai/20xx 1506 - Mai/199x 1507 - Mai/198x 1508 - Mai/197x 1509 - Mai/19xx 1510 """ 1511 matches = [] 1512 now = mxDT.now() 1513 if regex.match(u"^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE): 1514 val = int(regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE)[0]) 1515 1516 if val < 100 and val >= 0: 1517 matches.append ({ 1518 'data': None, 1519 'label': '%s/' % (val + 1900) 1520 }) 1521 1522 if val < 26 and val >= 0: 1523 matches.append ({ 1524 'data': None, 1525 'label': '%s/' % (val + 2000) 1526 }) 1527 1528 if val < 10 and val >= 0: 1529 matches.append ({ 1530 'data': None, 1531 'label': '%s/' % (val + 1990) 1532 }) 1533 1534 if val < 13 and val > 0: 1535 matches.append ({ 1536 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months), 1537 'label': '%.2d/%s' % (val, now.year) 1538 }) 1539 ts = now + mxDT.RelativeDateTime(years = 1) 1540 matches.append ({ 1541 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months), 1542 'label': '%.2d/%s' % (val, ts.year) 1543 }) 1544 ts = now + mxDT.RelativeDateTime(years = -1) 1545 matches.append ({ 1546 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months), 1547 'label': '%.2d/%s' % (val, ts.year) 1548 }) 1549 matches.append ({ 1550 'data': None, 1551 'label': '%.2d/200' % val 1552 }) 1553 matches.append ({ 1554 'data': None, 1555 'label': '%.2d/20' % val 1556 }) 1557 matches.append ({ 1558 'data': None, 1559 'label': '%.2d/199' % val 1560 }) 1561 matches.append ({ 1562 'data': None, 1563 'label': '%.2d/198' % val 1564 }) 1565 matches.append ({ 1566 'data': None, 1567 'label': '%.2d/197' % val 1568 }) 1569 matches.append ({ 1570 'data': None, 1571 'label': '%.2d/19' % val 1572 }) 1573 1574 elif regex.match(u"^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*\d{4}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE): 1575 parts = regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE) 1576 fts = cFuzzyTimestamp ( 1577 timestamp = mxDT.now() + mxDT.RelativeDateTime(year = int(parts[1]), month = int(parts[0])), 1578 accuracy = acc_months 1579 ) 1580 matches.append ({ 1581 'data': fts, 1582 'label': fts.format_accurately() 1583 }) 1584 1585 return matches
1586 #---------------------------------------------------------------------------
1587 -def __numbers_only(str2parse):
1588 """This matches on single numbers. 1589 1590 Spaces or tabs are discarded. 1591 """ 1592 if not regex.match(u"^(\s|\t)*\d{1,4}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE): 1593 return [] 1594 1595 # strftime() returns str but in the localized encoding, 1596 # so we may need to decode that to unicode 1597 enc = gmI18N.get_encoding() 1598 now = mxDT.now() 1599 val = int(regex.findall(u'\d{1,4}', str2parse, flags = regex.LOCALE | regex.UNICODE)[0]) 1600 1601 matches = [] 1602 1603 # that year 1604 if (1850 < val) and (val < 2100): 1605 ts = now + mxDT.RelativeDateTime(year = val) 1606 target_date = cFuzzyTimestamp ( 1607 timestamp = ts, 1608 accuracy = acc_years 1609 ) 1610 tmp = { 1611 'data': target_date, 1612 'label': '%s' % target_date 1613 } 1614 matches.append(tmp) 1615 1616 # day X of this month 1617 if val <= gregorian_month_length[now.month]: 1618 ts = now + mxDT.RelativeDateTime(day = val) 1619 target_date = cFuzzyTimestamp ( 1620 timestamp = ts, 1621 accuracy = acc_days 1622 ) 1623 tmp = { 1624 'data': target_date, 1625 'label': _('%d. of %s (this month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc)) 1626 } 1627 matches.append(tmp) 1628 1629 # day X of next month 1630 if val <= gregorian_month_length[(now + mxDT.RelativeDateTime(months = 1)).month]: 1631 ts = now + mxDT.RelativeDateTime(months = 1, day = val) 1632 target_date = cFuzzyTimestamp ( 1633 timestamp = ts, 1634 accuracy = acc_days 1635 ) 1636 tmp = { 1637 'data': target_date, 1638 'label': _('%d. of %s (next month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc)) 1639 } 1640 matches.append(tmp) 1641 1642 # day X of last month 1643 if val <= gregorian_month_length[(now + mxDT.RelativeDateTime(months = -1)).month]: 1644 ts = now + mxDT.RelativeDateTime(months = -1, day = val) 1645 target_date = cFuzzyTimestamp ( 1646 timestamp = ts, 1647 accuracy = acc_days 1648 ) 1649 tmp = { 1650 'data': target_date, 1651 'label': _('%d. of %s (last month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc)) 1652 } 1653 matches.append(tmp) 1654 1655 # X days from now 1656 if val <= 400: # more than a year ahead in days ?? nah ! 1657 ts = now + mxDT.RelativeDateTime(days = val) 1658 target_date = cFuzzyTimestamp ( 1659 timestamp = ts 1660 ) 1661 tmp = { 1662 'data': target_date, 1663 'label': _('in %d day(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d').decode(enc)) 1664 } 1665 matches.append(tmp) 1666 1667 # X weeks from now 1668 if val <= 50: # pregnancy takes about 40 weeks :-) 1669 ts = now + mxDT.RelativeDateTime(weeks = val) 1670 target_date = cFuzzyTimestamp ( 1671 timestamp = ts 1672 ) 1673 tmp = { 1674 'data': target_date, 1675 'label': _('in %d week(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d').decode(enc)) 1676 } 1677 matches.append(tmp) 1678 1679 # month X of ... 1680 if val < 13: 1681 # ... this year 1682 ts = now + mxDT.RelativeDateTime(month = val) 1683 target_date = cFuzzyTimestamp ( 1684 timestamp = ts, 1685 accuracy = acc_months 1686 ) 1687 tmp = { 1688 'data': target_date, 1689 'label': _('%s (%s this year)') % (target_date, ts.strftime('%B').decode(enc)) 1690 } 1691 matches.append(tmp) 1692 1693 # ... next year 1694 ts = now + mxDT.RelativeDateTime(years = 1, month = val) 1695 target_date = cFuzzyTimestamp ( 1696 timestamp = ts, 1697 accuracy = acc_months 1698 ) 1699 tmp = { 1700 'data': target_date, 1701 'label': _('%s (%s next year)') % (target_date, ts.strftime('%B').decode(enc)) 1702 } 1703 matches.append(tmp) 1704 1705 # ... last year 1706 ts = now + mxDT.RelativeDateTime(years = -1, month = val) 1707 target_date = cFuzzyTimestamp ( 1708 timestamp = ts, 1709 accuracy = acc_months 1710 ) 1711 tmp = { 1712 'data': target_date, 1713 'label': _('%s (%s last year)') % (target_date, ts.strftime('%B').decode(enc)) 1714 } 1715 matches.append(tmp) 1716 1717 # fragment expansion 1718 matches.append ({ 1719 'data': None, 1720 'label': '%s/200' % val 1721 }) 1722 matches.append ({ 1723 'data': None, 1724 'label': '%s/199' % val 1725 }) 1726 matches.append ({ 1727 'data': None, 1728 'label': '%s/198' % val 1729 }) 1730 matches.append ({ 1731 'data': None, 1732 'label': '%s/19' % val 1733 }) 1734 1735 # day X of ... 1736 if val < 8: 1737 # ... this week 1738 ts = now + mxDT.RelativeDateTime(weekday = (val-1, 0)) 1739 target_date = cFuzzyTimestamp ( 1740 timestamp = ts, 1741 accuracy = acc_days 1742 ) 1743 tmp = { 1744 'data': target_date, 1745 'label': _('%s this week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc)) 1746 } 1747 matches.append(tmp) 1748 1749 # ... next week 1750 ts = now + mxDT.RelativeDateTime(weeks = +1, weekday = (val-1, 0)) 1751 target_date = cFuzzyTimestamp ( 1752 timestamp = ts, 1753 accuracy = acc_days 1754 ) 1755 tmp = { 1756 'data': target_date, 1757 'label': _('%s next week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc)) 1758 } 1759 matches.append(tmp) 1760 1761 # ... last week 1762 ts = now + mxDT.RelativeDateTime(weeks = -1, weekday = (val-1, 0)) 1763 target_date = cFuzzyTimestamp ( 1764 timestamp = ts, 1765 accuracy = acc_days 1766 ) 1767 tmp = { 1768 'data': target_date, 1769 'label': _('%s last week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc)) 1770 } 1771 matches.append(tmp) 1772 1773 if val < 100: 1774 matches.append ({ 1775 'data': None, 1776 'label': '%s/' % (1900 + val) 1777 }) 1778 1779 if val == 200: 1780 tmp = { 1781 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_days), 1782 'label': '%s' % target_date 1783 } 1784 matches.append(tmp) 1785 matches.append ({ 1786 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months), 1787 'label': '%.2d/%s' % (now.month, now.year) 1788 }) 1789 matches.append ({ 1790 'data': None, 1791 'label': '%s/' % now.year 1792 }) 1793 matches.append ({ 1794 'data': None, 1795 'label': '%s/' % (now.year + 1) 1796 }) 1797 matches.append ({ 1798 'data': None, 1799 'label': '%s/' % (now.year - 1) 1800 }) 1801 1802 if val < 200 and val >= 190: 1803 for i in range(10): 1804 matches.append ({ 1805 'data': None, 1806 'label': '%s%s/' % (val, i) 1807 }) 1808 1809 return matches
1810 #---------------------------------------------------------------------------
1811 -def __single_dot(str2parse):
1812 """Expand fragments containing a single dot. 1813 1814 Standard colloquial date format in Germany: day.month.year 1815 1816 "14." 1817 - 14th current month this year 1818 - 14th next month this year 1819 """ 1820 if not regex.match(u"^(\s|\t)*\d{1,2}\.{1}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE): 1821 return [] 1822 1823 val = int(regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE)[0]) 1824 now = mxDT.now() 1825 enc = gmI18N.get_encoding() 1826 1827 matches = [] 1828 1829 # day X of this month 1830 ts = now + mxDT.RelativeDateTime(day = val) 1831 if val > 0 and val <= gregorian_month_length[ts.month]: 1832 matches.append ({ 1833 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days), 1834 'label': '%s.%s.%s - a %s this month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc)) 1835 }) 1836 1837 # day X of next month 1838 ts = now + mxDT.RelativeDateTime(day = val, months = +1) 1839 if val > 0 and val <= gregorian_month_length[ts.month]: 1840 matches.append ({ 1841 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days), 1842 'label': '%s.%s.%s - a %s next month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc)) 1843 }) 1844 1845 # day X of last month 1846 ts = now + mxDT.RelativeDateTime(day = val, months = -1) 1847 if val > 0 and val <= gregorian_month_length[ts.month]: 1848 matches.append ({ 1849 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days), 1850 'label': '%s.%s.%s - a %s last month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc)) 1851 }) 1852 1853 return matches
1854 #---------------------------------------------------------------------------
1855 -def str2fuzzy_timestamp_matches(str2parse=None, default_time=None, patterns=None):
1856 """ 1857 Turn a string into candidate fuzzy timestamps and auto-completions the user is likely to type. 1858 1859 You MUST have called locale.setlocale(locale.LC_ALL, '') 1860 somewhere in your code previously. 1861 1862 @param default_time: if you want to force the time part of the time 1863 stamp to a given value and the user doesn't type any time part 1864 this value will be used 1865 @type default_time: an mx.DateTime.DateTimeDelta instance 1866 1867 @param patterns: list of [time.strptime compatible date/time pattern, accuracy] 1868 @type patterns: list 1869 """ 1870 matches = __single_dot(str2parse) 1871 matches.extend(__numbers_only(str2parse)) 1872 matches.extend(__single_slash(str2parse)) 1873 ms = __single_char2py_dt(str2parse) 1874 for m in ms: 1875 matches.append ({ 1876 'data': cFuzzyTimestamp ( 1877 timestamp = m['data'], 1878 accuracy = acc_days 1879 ), 1880 'label': m['label'] 1881 }) 1882 matches.extend(__explicit_offset(str2parse)) 1883 1884 # try mxDT parsers 1885 try: 1886 # date ? 1887 date_only = mxDT.Parser.DateFromString ( 1888 text = str2parse, 1889 formats = ('euro', 'iso', 'us', 'altus', 'altiso', 'lit', 'altlit', 'eurlit') 1890 ) 1891 # time, too ? 1892 time_part = mxDT.Parser.TimeFromString(text = str2parse) 1893 datetime = date_only + time_part 1894 if datetime == date_only: 1895 accuracy = acc_days 1896 if isinstance(default_time, mxDT.DateTimeDeltaType): 1897 datetime = date_only + default_time 1898 accuracy = acc_minutes 1899 else: 1900 accuracy = acc_subseconds 1901 fts = cFuzzyTimestamp ( 1902 timestamp = datetime, 1903 accuracy = accuracy 1904 ) 1905 matches.append ({ 1906 'data': fts, 1907 'label': fts.format_accurately() 1908 }) 1909 except (ValueError, mxDT.RangeError): 1910 pass 1911 1912 if patterns is None: 1913 patterns = [] 1914 1915 patterns.append(['%Y-%m-%d', acc_days]) 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 1920 patterns.append(['%d-%m-%Y', acc_days]) 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 1925 patterns.append(['%m-%d-%Y', acc_days]) 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 1930 patterns.append(['%Y.%m.%d', acc_days]) 1931 patterns.append(['%y.%m.%d', acc_days]) 1932 1933 1934 for pattern in patterns: 1935 try: 1936 fts = cFuzzyTimestamp ( 1937 timestamp = pyDT.datetime.fromtimestamp(time.mktime(time.strptime(str2parse, pattern[0]))), 1938 accuracy = pattern[1] 1939 ) 1940 matches.append ({ 1941 'data': fts, 1942 'label': fts.format_accurately() 1943 }) 1944 except AttributeError: 1945 # strptime() only available starting with Python 2.5 1946 break 1947 except OverflowError: 1948 # time.mktime() cannot handle dates older than a platform-dependant limit :-( 1949 continue 1950 except ValueError: 1951 # C-level overflow 1952 continue 1953 1954 return matches
1955 #=========================================================================== 1956 # fuzzy timestamp class 1957 #---------------------------------------------------------------------------
1958 -class cFuzzyTimestamp:
1959 1960 # FIXME: add properties for year, month, ... 1961 1962 """A timestamp implementation with definable inaccuracy. 1963 1964 This class contains an mxDateTime.DateTime instance to 1965 hold the actual timestamp. It adds an accuracy attribute 1966 to allow the programmer to set the precision of the 1967 timestamp. 1968 1969 The timestamp will have to be initialzed with a fully 1970 precise value (which may, of course, contain partially 1971 fake data to make up for missing values). One can then 1972 set the accuracy value to indicate up to which part of 1973 the timestamp the data is valid. Optionally a modifier 1974 can be set to indicate further specification of the 1975 value (such as "summer", "afternoon", etc). 1976 1977 accuracy values: 1978 1: year only 1979 ... 1980 7: everything including milliseconds value 1981 1982 Unfortunately, one cannot directly derive a class from mx.DateTime.DateTime :-( 1983 """ 1984 #-----------------------------------------------------------------------
1985 - def __init__(self, timestamp=None, accuracy=acc_subseconds, modifier=''):
1986 1987 if timestamp is None: 1988 timestamp = mxDT.now() 1989 accuracy = acc_subseconds 1990 modifier = '' 1991 1992 if (accuracy < 1) or (accuracy > 8): 1993 raise ValueError('%s.__init__(): <accuracy> must be between 1 and 8' % self.__class__.__name__) 1994 1995 if isinstance(timestamp, pyDT.datetime): 1996 timestamp = mxDT.DateTime(timestamp.year, timestamp.month, timestamp.day, timestamp.hour, timestamp.minute, timestamp.second) 1997 1998 if type(timestamp) != mxDT.DateTimeType: 1999 raise TypeError('%s.__init__(): <timestamp> must be of mx.DateTime.DateTime or datetime.datetime type' % self.__class__.__name__) 2000 2001 self.timestamp = timestamp 2002 self.accuracy = accuracy 2003 self.modifier = modifier
2004 #----------------------------------------------------------------------- 2005 # magic API 2006 #-----------------------------------------------------------------------
2007 - def __str__(self):
2008 """Return string representation meaningful to a user, also for %s formatting.""" 2009 return self.format_accurately()
2010 #-----------------------------------------------------------------------
2011 - def __repr__(self):
2012 """Return string meaningful to a programmer to aid in debugging.""" 2013 tmp = '<[%s]: timestamp [%s], accuracy [%s] (%s), modifier [%s] at %s>' % ( 2014 self.__class__.__name__, 2015 repr(self.timestamp), 2016 self.accuracy, 2017 _accuracy_strings[self.accuracy], 2018 self.modifier, 2019 id(self) 2020 ) 2021 return tmp
2022 #----------------------------------------------------------------------- 2023 # external API 2024 #-----------------------------------------------------------------------
2025 - def strftime(self, format_string):
2026 if self.accuracy == 7: 2027 return self.timestamp.strftime(format_string) 2028 return self.format_accurately()
2029 #-----------------------------------------------------------------------
2030 - def Format(self, format_string):
2031 return self.strftime(format_string)
2032 #-----------------------------------------------------------------------
2033 - def format_accurately(self, accuracy=None):
2034 if accuracy is None: 2035 accuracy = self.accuracy 2036 2037 if accuracy == acc_years: 2038 return unicode(self.timestamp.year) 2039 2040 if accuracy == acc_months: 2041 return unicode(self.timestamp.strftime('%m/%Y')) # FIXME: use 3-letter month ? 2042 2043 if accuracy == acc_weeks: 2044 return unicode(self.timestamp.strftime('%m/%Y')) # FIXME: use 3-letter month ? 2045 2046 if accuracy == acc_days: 2047 return unicode(self.timestamp.strftime('%Y-%m-%d')) 2048 2049 if accuracy == acc_hours: 2050 return unicode(self.timestamp.strftime("%Y-%m-%d %I%p")) 2051 2052 if accuracy == acc_minutes: 2053 return unicode(self.timestamp.strftime("%Y-%m-%d %H:%M")) 2054 2055 if accuracy == acc_seconds: 2056 return unicode(self.timestamp.strftime("%Y-%m-%d %H:%M:%S")) 2057 2058 if accuracy == acc_subseconds: 2059 return unicode(self.timestamp) 2060 2061 raise ValueError, '%s.format_accurately(): <accuracy> (%s) must be between 1 and 7' % ( 2062 self.__class__.__name__, 2063 accuracy 2064 )
2065 #-----------------------------------------------------------------------
2066 - def get_mxdt(self):
2067 return self.timestamp
2068 #-----------------------------------------------------------------------
2069 - def get_pydt(self):
2070 try: 2071 gmtoffset = self.timestamp.gmtoffset() 2072 except mxDT.Error: 2073 # Windows cannot deal with dates < 1970, so 2074 # when that happens switch to now() 2075 now = mxDT.now() 2076 gmtoffset = now.gmtoffset() 2077 tz = cFixedOffsetTimezone(gmtoffset.minutes, self.timestamp.tz) 2078 secs, msecs = divmod(self.timestamp.second, 1) 2079 ts = pyDT.datetime ( 2080 year = self.timestamp.year, 2081 month = self.timestamp.month, 2082 day = self.timestamp.day, 2083 hour = self.timestamp.hour, 2084 minute = self.timestamp.minute, 2085 second = int(secs), 2086 microsecond = int(msecs * 1000), 2087 tzinfo = tz 2088 ) 2089 return ts
2090 #=========================================================================== 2091 # main 2092 #--------------------------------------------------------------------------- 2093 if __name__ == '__main__': 2094 2095 if len(sys.argv) < 2: 2096 sys.exit() 2097 2098 if sys.argv[1] != "test": 2099 sys.exit() 2100 2101 #----------------------------------------------------------------------- 2102 intervals_as_str = [ 2103 '7', '12', ' 12', '12 ', ' 12 ', ' 12 ', '0', '~12', '~ 12', ' ~ 12', ' ~ 12 ', 2104 '12a', '12 a', '12 a', '12j', '12J', '12y', '12Y', ' ~ 12 a ', '~0a', 2105 '12m', '17 m', '12 m', '17M', ' ~ 17 m ', ' ~ 3 / 12 ', '7/12', '0/12', 2106 '12w', '17 w', '12 w', '17W', ' ~ 17 w ', ' ~ 15 / 52', '2/52', '0/52', 2107 '12d', '17 d', '12 t', '17D', ' ~ 17 T ', ' ~ 12 / 7', '3/7', '0/7', 2108 '12h', '17 h', '12 H', '17H', ' ~ 17 h ', ' ~ 36 / 24', '7/24', '0/24', 2109 ' ~ 36 / 60', '7/60', '190/60', '0/60', 2110 '12a1m', '12 a 1 M', '12 a17m', '12j 12m', '12J7m', '12y7m', '12Y7M', ' ~ 12 a 37 m ', '~0a0m', 2111 '10m1w', 2112 'invalid interval input' 2113 ] 2114 #-----------------------------------------------------------------------
2115 - def test_format_interval():
2116 for tmp in intervals_as_str: 2117 intv = str2interval(str_interval = tmp) 2118 for acc in _accuracy_strings.keys(): 2119 print '[%s]: "%s" -> "%s"' % (acc, tmp, format_interval(intv, acc))
2120 #-----------------------------------------------------------------------
2121 - def test_format_interval_medically():
2122 2123 intervals = [ 2124 pyDT.timedelta(seconds = 1), 2125 pyDT.timedelta(seconds = 5), 2126 pyDT.timedelta(seconds = 30), 2127 pyDT.timedelta(seconds = 60), 2128 pyDT.timedelta(seconds = 94), 2129 pyDT.timedelta(seconds = 120), 2130 pyDT.timedelta(minutes = 5), 2131 pyDT.timedelta(minutes = 30), 2132 pyDT.timedelta(minutes = 60), 2133 pyDT.timedelta(minutes = 90), 2134 pyDT.timedelta(minutes = 120), 2135 pyDT.timedelta(minutes = 200), 2136 pyDT.timedelta(minutes = 400), 2137 pyDT.timedelta(minutes = 600), 2138 pyDT.timedelta(minutes = 800), 2139 pyDT.timedelta(minutes = 1100), 2140 pyDT.timedelta(minutes = 2000), 2141 pyDT.timedelta(minutes = 3500), 2142 pyDT.timedelta(minutes = 4000), 2143 pyDT.timedelta(hours = 1), 2144 pyDT.timedelta(hours = 2), 2145 pyDT.timedelta(hours = 4), 2146 pyDT.timedelta(hours = 8), 2147 pyDT.timedelta(hours = 12), 2148 pyDT.timedelta(hours = 20), 2149 pyDT.timedelta(hours = 23), 2150 pyDT.timedelta(hours = 24), 2151 pyDT.timedelta(hours = 25), 2152 pyDT.timedelta(hours = 30), 2153 pyDT.timedelta(hours = 48), 2154 pyDT.timedelta(hours = 98), 2155 pyDT.timedelta(hours = 120), 2156 pyDT.timedelta(days = 1), 2157 pyDT.timedelta(days = 2), 2158 pyDT.timedelta(days = 4), 2159 pyDT.timedelta(days = 16), 2160 pyDT.timedelta(days = 29), 2161 pyDT.timedelta(days = 30), 2162 pyDT.timedelta(days = 31), 2163 pyDT.timedelta(days = 37), 2164 pyDT.timedelta(days = 40), 2165 pyDT.timedelta(days = 47), 2166 pyDT.timedelta(days = 126), 2167 pyDT.timedelta(days = 127), 2168 pyDT.timedelta(days = 128), 2169 pyDT.timedelta(days = 300), 2170 pyDT.timedelta(days = 359), 2171 pyDT.timedelta(days = 360), 2172 pyDT.timedelta(days = 361), 2173 pyDT.timedelta(days = 362), 2174 pyDT.timedelta(days = 363), 2175 pyDT.timedelta(days = 364), 2176 pyDT.timedelta(days = 365), 2177 pyDT.timedelta(days = 366), 2178 pyDT.timedelta(days = 367), 2179 pyDT.timedelta(days = 400), 2180 pyDT.timedelta(weeks = 52 * 30), 2181 pyDT.timedelta(weeks = 52 * 79, days = 33) 2182 ] 2183 2184 idx = 1 2185 for intv in intervals: 2186 print '%s) %s -> %s' % (idx, intv, format_interval_medically(intv)) 2187 idx += 1
2188 #-----------------------------------------------------------------------
2189 - def test_str2interval():
2190 print "testing str2interval()" 2191 print "----------------------" 2192 2193 for interval_as_str in intervals_as_str: 2194 print "input: <%s>" % interval_as_str 2195 print " ==>", str2interval(str_interval=interval_as_str) 2196 2197 return True
2198 #-------------------------------------------------
2199 - def test_date_time():
2200 print "DST currently in effect:", dst_currently_in_effect 2201 print "current UTC offset:", current_local_utc_offset_in_seconds, "seconds" 2202 print "current timezone (interval):", current_local_timezone_interval 2203 print "current timezone (ISO conformant numeric string):", current_local_iso_numeric_timezone_string 2204 print "local timezone class:", cLocalTimezone 2205 print "" 2206 tz = cLocalTimezone() 2207 print "local timezone instance:", tz 2208 print " (total) UTC offset:", tz.utcoffset(pyDT.datetime.now()) 2209 print " DST adjustment:", tz.dst(pyDT.datetime.now()) 2210 print " timezone name:", tz.tzname(pyDT.datetime.now()) 2211 print "" 2212 print "current local timezone:", gmCurrentLocalTimezone 2213 print " (total) UTC offset:", gmCurrentLocalTimezone.utcoffset(pyDT.datetime.now()) 2214 print " DST adjustment:", gmCurrentLocalTimezone.dst(pyDT.datetime.now()) 2215 print " timezone name:", gmCurrentLocalTimezone.tzname(pyDT.datetime.now()) 2216 print "" 2217 print "now here:", pydt_now_here() 2218 print ""
2219 #-------------------------------------------------
2220 - def test_str2fuzzy_timestamp_matches():
2221 print "testing function str2fuzzy_timestamp_matches" 2222 print "--------------------------------------------" 2223 2224 val = None 2225 while val != 'exit': 2226 val = raw_input('Enter date fragment ("exit" quits): ') 2227 matches = str2fuzzy_timestamp_matches(str2parse = val) 2228 for match in matches: 2229 print 'label shown :', match['label'] 2230 print 'data attached:', match['data'], match['data'].timestamp 2231 print "" 2232 print "---------------"
2233 #-------------------------------------------------
2234 - def test_cFuzzyTimeStamp():
2235 print "testing fuzzy timestamp class" 2236 print "-----------------------------" 2237 2238 ts = mxDT.now() 2239 print "mx.DateTime timestamp", type(ts) 2240 print " print ... :", ts 2241 print " print '%%s' %% ...: %s" % ts 2242 print " str() :", str(ts) 2243 print " repr() :", repr(ts) 2244 2245 fts = cFuzzyTimestamp() 2246 print "\nfuzzy timestamp <%s '%s'>" % ('class', fts.__class__.__name__) 2247 for accuracy in range(1,8): 2248 fts.accuracy = accuracy 2249 print " accuracy : %s (%s)" % (accuracy, _accuracy_strings[accuracy]) 2250 print " format_accurately:", fts.format_accurately() 2251 print " strftime() :", fts.strftime('%Y %b %d %H:%M:%S') 2252 print " print ... :", fts 2253 print " print '%%s' %% ... : %s" % fts 2254 print " str() :", str(fts) 2255 print " repr() :", repr(fts) 2256 raw_input('press ENTER to continue')
2257 #-------------------------------------------------
2258 - def test_get_pydt():
2259 print "testing platform for handling dates before 1970" 2260 print "-----------------------------------------------" 2261 ts = mxDT.DateTime(1935, 4, 2) 2262 fts = cFuzzyTimestamp(timestamp=ts) 2263 print "fts :", fts 2264 print "fts.get_pydt():", fts.get_pydt()
2265 #-------------------------------------------------
2266 - def test_calculate_apparent_age():
2267 # test leap year glitches 2268 start = pydt_now_here().replace(year = 2000).replace(month = 2).replace(day = 29) 2269 end = pydt_now_here().replace(year = 2012).replace(month = 2).replace(day = 27) 2270 print "start is leap year: 29.2.2000" 2271 print " ", calculate_apparent_age(start = start, end = end) 2272 print " ", format_apparent_age_medically(calculate_apparent_age(start = start)) 2273 2274 start = pydt_now_here().replace(month = 10).replace(day = 23).replace(year = 1974) 2275 end = pydt_now_here().replace(year = 2012).replace(month = 2).replace(day = 29) 2276 print "end is leap year: 29.2.2012" 2277 print " ", calculate_apparent_age(start = start, end = end) 2278 print " ", format_apparent_age_medically(calculate_apparent_age(start = start)) 2279 2280 start = pydt_now_here().replace(year = 2000).replace(month = 2).replace(day = 29) 2281 end = pydt_now_here().replace(year = 2012).replace(month = 2).replace(day = 29) 2282 print "start is leap year: 29.2.2000" 2283 print "end is leap year: 29.2.2012" 2284 print " ", calculate_apparent_age(start = start, end = end) 2285 print " ", format_apparent_age_medically(calculate_apparent_age(start = start)) 2286 2287 print "leap year tests worked" 2288 2289 start = pydt_now_here().replace(month = 10).replace(day = 23).replace(year = 1974) 2290 print calculate_apparent_age(start = start) 2291 print format_apparent_age_medically(calculate_apparent_age(start = start)) 2292 2293 start = pydt_now_here().replace(month = 3).replace(day = 13).replace(year = 1979) 2294 print calculate_apparent_age(start = start) 2295 print format_apparent_age_medically(calculate_apparent_age(start = start)) 2296 2297 start = pydt_now_here().replace(month = 2, day = 2).replace(year = 1979) 2298 end = pydt_now_here().replace(month = 3).replace(day = 31).replace(year = 1979) 2299 print calculate_apparent_age(start = start, end = end) 2300 2301 start = pydt_now_here().replace(month = 7, day = 21).replace(year = 2009) 2302 print format_apparent_age_medically(calculate_apparent_age(start = start)) 2303 2304 print "-------" 2305 start = pydt_now_here().replace(month = 1).replace(day = 23).replace(hour = 12).replace(minute = 11).replace(year = 2011) 2306 print calculate_apparent_age(start = start) 2307 print format_apparent_age_medically(calculate_apparent_age(start = start))
2308 #-------------------------------------------------
2309 - def test_str2pydt():
2310 print "testing function str2pydt_matches" 2311 print "---------------------------------" 2312 2313 val = None 2314 while val != 'exit': 2315 val = raw_input('Enter date fragment ("exit" quits): ') 2316 matches = str2pydt_matches(str2parse = val) 2317 for match in matches: 2318 print 'label shown :', match['label'] 2319 print 'data attached:', match['data'] 2320 print "" 2321 print "---------------"
2322 #-------------------------------------------------
2323 - def test_pydt_strftime():
2324 dt = pydt_now_here() 2325 print pydt_strftime(dt) 2326 print pydt_strftime(dt, accuracy = acc_days) 2327 print pydt_strftime(dt, accuracy = acc_minutes) 2328 print pydt_strftime(dt, accuracy = acc_seconds) 2329 dt = dt.replace(year = 1899) 2330 print pydt_strftime(dt) 2331 print pydt_strftime(dt, accuracy = acc_days) 2332 print pydt_strftime(dt, accuracy = acc_minutes) 2333 print pydt_strftime(dt, accuracy = acc_seconds)
2334 #-------------------------------------------------
2335 - def test_is_leap_year():
2336 for year in range(1995, 2017): 2337 print year, "leaps:", is_leap_year(year)
2338 #------------------------------------------------- 2339 # GNUmed libs 2340 gmI18N.activate_locale() 2341 gmI18N.install_domain('gnumed') 2342 2343 init() 2344 2345 #test_date_time() 2346 #test_str2fuzzy_timestamp_matches() 2347 #test_cFuzzyTimeStamp() 2348 #test_get_pydt() 2349 #test_str2interval() 2350 #test_format_interval() 2351 #test_format_interval_medically() 2352 test_str2pydt() 2353 #test_pydt_strftime() 2354 #test_calculate_apparent_age() 2355 #test_is_leap_year() 2356 2357 #=========================================================================== 2358