1 __doc__ = """
2 GNUmed date/time handling.
3
4 This modules provides access to date/time handling
5 and offers an fuzzy timestamp implementation
6
7 It utilizes
8
9 - Python time
10 - Python datetime
11 - mxDateTime
12
13 Note that if you want locale-aware formatting you need to call
14
15 locale.setlocale(locale.LC_ALL, '')
16
17 somewhere before importing this script.
18
19 Note regarding UTC offsets
20 --------------------------
21
22 Looking from Greenwich:
23 WEST (IOW "behind"): negative values
24 EAST (IOW "ahead"): positive values
25
26 This is in compliance with what datetime.tzinfo.utcoffset()
27 does but NOT what time.altzone/time.timezone do !
28
29 This module also implements a class which allows the
30 programmer to define the degree of fuzziness, uncertainty
31 or imprecision of the timestamp contained within.
32
33 This is useful in fields such as medicine where only partial
34 timestamps may be known for certain events.
35 """
36
37 __version__ = "$Revision: 1.34 $"
38 __author__ = "K. Hilbert <Karsten.Hilbert@gmx.net>"
39 __license__ = "GPL (details at http://www.gnu.org)"
40
41
42 import sys, datetime as pyDT, time, os, re as regex, locale, logging
43
44
45
46 import mx.DateTime as mxDT
47 import psycopg2
48
49
50 if __name__ == '__main__':
51 sys.path.insert(0, '../../')
52 from Gnumed.pycommon import gmI18N
53
54
55 _log = logging.getLogger('gm.datetime')
56 _log.info(__version__)
57 _log.info(u'mx.DateTime version: %s', mxDT.__version__)
58
59 dst_locally_in_use = None
60 dst_currently_in_effect = None
61
62 current_local_utc_offset_in_seconds = None
63 current_local_timezone_interval = None
64 current_local_iso_numeric_timezone_string = None
65 current_local_timezone_name = None
66 py_timezone_name = None
67 py_dst_timezone_name = None
68
69 cLocalTimezone = psycopg2.tz.LocalTimezone
70 cFixedOffsetTimezone = psycopg2.tz.FixedOffsetTimezone
71 gmCurrentLocalTimezone = 'gmCurrentLocalTimezone not initialized'
72
73
74 ( acc_years,
75 acc_months,
76 acc_weeks,
77 acc_days,
78 acc_hours,
79 acc_minutes,
80 acc_seconds,
81 acc_subseconds
82 ) = range(1,9)
83
84 _accuracy_strings = {
85 1: 'years',
86 2: 'months',
87 3: 'weeks',
88 4: 'days',
89 5: 'hours',
90 6: 'minutes',
91 7: 'seconds',
92 8: 'subseconds'
93 }
94
95 gregorian_month_length = {
96 1: 31,
97 2: 28,
98 3: 31,
99 4: 30,
100 5: 31,
101 6: 30,
102 7: 31,
103 8: 31,
104 9: 30,
105 10: 31,
106 11: 30,
107 12: 31
108 }
109
110 avg_days_per_gregorian_year = 365
111 avg_days_per_gregorian_month = 30
112 avg_seconds_per_day = 24 * 60 * 60
113 days_per_week = 7
114
115
116
117
119
120 _log.debug('mx.DateTime.now(): [%s]' % mxDT.now())
121 _log.debug('datetime.now() : [%s]' % pyDT.datetime.now())
122 _log.debug('time.localtime() : [%s]' % str(time.localtime()))
123 _log.debug('time.gmtime() : [%s]' % str(time.gmtime()))
124
125 try:
126 _log.debug('$TZ: [%s]' % os.environ['TZ'])
127 except KeyError:
128 _log.debug('$TZ not defined')
129
130 _log.debug('time.daylight: [%s] (whether or not DST is locally used at all)' % time.daylight)
131 _log.debug('time.timezone: [%s] seconds' % time.timezone)
132 _log.debug('time.altzone : [%s] seconds' % time.altzone)
133 _log.debug('time.tzname : [%s / %s] (non-DST / DST)' % time.tzname)
134 _log.debug('mx.DateTime.now().gmtoffset(): [%s]' % mxDT.now().gmtoffset())
135
136 global py_timezone_name
137 py_timezone_name = time.tzname[0].decode(gmI18N.get_encoding(), 'replace')
138
139 global py_dst_timezone_name
140 py_dst_timezone_name = time.tzname[1].decode(gmI18N.get_encoding(), 'replace')
141
142 global dst_locally_in_use
143 dst_locally_in_use = (time.daylight != 0)
144
145 global dst_currently_in_effect
146 dst_currently_in_effect = bool(time.localtime()[8])
147 _log.debug('DST currently in effect: [%s]' % dst_currently_in_effect)
148
149 if (not dst_locally_in_use) and dst_currently_in_effect:
150 _log.error('system inconsistency: DST not in use - but DST currently in effect ?')
151
152 global current_local_utc_offset_in_seconds
153 msg = 'DST currently%sin effect: using UTC offset of [%s] seconds instead of [%s] seconds'
154 if dst_currently_in_effect:
155 current_local_utc_offset_in_seconds = time.altzone * -1
156 _log.debug(msg % (' ', time.altzone * -1, time.timezone * -1))
157 else:
158 current_local_utc_offset_in_seconds = time.timezone * -1
159 _log.debug(msg % (' not ', time.timezone * -1, time.altzone * -1))
160
161 if current_local_utc_offset_in_seconds > 0:
162 _log.debug('UTC offset is positive, assuming EAST of Greenwich (clock is "ahead")')
163 elif current_local_utc_offset_in_seconds < 0:
164 _log.debug('UTC offset is negative, assuming WEST of Greenwich (clock is "behind")')
165 else:
166 _log.debug('UTC offset is ZERO, assuming Greenwich Time')
167
168 global current_local_timezone_interval
169 current_local_timezone_interval = mxDT.now().gmtoffset()
170 _log.debug('ISO timezone: [%s] (taken from mx.DateTime.now().gmtoffset())' % current_local_timezone_interval)
171
172 global current_local_iso_numeric_timezone_string
173 current_local_iso_numeric_timezone_string = str(current_local_timezone_interval).replace(',', '.')
174
175 global current_local_timezone_name
176 try:
177 current_local_timezone_name = os.environ['TZ'].decode(gmI18N.get_encoding(), 'replace')
178 except KeyError:
179 if dst_currently_in_effect:
180 current_local_timezone_name = time.tzname[1].decode(gmI18N.get_encoding(), 'replace')
181 else:
182 current_local_timezone_name = time.tzname[0].decode(gmI18N.get_encoding(), 'replace')
183
184
185
186
187
188
189 global gmCurrentLocalTimezone
190 gmCurrentLocalTimezone = cFixedOffsetTimezone (
191 offset = (current_local_utc_offset_in_seconds / 60),
192 name = current_local_iso_numeric_timezone_string
193 )
194
196 """Returns NOW @ HERE (IOW, in the local timezone."""
197 return pyDT.datetime.now(gmCurrentLocalTimezone)
198
201
205
206
207
209 if not wxDate.IsValid():
210 raise ArgumentError (u'invalid wxDate: %s-%s-%s %s:%s %s.%s',
211 wxDate.GetYear(),
212 wxDate.GetMonth(),
213 wxDate.GetDay(),
214 wxDate.GetHour(),
215 wxDate.GetMinute(),
216 wxDate.GetSecond(),
217 wxDate.GetMillisecond()
218 )
219
220 try:
221 return pyDT.datetime (
222 year = wxDate.GetYear(),
223 month = wxDate.GetMonth() + 1,
224 day = wxDate.GetDay(),
225 tzinfo = gmCurrentLocalTimezone
226 )
227 except:
228 _log.debug (u'error converting wxDateTime to Python: %s-%s-%s %s:%s %s.%s',
229 wxDate.GetYear(),
230 wxDate.GetMonth(),
231 wxDate.GetDay(),
232 wxDate.GetHour(),
233 wxDate.GetMinute(),
234 wxDate.GetSecond(),
235 wxDate.GetMillisecond()
236 )
237 raise
238
240 _log.debug(u'setting wx.DateTime from: %s-%s-%s', py_dt.year, py_dt.month, py_dt.day)
241
242
243
244 wxdt = wx.DateTimeFromDMY(py_dt.day, py_dt.month-1, py_dt.year)
245 return wxdt
246
247
248
300
365
367 """The result of this is a tuple (years, ..., seconds) as one would
368 'expect' a date to look like, that is, simple differences between
369 the fields.
370
371 No need for 100/400 years leap days rule because 2000 WAS a leap year.
372
373 This does not take into account time zones which may
374 shift the result by one day.
375
376 <start> and <end> must by python datetime instances
377 <end> is assumed to be "now" if not given
378 """
379 if end is None:
380 end = pyDT.datetime.now(gmCurrentLocalTimezone)
381
382 if end < start:
383 raise ValueError('calculate_apparent_age(): <end> (%s) before <start> %s' % (end, start))
384
385 if end == start:
386 years = months = days = hours = minutes = seconds = 0
387 return (years, months, days, hours, minutes, seconds)
388
389
390 years = end.year - start.year
391 end = end.replace(year = start.year)
392 if end < start:
393 years = years - 1
394
395
396 if end.month == start.month:
397 months = 0
398 else:
399 months = end.month - start.month
400 if months < 0:
401 months = months + 12
402 if end.day > gregorian_month_length[start.month]:
403 end = end.replace(month = start.month, day = gregorian_month_length[start.month])
404 else:
405 end = end.replace(month = start.month)
406 if end < start:
407 months = months - 1
408
409
410 if end.day == start.day:
411 days = 0
412 else:
413 days = end.day - start.day
414 if days < 0:
415 days = days + gregorian_month_length[start.month]
416 end = end.replace(day = start.day)
417 if end < start:
418 days = days - 1
419
420
421 if end.hour == start.hour:
422 hours = 0
423 else:
424 hours = end.hour - start.hour
425 if hours < 0:
426 hours = hours + 24
427 end = end.replace(hour = start.hour)
428 if end < start:
429 hours = hours - 1
430
431
432 if end.minute == start.minute:
433 minutes = 0
434 else:
435 minutes = end.minute - start.minute
436 if minutes < 0:
437 minutes = minutes + 60
438 end = end.replace(minute = start.minute)
439 if end < start:
440 minutes = minutes - 1
441
442
443 if end.second == start.second:
444 seconds = 0
445 else:
446 seconds = end.second - start.second
447 if seconds < 0:
448 seconds = seconds + 60
449 end = end.replace(second = start.second)
450 if end < start:
451 seconds = seconds - 1
452
453 return (years, months, days, hours, minutes, seconds)
454
558
560
561 unit_keys = {
562 'year': _('yYaA_keys_year'),
563 'month': _('mM_keys_month'),
564 'week': _('wW_keys_week'),
565 'day': _('dD_keys_day'),
566 'hour': _('hH_keys_hour')
567 }
568
569 str_interval = str_interval.strip()
570
571
572 keys = '|'.join(list(unit_keys['year'].replace('_keys_year', u'')))
573 if regex.match(u'^~*(\s|\t)*\d+(%s)*$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
574 return pyDT.timedelta(days = (int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]) * avg_days_per_gregorian_year))
575
576
577 keys = '|'.join(list(unit_keys['month'].replace('_keys_month', u'')))
578 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
579 years, months = divmod (
580 int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]),
581 12
582 )
583 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month)))
584
585
586 keys = '|'.join(list(unit_keys['week'].replace('_keys_week', u'')))
587 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
588 return pyDT.timedelta(weeks = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
589
590
591 keys = '|'.join(list(unit_keys['day'].replace('_keys_day', u'')))
592 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
593 return pyDT.timedelta(days = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
594
595
596 keys = '|'.join(list(unit_keys['hour'].replace('_keys_hour', u'')))
597 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
598 return pyDT.timedelta(hours = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
599
600
601 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*12$', str_interval, flags = regex.LOCALE | regex.UNICODE):
602 years, months = divmod (
603 int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]),
604 12
605 )
606 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month)))
607
608
609 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*52$', str_interval, flags = regex.LOCALE | regex.UNICODE):
610
611 return pyDT.timedelta(weeks = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
612
613
614 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*7$', str_interval, flags = regex.LOCALE | regex.UNICODE):
615 return pyDT.timedelta(days = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
616
617
618 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*24$', str_interval, flags = regex.LOCALE | regex.UNICODE):
619 return pyDT.timedelta(hours = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
620
621
622 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*60$', str_interval, flags = regex.LOCALE | regex.UNICODE):
623 return pyDT.timedelta(minutes = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
624
625
626 keys_year = '|'.join(list(unit_keys['year'].replace('_keys_year', u'')))
627 keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', u'')))
628 if regex.match(u'^~*(\s|\t)*\d+(%s|\s|\t)+\d+(\s|\t)*(%s)+$' % (keys_year, keys_month), str_interval, flags = regex.LOCALE | regex.UNICODE):
629 parts = regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)
630 years, months = divmod(int(parts[1]), 12)
631 years += int(parts[0])
632 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month)))
633
634
635 keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', u'')))
636 keys_week = '|'.join(list(unit_keys['week'].replace('_keys_week', u'')))
637 if regex.match(u'^~*(\s|\t)*\d+(%s|\s|\t)+\d+(\s|\t)*(%s)+$' % (keys_month, keys_week), str_interval, flags = regex.LOCALE | regex.UNICODE):
638 parts = regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)
639 months, weeks = divmod(int(parts[1]), 4)
640 months += int(parts[0])
641 return pyDT.timedelta(days = ((months * avg_days_per_gregorian_month) + (weeks * days_per_week)))
642
643 return None
644
645
646
647
649 """
650 Default is 'hdwm':
651 h - hours
652 d - days
653 w - weeks
654 m - months
655 y - years
656
657 This also defines the significance of the order of the characters.
658 """
659 if offset_chars is None:
660 offset_chars = _('hdwmy (single character date offset triggers)')[:5].lower()
661
662
663 if not regex.match(u"^(\s|\t)*(\+|-)?(\s|\t)*\d{1,2}(\s|\t)*[%s](\s|\t)*$" % offset_chars, str2parse, flags = regex.LOCALE | regex.UNICODE):
664 return []
665 val = int(regex.findall(u'\d{1,2}', str2parse, flags = regex.LOCALE | regex.UNICODE)[0])
666 offset_char = regex.findall(u'[%s]' % offset_chars, str2parse, flags = regex.LOCALE | regex.UNICODE)[0].lower()
667
668 now = mxDT.now()
669 enc = gmI18N.get_encoding()
670
671
672 is_future = True
673 if str2parse.find('-') > -1:
674 is_future = False
675
676 ts = None
677
678 if offset_char == offset_chars[0]:
679 if is_future:
680 ts = now + mxDT.RelativeDateTime(hours = val)
681 label = _('in %d hour(s) - %s') % (val, ts.strftime('%H:%M'))
682 else:
683 ts = now - mxDT.RelativeDateTime(hours = val)
684 label = _('%d hour(s) ago - %s') % (val, ts.strftime('%H:%M'))
685 accuracy = acc_subseconds
686
687 elif offset_char == offset_chars[1]:
688 if is_future:
689 ts = now + mxDT.RelativeDateTime(days = val)
690 label = _('in %d day(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
691 else:
692 ts = now - mxDT.RelativeDateTime(days = val)
693 label = _('%d day(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
694 accuracy = acc_days
695
696 elif offset_char == offset_chars[2]:
697 if is_future:
698 ts = now + mxDT.RelativeDateTime(weeks = val)
699 label = _('in %d week(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
700 else:
701 ts = now - mxDT.RelativeDateTime(weeks = val)
702 label = _('%d week(s) ago - %s)') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
703 accuracy = acc_days
704
705 elif offset_char == offset_chars[3]:
706 if is_future:
707 ts = now + mxDT.RelativeDateTime(months = val)
708 label = _('in %d month(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
709 else:
710 ts = now - mxDT.RelativeDateTime(months = val)
711 label = _('%d month(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
712 accuracy = acc_days
713
714 elif offset_char == offset_chars[4]:
715 if is_future:
716 ts = now + mxDT.RelativeDateTime(years = val)
717 label = _('in %d year(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
718 else:
719 ts = now - mxDT.RelativeDateTime(years = val)
720 label = _('%d year(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
721 accuracy = acc_months
722
723 if ts is None:
724 return []
725
726 tmp = {
727 'data': cFuzzyTimestamp(timestamp = ts, accuracy = accuracy),
728 'label': label
729 }
730 return [tmp]
731
733 """This matches on single characters.
734
735 Spaces and tabs are discarded.
736
737 Default is 'ndmy':
738 n - now
739 d - toDay
740 m - toMorrow Someone please suggest a synonym !
741 y - yesterday
742
743 This also defines the significance of the order of the characters.
744 """
745 if trigger_chars is None:
746 trigger_chars = _('ndmy (single character date triggers)')[:4].lower()
747
748 if not regex.match(u'^(\s|\t)*[%s]{1}(\s|\t)*$' % trigger_chars, str2parse, flags = regex.LOCALE | regex.UNICODE):
749 return []
750 val = str2parse.strip().lower()
751
752 now = mxDT.now()
753 enc = gmI18N.get_encoding()
754
755
756
757
758 if val == trigger_chars[0]:
759 ts = now
760 return [{
761 'data': cFuzzyTimestamp (
762 timestamp = ts,
763 accuracy = acc_subseconds
764 ),
765 'label': _('right now (%s, %s)') % (ts.strftime('%A').decode(enc), ts)
766 }]
767
768
769 if val == trigger_chars[1]:
770 return [{
771 'data': cFuzzyTimestamp (
772 timestamp = now,
773 accuracy = acc_days
774 ),
775 'label': _('today (%s)') % now.strftime('%A, %Y-%m-%d').decode(enc)
776 }]
777
778
779 if val == trigger_chars[2]:
780 ts = now + mxDT.RelativeDateTime(days = +1)
781 return [{
782 'data': cFuzzyTimestamp (
783 timestamp = ts,
784 accuracy = acc_days
785 ),
786 'label': _('tomorrow (%s)') % ts.strftime('%A, %Y-%m-%d').decode(enc)
787 }]
788
789
790 if val == trigger_chars[3]:
791 ts = now + mxDT.RelativeDateTime(days = -1)
792 return [{
793 'data': cFuzzyTimestamp (
794 timestamp = ts,
795 accuracy = acc_days
796 ),
797 'label': _('yesterday (%s)') % ts.strftime('%A, %Y-%m-%d').decode(enc)
798 }]
799
800 return []
801
803 """Expand fragments containing a single slash.
804
805 "5/"
806 - 2005/ (2000 - 2025)
807 - 1995/ (1990 - 1999)
808 - Mai/current year
809 - Mai/next year
810 - Mai/last year
811 - Mai/200x
812 - Mai/20xx
813 - Mai/199x
814 - Mai/198x
815 - Mai/197x
816 - Mai/19xx
817 """
818 matches = []
819 now = mxDT.now()
820 if regex.match(u"^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE):
821 val = int(regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE)[0])
822
823 if val < 100 and val >= 0:
824 matches.append ({
825 'data': None,
826 'label': '%s/' % (val + 1900)
827 })
828
829 if val < 26 and val >= 0:
830 matches.append ({
831 'data': None,
832 'label': '%s/' % (val + 2000)
833 })
834
835 if val < 10 and val >= 0:
836 matches.append ({
837 'data': None,
838 'label': '%s/' % (val + 1990)
839 })
840
841 if val < 13 and val > 0:
842 matches.append ({
843 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months),
844 'label': '%.2d/%s' % (val, now.year)
845 })
846 ts = now + mxDT.RelativeDateTime(years = 1)
847 matches.append ({
848 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months),
849 'label': '%.2d/%s' % (val, ts.year)
850 })
851 ts = now + mxDT.RelativeDateTime(years = -1)
852 matches.append ({
853 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months),
854 'label': '%.2d/%s' % (val, ts.year)
855 })
856 matches.append ({
857 'data': None,
858 'label': '%.2d/200' % val
859 })
860 matches.append ({
861 'data': None,
862 'label': '%.2d/20' % val
863 })
864 matches.append ({
865 'data': None,
866 'label': '%.2d/199' % val
867 })
868 matches.append ({
869 'data': None,
870 'label': '%.2d/198' % val
871 })
872 matches.append ({
873 'data': None,
874 'label': '%.2d/197' % val
875 })
876 matches.append ({
877 'data': None,
878 'label': '%.2d/19' % val
879 })
880
881 elif regex.match(u"^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*\d{4}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE):
882 parts = regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE)
883 fts = cFuzzyTimestamp (
884 timestamp = mxDT.now() + mxDT.RelativeDateTime(year = int(parts[1]), month = int(parts[0])),
885 accuracy = acc_months
886 )
887 matches.append ({
888 'data': fts,
889 'label': fts.format_accurately()
890 })
891
892 return matches
893
895 """This matches on single numbers.
896
897 Spaces or tabs are discarded.
898 """
899 if not regex.match(u"^(\s|\t)*\d{1,4}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE):
900 return []
901
902
903
904 enc = gmI18N.get_encoding()
905 now = mxDT.now()
906 val = int(regex.findall(u'\d{1,4}', str2parse, flags = regex.LOCALE | regex.UNICODE)[0])
907
908 matches = []
909
910
911 if (1850 < val) and (val < 2100):
912 ts = now + mxDT.RelativeDateTime(year = val)
913 target_date = cFuzzyTimestamp (
914 timestamp = ts,
915 accuracy = acc_years
916 )
917 tmp = {
918 'data': target_date,
919 'label': '%s' % target_date
920 }
921 matches.append(tmp)
922
923
924 if val <= gregorian_month_length[now.month]:
925 ts = now + mxDT.RelativeDateTime(day = val)
926 target_date = cFuzzyTimestamp (
927 timestamp = ts,
928 accuracy = acc_days
929 )
930 tmp = {
931 'data': target_date,
932 'label': _('%d. of %s (this month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc))
933 }
934 matches.append(tmp)
935
936
937 if val <= gregorian_month_length[(now + mxDT.RelativeDateTime(months = 1)).month]:
938 ts = now + mxDT.RelativeDateTime(months = 1, day = val)
939 target_date = cFuzzyTimestamp (
940 timestamp = ts,
941 accuracy = acc_days
942 )
943 tmp = {
944 'data': target_date,
945 'label': _('%d. of %s (next month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc))
946 }
947 matches.append(tmp)
948
949
950 if val <= gregorian_month_length[(now + mxDT.RelativeDateTime(months = -1)).month]:
951 ts = now + mxDT.RelativeDateTime(months = -1, day = val)
952 target_date = cFuzzyTimestamp (
953 timestamp = ts,
954 accuracy = acc_days
955 )
956 tmp = {
957 'data': target_date,
958 'label': _('%d. of %s (last month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc))
959 }
960 matches.append(tmp)
961
962
963 if val <= 400:
964 ts = now + mxDT.RelativeDateTime(days = val)
965 target_date = cFuzzyTimestamp (
966 timestamp = ts
967 )
968 tmp = {
969 'data': target_date,
970 'label': _('in %d day(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d').decode(enc))
971 }
972 matches.append(tmp)
973
974
975 if val <= 50:
976 ts = now + mxDT.RelativeDateTime(weeks = val)
977 target_date = cFuzzyTimestamp (
978 timestamp = ts
979 )
980 tmp = {
981 'data': target_date,
982 'label': _('in %d week(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d').decode(enc))
983 }
984 matches.append(tmp)
985
986
987 if val < 13:
988
989 ts = now + mxDT.RelativeDateTime(month = val)
990 target_date = cFuzzyTimestamp (
991 timestamp = ts,
992 accuracy = acc_months
993 )
994 tmp = {
995 'data': target_date,
996 'label': _('%s (%s this year)') % (target_date, ts.strftime('%B').decode(enc))
997 }
998 matches.append(tmp)
999
1000
1001 ts = now + mxDT.RelativeDateTime(years = 1, month = val)
1002 target_date = cFuzzyTimestamp (
1003 timestamp = ts,
1004 accuracy = acc_months
1005 )
1006 tmp = {
1007 'data': target_date,
1008 'label': _('%s (%s next year)') % (target_date, ts.strftime('%B').decode(enc))
1009 }
1010 matches.append(tmp)
1011
1012
1013 ts = now + mxDT.RelativeDateTime(years = -1, month = val)
1014 target_date = cFuzzyTimestamp (
1015 timestamp = ts,
1016 accuracy = acc_months
1017 )
1018 tmp = {
1019 'data': target_date,
1020 'label': _('%s (%s last year)') % (target_date, ts.strftime('%B').decode(enc))
1021 }
1022 matches.append(tmp)
1023
1024
1025 matches.append ({
1026 'data': None,
1027 'label': '%s/200' % val
1028 })
1029 matches.append ({
1030 'data': None,
1031 'label': '%s/199' % val
1032 })
1033 matches.append ({
1034 'data': None,
1035 'label': '%s/198' % val
1036 })
1037 matches.append ({
1038 'data': None,
1039 'label': '%s/19' % val
1040 })
1041
1042
1043 if val < 8:
1044
1045 ts = now + mxDT.RelativeDateTime(weekday = (val-1, 0))
1046 target_date = cFuzzyTimestamp (
1047 timestamp = ts,
1048 accuracy = acc_days
1049 )
1050 tmp = {
1051 'data': target_date,
1052 'label': _('%s this week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc))
1053 }
1054 matches.append(tmp)
1055
1056
1057 ts = now + mxDT.RelativeDateTime(weeks = +1, weekday = (val-1, 0))
1058 target_date = cFuzzyTimestamp (
1059 timestamp = ts,
1060 accuracy = acc_days
1061 )
1062 tmp = {
1063 'data': target_date,
1064 'label': _('%s next week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc))
1065 }
1066 matches.append(tmp)
1067
1068
1069 ts = now + mxDT.RelativeDateTime(weeks = -1, weekday = (val-1, 0))
1070 target_date = cFuzzyTimestamp (
1071 timestamp = ts,
1072 accuracy = acc_days
1073 )
1074 tmp = {
1075 'data': target_date,
1076 'label': _('%s last week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc))
1077 }
1078 matches.append(tmp)
1079
1080 if val < 100:
1081 matches.append ({
1082 'data': None,
1083 'label': '%s/' % (1900 + val)
1084 })
1085
1086 if val == 200:
1087 tmp = {
1088 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_days),
1089 'label': '%s' % target_date
1090 }
1091 matches.append(tmp)
1092 matches.append ({
1093 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months),
1094 'label': '%.2d/%s' % (now.month, now.year)
1095 })
1096 matches.append ({
1097 'data': None,
1098 'label': '%s/' % now.year
1099 })
1100 matches.append ({
1101 'data': None,
1102 'label': '%s/' % (now.year + 1)
1103 })
1104 matches.append ({
1105 'data': None,
1106 'label': '%s/' % (now.year - 1)
1107 })
1108
1109 if val < 200 and val >= 190:
1110 for i in range(10):
1111 matches.append ({
1112 'data': None,
1113 'label': '%s%s/' % (val, i)
1114 })
1115
1116 return matches
1117
1119 """Expand fragments containing a single dot.
1120
1121 Standard colloquial date format in Germany: day.month.year
1122
1123 "14."
1124 - 14th current month this year
1125 - 14th next month this year
1126 """
1127 if not regex.match(u"^(\s|\t)*\d{1,2}\.{1}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE):
1128 return []
1129
1130 val = int(regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE)[0])
1131 now = mxDT.now()
1132 enc = gmI18N.get_encoding()
1133
1134 matches = []
1135
1136
1137 ts = now + mxDT.RelativeDateTime(day = val)
1138 if val > 0 and val <= gregorian_month_length[ts.month]:
1139 matches.append ({
1140 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days),
1141 'label': '%s.%s.%s - a %s this month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc))
1142 })
1143
1144
1145 ts = now + mxDT.RelativeDateTime(day = val, months = +1)
1146 if val > 0 and val <= gregorian_month_length[ts.month]:
1147 matches.append ({
1148 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days),
1149 'label': '%s.%s.%s - a %s next month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc))
1150 })
1151
1152
1153 ts = now + mxDT.RelativeDateTime(day = val, months = -1)
1154 if val > 0 and val <= gregorian_month_length[ts.month]:
1155 matches.append ({
1156 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days),
1157 'label': '%s.%s.%s - a %s last month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc))
1158 })
1159
1160 return matches
1161
1163 """
1164 Turn a string into candidate fuzzy timestamps and auto-completions the user is likely to type.
1165
1166 You MUST have called locale.setlocale(locale.LC_ALL, '')
1167 somewhere in your code previously.
1168
1169 @param default_time: if you want to force the time part of the time
1170 stamp to a given value and the user doesn't type any time part
1171 this value will be used
1172 @type default_time: an mx.DateTime.DateTimeDelta instance
1173
1174 @param patterns: list of [time.strptime compatible date/time pattern, accuracy]
1175 @type patterns: list
1176 """
1177 matches = __single_dot(str2parse)
1178 matches.extend(__numbers_only(str2parse))
1179 matches.extend(__single_slash(str2parse))
1180 matches.extend(__single_char(str2parse))
1181 matches.extend(__explicit_offset(str2parse))
1182
1183
1184 if mxDT is not None:
1185 try:
1186
1187 date_only = mxDT.Parser.DateFromString (
1188 text = str2parse,
1189 formats = ('euro', 'iso', 'us', 'altus', 'altiso', 'lit', 'altlit', 'eurlit')
1190 )
1191
1192 time_part = mxDT.Parser.TimeFromString(text = str2parse)
1193 datetime = date_only + time_part
1194 if datetime == date_only:
1195 accuracy = acc_days
1196 if isinstance(default_time, mxDT.DateTimeDeltaType):
1197 datetime = date_only + default_time
1198 accuracy = acc_minutes
1199 else:
1200 accuracy = acc_subseconds
1201 fts = cFuzzyTimestamp (
1202 timestamp = datetime,
1203 accuracy = accuracy
1204 )
1205 matches.append ({
1206 'data': fts,
1207 'label': fts.format_accurately()
1208 })
1209 except (ValueError, mxDT.RangeError):
1210 pass
1211
1212 if patterns is None:
1213 patterns = []
1214
1215 patterns.append(['%Y.%m.%d', acc_days])
1216 patterns.append(['%Y/%m/%d', acc_days])
1217
1218 for pattern in patterns:
1219 try:
1220 fts = cFuzzyTimestamp (
1221 timestamp = pyDT.datetime.fromtimestamp(time.mktime(time.strptime(str2parse, pattern[0]))),
1222 accuracy = pattern[1]
1223 )
1224 matches.append ({
1225 'data': fts,
1226 'label': fts.format_accurately()
1227 })
1228 except AttributeError:
1229
1230 break
1231 except OverflowError:
1232
1233 continue
1234 except ValueError:
1235
1236 continue
1237
1238 return matches
1239
1240
1241
1243
1244
1245
1246 """A timestamp implementation with definable inaccuracy.
1247
1248 This class contains an mxDateTime.DateTime instance to
1249 hold the actual timestamp. It adds an accuracy attribute
1250 to allow the programmer to set the precision of the
1251 timestamp.
1252
1253 The timestamp will have to be initialzed with a fully
1254 precise value (which may, of course, contain partially
1255 fake data to make up for missing values). One can then
1256 set the accuracy value to indicate up to which part of
1257 the timestamp the data is valid. Optionally a modifier
1258 can be set to indicate further specification of the
1259 value (such as "summer", "afternoon", etc).
1260
1261 accuracy values:
1262 1: year only
1263 ...
1264 7: everything including milliseconds value
1265
1266 Unfortunately, one cannot directly derive a class from mx.DateTime.DateTime :-(
1267 """
1268
1270
1271 if timestamp is None:
1272 timestamp = mxDT.now()
1273 accuracy = acc_subseconds
1274 modifier = ''
1275
1276 if isinstance(timestamp, pyDT.datetime):
1277 timestamp = mxDT.DateTime(timestamp.year, timestamp.month, timestamp.day, timestamp.hour, timestamp.minute, timestamp.second)
1278
1279 if type(timestamp) != mxDT.DateTimeType:
1280 raise TypeError, '%s.__init__(): <timestamp> must be of mx.DateTime.DateTime or datetime.datetime type' % self.__class__.__name__
1281
1282 self.timestamp = timestamp
1283
1284 if (accuracy < 1) or (accuracy > 8):
1285 raise ValueError, '%s.__init__(): <accuracy> must be between 1 and 7' % self.__class__.__name__
1286 self.accuracy = accuracy
1287
1288 self.modifier = modifier
1289
1290
1291
1292
1294 """Return string representation meaningful to a user, also for %s formatting."""
1295 return self.format_accurately()
1296
1298 """Return string meaningful to a programmer to aid in debugging."""
1299 tmp = '<[%s]: timestamp [%s], accuracy [%s] (%s), modifier [%s] at %s>' % (
1300 self.__class__.__name__,
1301 repr(self.timestamp),
1302 self.accuracy,
1303 _accuracy_strings[self.accuracy],
1304 self.modifier,
1305 id(self)
1306 )
1307 return tmp
1308
1309
1310
1315
1318
1345
1347 return self.timestamp
1348
1350 try:
1351 gmtoffset = self.timestamp.gmtoffset()
1352 except mxDT.Error:
1353
1354
1355 now = mxDT.now()
1356 gmtoffset = now.gmtoffset()
1357 tz = cFixedOffsetTimezone(gmtoffset.minutes, self.timestamp.tz)
1358 secs, msecs = divmod(self.timestamp.second, 1)
1359 ts = pyDT.datetime (
1360 year = self.timestamp.year,
1361 month = self.timestamp.month,
1362 day = self.timestamp.day,
1363 hour = self.timestamp.hour,
1364 minute = self.timestamp.minute,
1365 second = secs,
1366 microsecond = msecs,
1367 tzinfo = tz
1368 )
1369 return ts
1370
1371
1372
1373 if __name__ == '__main__':
1374
1375 intervals_as_str = [
1376 '7', '12', ' 12', '12 ', ' 12 ', ' 12 ', '0', '~12', '~ 12', ' ~ 12', ' ~ 12 ',
1377 '12a', '12 a', '12 a', '12j', '12J', '12y', '12Y', ' ~ 12 a ', '~0a',
1378 '12m', '17 m', '12 m', '17M', ' ~ 17 m ', ' ~ 3 / 12 ', '7/12', '0/12',
1379 '12w', '17 w', '12 w', '17W', ' ~ 17 w ', ' ~ 15 / 52', '2/52', '0/52',
1380 '12d', '17 d', '12 t', '17D', ' ~ 17 T ', ' ~ 12 / 7', '3/7', '0/7',
1381 '12h', '17 h', '12 H', '17H', ' ~ 17 h ', ' ~ 36 / 24', '7/24', '0/24',
1382 ' ~ 36 / 60', '7/60', '190/60', '0/60',
1383 '12a1m', '12 a 1 M', '12 a17m', '12j 12m', '12J7m', '12y7m', '12Y7M', ' ~ 12 a 37 m ', '~0a0m',
1384 '10m1w',
1385 'invalid interval input'
1386 ]
1387
1393
1461
1463 print "testing str2interval()"
1464 print "----------------------"
1465
1466 for interval_as_str in intervals_as_str:
1467 print "input: <%s>" % interval_as_str
1468 print " ==>", str2interval(str_interval=interval_as_str)
1469
1470 return True
1471
1473 print "DST currently in effect:", dst_currently_in_effect
1474 print "current UTC offset:", current_local_utc_offset_in_seconds, "seconds"
1475 print "current timezone (interval):", current_local_timezone_interval
1476 print "current timezone (ISO conformant numeric string):", current_local_iso_numeric_timezone_string
1477 print "local timezone class:", cLocalTimezone
1478 print ""
1479 tz = cLocalTimezone()
1480 print "local timezone instance:", tz
1481 print " (total) UTC offset:", tz.utcoffset(pyDT.datetime.now())
1482 print " DST adjustment:", tz.dst(pyDT.datetime.now())
1483 print " timezone name:", tz.tzname(pyDT.datetime.now())
1484 print ""
1485 print "current local timezone:", gmCurrentLocalTimezone
1486 print " (total) UTC offset:", gmCurrentLocalTimezone.utcoffset(pyDT.datetime.now())
1487 print " DST adjustment:", gmCurrentLocalTimezone.dst(pyDT.datetime.now())
1488 print " timezone name:", gmCurrentLocalTimezone.tzname(pyDT.datetime.now())
1489 print ""
1490 print "now here:", pydt_now_here()
1491 print ""
1492
1494 print "testing function str2fuzzy_timestamp_matches"
1495 print "--------------------------------------------"
1496
1497 val = None
1498 while val != 'exit':
1499 val = raw_input('Enter date fragment ("exit" quits): ')
1500 matches = str2fuzzy_timestamp_matches(str2parse = val)
1501 for match in matches:
1502 print 'label shown :', match['label']
1503 print 'data attached:', match['data']
1504 print ""
1505 print "---------------"
1506
1508 print "testing fuzzy timestamp class"
1509 print "-----------------------------"
1510
1511 ts = mxDT.now()
1512 print "mx.DateTime timestamp", type(ts)
1513 print " print ... :", ts
1514 print " print '%%s' %% ...: %s" % ts
1515 print " str() :", str(ts)
1516 print " repr() :", repr(ts)
1517
1518 fts = cFuzzyTimestamp()
1519 print "\nfuzzy timestamp <%s '%s'>" % ('class', fts.__class__.__name__)
1520 for accuracy in range(1,8):
1521 fts.accuracy = accuracy
1522 print " accuracy : %s (%s)" % (accuracy, _accuracy_strings[accuracy])
1523 print " format_accurately:", fts.format_accurately()
1524 print " strftime() :", fts.strftime('%c')
1525 print " print ... :", fts
1526 print " print '%%s' %% ... : %s" % fts
1527 print " str() :", str(fts)
1528 print " repr() :", repr(fts)
1529 raw_input('press ENTER to continue')
1530
1532 print "testing platform for handling dates before 1970"
1533 print "-----------------------------------------------"
1534 ts = mxDT.DateTime(1935, 4, 2)
1535 fts = cFuzzyTimestamp(timestamp=ts)
1536 print "fts :", fts
1537 print "fts.get_pydt():", fts.get_pydt()
1538
1550
1551 if len(sys.argv) > 1 and sys.argv[1] == "test":
1552
1553
1554 gmI18N.activate_locale()
1555 gmI18N.install_domain('gnumed')
1556
1557 init()
1558
1559
1560
1561
1562
1563
1564
1565
1566 test_calculate_apparent_age()
1567
1568
1569