date.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  2. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
  3. #
  4. # This file is part of logilab-common.
  5. #
  6. # logilab-common is free software: you can redistribute it and/or modify it under
  7. # the terms of the GNU Lesser General Public License as published by the Free
  8. # Software Foundation, either version 2.1 of the License, or (at your option) any
  9. # later version.
  10. #
  11. # logilab-common is distributed in the hope that it will be useful, but WITHOUT
  12. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  13. # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  14. # details.
  15. #
  16. # You should have received a copy of the GNU Lesser General Public License along
  17. # with logilab-common. If not, see <http://www.gnu.org/licenses/>.
  18. """Date manipulation helper functions."""
  19. from __future__ import division
  20. __docformat__ = "restructuredtext en"
  21. import math
  22. import re
  23. from locale import getpreferredencoding
  24. from datetime import date, time, datetime, timedelta
  25. from time import strptime as time_strptime
  26. from calendar import monthrange, timegm
  27. try:
  28. from mx.DateTime import RelativeDateTime, Date, DateTimeType
  29. except ImportError:
  30. endOfMonth = None
  31. DateTimeType = datetime
  32. else:
  33. endOfMonth = RelativeDateTime(months=1, day=-1)
  34. # NOTE: should we implement a compatibility layer between date representations
  35. # as we have in lgc.db ?
  36. FRENCH_FIXED_HOLIDAYS = {
  37. 'jour_an': '%s-01-01',
  38. 'fete_travail': '%s-05-01',
  39. 'armistice1945': '%s-05-08',
  40. 'fete_nat': '%s-07-14',
  41. 'assomption': '%s-08-15',
  42. 'toussaint': '%s-11-01',
  43. 'armistice1918': '%s-11-11',
  44. 'noel': '%s-12-25',
  45. }
  46. FRENCH_MOBILE_HOLIDAYS = {
  47. 'paques2004': '2004-04-12',
  48. 'ascension2004': '2004-05-20',
  49. 'pentecote2004': '2004-05-31',
  50. 'paques2005': '2005-03-28',
  51. 'ascension2005': '2005-05-05',
  52. 'pentecote2005': '2005-05-16',
  53. 'paques2006': '2006-04-17',
  54. 'ascension2006': '2006-05-25',
  55. 'pentecote2006': '2006-06-05',
  56. 'paques2007': '2007-04-09',
  57. 'ascension2007': '2007-05-17',
  58. 'pentecote2007': '2007-05-28',
  59. 'paques2008': '2008-03-24',
  60. 'ascension2008': '2008-05-01',
  61. 'pentecote2008': '2008-05-12',
  62. 'paques2009': '2009-04-13',
  63. 'ascension2009': '2009-05-21',
  64. 'pentecote2009': '2009-06-01',
  65. 'paques2010': '2010-04-05',
  66. 'ascension2010': '2010-05-13',
  67. 'pentecote2010': '2010-05-24',
  68. 'paques2011': '2011-04-25',
  69. 'ascension2011': '2011-06-02',
  70. 'pentecote2011': '2011-06-13',
  71. 'paques2012': '2012-04-09',
  72. 'ascension2012': '2012-05-17',
  73. 'pentecote2012': '2012-05-28',
  74. }
  75. # XXX this implementation cries for multimethod dispatching
  76. def get_step(dateobj, nbdays=1):
  77. # assume date is either a python datetime or a mx.DateTime object
  78. if isinstance(dateobj, date):
  79. return ONEDAY * nbdays
  80. return nbdays # mx.DateTime is ok with integers
  81. def datefactory(year, month, day, sampledate):
  82. # assume date is either a python datetime or a mx.DateTime object
  83. if isinstance(sampledate, datetime):
  84. return datetime(year, month, day)
  85. if isinstance(sampledate, date):
  86. return date(year, month, day)
  87. return Date(year, month, day)
  88. def weekday(dateobj):
  89. # assume date is either a python datetime or a mx.DateTime object
  90. if isinstance(dateobj, date):
  91. return dateobj.weekday()
  92. return dateobj.day_of_week
  93. def str2date(datestr, sampledate):
  94. # NOTE: datetime.strptime is not an option until we drop py2.4 compat
  95. year, month, day = [int(chunk) for chunk in datestr.split('-')]
  96. return datefactory(year, month, day, sampledate)
  97. def days_between(start, end):
  98. if isinstance(start, date):
  99. delta = end - start
  100. # datetime.timedelta.days is always an integer (floored)
  101. if delta.seconds:
  102. return delta.days + 1
  103. return delta.days
  104. else:
  105. return int(math.ceil((end - start).days))
  106. def get_national_holidays(begin, end):
  107. """return french national days off between begin and end"""
  108. begin = datefactory(begin.year, begin.month, begin.day, begin)
  109. end = datefactory(end.year, end.month, end.day, end)
  110. holidays = [str2date(datestr, begin)
  111. for datestr in FRENCH_MOBILE_HOLIDAYS.values()]
  112. for year in xrange(begin.year, end.year+1):
  113. for datestr in FRENCH_FIXED_HOLIDAYS.values():
  114. date = str2date(datestr % year, begin)
  115. if date not in holidays:
  116. holidays.append(date)
  117. return [day for day in holidays if begin <= day < end]
  118. def add_days_worked(start, days):
  119. """adds date but try to only take days worked into account"""
  120. step = get_step(start)
  121. weeks, plus = divmod(days, 5)
  122. end = start + ((weeks * 7) + plus) * step
  123. if weekday(end) >= 5: # saturday or sunday
  124. end += (2 * step)
  125. end += len([x for x in get_national_holidays(start, end + step)
  126. if weekday(x) < 5]) * step
  127. if weekday(end) >= 5: # saturday or sunday
  128. end += (2 * step)
  129. return end
  130. def nb_open_days(start, end):
  131. assert start <= end
  132. step = get_step(start)
  133. days = days_between(start, end)
  134. weeks, plus = divmod(days, 7)
  135. if weekday(start) > weekday(end):
  136. plus -= 2
  137. elif weekday(end) == 6:
  138. plus -= 1
  139. open_days = weeks * 5 + plus
  140. nb_week_holidays = len([x for x in get_national_holidays(start, end+step)
  141. if weekday(x) < 5 and x < end])
  142. open_days -= nb_week_holidays
  143. if open_days < 0:
  144. return 0
  145. return open_days
  146. def date_range(begin, end, incday=None, incmonth=None):
  147. """yields each date between begin and end
  148. :param begin: the start date
  149. :param end: the end date
  150. :param incr: the step to use to iterate over dates. Default is
  151. one day.
  152. :param include: None (means no exclusion) or a function taking a
  153. date as parameter, and returning True if the date
  154. should be included.
  155. When using mx datetime, you should *NOT* use incmonth argument, use instead
  156. oneDay, oneHour, oneMinute, oneSecond, oneWeek or endOfMonth (to enumerate
  157. months) as `incday` argument
  158. """
  159. assert not (incday and incmonth)
  160. begin = todate(begin)
  161. end = todate(end)
  162. if incmonth:
  163. while begin < end:
  164. begin = next_month(begin, incmonth)
  165. yield begin
  166. else:
  167. incr = get_step(begin, incday or 1)
  168. while begin < end:
  169. yield begin
  170. begin += incr
  171. # makes py datetime usable #####################################################
  172. ONEDAY = timedelta(days=1)
  173. ONEWEEK = timedelta(days=7)
  174. try:
  175. strptime = datetime.strptime
  176. except AttributeError: # py < 2.5
  177. from time import strptime as time_strptime
  178. def strptime(value, format):
  179. return datetime(*time_strptime(value, format)[:6])
  180. def strptime_time(value, format='%H:%M'):
  181. return time(*time_strptime(value, format)[3:6])
  182. def todate(somedate):
  183. """return a date from a date (leaving unchanged) or a datetime"""
  184. if isinstance(somedate, datetime):
  185. return date(somedate.year, somedate.month, somedate.day)
  186. assert isinstance(somedate, (date, DateTimeType)), repr(somedate)
  187. return somedate
  188. def totime(somedate):
  189. """return a time from a time (leaving unchanged), date or datetime"""
  190. # XXX mx compat
  191. if not isinstance(somedate, time):
  192. return time(somedate.hour, somedate.minute, somedate.second)
  193. assert isinstance(somedate, (time)), repr(somedate)
  194. return somedate
  195. def todatetime(somedate):
  196. """return a date from a date (leaving unchanged) or a datetime"""
  197. # take care, datetime is a subclass of date
  198. if isinstance(somedate, datetime):
  199. return somedate
  200. assert isinstance(somedate, (date, DateTimeType)), repr(somedate)
  201. return datetime(somedate.year, somedate.month, somedate.day)
  202. def datetime2ticks(somedate):
  203. return timegm(somedate.timetuple()) * 1000
  204. def ticks2datetime(ticks):
  205. miliseconds, microseconds = divmod(ticks, 1000)
  206. try:
  207. return datetime.fromtimestamp(miliseconds)
  208. except (ValueError, OverflowError):
  209. epoch = datetime.fromtimestamp(0)
  210. nb_days, seconds = divmod(int(miliseconds), 86400)
  211. delta = timedelta(nb_days, seconds=seconds, microseconds=microseconds)
  212. try:
  213. return epoch + delta
  214. except (ValueError, OverflowError):
  215. raise
  216. def days_in_month(somedate):
  217. return monthrange(somedate.year, somedate.month)[1]
  218. def days_in_year(somedate):
  219. feb = date(somedate.year, 2, 1)
  220. if days_in_month(feb) == 29:
  221. return 366
  222. else:
  223. return 365
  224. def previous_month(somedate, nbmonth=1):
  225. while nbmonth:
  226. somedate = first_day(somedate) - ONEDAY
  227. nbmonth -= 1
  228. return somedate
  229. def next_month(somedate, nbmonth=1):
  230. while nbmonth:
  231. somedate = last_day(somedate) + ONEDAY
  232. nbmonth -= 1
  233. return somedate
  234. def first_day(somedate):
  235. return date(somedate.year, somedate.month, 1)
  236. def last_day(somedate):
  237. return date(somedate.year, somedate.month, days_in_month(somedate))
  238. def ustrftime(somedate, fmt='%Y-%m-%d'):
  239. """like strftime, but returns a unicode string instead of an encoded
  240. string which' may be problematic with localized date.
  241. encoding is guessed by locale.getpreferredencoding()
  242. """
  243. encoding = getpreferredencoding(do_setlocale=False) or 'UTF-8'
  244. try:
  245. return unicode(somedate.strftime(str(fmt)), encoding)
  246. except ValueError, exc:
  247. if somedate.year >= 1900:
  248. raise
  249. # datetime is not happy with dates before 1900
  250. # we try to work around this, assuming a simple
  251. # format string
  252. fields = {'Y': somedate.year,
  253. 'm': somedate.month,
  254. 'd': somedate.day,
  255. }
  256. if isinstance(somedate, datetime):
  257. fields.update({'H': somedate.hour,
  258. 'M': somedate.minute,
  259. 'S': somedate.second})
  260. fmt = re.sub('%([YmdHMS])', r'%(\1)02d', fmt)
  261. return unicode(fmt) % fields
  262. def utcdatetime(dt):
  263. if dt.tzinfo is None:
  264. return dt
  265. return datetime(*dt.utctimetuple()[:7])
  266. def utctime(dt):
  267. if dt.tzinfo is None:
  268. return dt
  269. return (dt + dt.utcoffset() + dt.dst()).replace(tzinfo=None)
  270. def datetime_to_seconds(date):
  271. """return the number of seconds since the begining of the day for that date
  272. """
  273. return date.second+60*date.minute + 3600*date.hour
  274. def timedelta_to_days(delta):
  275. """return the time delta as a number of seconds"""
  276. return delta.days + delta.seconds / (3600*24)
  277. def timedelta_to_seconds(delta):
  278. """return the time delta as a fraction of days"""
  279. return delta.days*(3600*24) + delta.seconds