''' Utilities for working with dates and times, bringing together many of the standard Python libraries. This module takes the perspective of supporting *conversions* between the many temporal representations supported by different libraries. These representations are as follows: * Strings, esp. those formatted according to ISO-8601 [str] '1980-07-31T13:07:13-04:00' * datetime.datetime objects [dt] datetime.datetime(1980, 7, 31, 13, 7, 13, tzinfo=tzoffset(None, -14400)) * time.struct_time objects, a.k.a. time tuples [tt] time.struct_time(tm_year=1980, tm_mon=7, tm_mday=31, tm_hour=13, tm_min=7, tm_sec=13, tm_wday=3, tm_yday=213, tm_isdst=1) * seconds since the Unix epoch, 1 Jan 1970 00:00:00 UTC [secs] 333896833 Time zones are obviously a major complicating factor: we need to support conversions among the system's local time zone, UTC, and other specific time zones specified in some representation. @author: Nathan Schneider (nschneid) @since: 2010-07-31 @see: http://docs.python.org/library/time.html http://docs.python.org/library/datetime.html http://labix.org/python-dateutil http://pytz.sourceforge.net/ ''' from dateutil.parser import parse as parseTime def str2dt(strt): ''' >>> str2dt('3 Jan 1982') datetime.datetime(1982, 1, 3, 0, 0) >>> str2dt('January 3, 1982 11pm') datetime.datetime(1982, 1, 3, 23, 0) >>> str2dt('Jul. 3, 1982 11:18pm PDT') datetime.datetime(1982, 7, 3, 23, 18) >>> str2dt('1982-01-03T23:00:00-04:00') datetime.datetime(1982, 1, 3, 23, 0, tzinfo=tzoffset(None, -14400)) >>> str2dt('baloney') Traceback (most recent call last): ... ValueError: unknown string format: 'baloney' ''' try: return parseTime(strt) except ValueError as ex: raise ValueError(ex.message + ': ' + repr(strt)) import calendar, time, datetime, dateutil, pytz def is_dt(t): return isinstance(t, datetime.datetime) def is_str(t): return isinstance(t, basestring) def is_tt(t): return isinstance(t, time.struct_time) def is_secs(t): return isinstance(t, int) ''' [tt] The time.struct_time representation does not encode a timezone offset, but it DOES include a tm_isdst flag. struct_time conversions offered by the 'time' library: (now) -> struct_time in UTC: gmtime() seconds since the epoch -> struct_time in UTC: gmtime(secs) (now) -> struct_time in local time: localtime() seconds since the epoch -> struct_time in local time: localtime(secs) struct_time in UTC -> seconds since the epoch: calendar.timegm(tt) struct_time in local time -> seconds since the epoch: mktime(tt) ''' def now2tt(target_local=False): return time.localtime() if target_local else time.gmtime() def secs2tt(secs, target_local=False): return time.localtime(secs) if target_local else time.gmtime(secs) def tt2secs(tt, source_local=False): return int(time.mktime(tt)) if source_local else calendar.timegm(tt) def now2secs(): return calendar.timegm(now2tt()) # equivalently: now2tt(target_local=True) ''' [dt] The datetime.datetime representation optionally encodes a timezone, which must be a subtype of the abstract class datetime.tzinfo. ''' utc = pytz.utc et = pytz.timezone('US/Eastern') # Note that Indiana is split between Central and Eastern. See "America/Indiana*" in pytz.all_timezones . ct = pytz.timezone('US/Central') mt = pytz.timezone('US/Mountain') az = pytz.timezone('US/Arizona') pt = pytz.timezone('US/Pacific') ak = pytz.timezone('US/Alaska') hi = pytz.timezone('US/Hawaii') localtz = dateutil.tz.tzlocal() def dt(t, **kwargs): '''Converts the provided time argument to a datetime.datetime representation, if it is not already one.''' if is_dt(t): return dt2dt(t, **kwargs) if is_str(t): return str2dt(t, **kwargs) if is_secs(t): return secs2dt(t, **kwargs) if is_tt(t): return tt2dt(t, **kwargs) raise ValueError('Argument to dt() not a valid date representation: {}'.format(repr(t))) def now2dt(target_local=False, with_zone=True, strip_micros=True): return dt2dt((dt_strip_micros if strip_micros else lambda x: x)(datetime.datetime.now() if target_local else datetime.datetime.utcnow()), target_zone=((localtz if target_local else utc) if with_zone else None)) def dt_zone(dt): return dt.tzinfo def dt_strip_zone(dt): return dt.replace(tzinfo=None) def dt_strip_micros(dt): return dt.replace(microsecond=0) def dt_diff(dt1, dt2, unit=None): delta = dt2-dt1 if unit is None: return delta # a datetime.timedelta object assert unit in {'seconds','minutes','hours','days'} d = int(delta.total_seconds()) if unit=='seconds': return d d /= 60 if unit=='minutes': return d d /= 60 if unit=='hours': return d d /= 24 return d def str_diff(str1, str2, unit=None): return dt_diff(parseTime(str1), parseTime(str2), unit=unit) def time_difference(t1, t2, unit=None): return dt_diff(dt(t1), dt(t2), unit=unit) def equiv_zones(z1, z2, dt): return z1.utcoffset(dt)==z2.utcoffset(dt) def dt2dt(dt, target_zone=utc, default_source_zone=None): '''Returns a copy of the datetime.datetime object, optionally converting between time zones (by default, converts to UTC). Unless 'target_zone' and 'default_source_zone' are both None, the result is guaranteed to include a timezone. If 'target_zone' and 'default_source_zone' are both None, the timezone information in the provided datetime will be preserved. @see: dt_strip_zone() to remove timezone information from a datetime object ''' if target_zone is None: if dt_zone(dt): return dt.replace() # simply return a copy of the datetime return dt.replace(tzinfo=default_source_zone) # add the provided default timezone elif dt_zone(dt): return dt.astimezone(target_zone) # convert to new timezone elif default_source_zone is not None: return dt.replace(tzinfo=default_source_zone).astimezone(target_zone) # convert from default to new timezone return dt.replace(tzinfo=target_zone) # specify timezone without any conversion def dt2tt(dt, default_source_zone=None, default_is_dst=-1): if dt_zone(dt) is None and default_source_zone is None: assert default_is_dst is not None pass def dt2secs(dt, default_source_zone=None): return tt2secs(dt2tt(dt2dt(dt,default_source_zone)), source_local=False) def tt2dt(tt, target_zone=None): pass def secs2dt(secs, target_zone=None): pass def timestamp(t=None, local=False): '''Make an ISO-8601 timestamp. 't' may be a time tuple, a datetime object, or the number of seconds since the Unix epoch. 'local' indicates whether the local time or UTC time should be used in the timestamp (this will also govern the interpretation of 't'). If 't' includes a time zone specification, it will be converted to local time if 'local' is True or to UTC otherwise. Based on: http://stackoverflow.com/questions/2150739/iso-time-iso-8601-in-python @since: 2010-07-31 @author: Nathan Schneider (nschneid) >>> print(timestamp(datetime.datetime(1980,7,31,11,7,13))) 1980-07-31T11:07:13Z >>> import pytz >>> mountain = pytz.timezone('US/Mountain') >>> print(timestamp(mountain.localize(datetime.datetime(1980,7,31,11,7,13)))) 1980-07-31T17:07:13Z >>> print(timestamp(mountain.localize(datetime.datetime(1980,7,31,11,7,13)), local=True)) 1980-07-31T13:07:13-04:00 >>> print(timestamp(3600)) 1970-01-01T01:00:00Z >>> timestamp(now2dt(target_local=True,with_zone=False))[:-1]==now2dt(target_local=True,with_zone=False).isoformat() True >>> timestamp(now2dt(target_local=True),local=True)==now2dt(target_local=True).isoformat() True >>> timestamp(time.localtime(),local=True)==timestamp(local=True) True >>> timestamp(time.gmtime(),local=False)==timestamp(local=False) True >>> goodTimestamp = lambda t, **kwargs: calendar.timegm(parseTime(timestamp(t, **kwargs)).timetuple())==calendar.timegm(t) >>> goodTimestamp(time.localtime(),local=True) True >>> goodTimestamp(time.gmtime()) True >>> goodTimestamp(time.gmtime(),local=False) True ''' if t is None or isinstance(t,int): if local: t = time.localtime(t) else: t = time.gmtime(t) elif isinstance(t,datetime.datetime): if t.tzinfo is not None: # a zone is specified in the datetime if local: t = t.astimezone(dateutil.tz.tzlocal()) # convert to local time else: t = t.astimezone(pytz.utc) # convert to UTC t = t.timetuple() if local: z = time.altzone else: z = 'Z' return tt2str(t, zone=z) def dt2str(dt): return dt.isoformat() def tt2str(tt, zone=None): # TODO: instead convert to datetime and use .isoformat()? ''' Formats a given struct_time and display zone as an ISO-8601 string. The zone may be specified as an integer representing seconds *before* UTC, or as a string to be appended at the end of the time. If 'zone' is None, it will be omitted from the string. No timezone conversions will be performed by this method. ''' formatted = time.strftime(timestamp_format_string(), tt) if zone is None: tz = '' elif isinstance(zone,int): tz = '{0:+06.2f}'.format(-float(zone) / 3600).replace('.',':') else: tz = zone result = formatted + tz try: parseTime(result) except: print(result) return result def timestamp_format_string(micros=False, zone=False): '''Note: the %f format specifier for microseconds is supported by the datetime module but not the time module.''' s = '%Y-%m-%dT%H:%M:%S' if micros: s += '.%f' if zone: s += '%z' return s if __name__ == '__main__': import doctest doctest.testmod()