tempora.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. # -*- coding: UTF-8 -*-
  2. "Objects and routines pertaining to date and time (tempora)"
  3. from __future__ import division, unicode_literals
  4. import datetime
  5. import time
  6. import re
  7. import numbers
  8. import functools
  9. import six
  10. class Parser(object):
  11. """
  12. Datetime parser: parses a date-time string using multiple possible
  13. formats.
  14. >>> p = Parser(('%H%M', '%H:%M'))
  15. >>> tuple(p.parse('1319'))
  16. (1900, 1, 1, 13, 19, 0, 0, 1, -1)
  17. >>> dateParser = Parser(('%m/%d/%Y', '%Y-%m-%d', '%d-%b-%Y'))
  18. >>> tuple(dateParser.parse('2003-12-20'))
  19. (2003, 12, 20, 0, 0, 0, 5, 354, -1)
  20. >>> tuple(dateParser.parse('16-Dec-1994'))
  21. (1994, 12, 16, 0, 0, 0, 4, 350, -1)
  22. >>> tuple(dateParser.parse('5/19/2003'))
  23. (2003, 5, 19, 0, 0, 0, 0, 139, -1)
  24. >>> dtParser = Parser(('%Y-%m-%d %H:%M:%S', '%a %b %d %H:%M:%S %Y'))
  25. >>> tuple(dtParser.parse('2003-12-20 19:13:26'))
  26. (2003, 12, 20, 19, 13, 26, 5, 354, -1)
  27. >>> tuple(dtParser.parse('Tue Jan 20 16:19:33 2004'))
  28. (2004, 1, 20, 16, 19, 33, 1, 20, -1)
  29. Be forewarned, a ValueError will be raised if more than one format
  30. matches:
  31. >>> Parser(('%H%M', '%H%M%S')).parse('732')
  32. Traceback (most recent call last):
  33. ...
  34. ValueError: More than one format string matched target 732.
  35. """
  36. formats = ('%m/%d/%Y', '%m/%d/%y', '%Y-%m-%d', '%d-%b-%Y', '%d-%b-%y')
  37. "some common default formats"
  38. def __init__(self, formats = None):
  39. if formats:
  40. self.formats = formats
  41. def parse(self, target):
  42. self.target = target
  43. results = tuple(filter(None, map(self._parse, self.formats)))
  44. del self.target
  45. if not results:
  46. raise ValueError("No format strings matched the target %s."
  47. % target)
  48. if not len(results) == 1:
  49. raise ValueError("More than one format string matched target %s."
  50. % target)
  51. return results[0]
  52. def _parse(self, format):
  53. try:
  54. result = time.strptime(self.target, format)
  55. except ValueError:
  56. result = False
  57. return result
  58. # some useful constants
  59. osc_per_year = 290091329207984000
  60. """
  61. mean vernal equinox year expressed in oscillations of atomic cesium at the
  62. year 2000 (see http://webexhibits.org/calendars/timeline.html for more info).
  63. """
  64. osc_per_second = 9192631770
  65. seconds_per_second = 1
  66. seconds_per_year = 31556940
  67. seconds_per_minute = 60
  68. minutes_per_hour = 60
  69. hours_per_day = 24
  70. seconds_per_hour = seconds_per_minute * minutes_per_hour
  71. seconds_per_day = seconds_per_hour * hours_per_day
  72. days_per_year = seconds_per_year / seconds_per_day
  73. thirty_days = datetime.timedelta(days=30)
  74. # these values provide useful averages
  75. six_months = datetime.timedelta(days=days_per_year/2)
  76. seconds_per_month = seconds_per_year/12
  77. hours_per_month=hours_per_day*days_per_year/12
  78. def strftime(fmt, t):
  79. """A class to replace the strftime in datetime package or time module.
  80. Identical to strftime behavior in those modules except supports any
  81. year.
  82. Also supports datetime.datetime times.
  83. Also supports milliseconds using %s
  84. Also supports microseconds using %u"""
  85. if isinstance(t, (time.struct_time, tuple)):
  86. t = datetime.datetime(*t[:6])
  87. assert isinstance(t, (datetime.datetime, datetime.time, datetime.date))
  88. try:
  89. year = t.year
  90. if year < 1900: t = t.replace(year = 1900)
  91. except AttributeError:
  92. year = 1900
  93. subs = (
  94. ('%Y', '%04d' % year),
  95. ('%y', '%02d' % (year % 100)),
  96. ('%s', '%03d' % (t.microsecond // 1000)),
  97. ('%u', '%03d' % (t.microsecond % 1000))
  98. )
  99. doSub = lambda s, sub: s.replace(*sub)
  100. doSubs = lambda s: functools.reduce(doSub, subs, s)
  101. fmt = '%%'.join(map(doSubs, fmt.split('%%')))
  102. return t.strftime(fmt)
  103. def strptime(s, fmt, tzinfo = None):
  104. """
  105. A function to replace strptime in the time module. Should behave
  106. identically to the strptime function except it returns a datetime.datetime
  107. object instead of a time.struct_time object.
  108. Also takes an optional tzinfo parameter which is a time zone info object.
  109. """
  110. res = time.strptime(s, fmt)
  111. return datetime.datetime(tzinfo = tzinfo, *res[:6])
  112. class DatetimeConstructor(object):
  113. """
  114. >>> cd = DatetimeConstructor.construct_datetime
  115. >>> cd(datetime.datetime(2011,1,1))
  116. datetime.datetime(2011, 1, 1, 0, 0)
  117. """
  118. @classmethod
  119. def construct_datetime(cls, *args, **kwargs):
  120. """Construct a datetime.datetime from a number of different time
  121. types found in python and pythonwin"""
  122. if len(args) == 1:
  123. arg = args[0]
  124. method = cls.__get_dt_constructor(type(arg).__module__,
  125. type(arg).__name__)
  126. result = method(arg)
  127. try:
  128. result = result.replace(tzinfo = kwargs.pop('tzinfo'))
  129. except KeyError:
  130. pass
  131. if kwargs:
  132. first_key = kwargs.keys()[0]
  133. raise TypeError("{first_key} is an invalid keyword "
  134. "argument for this function.".format(**locals()))
  135. else:
  136. result = datetime.datetime(*args, **kwargs)
  137. return result
  138. @classmethod
  139. def __get_dt_constructor(cls, moduleName, name):
  140. try:
  141. method_name = '__dt_from_{moduleName}_{name}__'.format(**locals())
  142. return getattr(cls, method_name)
  143. except AttributeError:
  144. raise TypeError("No way to construct datetime.datetime from "
  145. "{moduleName}.{name}".format(**locals()))
  146. @staticmethod
  147. def __dt_from_datetime_datetime__(source):
  148. dtattrs = ('year', 'month', 'day', 'hour', 'minute', 'second',
  149. 'microsecond', 'tzinfo')
  150. attrs = map(lambda a: getattr(source, a), dtattrs)
  151. return datetime.datetime(*attrs)
  152. @staticmethod
  153. def __dt_from___builtin___time__(pyt):
  154. "Construct a datetime.datetime from a pythonwin time"
  155. fmtString = '%Y-%m-%d %H:%M:%S'
  156. result = strptime(pyt.Format(fmtString), fmtString)
  157. # get milliseconds and microseconds. The only way to do this is
  158. # to use the __float__ attribute of the time, which is in days.
  159. microseconds_per_day = seconds_per_day * 1000000
  160. microseconds = float(pyt) * microseconds_per_day
  161. microsecond = int(microseconds % 1000000)
  162. result = result.replace(microsecond = microsecond)
  163. return result
  164. @staticmethod
  165. def __dt_from_timestamp__(timestamp):
  166. return datetime.datetime.utcfromtimestamp(timestamp)
  167. __dt_from___builtin___float__ = __dt_from_timestamp__
  168. __dt_from___builtin___long__ = __dt_from_timestamp__
  169. __dt_from___builtin___int__ = __dt_from_timestamp__
  170. @staticmethod
  171. def __dt_from_time_struct_time__(s):
  172. return datetime.datetime(*s[:6])
  173. def datetime_mod(dt, period, start = None):
  174. """
  175. Find the time which is the specified date/time truncated to the time delta
  176. relative to the start date/time.
  177. By default, the start time is midnight of the same day as the specified
  178. date/time.
  179. >>> datetime_mod(datetime.datetime(2004, 1, 2, 3),
  180. ... datetime.timedelta(days = 1.5),
  181. ... start = datetime.datetime(2004, 1, 1))
  182. datetime.datetime(2004, 1, 1, 0, 0)
  183. >>> datetime_mod(datetime.datetime(2004, 1, 2, 13),
  184. ... datetime.timedelta(days = 1.5),
  185. ... start = datetime.datetime(2004, 1, 1))
  186. datetime.datetime(2004, 1, 2, 12, 0)
  187. >>> datetime_mod(datetime.datetime(2004, 1, 2, 13),
  188. ... datetime.timedelta(days = 7),
  189. ... start = datetime.datetime(2004, 1, 1))
  190. datetime.datetime(2004, 1, 1, 0, 0)
  191. >>> datetime_mod(datetime.datetime(2004, 1, 10, 13),
  192. ... datetime.timedelta(days = 7),
  193. ... start = datetime.datetime(2004, 1, 1))
  194. datetime.datetime(2004, 1, 8, 0, 0)
  195. """
  196. if start is None:
  197. # use midnight of the same day
  198. start = datetime.datetime.combine(dt.date(), datetime.time())
  199. # calculate the difference between the specified time and the start date.
  200. delta = dt - start
  201. # now aggregate the delta and the period into microseconds
  202. # Use microseconds because that's the highest precision of these time
  203. # pieces. Also, using microseconds ensures perfect precision (no floating
  204. # point errors).
  205. get_time_delta_microseconds = lambda td: (
  206. (td.days * seconds_per_day + td.seconds) * 1000000 + td.microseconds
  207. )
  208. delta, period = map(get_time_delta_microseconds, (delta, period))
  209. offset = datetime.timedelta(microseconds = delta % period)
  210. # the result is the original specified time minus the offset
  211. result = dt - offset
  212. return result
  213. def datetime_round(dt, period, start = None):
  214. """
  215. Find the nearest even period for the specified date/time.
  216. >>> datetime_round(datetime.datetime(2004, 11, 13, 8, 11, 13),
  217. ... datetime.timedelta(hours = 1))
  218. datetime.datetime(2004, 11, 13, 8, 0)
  219. >>> datetime_round(datetime.datetime(2004, 11, 13, 8, 31, 13),
  220. ... datetime.timedelta(hours = 1))
  221. datetime.datetime(2004, 11, 13, 9, 0)
  222. >>> datetime_round(datetime.datetime(2004, 11, 13, 8, 30),
  223. ... datetime.timedelta(hours = 1))
  224. datetime.datetime(2004, 11, 13, 9, 0)
  225. """
  226. result = datetime_mod(dt, period, start)
  227. if abs(dt - result) >= period // 2:
  228. result += period
  229. return result
  230. def get_nearest_year_for_day(day):
  231. """
  232. Returns the nearest year to now inferred from a Julian date.
  233. """
  234. now = time.gmtime()
  235. result = now.tm_year
  236. # if the day is far greater than today, it must be from last year
  237. if day - now.tm_yday > 365//2:
  238. result -= 1
  239. # if the day is far less than today, it must be for next year.
  240. if now.tm_yday - day > 365//2:
  241. result += 1
  242. return result
  243. def gregorian_date(year, julian_day):
  244. """
  245. Gregorian Date is defined as a year and a julian day (1-based
  246. index into the days of the year).
  247. >>> gregorian_date(2007, 15)
  248. datetime.date(2007, 1, 15)
  249. """
  250. result = datetime.date(year, 1, 1)
  251. result += datetime.timedelta(days = julian_day - 1)
  252. return result
  253. def get_period_seconds(period):
  254. """
  255. return the number of seconds in the specified period
  256. >>> get_period_seconds('day')
  257. 86400
  258. >>> get_period_seconds(86400)
  259. 86400
  260. >>> get_period_seconds(datetime.timedelta(hours=24))
  261. 86400
  262. >>> get_period_seconds('day + os.system("rm -Rf *")')
  263. Traceback (most recent call last):
  264. ...
  265. ValueError: period not in (second, minute, hour, day, month, year)
  266. """
  267. if isinstance(period, six.string_types):
  268. try:
  269. name = 'seconds_per_' + period.lower()
  270. result = globals()[name]
  271. except KeyError:
  272. raise ValueError("period not in (second, minute, hour, day, "
  273. "month, year)")
  274. elif isinstance(period, numbers.Number):
  275. result = period
  276. elif isinstance(period, datetime.timedelta):
  277. result = period.days * get_period_seconds('day') + period.seconds
  278. else:
  279. raise TypeError('period must be a string or integer')
  280. return result
  281. def get_date_format_string(period):
  282. """
  283. For a given period (e.g. 'month', 'day', or some numeric interval
  284. such as 3600 (in secs)), return the format string that can be
  285. used with strftime to format that time to specify the times
  286. across that interval, but no more detailed.
  287. For example,
  288. >>> get_date_format_string('month')
  289. '%Y-%m'
  290. >>> get_date_format_string(3600)
  291. '%Y-%m-%d %H'
  292. >>> get_date_format_string('hour')
  293. '%Y-%m-%d %H'
  294. >>> get_date_format_string(None)
  295. Traceback (most recent call last):
  296. ...
  297. TypeError: period must be a string or integer
  298. >>> get_date_format_string('garbage')
  299. Traceback (most recent call last):
  300. ...
  301. ValueError: period not in (second, minute, hour, day, month, year)
  302. """
  303. # handle the special case of 'month' which doesn't have
  304. # a static interval in seconds
  305. if isinstance(period, six.string_types) and period.lower() == 'month':
  306. return '%Y-%m'
  307. file_period_secs = get_period_seconds(period)
  308. format_pieces = ('%Y', '-%m-%d', ' %H', '-%M', '-%S')
  309. intervals = (
  310. seconds_per_year,
  311. seconds_per_day,
  312. seconds_per_hour,
  313. seconds_per_minute,
  314. 1, # seconds_per_second
  315. )
  316. mods = list(map(lambda interval: file_period_secs % interval, intervals))
  317. format_pieces = format_pieces[: mods.index(0) + 1]
  318. return ''.join(format_pieces)
  319. def divide_timedelta_float(td, divisor):
  320. """
  321. Divide a timedelta by a float value
  322. >>> one_day = datetime.timedelta(days=1)
  323. >>> half_day = datetime.timedelta(days=.5)
  324. >>> divide_timedelta_float(one_day, 2.0) == half_day
  325. True
  326. >>> divide_timedelta_float(one_day, 2) == half_day
  327. True
  328. """
  329. # td is comprised of days, seconds, microseconds
  330. dsm = [getattr(td, attr) for attr in ('days', 'seconds', 'microseconds')]
  331. dsm = map(lambda elem: elem/divisor, dsm)
  332. return datetime.timedelta(*dsm)
  333. def calculate_prorated_values():
  334. """
  335. A utility function to prompt for a rate (a string in units per
  336. unit time), and return that same rate for various time periods.
  337. """
  338. rate = six.moves.input("Enter the rate (3/hour, 50/month)> ")
  339. res = re.match('(?P<value>[\d.]+)/(?P<period>\w+)$', rate).groupdict()
  340. value = float(res['value'])
  341. value_per_second = value / get_period_seconds(res['period'])
  342. for period in ('minute', 'hour', 'day', 'month', 'year'):
  343. period_value = value_per_second * get_period_seconds(period)
  344. print("per {period}: {period_value}".format(**locals()))
  345. def parse_timedelta(str):
  346. """
  347. Take a string representing a span of time and parse it to a time delta.
  348. Accepts any string of comma-separated numbers each with a unit indicator.
  349. >>> parse_timedelta('1 day')
  350. datetime.timedelta(1)
  351. >>> parse_timedelta('1 day, 30 seconds')
  352. datetime.timedelta(1, 30)
  353. >>> parse_timedelta('47.32 days, 20 minutes, 15.4 milliseconds')
  354. datetime.timedelta(47, 28848, 15400)
  355. """
  356. deltas = (_parse_timedelta_part(part.strip()) for part in str.split(','))
  357. return sum(deltas, datetime.timedelta())
  358. def _parse_timedelta_part(part):
  359. match = re.match('(?P<value>[\d.]+) (?P<unit>\w+)', part)
  360. if not match:
  361. msg = "Unable to parse {part!r} as a time delta".format(**locals())
  362. raise ValueError(msg)
  363. unit = match.group('unit')
  364. if not unit.endswith('s'):
  365. unit += 's'
  366. return datetime.timedelta(**{unit: float(match.group('value'))})
  367. def divide_timedelta(td1, td2):
  368. """
  369. Get the ratio of two timedeltas
  370. >>> one_day = datetime.timedelta(days=1)
  371. >>> one_hour = datetime.timedelta(hours=1)
  372. >>> divide_timedelta(one_hour, one_day) == 1 / 24
  373. True
  374. """
  375. try:
  376. return td1 / td2
  377. except TypeError:
  378. # Python 3.2 gets division
  379. # http://bugs.python.org/issue2706
  380. return td1.total_seconds() / td2.total_seconds()
  381. def date_range(start=None, stop=None, step=None):
  382. """
  383. Much like the built-in function range, but works with dates
  384. >>> range_items = date_range(
  385. ... datetime.datetime(2005,12,21),
  386. ... datetime.datetime(2005,12,25),
  387. ... )
  388. >>> my_range = tuple(range_items)
  389. >>> datetime.datetime(2005,12,21) in my_range
  390. True
  391. >>> datetime.datetime(2005,12,22) in my_range
  392. True
  393. >>> datetime.datetime(2005,12,25) in my_range
  394. False
  395. """
  396. if step is None: step = datetime.timedelta(days=1)
  397. if start is None: start = datetime.datetime.now()
  398. while start < stop:
  399. yield start
  400. start += step