Gnumed.pycommon.gmDateTime

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

# Source Code for Module Gnumed.pycommon.gmDateTime

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