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
202
203
204
206 if not wxDate.IsValid():
207 raise ArgumentError (u'invalid wxDate: %s-%s-%s %s:%s %s.%s',
208 wxDate.GetYear(),
209 wxDate.GetMonth(),
210 wxDate.GetDay(),
211 wxDate.GetHour(),
212 wxDate.GetMinute(),
213 wxDate.GetSecond(),
214 wxDate.GetMillisecond()
215 )
216
217 try:
218 return pyDT.datetime (
219 year = wxDate.GetYear(),
220 month = wxDate.GetMonth() + 1,
221 day = wxDate.GetDay(),
222 tzinfo = gmCurrentLocalTimezone
223 )
224 except:
225 _log.debug (u'error converting wxDateTime to Python: %s-%s-%s %s:%s %s.%s',
226 wxDate.GetYear(),
227 wxDate.GetMonth(),
228 wxDate.GetDay(),
229 wxDate.GetHour(),
230 wxDate.GetMinute(),
231 wxDate.GetSecond(),
232 wxDate.GetMillisecond()
233 )
234 raise
235
237 wxdt = wx.DateTime()
238 wxdt.SetYear(py_dt.year)
239 wxdt.SetMonth(py_dt.month-1)
240 wxdt.SetDay(py_dt.day)
241 return wxdt
242
243
244
296
361
363 """The result of this is a tuple (years, ..., seconds) as one would
364 'expect' a date to look like, that is, simple differences between
365 the fields.
366
367 No need for 100/400 years leap days rule because 2000 WAS a leap year.
368
369 This does not take into account time zones which may
370 shift the result by one day.
371
372 <start> and <end> must by python datetime instances
373 <end> is assumed to be "now" if not given
374 """
375 if end is None:
376 end = pyDT.datetime.now(gmCurrentLocalTimezone)
377
378 if end < start:
379 raise ValueError('calculate_apparent_age(): <end> (%s) before <start> %s' % (end, start))
380
381 if end == start:
382 years = months = days = hours = minutes = seconds = 0
383 return (years, months, days, hours, minutes, seconds)
384
385
386 years = end.year - start.year
387 end = end.replace(year = start.year)
388 if end < start:
389 years = years - 1
390
391
392 if end.month == start.month:
393 months = 0
394 else:
395 months = end.month - start.month
396 if months < 0:
397 months = months + 12
398 end = end.replace(month = start.month)
399 if end < start:
400 months = months - 1
401
402
403 if end.day == start.day:
404 days = 0
405 else:
406 days = end.day - start.day
407 if days < 0:
408 days = days + gregorian_month_length[start.month]
409 end = end.replace(day = start.day)
410 if end < start:
411 days = days - 1
412
413
414 if end.hour == start.hour:
415 hours = 0
416 else:
417 hours = end.hour - start.hour
418 if hours < 0:
419 hours = hours + 24
420 end = end.replace(hour = start.hour)
421 if end < start:
422 hours = hours - 1
423
424
425 if end.minute == start.minute:
426 minutes = 0
427 else:
428 minutes = end.minute - start.minute
429 if minutes < 0:
430 minutes = minutes + 60
431 end = end.replace(minute = start.minute)
432 if end < start:
433 minutes = minutes - 1
434
435
436 if end.second == start.second:
437 seconds = 0
438 else:
439 seconds = end.second - start.second
440 if seconds < 0:
441 seconds = seconds + 60
442 end = end.replace(second = start.second)
443 if end < start:
444 seconds = seconds - 1
445
446 return (years, months, days, hours, minutes, seconds)
447
551
553
554 unit_keys = {
555 'year': _('yYaA_keys_year'),
556 'month': _('mM_keys_month'),
557 'week': _('wW_keys_week'),
558 'day': _('dD_keys_day'),
559 'hour': _('hH_keys_hour')
560 }
561
562 str_interval = str_interval.strip()
563
564
565 keys = '|'.join(list(unit_keys['year'].replace('_keys_year', u'')))
566 if regex.match(u'^~*(\s|\t)*\d+(%s)*$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
567 return pyDT.timedelta(days = (int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]) * avg_days_per_gregorian_year))
568
569
570 keys = '|'.join(list(unit_keys['month'].replace('_keys_month', u'')))
571 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
572 years, months = divmod (
573 int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]),
574 12
575 )
576 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month)))
577
578
579 keys = '|'.join(list(unit_keys['week'].replace('_keys_week', u'')))
580 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
581 return pyDT.timedelta(weeks = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
582
583
584 keys = '|'.join(list(unit_keys['day'].replace('_keys_day', u'')))
585 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
586 return pyDT.timedelta(days = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
587
588
589 keys = '|'.join(list(unit_keys['hour'].replace('_keys_hour', u'')))
590 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
591 return pyDT.timedelta(hours = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
592
593
594 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*12$', str_interval, flags = regex.LOCALE | regex.UNICODE):
595 years, months = divmod (
596 int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]),
597 12
598 )
599 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month)))
600
601
602 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*52$', str_interval, flags = regex.LOCALE | regex.UNICODE):
603
604 return pyDT.timedelta(weeks = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
605
606
607 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*7$', str_interval, flags = regex.LOCALE | regex.UNICODE):
608 return pyDT.timedelta(days = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
609
610
611 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*24$', str_interval, flags = regex.LOCALE | regex.UNICODE):
612 return pyDT.timedelta(hours = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
613
614
615 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*60$', str_interval, flags = regex.LOCALE | regex.UNICODE):
616 return pyDT.timedelta(minutes = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
617
618
619 keys_year = '|'.join(list(unit_keys['year'].replace('_keys_year', u'')))
620 keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', u'')))
621 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):
622 parts = regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)
623 years, months = divmod(int(parts[1]), 12)
624 years += int(parts[0])
625 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month)))
626
627
628 keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', u'')))
629 keys_week = '|'.join(list(unit_keys['week'].replace('_keys_week', u'')))
630 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):
631 parts = regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)
632 months, weeks = divmod(int(parts[1]), 4)
633 months += int(parts[0])
634 return pyDT.timedelta(days = ((months * avg_days_per_gregorian_month) + (weeks * days_per_week)))
635
636 return None
637
638
639
640
642 """
643 Default is 'hdwm':
644 h - hours
645 d - days
646 w - weeks
647 m - months
648 y - years
649
650 This also defines the significance of the order of the characters.
651 """
652 if offset_chars is None:
653 offset_chars = _('hdwmy (single character date offset triggers)')[:5].lower()
654
655
656 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):
657 return []
658 val = int(regex.findall(u'\d{1,2}', str2parse, flags = regex.LOCALE | regex.UNICODE)[0])
659 offset_char = regex.findall(u'[%s]' % offset_chars, str2parse, flags = regex.LOCALE | regex.UNICODE)[0].lower()
660
661 now = mxDT.now()
662 enc = gmI18N.get_encoding()
663
664
665 is_future = True
666 if str2parse.find('-') > -1:
667 is_future = False
668
669 ts = None
670
671 if offset_char == offset_chars[0]:
672 if is_future:
673 ts = now + mxDT.RelativeDateTime(hours = val)
674 label = _('in %d hour(s) - %s') % (val, ts.strftime('%H:%M'))
675 else:
676 ts = now - mxDT.RelativeDateTime(hours = val)
677 label = _('%d hour(s) ago - %s') % (val, ts.strftime('%H:%M'))
678 accuracy = acc_subseconds
679
680 elif offset_char == offset_chars[1]:
681 if is_future:
682 ts = now + mxDT.RelativeDateTime(days = val)
683 label = _('in %d day(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
684 else:
685 ts = now - mxDT.RelativeDateTime(days = val)
686 label = _('%d day(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
687 accuracy = acc_days
688
689 elif offset_char == offset_chars[2]:
690 if is_future:
691 ts = now + mxDT.RelativeDateTime(weeks = val)
692 label = _('in %d week(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
693 else:
694 ts = now - mxDT.RelativeDateTime(weeks = val)
695 label = _('%d week(s) ago - %s)') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
696 accuracy = acc_days
697
698 elif offset_char == offset_chars[3]:
699 if is_future:
700 ts = now + mxDT.RelativeDateTime(months = val)
701 label = _('in %d month(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
702 else:
703 ts = now - mxDT.RelativeDateTime(months = val)
704 label = _('%d month(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
705 accuracy = acc_days
706
707 elif offset_char == offset_chars[4]:
708 if is_future:
709 ts = now + mxDT.RelativeDateTime(years = val)
710 label = _('in %d year(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
711 else:
712 ts = now - mxDT.RelativeDateTime(years = val)
713 label = _('%d year(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
714 accuracy = acc_months
715
716 if ts is None:
717 return []
718
719 tmp = {
720 'data': cFuzzyTimestamp(timestamp = ts, accuracy = accuracy),
721 'label': label
722 }
723 return [tmp]
724
726 """This matches on single characters.
727
728 Spaces and tabs are discarded.
729
730 Default is 'ndmy':
731 n - now
732 d - toDay
733 m - toMorrow Someone please suggest a synonym !
734 y - yesterday
735
736 This also defines the significance of the order of the characters.
737 """
738 if trigger_chars is None:
739 trigger_chars = _('ndmy (single character date triggers)')[:4].lower()
740
741 if not regex.match(u'^(\s|\t)*[%s]{1}(\s|\t)*$' % trigger_chars, str2parse, flags = regex.LOCALE | regex.UNICODE):
742 return []
743 val = str2parse.strip().lower()
744
745 now = mxDT.now()
746 enc = gmI18N.get_encoding()
747
748
749
750
751 if val == trigger_chars[0]:
752 ts = now
753 return [{
754 'data': cFuzzyTimestamp (
755 timestamp = ts,
756 accuracy = acc_subseconds
757 ),
758 'label': _('right now (%s, %s)') % (ts.strftime('%A').decode(enc), ts)
759 }]
760
761
762 if val == trigger_chars[1]:
763 return [{
764 'data': cFuzzyTimestamp (
765 timestamp = now,
766 accuracy = acc_days
767 ),
768 'label': _('today (%s)') % now.strftime('%A, %Y-%m-%d').decode(enc)
769 }]
770
771
772 if val == trigger_chars[2]:
773 ts = now + mxDT.RelativeDateTime(days = +1)
774 return [{
775 'data': cFuzzyTimestamp (
776 timestamp = ts,
777 accuracy = acc_days
778 ),
779 'label': _('tomorrow (%s)') % ts.strftime('%A, %Y-%m-%d').decode(enc)
780 }]
781
782
783 if val == trigger_chars[3]:
784 ts = now + mxDT.RelativeDateTime(days = -1)
785 return [{
786 'data': cFuzzyTimestamp (
787 timestamp = ts,
788 accuracy = acc_days
789 ),
790 'label': _('yesterday (%s)') % ts.strftime('%A, %Y-%m-%d').decode(enc)
791 }]
792
793 return []
794
796 """Expand fragments containing a single slash.
797
798 "5/"
799 - 2005/ (2000 - 2025)
800 - 1995/ (1990 - 1999)
801 - Mai/current year
802 - Mai/next year
803 - Mai/last year
804 - Mai/200x
805 - Mai/20xx
806 - Mai/199x
807 - Mai/198x
808 - Mai/197x
809 - Mai/19xx
810 """
811 matches = []
812 now = mxDT.now()
813 if regex.match(u"^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE):
814 val = int(regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE)[0])
815
816 if val < 100 and val >= 0:
817 matches.append ({
818 'data': None,
819 'label': '%s/' % (val + 1900)
820 })
821
822 if val < 26 and val >= 0:
823 matches.append ({
824 'data': None,
825 'label': '%s/' % (val + 2000)
826 })
827
828 if val < 10 and val >= 0:
829 matches.append ({
830 'data': None,
831 'label': '%s/' % (val + 1990)
832 })
833
834 if val < 13 and val > 0:
835 matches.append ({
836 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months),
837 'label': '%.2d/%s' % (val, now.year)
838 })
839 ts = now + mxDT.RelativeDateTime(years = 1)
840 matches.append ({
841 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months),
842 'label': '%.2d/%s' % (val, ts.year)
843 })
844 ts = now + mxDT.RelativeDateTime(years = -1)
845 matches.append ({
846 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months),
847 'label': '%.2d/%s' % (val, ts.year)
848 })
849 matches.append ({
850 'data': None,
851 'label': '%.2d/200' % val
852 })
853 matches.append ({
854 'data': None,
855 'label': '%.2d/20' % val
856 })
857 matches.append ({
858 'data': None,
859 'label': '%.2d/199' % val
860 })
861 matches.append ({
862 'data': None,
863 'label': '%.2d/198' % val
864 })
865 matches.append ({
866 'data': None,
867 'label': '%.2d/197' % val
868 })
869 matches.append ({
870 'data': None,
871 'label': '%.2d/19' % val
872 })
873
874 elif regex.match(u"^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*\d{4}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE):
875 parts = regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE)
876 fts = cFuzzyTimestamp (
877 timestamp = mxDT.now() + mxDT.RelativeDateTime(year = int(parts[1]), month = int(parts[0])),
878 accuracy = acc_months
879 )
880 matches.append ({
881 'data': fts,
882 'label': fts.format_accurately()
883 })
884
885 return matches
886
888 """This matches on single numbers.
889
890 Spaces or tabs are discarded.
891 """
892 if not regex.match(u"^(\s|\t)*\d{1,4}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE):
893 return []
894
895
896
897 enc = gmI18N.get_encoding()
898 now = mxDT.now()
899 val = int(regex.findall(u'\d{1,4}', str2parse, flags = regex.LOCALE | regex.UNICODE)[0])
900
901 matches = []
902
903
904 if (1850 < val) and (val < 2100):
905 ts = now + mxDT.RelativeDateTime(year = val)
906 target_date = cFuzzyTimestamp (
907 timestamp = ts,
908 accuracy = acc_years
909 )
910 tmp = {
911 'data': target_date,
912 'label': '%s' % target_date
913 }
914 matches.append(tmp)
915
916
917 if val <= gregorian_month_length[now.month]:
918 ts = now + mxDT.RelativeDateTime(day = val)
919 target_date = cFuzzyTimestamp (
920 timestamp = ts,
921 accuracy = acc_days
922 )
923 tmp = {
924 'data': target_date,
925 'label': _('%d. of %s (this month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc))
926 }
927 matches.append(tmp)
928
929
930 if val <= gregorian_month_length[(now + mxDT.RelativeDateTime(months = 1)).month]:
931 ts = now + mxDT.RelativeDateTime(months = 1, day = val)
932 target_date = cFuzzyTimestamp (
933 timestamp = ts,
934 accuracy = acc_days
935 )
936 tmp = {
937 'data': target_date,
938 'label': _('%d. of %s (next month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc))
939 }
940 matches.append(tmp)
941
942
943 if val <= gregorian_month_length[(now + mxDT.RelativeDateTime(months = -1)).month]:
944 ts = now + mxDT.RelativeDateTime(months = -1, day = val)
945 target_date = cFuzzyTimestamp (
946 timestamp = ts,
947 accuracy = acc_days
948 )
949 tmp = {
950 'data': target_date,
951 'label': _('%d. of %s (last month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc))
952 }
953 matches.append(tmp)
954
955
956 if val <= 400:
957 ts = now + mxDT.RelativeDateTime(days = val)
958 target_date = cFuzzyTimestamp (
959 timestamp = ts
960 )
961 tmp = {
962 'data': target_date,
963 'label': _('in %d day(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d').decode(enc))
964 }
965 matches.append(tmp)
966
967
968 if val <= 50:
969 ts = now + mxDT.RelativeDateTime(weeks = val)
970 target_date = cFuzzyTimestamp (
971 timestamp = ts
972 )
973 tmp = {
974 'data': target_date,
975 'label': _('in %d week(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d').decode(enc))
976 }
977 matches.append(tmp)
978
979
980 if val < 13:
981
982 ts = now + mxDT.RelativeDateTime(month = val)
983 target_date = cFuzzyTimestamp (
984 timestamp = ts,
985 accuracy = acc_months
986 )
987 tmp = {
988 'data': target_date,
989 'label': _('%s (%s this year)') % (target_date, ts.strftime('%B').decode(enc))
990 }
991 matches.append(tmp)
992
993
994 ts = now + mxDT.RelativeDateTime(years = 1, month = val)
995 target_date = cFuzzyTimestamp (
996 timestamp = ts,
997 accuracy = acc_months
998 )
999 tmp = {
1000 'data': target_date,
1001 'label': _('%s (%s next year)') % (target_date, ts.strftime('%B').decode(enc))
1002 }
1003 matches.append(tmp)
1004
1005
1006 ts = now + mxDT.RelativeDateTime(years = -1, month = val)
1007 target_date = cFuzzyTimestamp (
1008 timestamp = ts,
1009 accuracy = acc_months
1010 )
1011 tmp = {
1012 'data': target_date,
1013 'label': _('%s (%s last year)') % (target_date, ts.strftime('%B').decode(enc))
1014 }
1015 matches.append(tmp)
1016
1017
1018 matches.append ({
1019 'data': None,
1020 'label': '%s/200' % val
1021 })
1022 matches.append ({
1023 'data': None,
1024 'label': '%s/199' % val
1025 })
1026 matches.append ({
1027 'data': None,
1028 'label': '%s/198' % val
1029 })
1030 matches.append ({
1031 'data': None,
1032 'label': '%s/19' % val
1033 })
1034
1035
1036 if val < 8:
1037
1038 ts = now + mxDT.RelativeDateTime(weekday = (val-1, 0))
1039 target_date = cFuzzyTimestamp (
1040 timestamp = ts,
1041 accuracy = acc_days
1042 )
1043 tmp = {
1044 'data': target_date,
1045 'label': _('%s this week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc))
1046 }
1047 matches.append(tmp)
1048
1049
1050 ts = now + mxDT.RelativeDateTime(weeks = +1, weekday = (val-1, 0))
1051 target_date = cFuzzyTimestamp (
1052 timestamp = ts,
1053 accuracy = acc_days
1054 )
1055 tmp = {
1056 'data': target_date,
1057 'label': _('%s next week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc))
1058 }
1059 matches.append(tmp)
1060
1061
1062 ts = now + mxDT.RelativeDateTime(weeks = -1, weekday = (val-1, 0))
1063 target_date = cFuzzyTimestamp (
1064 timestamp = ts,
1065 accuracy = acc_days
1066 )
1067 tmp = {
1068 'data': target_date,
1069 'label': _('%s last week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc))
1070 }
1071 matches.append(tmp)
1072
1073 if val < 100:
1074 matches.append ({
1075 'data': None,
1076 'label': '%s/' % (1900 + val)
1077 })
1078
1079 if val == 200:
1080 tmp = {
1081 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_days),
1082 'label': '%s' % target_date
1083 }
1084 matches.append(tmp)
1085 matches.append ({
1086 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months),
1087 'label': '%.2d/%s' % (now.month, now.year)
1088 })
1089 matches.append ({
1090 'data': None,
1091 'label': '%s/' % now.year
1092 })
1093 matches.append ({
1094 'data': None,
1095 'label': '%s/' % (now.year + 1)
1096 })
1097 matches.append ({
1098 'data': None,
1099 'label': '%s/' % (now.year - 1)
1100 })
1101
1102 if val < 200 and val >= 190:
1103 for i in range(10):
1104 matches.append ({
1105 'data': None,
1106 'label': '%s%s/' % (val, i)
1107 })
1108
1109 return matches
1110
1112 """Expand fragments containing a single dot.
1113
1114 Standard colloquial date format in Germany: day.month.year
1115
1116 "14."
1117 - 14th current month this year
1118 - 14th next month this year
1119 """
1120 if not regex.match(u"^(\s|\t)*\d{1,2}\.{1}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE):
1121 return []
1122
1123 val = int(regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE)[0])
1124 now = mxDT.now()
1125 enc = gmI18N.get_encoding()
1126
1127 matches = []
1128
1129
1130 ts = now + mxDT.RelativeDateTime(day = val)
1131 if val > 0 and val <= gregorian_month_length[ts.month]:
1132 matches.append ({
1133 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days),
1134 'label': '%s.%s.%s - a %s this month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc))
1135 })
1136
1137
1138 ts = now + mxDT.RelativeDateTime(day = val, months = +1)
1139 if val > 0 and val <= gregorian_month_length[ts.month]:
1140 matches.append ({
1141 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days),
1142 'label': '%s.%s.%s - a %s next month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc))
1143 })
1144
1145
1146 ts = now + mxDT.RelativeDateTime(day = val, months = -1)
1147 if val > 0 and val <= gregorian_month_length[ts.month]:
1148 matches.append ({
1149 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days),
1150 'label': '%s.%s.%s - a %s last month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc))
1151 })
1152
1153 return matches
1154
1156 """
1157 Turn a string into candidate fuzzy timestamps and auto-completions the user is likely to type.
1158
1159 You MUST have called locale.setlocale(locale.LC_ALL, '')
1160 somewhere in your code previously.
1161
1162 @param default_time: if you want to force the time part of the time
1163 stamp to a given value and the user doesn't type any time part
1164 this value will be used
1165 @type default_time: an mx.DateTime.DateTimeDelta instance
1166
1167 @param patterns: list of [time.strptime compatible date/time pattern, accuracy]
1168 @type patterns: list
1169 """
1170 matches = __single_dot(str2parse)
1171 matches.extend(__numbers_only(str2parse))
1172 matches.extend(__single_slash(str2parse))
1173 matches.extend(__single_char(str2parse))
1174 matches.extend(__explicit_offset(str2parse))
1175
1176
1177 if mxDT is not None:
1178 try:
1179
1180 date_only = mxDT.Parser.DateFromString (
1181 text = str2parse,
1182 formats = ('euro', 'iso', 'us', 'altus', 'altiso', 'lit', 'altlit', 'eurlit')
1183 )
1184
1185 time_part = mxDT.Parser.TimeFromString(text = str2parse)
1186 datetime = date_only + time_part
1187 if datetime == date_only:
1188 accuracy = acc_days
1189 if isinstance(default_time, mxDT.DateTimeDeltaType):
1190 datetime = date_only + default_time
1191 accuracy = acc_minutes
1192 else:
1193 accuracy = acc_subseconds
1194 fts = cFuzzyTimestamp (
1195 timestamp = datetime,
1196 accuracy = accuracy
1197 )
1198 matches.append ({
1199 'data': fts,
1200 'label': fts.format_accurately()
1201 })
1202 except (ValueError, mxDT.RangeError):
1203 pass
1204
1205 if patterns is None:
1206 patterns = []
1207
1208 patterns.append(['%Y.%m.%d', acc_days])
1209 patterns.append(['%Y/%m/%d', acc_days])
1210
1211 for pattern in patterns:
1212 try:
1213 fts = cFuzzyTimestamp (
1214 timestamp = pyDT.datetime.fromtimestamp(time.mktime(time.strptime(str2parse, pattern[0]))),
1215 accuracy = pattern[1]
1216 )
1217 matches.append ({
1218 'data': fts,
1219 'label': fts.format_accurately()
1220 })
1221 except AttributeError:
1222
1223 break
1224 except OverflowError:
1225
1226 continue
1227 except ValueError:
1228
1229 continue
1230
1231 return matches
1232
1233
1234
1236
1237
1238
1239 """A timestamp implementation with definable inaccuracy.
1240
1241 This class contains an mxDateTime.DateTime instance to
1242 hold the actual timestamp. It adds an accuracy attribute
1243 to allow the programmer to set the precision of the
1244 timestamp.
1245
1246 The timestamp will have to be initialzed with a fully
1247 precise value (which may, of course, contain partially
1248 fake data to make up for missing values). One can then
1249 set the accuracy value to indicate up to which part of
1250 the timestamp the data is valid. Optionally a modifier
1251 can be set to indicate further specification of the
1252 value (such as "summer", "afternoon", etc).
1253
1254 accuracy values:
1255 1: year only
1256 ...
1257 7: everything including milliseconds value
1258
1259 Unfortunately, one cannot directly derive a class from mx.DateTime.DateTime :-(
1260 """
1261
1263
1264 if timestamp is None:
1265 timestamp = mxDT.now()
1266 accuracy = acc_subseconds
1267 modifier = ''
1268
1269 if isinstance(timestamp, pyDT.datetime):
1270 timestamp = mxDT.DateTime(timestamp.year, timestamp.month, timestamp.day, timestamp.hour, timestamp.minute, timestamp.second)
1271
1272 if type(timestamp) != mxDT.DateTimeType:
1273 raise TypeError, '%s.__init__(): <timestamp> must be of mx.DateTime.DateTime or datetime.datetime type' % self.__class__.__name__
1274
1275 self.timestamp = timestamp
1276
1277 if (accuracy < 1) or (accuracy > 8):
1278 raise ValueError, '%s.__init__(): <accuracy> must be between 1 and 7' % self.__class__.__name__
1279 self.accuracy = accuracy
1280
1281 self.modifier = modifier
1282
1283
1284
1285
1287 """Return string representation meaningful to a user, also for %s formatting."""
1288 return self.format_accurately()
1289
1291 """Return string meaningful to a programmer to aid in debugging."""
1292 tmp = '<[%s]: timestamp [%s], accuracy [%s] (%s), modifier [%s] at %s>' % (
1293 self.__class__.__name__,
1294 repr(self.timestamp),
1295 self.accuracy,
1296 _accuracy_strings[self.accuracy],
1297 self.modifier,
1298 id(self)
1299 )
1300 return tmp
1301
1302
1303
1308
1311
1338
1340 return self.timestamp
1341
1343 try:
1344 gmtoffset = self.timestamp.gmtoffset()
1345 except mxDT.Error:
1346
1347
1348 now = mxDT.now()
1349 gmtoffset = now.gmtoffset()
1350 tz = cFixedOffsetTimezone(gmtoffset.minutes, self.timestamp.tz)
1351 secs, msecs = divmod(self.timestamp.second, 1)
1352 ts = pyDT.datetime (
1353 year = self.timestamp.year,
1354 month = self.timestamp.month,
1355 day = self.timestamp.day,
1356 hour = self.timestamp.hour,
1357 minute = self.timestamp.minute,
1358 second = secs,
1359 microsecond = msecs,
1360 tzinfo = tz
1361 )
1362 return ts
1363
1364
1365
1366 if __name__ == '__main__':
1367
1368 intervals_as_str = [
1369 '7', '12', ' 12', '12 ', ' 12 ', ' 12 ', '0', '~12', '~ 12', ' ~ 12', ' ~ 12 ',
1370 '12a', '12 a', '12 a', '12j', '12J', '12y', '12Y', ' ~ 12 a ', '~0a',
1371 '12m', '17 m', '12 m', '17M', ' ~ 17 m ', ' ~ 3 / 12 ', '7/12', '0/12',
1372 '12w', '17 w', '12 w', '17W', ' ~ 17 w ', ' ~ 15 / 52', '2/52', '0/52',
1373 '12d', '17 d', '12 t', '17D', ' ~ 17 T ', ' ~ 12 / 7', '3/7', '0/7',
1374 '12h', '17 h', '12 H', '17H', ' ~ 17 h ', ' ~ 36 / 24', '7/24', '0/24',
1375 ' ~ 36 / 60', '7/60', '190/60', '0/60',
1376 '12a1m', '12 a 1 M', '12 a17m', '12j 12m', '12J7m', '12y7m', '12Y7M', ' ~ 12 a 37 m ', '~0a0m',
1377 '10m1w',
1378 'invalid interval input'
1379 ]
1380
1386
1454
1456 print "testing str2interval()"
1457 print "----------------------"
1458
1459 for interval_as_str in intervals_as_str:
1460 print "input: <%s>" % interval_as_str
1461 print " ==>", str2interval(str_interval=interval_as_str)
1462
1463 return True
1464
1466 print "DST currently in effect:", dst_currently_in_effect
1467 print "current UTC offset:", current_local_utc_offset_in_seconds, "seconds"
1468 print "current timezone (interval):", current_local_timezone_interval
1469 print "current timezone (ISO conformant numeric string):", current_local_iso_numeric_timezone_string
1470 print "local timezone class:", cLocalTimezone
1471 print ""
1472 tz = cLocalTimezone()
1473 print "local timezone instance:", tz
1474 print " (total) UTC offset:", tz.utcoffset(pyDT.datetime.now())
1475 print " DST adjustment:", tz.dst(pyDT.datetime.now())
1476 print " timezone name:", tz.tzname(pyDT.datetime.now())
1477 print ""
1478 print "current local timezone:", gmCurrentLocalTimezone
1479 print " (total) UTC offset:", gmCurrentLocalTimezone.utcoffset(pyDT.datetime.now())
1480 print " DST adjustment:", gmCurrentLocalTimezone.dst(pyDT.datetime.now())
1481 print " timezone name:", gmCurrentLocalTimezone.tzname(pyDT.datetime.now())
1482 print ""
1483 print "now here:", pydt_now_here()
1484 print ""
1485
1487 print "testing function str2fuzzy_timestamp_matches"
1488 print "--------------------------------------------"
1489
1490 val = None
1491 while val != 'exit':
1492 val = raw_input('Enter date fragment ("exit" quits): ')
1493 matches = str2fuzzy_timestamp_matches(str2parse = val)
1494 for match in matches:
1495 print 'label shown :', match['label']
1496 print 'data attached:', match['data']
1497 print ""
1498 print "---------------"
1499
1501 print "testing fuzzy timestamp class"
1502 print "-----------------------------"
1503
1504 ts = mxDT.now()
1505 print "mx.DateTime timestamp", type(ts)
1506 print " print ... :", ts
1507 print " print '%%s' %% ...: %s" % ts
1508 print " str() :", str(ts)
1509 print " repr() :", repr(ts)
1510
1511 fts = cFuzzyTimestamp()
1512 print "\nfuzzy timestamp <%s '%s'>" % ('class', fts.__class__.__name__)
1513 for accuracy in range(1,8):
1514 fts.accuracy = accuracy
1515 print " accuracy : %s (%s)" % (accuracy, _accuracy_strings[accuracy])
1516 print " format_accurately:", fts.format_accurately()
1517 print " strftime() :", fts.strftime('%c')
1518 print " print ... :", fts
1519 print " print '%%s' %% ... : %s" % fts
1520 print " str() :", str(fts)
1521 print " repr() :", repr(fts)
1522 raw_input('press ENTER to continue')
1523
1525 print "testing platform for handling dates before 1970"
1526 print "-----------------------------------------------"
1527 ts = mxDT.DateTime(1935, 4, 2)
1528 fts = cFuzzyTimestamp(timestamp=ts)
1529 print "fts :", fts
1530 print "fts.get_pydt():", fts.get_pydt()
1531
1539
1540 if len(sys.argv) > 1 and sys.argv[1] == "test":
1541
1542
1543 gmI18N.activate_locale()
1544 gmI18N.install_domain('gnumed')
1545
1546 init()
1547
1548
1549
1550
1551
1552
1553
1554
1555 test_calculate_apparent_age()
1556
1557
1558