changeset 3619:df7bff6f8a2f

full timezone support (based on patch [SF#1465296])
author Alexander Smishlajev <a1s@users.sourceforge.net>
date Sat, 06 May 2006 16:43:29 +0000
parents b31a2e35be80
children 17124caa2491
files roundup/date.py
diffstat 1 files changed, 139 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/roundup/date.py	Sat May 06 16:19:27 2006 +0000
+++ b/roundup/date.py	Sat May 06 16:43:29 2006 +0000
@@ -15,20 +15,23 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: date.py,v 1.86 2006-04-27 05:15:16 richard Exp $
+# $Id: date.py,v 1.87 2006-05-06 16:43:29 a1s Exp $
 
 """Date, time and time interval handling.
 """
 __docformat__ = 'restructuredtext'
 
-import time, re, calendar
-import i18n
+import calendar
+import datetime
+import time
+import re
 
 try:
-    import datetime
-    have_datetime = 1
-except:
-    have_datetime = 0
+    import pytz
+except ImportError:
+    pytz = None
+
+from roundup import i18n
 
 def _add_granularity(src, order, value = 1):
     '''Increment first non-None value in src dictionary ordered by 'order'
@@ -52,6 +55,119 @@
     (\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d?(\.\d+)?)
 ''', re.VERBOSE)
 
+_timedelta0 = datetime.timedelta(0)
+
+# load UTC tzinfo
+if pytz:
+    UTC = pytz.utc
+else:
+    # fallback implementation from Python Library Reference
+
+    class _UTC(datetime.tzinfo):
+
+        """Universal Coordinated Time zoneinfo"""
+
+        def utcoffset(self, dt):
+            return _timedelta0
+
+        def tzname(self, dt):
+            return "UTC"
+
+        def dst(self, dt):
+            return _timedelta0
+
+        def __repr__(self):
+            return "<UTC>"
+
+        # pytz adjustments interface
+        # Note: pytz verifies that dt is naive datetime for localize()
+        # and not naive datetime for normalize().
+        # In this implementation, we don't care.
+
+        def normalize(self, dt, is_dst=False):
+            return dt.replace(tzinfo=self)
+
+        def localize(self, dt, is_dst=False):
+            return dt.replace(tzinfo=self)
+
+    UTC = _UTC()
+
+# integral hours offsets were available in Roundup versions prior to 1.1.3
+# and still are supported as a fallback if pytz module is not installed
+class SimpleTimezone(datetime.tzinfo):
+
+    """Simple zoneinfo with fixed numeric offset and no daylight savings"""
+
+    def __init__(self, offset=0, name=None):
+        super(SimpleTimezone, self).__init__()
+        self.offset = offset
+        if name:
+            self.name = name
+        else:
+            self.name = "Etc/GMT%+d" % self.offset
+
+    def utcoffset(self, dt):
+        return datetime.timedelta(hours=self.offset)
+
+    def tzname(self, dt):
+        return self.name
+
+    def dst(self, dt):
+        return _timedelta0
+
+    def __repr__(self):
+        return "<%s: %s>" % (self.__class__.__name__, self.name)
+
+    # pytz adjustments interface
+
+    def normalize(self, dt):
+        return dt.replace(tzinfo=self)
+
+    def localize(self, dt, is_dst=False):
+        return dt.replace(tzinfo=self)
+
+# simple timezones with fixed offset
+_tzoffsets = dict(GMT=0, UCT=0, EST=5, MST=7, HST=10)
+
+def get_timezone(tz):
+    # if tz is None, return None (will result in naive datetimes)
+    # XXX should we return UTC for None?
+    if tz is None:
+        return None
+    # try integer offset first for backward compatibility
+    try:
+        utcoffset = int(tz)
+    except (TypeError, ValueError):
+        pass
+    else:
+        if utcoffset == 0:
+            return UTC
+        else:
+            return SimpleTimezone(utcoffset)
+    # tz is a timezone name
+    if pytz:
+        return pytz.timezone(tz)
+    elif tz == "UTC":
+        return UTC
+    elif tz in _tzoffsets:
+        return SimpleTimezone(_tzoffsets[tz], tz)
+    else:
+        raise KeyError, tz
+
+def _utc_to_local(y,m,d,H,M,S,tz):
+    TZ = get_timezone(tz)
+    frac = S - int(S)
+    dt = datetime.datetime(y, m, d, H, M, int(S), tzinfo=UTC)
+    y,m,d,H,M,S = dt.astimezone(TZ).timetuple()[:6]
+    S = S + frac
+    return (y,m,d,H,M,S)
+
+def _local_to_utc(y,m,d,H,M,S,tz):
+    TZ = get_timezone(tz)
+    dt = datetime.datetime(y,m,d,H,M,int(S))
+    y,m,d,H,M,S = TZ.localize(dt).utctimetuple()[:6]
+    return (y,m,d,H,M,S)
+
 class Date:
     '''
     As strings, date-and-time stamps are specified with the date in
@@ -144,7 +260,7 @@
         if type(spec) == type(''):
             self.set(spec, offset=offset, add_granularity=add_granularity)
             return
-        elif have_datetime and isinstance(spec, datetime.datetime):
+        elif isinstance(spec, datetime.datetime):
             # Python 2.3+ datetime object
             y,m,d,H,M,S,x,x,x = spec.timetuple()
             if y < 1970: raise ValueError, 'year must be > 1970'
@@ -158,9 +274,8 @@
             y,m,d,H,M,S,x,x,x = spec
             if y < 1970: raise ValueError, 'year must be > 1970'
             frac = S - int(S)
-            ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
             self.year, self.month, self.day, self.hour, self.minute, \
-                self.second, x, x, x = time.gmtime(ts)
+                self.second = _local_to_utc(y, m, d, H, M, S, offset)
             # we lost the fractional part
             self.second = self.second + frac
         except:
@@ -199,6 +314,9 @@
         # gmtime loses the fractional seconds
         S = S + frac
 
+        # whether we need to convert to UTC
+        adjust = False
+
         if info['y'] is not None or info['a'] is not None:
             if info['y'] is not None:
                 y = int(info['y'])
@@ -211,16 +329,18 @@
             if info['a'] is not None:
                 m = int(info['a'])
                 d = int(info['b'])
-            H = -offset
+            H = 0
             M = S = 0
+            adjust = True
 
         # override hour, minute, second parts
         if info['H'] is not None and info['M'] is not None:
-            H = int(info['H']) - offset
+            H = int(info['H'])
             M = int(info['M'])
             S = 0
             if info['S'] is not None:
                 S = float(info['S'])
+            adjust = True
 
         if add_granularity:
             S = S - 1
@@ -228,8 +348,11 @@
         # now handle the adjustment of hour
         frac = S - int(S)
         ts = calendar.timegm((y,m,d,H,M,S,0,0,0))
+        y, m, d, H, M, S, x, x, x = time.gmtime(ts)
+        if adjust:
+            y, m, d, H, M, S = _local_to_utc(y, m, d, H, M, S, offset)
         self.year, self.month, self.day, self.hour, self.minute, \
-            self.second, x, x, x = time.gmtime(ts)
+            self.second = y, m, d, H, M, S
         # we lost the fractional part along the way
         self.second = self.second + frac
 
@@ -396,8 +519,9 @@
     def local(self, offset):
         """ Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone.
         """
-        return Date((self.year, self.month, self.day, self.hour + offset,
-            self.minute, self.second, 0, 0, 0), translator=self.translator)
+        y, m, d, H, M, S = _utc_to_local(self.year, self.month, self.day,
+                self.hour, self.minute, self.second, offset)
+        return Date((y, m, d, H, M, S, 0, 0, 0), translator=self.translator)
 
     def __deepcopy__(self, memo):
         return Date((self.year, self.month, self.day, self.hour,

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