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  #=========================================================================== 
  37  __version__ = "$Revision: 1.34 $" 
  38  __author__ = "K. Hilbert <Karsten.Hilbert@gmx.net>" 
  39  __license__ = "GPL (details at http://www.gnu.org)" 
  40   
  41  # stdlib 
  42  import sys, datetime as pyDT, time, os, re as regex, locale, logging 
  43   
  44   
  45  # 3rd party 
  46  import mx.DateTime as mxDT 
  47  import psycopg2                                         # this will go once datetime has timezone classes 
  48   
  49   
  50  if __name__ == '__main__': 
  51          sys.path.insert(0, '../../') 
  52  from Gnumed.pycommon import gmI18N 
  53   
  54   
  55  _log = logging.getLogger('gm.datetime') 
  56  _log.info(__version__) 
  57  _log.info(u'mx.DateTime version: %s', mxDT.__version__) 
  58   
  59  dst_locally_in_use = None 
  60  dst_currently_in_effect = None 
  61   
  62  current_local_utc_offset_in_seconds = None 
  63  current_local_timezone_interval = None 
  64  current_local_iso_numeric_timezone_string = None 
  65  current_local_timezone_name = None 
  66  py_timezone_name = None 
  67  py_dst_timezone_name = None 
  68   
  69  cLocalTimezone = psycopg2.tz.LocalTimezone                                      # remove as soon as datetime supports timezone classes 
  70  cFixedOffsetTimezone = psycopg2.tz.FixedOffsetTimezone          # remove as soon as datetime supports timezone classes 
  71  gmCurrentLocalTimezone = 'gmCurrentLocalTimezone not initialized' 
  72   
  73   
  74  (       acc_years, 
  75          acc_months, 
  76          acc_weeks, 
  77          acc_days, 
  78          acc_hours, 
  79          acc_minutes, 
  80          acc_seconds, 
  81          acc_subseconds 
  82  ) = range(1,9) 
  83   
  84  _accuracy_strings = { 
  85          1: 'years', 
  86          2: 'months', 
  87          3: 'weeks', 
  88          4: 'days', 
  89          5: 'hours', 
  90          6: 'minutes', 
  91          7: 'seconds', 
  92          8: 'subseconds' 
  93  } 
  94   
  95  gregorian_month_length = { 
  96          1: 31, 
  97          2: 28,          # FIXME: make leap year aware 
  98          3: 31, 
  99          4: 30, 
 100          5: 31, 
 101          6: 30, 
 102          7: 31, 
 103          8: 31, 
 104          9: 30, 
 105          10: 31, 
 106          11: 30, 
 107          12: 31 
 108  } 
 109   
 110  avg_days_per_gregorian_year = 365 
 111  avg_days_per_gregorian_month = 30 
 112  avg_seconds_per_day = 24 * 60 * 60 
 113  days_per_week = 7 
 114   
 115  #=========================================================================== 
 116  # module init 
 117  #--------------------------------------------------------------------------- 
118 -def init():
119 120 _log.debug('mx.DateTime.now(): [%s]' % mxDT.now()) 121 _log.debug('datetime.now() : [%s]' % pyDT.datetime.now()) 122 _log.debug('time.localtime() : [%s]' % str(time.localtime())) 123 _log.debug('time.gmtime() : [%s]' % str(time.gmtime())) 124 125 try: 126 _log.debug('$TZ: [%s]' % os.environ['TZ']) 127 except KeyError: 128 _log.debug('$TZ not defined') 129 130 _log.debug('time.daylight: [%s] (whether or not DST is locally used at all)' % time.daylight) 131 _log.debug('time.timezone: [%s] seconds' % time.timezone) 132 _log.debug('time.altzone : [%s] seconds' % time.altzone) 133 _log.debug('time.tzname : [%s / %s] (non-DST / DST)' % time.tzname) 134 _log.debug('mx.DateTime.now().gmtoffset(): [%s]' % mxDT.now().gmtoffset()) 135 136 global py_timezone_name 137 py_timezone_name = time.tzname[0].decode(gmI18N.get_encoding(), 'replace') 138 139 global py_dst_timezone_name 140 py_dst_timezone_name = time.tzname[1].decode(gmI18N.get_encoding(), 'replace') 141 142 global dst_locally_in_use 143 dst_locally_in_use = (time.daylight != 0) 144 145 global dst_currently_in_effect 146 dst_currently_in_effect = bool(time.localtime()[8]) 147 _log.debug('DST currently in effect: [%s]' % dst_currently_in_effect) 148 149 if (not dst_locally_in_use) and dst_currently_in_effect: 150 _log.error('system inconsistency: DST not in use - but DST currently in effect ?') 151 152 global current_local_utc_offset_in_seconds 153 msg = 'DST currently%sin effect: using UTC offset of [%s] seconds instead of [%s] seconds' 154 if dst_currently_in_effect: 155 current_local_utc_offset_in_seconds = time.altzone * -1 156 _log.debug(msg % (' ', time.altzone * -1, time.timezone * -1)) 157 else: 158 current_local_utc_offset_in_seconds = time.timezone * -1 159 _log.debug(msg % (' not ', time.timezone * -1, time.altzone * -1)) 160 161 if current_local_utc_offset_in_seconds > 0: 162 _log.debug('UTC offset is positive, assuming EAST of Greenwich (clock is "ahead")') 163 elif current_local_utc_offset_in_seconds < 0: 164 _log.debug('UTC offset is negative, assuming WEST of Greenwich (clock is "behind")') 165 else: 166 _log.debug('UTC offset is ZERO, assuming Greenwich Time') 167 168 global current_local_timezone_interval 169 current_local_timezone_interval = mxDT.now().gmtoffset() 170 _log.debug('ISO timezone: [%s] (taken from mx.DateTime.now().gmtoffset())' % current_local_timezone_interval) 171 172 global current_local_iso_numeric_timezone_string 173 current_local_iso_numeric_timezone_string = str(current_local_timezone_interval).replace(',', '.') 174 175 global current_local_timezone_name 176 try: 177 current_local_timezone_name = os.environ['TZ'].decode(gmI18N.get_encoding(), 'replace') 178 except KeyError: 179 if dst_currently_in_effect: 180 current_local_timezone_name = time.tzname[1].decode(gmI18N.get_encoding(), 'replace') 181 else: 182 current_local_timezone_name = time.tzname[0].decode(gmI18N.get_encoding(), 'replace') 183 184 # do some magic to convert Python's timezone to a valid ISO timezone 185 # is this safe or will it return things like 13.5 hours ? 186 #_default_client_timezone = "%+.1f" % (-tz / 3600.0) 187 #_log.info('assuming default client time zone of [%s]' % _default_client_timezone) 188 189 global gmCurrentLocalTimezone 190 gmCurrentLocalTimezone = cFixedOffsetTimezone ( 191 offset = (current_local_utc_offset_in_seconds / 60), 192 name = current_local_iso_numeric_timezone_string 193 )
194 #===========================================================================
195 -def pydt_now_here():
196 """Returns NOW @ HERE (IOW, in the local timezone.""" 197 return pyDT.datetime.now(gmCurrentLocalTimezone)
198 #---------------------------------------------------------------------------
199 -def wx_now_here(wx=None):
200 """Returns NOW @ HERE (IOW, in the local timezone.""" 201 return py_dt2wxDate(py_dt = pydt_now_here(), wx = wx)
202 #=========================================================================== 203 # wxPython conversions 204 #---------------------------------------------------------------------------
205 -def wxDate2py_dt(wxDate=None):
206 if not wxDate.IsValid(): 207 raise ArgumentError (u'invalid wxDate: %s-%s-%s %s:%s %s.%s', 208 wxDate.GetYear(), 209 wxDate.GetMonth(), 210 wxDate.GetDay(), 211 wxDate.GetHour(), 212 wxDate.GetMinute(), 213 wxDate.GetSecond(), 214 wxDate.GetMillisecond() 215 ) 216 217 try: 218 return pyDT.datetime ( 219 year = wxDate.GetYear(), 220 month = wxDate.GetMonth() + 1, 221 day = wxDate.GetDay(), 222 tzinfo = gmCurrentLocalTimezone 223 ) 224 except: 225 _log.debug (u'error converting wxDateTime to Python: %s-%s-%s %s:%s %s.%s', 226 wxDate.GetYear(), 227 wxDate.GetMonth(), 228 wxDate.GetDay(), 229 wxDate.GetHour(), 230 wxDate.GetMinute(), 231 wxDate.GetSecond(), 232 wxDate.GetMillisecond() 233 ) 234 raise
235 #---------------------------------------------------------------------------
236 -def py_dt2wxDate(py_dt=None, wx=None):
237 _log.debug(u'setting wx.DateTime from: %s-%s-%s', py_dt.year, py_dt.month, py_dt.day) 238 # Robin Dunn says that for SetYear/*Month/*Day the wx.DateTime MUST already 239 # be valid (by definition) or, put the other way round, you must Set() day, 240 # month, and year at once 241 wxdt = wx.DateTimeFromDMY(py_dt.day, py_dt.month-1, py_dt.year) 242 return wxdt
243 #=========================================================================== 244 # interval related 245 #---------------------------------------------------------------------------
246 -def format_interval(interval=None, accuracy_wanted=acc_seconds):
247 248 years, days = divmod(interval.days, avg_days_per_gregorian_year) 249 months, days = divmod(days, avg_days_per_gregorian_month) 250 weeks, days = divmod(days, days_per_week) 251 days, secs = divmod((days * avg_seconds_per_day) + interval.seconds, avg_seconds_per_day) 252 hours, secs = divmod(secs, 3600) 253 mins, secs = divmod(secs, 60) 254 255 tmp = u'' 256 257 if years > 0: 258 tmp += u'%s%s' % (int(years), _('interval_format_tag::years::y')[-1:]) 259 260 if accuracy_wanted < acc_months: 261 return tmp.strip() 262 263 if months > 0: 264 tmp += u' %s%s' % (int(months), _('interval_format_tag::months::m')[-1:]) 265 266 if accuracy_wanted < acc_weeks: 267 return tmp.strip() 268 269 if weeks > 0: 270 tmp += u' %s%s' % (int(weeks), _('interval_format_tag::weeks::w')[-1:]) 271 272 if accuracy_wanted < acc_days: 273 return tmp.strip() 274 275 if days > 0: 276 tmp += u' %s%s' % (int(days), _('interval_format_tag::days::d')[-1:]) 277 278 if accuracy_wanted < acc_hours: 279 return tmp.strip() 280 281 if hours > 0: 282 tmp += u' %s/24' % int(hours) 283 284 if accuracy_wanted < acc_minutes: 285 return tmp.strip() 286 287 if mins > 0: 288 tmp += u' %s/60' % int(mins) 289 290 if accuracy_wanted < acc_seconds: 291 return tmp.strip() 292 293 if secs > 0: 294 tmp += u' %s/60' % int(secs) 295 296 return tmp.strip()
297 #---------------------------------------------------------------------------
298 -def format_interval_medically(interval=None):
299 """Formats an interval. 300 301 This isn't mathematically correct but close enough for display. 302 """ 303 # FIXME: i18n for abbrevs 304 305 # more than 1 year ? 306 if interval.days > 363: 307 years, days = divmod(interval.days, 364) 308 leap_days, tmp = divmod(years, 4) 309 months, day = divmod((days + leap_days), 30.33) 310 if int(months) == 0: 311 return "%sy" % int(years) 312 return "%sy %sm" % (int(years), int(months)) 313 314 # more than 30 days / 1 month ? 315 if interval.days > 30: 316 months, days = divmod(interval.days, 30.33) 317 weeks, days = divmod(days, 7) 318 if int(weeks + days) == 0: 319 result = '%smo' % int(months) 320 else: 321 result = '%sm' % int(months) 322 if int(weeks) != 0: 323 result += ' %sw' % int(weeks) 324 if int(days) != 0: 325 result += ' %sd' % int(days) 326 return result 327 328 # between 7 and 30 days ? 329 if interval.days > 7: 330 return "%sd" % interval.days 331 332 # between 1 and 7 days ? 333 if interval.days > 0: 334 hours, seconds = divmod(interval.seconds, 3600) 335 if hours == 0: 336 return '%sd' % interval.days 337 return "%sd (%sh)" % (interval.days, int(hours)) 338 339 # between 5 hours and 1 day 340 if interval.seconds > (5*3600): 341 return "%sh" % int(interval.seconds // 3600) 342 343 # between 1 and 5 hours 344 if interval.seconds > 3600: 345 hours, seconds = divmod(interval.seconds, 3600) 346 minutes = seconds // 60 347 if minutes == 0: 348 return '%sh' % int(hours) 349 return "%s:%02d" % (int(hours), int(minutes)) 350 351 # minutes only 352 if interval.seconds > (5*60): 353 return "0:%02d" % (int(interval.seconds // 60)) 354 355 # seconds 356 minutes, seconds = divmod(interval.seconds, 60) 357 if minutes == 0: 358 return '%ss' % int(seconds) 359 if seconds == 0: 360 return '0:%02d' % int(minutes) 361 return "%s.%ss" % (int(minutes), int(seconds))
362 #---------------------------------------------------------------------------
363 -def calculate_apparent_age(start=None, end=None):
364 """The result of this is a tuple (years, ..., seconds) as one would 365 'expect' a date to look like, that is, simple differences between 366 the fields. 367 368 No need for 100/400 years leap days rule because 2000 WAS a leap year. 369 370 This does not take into account time zones which may 371 shift the result by one day. 372 373 <start> and <end> must by python datetime instances 374 <end> is assumed to be "now" if not given 375 """ 376 if end is None: 377 end = pyDT.datetime.now(gmCurrentLocalTimezone) 378 379 if end < start: 380 raise ValueError('calculate_apparent_age(): <end> (%s) before <start> %s' % (end, start)) 381 382 if end == start: 383 years = months = days = hours = minutes = seconds = 0 384 return (years, months, days, hours, minutes, seconds) 385 386 # years 387 years = end.year - start.year 388 end = end.replace(year = start.year) 389 if end < start: 390 years = years - 1 391 392 # months 393 if end.month == start.month: 394 months = 0 395 else: 396 months = end.month - start.month 397 if months < 0: 398 months = months + 12 399 end = end.replace(month = start.month) 400 if end < start: 401 months = months - 1 402 403 # days 404 if end.day == start.day: 405 days = 0 406 else: 407 days = end.day - start.day 408 if days < 0: 409 days = days + gregorian_month_length[start.month] 410 end = end.replace(day = start.day) 411 if end < start: 412 days = days - 1 413 414 # hours 415 if end.hour == start.hour: 416 hours = 0 417 else: 418 hours = end.hour - start.hour 419 if hours < 0: 420 hours = hours + 24 421 end = end.replace(hour = start.hour) 422 if end < start: 423 hours = hours - 1 424 425 # minutes 426 if end.minute == start.minute: 427 minutes = 0 428 else: 429 minutes = end.minute - start.minute 430 if minutes < 0: 431 minutes = minutes + 60 432 end = end.replace(minute = start.minute) 433 if end < start: 434 minutes = minutes - 1 435 436 # seconds 437 if end.second == start.second: 438 seconds = 0 439 else: 440 seconds = end.second - start.second 441 if seconds < 0: 442 seconds = seconds + 60 443 end = end.replace(second = start.second) 444 if end < start: 445 seconds = seconds - 1 446 447 return (years, months, days, hours, minutes, seconds)
448 #---------------------------------------------------------------------------
449 -def format_apparent_age_medically(age=None):
450 """<age> must be a tuple as created by calculate_apparent_age()""" 451 452 (years, months, days, hours, minutes, seconds) = age 453 454 # more than 1 year ? 455 if years > 1: 456 if months == 0: 457 return u'%s%s' % ( 458 years, 459 _('y::year_abbreviation').replace('::year_abbreviation', u'') 460 ) 461 return u'%s%s %s%s' % ( 462 years, 463 _('y::year_abbreviation').replace('::year_abbreviation', u''), 464 months, 465 _('m::month_abbreviation').replace('::month_abbreviation', u'') 466 ) 467 468 # more than 1 month ? 469 if months > 1: 470 if days == 0: 471 return u'%s%s' % ( 472 months, 473 _('mo::month_only_abbreviation').replace('::month_only_abbreviation', u'') 474 ) 475 476 result = u'%s%s' % ( 477 months, 478 _('m::month_abbreviation').replace('::month_abbreviation', u'') 479 ) 480 481 weeks, days = divmod(days, 7) 482 if int(weeks) != 0: 483 result += u'%s%s' % ( 484 int(weeks), 485 _('w::week_abbreviation').replace('::week_abbreviation', u'') 486 ) 487 if int(days) != 0: 488 result += u'%s%s' % ( 489 int(days), 490 _('d::day_abbreviation').replace('::day_abbreviation', u'') 491 ) 492 493 return result 494 495 # between 7 days and 1 month 496 if days > 7: 497 return u"%s%s" % ( 498 days, 499 _('d::day_abbreviation').replace('::day_abbreviation', u'') 500 ) 501 502 # between 1 and 7 days ? 503 if days > 0: 504 if hours == 0: 505 return u'%s%s' % ( 506 days, 507 _('d::day_abbreviation').replace('::day_abbreviation', u'') 508 ) 509 return u'%s%s (%s%s)' % ( 510 days, 511 _('d::day_abbreviation').replace('::day_abbreviation', u''), 512 hours, 513 _('h::hour_abbreviation').replace('::hour_abbreviation', u'') 514 ) 515 516 # between 5 hours and 1 day 517 if hours > 5: 518 return u'%s%s' % ( 519 hours, 520 _('h::hour_abbreviation').replace('::hour_abbreviation', u'') 521 ) 522 523 # between 1 and 5 hours 524 if hours > 1: 525 if minutes == 0: 526 return u'%s%s' % ( 527 hours, 528 _('h::hour_abbreviation').replace('::hour_abbreviation', u'') 529 ) 530 return u'%s:%02d' % ( 531 hours, 532 minutes 533 ) 534 535 # between 5 and 60 minutes 536 if minutes > 5: 537 return u"0:%02d" % minutes 538 539 # less than 5 minutes 540 if minutes == 0: 541 return u'%s%s' % ( 542 seconds, 543 _('s::second_abbreviation').replace('::second_abbreviation', u'') 544 ) 545 if seconds == 0: 546 return u"0:%02d" % minutes 547 return "%s.%s%s" % ( 548 minutes, 549 seconds, 550 _('s::second_abbreviation').replace('::second_abbreviation', u'') 551 )
552 #---------------------------------------------------------------------------
553 -def str2interval(str_interval=None):
554 555 unit_keys = { 556 'year': _('yYaA_keys_year'), 557 'month': _('mM_keys_month'), 558 'week': _('wW_keys_week'), 559 'day': _('dD_keys_day'), 560 'hour': _('hH_keys_hour') 561 } 562 563 str_interval = str_interval.strip() 564 565 # "(~)35(yY)" - at age 35 years 566 keys = '|'.join(list(unit_keys['year'].replace('_keys_year', u''))) 567 if regex.match(u'^~*(\s|\t)*\d+(%s)*$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE): 568 return pyDT.timedelta(days = (int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]) * avg_days_per_gregorian_year)) 569 570 # "(~)12mM" - at age 12 months 571 keys = '|'.join(list(unit_keys['month'].replace('_keys_month', u''))) 572 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE): 573 years, months = divmod ( 574 int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]), 575 12 576 ) 577 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month))) 578 579 # weeks 580 keys = '|'.join(list(unit_keys['week'].replace('_keys_week', u''))) 581 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE): 582 return pyDT.timedelta(weeks = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0])) 583 584 # days 585 keys = '|'.join(list(unit_keys['day'].replace('_keys_day', u''))) 586 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE): 587 return pyDT.timedelta(days = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0])) 588 589 # hours 590 keys = '|'.join(list(unit_keys['hour'].replace('_keys_hour', u''))) 591 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE): 592 return pyDT.timedelta(hours = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0])) 593 594 # x/12 - months 595 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*12$', str_interval, flags = regex.LOCALE | regex.UNICODE): 596 years, months = divmod ( 597 int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]), 598 12 599 ) 600 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month))) 601 602 # x/52 - weeks 603 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*52$', str_interval, flags = regex.LOCALE | regex.UNICODE): 604 # return pyDT.timedelta(days = (int(regex.findall('\d+', str_interval)[0]) * days_per_week)) 605 return pyDT.timedelta(weeks = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0])) 606 607 # x/7 - days 608 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*7$', str_interval, flags = regex.LOCALE | regex.UNICODE): 609 return pyDT.timedelta(days = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0])) 610 611 # x/24 - hours 612 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*24$', str_interval, flags = regex.LOCALE | regex.UNICODE): 613 return pyDT.timedelta(hours = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0])) 614 615 # x/60 - minutes 616 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*60$', str_interval, flags = regex.LOCALE | regex.UNICODE): 617 return pyDT.timedelta(minutes = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0])) 618 619 # nYnM - years, months 620 keys_year = '|'.join(list(unit_keys['year'].replace('_keys_year', u''))) 621 keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', u''))) 622 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): 623 parts = regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE) 624 years, months = divmod(int(parts[1]), 12) 625 years += int(parts[0]) 626 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month))) 627 628 # nMnW - months, weeks 629 keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', u''))) 630 keys_week = '|'.join(list(unit_keys['week'].replace('_keys_week', u''))) 631 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): 632 parts = regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE) 633 months, weeks = divmod(int(parts[1]), 4) 634 months += int(parts[0]) 635 return pyDT.timedelta(days = ((months * avg_days_per_gregorian_month) + (weeks * days_per_week))) 636 637 return None
638 639 #=========================================================================== 640 # string -> timestamp parsers 641 #---------------------------------------------------------------------------
642 -def __explicit_offset(str2parse, offset_chars=None):
643 """ 644 Default is 'hdwm': 645 h - hours 646 d - days 647 w - weeks 648 m - months 649 y - years 650 651 This also defines the significance of the order of the characters. 652 """ 653 if offset_chars is None: 654 offset_chars = _('hdwmy (single character date offset triggers)')[:5].lower() 655 656 # "+/-XXd/w/m/t" 657 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): 658 return [] 659 val = int(regex.findall(u'\d{1,2}', str2parse, flags = regex.LOCALE | regex.UNICODE)[0]) 660 offset_char = regex.findall(u'[%s]' % offset_chars, str2parse, flags = regex.LOCALE | regex.UNICODE)[0].lower() 661 662 now = mxDT.now() 663 enc = gmI18N.get_encoding() 664 665 # allow past ? 666 is_future = True 667 if str2parse.find('-') > -1: 668 is_future = False 669 670 ts = None 671 # hours 672 if offset_char == offset_chars[0]: 673 if is_future: 674 ts = now + mxDT.RelativeDateTime(hours = val) 675 label = _('in %d hour(s) - %s') % (val, ts.strftime('%H:%M')) 676 else: 677 ts = now - mxDT.RelativeDateTime(hours = val) 678 label = _('%d hour(s) ago - %s') % (val, ts.strftime('%H:%M')) 679 accuracy = acc_subseconds 680 # days 681 elif offset_char == offset_chars[1]: 682 if is_future: 683 ts = now + mxDT.RelativeDateTime(days = val) 684 label = _('in %d day(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 685 else: 686 ts = now - mxDT.RelativeDateTime(days = val) 687 label = _('%d day(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 688 accuracy = acc_days 689 # weeks 690 elif offset_char == offset_chars[2]: 691 if is_future: 692 ts = now + mxDT.RelativeDateTime(weeks = val) 693 label = _('in %d week(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 694 else: 695 ts = now - mxDT.RelativeDateTime(weeks = val) 696 label = _('%d week(s) ago - %s)') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 697 accuracy = acc_days 698 # months 699 elif offset_char == offset_chars[3]: 700 if is_future: 701 ts = now + mxDT.RelativeDateTime(months = val) 702 label = _('in %d month(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 703 else: 704 ts = now - mxDT.RelativeDateTime(months = val) 705 label = _('%d month(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 706 accuracy = acc_days 707 # years 708 elif offset_char == offset_chars[4]: 709 if is_future: 710 ts = now + mxDT.RelativeDateTime(years = val) 711 label = _('in %d year(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 712 else: 713 ts = now - mxDT.RelativeDateTime(years = val) 714 label = _('%d year(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc)) 715 accuracy = acc_months 716 717 if ts is None: 718 return [] 719 720 tmp = { 721 'data': cFuzzyTimestamp(timestamp = ts, accuracy = accuracy), 722 'label': label 723 } 724 return [tmp]
725 #---------------------------------------------------------------------------
726 -def __single_char(str2parse, trigger_chars=None):
727 """This matches on single characters. 728 729 Spaces and tabs are discarded. 730 731 Default is 'ndmy': 732 n - now 733 d - toDay 734 m - toMorrow Someone please suggest a synonym ! 735 y - yesterday 736 737 This also defines the significance of the order of the characters. 738 """ 739 if trigger_chars is None: 740 trigger_chars = _('ndmy (single character date triggers)')[:4].lower() 741 742 if not regex.match(u'^(\s|\t)*[%s]{1}(\s|\t)*$' % trigger_chars, str2parse, flags = regex.LOCALE | regex.UNICODE): 743 return [] 744 val = str2parse.strip().lower() 745 746 now = mxDT.now() 747 enc = gmI18N.get_encoding() 748 749 # FIXME: handle uebermorgen/vorgestern ? 750 751 # right now 752 if val == trigger_chars[0]: 753 ts = now 754 return [{ 755 'data': cFuzzyTimestamp ( 756 timestamp = ts, 757 accuracy = acc_subseconds 758 ), 759 'label': _('right now (%s, %s)') % (ts.strftime('%A').decode(enc), ts) 760 }] 761 762 # today 763 if val == trigger_chars[1]: 764 return [{ 765 'data': cFuzzyTimestamp ( 766 timestamp = now, 767 accuracy = acc_days 768 ), 769 'label': _('today (%s)') % now.strftime('%A, %Y-%m-%d').decode(enc) 770 }] 771 772 # tomorrow 773 if val == trigger_chars[2]: 774 ts = now + mxDT.RelativeDateTime(days = +1) 775 return [{ 776 'data': cFuzzyTimestamp ( 777 timestamp = ts, 778 accuracy = acc_days 779 ), 780 'label': _('tomorrow (%s)') % ts.strftime('%A, %Y-%m-%d').decode(enc) 781 }] 782 783 # yesterday 784 if val == trigger_chars[3]: 785 ts = now + mxDT.RelativeDateTime(days = -1) 786 return [{ 787 'data': cFuzzyTimestamp ( 788 timestamp = ts, 789 accuracy = acc_days 790 ), 791 'label': _('yesterday (%s)') % ts.strftime('%A, %Y-%m-%d').decode(enc) 792 }] 793 794 return []
795 #---------------------------------------------------------------------------
796 -def __single_slash(str2parse):
797 """Expand fragments containing a single slash. 798 799 "5/" 800 - 2005/ (2000 - 2025) 801 - 1995/ (1990 - 1999) 802 - Mai/current year 803 - Mai/next year 804 - Mai/last year 805 - Mai/200x 806 - Mai/20xx 807 - Mai/199x 808 - Mai/198x 809 - Mai/197x 810 - Mai/19xx 811 """ 812 matches = [] 813 now = mxDT.now() 814 if regex.match(u"^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE): 815 val = int(regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE)[0]) 816 817 if val < 100 and val >= 0: 818 matches.append ({ 819 'data': None, 820 'label': '%s/' % (val + 1900) 821 }) 822 823 if val < 26 and val >= 0: 824 matches.append ({ 825 'data': None, 826 'label': '%s/' % (val + 2000) 827 }) 828 829 if val < 10 and val >= 0: 830 matches.append ({ 831 'data': None, 832 'label': '%s/' % (val + 1990) 833 }) 834 835 if val < 13 and val > 0: 836 matches.append ({ 837 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months), 838 'label': '%.2d/%s' % (val, now.year) 839 }) 840 ts = now + mxDT.RelativeDateTime(years = 1) 841 matches.append ({ 842 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months), 843 'label': '%.2d/%s' % (val, ts.year) 844 }) 845 ts = now + mxDT.RelativeDateTime(years = -1) 846 matches.append ({ 847 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months), 848 'label': '%.2d/%s' % (val, ts.year) 849 }) 850 matches.append ({ 851 'data': None, 852 'label': '%.2d/200' % val 853 }) 854 matches.append ({ 855 'data': None, 856 'label': '%.2d/20' % val 857 }) 858 matches.append ({ 859 'data': None, 860 'label': '%.2d/199' % val 861 }) 862 matches.append ({ 863 'data': None, 864 'label': '%.2d/198' % val 865 }) 866 matches.append ({ 867 'data': None, 868 'label': '%.2d/197' % val 869 }) 870 matches.append ({ 871 'data': None, 872 'label': '%.2d/19' % val 873 }) 874 875 elif regex.match(u"^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*\d{4}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE): 876 parts = regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE) 877 fts = cFuzzyTimestamp ( 878 timestamp = mxDT.now() + mxDT.RelativeDateTime(year = int(parts[1]), month = int(parts[0])), 879 accuracy = acc_months 880 ) 881 matches.append ({ 882 'data': fts, 883 'label': fts.format_accurately() 884 }) 885 886 return matches
887 #---------------------------------------------------------------------------
888 -def __numbers_only(str2parse):
889 """This matches on single numbers. 890 891 Spaces or tabs are discarded. 892 """ 893 if not regex.match(u"^(\s|\t)*\d{1,4}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE): 894 return [] 895 896 # strftime() returns str but in the localized encoding, 897 # so we may need to decode that to unicode 898 enc = gmI18N.get_encoding() 899 now = mxDT.now() 900 val = int(regex.findall(u'\d{1,4}', str2parse, flags = regex.LOCALE | regex.UNICODE)[0]) 901 902 matches = [] 903 904 # that year 905 if (1850 < val) and (val < 2100): 906 ts = now + mxDT.RelativeDateTime(year = val) 907 target_date = cFuzzyTimestamp ( 908 timestamp = ts, 909 accuracy = acc_years 910 ) 911 tmp = { 912 'data': target_date, 913 'label': '%s' % target_date 914 } 915 matches.append(tmp) 916 917 # day X of this month 918 if val <= gregorian_month_length[now.month]: 919 ts = now + mxDT.RelativeDateTime(day = val) 920 target_date = cFuzzyTimestamp ( 921 timestamp = ts, 922 accuracy = acc_days 923 ) 924 tmp = { 925 'data': target_date, 926 'label': _('%d. of %s (this month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc)) 927 } 928 matches.append(tmp) 929 930 # day X of next month 931 if val <= gregorian_month_length[(now + mxDT.RelativeDateTime(months = 1)).month]: 932 ts = now + mxDT.RelativeDateTime(months = 1, day = val) 933 target_date = cFuzzyTimestamp ( 934 timestamp = ts, 935 accuracy = acc_days 936 ) 937 tmp = { 938 'data': target_date, 939 'label': _('%d. of %s (next month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc)) 940 } 941 matches.append(tmp) 942 943 # day X of last month 944 if val <= gregorian_month_length[(now + mxDT.RelativeDateTime(months = -1)).month]: 945 ts = now + mxDT.RelativeDateTime(months = -1, day = val) 946 target_date = cFuzzyTimestamp ( 947 timestamp = ts, 948 accuracy = acc_days 949 ) 950 tmp = { 951 'data': target_date, 952 'label': _('%d. of %s (last month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc)) 953 } 954 matches.append(tmp) 955 956 # X days from now 957 if val <= 400: # more than a year ahead in days ?? nah ! 958 ts = now + mxDT.RelativeDateTime(days = val) 959 target_date = cFuzzyTimestamp ( 960 timestamp = ts 961 ) 962 tmp = { 963 'data': target_date, 964 'label': _('in %d day(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d').decode(enc)) 965 } 966 matches.append(tmp) 967 968 # X weeks from now 969 if val <= 50: # pregnancy takes about 40 weeks :-) 970 ts = now + mxDT.RelativeDateTime(weeks = val) 971 target_date = cFuzzyTimestamp ( 972 timestamp = ts 973 ) 974 tmp = { 975 'data': target_date, 976 'label': _('in %d week(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d').decode(enc)) 977 } 978 matches.append(tmp) 979 980 # month X of ... 981 if val < 13: 982 # ... this year 983 ts = now + mxDT.RelativeDateTime(month = val) 984 target_date = cFuzzyTimestamp ( 985 timestamp = ts, 986 accuracy = acc_months 987 ) 988 tmp = { 989 'data': target_date, 990 'label': _('%s (%s this year)') % (target_date, ts.strftime('%B').decode(enc)) 991 } 992 matches.append(tmp) 993 994 # ... next year 995 ts = now + mxDT.RelativeDateTime(years = 1, month = val) 996 target_date = cFuzzyTimestamp ( 997 timestamp = ts, 998 accuracy = acc_months 999 ) 1000 tmp = { 1001 'data': target_date, 1002 'label': _('%s (%s next year)') % (target_date, ts.strftime('%B').decode(enc)) 1003 } 1004 matches.append(tmp) 1005 1006 # ... last year 1007 ts = now + mxDT.RelativeDateTime(years = -1, month = val) 1008 target_date = cFuzzyTimestamp ( 1009 timestamp = ts, 1010 accuracy = acc_months 1011 ) 1012 tmp = { 1013 'data': target_date, 1014 'label': _('%s (%s last year)') % (target_date, ts.strftime('%B').decode(enc)) 1015 } 1016 matches.append(tmp) 1017 1018 # fragment expansion 1019 matches.append ({ 1020 'data': None, 1021 'label': '%s/200' % val 1022 }) 1023 matches.append ({ 1024 'data': None, 1025 'label': '%s/199' % val 1026 }) 1027 matches.append ({ 1028 'data': None, 1029 'label': '%s/198' % val 1030 }) 1031 matches.append ({ 1032 'data': None, 1033 'label': '%s/19' % val 1034 }) 1035 1036 # day X of ... 1037 if val < 8: 1038 # ... this week 1039 ts = now + mxDT.RelativeDateTime(weekday = (val-1, 0)) 1040 target_date = cFuzzyTimestamp ( 1041 timestamp = ts, 1042 accuracy = acc_days 1043 ) 1044 tmp = { 1045 'data': target_date, 1046 'label': _('%s this week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc)) 1047 } 1048 matches.append(tmp) 1049 1050 # ... next week 1051 ts = now + mxDT.RelativeDateTime(weeks = +1, weekday = (val-1, 0)) 1052 target_date = cFuzzyTimestamp ( 1053 timestamp = ts, 1054 accuracy = acc_days 1055 ) 1056 tmp = { 1057 'data': target_date, 1058 'label': _('%s next week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc)) 1059 } 1060 matches.append(tmp) 1061 1062 # ... last week 1063 ts = now + mxDT.RelativeDateTime(weeks = -1, weekday = (val-1, 0)) 1064 target_date = cFuzzyTimestamp ( 1065 timestamp = ts, 1066 accuracy = acc_days 1067 ) 1068 tmp = { 1069 'data': target_date, 1070 'label': _('%s last week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc)) 1071 } 1072 matches.append(tmp) 1073 1074 if val < 100: 1075 matches.append ({ 1076 'data': None, 1077 'label': '%s/' % (1900 + val) 1078 }) 1079 1080 if val == 200: 1081 tmp = { 1082 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_days), 1083 'label': '%s' % target_date 1084 } 1085 matches.append(tmp) 1086 matches.append ({ 1087 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months), 1088 'label': '%.2d/%s' % (now.month, now.year) 1089 }) 1090 matches.append ({ 1091 'data': None, 1092 'label': '%s/' % now.year 1093 }) 1094 matches.append ({ 1095 'data': None, 1096 'label': '%s/' % (now.year + 1) 1097 }) 1098 matches.append ({ 1099 'data': None, 1100 'label': '%s/' % (now.year - 1) 1101 }) 1102 1103 if val < 200 and val >= 190: 1104 for i in range(10): 1105 matches.append ({ 1106 'data': None, 1107 'label': '%s%s/' % (val, i) 1108 }) 1109 1110 return matches
1111 #---------------------------------------------------------------------------
1112 -def __single_dot(str2parse):
1113 """Expand fragments containing a single dot. 1114 1115 Standard colloquial date format in Germany: day.month.year 1116 1117 "14." 1118 - 14th current month this year 1119 - 14th next month this year 1120 """ 1121 if not regex.match(u"^(\s|\t)*\d{1,2}\.{1}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE): 1122 return [] 1123 1124 val = int(regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE)[0]) 1125 now = mxDT.now() 1126 enc = gmI18N.get_encoding() 1127 1128 matches = [] 1129 1130 # day X of this month 1131 ts = now + mxDT.RelativeDateTime(day = val) 1132 if val > 0 and val <= gregorian_month_length[ts.month]: 1133 matches.append ({ 1134 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days), 1135 'label': '%s.%s.%s - a %s this month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc)) 1136 }) 1137 1138 # day X of next month 1139 ts = now + mxDT.RelativeDateTime(day = val, months = +1) 1140 if val > 0 and val <= gregorian_month_length[ts.month]: 1141 matches.append ({ 1142 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days), 1143 'label': '%s.%s.%s - a %s next month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc)) 1144 }) 1145 1146 # day X of last month 1147 ts = now + mxDT.RelativeDateTime(day = val, months = -1) 1148 if val > 0 and val <= gregorian_month_length[ts.month]: 1149 matches.append ({ 1150 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days), 1151 'label': '%s.%s.%s - a %s last month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc)) 1152 }) 1153 1154 return matches
1155 #---------------------------------------------------------------------------
1156 -def str2fuzzy_timestamp_matches(str2parse=None, default_time=None, patterns=None):
1157 """ 1158 Turn a string into candidate fuzzy timestamps and auto-completions the user is likely to type. 1159 1160 You MUST have called locale.setlocale(locale.LC_ALL, '') 1161 somewhere in your code previously. 1162 1163 @param default_time: if you want to force the time part of the time 1164 stamp to a given value and the user doesn't type any time part 1165 this value will be used 1166 @type default_time: an mx.DateTime.DateTimeDelta instance 1167 1168 @param patterns: list of [time.strptime compatible date/time pattern, accuracy] 1169 @type patterns: list 1170 """ 1171 matches = __single_dot(str2parse) 1172 matches.extend(__numbers_only(str2parse)) 1173 matches.extend(__single_slash(str2parse)) 1174 matches.extend(__single_char(str2parse)) 1175 matches.extend(__explicit_offset(str2parse)) 1176 1177 # try mxDT parsers 1178 if mxDT is not None: 1179 try: 1180 # date ? 1181 date_only = mxDT.Parser.DateFromString ( 1182 text = str2parse, 1183 formats = ('euro', 'iso', 'us', 'altus', 'altiso', 'lit', 'altlit', 'eurlit') 1184 ) 1185 # time, too ? 1186 time_part = mxDT.Parser.TimeFromString(text = str2parse) 1187 datetime = date_only + time_part 1188 if datetime == date_only: 1189 accuracy = acc_days 1190 if isinstance(default_time, mxDT.DateTimeDeltaType): 1191 datetime = date_only + default_time 1192 accuracy = acc_minutes 1193 else: 1194 accuracy = acc_subseconds 1195 fts = cFuzzyTimestamp ( 1196 timestamp = datetime, 1197 accuracy = accuracy 1198 ) 1199 matches.append ({ 1200 'data': fts, 1201 'label': fts.format_accurately() 1202 }) 1203 except (ValueError, mxDT.RangeError): 1204 pass 1205 1206 if patterns is None: 1207 patterns = [] 1208 1209 patterns.append(['%Y.%m.%d', acc_days]) 1210 patterns.append(['%Y/%m/%d', acc_days]) 1211 1212 for pattern in patterns: 1213 try: 1214 fts = cFuzzyTimestamp ( 1215 timestamp = pyDT.datetime.fromtimestamp(time.mktime(time.strptime(str2parse, pattern[0]))), 1216 accuracy = pattern[1] 1217 ) 1218 matches.append ({ 1219 'data': fts, 1220 'label': fts.format_accurately() 1221 }) 1222 except AttributeError: 1223 # strptime() only available starting with Python 2.5 1224 break 1225 except OverflowError: 1226 # time.mktime() cannot handle dates older than a platform-dependant limit :-( 1227 continue 1228 except ValueError: 1229 # C-level overflow 1230 continue 1231 1232 return matches
1233 #=========================================================================== 1234 # fuzzy timestamp class 1235 #---------------------------------------------------------------------------
1236 -class cFuzzyTimestamp:
1237 1238 # FIXME: add properties for year, month, ... 1239 1240 """A timestamp implementation with definable inaccuracy. 1241 1242 This class contains an mxDateTime.DateTime instance to 1243 hold the actual timestamp. It adds an accuracy attribute 1244 to allow the programmer to set the precision of the 1245 timestamp. 1246 1247 The timestamp will have to be initialzed with a fully 1248 precise value (which may, of course, contain partially 1249 fake data to make up for missing values). One can then 1250 set the accuracy value to indicate up to which part of 1251 the timestamp the data is valid. Optionally a modifier 1252 can be set to indicate further specification of the 1253 value (such as "summer", "afternoon", etc). 1254 1255 accuracy values: 1256 1: year only 1257 ... 1258 7: everything including milliseconds value 1259 1260 Unfortunately, one cannot directly derive a class from mx.DateTime.DateTime :-( 1261 """ 1262 #-----------------------------------------------------------------------
1263 - def __init__(self, timestamp=None, accuracy=acc_subseconds, modifier=''):
1264 1265 if timestamp is None: 1266 timestamp = mxDT.now() 1267 accuracy = acc_subseconds 1268 modifier = '' 1269 1270 if isinstance(timestamp, pyDT.datetime): 1271 timestamp = mxDT.DateTime(timestamp.year, timestamp.month, timestamp.day, timestamp.hour, timestamp.minute, timestamp.second) 1272 1273 if type(timestamp) != mxDT.DateTimeType: 1274 raise TypeError, '%s.__init__(): <timestamp> must be of mx.DateTime.DateTime or datetime.datetime type' % self.__class__.__name__ 1275 1276 self.timestamp = timestamp 1277 1278 if (accuracy < 1) or (accuracy > 8): 1279 raise ValueError, '%s.__init__(): <accuracy> must be between 1 and 7' % self.__class__.__name__ 1280 self.accuracy = accuracy 1281 1282 self.modifier = modifier
1283 1284 #----------------------------------------------------------------------- 1285 # magic API 1286 #-----------------------------------------------------------------------
1287 - def __str__(self):
1288 """Return string representation meaningful to a user, also for %s formatting.""" 1289 return self.format_accurately()
1290 #-----------------------------------------------------------------------
1291 - def __repr__(self):
1292 """Return string meaningful to a programmer to aid in debugging.""" 1293 tmp = '<[%s]: timestamp [%s], accuracy [%s] (%s), modifier [%s] at %s>' % ( 1294 self.__class__.__name__, 1295 repr(self.timestamp), 1296 self.accuracy, 1297 _accuracy_strings[self.accuracy], 1298 self.modifier, 1299 id(self) 1300 ) 1301 return tmp
1302 #----------------------------------------------------------------------- 1303 # external API 1304 #-----------------------------------------------------------------------
1305 - def strftime(self, format_string):
1306 if self.accuracy == 7: 1307 return self.timestamp.strftime(format_string) 1308 return self.format_accurately()
1309 #-----------------------------------------------------------------------
1310 - def Format(self, format_string):
1311 return self.strftime(format_string)
1312 #-----------------------------------------------------------------------
1313 - def format_accurately(self):
1314 if self.accuracy == acc_years: 1315 return unicode(self.timestamp.year) 1316 1317 if self.accuracy == acc_months: 1318 return unicode(self.timestamp.strftime('%m/%Y')) # FIXME: use 3-letter month ? 1319 1320 if self.accuracy == acc_days: 1321 return unicode(self.timestamp.strftime('%Y-%m-%d')) 1322 1323 if self.accuracy == acc_hours: 1324 return unicode(self.timestamp.strftime("%Y-%m-%d %I%p")) 1325 1326 if self.accuracy == acc_minutes: 1327 return unicode(self.timestamp.strftime("%Y-%m-%d %H:%M")) 1328 1329 if self.accuracy == acc_seconds: 1330 return unicode(self.timestamp.strftime("%Y-%m-%d %H:%M:%S")) 1331 1332 if self.accuracy == acc_subseconds: 1333 return unicode(self.timestamp) 1334 1335 raise ValueError, '%s.format_accurately(): <accuracy> (%s) must be between 1 and 7' % ( 1336 self.__class__.__name__, 1337 self.accuracy 1338 )
1339 #-----------------------------------------------------------------------
1340 - def get_mxdt(self):
1341 return self.timestamp
1342 #-----------------------------------------------------------------------
1343 - def get_pydt(self):
1344 try: 1345 gmtoffset = self.timestamp.gmtoffset() 1346 except mxDT.Error: 1347 # Windows cannot deal with dates < 1970, so 1348 # when that happens switch to now() 1349 now = mxDT.now() 1350 gmtoffset = now.gmtoffset() 1351 tz = cFixedOffsetTimezone(gmtoffset.minutes, self.timestamp.tz) 1352 secs, msecs = divmod(self.timestamp.second, 1) 1353 ts = pyDT.datetime ( 1354 year = self.timestamp.year, 1355 month = self.timestamp.month, 1356 day = self.timestamp.day, 1357 hour = self.timestamp.hour, 1358 minute = self.timestamp.minute, 1359 second = secs, 1360 microsecond = msecs, 1361 tzinfo = tz 1362 ) 1363 return ts
1364 #=========================================================================== 1365 # main 1366 #--------------------------------------------------------------------------- 1367 if __name__ == '__main__': 1368 1369 intervals_as_str = [ 1370 '7', '12', ' 12', '12 ', ' 12 ', ' 12 ', '0', '~12', '~ 12', ' ~ 12', ' ~ 12 ', 1371 '12a', '12 a', '12 a', '12j', '12J', '12y', '12Y', ' ~ 12 a ', '~0a', 1372 '12m', '17 m', '12 m', '17M', ' ~ 17 m ', ' ~ 3 / 12 ', '7/12', '0/12', 1373 '12w', '17 w', '12 w', '17W', ' ~ 17 w ', ' ~ 15 / 52', '2/52', '0/52', 1374 '12d', '17 d', '12 t', '17D', ' ~ 17 T ', ' ~ 12 / 7', '3/7', '0/7', 1375 '12h', '17 h', '12 H', '17H', ' ~ 17 h ', ' ~ 36 / 24', '7/24', '0/24', 1376 ' ~ 36 / 60', '7/60', '190/60', '0/60', 1377 '12a1m', '12 a 1 M', '12 a17m', '12j 12m', '12J7m', '12y7m', '12Y7M', ' ~ 12 a 37 m ', '~0a0m', 1378 '10m1w', 1379 'invalid interval input' 1380 ] 1381 #-----------------------------------------------------------------------
1382 - def test_format_interval():
1383 for tmp in intervals_as_str: 1384 intv = str2interval(str_interval = tmp) 1385 for acc in _accuracy_strings.keys(): 1386 print '[%s]: "%s" -> "%s"' % (acc, tmp, format_interval(intv, acc))
1387 #-----------------------------------------------------------------------
1388 - def test_format_interval_medically():
1389 1390 intervals = [ 1391 pyDT.timedelta(seconds = 1), 1392 pyDT.timedelta(seconds = 5), 1393 pyDT.timedelta(seconds = 30), 1394 pyDT.timedelta(seconds = 60), 1395 pyDT.timedelta(seconds = 94), 1396 pyDT.timedelta(seconds = 120), 1397 pyDT.timedelta(minutes = 5), 1398 pyDT.timedelta(minutes = 30), 1399 pyDT.timedelta(minutes = 60), 1400 pyDT.timedelta(minutes = 90), 1401 pyDT.timedelta(minutes = 120), 1402 pyDT.timedelta(minutes = 200), 1403 pyDT.timedelta(minutes = 400), 1404 pyDT.timedelta(minutes = 600), 1405 pyDT.timedelta(minutes = 800), 1406 pyDT.timedelta(minutes = 1100), 1407 pyDT.timedelta(minutes = 2000), 1408 pyDT.timedelta(minutes = 3500), 1409 pyDT.timedelta(minutes = 4000), 1410 pyDT.timedelta(hours = 1), 1411 pyDT.timedelta(hours = 2), 1412 pyDT.timedelta(hours = 4), 1413 pyDT.timedelta(hours = 8), 1414 pyDT.timedelta(hours = 12), 1415 pyDT.timedelta(hours = 20), 1416 pyDT.timedelta(hours = 23), 1417 pyDT.timedelta(hours = 24), 1418 pyDT.timedelta(hours = 25), 1419 pyDT.timedelta(hours = 30), 1420 pyDT.timedelta(hours = 48), 1421 pyDT.timedelta(hours = 98), 1422 pyDT.timedelta(hours = 120), 1423 pyDT.timedelta(days = 1), 1424 pyDT.timedelta(days = 2), 1425 pyDT.timedelta(days = 4), 1426 pyDT.timedelta(days = 16), 1427 pyDT.timedelta(days = 29), 1428 pyDT.timedelta(days = 30), 1429 pyDT.timedelta(days = 31), 1430 pyDT.timedelta(days = 37), 1431 pyDT.timedelta(days = 40), 1432 pyDT.timedelta(days = 47), 1433 pyDT.timedelta(days = 126), 1434 pyDT.timedelta(days = 127), 1435 pyDT.timedelta(days = 128), 1436 pyDT.timedelta(days = 300), 1437 pyDT.timedelta(days = 359), 1438 pyDT.timedelta(days = 360), 1439 pyDT.timedelta(days = 361), 1440 pyDT.timedelta(days = 362), 1441 pyDT.timedelta(days = 363), 1442 pyDT.timedelta(days = 364), 1443 pyDT.timedelta(days = 365), 1444 pyDT.timedelta(days = 366), 1445 pyDT.timedelta(days = 367), 1446 pyDT.timedelta(days = 400), 1447 pyDT.timedelta(weeks = 52 * 30), 1448 pyDT.timedelta(weeks = 52 * 79, days = 33) 1449 ] 1450 1451 idx = 1 1452 for intv in intervals: 1453 print '%s) %s -> %s' % (idx, intv, format_interval_medically(intv)) 1454 idx += 1
1455 #-----------------------------------------------------------------------
1456 - def test_str2interval():
1457 print "testing str2interval()" 1458 print "----------------------" 1459 1460 for interval_as_str in intervals_as_str: 1461 print "input: <%s>" % interval_as_str 1462 print " ==>", str2interval(str_interval=interval_as_str) 1463 1464 return True
1465 #-------------------------------------------------
1466 - def test_date_time():
1467 print "DST currently in effect:", dst_currently_in_effect 1468 print "current UTC offset:", current_local_utc_offset_in_seconds, "seconds" 1469 print "current timezone (interval):", current_local_timezone_interval 1470 print "current timezone (ISO conformant numeric string):", current_local_iso_numeric_timezone_string 1471 print "local timezone class:", cLocalTimezone 1472 print "" 1473 tz = cLocalTimezone() 1474 print "local timezone instance:", tz 1475 print " (total) UTC offset:", tz.utcoffset(pyDT.datetime.now()) 1476 print " DST adjustment:", tz.dst(pyDT.datetime.now()) 1477 print " timezone name:", tz.tzname(pyDT.datetime.now()) 1478 print "" 1479 print "current local timezone:", gmCurrentLocalTimezone 1480 print " (total) UTC offset:", gmCurrentLocalTimezone.utcoffset(pyDT.datetime.now()) 1481 print " DST adjustment:", gmCurrentLocalTimezone.dst(pyDT.datetime.now()) 1482 print " timezone name:", gmCurrentLocalTimezone.tzname(pyDT.datetime.now()) 1483 print "" 1484 print "now here:", pydt_now_here() 1485 print ""
1486 #-------------------------------------------------
1487 - def test_str2fuzzy_timestamp_matches():
1488 print "testing function str2fuzzy_timestamp_matches" 1489 print "--------------------------------------------" 1490 1491 val = None 1492 while val != 'exit': 1493 val = raw_input('Enter date fragment ("exit" quits): ') 1494 matches = str2fuzzy_timestamp_matches(str2parse = val) 1495 for match in matches: 1496 print 'label shown :', match['label'] 1497 print 'data attached:', match['data'] 1498 print "" 1499 print "---------------"
1500 #-------------------------------------------------
1501 - def test_cFuzzyTimeStamp():
1502 print "testing fuzzy timestamp class" 1503 print "-----------------------------" 1504 1505 ts = mxDT.now() 1506 print "mx.DateTime timestamp", type(ts) 1507 print " print ... :", ts 1508 print " print '%%s' %% ...: %s" % ts 1509 print " str() :", str(ts) 1510 print " repr() :", repr(ts) 1511 1512 fts = cFuzzyTimestamp() 1513 print "\nfuzzy timestamp <%s '%s'>" % ('class', fts.__class__.__name__) 1514 for accuracy in range(1,8): 1515 fts.accuracy = accuracy 1516 print " accuracy : %s (%s)" % (accuracy, _accuracy_strings[accuracy]) 1517 print " format_accurately:", fts.format_accurately() 1518 print " strftime() :", fts.strftime('%c') 1519 print " print ... :", fts 1520 print " print '%%s' %% ... : %s" % fts 1521 print " str() :", str(fts) 1522 print " repr() :", repr(fts) 1523 raw_input('press ENTER to continue')
1524 #-------------------------------------------------
1525 - def test_get_pydt():
1526 print "testing platform for handling dates before 1970" 1527 print "-----------------------------------------------" 1528 ts = mxDT.DateTime(1935, 4, 2) 1529 fts = cFuzzyTimestamp(timestamp=ts) 1530 print "fts :", fts 1531 print "fts.get_pydt():", fts.get_pydt()
1532 #-------------------------------------------------
1533 - def test_calculate_apparent_age():
1534 start = pydt_now_here().replace(year = 1974).replace(month = 10).replace(day = 23) 1535 print calculate_apparent_age(start = start) 1536 print format_apparent_age_medically(calculate_apparent_age(start = start)) 1537 start = pydt_now_here().replace(year = 1979).replace(month = 3).replace(day = 13) 1538 print calculate_apparent_age(start = start) 1539 print format_apparent_age_medically(calculate_apparent_age(start = start))
1540 #------------------------------------------------- 1541 if len(sys.argv) > 1 and sys.argv[1] == "test": 1542 1543 # GNUmed libs 1544 gmI18N.activate_locale() 1545 gmI18N.install_domain('gnumed') 1546 1547 init() 1548 1549 #test_date_time() 1550 #test_str2fuzzy_timestamp_matches() 1551 #test_cFuzzyTimeStamp() 1552 #test_get_pydt() 1553 #test_str2interval() 1554 #test_format_interval() 1555 #test_format_interval_medically() 1556 test_calculate_apparent_age() 1557 1558 #=========================================================================== 1559