Index: third_party/logilab/common/date.py |
diff --git a/third_party/logilab/common/date.py b/third_party/logilab/common/date.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..b069a6fdcc24ba7a7b779e80d7ffaeb0a1e3223c |
--- /dev/null |
+++ b/third_party/logilab/common/date.py |
@@ -0,0 +1,327 @@ |
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
+# |
+# This file is part of logilab-common. |
+# |
+# logilab-common is free software: you can redistribute it and/or modify it under |
+# the terms of the GNU Lesser General Public License as published by the Free |
+# Software Foundation, either version 2.1 of the License, or (at your option) any |
+# later version. |
+# |
+# logilab-common is distributed in the hope that it will be useful, but WITHOUT |
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
+# details. |
+# |
+# You should have received a copy of the GNU Lesser General Public License along |
+# with logilab-common. If not, see <http://www.gnu.org/licenses/>. |
+"""Date manipulation helper functions.""" |
+from __future__ import division |
+ |
+__docformat__ = "restructuredtext en" |
+ |
+import math |
+import re |
+from locale import getpreferredencoding |
+from datetime import date, time, datetime, timedelta |
+from time import strptime as time_strptime |
+from calendar import monthrange, timegm |
+ |
+try: |
+ from mx.DateTime import RelativeDateTime, Date, DateTimeType |
+except ImportError: |
+ endOfMonth = None |
+ DateTimeType = datetime |
+else: |
+ endOfMonth = RelativeDateTime(months=1, day=-1) |
+ |
+# NOTE: should we implement a compatibility layer between date representations |
+# as we have in lgc.db ? |
+ |
+FRENCH_FIXED_HOLIDAYS = { |
+ 'jour_an': '%s-01-01', |
+ 'fete_travail': '%s-05-01', |
+ 'armistice1945': '%s-05-08', |
+ 'fete_nat': '%s-07-14', |
+ 'assomption': '%s-08-15', |
+ 'toussaint': '%s-11-01', |
+ 'armistice1918': '%s-11-11', |
+ 'noel': '%s-12-25', |
+ } |
+ |
+FRENCH_MOBILE_HOLIDAYS = { |
+ 'paques2004': '2004-04-12', |
+ 'ascension2004': '2004-05-20', |
+ 'pentecote2004': '2004-05-31', |
+ |
+ 'paques2005': '2005-03-28', |
+ 'ascension2005': '2005-05-05', |
+ 'pentecote2005': '2005-05-16', |
+ |
+ 'paques2006': '2006-04-17', |
+ 'ascension2006': '2006-05-25', |
+ 'pentecote2006': '2006-06-05', |
+ |
+ 'paques2007': '2007-04-09', |
+ 'ascension2007': '2007-05-17', |
+ 'pentecote2007': '2007-05-28', |
+ |
+ 'paques2008': '2008-03-24', |
+ 'ascension2008': '2008-05-01', |
+ 'pentecote2008': '2008-05-12', |
+ |
+ 'paques2009': '2009-04-13', |
+ 'ascension2009': '2009-05-21', |
+ 'pentecote2009': '2009-06-01', |
+ |
+ 'paques2010': '2010-04-05', |
+ 'ascension2010': '2010-05-13', |
+ 'pentecote2010': '2010-05-24', |
+ |
+ 'paques2011': '2011-04-25', |
+ 'ascension2011': '2011-06-02', |
+ 'pentecote2011': '2011-06-13', |
+ |
+ 'paques2012': '2012-04-09', |
+ 'ascension2012': '2012-05-17', |
+ 'pentecote2012': '2012-05-28', |
+ } |
+ |
+# XXX this implementation cries for multimethod dispatching |
+ |
+def get_step(dateobj, nbdays=1): |
+ # assume date is either a python datetime or a mx.DateTime object |
+ if isinstance(dateobj, date): |
+ return ONEDAY * nbdays |
+ return nbdays # mx.DateTime is ok with integers |
+ |
+def datefactory(year, month, day, sampledate): |
+ # assume date is either a python datetime or a mx.DateTime object |
+ if isinstance(sampledate, datetime): |
+ return datetime(year, month, day) |
+ if isinstance(sampledate, date): |
+ return date(year, month, day) |
+ return Date(year, month, day) |
+ |
+def weekday(dateobj): |
+ # assume date is either a python datetime or a mx.DateTime object |
+ if isinstance(dateobj, date): |
+ return dateobj.weekday() |
+ return dateobj.day_of_week |
+ |
+def str2date(datestr, sampledate): |
+ # NOTE: datetime.strptime is not an option until we drop py2.4 compat |
+ year, month, day = [int(chunk) for chunk in datestr.split('-')] |
+ return datefactory(year, month, day, sampledate) |
+ |
+def days_between(start, end): |
+ if isinstance(start, date): |
+ delta = end - start |
+ # datetime.timedelta.days is always an integer (floored) |
+ if delta.seconds: |
+ return delta.days + 1 |
+ return delta.days |
+ else: |
+ return int(math.ceil((end - start).days)) |
+ |
+def get_national_holidays(begin, end): |
+ """return french national days off between begin and end""" |
+ begin = datefactory(begin.year, begin.month, begin.day, begin) |
+ end = datefactory(end.year, end.month, end.day, end) |
+ holidays = [str2date(datestr, begin) |
+ for datestr in FRENCH_MOBILE_HOLIDAYS.values()] |
+ for year in xrange(begin.year, end.year+1): |
+ for datestr in FRENCH_FIXED_HOLIDAYS.values(): |
+ date = str2date(datestr % year, begin) |
+ if date not in holidays: |
+ holidays.append(date) |
+ return [day for day in holidays if begin <= day < end] |
+ |
+def add_days_worked(start, days): |
+ """adds date but try to only take days worked into account""" |
+ step = get_step(start) |
+ weeks, plus = divmod(days, 5) |
+ end = start + ((weeks * 7) + plus) * step |
+ if weekday(end) >= 5: # saturday or sunday |
+ end += (2 * step) |
+ end += len([x for x in get_national_holidays(start, end + step) |
+ if weekday(x) < 5]) * step |
+ if weekday(end) >= 5: # saturday or sunday |
+ end += (2 * step) |
+ return end |
+ |
+def nb_open_days(start, end): |
+ assert start <= end |
+ step = get_step(start) |
+ days = days_between(start, end) |
+ weeks, plus = divmod(days, 7) |
+ if weekday(start) > weekday(end): |
+ plus -= 2 |
+ elif weekday(end) == 6: |
+ plus -= 1 |
+ open_days = weeks * 5 + plus |
+ nb_week_holidays = len([x for x in get_national_holidays(start, end+step) |
+ if weekday(x) < 5 and x < end]) |
+ open_days -= nb_week_holidays |
+ if open_days < 0: |
+ return 0 |
+ return open_days |
+ |
+def date_range(begin, end, incday=None, incmonth=None): |
+ """yields each date between begin and end |
+ |
+ :param begin: the start date |
+ :param end: the end date |
+ :param incr: the step to use to iterate over dates. Default is |
+ one day. |
+ :param include: None (means no exclusion) or a function taking a |
+ date as parameter, and returning True if the date |
+ should be included. |
+ |
+ When using mx datetime, you should *NOT* use incmonth argument, use instead |
+ oneDay, oneHour, oneMinute, oneSecond, oneWeek or endOfMonth (to enumerate |
+ months) as `incday` argument |
+ """ |
+ assert not (incday and incmonth) |
+ begin = todate(begin) |
+ end = todate(end) |
+ if incmonth: |
+ while begin < end: |
+ begin = next_month(begin, incmonth) |
+ yield begin |
+ else: |
+ incr = get_step(begin, incday or 1) |
+ while begin < end: |
+ yield begin |
+ begin += incr |
+ |
+# makes py datetime usable ##################################################### |
+ |
+ONEDAY = timedelta(days=1) |
+ONEWEEK = timedelta(days=7) |
+ |
+try: |
+ strptime = datetime.strptime |
+except AttributeError: # py < 2.5 |
+ from time import strptime as time_strptime |
+ def strptime(value, format): |
+ return datetime(*time_strptime(value, format)[:6]) |
+ |
+def strptime_time(value, format='%H:%M'): |
+ return time(*time_strptime(value, format)[3:6]) |
+ |
+def todate(somedate): |
+ """return a date from a date (leaving unchanged) or a datetime""" |
+ if isinstance(somedate, datetime): |
+ return date(somedate.year, somedate.month, somedate.day) |
+ assert isinstance(somedate, (date, DateTimeType)), repr(somedate) |
+ return somedate |
+ |
+def totime(somedate): |
+ """return a time from a time (leaving unchanged), date or datetime""" |
+ # XXX mx compat |
+ if not isinstance(somedate, time): |
+ return time(somedate.hour, somedate.minute, somedate.second) |
+ assert isinstance(somedate, (time)), repr(somedate) |
+ return somedate |
+ |
+def todatetime(somedate): |
+ """return a date from a date (leaving unchanged) or a datetime""" |
+ # take care, datetime is a subclass of date |
+ if isinstance(somedate, datetime): |
+ return somedate |
+ assert isinstance(somedate, (date, DateTimeType)), repr(somedate) |
+ return datetime(somedate.year, somedate.month, somedate.day) |
+ |
+def datetime2ticks(somedate): |
+ return timegm(somedate.timetuple()) * 1000 |
+ |
+def ticks2datetime(ticks): |
+ miliseconds, microseconds = divmod(ticks, 1000) |
+ try: |
+ return datetime.fromtimestamp(miliseconds) |
+ except (ValueError, OverflowError): |
+ epoch = datetime.fromtimestamp(0) |
+ nb_days, seconds = divmod(int(miliseconds), 86400) |
+ delta = timedelta(nb_days, seconds=seconds, microseconds=microseconds) |
+ try: |
+ return epoch + delta |
+ except (ValueError, OverflowError): |
+ raise |
+ |
+def days_in_month(somedate): |
+ return monthrange(somedate.year, somedate.month)[1] |
+ |
+def days_in_year(somedate): |
+ feb = date(somedate.year, 2, 1) |
+ if days_in_month(feb) == 29: |
+ return 366 |
+ else: |
+ return 365 |
+ |
+def previous_month(somedate, nbmonth=1): |
+ while nbmonth: |
+ somedate = first_day(somedate) - ONEDAY |
+ nbmonth -= 1 |
+ return somedate |
+ |
+def next_month(somedate, nbmonth=1): |
+ while nbmonth: |
+ somedate = last_day(somedate) + ONEDAY |
+ nbmonth -= 1 |
+ return somedate |
+ |
+def first_day(somedate): |
+ return date(somedate.year, somedate.month, 1) |
+ |
+def last_day(somedate): |
+ return date(somedate.year, somedate.month, days_in_month(somedate)) |
+ |
+def ustrftime(somedate, fmt='%Y-%m-%d'): |
+ """like strftime, but returns a unicode string instead of an encoded |
+ string which' may be problematic with localized date. |
+ |
+ encoding is guessed by locale.getpreferredencoding() |
+ """ |
+ encoding = getpreferredencoding(do_setlocale=False) or 'UTF-8' |
+ try: |
+ return unicode(somedate.strftime(str(fmt)), encoding) |
+ except ValueError, exc: |
+ if somedate.year >= 1900: |
+ raise |
+ # datetime is not happy with dates before 1900 |
+ # we try to work around this, assuming a simple |
+ # format string |
+ fields = {'Y': somedate.year, |
+ 'm': somedate.month, |
+ 'd': somedate.day, |
+ } |
+ if isinstance(somedate, datetime): |
+ fields.update({'H': somedate.hour, |
+ 'M': somedate.minute, |
+ 'S': somedate.second}) |
+ fmt = re.sub('%([YmdHMS])', r'%(\1)02d', fmt) |
+ return unicode(fmt) % fields |
+ |
+def utcdatetime(dt): |
+ if dt.tzinfo is None: |
+ return dt |
+ return datetime(*dt.utctimetuple()[:7]) |
+ |
+def utctime(dt): |
+ if dt.tzinfo is None: |
+ return dt |
+ return (dt + dt.utcoffset() + dt.dst()).replace(tzinfo=None) |
+ |
+def datetime_to_seconds(date): |
+ """return the number of seconds since the begining of the day for that date |
+ """ |
+ return date.second+60*date.minute + 3600*date.hour |
+ |
+def timedelta_to_days(delta): |
+ """return the time delta as a number of seconds""" |
+ return delta.days + delta.seconds / (3600*24) |
+ |
+def timedelta_to_seconds(delta): |
+ """return the time delta as a fraction of days""" |
+ return delta.days*(3600*24) + delta.seconds |