| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- # 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
|