Package logilab :: Package common :: Module date

[frames] | no frames]

# Source Code for Module logilab.common.date

      1  # copyright 2003-2012 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
     21  __docformat__ = "restructuredtext en"
     22
     23  import math
     24  import re
     25  import sys
     26  from locale import getlocale, LC_TIME
     27  from datetime import date, time, datetime, timedelta
     28  from time import strptime as time_strptime
     29  from calendar import monthrange, timegm
     30
     31  from six.moves import range
     32
     33  try:
     34      from mx.DateTime import RelativeDateTime, Date, DateTimeType
     35  except ImportError:
     36      endOfMonth = None
     37      DateTimeType = datetime
     38  else:
     39      endOfMonth = RelativeDateTime(months=1, day=-1)
     40
     41  # NOTE: should we implement a compatibility layer between date representations
     42  #       as we have in lgc.db ?
     43
     44  FRENCH_FIXED_HOLIDAYS = {
     45      'jour_an': '%s-01-01',
     46      'fete_travail': '%s-05-01',
     47      'armistice1945': '%s-05-08',
     48      'fete_nat': '%s-07-14',
     49      'assomption': '%s-08-15',
     50      'toussaint': '%s-11-01',
     51      'armistice1918': '%s-11-11',
     52      'noel': '%s-12-25',
     53      }
     54
     55  FRENCH_MOBILE_HOLIDAYS = {
     56      'paques2004': '2004-04-12',
     57      'ascension2004': '2004-05-20',
     58      'pentecote2004': '2004-05-31',
     59
     60      'paques2005': '2005-03-28',
     61      'ascension2005': '2005-05-05',
     62      'pentecote2005': '2005-05-16',
     63
     64      'paques2006': '2006-04-17',
     65      'ascension2006': '2006-05-25',
     66      'pentecote2006': '2006-06-05',
     67
     68      'paques2007': '2007-04-09',
     69      'ascension2007': '2007-05-17',
     70      'pentecote2007': '2007-05-28',
     71
     72      'paques2008': '2008-03-24',
     73      'ascension2008': '2008-05-01',
     74      'pentecote2008': '2008-05-12',
     75
     76      'paques2009': '2009-04-13',
     77      'ascension2009': '2009-05-21',
     78      'pentecote2009': '2009-06-01',
     79
     80      'paques2010': '2010-04-05',
     81      'ascension2010': '2010-05-13',
     82      'pentecote2010': '2010-05-24',
     83
     84      'paques2011': '2011-04-25',
     85      'ascension2011': '2011-06-02',
     86      'pentecote2011': '2011-06-13',
     87
     88      'paques2012': '2012-04-09',
     89      'ascension2012': '2012-05-17',
     90      'pentecote2012': '2012-05-28',
     91      }
     92
     93  # XXX this implementation cries for multimethod dispatching
     94
     95 -def get_step(dateobj, nbdays=1):
     96      # assume date is either a python datetime or a mx.DateTime object
     97      if isinstance(dateobj, date):
     98          return ONEDAY * nbdays
     99      return nbdays # mx.DateTime is ok with integers
    100
    101 -def datefactory(year, month, day, sampledate):
    102      # assume date is either a python datetime or a mx.DateTime object
    103      if isinstance(sampledate, datetime):
    104          return datetime(year, month, day)
    105      if isinstance(sampledate, date):
    106          return date(year, month, day)
    107      return Date(year, month, day)
    108
    109 -def weekday(dateobj):
    110      # assume date is either a python datetime or a mx.DateTime object
    111      if isinstance(dateobj, date):
    112          return dateobj.weekday()
    113      return dateobj.day_of_week
    114
    115 -def str2date(datestr, sampledate):
    116      # NOTE: datetime.strptime is not an option until we drop py2.4 compat
    117      year, month, day = [int(chunk) for chunk in datestr.split('-')]
    118      return datefactory(year, month, day, sampledate)
    119
    120 -def days_between(start, end):
    121      if isinstance(start, date):
    122          delta = end - start
    123          # datetime.timedelta.days is always an integer (floored)
    124          if delta.seconds:
    125              return delta.days + 1
    126          return delta.days
    127      else:
    128          return int(math.ceil((end - start).days))
    129
    130 -def get_national_holidays(begin, end):
    131      """return french national days off between begin and end"""
    132      begin = datefactory(begin.year, begin.month, begin.day, begin)
    133      end = datefactory(end.year, end.month, end.day, end)
    134      holidays = [str2date(datestr, begin)
    135                  for datestr in FRENCH_MOBILE_HOLIDAYS.values()]
    136      for year in range(begin.year, end.year+1):
    137          for datestr in FRENCH_FIXED_HOLIDAYS.values():
    138              date = str2date(datestr % year, begin)
    139              if date not in holidays:
    140                  holidays.append(date)
    141      return [day for day in holidays if begin <= day < end]
    142
    143 -def add_days_worked(start, days):
    144      """adds date but try to only take days worked into account"""
    145      step = get_step(start)
    146      weeks, plus = divmod(days, 5)
    147      end = start + ((weeks * 7) + plus) * step
    148      if weekday(end) >= 5: # saturday or sunday
    149          end += (2 * step)
    150      end += len([x for x in get_national_holidays(start, end + step)
    151                  if weekday(x) < 5]) * step
    152      if weekday(end) >= 5: # saturday or sunday
    153          end += (2 * step)
    154      return end
    155
    156 -def nb_open_days(start, end):
    157      assert start <= end
    158      step = get_step(start)
    159      days = days_between(start, end)
    160      weeks, plus = divmod(days, 7)
    161      if weekday(start) > weekday(end):
    162          plus -= 2
    163      elif weekday(end) == 6:
    164          plus -= 1
    165      open_days = weeks * 5 + plus
    166      nb_week_holidays = len([x for x in get_national_holidays(start, end+step)
    167                              if weekday(x) < 5 and x < end])
    168      open_days -= nb_week_holidays
    169      if open_days < 0:
    170          return 0
    171      return open_days
    172
    173 -def date_range(begin, end, incday=None, incmonth=None):
    174      """yields each date between begin and end
    175
    176      :param begin: the start date
    177      :param end: the end date
    178      :param incr: the step to use to iterate over dates. Default is
    179                   one day.
    180      :param include: None (means no exclusion) or a function taking a
    181                      date as parameter, and returning True if the date
    182                      should be included.
    183
    184      When using mx datetime, you should *NOT* use incmonth argument, use instead
    185      oneDay, oneHour, oneMinute, oneSecond, oneWeek or endOfMonth (to enumerate
    186      months) as `incday` argument
    187      """
    188      assert not (incday and incmonth)
    189      begin = todate(begin)
    190      end = todate(end)
    191      if incmonth:
    192          while begin < end:
    193              yield begin
    194              begin = next_month(begin, incmonth)
    195      else:
    196          incr = get_step(begin, incday or 1)
    197          while begin < end:
    198             yield begin
    199             begin += incr
    200
    201  # makes py datetime usable #####################################################
    202
    203  ONEDAY = timedelta(days=1)
    204  ONEWEEK = timedelta(days=7)
    205
    206  try:
    207      strptime = datetime.strptime
    208  except AttributeError: # py < 2.5
    209      from time import strptime as time_strptime
    210 -    def strptime(value, format):
    211          return datetime(*time_strptime(value, format)[:6])
    212
    213 -def strptime_time(value, format='%H:%M'):
    214      return time(*time_strptime(value, format)[3:6])
    215
    216 -def todate(somedate):
    217      """return a date from a date (leaving unchanged) or a datetime"""
    218      if isinstance(somedate, datetime):
    219          return date(somedate.year, somedate.month, somedate.day)
    220      assert isinstance(somedate, (date, DateTimeType)), repr(somedate)
    221      return somedate
    222
    223 -def totime(somedate):
    224      """return a time from a time (leaving unchanged), date or datetime"""
    225      # XXX mx compat
    226      if not isinstance(somedate, time):
    227          return time(somedate.hour, somedate.minute, somedate.second)
    228      assert isinstance(somedate, (time)), repr(somedate)
    229      return somedate
    230
    231 -def todatetime(somedate):
    232      """return a date from a date (leaving unchanged) or a datetime"""
    233      # take care, datetime is a subclass of date
    234      if isinstance(somedate, datetime):
    235          return somedate
    236      assert isinstance(somedate, (date, DateTimeType)), repr(somedate)
    237      return datetime(somedate.year, somedate.month, somedate.day)
    238
    239 -def datetime2ticks(somedate):
    240      return timegm(somedate.timetuple()) * 1000
    241
    242 -def ticks2datetime(ticks):
    243      miliseconds, microseconds = divmod(ticks, 1000)
    244      try:
    245          return datetime.fromtimestamp(miliseconds)
    246      except (ValueError, OverflowError):
    247          epoch = datetime.fromtimestamp(0)
    248          nb_days, seconds = divmod(int(miliseconds), 86400)
    249          delta = timedelta(nb_days, seconds=seconds, microseconds=microseconds)
    250          try:
    251              return epoch + delta
    252          except (ValueError, OverflowError):
    253              raise
    254
    255 -def days_in_month(somedate):
    256      return monthrange(somedate.year, somedate.month)[1]
    257
    258 -def days_in_year(somedate):
    259      feb = date(somedate.year, 2, 1)
    260      if days_in_month(feb) == 29:
    261          return 366
    262      else:
    263          return 365
    264
    265 -def previous_month(somedate, nbmonth=1):
    266      while nbmonth:
    267          somedate = first_day(somedate) - ONEDAY
    268          nbmonth -= 1
    269      return somedate
    270
    271 -def next_month(somedate, nbmonth=1):
    272      while nbmonth:
    273          somedate = last_day(somedate) + ONEDAY
    274          nbmonth -= 1
    275      return somedate
    276
    277 -def first_day(somedate):
    278      return date(somedate.year, somedate.month, 1)
    279
    280 -def last_day(somedate):
    281      return date(somedate.year, somedate.month, days_in_month(somedate))
    282
    283 -def ustrftime(somedate, fmt='%Y-%m-%d'):
    284      """like strftime, but returns a unicode string instead of an encoded
    285      string which may be problematic with localized date.
    286      """
    287      if sys.version_info >= (3, 3):
    288          # datetime.date.strftime() supports dates since year 1 in Python >=3.3.
    289          return somedate.strftime(fmt)
    290      else:
    291          try:
    292              if sys.version_info < (3, 0):
    293                  encoding = getlocale(LC_TIME)[1] or 'ascii'
    294                  return unicode(somedate.strftime(str(fmt)), encoding)
    295              else:
    296                  return somedate.strftime(fmt)
    297          except ValueError:
    298              if somedate.year >= 1900:
    299                  raise
    300              # datetime is not happy with dates before 1900
    301              # we try to work around this, assuming a simple
    302              # format string
    303              fields = {'Y': somedate.year,
    304                        'm': somedate.month,
    305                        'd': somedate.day,
    306                        }
    307              if isinstance(somedate, datetime):
    308                  fields.update({'H': somedate.hour,
    309                                 'M': somedate.minute,
    310                                 'S': somedate.second})
    311              fmt = re.sub('%([YmdHMS])', r'%(\1)02d', fmt)
    312              return unicode(fmt) % fields
    313
    314 -def utcdatetime(dt):
    315      if dt.tzinfo is None:
    316          return dt
    317      return (dt.replace(tzinfo=None) - dt.utcoffset())
    318
    319 -def utctime(dt):
    320      if dt.tzinfo is None:
    321          return dt
    322      return (dt + dt.utcoffset() + dt.dst()).replace(tzinfo=None)
    323
    324 -def datetime_to_seconds(date):
    325      """return the number of seconds since the begining of the day for that date
    326      """
    327      return date.second+60*date.minute + 3600*date.hour
    328
    329 -def timedelta_to_days(delta):
    330      """return the time delta as a number of seconds"""
    331      return delta.days + delta.seconds / (3600*24)
    332
    333 -def timedelta_to_seconds(delta):
    334      """return the time delta as a fraction of days"""
    335      return delta.days*(3600*24) + delta.seconds
    336