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
364 unit_keys = {
365 'year': _('yYaA_keys_year'),
366 'month': _('mM_keys_month'),
367 'week': _('wW_keys_week'),
368 'day': _('dD_keys_day'),
369 'hour': _('hH_keys_hour')
370 }
371
372 str_interval = str_interval.strip()
373
374
375 keys = '|'.join(list(unit_keys['year'].replace('_keys_year', u'')))
376 if regex.match(u'^~*(\s|\t)*\d+(%s)*$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
377 return pyDT.timedelta(days = (int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]) * avg_days_per_gregorian_year))
378
379
380 keys = '|'.join(list(unit_keys['month'].replace('_keys_month', u'')))
381 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
382 years, months = divmod (
383 int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]),
384 12
385 )
386 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month)))
387
388
389 keys = '|'.join(list(unit_keys['week'].replace('_keys_week', u'')))
390 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
391 return pyDT.timedelta(weeks = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
392
393
394 keys = '|'.join(list(unit_keys['day'].replace('_keys_day', u'')))
395 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
396 return pyDT.timedelta(days = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
397
398
399 keys = '|'.join(list(unit_keys['hour'].replace('_keys_hour', u'')))
400 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.LOCALE | regex.UNICODE):
401 return pyDT.timedelta(hours = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
402
403
404 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*12$', str_interval, flags = regex.LOCALE | regex.UNICODE):
405 years, months = divmod (
406 int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]),
407 12
408 )
409 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month)))
410
411
412 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*52$', str_interval, flags = regex.LOCALE | regex.UNICODE):
413
414 return pyDT.timedelta(weeks = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
415
416
417 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*7$', str_interval, flags = regex.LOCALE | regex.UNICODE):
418 return pyDT.timedelta(days = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
419
420
421 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*24$', str_interval, flags = regex.LOCALE | regex.UNICODE):
422 return pyDT.timedelta(hours = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
423
424
425 if regex.match(u'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*60$', str_interval, flags = regex.LOCALE | regex.UNICODE):
426 return pyDT.timedelta(minutes = int(regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)[0]))
427
428
429 keys_year = '|'.join(list(unit_keys['year'].replace('_keys_year', u'')))
430 keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', u'')))
431 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):
432 parts = regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)
433 years, months = divmod(int(parts[1]), 12)
434 years += int(parts[0])
435 return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month)))
436
437
438 keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', u'')))
439 keys_week = '|'.join(list(unit_keys['week'].replace('_keys_week', u'')))
440 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):
441 parts = regex.findall(u'\d+', str_interval, flags = regex.LOCALE | regex.UNICODE)
442 months, weeks = divmod(int(parts[1]), 4)
443 months += int(parts[0])
444 return pyDT.timedelta(days = ((months * avg_days_per_gregorian_month) + (weeks * days_per_week)))
445
446 return None
447
448
449
450
452 """
453 Default is 'hdwm':
454 h - hours
455 d - days
456 w - weeks
457 m - months
458 y - years
459
460 This also defines the significance of the order of the characters.
461 """
462 if offset_chars is None:
463 offset_chars = _('hdwmy (single character date offset triggers)')[:5].lower()
464
465
466 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):
467 return []
468 val = int(regex.findall(u'\d{1,2}', str2parse, flags = regex.LOCALE | regex.UNICODE)[0])
469 offset_char = regex.findall(u'[%s]' % offset_chars, str2parse, flags = regex.LOCALE | regex.UNICODE)[0].lower()
470
471 now = mxDT.now()
472 enc = gmI18N.get_encoding()
473
474
475 is_future = True
476 if str2parse.find('-') > -1:
477 is_future = False
478
479 ts = None
480
481 if offset_char == offset_chars[0]:
482 if is_future:
483 ts = now + mxDT.RelativeDateTime(hours = val)
484 label = _('in %d hour(s) - %s') % (val, ts.strftime('%H:%M'))
485 else:
486 ts = now - mxDT.RelativeDateTime(hours = val)
487 label = _('%d hour(s) ago - %s') % (val, ts.strftime('%H:%M'))
488 accuracy = acc_subseconds
489
490 elif offset_char == offset_chars[1]:
491 if is_future:
492 ts = now + mxDT.RelativeDateTime(days = val)
493 label = _('in %d day(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
494 else:
495 ts = now - mxDT.RelativeDateTime(days = val)
496 label = _('%d day(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
497 accuracy = acc_days
498
499 elif offset_char == offset_chars[2]:
500 if is_future:
501 ts = now + mxDT.RelativeDateTime(weeks = val)
502 label = _('in %d week(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
503 else:
504 ts = now - mxDT.RelativeDateTime(weeks = val)
505 label = _('%d week(s) ago - %s)') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
506 accuracy = acc_days
507
508 elif offset_char == offset_chars[3]:
509 if is_future:
510 ts = now + mxDT.RelativeDateTime(months = val)
511 label = _('in %d month(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
512 else:
513 ts = now - mxDT.RelativeDateTime(months = val)
514 label = _('%d month(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
515 accuracy = acc_days
516
517 elif offset_char == offset_chars[4]:
518 if is_future:
519 ts = now + mxDT.RelativeDateTime(years = val)
520 label = _('in %d year(s) - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
521 else:
522 ts = now - mxDT.RelativeDateTime(years = val)
523 label = _('%d year(s) ago - %s') % (val, ts.strftime('%A, %Y-%m-%d').decode(enc))
524 accuracy = acc_months
525
526 if ts is None:
527 return []
528
529 tmp = {
530 'data': cFuzzyTimestamp(timestamp = ts, accuracy = accuracy),
531 'label': label
532 }
533 return [tmp]
534
536 """This matches on single characters.
537
538 Spaces and tabs are discarded.
539
540 Default is 'ndmy':
541 n - now
542 d - toDay
543 m - toMorrow Someone please suggest a synonym !
544 y - yesterday
545
546 This also defines the significance of the order of the characters.
547 """
548 if trigger_chars is None:
549 trigger_chars = _('ndmy (single character date triggers)')[:4].lower()
550
551 if not regex.match(u'^(\s|\t)*[%s]{1}(\s|\t)*$' % trigger_chars, str2parse, flags = regex.LOCALE | regex.UNICODE):
552 return []
553 val = str2parse.strip().lower()
554
555 now = mxDT.now()
556 enc = gmI18N.get_encoding()
557
558
559
560
561 if val == trigger_chars[0]:
562 ts = now
563 return [{
564 'data': cFuzzyTimestamp (
565 timestamp = ts,
566 accuracy = acc_subseconds
567 ),
568 'label': _('right now (%s, %s)') % (ts.strftime('%A').decode(enc), ts)
569 }]
570
571
572 if val == trigger_chars[1]:
573 return [{
574 'data': cFuzzyTimestamp (
575 timestamp = now,
576 accuracy = acc_days
577 ),
578 'label': _('today (%s)') % now.strftime('%A, %Y-%m-%d').decode(enc)
579 }]
580
581
582 if val == trigger_chars[2]:
583 ts = now + mxDT.RelativeDateTime(days = +1)
584 return [{
585 'data': cFuzzyTimestamp (
586 timestamp = ts,
587 accuracy = acc_days
588 ),
589 'label': _('tomorrow (%s)') % ts.strftime('%A, %Y-%m-%d').decode(enc)
590 }]
591
592
593 if val == trigger_chars[3]:
594 ts = now + mxDT.RelativeDateTime(days = -1)
595 return [{
596 'data': cFuzzyTimestamp (
597 timestamp = ts,
598 accuracy = acc_days
599 ),
600 'label': _('yesterday (%s)') % ts.strftime('%A, %Y-%m-%d').decode(enc)
601 }]
602
603 return []
604
606 """Expand fragments containing a single slash.
607
608 "5/"
609 - 2005/ (2000 - 2025)
610 - 1995/ (1990 - 1999)
611 - Mai/current year
612 - Mai/next year
613 - Mai/last year
614 - Mai/200x
615 - Mai/20xx
616 - Mai/199x
617 - Mai/198x
618 - Mai/197x
619 - Mai/19xx
620 """
621 matches = []
622 now = mxDT.now()
623 if regex.match(u"^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE):
624 val = int(regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE)[0])
625
626 if val < 100 and val >= 0:
627 matches.append ({
628 'data': None,
629 'label': '%s/' % (val + 1900)
630 })
631
632 if val < 26 and val >= 0:
633 matches.append ({
634 'data': None,
635 'label': '%s/' % (val + 2000)
636 })
637
638 if val < 10 and val >= 0:
639 matches.append ({
640 'data': None,
641 'label': '%s/' % (val + 1990)
642 })
643
644 if val < 13 and val > 0:
645 matches.append ({
646 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months),
647 'label': '%.2d/%s' % (val, now.year)
648 })
649 ts = now + mxDT.RelativeDateTime(years = 1)
650 matches.append ({
651 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months),
652 'label': '%.2d/%s' % (val, ts.year)
653 })
654 ts = now + mxDT.RelativeDateTime(years = -1)
655 matches.append ({
656 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_months),
657 'label': '%.2d/%s' % (val, ts.year)
658 })
659 matches.append ({
660 'data': None,
661 'label': '%.2d/200' % val
662 })
663 matches.append ({
664 'data': None,
665 'label': '%.2d/20' % val
666 })
667 matches.append ({
668 'data': None,
669 'label': '%.2d/199' % val
670 })
671 matches.append ({
672 'data': None,
673 'label': '%.2d/198' % val
674 })
675 matches.append ({
676 'data': None,
677 'label': '%.2d/197' % val
678 })
679 matches.append ({
680 'data': None,
681 'label': '%.2d/19' % val
682 })
683
684 elif regex.match(u"^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*\d{4}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE):
685 parts = regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE)
686 fts = cFuzzyTimestamp (
687 timestamp = mxDT.now() + mxDT.RelativeDateTime(year = int(parts[1]), month = int(parts[0])),
688 accuracy = acc_months
689 )
690 matches.append ({
691 'data': fts,
692 'label': fts.format_accurately()
693 })
694
695 return matches
696
698 """This matches on single numbers.
699
700 Spaces or tabs are discarded.
701 """
702 if not regex.match(u"^(\s|\t)*\d{1,4}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE):
703 return []
704
705
706
707 enc = gmI18N.get_encoding()
708 now = mxDT.now()
709 val = int(regex.findall(u'\d{1,4}', str2parse, flags = regex.LOCALE | regex.UNICODE)[0])
710
711 matches = []
712
713
714 if (1850 < val) and (val < 2100):
715 ts = now + mxDT.RelativeDateTime(year = val)
716 target_date = cFuzzyTimestamp (
717 timestamp = ts,
718 accuracy = acc_years
719 )
720 tmp = {
721 'data': target_date,
722 'label': '%s' % target_date
723 }
724 matches.append(tmp)
725
726
727 if val <= gregorian_month_length[now.month]:
728 ts = now + mxDT.RelativeDateTime(day = val)
729 target_date = cFuzzyTimestamp (
730 timestamp = ts,
731 accuracy = acc_days
732 )
733 tmp = {
734 'data': target_date,
735 'label': _('%d. of %s (this month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc))
736 }
737 matches.append(tmp)
738
739
740 if val <= gregorian_month_length[(now + mxDT.RelativeDateTime(months = 1)).month]:
741 ts = now + mxDT.RelativeDateTime(months = 1, day = val)
742 target_date = cFuzzyTimestamp (
743 timestamp = ts,
744 accuracy = acc_days
745 )
746 tmp = {
747 'data': target_date,
748 'label': _('%d. of %s (next month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc))
749 }
750 matches.append(tmp)
751
752
753 if val <= gregorian_month_length[(now + mxDT.RelativeDateTime(months = -1)).month]:
754 ts = now + mxDT.RelativeDateTime(months = -1, day = val)
755 target_date = cFuzzyTimestamp (
756 timestamp = ts,
757 accuracy = acc_days
758 )
759 tmp = {
760 'data': target_date,
761 'label': _('%d. of %s (last month) - a %s') % (val, ts.strftime('%B').decode(enc), ts.strftime('%A').decode(enc))
762 }
763 matches.append(tmp)
764
765
766 if val <= 400:
767 ts = now + mxDT.RelativeDateTime(days = val)
768 target_date = cFuzzyTimestamp (
769 timestamp = ts
770 )
771 tmp = {
772 'data': target_date,
773 'label': _('in %d day(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d').decode(enc))
774 }
775 matches.append(tmp)
776
777
778 if val <= 50:
779 ts = now + mxDT.RelativeDateTime(weeks = val)
780 target_date = cFuzzyTimestamp (
781 timestamp = ts
782 )
783 tmp = {
784 'data': target_date,
785 'label': _('in %d week(s) - %s') % (val, target_date.timestamp.strftime('%A, %Y-%m-%d').decode(enc))
786 }
787 matches.append(tmp)
788
789
790 if val < 13:
791
792 ts = now + mxDT.RelativeDateTime(month = val)
793 target_date = cFuzzyTimestamp (
794 timestamp = ts,
795 accuracy = acc_months
796 )
797 tmp = {
798 'data': target_date,
799 'label': _('%s (%s this year)') % (target_date, ts.strftime('%B').decode(enc))
800 }
801 matches.append(tmp)
802
803
804 ts = now + mxDT.RelativeDateTime(years = 1, month = val)
805 target_date = cFuzzyTimestamp (
806 timestamp = ts,
807 accuracy = acc_months
808 )
809 tmp = {
810 'data': target_date,
811 'label': _('%s (%s next year)') % (target_date, ts.strftime('%B').decode(enc))
812 }
813 matches.append(tmp)
814
815
816 ts = now + mxDT.RelativeDateTime(years = -1, month = val)
817 target_date = cFuzzyTimestamp (
818 timestamp = ts,
819 accuracy = acc_months
820 )
821 tmp = {
822 'data': target_date,
823 'label': _('%s (%s last year)') % (target_date, ts.strftime('%B').decode(enc))
824 }
825 matches.append(tmp)
826
827
828 matches.append ({
829 'data': None,
830 'label': '%s/200' % val
831 })
832 matches.append ({
833 'data': None,
834 'label': '%s/199' % val
835 })
836 matches.append ({
837 'data': None,
838 'label': '%s/198' % val
839 })
840 matches.append ({
841 'data': None,
842 'label': '%s/19' % val
843 })
844
845
846 if val < 8:
847
848 ts = now + mxDT.RelativeDateTime(weekday = (val-1, 0))
849 target_date = cFuzzyTimestamp (
850 timestamp = ts,
851 accuracy = acc_days
852 )
853 tmp = {
854 'data': target_date,
855 'label': _('%s this week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc))
856 }
857 matches.append(tmp)
858
859
860 ts = now + mxDT.RelativeDateTime(weeks = +1, weekday = (val-1, 0))
861 target_date = cFuzzyTimestamp (
862 timestamp = ts,
863 accuracy = acc_days
864 )
865 tmp = {
866 'data': target_date,
867 'label': _('%s next week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc))
868 }
869 matches.append(tmp)
870
871
872 ts = now + mxDT.RelativeDateTime(weeks = -1, weekday = (val-1, 0))
873 target_date = cFuzzyTimestamp (
874 timestamp = ts,
875 accuracy = acc_days
876 )
877 tmp = {
878 'data': target_date,
879 'label': _('%s last week (%s of %s)') % (ts.strftime('%A').decode(enc), ts.day, ts.strftime('%B').decode(enc))
880 }
881 matches.append(tmp)
882
883 if val < 100:
884 matches.append ({
885 'data': None,
886 'label': '%s/' % (1900 + val)
887 })
888
889 if val == 200:
890 tmp = {
891 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_days),
892 'label': '%s' % target_date
893 }
894 matches.append(tmp)
895 matches.append ({
896 'data': cFuzzyTimestamp(timestamp = now, accuracy = acc_months),
897 'label': '%.2d/%s' % (now.month, now.year)
898 })
899 matches.append ({
900 'data': None,
901 'label': '%s/' % now.year
902 })
903 matches.append ({
904 'data': None,
905 'label': '%s/' % (now.year + 1)
906 })
907 matches.append ({
908 'data': None,
909 'label': '%s/' % (now.year - 1)
910 })
911
912 if val < 200 and val >= 190:
913 for i in range(10):
914 matches.append ({
915 'data': None,
916 'label': '%s%s/' % (val, i)
917 })
918
919 return matches
920
922 """Expand fragments containing a single dot.
923
924 Standard colloquial date format in Germany: day.month.year
925
926 "14."
927 - 14th current month this year
928 - 14th next month this year
929 """
930 if not regex.match(u"^(\s|\t)*\d{1,2}\.{1}(\s|\t)*$", str2parse, flags = regex.LOCALE | regex.UNICODE):
931 return []
932
933 val = int(regex.findall(u'\d+', str2parse, flags = regex.LOCALE | regex.UNICODE)[0])
934 now = mxDT.now()
935 enc = gmI18N.get_encoding()
936
937 matches = []
938
939
940 ts = now + mxDT.RelativeDateTime(day = val)
941 if val > 0 and val <= gregorian_month_length[ts.month]:
942 matches.append ({
943 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days),
944 'label': '%s.%s.%s - a %s this month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc))
945 })
946
947
948 ts = now + mxDT.RelativeDateTime(day = val, months = +1)
949 if val > 0 and val <= gregorian_month_length[ts.month]:
950 matches.append ({
951 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days),
952 'label': '%s.%s.%s - a %s next month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc))
953 })
954
955
956 ts = now + mxDT.RelativeDateTime(day = val, months = -1)
957 if val > 0 and val <= gregorian_month_length[ts.month]:
958 matches.append ({
959 'data': cFuzzyTimestamp(timestamp = ts, accuracy = acc_days),
960 'label': '%s.%s.%s - a %s last month' % (ts.day, ts.month, ts.year, ts.strftime('%A').decode(enc))
961 })
962
963 return matches
964
966 """
967 Turn a string into candidate fuzzy timestamps and auto-completions the user is likely to type.
968
969 You MUST have called locale.setlocale(locale.LC_ALL, '')
970 somewhere in your code previously.
971
972 @param default_time: if you want to force the time part of the time
973 stamp to a given value and the user doesn't type any time part
974 this value will be used
975 @type default_time: an mx.DateTime.DateTimeDelta instance
976
977 @param patterns: list of [time.strptime compatible date/time pattern, accuracy]
978 @type patterns: list
979 """
980 matches = __single_dot(str2parse)
981 matches.extend(__numbers_only(str2parse))
982 matches.extend(__single_slash(str2parse))
983 matches.extend(__single_char(str2parse))
984 matches.extend(__explicit_offset(str2parse))
985
986
987 if mxDT is not None:
988 try:
989
990 date_only = mxDT.Parser.DateFromString (
991 text = str2parse,
992 formats = ('euro', 'iso', 'us', 'altus', 'altiso', 'lit', 'altlit', 'eurlit')
993 )
994
995 time_part = mxDT.Parser.TimeFromString(text = str2parse)
996 datetime = date_only + time_part
997 if datetime == date_only:
998 accuracy = acc_days
999 if isinstance(default_time, mxDT.DateTimeDeltaType):
1000 datetime = date_only + default_time
1001 accuracy = acc_minutes
1002 else:
1003 accuracy = acc_subseconds
1004 fts = cFuzzyTimestamp (
1005 timestamp = datetime,
1006 accuracy = accuracy
1007 )
1008 matches.append ({
1009 'data': fts,
1010 'label': fts.format_accurately()
1011 })
1012 except (ValueError, mxDT.RangeError):
1013 pass
1014
1015 if patterns is None:
1016 patterns = []
1017
1018 patterns.append(['%Y.%m.%d', acc_days])
1019 patterns.append(['%Y/%m/%d', acc_days])
1020
1021 for pattern in patterns:
1022 try:
1023 fts = cFuzzyTimestamp (
1024 timestamp = pyDT.datetime.fromtimestamp(time.mktime(time.strptime(str2parse, pattern[0]))),
1025 accuracy = pattern[1]
1026 )
1027 matches.append ({
1028 'data': fts,
1029 'label': fts.format_accurately()
1030 })
1031 except AttributeError:
1032
1033 break
1034 except OverflowError:
1035
1036 continue
1037 except ValueError:
1038
1039 continue
1040
1041 return matches
1042
1043
1044
1046
1047
1048
1049 """A timestamp implementation with definable inaccuracy.
1050
1051 This class contains an mxDateTime.DateTime instance to
1052 hold the actual timestamp. It adds an accuracy attribute
1053 to allow the programmer to set the precision of the
1054 timestamp.
1055
1056 The timestamp will have to be initialzed with a fully
1057 precise value (which may, of course, contain partially
1058 fake data to make up for missing values). One can then
1059 set the accuracy value to indicate up to which part of
1060 the timestamp the data is valid. Optionally a modifier
1061 can be set to indicate further specification of the
1062 value (such as "summer", "afternoon", etc).
1063
1064 accuracy values:
1065 1: year only
1066 ...
1067 7: everything including milliseconds value
1068
1069 Unfortunately, one cannot directly derive a class from mx.DateTime.DateTime :-(
1070 """
1071
1073
1074 if timestamp is None:
1075 timestamp = mxDT.now()
1076 accuracy = acc_subseconds
1077 modifier = ''
1078
1079 if isinstance(timestamp, pyDT.datetime):
1080 timestamp = mxDT.DateTime(timestamp.year, timestamp.month, timestamp.day, timestamp.hour, timestamp.minute, timestamp.second)
1081
1082 if type(timestamp) != mxDT.DateTimeType:
1083 raise TypeError, '%s.__init__(): <timestamp> must be of mx.DateTime.DateTime or datetime.datetime type' % self.__class__.__name__
1084
1085 self.timestamp = timestamp
1086
1087 if (accuracy < 1) or (accuracy > 8):
1088 raise ValueError, '%s.__init__(): <accuracy> must be between 1 and 7' % self.__class__.__name__
1089 self.accuracy = accuracy
1090
1091 self.modifier = modifier
1092
1093
1094
1095
1097 """Return string representation meaningful to a user, also for %s formatting."""
1098 return self.format_accurately()
1099
1101 """Return string meaningful to a programmer to aid in debugging."""
1102 tmp = '<[%s]: timestamp [%s], accuracy [%s] (%s), modifier [%s] at %s>' % (
1103 self.__class__.__name__,
1104 repr(self.timestamp),
1105 self.accuracy,
1106 _accuracy_strings[self.accuracy],
1107 self.modifier,
1108 id(self)
1109 )
1110 return tmp
1111
1112
1113
1118
1121
1148
1150 return self.timestamp
1151
1153 try:
1154 gmtoffset = self.timestamp.gmtoffset()
1155 except mxDT.Error:
1156
1157
1158 now = mxDT.now()
1159 gmtoffset = now.gmtoffset()
1160 tz = cFixedOffsetTimezone(gmtoffset.minutes, self.timestamp.tz)
1161 secs, msecs = divmod(self.timestamp.second, 1)
1162 ts = pyDT.datetime (
1163 year = self.timestamp.year,
1164 month = self.timestamp.month,
1165 day = self.timestamp.day,
1166 hour = self.timestamp.hour,
1167 minute = self.timestamp.minute,
1168 second = secs,
1169 microsecond = msecs,
1170 tzinfo = tz
1171 )
1172 return ts
1173
1174
1175
1176 if __name__ == '__main__':
1177
1178 intervals_as_str = [
1179 '7', '12', ' 12', '12 ', ' 12 ', ' 12 ', '0', '~12', '~ 12', ' ~ 12', ' ~ 12 ',
1180 '12a', '12 a', '12 a', '12j', '12J', '12y', '12Y', ' ~ 12 a ', '~0a',
1181 '12m', '17 m', '12 m', '17M', ' ~ 17 m ', ' ~ 3 / 12 ', '7/12', '0/12',
1182 '12w', '17 w', '12 w', '17W', ' ~ 17 w ', ' ~ 15 / 52', '2/52', '0/52',
1183 '12d', '17 d', '12 t', '17D', ' ~ 17 T ', ' ~ 12 / 7', '3/7', '0/7',
1184 '12h', '17 h', '12 H', '17H', ' ~ 17 h ', ' ~ 36 / 24', '7/24', '0/24',
1185 ' ~ 36 / 60', '7/60', '190/60', '0/60',
1186 '12a1m', '12 a 1 M', '12 a17m', '12j 12m', '12J7m', '12y7m', '12Y7M', ' ~ 12 a 37 m ', '~0a0m',
1187 '10m1w',
1188 'invalid interval input'
1189 ]
1190
1196
1264
1266 print "testing str2interval()"
1267 print "----------------------"
1268
1269 for interval_as_str in intervals_as_str:
1270 print "input: <%s>" % interval_as_str
1271 print " ==>", str2interval(str_interval=interval_as_str)
1272
1273 return True
1274
1276 print "DST currently in effect:", dst_currently_in_effect
1277 print "current UTC offset:", current_local_utc_offset_in_seconds, "seconds"
1278 print "current timezone (interval):", current_local_timezone_interval
1279 print "current timezone (ISO conformant numeric string):", current_local_iso_numeric_timezone_string
1280 print "local timezone class:", cLocalTimezone
1281 print ""
1282 tz = cLocalTimezone()
1283 print "local timezone instance:", tz
1284 print " (total) UTC offset:", tz.utcoffset(pyDT.datetime.now())
1285 print " DST adjustment:", tz.dst(pyDT.datetime.now())
1286 print " timezone name:", tz.tzname(pyDT.datetime.now())
1287 print ""
1288 print "current local timezone:", gmCurrentLocalTimezone
1289 print " (total) UTC offset:", gmCurrentLocalTimezone.utcoffset(pyDT.datetime.now())
1290 print " DST adjustment:", gmCurrentLocalTimezone.dst(pyDT.datetime.now())
1291 print " timezone name:", gmCurrentLocalTimezone.tzname(pyDT.datetime.now())
1292 print ""
1293 print "now here:", pydt_now_here()
1294 print ""
1295
1297 print "testing function str2fuzzy_timestamp_matches"
1298 print "--------------------------------------------"
1299
1300 val = None
1301 while val != 'exit':
1302 val = raw_input('Enter date fragment ("exit" quits): ')
1303 matches = str2fuzzy_timestamp_matches(str2parse = val)
1304 for match in matches:
1305 print 'label shown :', match['label']
1306 print 'data attached:', match['data']
1307 print ""
1308 print "---------------"
1309
1311 print "testing fuzzy timestamp class"
1312 print "-----------------------------"
1313
1314 ts = mxDT.now()
1315 print "mx.DateTime timestamp", type(ts)
1316 print " print ... :", ts
1317 print " print '%%s' %% ...: %s" % ts
1318 print " str() :", str(ts)
1319 print " repr() :", repr(ts)
1320
1321 fts = cFuzzyTimestamp()
1322 print "\nfuzzy timestamp <%s '%s'>" % ('class', fts.__class__.__name__)
1323 for accuracy in range(1,8):
1324 fts.accuracy = accuracy
1325 print " accuracy : %s (%s)" % (accuracy, _accuracy_strings[accuracy])
1326 print " format_accurately:", fts.format_accurately()
1327 print " strftime() :", fts.strftime('%c')
1328 print " print ... :", fts
1329 print " print '%%s' %% ... : %s" % fts
1330 print " str() :", str(fts)
1331 print " repr() :", repr(fts)
1332 raw_input('press ENTER to continue')
1333
1335 print "testing platform for handling dates before 1970"
1336 print "-----------------------------------------------"
1337 ts = mxDT.DateTime(1935, 4, 2)
1338 fts = cFuzzyTimestamp(timestamp=ts)
1339 print "fts :", fts
1340 print "fts.get_pydt():", fts.get_pydt()
1341
1342 if len(sys.argv) > 1 and sys.argv[1] == "test":
1343
1344
1345 gmI18N.activate_locale()
1346 gmI18N.install_domain('gnumed')
1347
1348 init()
1349
1350
1351
1352
1353
1354
1355
1356 test_format_interval_medically()
1357
1358
1359