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

# Source Code for Module Gnumed.pycommon.gmDateTime

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