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

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