# -*- coding: utf-8 -*-
#
# Copyright (C) 2003-2009 Edgewall Software
# Copyright (C) 2003-2006 Jonas Borgström <jonas@edgewall.com>
# Copyright (C) 2006 Matthew Good <trac@matt-good.net>
# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://trac.edgewall.org/wiki/TracLicense.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://trac.edgewall.org/log/.
#
# Author: Jonas Borgström <jonas@edgewall.com>
# Matthew Good <trac@matt-good.net>
import sys
import time
import locale
import re
from datetime import date, datetime, timedelta, tzinfo
from tic.core import TicError
# Date/time utilities
# -- conversion
[docs]def to_datetime(t, tzinfo=None):
"""Convert `t` into a `datetime` object, using the following rules:
- If `t` is already a `datetime` object, it is simply returned.
- If `t` is None, the current time will be used.
- If `t` is a number, it is interpreted as a timestamp.
If no `tzinfo` is given, the local timezone will be used.
Any other input will trigger a `TypeError`.
"""
if t is None:
return datetime.now(tzinfo or localtz)
elif isinstance(t, datetime):
return t
elif isinstance(t, date):
return (tzinfo or localtz).localize(datetime(t.year, t.month, t.day))
elif isinstance(t, (int,long,float)):
return datetime.fromtimestamp(t, tzinfo or localtz)
raise TypeError('expecting datetime, int, long, float, or None; got %s' %
type(t))
[docs]def to_timestamp(dt):
"""Return the corresponding POSIX timestamp"""
if dt:
diff = dt - _epoc
return diff.days * 86400 + diff.seconds
else:
return 0
# -- formatting
[docs]def pretty_timedelta(time1, time2=None, resolution=None):
"""Calculate time delta between two `datetime` objects.
(the result is somewhat imprecise, only use for prettyprinting).
If either `time1` or `time2` is None, the current time will be used
instead.
"""
time1 = to_datetime(time1)
time2 = to_datetime(time2)
if time1 > time2:
time2, time1 = time1, time2
units = ((3600 * 24 * 365, 'year', 'years'),
(3600 * 24 * 30, 'month', 'months'),
(3600 * 24 * 7, 'week', 'weeks'),
(3600 * 24, 'day', 'days'),
(3600, 'hour', 'hours'),
(60, 'minute', 'minutes'))
diff = time2 - time1
age_s = int(diff.days * 86400 + diff.seconds)
if resolution and age_s < resolution:
return ''
if age_s <= 60 * 1.9:
return '%i second%s' % (age_s, age_s != 1 and 's' or '')
for u, unit, unit_plural in units:
r = float(age_s) / float(u)
if r >= 1.9:
r = int(round(r))
return '%d %s' % (r, r == 1 and unit or unit_plural)
return ''
[docs]def http_date(t=None):
"""Format `datetime` object `t` as a rfc822 timestamp"""
t = to_datetime(t).astimezone(utc)
weekdays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
'Oct', 'Nov', 'Dec')
return '%s, %02d %s %04d %02d:%02d:%02d GMT' % (
weekdays[t.weekday()], t.day, months[t.month - 1], t.year,
t.hour, t.minute, t.second)
# -- parsing
_ISO_8601_RE = re.compile(r'(\d\d\d\d)(?:-?(\d\d)(?:-?(\d\d))?)?' # date
r'(?:T(\d\d)(?::?(\d\d)(?::?(\d\d))?)?)?' # time
r'(Z?(?:([-+])?(\d\d):?(\d\d)?)?)?$' # timezone
)
[docs]def parse_date(text, tzinfo=None):
tzinfo = tzinfo or localtz
if text == 'now': # TODO: today, yesterday, etc.
return datetime.now(utc)
tm = None
text = text.strip()
# normalize ISO time
match = _ISO_8601_RE.match(text)
if match:
try:
g = match.groups()
years = g[0]
months = g[1] or '01'
days = g[2] or '01'
hours, minutes, seconds = [x or '00' for x in g[3:6]]
z, tzsign, tzhours, tzminutes = g[6:10]
if z:
tz = timedelta(hours=int(tzhours or '0'),
minutes=int(tzminutes or '0')).seconds / 60
if tz == 0:
tzinfo = utc
else:
tzinfo = FixedOffset(tzsign == '-' and -tz or tz,
'%s%s:%s' %
(tzsign, tzhours, tzminutes))
tm = time.strptime('%s ' * 6 % (years, months, days,
hours, minutes, seconds),
'%Y %m %d %H %M %S ')
except ValueError:
pass
else:
for format in ['%x %X', '%x, %X', '%X %x', '%X, %x', '%x', '%c',
'%b %d, %Y']:
try:
tm = time.strptime(text, format)
break
except ValueError:
continue
if tm == None:
hint = get_date_format_hint()
raise TicError('"%s" is an invalid date, or the date format '
'is not known. Try "%s" instead.' % (text, hint),
'Invalid Date')
dt = tzinfo.localize(datetime(*tm[0:6]))
# Make sure we can convert it to a timestamp and back - fromtimestamp()
# may raise ValueError if larger than platform C localtime() or gmtime()
try:
to_datetime(to_timestamp(dt), tzinfo)
except ValueError:
raise TicError('The date "%s" is outside valid range. '
'Try a date closer to present time.' % (text,),
'Invalid Date')
return dt
# -- timezone utilities
[docs]class FixedOffset(tzinfo):
"""Fixed offset in minutes east from UTC."""
def __init__(self, offset, name):
self._offset = timedelta(minutes=offset)
self.zone = name
def __str__(self):
return self.zone
def __repr__(self):
return '<FixedOffset "%s" %s>' % (self.zone, self._offset)
[docs] def utcoffset(self, dt):
return self._offset
[docs] def tzname(self, dt):
return self.zone
[docs] def dst(self, dt):
return _zero
[docs] def localize(self, dt, is_dst=False):
if dt.tzinfo is not None:
raise ValueError('Not naive datetime (tzinfo is already set)')
return dt.replace(tzinfo=self)
STDOFFSET = timedelta(seconds=-time.timezone)
if time.daylight:
DSTOFFSET = timedelta(seconds=-time.altzone)
else:
DSTOFFSET = STDOFFSET
DSTDIFF = DSTOFFSET - STDOFFSET
[docs]class LocalTimezone(tzinfo):
"""A 'local' time zone implementation"""
[docs] def utcoffset(self, dt):
if self._isdst(dt):
return DSTOFFSET
else:
return STDOFFSET
[docs] def dst(self, dt):
if self._isdst(dt):
return DSTDIFF
else:
return _zero
[docs] def tzname(self, dt):
return time.tzname[self._isdst(dt)]
def _isdst(self, dt):
tt = (dt.year, dt.month, dt.day,
dt.hour, dt.minute, dt.second,
dt.weekday(), 0, -1)
try:
stamp = time.mktime(tt)
tt = time.localtime(stamp)
return tt.tm_isdst > 0
except OverflowError:
return False
[docs] def localize(self, dt, is_dst=False):
if dt.tzinfo is not None:
raise ValueError('Not naive datetime (tzinfo is already set)')
return dt.replace(tzinfo=self)
utc = FixedOffset(0, 'UTC')
utcmin = datetime.min.replace(tzinfo=utc)
utcmax = datetime.max.replace(tzinfo=utc)
_epoc = datetime(1970, 1, 1, tzinfo=utc)
_zero = timedelta(0)
localtz = LocalTimezone()
# Use a makeshift timezone implementation if pytz is not available.
# This implementation only supports fixed offset time zones.
#
_timezones = [
FixedOffset(0, 'UTC'),
FixedOffset(-720, 'GMT -12:00'), FixedOffset(-660, 'GMT -11:00'),
FixedOffset(-600, 'GMT -10:00'), FixedOffset(-540, 'GMT -9:00'),
FixedOffset(-480, 'GMT -8:00'), FixedOffset(-420, 'GMT -7:00'),
FixedOffset(-360, 'GMT -6:00'), FixedOffset(-300, 'GMT -5:00'),
FixedOffset(-240, 'GMT -4:00'), FixedOffset(-180, 'GMT -3:00'),
FixedOffset(-120, 'GMT -2:00'), FixedOffset(-60, 'GMT -1:00'),
FixedOffset(0, 'GMT'), FixedOffset(60, 'GMT +1:00'),
FixedOffset(120, 'GMT +2:00'), FixedOffset(180, 'GMT +3:00'),
FixedOffset(240, 'GMT +4:00'), FixedOffset(300, 'GMT +5:00'),
FixedOffset(360, 'GMT +6:00'), FixedOffset(420, 'GMT +7:00'),
FixedOffset(480, 'GMT +8:00'), FixedOffset(540, 'GMT +9:00'),
FixedOffset(600, 'GMT +10:00'), FixedOffset(660, 'GMT +11:00'),
FixedOffset(720, 'GMT +12:00'), FixedOffset(780, 'GMT +13:00')]
_tzmap = dict([(z.zone, z) for z in _timezones])
all_timezones = [z.zone for z in _timezones]
try:
import pytz
_tzoffsetmap = dict([(tz.utcoffset(None), tz) for tz in _timezones
if tz.zone != 'UTC'])
def timezone(tzname):
tz = get_timezone(tzname)
if not tz:
raise KeyError(tzname)
return tz
def get_timezone(tzname):
"""Fetch timezone instance by name or return `None`"""
try:
# if given unicode parameter, pytz.timezone fails with:
# "type() argument 1 must be string, not unicode"
tz = pytz.timezone(unicode(tzname).encode('ascii', 'replace'))
except (KeyError, IOError):
tz = _tzmap.get(tzname)
if tz and tzname.startswith('Etc/'):
tz = _tzoffsetmap.get(tz.utcoffset(None))
return tz
_pytz_zones = [tzname for tzname in pytz.common_timezones
if not tzname.startswith('Etc/') and
not tzname.startswith('GMT')]
# insert just the GMT timezones into the pytz zones at the right location
# the pytz zones already include UTC so skip it
from bisect import bisect
_gmt_index = bisect(_pytz_zones, 'GMT')
all_timezones = _pytz_zones[:_gmt_index] + all_timezones[1:] + \
_pytz_zones[_gmt_index:]
except ImportError:
[docs] def timezone(tzname):
"""Fetch timezone instance by name or raise `KeyError`"""
return _tzmap[tzname]
[docs] def get_timezone(tzname):
"""Fetch timezone instance by name or return `None`"""
return _tzmap.get(tzname)