1""" 2Convert COARDS time specification to a datetime object. 3 4""" 5 6from datetime import datetime, timedelta 7import re 8import warnings 9 10 11EPOCH = datetime(1970, 1, 1) 12 13 14# constants in seconds 15SECOND = 1.0 16MINUTE = 60.0 17HOUR = 3.6e3 18DAY = 8.64e4 19SHAKE = 1e-8 20SIDEREAL_DAY = 8.616409e4 21SIDEREAL_HOUR = 3.590170e3 22SIDEREAL_MINUTE = 5.983617e1 23SIDEREAL_SECOND = 0.9972696 24SIDEREAL_YEAR = 3.155815e7 25TROPICAL_YEAR = 3.15569259747e7 26LUNAR_MONTH = 29.530589 * DAY 27COMMON_YEAR = 365 * DAY 28LEAP_YEAR = 366 * DAY 29JULIAN_YEAR = 365.25 * DAY 30GREGORIAN_YEAR = 365.2425 * DAY 31SIDEREAL_MONTH = 27.321661 * DAY 32TROPICAL_MONTH = 27.321582 * DAY 33FORTNIGHT = 14 * DAY 34WEEK = 7 * DAY 35JIFFY = 1e-2 36EON = 1e9 * TROPICAL_YEAR 37MONTH = TROPICAL_YEAR/12 38MILLISECOND = 1e-3 39MICROSECOND = 1e-6 40 41 42class ParserError(Exception): 43 pass 44 45 46class Parser(object): 47 def __init__(self, units): 48 parts = units.split(' since ') 49 self.units = parse_units(parts[0]) 50 self.offset = parse_date(parts[1]) 51 52 def __call__(self, value): 53 seconds = value*self.units 54 55 try: 56 date = EPOCH + timedelta(seconds=self.offset+seconds) 57 except OverflowError: 58 warnings.warn( 59 "Shifted data 366 days to the future, since year zero does not exist.", 60 UserWarning) 61 date = EPOCH + timedelta(seconds=self.offset+seconds+LEAP_YEAR) 62 63 return date 64 65 66class Formatter(object): 67 def __init__(self, units): 68 parts = units.split(' since ') 69 self.units = parse_units(parts[0]) 70 self.offset = parse_date(parts[1]) 71 72 def __call__(self, date): 73 dt = (date - EPOCH) 74 value = dt.days*DAY + dt.seconds + dt.microseconds*MICROSECOND - self.offset 75 return value / self.units 76 77 78class Converter(object): 79 def __init__(self, from_, to): 80 self.parser = Parser(from_) 81 self.formatter = Formatter(to) 82 83 def __call__(self, value): 84 return self.formatter(self.parser(value)) 85 86 87def parse(value, units): 88 """ 89 Parse a COARDS compliant date:: 90 91 >>> parse(0, "hours since 1970-01-01 00:00:00") 92 datetime.datetime(1970, 1, 1, 0, 0) 93 >>> parse(0, "hours since 1970-01-01 00:00:00 +2:30") 94 datetime.datetime(1969, 12, 31, 21, 30) 95 >>> parse(10, "hours since 1996-1-1") 96 datetime.datetime(1996, 1, 1, 10, 0) 97 >>> parse(10, "hours since 1-1-1") 98 datetime.datetime(1, 1, 1, 10, 0) 99 >>> parse(10, "hours since 1990-11-25 12:00:00") 100 datetime.datetime(1990, 11, 25, 22, 0) 101 >>> parse(10, "hours since 1990-11-25 12:00") 102 datetime.datetime(1990, 11, 25, 22, 0) 103 >>> parse(10, "hours since 1990-11-25 12:00 +2:00") 104 datetime.datetime(1990, 11, 25, 20, 0) 105 >>> parse(10, "hours since 1990-11-25 12:00 UTC") 106 datetime.datetime(1990, 11, 25, 22, 0) 107 >>> parse(10, "seconds since 1970-1-1") 108 datetime.datetime(1970, 1, 1, 0, 0, 10) 109 110 It works with a year that never existed, since it's usual to have the 111 origin set to the year zero in climatological datasets:: 112 113 >>> parse(366, "days since 0000-01-01 00:00:00") 114 datetime.datetime(1, 1, 1, 0, 0) 115 116 """ 117 parser = Parser(units) 118 return parser(value) 119 120 121def format(date, units): 122 """ 123 Convert a datetime object into a COARDS compliant date:: 124 125 >>> print format(datetime(1970, 1, 1, 0, 0), "hours since 1970-01-01 00:00:00") 126 0.0 127 >>> print format(datetime(1969, 12, 31, 21, 30), "hours since 1970-01-01 00:00:00 +2:30") 128 0.0 129 >>> print format(datetime(1996, 1, 1, 10, 0), "hours since 1996-1-1") 130 10.0 131 >>> print format(datetime(1, 1, 1, 10, 0), "hours since 1-1-1") 132 10.0 133 >>> print format(datetime(1990, 11, 25, 22, 0), "hours since 1990-11-25 12:00:00") 134 10.0 135 >>> print format(datetime(1990, 11, 25, 22, 0), "hours since 1990-11-25 12:00") 136 10.0 137 >>> print format(datetime(1990, 11, 25, 20, 0), "hours since 1990-11-25 12:00 +2:00") 138 10.0 139 >>> print format(datetime(1990, 11, 25, 22, 0), "hours since 1990-11-25 12:00 UTC") 140 10.0 141 >>> print format(datetime(1970, 1, 1, 0, 0, 10), "seconds since 1970-1-1") 142 10.0 143 144 It works with a year that never existed, since it's usual to have the 145 origin set to the year zero in climatological datasets:: 146 147 >>> print format(datetime(1, 1, 1, 0, 0), "days since 0000-01-01 00:00:00") 148 366.0 149 150 """ 151 formatter = Formatter(units) 152 return formatter(date) 153 154 155def parse_units(units): 156 """ 157 Parse units. 158 159 This function transforms all Udunits defined time units, returning it 160 converted to seconds:: 161 162 >>> print parse_units("min") 163 60.0 164 165 """ 166 udunits = [(SECOND, ['second', 'seconds', 'sec', 's', 'secs']), 167 (MINUTE, ['minute', 'minutes', 'min']), 168 (HOUR, ['hour', 'hours', 'hr', 'h']), 169 (DAY, ['day', 'days', 'd']), 170 (SHAKE, ['shake', 'shakes']), 171 (SIDEREAL_DAY, ['sidereal_day', 'sidereal_days']), 172 (SIDEREAL_HOUR, ['sidereal_hour', 'sidereal_hours']), 173 (SIDEREAL_MINUTE, ['sidereal_minute', 'sidereal_minutes']), 174 (SIDEREAL_SECOND, ['sidereal_second', 'sidereal_seconds']), 175 (SIDEREAL_YEAR, ['sidereal_year', 'sidereal_years']), 176 (TROPICAL_YEAR, ['tropical_year', 'tropical_years', 'year', 'years', 'yr', 'a']), 177 (LUNAR_MONTH, ['lunar_month', 'lunar_months']), 178 (COMMON_YEAR, ['common_year', 'common_years']), 179 (LEAP_YEAR, ['leap_year', 'leap_years']), 180 (JULIAN_YEAR, ['julian_year', 'julian_years']), 181 (GREGORIAN_YEAR, ['gregorian_year', 'gregorian_years']), 182 (SIDEREAL_MONTH, ['sidereal_month', 'sidereal_months']), 183 (TROPICAL_MONTH, ['tropical_month', 'tropical_months']), 184 (FORTNIGHT, ['fortnight', 'fortnights']), 185 (WEEK, ['week', 'weeks']), 186 (JIFFY, ['jiffy', 'jiffies']), 187 (EON, ['eon', 'eons']), 188 (MONTH, ['month', 'months']), 189 (MILLISECOND, ['msec', 'msecs']), 190 (MICROSECOND, ['usec', 'usecs', 'microsecond', 'microseconds']), 191 ] 192 193 for seconds, valid in udunits: 194 if units in valid: 195 return seconds 196 197 raise ParserError('Invalid date units: %s' % units) 198 199 200def parse_date(date): 201 """ 202 Parses a date string and returns number of seconds from the EPOCH. 203 204 """ 205 # yyyy-mm-dd [hh:mm:ss[.s][ [+-]hh[:][mm]]] 206 p = re.compile( r'''(?P<year>\d{1,4}) # yyyy 207 - # 208 (?P<month>\d{1,2}) # mm or m 209 - # 210 (?P<day>\d{1,2}) # dd or d 211 # 212 (?: # [optional time and timezone] 213 (?:\s|T) # 214 (?P<hour>\d{1,2}) # hh or h 215 :? # 216 (?P<min>\d{1,2})? # mm or m 217 (?: # [optional seconds] 218 : # 219 (?P<sec>\d{1,2}) # ss or s 220 # 221 (?: # [optional decisecond] 222 \. # . 223 (?P<dsec>\d) # s 224 )? # 225 )? # 226 (?: # [optional timezone] 227 \s? # 228 ((?: # 229 (?P<ho>[+-]? # [+ or -] 230 \d{1,2}) # hh or h 231 :? # [:] 232 (?P<mo>\d{2})? # [mm] 233 ) # 234 | # or 235 (?:UTC)|(?:Z)) # UTC | Z 236 )? # 237 )? # 238 $ # EOL 239 ''', re.VERBOSE) 240 241 m = p.match(date) 242 if m: 243 c = m.groupdict(0) 244 for k, v in c.items(): 245 c[k] = int(v) 246 247 # get timezone offset in seconds 248 tz_offset = c['ho']*HOUR + c['mo']*MINUTE 249 250 # Some datasets use the date "0000-01-01 00:00:00" as an origin, even though 251 # the year zero does not exist in the Gregorian/Julian calendars. 252 if c['year'] == 0: 253 c['year'] = 1 254 year_offset = LEAP_YEAR 255 else: 256 year_offset = 0 257 258 origin = datetime(c['year'], c['month'], c['day'], c['hour'], c['min'], c['sec'], c['dsec'] * 100000) 259 dt = origin - EPOCH 260 return dt.days*DAY + dt.seconds + dt.microseconds*MICROSECOND - year_offset - tz_offset 261 262 raise ParserError('Invalid date: %s' % date) 263 264 265from_udunits = parse 266to_udunits = format 267 268 269def _test(): 270 import doctest 271 doctest.testmod() 272 273 274if __name__ == "__main__": 275 _test() 276