times.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. # Copyright 2010 Matt Chaput. All rights reserved.
  2. #
  3. # Redistribution and use in source and binary forms, with or without
  4. # modification, are permitted provided that the following conditions are met:
  5. #
  6. # 1. Redistributions of source code must retain the above copyright notice,
  7. # this list of conditions and the following disclaimer.
  8. #
  9. # 2. Redistributions in binary form must reproduce the above copyright
  10. # notice, this list of conditions and the following disclaimer in the
  11. # documentation and/or other materials provided with the distribution.
  12. #
  13. # THIS SOFTWARE IS PROVIDED BY MATT CHAPUT ``AS IS'' AND ANY EXPRESS OR
  14. # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  15. # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
  16. # EVENT SHALL MATT CHAPUT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  17. # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  18. # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
  19. # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  20. # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  21. # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
  22. # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  23. #
  24. # The views and conclusions contained in the software and documentation are
  25. # those of the authors and should not be interpreted as representing official
  26. # policies, either expressed or implied, of Matt Chaput.
  27. import calendar
  28. import copy
  29. from datetime import date, datetime, timedelta
  30. from whoosh.compat import iteritems
  31. class TimeError(Exception):
  32. pass
  33. def relative_days(current_wday, wday, dir):
  34. """Returns the number of days (positive or negative) to the "next" or
  35. "last" of a certain weekday. ``current_wday`` and ``wday`` are numbers,
  36. i.e. 0 = monday, 1 = tuesday, 2 = wednesday, etc.
  37. >>> # Get the number of days to the next tuesday, if today is Sunday
  38. >>> relative_days(6, 1, 1)
  39. 2
  40. :param current_wday: the number of the current weekday.
  41. :param wday: the target weekday.
  42. :param dir: -1 for the "last" (past) weekday, 1 for the "next" (future)
  43. weekday.
  44. """
  45. if current_wday == wday:
  46. return 7 * dir
  47. if dir == 1:
  48. return (wday + 7 - current_wday) % 7
  49. else:
  50. return (current_wday + 7 - wday) % 7 * -1
  51. def timedelta_to_usecs(td):
  52. total = td.days * 86400000000 # Microseconds in a day
  53. total += td.seconds * 1000000 # Microseconds in a second
  54. total += td.microseconds
  55. return total
  56. def datetime_to_long(dt):
  57. """Converts a datetime object to a long integer representing the number
  58. of microseconds since ``datetime.min``.
  59. """
  60. return timedelta_to_usecs(dt.replace(tzinfo=None) - dt.min)
  61. def long_to_datetime(x):
  62. """Converts a long integer representing the number of microseconds since
  63. ``datetime.min`` to a datetime object.
  64. """
  65. days = x // 86400000000 # Microseconds in a day
  66. x -= days * 86400000000
  67. seconds = x // 1000000 # Microseconds in a second
  68. x -= seconds * 1000000
  69. return datetime.min + timedelta(days=days, seconds=seconds, microseconds=x)
  70. # Ambiguous datetime object
  71. class adatetime(object):
  72. """An "ambiguous" datetime object. This object acts like a
  73. ``datetime.datetime`` object but can have any of its attributes set to
  74. None, meaning unspecified.
  75. """
  76. units = frozenset(("year", "month", "day", "hour", "minute", "second",
  77. "microsecond"))
  78. def __init__(self, year=None, month=None, day=None, hour=None, minute=None,
  79. second=None, microsecond=None):
  80. if isinstance(year, datetime):
  81. dt = year
  82. self.year, self.month, self.day = dt.year, dt.month, dt.day
  83. self.hour, self.minute, self.second = dt.hour, dt.minute, dt.second
  84. self.microsecond = dt.microsecond
  85. else:
  86. if month is not None and (month < 1 or month > 12):
  87. raise TimeError("month must be in 1..12")
  88. if day is not None and day < 1:
  89. raise TimeError("day must be greater than 1")
  90. if (year is not None and month is not None and day is not None
  91. and day > calendar.monthrange(year, month)[1]):
  92. raise TimeError("day is out of range for month")
  93. if hour is not None and (hour < 0 or hour > 23):
  94. raise TimeError("hour must be in 0..23")
  95. if minute is not None and (minute < 0 or minute > 59):
  96. raise TimeError("minute must be in 0..59")
  97. if second is not None and (second < 0 or second > 59):
  98. raise TimeError("second must be in 0..59")
  99. if microsecond is not None and (microsecond < 0
  100. or microsecond > 999999):
  101. raise TimeError("microsecond must be in 0..999999")
  102. self.year, self.month, self.day = year, month, day
  103. self.hour, self.minute, self.second = hour, minute, second
  104. self.microsecond = microsecond
  105. def __eq__(self, other):
  106. if not other.__class__ is self.__class__:
  107. if not is_ambiguous(self) and isinstance(other, datetime):
  108. return fix(self) == other
  109. else:
  110. return False
  111. return all(getattr(self, unit) == getattr(other, unit)
  112. for unit in self.units)
  113. def __repr__(self):
  114. return "%s%r" % (self.__class__.__name__, self.tuple())
  115. def tuple(self):
  116. """Returns the attributes of the ``adatetime`` object as a tuple of
  117. ``(year, month, day, hour, minute, second, microsecond)``.
  118. """
  119. return (self.year, self.month, self.day, self.hour, self.minute,
  120. self.second, self.microsecond)
  121. def date(self):
  122. return date(self.year, self.month, self.day)
  123. def copy(self):
  124. return adatetime(year=self.year, month=self.month, day=self.day,
  125. hour=self.hour, minute=self.minute, second=self.second,
  126. microsecond=self.microsecond)
  127. def replace(self, **kwargs):
  128. """Returns a copy of this object with the attributes given as keyword
  129. arguments replaced.
  130. >>> adt = adatetime(year=2009, month=10, day=31)
  131. >>> adt.replace(year=2010)
  132. (2010, 10, 31, None, None, None, None)
  133. """
  134. newadatetime = self.copy()
  135. for key, value in iteritems(kwargs):
  136. if key in self.units:
  137. setattr(newadatetime, key, value)
  138. else:
  139. raise KeyError("Unknown argument %r" % key)
  140. return newadatetime
  141. def floor(self):
  142. """Returns a ``datetime`` version of this object with all unspecified
  143. (None) attributes replaced by their lowest values.
  144. This method raises an error if the ``adatetime`` object has no year.
  145. >>> adt = adatetime(year=2009, month=5)
  146. >>> adt.floor()
  147. datetime.datetime(2009, 5, 1, 0, 0, 0, 0)
  148. """
  149. y, m, d, h, mn, s, ms = (self.year, self.month, self.day, self.hour,
  150. self.minute, self.second, self.microsecond)
  151. if y is None:
  152. raise ValueError("Date has no year")
  153. if m is None:
  154. m = 1
  155. if d is None:
  156. d = 1
  157. if h is None:
  158. h = 0
  159. if mn is None:
  160. mn = 0
  161. if s is None:
  162. s = 0
  163. if ms is None:
  164. ms = 0
  165. return datetime(y, m, d, h, mn, s, ms)
  166. def ceil(self):
  167. """Returns a ``datetime`` version of this object with all unspecified
  168. (None) attributes replaced by their highest values.
  169. This method raises an error if the ``adatetime`` object has no year.
  170. >>> adt = adatetime(year=2009, month=5)
  171. >>> adt.floor()
  172. datetime.datetime(2009, 5, 30, 23, 59, 59, 999999)
  173. """
  174. y, m, d, h, mn, s, ms = (self.year, self.month, self.day, self.hour,
  175. self.minute, self.second, self.microsecond)
  176. if y is None:
  177. raise ValueError("Date has no year")
  178. if m is None:
  179. m = 12
  180. if d is None:
  181. d = calendar.monthrange(y, m)[1]
  182. if h is None:
  183. h = 23
  184. if mn is None:
  185. mn = 59
  186. if s is None:
  187. s = 59
  188. if ms is None:
  189. ms = 999999
  190. return datetime(y, m, d, h, mn, s, ms)
  191. def disambiguated(self, basedate):
  192. """Returns either a ``datetime`` or unambiguous ``timespan`` version
  193. of this object.
  194. Unless this ``adatetime`` object is full specified down to the
  195. microsecond, this method will return a timespan built from the "floor"
  196. and "ceil" of this object.
  197. This method raises an error if the ``adatetime`` object has no year.
  198. >>> adt = adatetime(year=2009, month=10, day=31)
  199. >>> adt.disambiguated()
  200. timespan(datetime(2009, 10, 31, 0, 0, 0, 0), datetime(2009, 10, 31, 23, 59 ,59, 999999)
  201. """
  202. dt = self
  203. if not is_ambiguous(dt):
  204. return fix(dt)
  205. return timespan(dt, dt).disambiguated(basedate)
  206. # Time span class
  207. class timespan(object):
  208. """A span of time between two ``datetime`` or ``adatetime`` objects.
  209. """
  210. def __init__(self, start, end):
  211. """
  212. :param start: a ``datetime`` or ``adatetime`` object representing the
  213. start of the time span.
  214. :param end: a ``datetime`` or ``adatetime`` object representing the
  215. end of the time span.
  216. """
  217. if not isinstance(start, (datetime, adatetime)):
  218. raise TimeError("%r is not a datetime object" % start)
  219. if not isinstance(end, (datetime, adatetime)):
  220. raise TimeError("%r is not a datetime object" % end)
  221. self.start = copy.copy(start)
  222. self.end = copy.copy(end)
  223. def __eq__(self, other):
  224. if not other.__class__ is self.__class__:
  225. return False
  226. return self.start == other.start and self.end == other.end
  227. def __repr__(self):
  228. return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end)
  229. def disambiguated(self, basedate, debug=0):
  230. """Returns an unambiguous version of this object.
  231. >>> start = adatetime(year=2009, month=2)
  232. >>> end = adatetime(year=2009, month=10)
  233. >>> ts = timespan(start, end)
  234. >>> ts
  235. timespan(adatetime(2009, 2, None, None, None, None, None), adatetime(2009, 10, None, None, None, None, None))
  236. >>> td.disambiguated(datetime.now())
  237. timespan(datetime(2009, 2, 28, 0, 0, 0, 0), datetime(2009, 10, 31, 23, 59 ,59, 999999)
  238. """
  239. #- If year is in start but not end, use basedate.year for end
  240. #-- If year is in start but not end, but startdate is > basedate,
  241. # use "next <monthname>" to get end month/year
  242. #- If year is in end but not start, copy year from end to start
  243. #- Support "next february", "last april", etc.
  244. start, end = copy.copy(self.start), copy.copy(self.end)
  245. start_year_was_amb = start.year is None
  246. end_year_was_amb = end.year is None
  247. if has_no_date(start) and has_no_date(end):
  248. # The start and end points are just times, so use the basedate
  249. # for the date information.
  250. by, bm, bd = basedate.year, basedate.month, basedate.day
  251. start = start.replace(year=by, month=bm, day=bd)
  252. end = end.replace(year=by, month=bm, day=bd)
  253. else:
  254. # If one side has a year and the other doesn't, the decision
  255. # of what year to assign to the ambiguous side is kind of
  256. # arbitrary. I've used a heuristic here based on how the range
  257. # "reads", but it may only be reasonable in English. And maybe
  258. # even just to me.
  259. if start.year is None and end.year is None:
  260. # No year on either side, use the basedate
  261. start.year = end.year = basedate.year
  262. elif start.year is None:
  263. # No year in the start, use the year from the end
  264. start.year = end.year
  265. elif end.year is None:
  266. end.year = max(start.year, basedate.year)
  267. if start.year == end.year:
  268. # Once again, if one side has a month and day but the other side
  269. # doesn't, the disambiguation is arbitrary. Does "3 am to 5 am
  270. # tomorrow" mean 3 AM today to 5 AM tomorrow, or 3am tomorrow to
  271. # 5 am tomorrow? What I picked is similar to the year: if the
  272. # end has a month+day and the start doesn't, copy the month+day
  273. # from the end to the start UNLESS that would make the end come
  274. # before the start on that day, in which case use the basedate
  275. # instead. If the start has a month+day and the end doesn't, use
  276. # the basedate.
  277. start_dm = not (start.month is None and start.day is None)
  278. end_dm = not (end.month is None and end.day is None)
  279. if end_dm and not start_dm:
  280. if start.floor().time() > end.ceil().time():
  281. start.month = basedate.month
  282. start.day = basedate.day
  283. else:
  284. start.month = end.month
  285. start.day = end.day
  286. elif start_dm and not end_dm:
  287. end.month = basedate.month
  288. end.day = basedate.day
  289. if floor(start).date() > ceil(end).date():
  290. # If the disambiguated dates are out of order:
  291. # - If no start year was given, reduce the start year to put the
  292. # start before the end
  293. # - If no end year was given, increase the end year to put the end
  294. # after the start
  295. # - If a year was specified for both, just swap the start and end
  296. if start_year_was_amb:
  297. start.year = end.year - 1
  298. elif end_year_was_amb:
  299. end.year = start.year + 1
  300. else:
  301. start, end = end, start
  302. start = floor(start)
  303. end = ceil(end)
  304. if start.date() == end.date() and start.time() > end.time():
  305. # If the start and end are on the same day, but the start time
  306. # is after the end time, move the end time to the next day
  307. end += timedelta(days=1)
  308. return timespan(start, end)
  309. # Functions for working with datetime/adatetime objects
  310. def floor(at):
  311. if isinstance(at, datetime):
  312. return at
  313. return at.floor()
  314. def ceil(at):
  315. if isinstance(at, datetime):
  316. return at
  317. return at.ceil()
  318. def fill_in(at, basedate, units=adatetime.units):
  319. """Returns a copy of ``at`` with any unspecified (None) units filled in
  320. with values from ``basedate``.
  321. """
  322. if isinstance(at, datetime):
  323. return at
  324. args = {}
  325. for unit in units:
  326. v = getattr(at, unit)
  327. if v is None:
  328. v = getattr(basedate, unit)
  329. args[unit] = v
  330. return fix(adatetime(**args))
  331. def has_no_date(at):
  332. """Returns True if the given object is an ``adatetime`` where ``year``,
  333. ``month``, and ``day`` are all None.
  334. """
  335. if isinstance(at, datetime):
  336. return False
  337. return at.year is None and at.month is None and at.day is None
  338. def has_no_time(at):
  339. """Returns True if the given object is an ``adatetime`` where ``hour``,
  340. ``minute``, ``second`` and ``microsecond`` are all None.
  341. """
  342. if isinstance(at, datetime):
  343. return False
  344. return (at.hour is None and at.minute is None and at.second is None
  345. and at.microsecond is None)
  346. def is_ambiguous(at):
  347. """Returns True if the given object is an ``adatetime`` with any of its
  348. attributes equal to None.
  349. """
  350. if isinstance(at, datetime):
  351. return False
  352. return any((getattr(at, attr) is None) for attr in adatetime.units)
  353. def is_void(at):
  354. """Returns True if the given object is an ``adatetime`` with all of its
  355. attributes equal to None.
  356. """
  357. if isinstance(at, datetime):
  358. return False
  359. return all((getattr(at, attr) is None) for attr in adatetime.units)
  360. def fix(at):
  361. """If the given object is an ``adatetime`` that is unambiguous (because
  362. all its attributes are specified, that is, not equal to None), returns a
  363. ``datetime`` version of it. Otherwise returns the ``adatetime`` object
  364. unchanged.
  365. """
  366. if is_ambiguous(at) or isinstance(at, datetime):
  367. return at
  368. return datetime(year=at.year, month=at.month, day=at.day, hour=at.hour,
  369. minute=at.minute, second=at.second,
  370. microsecond=at.microsecond)