comparison roundup/date.py @ 2339:6ba57546d212

fix i18n - mainly plural forms in Interval.pretty() Date and Interval constructors accept translator object to allow locale switching at runtime.
author Alexander Smishlajev <a1s@users.sourceforge.net>
date Wed, 19 May 2004 17:12:18 +0000
parents a13ec40cf8f5
children 8611bf29baec
comparison
equal deleted inserted replaced
2338:d9a6918aafd5 2339:6ba57546d212
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, 12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" 14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, 15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 # 17 #
18 # $Id: date.py,v 1.68 2004-05-06 02:35:46 richard Exp $ 18 # $Id: date.py,v 1.69 2004-05-19 17:12:18 a1s Exp $
19 19
20 """Date, time and time interval handling. 20 """Date, time and time interval handling.
21 """ 21 """
22 __docformat__ = 'restructuredtext' 22 __docformat__ = 'restructuredtext'
23 23
24 import time, re, calendar, types 24 import time, re, calendar, types
25 from types import * 25 from types import *
26 from i18n import _ 26 import i18n
27 27
28 def _add_granularity(src, order, value = 1): 28 def _add_granularity(src, order, value = 1):
29 '''Increment first non-None value in src dictionary ordered by 'order' 29 '''Increment first non-None value in src dictionary ordered by 'order'
30 parameter 30 parameter
31 ''' 31 '''
41 (hh:mm:ss) by a period ("."). Dates in this form can be easily compared 41 (hh:mm:ss) by a period ("."). Dates in this form can be easily compared
42 and are fairly readable when printed. An example of a valid stamp is 42 and are fairly readable when printed. An example of a valid stamp is
43 "2000-06-24.13:03:59". We'll call this the "full date format". When 43 "2000-06-24.13:03:59". We'll call this the "full date format". When
44 Timestamp objects are printed as strings, they appear in the full date 44 Timestamp objects are printed as strings, they appear in the full date
45 format with the time always given in GMT. The full date format is 45 format with the time always given in GMT. The full date format is
46 always exactly 19 characters long. 46 always exactly 19 characters long.
47 47
48 For user input, some partial forms are also permitted: the whole time 48 For user input, some partial forms are also permitted: the whole time
49 or just the seconds may be omitted; and the whole date may be omitted 49 or just the seconds may be omitted; and the whole date may be omitted
50 or just the year may be omitted. If the time is given, the time is 50 or just the year may be omitted. If the time is given, the time is
51 interpreted in the user's local time zone. The Date constructor takes 51 interpreted in the user's local time zone. The Date constructor takes
107 <Date 2004-04-06.22:04:20.000000> 107 <Date 2004-04-06.22:04:20.000000>
108 >>> d1-i1 108 >>> d1-i1
109 <Date 2003-07-01.00:00:0.000000> 109 <Date 2003-07-01.00:00:0.000000>
110 ''' 110 '''
111 111
112 def __init__(self, spec='.', offset=0, add_granularity=0): 112 def __init__(self, spec='.', offset=0, add_granularity=0, translator=i18n):
113 """Construct a date given a specification and a time zone offset. 113 """Construct a date given a specification and a time zone offset.
114 114
115 'spec' 115 'spec'
116 is a full date or a partial form, with an optional added or 116 is a full date or a partial form, with an optional added or
117 subtracted interval. Or a date 9-tuple. 117 subtracted interval. Or a date 9-tuple.
118 'offset' 118 'offset'
119 is the local time zone offset from GMT in hours. 119 is the local time zone offset from GMT in hours.
120 'translator'
121 is i18n module or one of gettext translation classes.
122 It must have attributes 'gettext' and 'ngettext',
123 serving as translation functions.
120 """ 124 """
125 self._ = translator.gettext
126 self.ngettext = translator.ngettext
121 if type(spec) == type(''): 127 if type(spec) == type(''):
122 self.set(spec, offset=offset, add_granularity=add_granularity) 128 self.set(spec, offset=offset, add_granularity=add_granularity)
123 return 129 return
124 elif hasattr(spec, 'tuple'): 130 elif hasattr(spec, 'tuple'):
125 spec = spec.tuple() 131 spec = spec.tuple()
157 return 163 return
158 164
159 # not serialised data, try usual format 165 # not serialised data, try usual format
160 m = date_re.match(spec) 166 m = date_re.match(spec)
161 if m is None: 167 if m is None:
162 raise ValueError, _('Not a date spec: %s' % self.usagespec) 168 raise ValueError, self._('Not a date spec: %s') % self.usagespec
163 169
164 info = m.groupdict() 170 info = m.groupdict()
165 171
166 if add_granularity: 172 if add_granularity:
167 _add_granularity(info, 'SMHdmyab') 173 _add_granularity(info, 'SMHdmyab')
168 174
169 # get the current date as our default 175 # get the current date as our default
170 ts = time.time() 176 ts = time.time()
171 frac = ts - int(ts) 177 frac = ts - int(ts)
172 y,m,d,H,M,S,x,x,x = time.gmtime(ts) 178 y,m,d,H,M,S,x,x,x = time.gmtime(ts)
173 # gmtime loses the fractional seconds 179 # gmtime loses the fractional seconds
174 S = S + frac 180 S = S + frac
175 181
176 if info['y'] is not None or info['a'] is not None: 182 if info['y'] is not None or info['a'] is not None:
177 if info['y'] is not None: 183 if info['y'] is not None:
178 y = int(info['y']) 184 y = int(info['y'])
195 if info['S'] is not None: 201 if info['S'] is not None:
196 S = float(info['S']) 202 S = float(info['S'])
197 203
198 if add_granularity: 204 if add_granularity:
199 S = S - 1 205 S = S - 1
200 206
201 # now handle the adjustment of hour 207 # now handle the adjustment of hour
202 frac = S - int(S) 208 frac = S - int(S)
203 ts = calendar.timegm((y,m,d,H,M,S,0,0,0)) 209 ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
204 self.year, self.month, self.day, self.hour, self.minute, \ 210 self.year, self.month, self.day, self.hour, self.minute, \
205 self.second, x, x, x = time.gmtime(ts) 211 self.second, x, x, x = time.gmtime(ts)
208 214
209 if info.get('o', None): 215 if info.get('o', None):
210 try: 216 try:
211 self.applyInterval(Interval(info['o'], allowdate=0)) 217 self.applyInterval(Interval(info['o'], allowdate=0))
212 except ValueError: 218 except ValueError:
213 raise ValueError, _('%r not a date spec (%s)')%(spec, 219 raise ValueError, self._('%r not a date spec (%s)')%(spec,
214 self.usagespec) 220 self.usagespec)
215 221
216 def addInterval(self, interval): 222 def addInterval(self, interval):
217 ''' Add the interval to this date, returning the date tuple 223 ''' Add the interval to this date, returning the date tuple
218 ''' 224 '''
247 if month == 2 and calendar.isleap(year): return 29 253 if month == 2 and calendar.isleap(year): return 29
248 else: return calendar.mdays[month] 254 else: return calendar.mdays[month]
249 255
250 while month < 1 or month > 12 or day < 1 or day > get_mdays(year,month): 256 while month < 1 or month > 12 or day < 1 or day > get_mdays(year,month):
251 # now to day under/over 257 # now to day under/over
252 if day < 1: 258 if day < 1:
253 # When going backwards, decrement month, then increment days 259 # When going backwards, decrement month, then increment days
254 month -= 1 260 month -= 1
255 day += get_mdays(year,month) 261 day += get_mdays(year,month)
256 elif day > get_mdays(year,month): 262 elif day > get_mdays(year,month):
257 # When going forwards, decrement days, then increment month 263 # When going forwards, decrement days, then increment month
258 day -= get_mdays(year,month) 264 day -= get_mdays(year,month)
259 month += 1 265 month += 1
260 266
261 # possibly fix up the month so we're within range 267 # possibly fix up the month so we're within range
430 minute, second) is the serialisation format returned by the serialise() 436 minute, second) is the serialisation format returned by the serialise()
431 method, and is accepted as an argument on instatiation. 437 method, and is accepted as an argument on instatiation.
432 438
433 TODO: more examples, showing the order of addition operation 439 TODO: more examples, showing the order of addition operation
434 ''' 440 '''
435 def __init__(self, spec, sign=1, allowdate=1, add_granularity=0): 441 def __init__(self, spec, sign=1, allowdate=1, add_granularity=0,
442 translator=i18n
443 ):
436 """Construct an interval given a specification.""" 444 """Construct an interval given a specification."""
445 self._ = translator.gettext
446 self.ngettext = translator.ngettext
437 if type(spec) in (IntType, FloatType, LongType): 447 if type(spec) in (IntType, FloatType, LongType):
438 self.from_seconds(spec) 448 self.from_seconds(spec)
439 elif type(spec) in (StringType, UnicodeType): 449 elif type(spec) in (StringType, UnicodeType):
440 self.set(spec, allowdate=allowdate, add_granularity=add_granularity) 450 self.set(spec, allowdate=allowdate, add_granularity=add_granularity)
441 else: 451 else:
472 self.sign = 1 482 self.sign = 1
473 m = serialised_re.match(spec) 483 m = serialised_re.match(spec)
474 if not m: 484 if not m:
475 m = interval_re.match(spec) 485 m = interval_re.match(spec)
476 if not m: 486 if not m:
477 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] ' 487 raise ValueError, self._('Not an interval spec:'
478 '[#d] [[[H]H:MM]:SS] [date spec]') 488 ' [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec]')
479 else: 489 else:
480 allowdate = 0 490 allowdate = 0
481 491
482 # pull out all the info specified 492 # pull out all the info specified
483 info = m.groupdict() 493 info = m.groupdict()
491 valid = 1 501 valid = 1
492 setattr(self, attr, int(info[group])) 502 setattr(self, attr, int(info[group]))
493 503
494 # make sure it's valid 504 # make sure it's valid
495 if not valid and not info['D']: 505 if not valid and not info['D']:
496 raise ValueError, _('Not an interval spec: [+-] [#y] [#m] [#w] ' 506 raise ValueError, self._('Not an interval spec:'
497 '[#d] [[[H]H:MM]:SS]') 507 ' [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]')
498 508
499 if self.week: 509 if self.week:
500 self.day = self.day + self.week*7 510 self.day = self.day + self.week*7
501 511
502 if info['s'] is not None: 512 if info['s'] is not None:
623 return '<Interval %s>'%self.__str__() 633 return '<Interval %s>'%self.__str__()
624 634
625 def pretty(self): 635 def pretty(self):
626 ''' print up the date date using one of these nice formats.. 636 ''' print up the date date using one of these nice formats..
627 ''' 637 '''
638 _quarters = self.minute / 15
628 if self.year: 639 if self.year:
629 if self.year == 1: 640 s = self.ngettext("%(number)s year", "%(number)s years",
630 s = _('1 year') 641 self.year) % {'number': self.year}
631 else: 642 elif self.month or self.day > 28:
632 s = _('%(number)s years')%{'number': self.year} 643 _months = int(((self.month * 30) + self.day) / 30)
633 elif self.month or self.day > 13: 644 s = self.ngettext("%(number)s month", "%(number)s months",
634 days = (self.month * 30) + self.day 645 _months) % {'number': _months}
635 if days > 28:
636 if int(days/30) > 1:
637 s = _('%(number)s months')%{'number': int(days/30)}
638 else:
639 s = _('1 month')
640 else:
641 s = _('%(number)s weeks')%{'number': int(days/7)}
642 elif self.day > 7: 646 elif self.day > 7:
643 s = _('1 week') 647 _weeks = int(self.day / 7)
648 s = self.ngettext("%(number)s week", "%(number)s weeks",
649 _weeks) % {'number': _weeks}
644 elif self.day > 1: 650 elif self.day > 1:
645 s = _('%(number)s days')%{'number': self.day} 651 # Note: singular form is not used
652 s = self.ngettext('%(number)s day', '%(number)s days',
653 self.day) % {'number': self.day}
646 elif self.day == 1 or self.hour > 12: 654 elif self.day == 1 or self.hour > 12:
647 if self.sign > 0: 655 if self.sign > 0:
648 return _('tomorrow') 656 return self._('tomorrow')
649 else: 657 else:
650 return _('yesterday') 658 return self._('yesterday')
651 elif self.hour > 1: 659 elif self.hour > 1:
652 s = _('%(number)s hours')%{'number': self.hour} 660 # Note: singular form is not used
661 s = self.ngettext('%(number)s hour', '%(number)s hours',
662 self.hour) % {'number': self.hour}
653 elif self.hour == 1: 663 elif self.hour == 1:
654 if self.minute < 15: 664 if self.minute < 15:
655 s = _('an hour') 665 s = self._('an hour')
656 elif self.minute/15 == 2: 666 elif _quarters == 2:
657 s = _('1 1/2 hours') 667 s = self._('1 1/2 hours')
658 else: 668 else:
659 s = _('1 %(number)s/4 hours')%{'number': self.minute/15} 669 s = self.ngettext('1 %(number)s/4 hours',
670 '1 %(number)s/4 hours', _quarters) % {'number': _quarters}
660 elif self.minute < 1: 671 elif self.minute < 1:
661 if self.sign > 0: 672 if self.sign > 0:
662 return _('in a moment') 673 return self._('in a moment')
663 else: 674 else:
664 return _('just now') 675 return self._('just now')
665 elif self.minute == 1: 676 elif self.minute == 1:
666 s = _('1 minute') 677 # Note: used in expressions "in 1 minute" or "1 minute ago"
678 s = self._('1 minute')
667 elif self.minute < 15: 679 elif self.minute < 15:
668 s = _('%(number)s minutes')%{'number': self.minute} 680 # Note: used in expressions "in 2 minutes" or "2 minutes ago"
669 elif int(self.minute/15) == 2: 681 s = self.ngettext('%(number)s minute', '%(number)s minutes',
670 s = _('1/2 an hour') 682 self.minute) % {'number': self.minute}
671 else: 683 elif _quarters == 2:
672 s = _('%(number)s/4 hour')%{'number': int(self.minute/15)} 684 s = self._('1/2 an hour')
673 if self.sign < 0: 685 else:
674 s = s + _(' ago') 686 s = self.ngettext('%(number)s/4 hours', '%(number)s/4 hours',
675 else: 687 _quarters) % {'number': _quarters}
676 s = _('in ') + s 688 # XXX this is internationally broken
689 if self.sign < 0:
690 s = self._('%s ago') % s
691 else:
692 s = self._('in %s') % s
677 return s 693 return s
678 694
679 def get_tuple(self): 695 def get_tuple(self):
680 return (self.sign, self.year, self.month, self.day, self.hour, 696 return (self.sign, self.year, self.month, self.day, self.hour,
681 self.minute, self.second) 697 self.minute, self.second)
685 return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month, 701 return '%s%04d%02d%02d%02d%02d%02d'%(sign, self.year, self.month,
686 self.day, self.hour, self.minute, self.second) 702 self.day, self.hour, self.minute, self.second)
687 703
688 def as_seconds(self): 704 def as_seconds(self):
689 '''Calculate the Interval as a number of seconds. 705 '''Calculate the Interval as a number of seconds.
690 706
691 Months are counted as 30 days, years as 365 days. Returns a Long 707 Months are counted as 30 days, years as 365 days. Returns a Long
692 int. 708 int.
693 ''' 709 '''
694 n = self.year * 365L 710 n = self.year * 365L
695 n = n + self.month * 30 711 n = n + self.month * 30
754 return (sign, y, m, d, H, M, S) 770 return (sign, y, m, d, H, M, S)
755 771
756 class Range: 772 class Range:
757 """Represents range between two values 773 """Represents range between two values
758 Ranges can be created using one of theese two alternative syntaxes: 774 Ranges can be created using one of theese two alternative syntaxes:
759 775
760 1. Native english syntax:: 776 1. Native english syntax::
761 777
762 [[From] <value>][ To <value>] 778 [[From] <value>][ To <value>]
763 779
764 Keywords "From" and "To" are case insensitive. Keyword "From" is 780 Keywords "From" and "To" are case insensitive. Keyword "From" is
772 788
773 Examples (consider local time is Sat Mar 8 22:07:48 EET 2003):: 789 Examples (consider local time is Sat Mar 8 22:07:48 EET 2003)::
774 790
775 >>> Range("from 2-12 to 4-2") 791 >>> Range("from 2-12 to 4-2")
776 <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00> 792 <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
777 793
778 >>> Range("18:00 TO +2m") 794 >>> Range("18:00 TO +2m")
779 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48> 795 <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
780 796
781 >>> Range("12:00") 797 >>> Range("12:00")
782 <Range from 2003-03-08.12:00:00 to None> 798 <Range from 2003-03-08.12:00:00 to None>
783 799
784 >>> Range("tO +3d") 800 >>> Range("tO +3d")
785 <Range from None to 2003-03-11.20:07:48> 801 <Range from None to 2003-03-11.20:07:48>
786 802
787 >>> Range("2002-11-10; 2002-12-12") 803 >>> Range("2002-11-10; 2002-12-12")
788 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00> 804 <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
789 805
790 >>> Range("; 20:00 +1d") 806 >>> Range("; 20:00 +1d")
791 <Range from None to 2003-03-09.20:00:00> 807 <Range from None to 2003-03-09.20:00:00>
792 808
793 """ 809 """
794 def __init__(self, spec, Type, allow_granularity=1, **params): 810 def __init__(self, spec, Type, allow_granularity=1, **params):
795 """Initializes Range of type <Type> from given <spec> string. 811 """Initializes Range of type <Type> from given <spec> string.
796 812
797 Sets two properties - from_value and to_value. None assigned to any of 813 Sets two properties - from_value and to_value. None assigned to any of
798 this properties means "infinitum" (-infinitum to from_value and 814 this properties means "infinitum" (-infinitum to from_value and
799 +infinitum to to_value) 815 +infinitum to to_value)
800 816
801 The Type parameter here should be class itself (e.g. Date), not a 817 The Type parameter here should be class itself (e.g. Date), not a
802 class instance. 818 class instance.
803 819
804 """ 820 """
805 self.range_type = Type 821 self.range_type = Type
806 re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)' 822 re_range = r'(?:^|from(.+?))(?:to(.+?)$|$)'
807 re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)' 823 re_geek_range = r'(?:^|(.+?));(?:(.+?)$|$)'
808 # Check which syntax to use 824 # Check which syntax to use
828 def __str__(self): 844 def __str__(self):
829 return "from %s to %s" % (self.from_value, self.to_value) 845 return "from %s to %s" % (self.from_value, self.to_value)
830 846
831 def __repr__(self): 847 def __repr__(self):
832 return "<Range %s>" % self.__str__() 848 return "<Range %s>" % self.__str__()
833 849
834 def test_range(): 850 def test_range():
835 rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d", 851 rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
836 "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12') 852 "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
837 rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d') 853 rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
838 for rspec in rspecs: 854 for rspec in rspecs:

Roundup Issue Tracker: http://roundup-tracker.org/