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