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