"Fossies" - the Fresh Open Source Software Archive

Member "roundup-2.0.0/roundup/date.py" (29 Jun 2020, 47666 Bytes) of package /linux/www/roundup-2.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "date.py": 1.6.1_vs_2.0.0.

    1 #
    2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
    3 # This module is free software, and you may redistribute it and/or modify
    4 # under the same terms as Python, so long as this copyright message and
    5 # disclaimer are retained in their original form.
    6 #
    7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
    8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
    9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
   10 # POSSIBILITY OF SUCH DAMAGE.
   11 #
   12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
   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"
   15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
   16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
   17 
   18 """Date, time and time interval handling.
   19 """
   20 from __future__ import print_function
   21 __docformat__ = 'restructuredtext'
   22 
   23 import calendar
   24 import datetime
   25 import re
   26 
   27 try:
   28     import pytz
   29 except ImportError:
   30     pytz = None
   31 
   32 try:
   33     cmp
   34 except NameError:
   35     # Python 3.
   36     def cmp(a, b):
   37         return (a > b) - (a < b)
   38 
   39 from roundup import i18n
   40 from roundup.anypy.strings import is_us
   41 
   42 # no, I don't know why we must anchor the date RE when we only ever use it
   43 # in a match()
   44 date_re = re.compile(r'''^
   45     ((?P<y>\d\d\d\d)([/-](?P<m>\d\d?)([/-](?P<d>\d\d?))?)? # yyyy[-mm[-dd]]
   46     |(?P<a>\d\d?)[/-](?P<b>\d\d?))?              # or mm-dd
   47     (?P<n>\.)?                                   # .
   48     (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d?(\.\d+)?))?)?  # hh:mm:ss
   49     (?:(?P<tz>\s?[+-]\d{4})|(?P<o>[\d\smywd\-+]+))? # time-zone offset, offset
   50 $''', re.VERBOSE)
   51 serialised_date_re = re.compile(r'''
   52     (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d?(\.\d+)?)
   53 ''', re.VERBOSE)
   54 
   55 _timedelta0 = datetime.timedelta(0)
   56 
   57 # load UTC tzinfo
   58 if pytz:
   59     UTC = pytz.utc
   60 else:
   61     # fallback implementation from Python Library Reference
   62 
   63     class _UTC(datetime.tzinfo):
   64 
   65         """Universal Coordinated Time zoneinfo"""
   66 
   67         def utcoffset(self, dt):
   68             return _timedelta0
   69 
   70         def tzname(self, dt):
   71             return "UTC"
   72 
   73         def dst(self, dt):
   74             return _timedelta0
   75 
   76         def __repr__(self):
   77             return "<UTC>"
   78 
   79         # pytz adjustments interface
   80         # Note: pytz verifies that dt is naive datetime for localize()
   81         # and not naive datetime for normalize().
   82         # In this implementation, we don't care.
   83 
   84         def normalize(self, dt, is_dst=False):
   85             return dt.replace(tzinfo=self)
   86 
   87         def localize(self, dt, is_dst=False):
   88             return dt.replace(tzinfo=self)
   89 
   90     UTC = _UTC()
   91 
   92 
   93 # integral hours offsets were available in Roundup versions prior to 1.1.3
   94 # and still are supported as a fallback if pytz module is not installed
   95 class SimpleTimezone(datetime.tzinfo):
   96 
   97     """Simple zoneinfo with fixed numeric offset and no daylight savings"""
   98 
   99     def __init__(self, offset=0, name=None):
  100         super(SimpleTimezone, self).__init__()
  101         self.offset = offset
  102         if name:
  103             self.name = name
  104         else:
  105             self.name = "Etc/GMT%+d" % self.offset
  106 
  107     def utcoffset(self, dt):
  108         return datetime.timedelta(hours=self.offset)
  109 
  110     def tzname(self, dt):
  111         return self.name
  112 
  113     def dst(self, dt):
  114         return _timedelta0
  115 
  116     def __repr__(self):
  117         return "<%s: %s>" % (self.__class__.__name__, self.name)
  118 
  119     # pytz adjustments interface
  120 
  121     def normalize(self, dt):
  122         return dt.replace(tzinfo=self)
  123 
  124     def localize(self, dt, is_dst=False):
  125         return dt.replace(tzinfo=self)
  126 
  127 
  128 # simple timezones with fixed offset
  129 _tzoffsets = dict(GMT=0, UCT=0, EST=5, MST=7, HST=10)
  130 
  131 
  132 def get_timezone(tz):
  133     # if tz is None, return None (will result in naive datetimes)
  134     # XXX should we return UTC for None?
  135     if tz is None:
  136         return None
  137     # try integer offset first for backward compatibility
  138     try:
  139         utcoffset = int(tz)
  140     except (TypeError, ValueError):
  141         pass
  142     else:
  143         if utcoffset == 0:
  144             return UTC
  145         else:
  146             return SimpleTimezone(utcoffset)
  147     # tz is a timezone name
  148     if pytz:
  149         return pytz.timezone(tz)
  150     elif tz == "UTC":
  151         return UTC
  152     elif tz in _tzoffsets:
  153         return SimpleTimezone(_tzoffsets[tz], tz)
  154     else:
  155         raise KeyError(tz)
  156 
  157 
  158 def _utc_to_local(y,m,d,H,M,S,tz):
  159     TZ = get_timezone(tz)
  160     S = min(S, 59.999)
  161     frac = S - int(S)
  162     dt = datetime.datetime(y, m, d, H, M, int(S), tzinfo=UTC)
  163     y,m,d,H,M,S = dt.astimezone(TZ).timetuple()[:6]
  164     S = S + frac
  165     return (y,m,d,H,M,S)
  166 
  167 
  168 def _local_to_utc(y,m,d,H,M,S,tz):
  169     TZ = get_timezone(tz)
  170     dt = datetime.datetime(y,m,d,H,M,int(S))
  171     y,m,d,H,M,S = TZ.localize(dt).utctimetuple()[:6]
  172     return (y,m,d,H,M,S)
  173 
  174 
  175 def test_ini(t):
  176     """ Monkey-patch to make doctest think it's always time t:
  177     """
  178     u = Date.now
  179     d = datetime.datetime.strptime(t, '%Y-%m-%d.%H:%M:%S.%f')
  180     Date.now = lambda x: d
  181     return u
  182 
  183 
  184 def test_fin(u):
  185     """ Undo monkey patch above
  186     """
  187     Date.now = u
  188 
  189 
  190 class Date:
  191     '''
  192     As strings, date-and-time stamps are specified with the date in
  193     international standard format (yyyy-mm-dd) joined to the time
  194     (hh:mm:ss) by a period ("."). Dates in this form can be easily compared
  195     and are fairly readable when printed. An example of a valid stamp is
  196     "2000-06-24.13:03:59". We'll call this the "full date format". When
  197     Timestamp objects are printed as strings, they appear in the full date
  198     format with the time always given in UTC. The full date format is
  199     always exactly 19 characters long.
  200 
  201     For user input, some partial forms are also permitted: the whole time
  202     or just the seconds may be omitted; and the whole date may be omitted
  203     or just the year may be omitted. If the time is given, the time is
  204     interpreted in the user's local time zone. The Date constructor takes
  205     care of these conversions. In the following examples, suppose that yyyy
  206     is the current year, mm is the current month, and dd is the current day
  207     of the month; and suppose that the user is on Eastern Standard Time.
  208 
  209     Note that Date conversion from user inputs will use the local
  210     timezone, either from the database user (some database schemas have
  211     a timezone property for a user) or from a default in the roundup
  212     configuration. Roundup will store all times in UTC in the database
  213     but display the time to the user in their local timezone as
  214     configured. In the following examples the timezone correction for
  215     Eastern Standard Time (GMT-5, no DST) will be applied explicitly via
  216     an offset, but times are given in UTC in the output.
  217 
  218     Examples::
  219 
  220 
  221         make doctest think it's always 2000-06-26.00:34:02:
  222         >>> u = test_ini('2000-06-26.00:34:02.0')
  223 
  224         >>> Date("2000-04-17-0500")
  225         <Date 2000-04-17.05:00:00.000>
  226         >>> Date("01-25-0500")
  227         <Date 2000-01-25.05:00:00.000>
  228         >>> Date("2000-04-17.03:45-0500")
  229         <Date 2000-04-17.08:45:00.000>
  230         >>> Date("08-13.22:13-0500")
  231         <Date 2000-08-14.03:13:00.000>
  232         >>> Date("11-07.09:32:43-0500")
  233         <Date 2000-11-07.14:32:43.000>
  234         >>> Date("14:25-0500")
  235         <Date 2000-06-26.19:25:00.000>
  236         >>> Date("8:47:11-0500")
  237         <Date 2000-06-26.13:47:11.000>
  238         >>> Date("2003 -0500")
  239         <Date 2003-01-01.05:00:00.000>
  240         >>> Date("2003-06 -0500")
  241         <Date 2003-06-01.05:00:00.000>
  242 
  243         "." means "right now":
  244         >>> Date(".")
  245         <Date 2000-06-26.00:34:02.000>
  246 
  247         >>> test_fin(u)
  248 
  249     The Date class should understand simple date expressions of the form
  250     stamp + interval and stamp - interval. When adding or subtracting
  251     intervals involving months or years, the components are handled
  252     separately. For example, when evaluating "2000-06-25 + 1m 10d", we
  253     first add one month to get 2000-07-25, then add 10 days to get
  254     2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40
  255     or 41 days).  Example usage::
  256 
  257         make doctest think it's always 2000-06-26.00:34:02:
  258         >>> u = test_ini('2000-06-26.00:34:02.0')
  259 
  260         >>> Date(".")
  261         <Date 2000-06-26.00:34:02.000>
  262         >>> _.local(-5)
  263         <Date 2000-06-25.19:34:02.000>
  264         >>> Date(". + 2d")
  265         <Date 2000-06-28.00:34:02.000>
  266         >>> Date("1997-04-17", -5)
  267         <Date 1997-04-17.05:00:00.000>
  268         >>> Date("01-25", -5)
  269         <Date 2000-01-25.05:00:00.000>
  270         >>> Date("08-13.22:13", -5)
  271         <Date 2000-08-14.03:13:00.000>
  272         >>> Date("14:25", -5)
  273         <Date 2000-06-26.19:25:00.000>
  274 
  275     The date format 'yyyymmddHHMMSS' (year, month, day, hour,
  276     minute, second) is the serialisation format returned by the serialise()
  277     method, and is accepted as an argument on instatiation.
  278 
  279     In addition, a timezone specifier can be appended to the date format.
  280     The timezone specifier is a sign ("+" or "-") followed by a 4-digit
  281     number as in the RFC 2822 date format.
  282     The first two digits indicate the number of hours, while the last two
  283     digits indicate the number of minutes the time is offset from
  284     Coordinated Universal Time (UTC). The "+" or "-" sign indicate whether
  285     the time is ahead of (east of) or behind (west of) UTC. Note that a
  286     given timezone specifier *overrides* an offset given to the Date
  287     constructor.  Examples::
  288 
  289         >>> Date ("2000-08-14+0200")
  290         <Date 2000-08-13.22:00:00.000>
  291         >>> Date ("08-15.22:00+0200")
  292         <Date 2000-08-15.20:00:00.000>
  293         >>> Date ("08-15.22:47+0200")
  294         <Date 2000-08-15.20:47:00.000>
  295         >>> Date ("08-15.22:47+0200", offset = 5)
  296         <Date 2000-08-15.20:47:00.000>
  297         >>> Date ("08-15.22:47", offset = 5)
  298         <Date 2000-08-15.17:47:00.000>
  299 
  300     The date class handles basic arithmetic, but note that arithmetic
  301     cannot be combined with timezone offsets (see last example)::
  302 
  303         >>> x=test_ini('2004-04-06.22:04:20.766830')
  304         >>> d1=Date('.')
  305         >>> d1
  306         <Date 2004-04-06.22:04:20.767>
  307         >>> d2=Date('2003-07-01')
  308         >>> d2
  309         <Date 2003-07-01.00:00:00.000>
  310         >>> d1-d2
  311         <Interval + 280d 22:04:20>
  312         >>> i1=_
  313         >>> d2+i1
  314         <Date 2004-04-06.22:04:20.000>
  315         >>> d1-i1
  316         <Date 2003-07-01.00:00:00.000>
  317 
  318         >>> test_fin(u)
  319     '''
  320 
  321     def __init__(self, spec='.', offset=0, add_granularity=False,
  322                  translator=i18n):
  323         """Construct a date given a specification and a time zone offset.
  324 
  325         'spec'
  326            is a full date or a partial form, with an optional added or
  327            subtracted interval. Or a date 9-tuple.
  328         'offset'
  329            is the local time zone offset from GMT in hours.
  330         'translator'
  331            is i18n module or one of gettext translation classes.
  332            It must have attributes 'gettext' and 'ngettext',
  333            serving as translation functions.
  334         """
  335         self.setTranslator(translator)
  336         # Python 2.3+ datetime object
  337         # common case when reading from database: avoid double-conversion
  338         if isinstance(spec, datetime.datetime):
  339             if offset == 0:
  340                 self.year, self.month, self.day, self.hour, self.minute, \
  341                     self.second = spec.timetuple()[:6]
  342             else:
  343                 TZ = get_timezone(tz)
  344                 self.year, self.month, self.day, self.hour, self.minute, \
  345                     self.second = TZ.localize(spec).utctimetuple()[:6]
  346             self.second += spec.microsecond/1000000.
  347             return
  348 
  349         if isinstance(spec, type('')):
  350             self.set(spec, offset=offset, add_granularity=add_granularity)
  351             return
  352         elif hasattr(spec, 'tuple'):
  353             spec = spec.tuple()
  354         elif isinstance(spec, Date):
  355             spec = spec.get_tuple()
  356         try:
  357             y,m,d,H,M,S,x,x,x = spec
  358             S = min(S, 59.999)
  359             frac = S - int(S)
  360             self.year, self.month, self.day, self.hour, self.minute, \
  361                 self.second = _local_to_utc(y, m, d, H, M, S, offset)
  362             # we lost the fractional part
  363             self.second = self.second + frac
  364             # making sure we match the precision of serialise()
  365             self.second = min(self.second, 59.999)
  366         except Exception:
  367             raise ValueError('Unknown spec %r' % (spec,))
  368 
  369     def now(self):
  370         """ To be able to override for testing
  371         """
  372         return datetime.datetime.utcnow()
  373 
  374     def set(self, spec, offset=0, date_re=date_re,
  375             serialised_re=serialised_date_re, add_granularity=False):
  376         ''' set the date to the value in spec
  377         '''
  378 
  379         m = serialised_re.match(spec)
  380         if m is not None:
  381             # we're serialised - easy!
  382             g = m.groups()
  383             (self.year, self.month, self.day, self.hour, self.minute) = \
  384                 map(int, g[:5])
  385             self.second = float(g[5])
  386             return
  387 
  388         # not serialised data, try usual format
  389         m = date_re.match(spec)
  390         if m is None:
  391             raise ValueError(self._('Not a date spec: %r '
  392                 '("yyyy-mm-dd", "mm-dd", "HH:MM", "HH:MM:SS" or '
  393                 '"yyyy-mm-dd.HH:MM:SS.SSS")' % spec))
  394 
  395         info = m.groupdict()
  396 
  397         # If add_granularity is true, construct the maximum time given
  398         # the precision of the input.  For example, given the input
  399         # "12:15", construct "12:15:59".  Or, for "2008", construct
  400         # "2008-12-31.23:59:59".
  401         if add_granularity:
  402             for gran in 'SMHdmy':
  403                 if info[gran] is not None:
  404                     if gran == 'S':
  405                         raise ValueError
  406                     elif gran == 'M':
  407                         add_granularity = Interval('00:01')
  408                     elif gran == 'H':
  409                         add_granularity = Interval('01:00')
  410                     else:
  411                         add_granularity = Interval('+1%s' % gran)
  412                     break
  413             else:
  414                 raise ValueError(self._('Could not determine granularity'))
  415 
  416         # get the current date as our default
  417         dt = self.now()
  418         y,m,d,H,M,S,x,x,x = dt.timetuple()
  419         S += dt.microsecond/1000000.
  420 
  421         # whether we need to convert to UTC
  422         adjust = False
  423 
  424         if info['y'] is not None or info['a'] is not None:
  425             if info['y'] is not None:
  426                 y = int(info['y'])
  427                 m,d = (1,1)
  428                 if info['m'] is not None:
  429                     m = int(info['m'])
  430                     if info['d'] is not None:
  431                         d = int(info['d'])
  432             if info['a'] is not None:
  433                 m = int(info['a'])
  434                 d = int(info['b'])
  435             H = 0
  436             M = S = 0
  437             adjust = True
  438 
  439         # override hour, minute, second parts
  440         if info['H'] is not None and info['M'] is not None:
  441             H = int(info['H'])
  442             M = int(info['M'])
  443             S = 0
  444             if info['S'] is not None:
  445                 S = float(info['S'])
  446             adjust = True
  447 
  448         if info.get('tz', None):
  449             offset = 0
  450 
  451         # now handle the adjustment of hour
  452         frac = S - int(S)
  453         dt = datetime.datetime(y,m,d,H,M,int(S), int(frac * 1000000.))
  454         y, m, d, H, M, S, x, x, x = dt.timetuple()
  455         if adjust:
  456             y, m, d, H, M, S = _local_to_utc(y, m, d, H, M, S, offset)
  457         self.year, self.month, self.day, self.hour, self.minute, \
  458             self.second = y, m, d, H, M, S
  459         # we lost the fractional part along the way
  460         self.second += dt.microsecond/1000000.
  461 
  462         if info.get('o', None):
  463             try:
  464                 self.applyInterval(Interval(info['o'], allowdate=0))
  465             except ValueError:
  466                 raise ValueError(self._('%r not a date / time spec '
  467                     '"yyyy-mm-dd", "mm-dd", "HH:MM", "HH:MM:SS" or '
  468                     '"yyyy-mm-dd.HH:MM:SS.SSS"') % (spec,))
  469 
  470         if info.get('tz', None):
  471             tz = info['tz'].strip()
  472             sign = [-1, 1][tz[0] == '-']
  473             minute = int(tz[3:], 10)
  474             hour = int(tz[1:3], 10)
  475             self.applyInterval(Interval((0, 0, 0, hour, minute, 0), sign=sign))
  476 
  477         # adjust by added granularity
  478         if add_granularity:
  479             self.applyInterval(add_granularity)
  480             self.applyInterval(Interval('- 00:00:01'))
  481 
  482     def addInterval(self, interval):
  483         ''' Add the interval to this date, returning the date tuple
  484         '''
  485         # do the basic calc
  486         sign = interval.sign
  487         year = self.year + sign * interval.year
  488         month = self.month + sign * interval.month
  489         day = self.day + sign * interval.day
  490         hour = self.hour + sign * interval.hour
  491         minute = self.minute + sign * interval.minute
  492         # Intervals work on whole seconds
  493         second = int(self.second) + sign * interval.second
  494 
  495         # now cope with under- and over-flow
  496         # first do the time
  497         while (second < 0 or second > 59 or minute < 0 or minute > 59 or
  498                 hour < 0 or hour > 23):
  499             if second < 0: minute -= 1; second += 60
  500             elif second > 59: minute += 1; second -= 60
  501             if minute < 0: hour -= 1; minute += 60
  502             elif minute > 59: hour += 1; minute -= 60
  503             if hour < 0: day -= 1; hour += 24
  504             elif hour > 23: day += 1; hour -= 24
  505 
  506         # fix up the month so we're within range
  507         while month < 1 or month > 12:
  508             if month < 1: year -= 1; month += 12
  509             if month > 12: year += 1; month -= 12
  510 
  511         # now do the days, now that we know what month we're in
  512         def get_mdays(year, month):
  513             if month == 2 and calendar.isleap(year): return 29
  514             else: return calendar.mdays[month]
  515 
  516         while month < 1 or month > 12 or day < 1 or \
  517               day > get_mdays(year, month):
  518             # now to day under/over
  519             if day < 1:
  520                 # When going backwards, decrement month, then increment days
  521                 month -= 1
  522                 day += get_mdays(year, month)
  523             elif day > get_mdays(year, month):
  524                 # When going forwards, decrement days, then increment month
  525                 day -= get_mdays(year, month)
  526                 month += 1
  527 
  528             # possibly fix up the month so we're within range
  529             while month < 1 or month > 12:
  530                 if month < 1: year -= 1; month += 12; day += 31
  531                 if month > 12: year += 1; month -= 12
  532 
  533         return (year, month, day, hour, minute, second, 0, 0, 0)
  534 
  535     def differenceDate(self, other):
  536         "Return the difference between this date and another date"
  537         return self - other
  538 
  539     def applyInterval(self, interval):
  540         ''' Apply the interval to this date
  541         '''
  542         self.year, self.month, self.day, self.hour, self.minute, \
  543             self.second, x, x, x = self.addInterval(interval)
  544 
  545     def __add__(self, interval):
  546         """Add an interval to this date to produce another date.
  547         """
  548         return Date(self.addInterval(interval), translator=self.translator)
  549 
  550     # deviates from spec to allow subtraction of dates as well
  551     def __sub__(self, other):
  552         """ Subtract:
  553              1. an interval from this date to produce another date.
  554              2. a date from this date to produce an interval.
  555         """
  556         if isinstance(other, Interval):
  557             other = Interval(other.get_tuple())
  558             other.sign *= -1
  559             return self.__add__(other)
  560 
  561         assert isinstance(other, Date), 'May only subtract Dates or Intervals'
  562 
  563         return self.dateDelta(other)
  564 
  565     def dateDelta(self, other):
  566         """ Produce an Interval of the difference between this date
  567             and another date. Only returns days:hours:minutes:seconds.
  568         """
  569         # Returning intervals larger than a day is almost
  570         # impossible - months, years, weeks, are all so imprecise.
  571         a = calendar.timegm((self.year, self.month, self.day, self.hour,
  572                              self.minute, self.second, 0, 0, 0))
  573         b = calendar.timegm((other.year, other.month, other.day,
  574                              other.hour, other.minute, other.second, 0, 0, 0))
  575         # intervals work in whole seconds
  576         diff = int(a - b)
  577         if diff > 0:
  578             sign = 1
  579         else:
  580             sign = -1
  581             diff = -diff
  582         S = diff % 60
  583         M = (diff//60) % 60
  584         H = (diff//(60*60)) % 24
  585         d = diff//(24*60*60)
  586         return Interval((0, 0, d, H, M, S), sign=sign,
  587                         translator=self.translator)
  588 
  589     def __cmp__(self, other, int_seconds=0):
  590         """Compare this date to another date."""
  591         if other is None:
  592             return 1
  593         for attr in ('year', 'month', 'day', 'hour', 'minute'):
  594             if not hasattr(other, attr):
  595                 return 1
  596             r = cmp(getattr(self, attr), getattr(other, attr))
  597             if r: return r
  598         if not hasattr(other, 'second'):
  599             return 1
  600         if int_seconds:
  601             return cmp(int(self.second), int(other.second))
  602         return cmp(self.second, other.second)
  603 
  604     def __lt__(self, other):
  605         return self.__cmp__(other) < 0
  606 
  607     def __le__(self, other):
  608         return self.__cmp__(other) <= 0
  609 
  610     def __eq__(self, other):
  611         return self.__cmp__(other) == 0
  612 
  613     def __ne__(self, other):
  614         return self.__cmp__(other) != 0
  615 
  616     def __gt__(self, other):
  617         return self.__cmp__(other) > 0
  618 
  619     def __ge__(self, other):
  620         return self.__cmp__(other) >= 0
  621 
  622     def __str__(self):
  623         """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
  624         return self.formal()
  625 
  626     def formal(self, sep='.', sec='%02d'):
  627         f = '%%04d-%%02d-%%02d%s%%02d:%%02d:%s' % (sep, sec)
  628         return f % (self.year, self.month, self.day, self.hour, self.minute,
  629                     self.second)
  630 
  631     def isoformat(self):
  632         ''' Represent the date/time in isoformat standard
  633 
  634             Originally needed for xml output support using
  635             dicttoxml in the rest interface.
  636         '''
  637         f = '%%04d-%%02d-%%02d%s%%02d:%%02d:%s' % ("T", "%02.6d")
  638         return f % (self.year, self.month, self.day, self.hour, self.minute,
  639                     self.second)
  640 
  641     def pretty(self, format='%d %B %Y'):
  642         ''' print up the date date using a pretty format...
  643 
  644             Note that if the day is zero, and the day appears first in the
  645             format, then the day number will be removed from output.
  646         '''
  647         dt = datetime.datetime(self.year, self.month, self.day, self.hour,
  648             self.minute, int(self.second),
  649             int((self.second - int(self.second)) * 1000000.))
  650         str = dt.strftime(format)
  651 
  652         # handle zero day by removing it
  653         if format.startswith('%d') and str[0] == '0':
  654             return ' ' + str[1:]
  655         return str
  656 
  657     def __repr__(self):
  658         return '<Date %s>' % self.formal(sec='%06.3f')
  659 
  660     def local(self, offset):
  661         """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
  662             The offset is a pytz tz offset if pytz is installed.
  663         """
  664         y,m,d,H,M,S = _utc_to_local(self.year, self.month, self.day,
  665                    self.hour, self.minute, self.second, offset)
  666         return Date((y,m,d,H,M,S,0,0,0), translator=self.translator)
  667 
  668     def __deepcopy__(self, memo):
  669         return Date((self.year, self.month, self.day, self.hour,
  670             self.minute, self.second, 0, 0, 0), translator=self.translator)
  671 
  672     def get_tuple(self):
  673         return (self.year, self.month, self.day, self.hour, self.minute,
  674                 self.second, 0, 0, 0)
  675 
  676     def serialise(self):
  677         """ Return serialised string for self's datetime.
  678 
  679         Uses '%06.3f' as format for self.second, which therefor
  680         must be <=59.999 to work. Otherwise it will be rounded
  681         to 60.000.
  682 
  683         """
  684         return '%04d%02d%02d%02d%02d%06.3f' % (self.year, self.month,
  685             self.day, self.hour, self.minute, self.second)
  686 
  687     def timestamp(self):
  688         ''' return a UNIX timestamp for this date '''
  689         frac = self.second - int(self.second)
  690         ts = calendar.timegm((self.year, self.month, self.day, self.hour,
  691                               self.minute, self.second, 0, 0, 0))
  692         # we lose the fractional part
  693         return ts + frac
  694 
  695     def setTranslator(self, translator):
  696         """Replace the translation engine
  697 
  698         'translator'
  699            is i18n module or one of gettext translation classes.
  700            It must have attributes 'gettext' and 'ngettext',
  701            serving as translation functions.
  702         """
  703         self.translator = translator
  704         self._ = translator.gettext
  705         self.ngettext = translator.ngettext
  706 
  707     def fromtimestamp(cls, ts):
  708         """Create a date object from a timestamp.
  709 
  710         The timestamp may be outside the gmtime year-range of
  711         1902-2038.
  712         """
  713         usec = int((ts - int(ts)) * 1000000.)
  714         delta = datetime.timedelta(seconds=int(ts), microseconds=usec)
  715         return cls(datetime.datetime(1970, 1, 1) + delta)
  716     fromtimestamp = classmethod(fromtimestamp)
  717 
  718 
  719 class Interval:
  720     '''
  721     Date intervals are specified using the suffixes "y", "m", and "d". The
  722     suffix "w" (for "week") means 7 days. Time intervals are specified in
  723     hh:mm:ss format (the seconds may be omitted, but the hours and minutes
  724     may not).
  725 
  726       "3y" means three years
  727       "2y 1m" means two years and one month
  728       "1m 25d" means one month and 25 days
  729       "2w 3d" means two weeks and three days
  730       "1d 2:50" means one day, two hours, and 50 minutes
  731       "14:00" means 14 hours
  732       "0:04:33" means four minutes and 33 seconds
  733 
  734     Example usage:
  735         make doctest think it's always 2000-06-26.00:34:02:
  736         >>> u = test_ini('2000-06-26.00:34:02.0')
  737 
  738         >>> Interval("  3w  1  d  2:00")
  739         <Interval + 22d 2:00>
  740         >>> Date(". + 2d") + Interval("- 3w")
  741         <Date 2000-06-07.00:34:02.000>
  742         >>> Interval('1:59:59') + Interval('00:00:01')
  743         <Interval + 2:00>
  744         >>> Interval('2:00') + Interval('- 00:00:01')
  745         <Interval + 1:59:59>
  746         >>> Interval('1y')/2
  747         <Interval + 6m>
  748         >>> Interval('1:00')/2
  749         <Interval + 0:30>
  750 
  751         [number of days between 2000-06-26.00:34:02 and 2003-03-18
  752         >>> Interval('2003-03-18')
  753         <Interval - 995d>
  754 
  755         [number of days between 2000-06-26.00:34:02 and 2003-03-14
  756         >>> Interval('-4d 2003-03-18')
  757         <Interval - 991d>
  758 
  759         >>> test_fin(u)
  760 
  761     Interval arithmetic is handled in a couple of special ways, trying
  762     to cater for the most common cases. Fundamentally, Intervals which
  763     have both date and time parts will result in strange results in
  764     arithmetic - because of the impossibility of handling day->month->year
  765     over- and under-flows. Intervals may also be divided by some number.
  766 
  767     Intervals are added to Dates in order of:
  768        seconds, minutes, hours, years, months, days
  769 
  770     Calculations involving months (eg '+2m') have no effect on days - only
  771     days (or over/underflow from hours/mins/secs) will do that, and
  772     days-per-month and leap years are accounted for. Leap seconds are not.
  773 
  774     The interval format 'syyyymmddHHMMSS' (sign, year, month, day, hour,
  775     minute, second) is the serialisation format returned by the serialise()
  776     method, and is accepted as an argument on instatiation.
  777 
  778     TODO: more examples, showing the order of addition operation
  779     '''
  780     def __init__(self, spec, sign=1, allowdate=1, add_granularity=False,
  781                  translator=i18n):
  782         """Construct an interval given a specification."""
  783         self.setTranslator(translator)
  784         try:
  785             # Python 2.
  786             arith_types = (int, float, long)
  787         except NameError:
  788             # Python 3.
  789             arith_types = (int, float)
  790         if isinstance(spec, arith_types):
  791             self.from_seconds(spec)
  792         elif is_us(spec):
  793             self.set(spec, allowdate=allowdate,
  794                      add_granularity=add_granularity)
  795         elif isinstance(spec, Interval):
  796             (self.sign, self.year, self.month, self.day, self.hour,
  797                 self.minute, self.second) = spec.get_tuple()
  798         else:
  799             if len(spec) == 7:
  800                 self.sign, self.year, self.month, self.day, self.hour, \
  801                     self.minute, self.second = spec
  802                 self.second = int(self.second)
  803             else:
  804                 # old, buggy spec form
  805                 self.sign = sign
  806                 self.year, self.month, self.day, self.hour, self.minute, \
  807                     self.second = spec
  808                 self.second = int(self.second)
  809 
  810     def __deepcopy__(self, memo):
  811         return Interval((self.sign, self.year, self.month, self.day,
  812             self.hour, self.minute, self.second), translator=self.translator)
  813 
  814     def set(self, spec, allowdate=1, interval_re=re.compile(r'''
  815             \s*(?P<s>[-+])?         # + or -
  816             \s*((?P<y>\d+\s*)y)?    # year
  817             \s*((?P<m>\d+\s*)m)?    # month
  818             \s*((?P<w>\d+\s*)w)?    # week
  819             \s*((?P<d>\d+\s*)d)?    # day
  820             \s*(((?P<H>\d+):(?P<M>\d+))?(:(?P<S>\d+))?)?   # time
  821             \s*(?P<D>
  822                  (\d\d\d\d[/-])?(\d\d?)?[/-](\d\d?)?       # [yyyy-]mm-dd
  823                  \.?                                       # .
  824                  (\d?\d:\d\d)?(:\d\d)?                     # hh:mm:ss
  825                )?''', re.VERBOSE), serialised_re=re.compile(r'''
  826             (?P<s>[+-])?1?(?P<y>([ ]{3}\d|\d{4}))(?P<m>\d{2})(?P<d>\d{2})
  827             (?P<H>\d{2})(?P<M>\d{2})(?P<S>\d{2})''', re.VERBOSE),
  828             add_granularity=False):
  829         ''' set the date to the value in spec
  830         '''
  831         self.year = self.month = self.week = self.day = self.hour = \
  832             self.minute = self.second = 0
  833         self.sign = 1
  834         m = serialised_re.match(spec)
  835         if not m:
  836             m = interval_re.match(spec)
  837             if not m:
  838                 raise ValueError(self._('Not an interval spec: "%s"'
  839                     ' ([+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date spec])'
  840                     % spec))
  841         else:
  842             allowdate = 0
  843 
  844         # pull out all the info specified
  845         info = m.groupdict()
  846         if add_granularity:
  847             for gran in 'SMHdwmy':
  848                 if info[gran] is not None:
  849                     info[gran] = int(info[gran]) + (info['s'] == '-' and -1 or 1)
  850                     break
  851 
  852         valid = 0
  853         for group, attr in {'y': 'year', 'm': 'month', 'w': 'week', 'd': 'day',
  854                 'H': 'hour', 'M': 'minute', 'S': 'second'}.items():
  855             if info.get(group, None) is not None:
  856                 valid = 1
  857                 setattr(self, attr, int(info[group]))
  858 
  859         # make sure it's valid
  860         if not valid and not info['D']:
  861             raise ValueError(self._('Not an interval spec: "%s"'
  862                 ' ([+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS])'
  863                 % spec))
  864 
  865         if self.week:
  866             self.day = self.day + self.week*7
  867 
  868         if info['s'] is not None:
  869             self.sign = {'+': 1, '-': -1}[info['s']]
  870 
  871         # use a date spec if one is given
  872         if allowdate and info['D'] is not None:
  873             now = Date('.')
  874             date = Date(info['D'])
  875             # if no time part was specified, nuke it in the "now" date
  876             if not date.hour or date.minute or date.second:
  877                 now.hour = now.minute = now.second = 0
  878             if date != now:
  879                 y = now - (date + self)
  880                 self.__init__(y.get_tuple())
  881 
  882     def __lt__(self, other):
  883         """Compare this interval to another interval."""
  884 
  885         if other is None:
  886             # we are always larger than None
  887             return False
  888         return self.as_seconds() < other.as_seconds()
  889 
  890     def __le__(self, other):
  891         """Compare this interval to another interval."""
  892 
  893         if other is None:
  894             # we are always larger than None
  895             return False
  896         return self.as_seconds() <= other.as_seconds()
  897 
  898     def __eq__(self, other):
  899         """Compare this interval to another interval."""
  900 
  901         if other is None:
  902             # we are always larger than None
  903             return False
  904         return self.as_seconds() == other.as_seconds()
  905 
  906     def __ne__(self, other):
  907         """Compare this interval to another interval."""
  908 
  909         if other is None:
  910             # we are always larger than None
  911             return True
  912         return self.as_seconds() != other.as_seconds()
  913 
  914     def __gt__(self, other):
  915         """Compare this interval to another interval."""
  916 
  917         if other is None:
  918             # we are always larger than None
  919             return True
  920         return self.as_seconds() > other.as_seconds()
  921 
  922     def __ge__(self, other):
  923         """Compare this interval to another interval."""
  924 
  925         if other is None:
  926             # we are always larger than None
  927             return True
  928         return self.as_seconds() >= other.as_seconds()
  929 
  930     def __str__(self):
  931         """Return this interval as a string."""
  932         l = []
  933         if self.year: l.append('%sy' % self.year)
  934         if self.month: l.append('%sm' % self.month)
  935         if self.day: l.append('%sd' % self.day)
  936         if self.second:
  937             l.append('%d:%02d:%02d' % (self.hour, self.minute, self.second))
  938         elif self.hour or self.minute:
  939             l.append('%d:%02d' % (self.hour, self.minute))
  940         if l:
  941             l.insert(0, {1: '+', -1: '-'}[self.sign])
  942         else:
  943             l.append('00:00')
  944         return ' '.join(l)
  945 
  946     def __add__(self, other):
  947         if isinstance(other, Date):
  948             # the other is a Date - produce a Date
  949             return Date(other.addInterval(self), translator=self.translator)
  950         elif isinstance(other, Interval):
  951             # add the other Interval to this one
  952             a = self.get_tuple()
  953             asgn = a[0]
  954             b = other.get_tuple()
  955             bsgn = b[0]
  956             i = [asgn*x + bsgn*y for x, y in zip(a[1:], b[1:])]
  957             i.insert(0, 1)
  958             i = fixTimeOverflow(i)
  959             return Interval(i, translator=self.translator)
  960         # nope, no idea what to do with this other...
  961         raise TypeError("Can't add %r" % other)
  962 
  963     def __sub__(self, other):
  964         if isinstance(other, Date):
  965             # the other is a Date - produce a Date
  966             interval = Interval(self.get_tuple())
  967             interval.sign *= -1
  968             return Date(other.addInterval(interval),
  969                         translator=self.translator)
  970         elif isinstance(other, Interval):
  971             # add the other Interval to this one
  972             a = self.get_tuple()
  973             asgn = a[0]
  974             b = other.get_tuple()
  975             bsgn = b[0]
  976             i = [asgn*x - bsgn*y for x, y in zip(a[1:], b[1:])]
  977             i.insert(0, 1)
  978             i = fixTimeOverflow(i)
  979             return Interval(i, translator=self.translator)
  980         # nope, no idea what to do with this other...
  981         raise TypeError("Can't add %r" % other)
  982 
  983     def __truediv__(self, other):
  984         """ Divide this interval by an int value.
  985 
  986             Can't divide years and months sensibly in the _same_
  987             calculation as days/time, so raise an error in that situation.
  988         """
  989         try:
  990             other = float(other)
  991         except TypeError:
  992             raise ValueError("Can only divide Intervals by numbers")
  993 
  994         y, m, d, H, M, S = (self.year, self.month, self.day,
  995                             self.hour, self.minute, self.second)
  996         if y or m:
  997             if d or H or M or S:
  998                 raise ValueError("Can't divide Interval with date and time")
  999             months = self.year*12 + self.month
 1000             months *= self.sign
 1001 
 1002             months = int(months/other)
 1003 
 1004             sign = months < 0 and -1 or 1
 1005             m = months % 12
 1006             y = months // 12
 1007             return Interval((sign, y, m, 0, 0, 0, 0),
 1008                             translator=self.translator)
 1009 
 1010         else:
 1011             # handle a day/time division
 1012             seconds = S + M*60 + H*60*60 + d*60*60*24
 1013             seconds *= self.sign
 1014 
 1015             seconds = int(seconds/other)
 1016 
 1017             sign = seconds < 0 and -1 or 1
 1018             seconds *= sign
 1019             S = seconds % 60
 1020             seconds //= 60
 1021             M = seconds % 60
 1022             seconds //= 60
 1023             H = seconds % 24
 1024             d = seconds // 24
 1025             return Interval((sign, 0, 0, d, H, M, S),
 1026                             translator=self.translator)
 1027     # Python 2 compatibility:
 1028     __div__ = __truediv__
 1029 
 1030     def __repr__(self):
 1031         return '<Interval %s>' % self.__str__()
 1032 
 1033     def pretty(self):
 1034         ''' print up the date date using one of these nice formats..
 1035         '''
 1036         _quarters = self.minute // 15
 1037         if self.year:
 1038             s = self.ngettext("%(number)s year", "%(number)s years",
 1039                               self.year) % {'number': self.year}
 1040         elif self.month or self.day > 28:
 1041             _months = max(1, int(((self.month * 30) + self.day) / 30))
 1042             s = self.ngettext("%(number)s month", "%(number)s months",
 1043                               _months) % {'number': _months}
 1044         elif self.day > 7:
 1045             _weeks = int(self.day / 7)
 1046             s = self.ngettext("%(number)s week", "%(number)s weeks",
 1047                               _weeks) % {'number': _weeks}
 1048         elif self.day > 1:
 1049             # Note: singular form is not used
 1050             s = self.ngettext('%(number)s day', '%(number)s days',
 1051                               self.day) % {'number': self.day}
 1052         elif self.day == 1 or self.hour > 12:
 1053             if self.sign > 0:
 1054                 return self._('tomorrow')
 1055             else:
 1056                 return self._('yesterday')
 1057         elif self.hour > 1:
 1058             # Note: singular form is not used
 1059             s = self.ngettext('%(number)s hour', '%(number)s hours',
 1060                               self.hour) % {'number': self.hour}
 1061         elif self.hour == 1:
 1062             if self.minute < 15:
 1063                 s = self._('an hour')
 1064             elif _quarters == 2:
 1065                 s = self._('1 1/2 hours')
 1066             else:
 1067                 s = self.ngettext('1 %(number)s/4 hours',
 1068                                   '1 %(number)s/4 hours',
 1069                                   _quarters) % {'number': _quarters}
 1070         elif self.minute < 1:
 1071             if self.sign > 0:
 1072                 return self._('in a moment')
 1073             else:
 1074                 return self._('just now')
 1075         elif self.minute == 1:
 1076             # Note: used in expressions "in 1 minute" or "1 minute ago"
 1077             s = self._('1 minute')
 1078         elif self.minute < 15:
 1079             # Note: used in expressions "in 2 minutes" or "2 minutes ago"
 1080             s = self.ngettext('%(number)s minute', '%(number)s minutes',
 1081                               self.minute) % {'number': self.minute}
 1082         elif _quarters == 2:
 1083             s = self._('1/2 an hour')
 1084         else:
 1085             s = self.ngettext('%(number)s/4 hour', '%(number)s/4 hours',
 1086                               _quarters) % {'number': _quarters}
 1087         # XXX this is internationally broken
 1088         if self.sign < 0:
 1089             s = self._('%s ago') % s
 1090         else:
 1091             s = self._('in %s') % s
 1092         return s
 1093 
 1094     def get_tuple(self):
 1095         return (self.sign, self.year, self.month, self.day, self.hour,
 1096                 self.minute, self.second)
 1097 
 1098     def serialise(self):
 1099         sign = self.sign > 0 and '+' or '-'
 1100         return '%s%04d%02d%02d%02d%02d%02d' % (sign, self.year, self.month,
 1101             self.day, self.hour, self.minute, self.second)
 1102 
 1103     def isoformat(self):
 1104         '''Represent interval as an ISO 8061 duration (absolute value)
 1105 
 1106            Originally needed for xml output support using
 1107            dicttoxml in the rest interface.
 1108         '''
 1109         return 'P%04dY%02dM%02dDT%02dH%02dM%02dS' % (self.year, self.month,
 1110             self.day, self.hour, self.minute, self.second)
 1111 
 1112     def as_seconds(self):
 1113         '''Calculate the Interval as a number of seconds.
 1114 
 1115         Months are counted as 30 days, years as 365 days. Returns a Long
 1116         int.
 1117         '''
 1118         n = self.year * 365
 1119         n = n + self.month * 30
 1120         n = n + self.day
 1121         n = n * 24
 1122         n = n + self.hour
 1123         n = n * 60
 1124         n = n + self.minute
 1125         n = n * 60
 1126         n = n + self.second
 1127         return n * self.sign
 1128 
 1129     def from_seconds(self, val):
 1130         '''Figure my second, minute, hour and day values using a seconds
 1131         value.
 1132         '''
 1133         val = int(val)
 1134         if val < 0:
 1135             self.sign = -1
 1136             val = -val
 1137         else:
 1138             self.sign = 1
 1139         self.second = val % 60
 1140         val = val // 60
 1141         self.minute = val % 60
 1142         val = val // 60
 1143         self.hour = val % 24
 1144         val = val // 24
 1145         self.day = val
 1146         self.month = self.year = 0
 1147 
 1148     def setTranslator(self, translator):
 1149         """Replace the translation engine
 1150 
 1151         'translator'
 1152            is i18n module or one of gettext translation classes.
 1153            It must have attributes 'gettext' and 'ngettext',
 1154            serving as translation functions.
 1155         """
 1156         self.translator = translator
 1157         self._ = translator.gettext
 1158         self.ngettext = translator.ngettext
 1159 
 1160 
 1161 def fixTimeOverflow(time):
 1162     """ Handle the overflow in the time portion (H, M, S) of "time":
 1163             (sign, y,m,d,H,M,S)
 1164 
 1165         Overflow and underflow will at most affect the _days_ portion of
 1166         the date. We do not overflow days to months as we don't know _how_
 1167         to, generally.
 1168     """
 1169     # XXX we could conceivably use this function for handling regular dates
 1170     # XXX too - we just need to interrogate the month/year for the day
 1171     # XXX overflow...
 1172 
 1173     sign, y, m, d, H, M, S = time
 1174     seconds = sign * (S + M*60 + H*60*60 + d*60*60*24)
 1175     if seconds:
 1176         sign = seconds < 0 and -1 or 1
 1177         seconds *= sign
 1178         S = seconds % 60
 1179         seconds //= 60
 1180         M = seconds % 60
 1181         seconds //= 60
 1182         H = seconds % 24
 1183         d = seconds // 24
 1184     else:
 1185         months = y*12 + m
 1186         sign = months < 0 and -1 or 1
 1187         months *= sign
 1188         m = months % 12
 1189         y = months//12
 1190 
 1191     return (sign, y, m, d, H, M, S)
 1192 
 1193 
 1194 class Range:
 1195     """Represents range between two values
 1196     Ranges can be created using one of theese two alternative syntaxes:
 1197 
 1198     1. Native english syntax::
 1199 
 1200             [[From] <value>][ To <value>]
 1201 
 1202        Keywords "From" and "To" are case insensitive. Keyword "From" is
 1203        optional.
 1204 
 1205     2. "Geek" syntax::
 1206 
 1207           [<value>][; <value>]
 1208 
 1209     Either first or second <value> can be omitted in both syntaxes.
 1210 
 1211     Examples (consider local time is Sat Mar  8 22:07:48 EET 2003)::
 1212 
 1213         make doctest think it's always 2000-06-26.00:34:02:
 1214         >>> u = test_ini('2003-03-08.20:07:48.0')
 1215 
 1216         >>> Range("from 2-12 to 4-2", Date)
 1217         <Range from 2003-02-12.00:00:00 to 2003-04-02.00:00:00>
 1218 
 1219         >>> Range("18:00 to +2m", Date)
 1220         <Range from 2003-03-08.18:00:00 to 2003-05-08.20:07:48>
 1221 
 1222         >>> Range("tO +3d", Date)
 1223         <Range from None to 2003-03-11.20:07:48>
 1224 
 1225         >>> Range("12:00 to", Date)
 1226         <Range from 2003-03-08.12:00:00 to None>
 1227 
 1228         >>> Range("12:00;", Date)
 1229         <Range from 2003-03-08.12:00:00 to None>
 1230 
 1231         >>> Range("2002-11-10; 2002-12-12", Date)
 1232         <Range from 2002-11-10.00:00:00 to 2002-12-12.00:00:00>
 1233 
 1234         >>> Range("; 20:00 +1d", Date)
 1235         <Range from None to 2003-03-09.20:00:00>
 1236 
 1237         >>> Range("from 2003-02-16", Date)
 1238         <Range from 2003-02-16.00:00:00 to None>
 1239 
 1240         >>> Range("2003-02-16;", Date)
 1241         <Range from 2003-02-16.00:00:00 to None>
 1242 
 1243         Granularity tests:
 1244 
 1245         >>> Range("12:00", Date)
 1246         <Range from 2003-03-08.12:00:00 to 2003-03-08.12:00:59>
 1247 
 1248         >>> Range("2003-03-08", Date)
 1249         <Range from 2003-03-08.00:00:00 to 2003-03-08.23:59:59>
 1250 
 1251         >>> test_fin(u)
 1252 
 1253         Range of Interval tests
 1254 
 1255         >>> Range ("from 0:50 to 2:00", Interval)
 1256         <Range from + 0:50 to + 2:00>
 1257         >>> Range ("from 0:50 to 1d 2:00", Interval)
 1258         <Range from + 0:50 to + 1d 2:00>
 1259         >>> Range ("from 5:50", Interval)
 1260         <Range from + 5:50 to None>
 1261         >>> Range ("to 0:05", Interval)
 1262         <Range from None to + 0:05>
 1263 
 1264     """
 1265     def __init__(self, spec, Type, allow_granularity=True, **params):
 1266         """Initializes Range of type <Type> from given <spec> string.
 1267 
 1268         Sets two properties - from_value and to_value. None assigned to any of
 1269         this properties means "infinitum" (-infinitum to from_value and
 1270         +infinitum to to_value)
 1271 
 1272         The Type parameter here should be class itself (e.g. Date), not a
 1273         class instance.
 1274         """
 1275         self.range_type = Type
 1276         re_range = r'^(?:from)?(.+?)?to(.+?)?$'
 1277         re_range_no_to = r'^from(.+)(.)?$'
 1278         re_geek_range = r'^(.+?)?;(.+?)?$'
 1279         # Check which syntax to use
 1280         if ';' in spec:
 1281             # Geek
 1282             m = re.search(re_geek_range, spec.strip())
 1283         else:
 1284             # Native english
 1285             m = re.search(re_range, spec.strip(), re.IGNORECASE)
 1286             if not m:
 1287                 m = re.search(re_range_no_to, spec.strip(), re.IGNORECASE)
 1288         if m:
 1289             self.from_value, self.to_value = m.groups()
 1290             if self.from_value:
 1291                 self.from_value = Type(self.from_value.strip(), **params)
 1292             if self.to_value:
 1293                 self.to_value = Type(self.to_value.strip(), **params)
 1294         else:
 1295             if allow_granularity:
 1296                 self.from_value = Type(spec, **params)
 1297                 self.to_value = Type(spec, add_granularity=True, **params)
 1298             else:
 1299                 raise ValueError("Invalid range")
 1300 
 1301     def __str__(self):
 1302         return "from %s to %s" % (self.from_value, self.to_value)
 1303 
 1304     def __repr__(self):
 1305         return "<Range %s>" % self.__str__()
 1306 
 1307 
 1308 def test_range():
 1309     rspecs = ("from 2-12 to 4-2", "from 18:00 TO +2m", "12:00;", "tO +3d",
 1310         "2002-11-10; 2002-12-12", "; 20:00 +1d", '2002-10-12')
 1311     rispecs = ('from -1w 2d 4:32 to 4d', '-2w 1d')
 1312     for rspec in rspecs:
 1313         print('>>> Range("%s")' % rspec)
 1314         print(repr(Range(rspec, Date)))
 1315         print()
 1316     for rspec in rispecs:
 1317         print('>>> Range("%s")' % rspec)
 1318         print(repr(Range(rspec, Interval)))
 1319         print()
 1320 
 1321 
 1322 def test():
 1323     intervals = ("  3w  1  d  2:00", " + 2d", "3w")
 1324     for interval in intervals:
 1325         print('>>> Interval("%s")' % interval)
 1326         print(repr(Interval(interval)))
 1327 
 1328     dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
 1329              "08-13.22:13", "14:25", '2002-12')
 1330     for date in dates:
 1331         print('>>> Date("%s")' % date)
 1332         print(repr(Date(date)))
 1333 
 1334     sums = ((". + 2d", "3w"), (".", "  3w  1  d  2:00"))
 1335     for date, interval in sums:
 1336         print('>>> Date("%s") + Interval("%s")' % (date, interval))
 1337         print(repr(Date(date) + Interval(interval)))
 1338 
 1339 
 1340 if __name__ == '__main__':
 1341     test()
 1342 
 1343 # vim: set filetype=python sts=4 sw=4 et si :