diff options
Diffstat (limited to 'date.py')
-rw-r--r-- | date.py | 225 |
1 files changed, 142 insertions, 83 deletions
@@ -1,6 +1,6 @@ """Date manipulation helper functions. -:copyright: 2006-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +:copyright: 2006-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr :license: General Public License version 2 - http://www.gnu.org/licenses """ @@ -8,100 +8,159 @@ __docformat__ = "restructuredtext en" import math +from datetime import date, datetime, timedelta try: - from mx.DateTime import RelativeDateTime, strptime, Date - STEP = 1 + from mx.DateTime import RelativeDateTime, Date except ImportError: - from warnings import warn - warn("mxDateTime not found, holiday management won't be available") - from datetime import timedelta - STEP = timedelta(days=1) + from datetime import date, timedelta + warn("mxDateTime not found, endsOfMonth won't be available") + def weekday(date): + return date.weekday() + endOfMonth = None else: endOfMonth = RelativeDateTime(months=1, day=-1) - 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', - } - - def get_national_holidays(begin, end): - """return french national days off between begin and end""" - begin = Date(begin.year, begin.month, begin.day) - end = Date(end.year, end.month, end.day) - holidays = [strptime(datestr, '%Y-%m-%d') - for datestr in FRENCH_MOBILE_HOLIDAYS.values()] - for year in xrange(begin.year, end.year+1): - for datestr in FRENCH_FIXED_HOLIDAYS.values(): - date = strptime(datestr % year, '%Y-%m-%d') - 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""" - weeks, plus = divmod(days, 5) - end = start+(weeks * 7) + plus - if end.day_of_week >= 5: # saturday or sunday - end += 2 - end += len([x for x in get_national_holidays(start, end+1) - if x.day_of_week < 5]) - if end.day_of_week >= 5: # saturday or sunday - end += 2 - return end - - def nb_open_days(start, end): - assert start <= end - days = int(math.ceil((end - start).days)) - weeks, plus = divmod(days, 7) - if start.day_of_week > end.day_of_week: - plus -= 2 - elif end.day_of_week == 6: - plus -= 1 - open_days = weeks * 5 + plus - nb_week_holidays = len([x for x in get_national_holidays(start, end+1) - if x.day_of_week < 5 and x < end]) - return open_days - nb_week_holidays - - -def date_range(begin, end, step=STEP): +# NOTE: should we implement a compatibility layer between date representations +# as we have in lgc.db ? + +PYDATE_STEP = timedelta(days=1) + +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', + } + +# this implementation cries for multimethod dispatching + +def get_step(dateobj): + # assume date is either a python datetime or a mx.DateTime object + if isinstance(dateobj, date): + return PYDATE_STEP + return 1 # 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]) + return open_days - nb_week_holidays + +def date_range(begin, end, step=None): """ enumerate dates between begin and end dates. step can either be oneDay, oneHour, oneMinute, oneSecond, oneWeek use endOfMonth to enumerate months """ + if step is None: + step = get_step(begin) date = begin while date < end : yield date date += step - |