summaryrefslogtreecommitdiff
path: root/date.py
blob: b5d05097fcb5ca570708712d315a45bfa24a4db8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
"""Date manipulation helper functions.

:copyright: 2006-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
:license: General Public License version 2 - http://www.gnu.org/licenses
"""
__docformat__ = "restructuredtext en"

import math

from datetime import date, datetime, timedelta
try:
    from mx.DateTime import RelativeDateTime, Date
except ImportError:
    from warnings import warn
    warn("mxDateTime not found, endsOfMonth won't be available")
    from datetime import date, timedelta
    def weekday(date):
        return date.weekday()
    endOfMonth = None
else:
    endOfMonth = RelativeDateTime(months=1, day=-1)

# NOTE: should we implement a compatibility layer between date representations
#       as we have in lgc.db ?

PYDATE_STEP = timedelta(days=1)

FRENCH_FIXED_HOLIDAYS = {
    'jour_an'        : '%s-01-01',
    'fete_travail'   : '%s-05-01',
    'armistice1945'  : '%s-05-08',
    'fete_nat'       : '%s-07-14',
    'assomption'     : '%s-08-15',
    'toussaint'      : '%s-11-01',
    'armistice1918'  : '%s-11-11',
    'noel'           : '%s-12-25',
    }

FRENCH_MOBILE_HOLIDAYS = {
    'paques2004'    : '2004-04-12',
    'ascension2004' : '2004-05-20',
    'pentecote2004' : '2004-05-31',

    'paques2005'    : '2005-03-28',
    'ascension2005' : '2005-05-05',
    'pentecote2005' : '2005-05-16',

    'paques2006'    : '2006-04-17',
    'ascension2006' : '2006-05-25',
    'pentecote2006' : '2006-06-05',

    'paques2007'    : '2007-04-09',
    'ascension2007' : '2007-05-17',
    'pentecote2007' : '2007-05-28',

    'paques2008'    : '2008-03-24',
    'ascension2008' : '2008-05-01',
    'pentecote2008' : '2008-05-12',

    'paques2009'    : '2009-04-13',
    'ascension2009' : '2009-05-21',
    'pentecote2009' : '2009-06-01',

    'paques2010'    : '2010-04-05',
    'ascension2010' : '2010-05-13',
    'pentecote2010' : '2010-05-24',

    'paques2011'    : '2011-04-25',
    'ascension2011' : '2011-06-02',
    'pentecote2011' : '2011-06-13',

    'paques2012'    : '2012-04-09',
    'ascension2012' : '2012-05-17',
    'pentecote2012' : '2012-05-28',
    }

# this implementation cries for multimethod dispatching

def get_step(dateobj):
    # assume date is either a python datetime or a mx.DateTime object
    if isinstance(dateobj, date):
        return PYDATE_STEP
    return 1 # mx.DateTime is ok with integers

def datefactory(year, month, day, sampledate):
    # assume date is either a python datetime or a mx.DateTime object
    if isinstance(sampledate, datetime):
        return datetime(year, month, day)
    if isinstance(sampledate, date):
        return date(year, month, day)
    return Date(year, month, day)

def weekday(dateobj):
    # assume date is either a python datetime or a mx.DateTime object
    if isinstance(dateobj, date):
        return dateobj.weekday()
    return dateobj.day_of_week

def str2date(datestr, sampledate):
    # NOTE: datetime.strptime is not an option until we drop py2.4 compat
    year, month, day = [int(chunk) for chunk in datestr.split('-')]
    return datefactory(year, month, day, sampledate)

def days_between(start, end):
    if isinstance(start, date):
        delta = end - start
        # datetime.timedelta.days is always an integer (floored)
        if delta.seconds:
            return delta.days + 1
        return delta.days
    else:
        return int(math.ceil((end - start).days))

def get_national_holidays(begin, end):
    """return french national days off between begin and end"""
    begin = datefactory(begin.year, begin.month, begin.day, begin)
    end = datefactory(end.year, end.month, end.day, end)
    holidays = [str2date(datestr, begin)
                for datestr in FRENCH_MOBILE_HOLIDAYS.values()]
    for year in xrange(begin.year, end.year+1):
        for datestr in FRENCH_FIXED_HOLIDAYS.values():
            date = str2date(datestr % year, begin)
            if date not in holidays:
                holidays.append(date)
    return [day for day in holidays if begin <= day < end]

def add_days_worked(start, days):
    """adds date but try to only take days worked into account"""
    step = get_step(start)
    weeks, plus = divmod(days, 5)
    end = start + ((weeks * 7) + plus) * step
    if weekday(end) >= 5: # saturday or sunday
        end += (2 * step)
    end += len([x for x in get_national_holidays(start, end + step)
                if weekday(x) < 5]) * step
    if weekday(end) >= 5: # saturday or sunday
        end += (2 * step)
    return end

def nb_open_days(start, end):
    assert start <= end
    step = get_step(start)
    days = days_between(start, end)
    weeks, plus = divmod(days, 7)
    if weekday(start) > weekday(end):
        plus -= 2
    elif weekday(end) == 6:
        plus -= 1
    open_days = weeks * 5 + plus
    nb_week_holidays = len([x for x in get_national_holidays(start, end+step)
                            if weekday(x) < 5 and x < end])
    return open_days - nb_week_holidays

def date_range(begin, end, step=None):
    """
    enumerate dates between begin and end dates.

    step can either be oneDay, oneHour, oneMinute, oneSecond, oneWeek
    use endOfMonth to enumerate months
    """
    if step is None:
        step = get_step(begin)
    date = begin
    while date < end :
        yield date
        date += step