Package logilab :: Package common :: Module date
[frames] | no frames]

Source Code for Module logilab.common.date

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