| Home | Trees | Indices | Help |
|
|---|
|
|
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
207
208 #-----------------------------------------------------------------------
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 #-----------------------------------------------------------------------
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 #-----------------------------------------------------------------------
231 if self._isdst(dt):
232 return self._DST_OFFSET_FROM_UTC
233 return self._nonDST_OFFSET_FROM_UTC
234
235 #-----------------------------------------------------------------------
240
241 #-----------------------------------------------------------------------
244
245 #-----------------------------------------------------------------------
256
257 #===========================================================================
258 # convenience functions
259 #---------------------------------------------------------------------------
265
266 #---------------------------------------------------------------------------
272
273 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #===========================================================================
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
462 """Returns NOW @ HERE (IOW, in the local timezone."""
463 return pyDT.datetime.now(gmCurrentLocalTimezone)
464
465 #---------------------------------------------------------------------------
468
469 #===========================================================================
470 # wxPython conversions
471 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #---------------------------------------------------------------------------
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 #-----------------------------------------------------------------------
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 #-----------------------------------------------------------------------
2069 """Return string representation meaningful to a user, also for %s formatting."""
2070 return self.format_accurately()
2071
2072 #-----------------------------------------------------------------------
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 #-----------------------------------------------------------------------
2089 if self.accuracy == 7:
2090 return self.timestamp.strftime(format_string)
2091 return self.format_accurately()
2092
2093 #-----------------------------------------------------------------------
2095 return self.strftime(format_string)
2096
2097 #-----------------------------------------------------------------------
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 #-----------------------------------------------------------------------
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 #-----------------------------------------------------------------------
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 #-----------------------------------------------------------------------
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 #-----------------------------------------------------------------------
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 #-------------------------------------------------
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 #-------------------------------------------------
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 #-------------------------------------------------
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 #-------------------------------------------------
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 #-------------------------------------------------
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 #-------------------------------------------------
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 #-------------------------------------------------
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 #-------------------------------------------------
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 #-------------------------------------------------
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
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Thu Jul 23 01:55:31 2020 | http://epydoc.sourceforge.net |