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

# Source Code for Module Gnumed.pycommon.gmDateTime

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