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