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          # do some magic to convert Python's timezone to a valid ISO timezone
     186          # is this safe or will it return things like 13.5 hours ?
     187          #_default_client_timezone = "%+.1f" % (-tz // 3600.0)
     188          #_log.info('assuming default client time zone of [%s]' % _default_client_timezone)
     189
     190          global gmCurrentLocalTimezone
     191          gmCurrentLocalTimezone = cPlatformLocalTimezone()
     192          _log.debug('local-timezone class: %s', cPlatformLocalTimezone)
     193          _log.debug('local-timezone instance: %s', gmCurrentLocalTimezone)
     194  #       _log.debug('')
     195  #               print (" (total) UTC offset:", gmCurrentLocalTimezone.utcoffset(pyDT.datetime.now()))
     196  #               print (" DST adjustment:", gmCurrentLocalTimezone.dst(pyDT.datetime.now()))
     197  #               print (" timezone name:", gmCurrentLocalTimezone.tzname(pyDT.datetime.now()))
     198
     199  #===========================================================================
     200  # local timezone implementation (lifted from the docs)
     201  #
     202  # A class capturing the platform's idea of local time.
     203  # (May result in wrong values on historical times in
     204  #  timezones where UTC offset and/or the DST rules had
     205  #  changed in the past.)
     206  #---------------------------------------------------------------------------
     207 -class cPlatformLocalTimezone(pyDT.tzinfo):
     208
     209          #-----------------------------------------------------------------------
     210 -        def __init__(self):
     211                  self._SECOND = pyDT.timedelta(seconds = 1)
     212                  self._nonDST_OFFSET_FROM_UTC = pyDT.timedelta(seconds = -time.timezone)
     213                  if time.daylight:
     214                          self._DST_OFFSET_FROM_UTC = pyDT.timedelta(seconds = -time.altzone)
     215                  else:
     216                          self._DST_OFFSET_FROM_UTC = self._nonDST_OFFSET_FROM_UTC
     217                  self._DST_SHIFT = self._DST_OFFSET_FROM_UTC - self._nonDST_OFFSET_FROM_UTC
     218                  _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)
     219
     220          #-----------------------------------------------------------------------
     221 -        def fromutc(self, dt):
     222                  assert dt.tzinfo is self
     223                  stamp = (dt - pyDT.datetime(1970, 1, 1, tzinfo = self)) // self._SECOND
     224                  args = time.localtime(stamp)[:6]
     225                  dst_diff = self._DST_SHIFT // self._SECOND
     226                  # Detect fold
     227                  fold = (args == time.localtime(stamp - dst_diff))
     228                  return pyDT.datetime(*args, microsecond = dt.microsecond, tzinfo = self, fold = fold)
     229
     230          #-----------------------------------------------------------------------
     231 -        def utcoffset(self, dt):
     232                  if self._isdst(dt):
     233                          return self._DST_OFFSET_FROM_UTC
     234                  return self._nonDST_OFFSET_FROM_UTC
     235
     236          #-----------------------------------------------------------------------
     237 -        def dst(self, dt):
     238                  if self._isdst(dt):
     239                          return self._DST_SHIFT
     240                  return pyDT.timedelta(0)
     241
     242          #-----------------------------------------------------------------------
     243 -        def tzname(self, dt):
     244                  return time.tzname[self._isdst(dt)]
     245
     246          #-----------------------------------------------------------------------
     247 -        def _isdst(self, dt):
     248                  tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.weekday(), 0, 0)
     249                  stamp = time.mktime(tt)
     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:
     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:
     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))
    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                  ts = pydt_replace(now, day = day_val)
    1158                  matches.append ({
    1159                          'data': ts,
    1160                          'label': _('%s-%s-%s: a %s this month') % (ts.year, ts.month, ts.day, ts.strftime('%A'))
    1161                  })
    1162                  # ... next month
    1163                  ts = pydt_replace(pydt_add(now, months = 1), day = day_val)
    1164                  if ts.day == day_val:
    1165                          matches.append ({
    1166                                  'data': ts,
    1167                                  'label': _('%s-%s-%s: a %s next month') % (ts.year, ts.month, ts.day, ts.strftime('%A'))
    1168                          })
    1169                  # ... last month
    1170                  ts = pydt_replace(pydt_add(now, months = -1), day = day_val)
    1171                  if ts.day == day_val:
    1172                          matches.append ({
    1173                                  'data': ts,
    1174                                  'label': _('%s-%s-%s: a %s last month') % (ts.year, ts.month, ts.day, ts.strftime('%A'))
    1175                          })
    1176
    1177          return matches
    1178
    1179  #---------------------------------------------------------------------------
    1180 -def __single_slash2py_dt(str2parse):
    1181          """Expand fragments containing a single slash.
    1182
    1183          "5/"
    1184                  - 2005/                                 (2000 - 2025)
    1185                  - 1995/                                 (1990 - 1999)
    1186                  - Mai/current year
    1187                  - Mai/next year
    1188                  - Mai/last year
    1189                  - Mai/200x
    1190                  - Mai/20xx
    1191                  - Mai/199x
    1192                  - Mai/198x
    1193                  - Mai/197x
    1194                  - Mai/19xx
    1195
    1196          5/1999
    1197          6/2004
    1198          """
    1199          str2parse = str2parse.strip()
    1200
    1201          now = pydt_now_here()
    1202
    1203          # 5/1999
    1204          if regex.match(r"^\d{1,2}(\s|\t)*/+(\s|\t)*\d{4}$", str2parse, flags = regex.UNICODE):
    1205                  month, year = regex.findall(r'\d+', str2parse, flags = regex.UNICODE)
    1206                  ts = pydt_replace(now, year = int(year), month = int(month))
    1207                  return [{
    1208                          'data': ts,
    1209                          'label': ts.strftime('%Y-%m-%d')
    1210                  }]
    1211
    1212          matches = []
    1213          # 5/
    1214          if regex.match(r"^\d{1,2}(\s|\t)*/+$", str2parse, flags = regex.UNICODE):
    1215                  val = int(str2parse.rstrip('/').strip())
    1216
    1217                  # "55/" -> "1955"
    1218                  if val < 100 and val >= 0:
    1219                          matches.append ({
    1220                                  'data': None,
    1221                                  'label': '%s-' % (val + 1900)
    1222                          })
    1223                  # "11/" -> "2011"
    1224                  if val < 26 and val >= 0:
    1225                          matches.append ({
    1226                                  'data': None,
    1227                                  'label': '%s-' % (val + 2000)
    1228                          })
    1229                  # "5/" -> "1995"
    1230                  if val < 10 and val >= 0:
    1231                          matches.append ({
    1232                                  'data': None,
    1233                                  'label': '%s-' % (val + 1990)
    1234                          })
    1235                  if val < 13 and val > 0:
    1236                          # "11/" -> "11/this year"
    1237                          matches.append ({
    1238                                  'data': None,
    1239                                  'label': '%s-%.2d-' % (now.year, val)
    1240                          })
    1241                          # "11/" -> "11/next year"
    1242                          ts = pydt_add(now, years = 1)
    1243                          matches.append ({
    1244                                  'data': None,
    1245                                  'label': '%s-%.2d-' % (ts.year, val)
    1246                          })
    1247                          # "11/" -> "11/last year"
    1248                          ts = pydt_add(now, years = -1)
    1249                          matches.append ({
    1250                                  'data': None,
    1251                                  'label': '%s-%.2d-' % (ts.year, val)
    1252                          })
    1253                          # "11/" -> "201?-11-"
    1254                          matches.append ({
    1255                                  'data': None,
    1256                                  'label': '201?-%.2d-' % val
    1257                          })
    1258                          # "11/" -> "200?-11-"
    1259                          matches.append ({
    1260                                  'data': None,
    1261                                  'label': '200?-%.2d-' % val
    1262                          })
    1263                          # "11/" -> "20??-11-"
    1264                          matches.append ({
    1265                                  'data': None,
    1266                                  'label': '20??-%.2d-' % val
    1267                          })
    1268                          # "11/" -> "199?-11-"
    1269                          matches.append ({
    1270                                  'data': None,
    1271                                  'label': '199?-%.2d-' % val
    1272                          })
    1273                          # "11/" -> "198?-11-"
    1274                          matches.append ({
    1275                                  'data': None,
    1276                                  'label': '198?-%.2d-' % val
    1277                          })
    1278                          # "11/" -> "198?-11-"
    1279                          matches.append ({
    1280                                  'data': None,
    1281                                  'label': '197?-%.2d-' % val
    1282                          })
    1283                          # "11/" -> "19??-11-"
    1284                          matches.append ({
    1285                                  'data': None,
    1286                                  'label': '19??-%.2d-' % val
    1287                          })
    1288
    1289          return matches
    1290
    1291  #---------------------------------------------------------------------------
    1292 -def __numbers_only2py_dt(str2parse):
    1293          """This matches on single numbers.
    1294
    1295          Spaces or tabs are discarded.
    1296          """
    1297          try:
    1298                  val = int(str2parse.strip())
    1299          except ValueError:
    1300                  return []
    1301
    1302          now = pydt_now_here()
    1303
    1304          matches = []
    1305
    1306          # that year
    1307          if (1850 < val) and (val < 2100):
    1308                  ts = pydt_replace(now, year = val)
    1309                  matches.append ({
    1310                          'data': ts,
    1311                          'label': ts.strftime('%Y-%m-%d')
    1312                  })
    1313          # day X of this month
    1314          if (val > 0) and (val <= gregorian_month_length[now.month]):
    1315                  ts = pydt_replace(now, day = val)
    1316                  matches.append ({
    1317                          'data': ts,
    1318                          'label': _('%d. of %s (this month): a %s') % (val, ts.strftime('%B'), ts.strftime('%A'))
    1319                  })
    1320          # day X of ...
    1321          if (val > 0) and (val < 32):
    1322                  # ... next month
    1323                  ts = pydt_replace(pydt_add(now, months = 1), day = val)
    1324                  matches.append ({
    1325                          'data': ts,
    1326                          'label': _('%d. of %s (next month): a %s') % (val, ts.strftime('%B'), ts.strftime('%A'))
    1327                  })
    1328                  # ... last month
    1329                  ts = pydt_replace(pydt_add(now, months = -1), day = val)
    1330                  matches.append ({
    1331                          'data': ts,
    1332                          'label': _('%d. of %s (last month): a %s') % (val, ts.strftime('%B'), ts.strftime('%A'))
    1333                  })
    1334          # X days from now
    1335          if (val > 0) and (val <= 400):                          # more than a year ahead in days ?? nah !
    1336                  ts = pydt_add(now, days = val)
    1337                  matches.append ({
    1338                          'data': ts,
    1339                          'label': _('in %d day(s): %s') % (val, ts.strftime('%A, %Y-%m-%d'))
    1340                  })
    1341          if (val < 0) and (val >= -400):                         # more than a year back in days ?? nah !
    1342                  ts = pydt_add(now, days = val)
    1343                  matches.append ({
    1344                          'data': ts,
    1345                          'label': _('%d day(s) ago: %s') % (abs(val), ts.strftime('%A, %Y-%m-%d'))
    1346                  })
    1347          # X weeks from now
    1348          if (val > 0) and (val <= 50):                           # pregnancy takes about 40 weeks :-)
    1349                  ts = pydt_add(now, weeks = val)
    1350                  matches.append ({
    1351                          'data': ts,
    1352                          'label': _('in %d week(s): %s') % (val, ts.strftime('%A, %Y-%m-%d'))
    1353                  })
    1354          if (val < 0) and (val >= -50):                          # pregnancy takes about 40 weeks :-)
    1355                  ts = pydt_add(now, weeks = val)
    1356                  matches.append ({
    1357                          'data': ts,
    1358                          'label': _('%d week(s) ago: %s') % (abs(val), ts.strftime('%A, %Y-%m-%d'))
    1359                  })
    1360
    1361          # month X of ...
    1362          if (val < 13) and (val > 0):
    1363                  # ... this year
    1364                  ts = pydt_replace(now, month = val)
    1365                  matches.append ({
    1366                          'data': ts,
    1367                          'label': _('%s (%s this year)') % (ts.strftime('%Y-%m-%d'), ts.strftime('%B'))
    1368                  })
    1369                  # ... next year
    1370                  ts = pydt_replace(pydt_add(now, years = 1), month = val)
    1371                  matches.append ({
    1372                          'data': ts,
    1373                          'label': _('%s (%s next year)') % (ts.strftime('%Y-%m-%d'), ts.strftime('%B'))
    1374                  })
    1375                  # ... last year
    1376                  ts = pydt_replace(pydt_add(now, years = -1), month = val)
    1377                  matches.append ({
    1378                          'data': ts,
    1379                          'label': _('%s (%s last year)') % (ts.strftime('%Y-%m-%d'), ts.strftime('%B'))
    1380                  })
    1381                  # fragment expansion
    1382                  matches.append ({
    1383                          'data': None,
    1384                          'label': '200?-%s' % val
    1385                  })
    1386                  matches.append ({
    1387                          'data': None,
    1388                          'label': '199?-%s' % val
    1389                  })
    1390                  matches.append ({
    1391                          'data': None,
    1392                          'label': '198?-%s' % val
    1393                  })
    1394                  matches.append ({
    1395                          'data': None,
    1396                          'label': '19??-%s' % val
    1397                  })
    1398
    1399          # needs mxDT
    1400  #       # day X of ...
    1401  #       if (val < 8) and (val > 0):
    1402  #               # ... this week
    1403  #               ts = now + mxDT.RelativeDateTime(weekday = (val-1, 0))
    1404  #               matches.append ({
    1405  #                       'data': mxdt2py_dt(ts),
    1406  #                       'label': _('%s this week (%s of %s)') % (ts.strftime('%A'), ts.day, ts.strftime('%B'))
    1407  #               })
    1408  #               # ... next week
    1409  #               ts = now + mxDT.RelativeDateTime(weeks = +1, weekday = (val-1, 0))
    1410  #               matches.append ({
    1411  #                       'data': mxdt2py_dt(ts),
    1412  #                       'label': _('%s next week (%s of %s)') % (ts.strftime('%A'), ts.day, ts.strftime('%B'))
    1413  #               })
    1414  #               # ... last week
    1415  #               ts = now + mxDT.RelativeDateTime(weeks = -1, weekday = (val-1, 0))
    1416  #               matches.append ({
    1417  #                       'data': mxdt2py_dt(ts),
    1418  #                       'label': _('%s last week (%s of %s)') % (ts.strftime('%A'), ts.day, ts.strftime('%B'))
    1419  #               })
    1420
    1421          if (val < 100) and (val > 0):
    1422                  matches.append ({
    1423                          'data': None,
    1424                          'label': '%s-' % (1900 + val)
    1425                  })
    1426
    1427          if val == 201:
    1428                  matches.append ({
    1429                          'data': now,
    1430                          'label': now.strftime('%Y-%m-%d')
    1431                  })
    1432                  matches.append ({
    1433                          'data': None,
    1434                          'label': now.strftime('%Y-%m')
    1435                  })
    1436                  matches.append ({
    1437                          'data': None,
    1438                          'label': now.strftime('%Y')
    1439                  })
    1440                  matches.append ({
    1441                          'data': None,
    1442                          'label': '%s-' % (now.year + 1)
    1443                  })
    1444                  matches.append ({
    1445                          'data': None,
    1446                          'label': '%s-' % (now.year - 1)
    1447                  })
    1448
    1449          if val < 200 and val >= 190:
    1450                  for i in range(10):
    1451                          matches.append ({
    1452                                  'data': None,
    1453                                  'label': '%s%s-' % (val, i)
    1454                          })
    1455
    1456          return matches
    1457
    1458  #---------------------------------------------------------------------------
    1459 -def __explicit_offset2py_dt(str2parse, offset_chars=None):
    1460          """Default is 'hdwmy':
    1461                          h - hours
    1462                          d - days
    1463                          w - weeks
    1464                          m - months
    1465                          y - years
    1466
    1467                  This also defines the significance of the order of the characters.
    1468          """
    1469          if offset_chars is None:
    1470                  offset_chars = _('hdwmy (single character date offset triggers)')[:5].lower()
    1471
    1472          str2parse = str2parse.replace(' ', '').replace('\t', '')
    1473          # "+/-XXXh/d/w/m/t"
    1474          if regex.fullmatch(r"(\+|-){,1}\d{1,3}[%s]" % offset_chars, str2parse) is None:
    1475                  return []
    1476
    1477          offset_val = int(str2parse[:-1])
    1478          offset_char = str2parse[-1:]
    1479          is_past = str2parse.startswith('-')
    1480          now = pydt_now_here()
    1481          ts = None
    1482
    1483          # hours
    1484          if offset_char == offset_chars[0]:
    1485                  ts = pydt_add(now, hours = offset_val)
    1486                  if is_past:
    1487                          label = _('%d hour(s) ago: %s') % (abs(offset_val), ts.strftime('%H:%M'))
    1488                  else:
    1489                          label = _('in %d hour(s): %s') % (offset_val, ts.strftime('%H:%M'))
    1490          # days
    1491          elif offset_char == offset_chars[1]:
    1492                  ts = pydt_add(now, days = offset_val)
    1493                  if is_past:
    1494                          label = _('%d day(s) ago: %s') % (abs(offset_val), ts.strftime('%A, %Y-%m-%d'))
    1495                  else:
    1496                          label = _('in %d day(s): %s') % (offset_val, ts.strftime('%A, %Y-%m-%d'))
    1497          # weeks
    1498          elif offset_char == offset_chars[2]:
    1499                  ts = pydt_add(now, weeks = offset_val)
    1500                  if is_past:
    1501                          label = _('%d week(s) ago: %s') % (abs(offset_val), ts.strftime('%A, %Y-%m-%d'))
    1502                  else:
    1503                          label = _('in %d week(s): %s') % (offset_val, ts.strftime('%A, %Y-%m-%d'))
    1504          # months
    1505          elif offset_char == offset_chars[3]:
    1506                  ts = pydt_add(now, months = offset_val)
    1507                  if is_past:
    1508                          label = _('%d month(s) ago: %s') % (abs(offset_val), ts.strftime('%A, %Y-%m-%d'))
    1509                  else:
    1510                          label = _('in %d month(s): %s') % (offset_val, ts.strftime('%A, %Y-%m-%d'))
    1511          # years
    1512          elif offset_char == offset_chars[4]:
    1513                  ts = pydt_add(now, years = offset_val)
    1514                  if is_past:
    1515                          label = _('%d year(s) ago: %s') % (abs(offset_val), ts.strftime('%A, %Y-%m-%d'))
    1516                  else:
    1517                          label = _('in %d year(s): %s') % (offset_val, ts.strftime('%A, %Y-%m-%d'))
    1518
    1519          if ts is None:
    1520                  return []
    1521
    1522          return [{'data': ts, 'label': label}]
    1523
    1524  #---------------------------------------------------------------------------
    1525 -def str2pydt_matches(str2parse=None, patterns=None):
    1526          """Turn a string into candidate dates and auto-completions the user is likely to type.
    1527
    1528          You MUST have called locale.setlocale(locale.LC_ALL, '')
    1529          somewhere in your code previously.
    1530
    1531          @param patterns: list of time.strptime compatible date pattern
    1532          @type patterns: list
    1533          """
    1534          matches = []
    1535          matches.extend(__single_dot2py_dt(str2parse))
    1536          matches.extend(__numbers_only2py_dt(str2parse))
    1537          matches.extend(__single_slash2py_dt(str2parse))
    1538          matches.extend(__single_char2py_dt(str2parse))
    1539          matches.extend(__explicit_offset2py_dt(str2parse))
    1540
    1541          # no more with Python3
    1542  #       # try mxDT parsers
    1543  #       try:
    1544  #               date = mxDT.Parser.DateFromString (
    1545  #                       text = str2parse,
    1546  #                       formats = ('euro', 'iso', 'us', 'altus', 'altiso', 'lit', 'altlit', 'eurlit')
    1547  #               )
    1548  #               matches.append ({
    1549  #                       'data': mxdt2py_dt(date),
    1550  #                       'label': date.strftime('%Y-%m-%d')
    1551  #               })
    1552  #       except (ValueError, OverflowError):
    1553  #               pass
    1554  #       except mxDT.RangeError:
    1555  #               pass
    1556
    1557          # apply explicit patterns
    1558          if patterns is None:
    1559                  patterns = []
    1560
    1561          patterns.append('%Y-%m-%d')
    1562          patterns.append('%y-%m-%d')
    1563          patterns.append('%Y/%m/%d')
    1564          patterns.append('%y/%m/%d')
    1565
    1566          patterns.append('%d-%m-%Y')
    1567          patterns.append('%d-%m-%y')
    1568          patterns.append('%d/%m/%Y')
    1569          patterns.append('%d/%m/%y')
    1570          patterns.append('%d.%m.%Y')
    1571
    1572          patterns.append('%m-%d-%Y')
    1573          patterns.append('%m-%d-%y')
    1574          patterns.append('%m/%d/%Y')
    1575          patterns.append('%m/%d/%y')
    1576
    1577          patterns.append('%Y.%m.%d')
    1578
    1579          for pattern in patterns:
    1580                  try:
    1581                          date = pyDT.datetime.strptime(str2parse, pattern).replace (
    1582                                  hour = 11,
    1583                                  minute = 11,
    1584                                  second = 11,
    1585                                  tzinfo = gmCurrentLocalTimezone
    1586                          )
    1587                          matches.append ({
    1588                                  'data': date,
    1589                                  'label': pydt_strftime(date, format = '%Y-%m-%d', accuracy = acc_days)
    1590                          })
    1591                  except ValueError:
    1592                          # C-level overflow
    1593                          continue
    1594
    1595          return matches
    1596
    1597  #===========================================================================
    1598  # string -> fuzzy timestamp parser
    1599  #---------------------------------------------------------------------------
    1600 -def __single_slash(str2parse):
    1601          """Expand fragments containing a single slash.
    1602
    1603          "5/"
    1604                  - 2005/                                 (2000 - 2025)
    1605                  - 1995/                                 (1990 - 1999)
    1606                  - Mai/current year
    1607                  - Mai/next year
    1608                  - Mai/last year
    1609                  - Mai/200x
    1610                  - Mai/20xx
    1611                  - Mai/199x
    1612                  - Mai/198x
    1613                  - Mai/197x
    1614                  - Mai/19xx
    1615          """
    1616          matches = []
    1617          now = pydt_now_here()
    1618          # "xx/yyyy"
    1619          if regex.match("^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*\d{4}(\s|\t)*$", str2parse, flags = regex.UNICODE):
    1620                  parts = regex.findall('\d+', str2parse, flags = regex.UNICODE)
    1621                  month = int(parts[0])
    1622                  if month in range(1, 13):
    1623                          fts = cFuzzyTimestamp (
    1624                                  timestamp = now.replace(year = int(parts[1], month = month)),
    1625                                  accuracy = acc_months
    1626                          )
    1627                          matches.append ({
    1628                                  'data': fts,
    1629                                  'label': fts.format_accurately()
    1630                          })
    1631          # "xx/"
    1632          elif regex.match("^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*$", str2parse, flags = regex.UNICODE):
    1633                  val = int(regex.findall('\d+', str2parse, flags = regex.UNICODE)[0])
    1634
    1635                  if val < 100 and val >= 0:
    1636                          matches.append ({
    1637                                  'data': None,
    1638                                  'label': '%s/' % (val + 1900)
    1639                          })
    1640
    1641                  if val < 26 and val >= 0:
    1642                          matches.append ({
    1643                                  'data': None,
    1644                                  'label': '%s/' % (val + 2000)
    1645                          })
    1646
    1647                  if val < 10 and val >= 0:
    1648                          matches.append ({
    1649                                  'data': None,
    1650                                  'label': '%s/' % (val + 1990)
    1651                          })
    1652
    1653                  if val < 13 and val > 0:
    1654                          matches.append ({
    1655                                  'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months),
    1656                                  'label': '%.2d/%s' % (val, now.year)
    1657                          })
    1658                          ts = now.replace(year = now.year + 1)
    1659                          matches.append ({
    1660                                  'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months),
    1661                                  'label': '%.2d/%s' % (val, ts.year)
    1662                          })
    1663                          ts = now.replace(year = now.year - 1)
    1664                          matches.append ({
    1665                                  'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months),
    1666                                  'label': '%.2d/%s' % (val, ts.year)
    1667                          })
    1668                          matches.append ({
    1669                                  'data': None,
    1670                                  'label': '%.2d/200' % val
    1671                          })
    1672                          matches.append ({
    1673                                  'data': None,
    1674                                  'label': '%.2d/20' % val
    1675                          })
    1676                          matches.append ({
    1677                                  'data': None,
    1678                                  'label': '%.2d/199' % val
    1679                          })
    1680                          matches.append ({
    1681                                  'data': None,
    1682                                  'label': '%.2d/198' % val
    1683                          })
    1684                          matches.append ({
    1685                                  'data': None,
    1686                                  'label': '%.2d/197' % val
    1687                          })
    1688                          matches.append ({
    1689                                  'data': None,
    1690                                  'label': '%.2d/19' % val
    1691                          })
    1692
    1693          return matches
    1694
    1695  #---------------------------------------------------------------------------
    1696 -def __numbers_only(str2parse):
    1697          """This matches on single numbers.
    1698
    1699          Spaces or tabs are discarded.
    1700          """
    1701          if not regex.match("^(\s|\t)*\d{1,4}(\s|\t)*$", str2parse, flags = regex.UNICODE):
    1702                  return []
    1703
    1704          now = pydt_now_here()
    1705          val = int(regex.findall('\d{1,4}', str2parse, flags = regex.UNICODE)[0])
    1706
    1707          matches = []
    1708
    1709          # today in that year
    1710          if (1850 < val) and (val < 2100):
    1711                  target_date = cFuzzyTimestamp (
    1712                          timestamp = now.replace(year = val),
    1713                          accuracy = acc_years
    1714                  )
    1715                  tmp = {
    1716                          'data': target_date,
    1717                          'label': '%s' % target_date
    1718                  }
    1719                  matches.append(tmp)
    1720
    1721          # day X of this month
    1722          if val <= gregorian_month_length[now.month]:
    1723                  ts = now.replace(day = val)
    1724                  target_date = cFuzzyTimestamp (
    1725                          timestamp = ts,
    1726                          accuracy = acc_days
    1727                  )
    1728                  tmp = {
    1729                          'data': target_date,
    1730                          'label': _('%d. of %s (this month) - a %s') % (val, ts.strftime('%B'), ts.strftime('%A'))
    1731                  }
    1732                  matches.append(tmp)
    1733
    1734          # day X of next month
    1735          next_month = get_next_month(now)
    1736          if val <= gregorian_month_length[next_month]:
    1737                  ts = now.replace(day = val, month = next_month)
    1738                  target_date = cFuzzyTimestamp (
    1739                          timestamp = ts,
    1740                          accuracy = acc_days
    1741                  )
    1742                  tmp = {
    1743                          'data': target_date,
    1744                          'label': _('%d. of %s (next month) - a %s') % (val, ts.strftime('%B'), ts.strftime('%A'))
    1745                  }
    1746                  matches.append(tmp)
    1747
    1748          # day X of last month
    1749          last_month = get_last_month(now)
    1750          if val <= gregorian_month_length[last_month]:
    1751                  ts = now.replace(day = val, month = last_month)
    1752                  target_date = cFuzzyTimestamp (
    1753                          timestamp = ts,
    1754                          accuracy = acc_days
    1755                  )
    1756                  tmp = {
    1757                          'data': target_date,
    1758                          'label': _('%d. of %s (last month) - a %s') % (val, ts.strftime('%B'), ts.strftime('%A'))
    1759                  }
    1760                  matches.append(tmp)
    1761
    1762          # X days from now
    1763          if val <= 400:                          # more than a year ahead in days ?? nah !
    1764                  target_date = cFuzzyTimestamp(timestamp = now + pyDT.timedelta(days = val))
    1765                  tmp = {
    1766                          'data': target_date,
    1767                          'label': _('in %d day(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d'))
    1768                  }
    1769                  matches.append(tmp)
    1770
    1771          # X weeks from now
    1772          if val <= 50:                           # pregnancy takes about 40 weeks :-)
    1773                  target_date = cFuzzyTimestamp(timestamp = now + pyDT.timedelta(weeks = val))
    1774                  tmp = {
    1775                          'data': target_date,
    1776                          'label': _('in %d week(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d'))
    1777                  }
    1778                  matches.append(tmp)
    1779
    1780          # month X of ...
    1781          if val < 13:
    1782                  # ... this year
    1783                  target_date = cFuzzyTimestamp (
    1784                          timestamp = pydt_replace(now, month = val),
    1785                          accuracy = acc_months
    1786                  )
    1787                  tmp = {
    1788                          'data': target_date,
    1789                          'label': _('%s (%s this year)') % (target_date, ts.strftime('%B'))
    1790                  }
    1791                  matches.append(tmp)
    1792
    1793                  # ... next year
    1794                  target_date = cFuzzyTimestamp (
    1795                          timestamp = pydt_add(pydt_replace(now, month = val), years = 1),
    1796                          accuracy = acc_months
    1797                  )
    1798                  tmp = {
    1799                          'data': target_date,
    1800                          'label': _('%s (%s next year)') % (target_date, ts.strftime('%B'))
    1801                  }
    1802                  matches.append(tmp)
    1803
    1804                  # ... last year
    1805                  target_date = cFuzzyTimestamp (
    1806                          timestamp = pydt_add(pydt_replace(now, month = val), years = -1),
    1807                          accuracy = acc_months
    1808                  )
    1809                  tmp = {
    1810                          'data': target_date,
    1811                          'label': _('%s (%s last year)') % (target_date, ts.strftime('%B'))
    1812                  }
    1813                  matches.append(tmp)
    1814
    1815                  # fragment expansion
    1816                  matches.append ({
    1817                          'data': None,
    1818                          'label': '%s/200' % val
    1819                  })
    1820                  matches.append ({
    1821                          'data': None,
    1822                          'label': '%s/199' % val
    1823                  })
    1824                  matches.append ({
    1825                          'data': None,
    1826                          'label': '%s/198' % val
    1827                  })
    1828                  matches.append ({
    1829                          'data': None,
    1830                          'label': '%s/19' % val
    1831                  })
    1832
    1833          # reactivate when mxDT becomes available on py3k
    1834  #       # day X of ...
    1835  #       if val < 8:
    1836  #               # ... this week
    1837  #               ts = now + mxDT.RelativeDateTime(weekday = (val-1, 0))
    1838  #               target_date = cFuzzyTimestamp (
    1839  #                       timestamp = ts,
    1840  #                       accuracy = acc_days
    1841  #               )
    1842  #               tmp = {
    1843  #                       'data': target_date,
    1844  #                       'label': _('%s this week (%s of %s)') % (ts.strftime('%A'), ts.day, ts.strftime('%B'))
    1845  #               }
    1846  #               matches.append(tmp)
    1847  #
    1848  #               # ... next week
    1849  #               ts = now + mxDT.RelativeDateTime(weeks = +1, weekday = (val-1, 0))
    1850  #               target_date = cFuzzyTimestamp (
    1851  #                       timestamp = ts,
    1852  #                       accuracy = acc_days
    1853  #               )
    1854  #               tmp = {
    1855  #                       'data': target_date,
    1856  #                       'label': _('%s next week (%s of %s)') % (ts.strftime('%A'), ts.day, ts.strftime('%B'))
    1857  #               }
    1858  #               matches.append(tmp)
    1859
    1860  #               # ... last week
    1861  #               ts = now + mxDT.RelativeDateTime(weeks = -1, weekday = (val-1, 0))
    1862  #               target_date = cFuzzyTimestamp (
    1863  #                       timestamp = ts,
    1864  #                       accuracy = acc_days
    1865  #               )
    1866  #               tmp = {
    1867  #                       'data': target_date,
    1868  #                       'label': _('%s last week (%s of %s)') % (ts.strftime('%A'), ts.day, ts.strftime('%B'))
    1869  #               }
    1870  #               matches.append(tmp)
    1871
    1872          if val < 100:
    1873                  matches.append ({
    1874                          'data': None,
    1875                          'label': '%s/' % (1900 + val)
    1876                  })
    1877
    1878          # year 2k
    1879          if val == 200:
    1880                  tmp = {
    1881                          'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_days),
    1882                          'label': '%s' % target_date
    1883                  }
    1884                  matches.append(tmp)
    1885                  matches.append ({
    1886                          'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months),
    1887                          'label': '%.2d/%s' % (now.month, now.year)
    1888                  })
    1889                  matches.append ({
    1890                          'data': None,
    1891                          'label': '%s/' % now.year
    1892                  })
    1893                  matches.append ({
    1894                          'data': None,
    1895                          'label': '%s/' % (now.year + 1)
    1896                  })
    1897                  matches.append ({
    1898                          'data': None,
    1899                          'label': '%s/' % (now.year - 1)
    1900                  })
    1901
    1902          if val < 200 and val >= 190:
    1903                  for i in range(10):
    1904                          matches.append ({
    1905                                  'data': None,
    1906                                  'label': '%s%s/' % (val, i)
    1907                          })
    1908
    1909          return matches
    1910
    1911  #---------------------------------------------------------------------------
    1912 -def str2fuzzy_timestamp_matches(str2parse=None, default_time=None, patterns=None):
    1913          """
    1914          Turn a string into candidate fuzzy timestamps and auto-completions the user is likely to type.
    1915
    1916          You MUST have called locale.setlocale(locale.LC_ALL, '')
    1917          somewhere in your code previously.
    1918
    1919          @param default_time: if you want to force the time part of the time
    1920                  stamp to a given value and the user doesn't type any time part
    1921                  this value will be used
    1922          @type default_time: an mx.DateTime.DateTimeDelta instance
    1923
    1924          @param patterns: list of [time.strptime compatible date/time pattern, accuracy]
    1925          @type patterns: list
    1926          """
    1927          matches = []
    1928
    1929          matches.extend(__numbers_only(str2parse))
    1930          matches.extend(__single_slash(str2parse))
    1931
    1932          matches.extend ([
    1933                  {       'data': cFuzzyTimestamp(timestamp = m['data'], accuracy = acc_days),
    1934                          'label': m['label']
    1935                  } for m in __single_dot2py_dt(str2parse)
    1936          ])
    1937          matches.extend ([
    1938                  {       'data': cFuzzyTimestamp(timestamp = m['data'], accuracy = acc_days),
    1939                          'label': m['label']
    1940                  } for m in __single_char2py_dt(str2parse)
    1941          ])
    1942          matches.extend ([
    1943                  {       'data': cFuzzyTimestamp(timestamp = m['data'], accuracy = acc_days),
    1944                          'label': m['label']
    1945                  } for m in __explicit_offset2py_dt(str2parse)
    1946          ])
    1947
    1948          # reactivate, once mxDT becomes available on Py3k
    1949  #       # try mxDT parsers
    1950  #       try:
    1951  #               # date ?
    1952  #               date_only = mxDT.Parser.DateFromString (
    1953  #                       text = str2parse,
    1954  #                       formats = ('euro', 'iso', 'us', 'altus', 'altiso', 'lit', 'altlit', 'eurlit')
    1955  #               )
    1956  #               # time, too ?
    1957  #               time_part = mxDT.Parser.TimeFromString(text = str2parse)
    1958  #               datetime = date_only + time_part
    1959  #               if datetime == date_only:
    1960  #                       accuracy = acc_days
    1961  #                       if isinstance(default_time, mxDT.DateTimeDeltaType):
    1962  #                               datetime = date_only + default_time
    1963  #                               accuracy = acc_minutes
    1964  #               else:
    1965  #                       accuracy = acc_subseconds
    1966  #               fts = cFuzzyTimestamp (
    1967  #                       timestamp = datetime,
    1968  #                       accuracy = accuracy
    1969  #               )
    1970  #               matches.append ({
    1971  #                       'data': fts,
    1972  #                       'label': fts.format_accurately()
    1973  #               })
    1974  #       except ValueError:
    1975  #               pass
    1976  #       except mxDT.RangeError:
    1977  #               pass
    1978
    1979          if patterns is None:
    1980                  patterns = []
    1981          patterns.extend([
    1982                  ['%Y-%m-%d', acc_days],
    1983                  ['%y-%m-%d', acc_days],
    1984                  ['%Y/%m/%d', acc_days],
    1985                  ['%y/%m/%d', acc_days],
    1986
    1987                  ['%d-%m-%Y', acc_days],
    1988                  ['%d-%m-%y', acc_days],
    1989                  ['%d/%m/%Y', acc_days],
    1990                  ['%d/%m/%y', acc_days],
    1991                  ['%d.%m.%Y', acc_days],
    1992
    1993                  ['%m-%d-%Y', acc_days],
    1994                  ['%m-%d-%y', acc_days],
    1995                  ['%m/%d/%Y', acc_days],
    1996                  ['%m/%d/%y', acc_days]
    1997          ])
    1998          for pattern in patterns:
    1999                  try:
    2000                          ts = pyDT.datetime.strptime(str2parse, pattern[0]).replace (
    2001                                  hour = 11,
    2002                                  minute = 11,
    2003                                  second = 11,
    2004                                  tzinfo = gmCurrentLocalTimezone
    2005                          )
    2006                          fts = cFuzzyTimestamp(timestamp = ts, accuracy = pattern[1])
    2007                          matches.append ({
    2008                                  'data': fts,
    2009                                  'label': fts.format_accurately()
    2010                          })
    2011                  except ValueError:
    2012                          # C-level overflow
    2013                          continue
    2014
    2015          return matches
    2016
    2017  #===========================================================================
    2018  # fuzzy timestamp class
    2019  #---------------------------------------------------------------------------
    2020 -class cFuzzyTimestamp:
    2021
    2022          # FIXME: add properties for year, month, ...
    2023
    2024          """A timestamp implementation with definable inaccuracy.
    2025
    2026          This class contains an datetime.datetime instance to
    2027          hold the actual timestamp. It adds an accuracy attribute
    2028          to allow the programmer to set the precision of the
    2029          timestamp.
    2030
    2031          The timestamp will have to be initialzed with a fully
    2032          precise value (which may, of course, contain partially
    2033          fake data to make up for missing values). One can then
    2034          set the accuracy value to indicate up to which part of
    2035          the timestamp the data is valid. Optionally a modifier
    2036          can be set to indicate further specification of the
    2037          value (such as "summer", "afternoon", etc).
    2038
    2039          accuracy values:
    2040                  1: year only
    2041                  ...
    2042                  7: everything including milliseconds value
    2043
    2044          Unfortunately, one cannot directly derive a class from mx.DateTime.DateTime :-(
    2045          """
    2046          #-----------------------------------------------------------------------
    2047 -        def __init__(self, timestamp=None, accuracy=acc_subseconds, modifier=''):
    2048
    2049                  if timestamp is None:
    2050                          timestamp = pydt_now_here()
    2051                          accuracy = acc_subseconds
    2052                          modifier = ''
    2053
    2054                  if (accuracy < 1) or (accuracy > 8):
    2055                          raise ValueError('%s.__init__(): <accuracy> must be between 1 and 8' % self.__class__.__name__)
    2056
    2057                  if not isinstance(timestamp, pyDT.datetime):
    2058                          raise TypeError('%s.__init__(): <timestamp> must be of datetime.datetime type, but is %s' % self.__class__.__name__, type(timestamp))
    2059
    2060                  if timestamp.tzinfo is None:
    2061                          raise ValueError('%s.__init__(): <tzinfo> must be defined' % self.__class__.__name__)
    2062
    2063                  self.timestamp = timestamp
    2064                  self.accuracy = accuracy
    2065                  self.modifier =  modifier
    2066
    2067          #-----------------------------------------------------------------------
    2068          # magic API
    2069          #-----------------------------------------------------------------------
    2070 -        def __str__(self):
    2071                  """Return string representation meaningful to a user, also for %s formatting."""
    2072                  return self.format_accurately()
    2073
    2074          #-----------------------------------------------------------------------
    2075 -        def __repr__(self):
    2076                  """Return string meaningful to a programmer to aid in debugging."""
    2077                  tmp = '<[%s]: timestamp [%s], accuracy [%s] (%s), modifier [%s] at %s>' % (
    2078                          self.__class__.__name__,
    2079                          repr(self.timestamp),
    2080                          self.accuracy,
    2081                          _accuracy_strings[self.accuracy],
    2082                          self.modifier,
    2083                          id(self)
    2084                  )
    2085                  return tmp
    2086
    2087          #-----------------------------------------------------------------------
    2088          # external API
    2089          #-----------------------------------------------------------------------
    2090 -        def strftime(self, format_string):
    2091                  if self.accuracy == 7:
    2092                          return self.timestamp.strftime(format_string)
    2093                  return self.format_accurately()
    2094
    2095          #-----------------------------------------------------------------------
    2096 -        def Format(self, format_string):
    2097                  return self.strftime(format_string)
    2098
    2099          #-----------------------------------------------------------------------
    2100 -        def format_accurately(self, accuracy=None):
    2101                  if accuracy is None:
    2102                          accuracy = self.accuracy
    2103
    2104                  if accuracy == acc_years:
    2105                          return str(self.timestamp.year)
    2106
    2107                  if accuracy == acc_months:
    2108                          return self.timestamp.strftime('%m/%Y') # FIXME: use 3-letter month ?
    2109
    2110                  if accuracy == acc_weeks:
    2111                          return self.timestamp.strftime('%m/%Y') # FIXME: use 3-letter month ?
    2112
    2113                  if accuracy == acc_days:
    2114                          return self.timestamp.strftime('%Y-%m-%d')
    2115
    2116                  if accuracy == acc_hours:
    2117                          return self.timestamp.strftime("%Y-%m-%d %I%p")
    2118
    2119                  if accuracy == acc_minutes:
    2120                          return self.timestamp.strftime("%Y-%m-%d %H:%M")
    2121
    2122                  if accuracy == acc_seconds:
    2123                          return self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
    2124
    2125                  if accuracy == acc_subseconds:
    2126                          return self.timestamp.strftime("%Y-%m-%d %H:%M:%S.%f")
    2127
    2128                  raise ValueError('%s.format_accurately(): <accuracy> (%s) must be between 1 and 7' % (
    2129                          self.__class__.__name__,
    2130                          accuracy
    2131                  ))
    2132
    2133          #-----------------------------------------------------------------------
    2134 -        def get_pydt(self):
    2135                  return self.timestamp
    2136
    2137  #===========================================================================
    2138  # main
    2139  #---------------------------------------------------------------------------
    2140  if __name__ == '__main__':
    2141
    2142          if len(sys.argv) < 2:
    2143                  sys.exit()
    2144
    2145          if sys.argv[1] != "test":
    2146                  sys.exit()
    2147
    2148          from Gnumed.pycommon import gmI18N
    2149          from Gnumed.pycommon import gmLog2
    2150
    2151          #-----------------------------------------------------------------------
    2152          intervals_as_str = [
    2153                  '7', '12', ' 12', '12 ', ' 12 ', '      12      ', '0', '~12', '~ 12', ' ~ 12', '       ~       12 ',
    2154                  '12a', '12 a', '12      a', '12j', '12J', '12y', '12Y', '       ~       12      a        ', '~0a',
    2155                  '12m', '17 m', '12      m', '17M', '    ~       17      m        ', ' ~ 3       / 12    ', '7/12', '0/12',
    2156                  '12w', '17 w', '12      w', '17W', '    ~       17      w        ', ' ~ 15      / 52', '2/52', '0/52',
    2157                  '12d', '17 d', '12      t', '17D', '    ~       17      T        ', ' ~ 12      / 7', '3/7', '0/7',
    2158                  '12h', '17 h', '12      H', '17H', '    ~       17      h        ', ' ~ 36      / 24', '7/24', '0/24',
    2159                  ' ~ 36  / 60', '7/60', '190/60', '0/60',
    2160                  '12a1m', '12 a 1  M', '12       a17m', '12j             12m', '12J7m', '12y7m', '12Y7M', '      ~       12      a        37 m   ', '~0a0m',
    2161                  '10m1w',
    2162                  'invalid interval input'
    2163          ]
    2164          #-----------------------------------------------------------------------
    2165 -        def test_format_interval():
    2166                  intv = pyDT.timedelta(minutes=1, seconds=2)
    2167                  for acc in _accuracy_strings.keys():
    2168                          print ('[%s]: "%s" -> "%s"' % (acc, intv, format_interval(intv, acc)))
    2169                  return
    2170
    2171                  for tmp in intervals_as_str:
    2172                          intv = str2interval(str_interval = tmp)
    2173                          if intv is None:
    2174                                  print(tmp, '->', intv)
    2175                                  continue
    2176                          for acc in _accuracy_strings.keys():
    2177                                  print ('[%s]: "%s" -> "%s"' % (acc, tmp, format_interval(intv, acc)))
    2178
    2179          #-----------------------------------------------------------------------
    2180 -        def test_format_interval_medically():
    2181
    2182                  intervals = [
    2183                          pyDT.timedelta(seconds = 1),
    2184                          pyDT.timedelta(seconds = 5),
    2185                          pyDT.timedelta(seconds = 30),
    2186                          pyDT.timedelta(seconds = 60),
    2187                          pyDT.timedelta(seconds = 94),
    2188                          pyDT.timedelta(seconds = 120),
    2189                          pyDT.timedelta(minutes = 5),
    2190                          pyDT.timedelta(minutes = 30),
    2191                          pyDT.timedelta(minutes = 60),
    2192                          pyDT.timedelta(minutes = 90),
    2193                          pyDT.timedelta(minutes = 120),
    2194                          pyDT.timedelta(minutes = 200),
    2195                          pyDT.timedelta(minutes = 400),
    2196                          pyDT.timedelta(minutes = 600),
    2197                          pyDT.timedelta(minutes = 800),
    2198                          pyDT.timedelta(minutes = 1100),
    2199                          pyDT.timedelta(minutes = 2000),
    2200                          pyDT.timedelta(minutes = 3500),
    2201                          pyDT.timedelta(minutes = 4000),
    2202                          pyDT.timedelta(hours = 1),
    2203                          pyDT.timedelta(hours = 2),
    2204                          pyDT.timedelta(hours = 4),
    2205                          pyDT.timedelta(hours = 8),
    2206                          pyDT.timedelta(hours = 12),
    2207                          pyDT.timedelta(hours = 20),
    2208                          pyDT.timedelta(hours = 23),
    2209                          pyDT.timedelta(hours = 24),
    2210                          pyDT.timedelta(hours = 25),
    2211                          pyDT.timedelta(hours = 30),
    2212                          pyDT.timedelta(hours = 48),
    2213                          pyDT.timedelta(hours = 98),
    2214                          pyDT.timedelta(hours = 120),
    2215                          pyDT.timedelta(days = 1),
    2216                          pyDT.timedelta(days = 2),
    2217                          pyDT.timedelta(days = 4),
    2218                          pyDT.timedelta(days = 16),
    2219                          pyDT.timedelta(days = 29),
    2220                          pyDT.timedelta(days = 30),
    2221                          pyDT.timedelta(days = 31),
    2222                          pyDT.timedelta(days = 37),
    2223                          pyDT.timedelta(days = 40),
    2224                          pyDT.timedelta(days = 47),
    2225                          pyDT.timedelta(days = 126),
    2226                          pyDT.timedelta(days = 127),
    2227                          pyDT.timedelta(days = 128),
    2228                          pyDT.timedelta(days = 300),
    2229                          pyDT.timedelta(days = 359),
    2230                          pyDT.timedelta(days = 360),
    2231                          pyDT.timedelta(days = 361),
    2232                          pyDT.timedelta(days = 362),
    2233                          pyDT.timedelta(days = 363),
    2234                          pyDT.timedelta(days = 364),
    2235                          pyDT.timedelta(days = 365),
    2236                          pyDT.timedelta(days = 366),
    2237                          pyDT.timedelta(days = 367),
    2238                          pyDT.timedelta(days = 400),
    2239                          pyDT.timedelta(weeks = 52 * 30),
    2240                          pyDT.timedelta(weeks = 52 * 79, days = 33)
    2241                  ]
    2242
    2243                  idx = 1
    2244                  for intv in intervals:
    2245                          print ('%s) %s -> %s' % (idx, intv, format_interval_medically(intv)))
    2246                          idx += 1
    2247          #-----------------------------------------------------------------------
    2248 -        def test_str2interval():
    2249                  print ("testing str2interval()")
    2250                  print ("----------------------")
    2251
    2252                  for interval_as_str in intervals_as_str:
    2253                          print ("input: <%s>" % interval_as_str)
    2254                          print ("  ==>", str2interval(str_interval=interval_as_str))
    2255
    2256                  return True
    2257          #-------------------------------------------------
    2258 -        def test_date_time():
    2259                  print ("DST currently in effect:", dst_currently_in_effect)
    2260                  print ("current UTC offset:", current_local_utc_offset_in_seconds, "seconds")
    2261                  print ("current timezone (interval):", current_local_timezone_interval)
    2262                  print ("current timezone (ISO conformant numeric string):", current_local_iso_numeric_timezone_string)
    2263                  print ("local timezone class:", cPlatformLocalTimezone)
    2264                  print ("")
    2265                  tz = cPlatformLocalTimezone()
    2266                  print ("local timezone instance:", tz)
    2267                  print (" (total) UTC offset:", tz.utcoffset(pyDT.datetime.now()))
    2268                  print (" DST adjustment:", tz.dst(pyDT.datetime.now()))
    2269                  print (" timezone name:", tz.tzname(pyDT.datetime.now()))
    2270                  print ("")
    2271                  print ("current local timezone:", gmCurrentLocalTimezone)
    2272                  print (" (total) UTC offset:", gmCurrentLocalTimezone.utcoffset(pyDT.datetime.now()))
    2273                  print (" DST adjustment:", gmCurrentLocalTimezone.dst(pyDT.datetime.now()))
    2274                  print (" timezone name:", gmCurrentLocalTimezone.tzname(pyDT.datetime.now()))
    2275                  print ("")
    2276                  print ("now here:", pydt_now_here())
    2277                  print ("")
    2278
    2279          #-------------------------------------------------
    2280 -        def test_str2fuzzy_timestamp_matches():
    2281                  print ("testing function str2fuzzy_timestamp_matches")
    2282                  print ("--------------------------------------------")
    2283
    2284                  val = None
    2285                  while val != 'exit':
    2286                          val = input('Enter date fragment ("exit" quits): ')
    2287                          matches = str2fuzzy_timestamp_matches(str2parse = val)
    2288                          for match in matches:
    2289                                  print ('label shown  :', match['label'])
    2290                                  print ('data attached:', match['data'], match['data'].timestamp)
    2291                                  print ("")
    2292                          print ("---------------")
    2293
    2294          #-------------------------------------------------
    2295 -        def test_cFuzzyTimeStamp():
    2296                  print ("testing fuzzy timestamp class")
    2297                  print ("-----------------------------")
    2298
    2299                  fts = cFuzzyTimestamp()
    2300                  print ("\nfuzzy timestamp <%s '%s'>" % ('class', fts.__class__.__name__))
    2301                  for accuracy in range(1,8):
    2302                          fts.accuracy = accuracy
    2303                          print ("  accuracy         : %s (%s)" % (accuracy, _accuracy_strings[accuracy]))
    2304                          print ("  format_accurately:", fts.format_accurately())
    2305                          print ("  strftime()       :", fts.strftime('%Y %b %d  %H:%M:%S'))
    2306                          print ("  print ...        :", fts)
    2307                          print ("  print '%%s' %% ... : %s" % fts)
    2308                          print ("  str()            :", str(fts))
    2309                          print ("  repr()           :", repr(fts))
    2310                          input('press ENTER to continue')
    2311
    2312          #-------------------------------------------------
    2313 -        def test_get_pydt():
    2314                  print ("testing platform for handling dates before 1970")
    2315                  print ("-----------------------------------------------")
    2316                  ts = mxDT.DateTime(1935, 4, 2)
    2317                  fts = cFuzzyTimestamp(timestamp=ts)
    2318                  print ("fts           :", fts)
    2319                  print ("fts.get_pydt():", fts.get_pydt())
    2320          #-------------------------------------------------
    2321 -        def test_calculate_apparent_age():
    2322                  # test leap year glitches
    2323                  start = pydt_now_here().replace(year = 2000).replace(month = 2).replace(day = 29)
    2324                  end = pydt_now_here().replace(year = 2012).replace(month = 2).replace(day = 27)
    2325                  print ("start is leap year: 29.2.2000")
    2326                  print (" ", calculate_apparent_age(start = start, end = end))
    2327                  print (" ", format_apparent_age_medically(calculate_apparent_age(start = start)))
    2328
    2329                  start = pydt_now_here().replace(month = 10).replace(day = 23).replace(year = 1974)
    2330                  end = pydt_now_here().replace(year = 2012).replace(month = 2).replace(day = 29)
    2331                  print ("end is leap year: 29.2.2012")
    2332                  print (" ", calculate_apparent_age(start = start, end = end))
    2333                  print (" ", format_apparent_age_medically(calculate_apparent_age(start = start)))
    2334
    2335                  start = pydt_now_here().replace(year = 2000).replace(month = 2).replace(day = 29)
    2336                  end = pydt_now_here().replace(year = 2012).replace(month = 2).replace(day = 29)
    2337                  print ("start is leap year: 29.2.2000")
    2338                  print ("end is leap year: 29.2.2012")
    2339                  print (" ", calculate_apparent_age(start = start, end = end))
    2340                  print (" ", format_apparent_age_medically(calculate_apparent_age(start = start)))
    2341
    2342                  print ("leap year tests worked")
    2343
    2344                  start = pydt_now_here().replace(month = 10).replace(day = 23).replace(year = 1974)
    2345                  print (calculate_apparent_age(start = start))
    2346                  print (format_apparent_age_medically(calculate_apparent_age(start = start)))
    2347
    2348                  start = pydt_now_here().replace(month = 3).replace(day = 13).replace(year = 1979)
    2349                  print (calculate_apparent_age(start = start))
    2350                  print (format_apparent_age_medically(calculate_apparent_age(start = start)))
    2351
    2352                  start = pydt_now_here().replace(month = 2, day = 2).replace(year = 1979)
    2353                  end = pydt_now_here().replace(month = 3).replace(day = 31).replace(year = 1979)
    2354                  print (calculate_apparent_age(start = start, end = end))
    2355
    2356                  start = pydt_now_here().replace(month = 7, day = 21).replace(year = 2009)
    2357                  print (format_apparent_age_medically(calculate_apparent_age(start = start)))
    2358
    2359                  print ("-------")
    2360                  start = pydt_now_here().replace(month = 1).replace(day = 23).replace(hour = 12).replace(minute = 11).replace(year = 2011)
    2361                  print (calculate_apparent_age(start = start))
    2362                  print (format_apparent_age_medically(calculate_apparent_age(start = start)))
    2363          #-------------------------------------------------
    2364 -        def test_str2pydt():
    2365                  print ("testing function str2pydt_matches")
    2366                  print ("---------------------------------")
    2367
    2368                  val = None
    2369                  while val != 'exit':
    2370                          val = input('Enter date fragment ("exit" quits): ')
    2371                          matches = str2pydt_matches(str2parse = val)
    2372                          for match in matches:
    2373                                  print ('label shown  :', match['label'])
    2374                                  print ('data attached:', match['data'])
    2375                                  print ("")
    2376                          print ("---------------")
    2377
    2378          #-------------------------------------------------
    2379 -        def test_pydt_strftime():
    2380                  dt = pydt_now_here()
    2381                  print (pydt_strftime(dt, '-(%Y %b %d)-'))
    2382                  print (pydt_strftime(dt))
    2383                  print (pydt_strftime(dt, accuracy = acc_days))
    2384                  print (pydt_strftime(dt, accuracy = acc_minutes))
    2385                  print (pydt_strftime(dt, accuracy = acc_seconds))
    2386                  dt = dt.replace(year = 1899)
    2387                  print (pydt_strftime(dt))
    2388                  print (pydt_strftime(dt, accuracy = acc_days))
    2389                  print (pydt_strftime(dt, accuracy = acc_minutes))
    2390                  print (pydt_strftime(dt, accuracy = acc_seconds))
    2391                  dt = dt.replace(year = 198)
    2392                  print (pydt_strftime(dt, accuracy = acc_seconds))
    2393          #-------------------------------------------------
    2394 -        def test_is_leap_year():
    2395                  for year in range(1995, 2017):
    2396                          print (year, "leaps:", is_leap_year(year))
    2397
    2398          #-------------------------------------------------
    2399 -        def test_get_date_of_weekday_in_week_of_date():
    2400                  dt = pydt_now_here()
    2401                  print('weekday', base_dt.isoweekday(), '(2day):', dt)
    2402                  for weekday in range(8):
    2403                          dt = get_date_of_weekday_in_week_of_date(weekday)
    2404                          print('weekday', weekday, '(same):', dt)
    2405                          dt = get_date_of_weekday_following_date(weekday)
    2406                          print('weekday', weekday, '(next):', dt)
    2407                  try:
    2408                          get_date_of_weekday_in_week_of_date(8)
    2409                  except ValueError as exc:
    2410                          print(exc)
    2411                  try:
    2412                          get_date_of_weekday_following_date(8)
    2413                  except ValueError as exc:
    2414                          print(exc)
    2415
    2416          #-------------------------------------------------
    2417          # GNUmed libs
    2418          gmI18N.activate_locale()
    2419          gmI18N.install_domain('gnumed')
    2420
    2421          init()
    2422
    2423          test_date_time()
    2424          #test_str2fuzzy_timestamp_matches()
    2425          #test_get_date_of_weekday_in_week_of_date()
    2426          #test_cFuzzyTimeStamp()
    2427          #test_get_pydt()
    2428          #test_str2interval()
    2429          #test_format_interval()
    2430          #test_format_interval_medically()
    2431          #test_str2pydt()
    2432          #test_pydt_strftime()
    2433          #test_calculate_apparent_age()
    2434          #test_is_leap_year()
    2435
    2436  #===========================================================================
    2437