Mercurial > p > roundup > code
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: |
