######################################################################## # $Header: /var/local/cvsroot/4Suite/Ft/Lib/Time.py,v 1.21 2005/04/06 23:36:48 jkloth Exp $ """ Date and time related functionality for use within 4Suite only. This module is experimental and may not be staying in 4Suite for long; application developers should avoid forming dependencies on it. Copyright 2005 Fourthought, Inc. (USA). Detailed license and copyright information: http://4suite.org/COPYRIGHT Project home, documentation, distributions: http://4suite.org/ """ import re, time, calendar, rfc822 _month_days = ( (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31), (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31), ) def DayOfYearFromYMD(year, month, day): """ Calculates the Julian day (day of year, between 1 and 366), for the given date. This function is accurate for dates back to 01 Jan 0004 (that's 4 A.D.), when the Julian calendar stabilized. """ days_table = _month_days[calendar.isleap(year)] days = 0 for m in range(1, month): days += days_table[m] return days + day def WeekdayFromYMD(year, month, day): """ Calculates the day of week (0=Mon, 6=Sun) for the given date. This function is accurate for dates on/after Friday, 15 Oct 1582, when the Gregorian reform took effect, although it should be noted that some nations didn't adopt the Gregorian calendar until as late as the 20th century, so dates that were referenced before then would have fallen on a different day of the week, at the time. """ # An alternate way of calculating weekdays is to let the python time # module do it for you, like this: # # time.localtime() on Windows won't take negative arguments! # min_year = time.localtime(0)[0] + 1 # max_year = time.localtime(sys.maxint)[0] - 1 # if self._localYear > min_year and self._localYear < max_year: # secsSinceEpoch = time.mktime(self.asPythonTimeTuple(local=1)) # self._utcWeekday = time.gmtime(secsSinceEpoch)[6] # self._localWeekday = time.localtime(secsSinceEpoch)[6] # # However, this can only be used with dates that time.localtime() can # handle, which varies from system to system and is likely to be within # the bounds of 1970 and 2037. # We use a convenient epoch of 2001-01-01: it was a Monday if year == 2001: days_from_epoch = 0 else: years = range(2001, year, cmp(year, 2001)) leap_days = len(filter(calendar.isleap, years)) days_from_epoch = len(years) * 365 + leap_days if year < 2001: # add the days after this date in the year day_of_year = DayOfYearFromYMD(year, month, day) days_from_epoch += (365 + calendar.isleap(year) - day_of_year) # this is the total number of days before the 20010101 epoch days_from_epoch *= -1 else: # this is the total number of days after the 20010101 epoch days_from_epoch += (DayOfYearFromYMD(year, month, day) - 1) return days_from_epoch % 7 class DT: """ A class that contains the data needed to represent a single point in time using many different date and time formats. Its constructor requires a UTC (GMT) date and time (year, month (0-11), day (0-31), hour (0-23), minute (0-59), second (0-59), millisecond (0-999), plus some information to help express this time in local terms: a local time zone name, or if that's not available, an hour offset of the local time from GMT (-11 to 14, typically), a minute offset of the local time from GMT (0 or 30, usually), and an optional flag indicating Daylight Savings Time, to help determine the time zone name. """ def __init__(self, year, month, day, hour, #In GMT minute, #in GMT second, milliSecond, daylightSavings, #1 means yes tzName, tzHourOffset, tzMinuteOffset): tzMinuteOffset = int(tzMinuteOffset) tzHourOffset = int(tzHourOffset) milliSecond = float(milliSecond) second = int(second) minute = int(minute) hour = int(hour) day = int(day) month = int(month) year = int(year) daylightSavings = not not daylightSavings #This is purely to help look up the tzName if tzName: self._tzName = tzName else: k = float(tzHourOffset) + float(tzMinuteOffset)/60.0 d = self.tzNameTable.get(k) if d: if daylightSavings: n = d[3] if not n: n = d[2] else: n = d[2] if not n: n = d[0] self._tzName = n else: self._tzName = '' #Normalize the milli-seconds between 0 and 999 secondShift = 0 while milliSecond < 0: secondShift -=1 milliSecond += 1000 while milliSecond > 999: secondShift +=1 milliSecond -=1000 self._milliSecond = milliSecond #Normalize the seconds between 0 and 59 second += secondShift minuteShift = 0 while second < 0: minuteShift -=1 second += 60 while second > 59: minuteShift +=1 second -=60 self._second = second #Normalize Minute between 0 and 59 utcHourShift, self._utcMinute = self.__normalizeMinute(minute + minuteShift) localHourShift, self._localMinute = self.__normalizeMinute(minute + minuteShift + tzMinuteOffset) #Normalize Hour between 0 and 23 utcDayShift, self._utcHour = self.__normalizeHour(hour + utcHourShift) localDayShift, self._localHour = self.__normalizeHour(hour + localHourShift + tzHourOffset) #Normalize Date as one between 0 and max day of month self._utcYear, self._utcMonth, self._utcDay = self.__normalizeDate(day + utcDayShift, month, year) self._localYear, self._localMonth, self._localDay = self.__normalizeDate(day + localDayShift, month, year) self._tzHourOffset = tzHourOffset self._tzMinuteOffset = tzMinuteOffset #Set Day In Year (Julian day) self._utcDayOfYear = DayOfYearFromYMD(self._utcYear, self._utcMonth, self._utcDay) self._localDayOfYear = DayOfYearFromYMD(self._localYear, self._localMonth, self._localDay) #Set Weekday self._utcWeekday = WeekdayFromYMD(self._utcYear, self._utcMonth, self._utcDay) self._localWeekday = WeekdayFromYMD(self._localYear, self._localMonth, self._localDay) #Lastly, set this for XPath self.stringValue = self.asISO8601DateTime(local=1) def asISO8601DateTime(self, local=0): """ Represents this DT object as an ISO 8601 date-time string, using UTC time like '2001-01-01T00:00:00Z' if local=0, or local time with UTC offset like '2000-12-31T17:00:00-07:00' if local=1. """ return "%s%s" % (self.asISO8601Date(local), self.asISO8601Time(local)) def asISO8601Date(self, local=0): """ Represents this DT object as an ISO 8601 date-time string, like '2001-01-01' if local=0, or '2000-12-31' if local=1. The local date may vary from UTC date depending on the time of day that is stored in the object. """ if local: y = self._localYear m = self._localMonth d = self._localDay else: y = self._utcYear m = self._utcMonth d = self._utcDay return "%d-%02d-%02d" % (y, m, d) def asISO8601Time(self, local=0): """ Represents this DT object as an ISO 8601 time string, using UTC time like 'T00:00:00Z' if local=0, or local time with UTC offset like 'T17:00:00-07:00' if local=1 """ if local: h = self._localHour m = self._localMinute useTz = 1 else: h = self._utcHour m = self._utcMinute useTz = 0 s = self._second ms = self._milliSecond * 100 rt = "T%02d:%02d:%02d" % (h, m, s) if ms: t = "%d" % ms if len(t) > 3: t = t[:3] while(t and t[-1] == '0'): t = t[:-1] rt += "," + t if not useTz or (not self._tzHourOffset and not self._tzMinuteOffset): rt += "Z" else: if self._tzHourOffset < 0: sign = "-" tzh = -1 * self._tzHourOffset else: sign = "+" tzh = self._tzHourOffset rt += ("%s%02d:%02d" % (sign, tzh, self._tzMinuteOffset)) return rt def asRFC822DateTime(self, local=0): """ Represents this DT object as an RFC 1123 (which updated RFC 822) date string, using UTC time like 'Mon, 01 Jan 2001 00:00:00 GMT' if local=0, or local time with time zone indicator or offset like 'Sun, 31 Dec 2000 17:00:00 MDT' if local=1. Although RFC 822 allows the weekday to be optional, it is always included in the returned string. """ if local: wday = self.abbreviatedWeekdayNameTable[self._localWeekday] mon = self.abbreviatedMonthNameTable[self._localMonth] day = self._localDay year = self._localYear hour = self._localHour minute = self._localMinute # RFC 822 only allows certain timezone names if self._tzName and self._tzName in ['GMT', 'EST', 'EDT', 'CST', 'CDT', 'MST', 'MDT', 'PST', 'PDT']: tz = self._tzName else: tz = '%+03d%02d' % (self._tzHourOffset, self._tzMinuteOffset) else: wday = self.abbreviatedWeekdayNameTable[self._utcWeekday] mon = self.abbreviatedMonthNameTable[self._utcMonth] day = self._utcDay year = self._utcYear hour = self._utcHour minute = self._utcMinute tz = "GMT" # "Thu, 04 Jan 2001 09:15:39 MDT" # RFC 1123 changed the RFC 822 format to use 4-digit years return "%s, %02d %s %d %02d:%02d:%02d %s" % (wday, day, mon, year, hour, minute, self._second, tz) def asPythonTime(self, local=0): """ Returns the stored date and time as a float indicating the number of seconds since the local machine's epoch. """ return time.mktime(self.asPythonTimeTuple(local)) def asPythonTimeTuple(self, local=0): """ Returns the stored date and time as a Python time tuple, as documented in the time module. If the tuple is going to be passed to a function that expects the local time, set local=1. The Daylight Savings flag is always -1, which means unknown, and may or may not have ramifications. """ if local: return (self._localYear, self._localMonth, self._localDay, self._localHour, self._localMinute, self._second, self._localWeekday, self._localDayOfYear, -1) else: return (self._utcYear, self._utcMonth, self._utcDay, self._utcHour, self._utcMinute, self._second, self._utcWeekday, self._utcDayOfYear, -1) def year(self, local=0): """ Returns the year component of the stored date and time as an int like 2001. """ if local: return self._localYear return self._utcYear def month(self, local=0): """ Returns the month component of the stored date and time as an int in the range 0-11. """ if local: return self._localMonth return self._utcMonth def monthName(self, local=0): """ Returns the month component of the stored date and time as a string like 'January'. """ if local: return self.monthNameTable[self._localMonth] return self.monthNameTable[self._utcMonth] def abbreviatedMonthName(self, local=0): """ Returns the month component of the stored date and time as a string like 'Jan'. """ if local: return self.abbreviatedMonthNameTable[self._localMonth] return self.abbreviatedMonthNameTable[self._utcMonth] def day(self, local=0): """ Returns the day component of the stored date and time as an integer in the range 1-31. """ if local: return self._localDay return self._utcDay def dayOfYear(self, local=0): """ Returns the day of year component of the stored date and time as an int in the range 1-366. """ if local: return self._localDayOfYear return self._utcDayOfYear def dayOfWeek(self, local=0): """ Returns the day of week component of the stored date and time as an int in the range 0-6 (0=Monday). """ if local: return self._localWeekday return self._utcWeekday def hour(self, local=0): """ Returns the hour component of the stored date and time as an int in the range 0-23. """ if local: return self._localHour return self._utcHour def minute(self, local=0): """ Returns the minute component of the stored date and time as an int in the range 0-59. """ if local: return self._localMinute return self._utcMinute def second(self): """ Returns the second component of the stored date and time as an int in the range 0-59. """ return self._second def milliSecond(self): """ Returns the millisecond component of the stored date and time as an int in the range 0-999. """ return self._milliSecond def tzName(self): """ Returns the local time's time zone name component of the stored date and time as a string like 'MST'. """ return self._tzName def tzHourOffset(self): """ Returns the local time's hour offset from GMT component of the stored date and time as an int, typically in the range -12 to 14. """ return self._tzHourOffset def tzMinuteOffset(self): """ Returns the local time's minute offset from GMT component of the stored date and time as an int in the range 0-59. """ return self._tzMinuteOffset def __normalizeMinute(self, minute): hourShift = 0 while minute < 0: hourShift -=1 minute += 60 while minute > 59: hourShift +=1 minute -= 60 return hourShift, minute def __normalizeHour(self, hour): dayShift = 0 while hour < 0: dayShift -=1 hour += 24 while hour > 23: dayShift +=1 hour -= 24 return dayShift, hour def __normalizeDate(self, day, month, year): # Returns a valid year, month and day, given a day value that is out # of the acceptable range. This is needed so that the correct local # date can be determined after adding the local time offset to the # UTC time. The time difference may result in the day being shifted, # for example Jan 1 may become Jan 0, which needs to be normalized # to Dec 31 of the preceding year. This function may also be used to # convert a Julian day (1-366) for the given year to a proper year, # month and day, if the month is initially set to 1. while (month < 1 or month > 12 or day < 1 or day > _month_days[calendar.isleap(year)][month] ): if month < 1: year -= 1 month += 12 elif month > 12: year += 1 month -= 12 elif day < 1: month -= 1 if month == 0: #Special case day += 31 else: day += _month_days[calendar.isleap(year)][month] elif day > _month_days[calendar.isleap(year)][month]: day -= _month_days[calendar.isleap(year)][month] month += 1 return year, month, day #Pythonic Interface __str__ = asISO8601DateTime def __cmp__(self, other): if isinstance(other, (str, unicode)): return cmp(self.asISO8601DateTime(), other) elif isinstance(other, (int, float)): return cmp(self.asPythonTime(), other) elif not isinstance(other, DT): raise TypeError("Cannot Compare DT with %s" % repr(other)) #Compare two instances #For now, compare our strings return cmp(self.asISO8601DateTime(), other.asISO8601DateTime()) def __hash__(self): return id(self) #For internal lookups abbreviatedMonthNameTable = ('ERR', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') monthNameTable = ('ERROR', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December') weekdayNameTable = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday') abbreviatedWeekdayNameTable = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') # keyed by offset; # values are (GMT TZ, military TZ, most likely civ TZ, # most likely civ TZ if on summer/daylight savings time) tzNameTable = { +0 : ("GMT", "Zulu", "GMT", "BST"), +1 : ("GMT+1", "Alpha", "CET", "MEST"), +2 : ("GMT+2", "Bravo", "EET", ""), +3 : ("GMT+3", "Charlie", "BT", ""), +3.5 : ("GMT+3:30", "", "", ""), +4 : ("GMT+4", "Delta", "", ""), +4.5 : ("GMT+4:30", "", "", ""), +5 : ("GMT+5", "Echo", "", ""), +5.5 : ("GMT+5:30", "", "", ""), +6 : ("GMT+6", "Foxtrot", "", ""), +6.5 : ("GMT+6:30", "", "", ""), +7 : ("GMT+7", "Golf", "WAST", ""), +8 : ("GMT+8", "Hotel", "CCT", ""), +9 : ("GMT+9", "India", "JST", ""), +9.5 : ("GMT+9:30", "", "Australia Central Time", ""), +10 : ("GMT+10", "Kilo", "GST", ""), +10.5 : ("GMT+10:30", "", "", ""), +11 : ("GMT+11", "Lima", "", ""), +11.5 : ("GMT+11:30", "", "", ""), +12 : ("GMT+12", "Mike", "NZST", ""), +13 : ("GMT+13", "", "", ""), +14 : ("GMT+14", "", "", ""), -1 : ("GMT-1", "November", "WAT", ""), -2 : ("GMT-2", "Oscar", "AT", ""), -3 : ("GMT-3", "Papa", "", "ADT"), -3.5 : ("GMT-3", "", "", ""), -4 : ("GMT-4", "Quebec", "AST", "EDT"), -5 : ("GMT-5", "Romeo", "EST", "CDT"), -6 : ("GMT-6", "Sierra", "CST", "MDT"), -7 : ("GMT-7", "Tango", "MST", "PDT"), -8 : ("GMT-8", "Uniform", "PST", ""), -8.5 : ("GMT-8:30", "", "", "YDT"), -9 : ("GMT-9", "Victor", "YST", ""), -9.5 : ("GMT-9:30", "", "", "HDT"), -10 : ("GMT-10", "Whiskey", "AHST", ""), -11 : ("GMT-11", "XRay", "NT", ""), -12 : ("GMT-11", "Yankee", "IDLW", ""), } CENTURY="(?P[0-9]{2,2})" YEAR="(?P[0-9]{2,2})" MONTH="(?P[0-9]{2,2})" DAY="(?P[0-9]{2,2})" BASIC_DATE="%s?%s%s%s" % (CENTURY, YEAR, MONTH, DAY) EXTENDED_DATE="%s?%s-%s-%s" % (CENTURY, YEAR, MONTH, DAY) YEAR_AND_MONTH_DATE="(-|%s)%s-%s" % (CENTURY, YEAR, MONTH) YEAR_AND_MONTH_DATE_EXTENDED="-%s%s" % (YEAR, MONTH) YEAR_ONLY_DATE="(-|%s)%s" % (CENTURY, YEAR) CENTURY_ONLY_DATE=CENTURY DAY_OF_MONTH="--%s(?:-?%s)?" % (MONTH, DAY) DAY_ONLY_DATE="---%s" % (DAY) #build the list of calendar date expressions cd_expressions = [BASIC_DATE, EXTENDED_DATE, YEAR_AND_MONTH_DATE, YEAR_AND_MONTH_DATE_EXTENDED, YEAR_ONLY_DATE, CENTURY_ONLY_DATE, DAY_OF_MONTH, DAY_ONLY_DATE] cd_expressions = map(lambda x:"(?P%s)" % x, cd_expressions) ORDINAL_DAY="(?P[0-9]{3,3})" ORDINAL_DATE="(?P%s?%s-?%s)" % (CENTURY, YEAR, ORDINAL_DAY) ORDINAL_DATE_ONLY="(?P-%s)" % (ORDINAL_DAY) od_expressions = [ORDINAL_DATE, ORDINAL_DATE_ONLY] WEEK="(?P[0-9][0-9])" WEEK_DAY="(?P[1-7])" BASIC_WEEK_DATE="%s?%sW%s%s?" %(CENTURY, YEAR, WEEK, WEEK_DAY) EXTENDED_WEEK_DATE="%s?%s-W%s(?:-%s)?" %(CENTURY, YEAR, WEEK, WEEK_DAY) WEEK_IN_DECADE="-(?P[0-9])W%s%s" % (WEEK, WEEK_DAY) WEEK_IN_DECADE_EXTENDED="-(?P[0-9])-W%s-%s" % (WEEK, WEEK_DAY) WEEK_AND_DAY_BASIC="-W%s(?:-?%s)?"%(WEEK, WEEK_DAY) WEEKDAY_ONLY="-W?-%s" % (WEEK_DAY) #build the list of week date expressions wd_expressions=[BASIC_WEEK_DATE, EXTENDED_WEEK_DATE, WEEK_IN_DECADE, WEEK_IN_DECADE_EXTENDED, WEEK_AND_DAY_BASIC, WEEKDAY_ONLY] wd_expressions = map(lambda x:"(?P%s)" % x, wd_expressions) #Build the list of date expressions date_expressions = map(lambda x:"(?P%s)" % x, cd_expressions+od_expressions+wd_expressions) HOUR="(?P(?:0[0-9])|(?:1[0-9])|(?:2[0-4]))" MINUTE="(?P(?:[0-5][0-9])|(?:60))" SECOND="(?P(?:[0-5][0-9])|(?:60))" DECIMAL_SEPARATOR="(?:\.|,)" DECIMAL_VALUE="(?P[0-9]*)" BASIC_TIME_FORMAT="(?:%s%s%s(?:%s%s)?)" % (HOUR, MINUTE, SECOND, DECIMAL_SEPARATOR, DECIMAL_VALUE) EXTENDED_TIME_FORMAT="(?:%s:%s:%s(?:%s%s)?)" % (HOUR, MINUTE, SECOND, DECIMAL_SEPARATOR, DECIMAL_VALUE) HOUR_MINUTE_TIME="(?:%s:?%s(?:%s%s)?)" % (HOUR, MINUTE, DECIMAL_SEPARATOR, DECIMAL_VALUE) HOUR_TIME="(?:%s(?:%s%s)?)" % (HOUR, DECIMAL_SEPARATOR, DECIMAL_VALUE) MINUTE_SECOND_TIME="(?:-%s:?%s(?:%s%s)?)" % (MINUTE, SECOND, DECIMAL_SEPARATOR, DECIMAL_VALUE) MINUTE_TIME="(?:-%s(?:%s%s)?)" % (MINUTE, DECIMAL_SEPARATOR, DECIMAL_VALUE) SECOND_TIME="(?P--%s(?:%s%s)?)" % (SECOND, DECIMAL_SEPARATOR, DECIMAL_VALUE) #build the basic time expressions bt_expressions = [BASIC_TIME_FORMAT, EXTENDED_TIME_FORMAT, HOUR_MINUTE_TIME, HOUR_TIME, MINUTE_SECOND_TIME, MINUTE_TIME, SECOND_TIME] bt_expressions = map(lambda x:"(?P