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