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

Source Code for Module logilab.common.date

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