I see France, I see the USA, I see holidays!
This commit is contained in:
commit
ed9a4b3f15
|
@ -0,0 +1,2 @@
|
|||
*.pyc
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2013 Novapost
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,20 @@
|
|||
===========
|
||||
Workalendar
|
||||
===========
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
Workalendar is a Python module that offers classes able to handle calendars,
|
||||
list legal / religious holidays and gives workday-related computation functions.
|
||||
|
||||
Status
|
||||
======
|
||||
|
||||
This is barely beta. Please consider this module as a work in progres.
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
This library is published under the terms of the MIT License. Please check the
|
||||
LICENSE file for more details.
|
|
@ -0,0 +1 @@
|
|||
python-dateutil
|
|
@ -0,0 +1,30 @@
|
|||
from workalendar.core import WesternCalendar
|
||||
from workalendar.core import SUN, MON, THU
|
||||
from datetime import date
|
||||
|
||||
|
||||
class UnitedStatesCalendar(WesternCalendar):
|
||||
"USA calendar"
|
||||
|
||||
@staticmethod
|
||||
def is_presidential_year(year):
|
||||
return (year % 4) == 0
|
||||
|
||||
def get_calendar_holidays(self, year):
|
||||
days = super(UnitedStatesCalendar, self).get_calendar_holidays(year)
|
||||
days.add(date(year, 7, 4))
|
||||
days.add(date(year, 11, 11))
|
||||
# variable days
|
||||
days.add(WesternCalendar.get_nth_weekday_in_month(year, 1, MON, 3))
|
||||
days.add(WesternCalendar.get_nth_weekday_in_month(year, 2, MON, 3))
|
||||
days.add(WesternCalendar.get_last_weekday_in_month(year, 5, MON))
|
||||
days.add(WesternCalendar.get_nth_weekday_in_month(year, 9, MON))
|
||||
days.add(WesternCalendar.get_nth_weekday_in_month(year, 10, MON, 2))
|
||||
days.add(WesternCalendar.get_nth_weekday_in_month(year, 11, THU, 4))
|
||||
# Inauguration day
|
||||
if UnitedStatesCalendar.is_presidential_year(year - 1):
|
||||
inauguration_day = date(year, 1, 20)
|
||||
if inauguration_day.weekday() == SUN:
|
||||
inauguration_day = date(year, 1, 21)
|
||||
days.add(inauguration_day)
|
||||
return days
|
|
@ -0,0 +1,132 @@
|
|||
"""Workday tools
|
||||
"""
|
||||
from calendar import monthrange
|
||||
from datetime import date, timedelta
|
||||
from dateutil import easter
|
||||
|
||||
MON, TUE, WED, THU, FRI, SAT, SUN = range(7)
|
||||
|
||||
|
||||
class Calendar(object):
|
||||
|
||||
EASTER_METHOD = 3 # 3 is 'Western'
|
||||
|
||||
def __init__(self):
|
||||
self._holidays = {}
|
||||
|
||||
def get_calendar_holidays(self, year):
|
||||
"""Get calendar holidays.
|
||||
This method **must** return a set or a list.
|
||||
You must override this method for each calendar."""
|
||||
return set([])
|
||||
|
||||
def holidays(self, year=None):
|
||||
"Computes holidays (non-working days) for a given year"
|
||||
if not year:
|
||||
year = date.today().year
|
||||
|
||||
if year in self._holidays:
|
||||
return self._holidays[year]
|
||||
|
||||
if year not in self._holidays:
|
||||
self._holidays[year] = set([])
|
||||
# Here we process the holiday specific calendar
|
||||
self._holidays[year] = self.get_calendar_holidays(year)
|
||||
return set(self._holidays[year])
|
||||
|
||||
def get_weekend_days(self):
|
||||
"""Return a list (or a tuple) of weekdays that are *not* workdays.
|
||||
|
||||
e.g: return (SAT, SUN,)
|
||||
|
||||
"""
|
||||
raise NotImplementedError("Your Calendar class must implement the"
|
||||
" `get_weekend_days` method")
|
||||
|
||||
def is_workday(self, day):
|
||||
"Return True if it's a workday."
|
||||
if day.weekday() in self.get_weekend_days():
|
||||
return False
|
||||
if day in self.holidays(day.year):
|
||||
return False
|
||||
return True
|
||||
|
||||
def add_workdays(self, day, delta):
|
||||
"Add `delta` workdays to the date."
|
||||
days = 0
|
||||
temp_day = day
|
||||
while days < delta:
|
||||
temp_day = temp_day + timedelta(1)
|
||||
if self.is_workday(temp_day):
|
||||
days += 1
|
||||
return temp_day
|
||||
|
||||
def get_easter_sunday(self, year):
|
||||
"Return the date of the easter (sunday) -- following the easter method"
|
||||
return easter.easter(year, self.EASTER_METHOD)
|
||||
|
||||
def get_easter_monday(self, year):
|
||||
"Return the date of the monday after easter"
|
||||
sunday = self.get_easter_sunday(year)
|
||||
return sunday + timedelta(days=1)
|
||||
|
||||
@staticmethod
|
||||
def get_nth_weekday_in_month(year, month, weekday, n=1):
|
||||
"""Get the nth weekday in a given month. e.g:
|
||||
|
||||
>>> # the 1st monday in Jan 2013
|
||||
>>> Calendar.get_nth_weekday_in_month(2013, 1, MON)
|
||||
datetime.date(2013, 1, 7)
|
||||
>>> # The 2nd monday in Jan 2013
|
||||
>>> Calendar.get_nth_weekday_in_month(2013, 1, MON, 2)
|
||||
datetime.date(2013, 1, 14)
|
||||
"""
|
||||
day = date(year, month, 1)
|
||||
counter = 0
|
||||
while True:
|
||||
if day.month != month:
|
||||
# Don't forget to break if "n" is too big
|
||||
return None
|
||||
if day.weekday() == weekday:
|
||||
counter += 1
|
||||
if counter == n:
|
||||
break
|
||||
day = day + timedelta(days=1)
|
||||
return day
|
||||
|
||||
@staticmethod
|
||||
def get_last_weekday_in_month(year, month, weekday):
|
||||
"""Get the last weekday in a given month. e.g:
|
||||
|
||||
>>> # the last monday in Jan 2013
|
||||
>>> Calendar.get_last_weekday_in_month(2013, 1, MON)
|
||||
datetime.date(2013, 1, 28)
|
||||
"""
|
||||
day = date(year, month, monthrange(year, month)[1])
|
||||
while True:
|
||||
if day.weekday() == weekday:
|
||||
break
|
||||
day = day - timedelta(days=1)
|
||||
return day
|
||||
|
||||
|
||||
class WesternCalendar(Calendar):
|
||||
"""
|
||||
General usage calendar for Western countries.
|
||||
|
||||
(chiefly Europe and Northern America)
|
||||
|
||||
"""
|
||||
EASTER_METHOD = 3 # 3 is 'Western'
|
||||
WEEK_END_DAYS = (SAT, SUN)
|
||||
|
||||
def get_calendar_holidays(self, year):
|
||||
"European countries have at least these 2 days as holidays in common"
|
||||
days = set([])
|
||||
days.add(date(year, 1, 1))
|
||||
days.add(date(year, 12, 25))
|
||||
return days
|
||||
|
||||
def get_weekend_days(self):
|
||||
"Week-end days are SATurday and SUNday."
|
||||
return self.WEEK_END_DAYS
|
|
@ -0,0 +1,28 @@
|
|||
from workalendar.core import WesternCalendar
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
class FranceCalendar(WesternCalendar):
|
||||
"France calendar class"
|
||||
|
||||
def get_ascension_thursday(self, year):
|
||||
easter = self.get_easter_sunday(year)
|
||||
return easter + timedelta(days=39)
|
||||
|
||||
def get_pentecote_monday(self, year):
|
||||
easter = self.get_easter_sunday(year)
|
||||
return easter + timedelta(days=50)
|
||||
|
||||
def get_calendar_holidays(self, year):
|
||||
days = super(FranceCalendar, self).get_calendar_holidays(year)
|
||||
days.add(date(year, 5, 1)) # Labour day
|
||||
days.add(date(year, 5, 8)) # 1939-45 victory
|
||||
days.add(date(year, 7, 14)) # National day
|
||||
days.add(date(year, 8, 15)) # Assomption
|
||||
days.add(date(year, 11, 1)) # Toussaint
|
||||
days.add(date(year, 11, 11)) # Armistice
|
||||
days.add(date(year, 12, 25)) # Christmas
|
||||
days.add(self.get_easter_monday(year))
|
||||
days.add(self.get_ascension_thursday(year))
|
||||
days.add(self.get_pentecote_monday(year))
|
||||
return days
|
|
@ -0,0 +1,13 @@
|
|||
from datetime import date
|
||||
from unittest import TestCase
|
||||
|
||||
from workalendar.core import Calendar
|
||||
|
||||
|
||||
class GenericCalendarTest(TestCase):
|
||||
|
||||
cal_class = Calendar
|
||||
|
||||
def setUp(self):
|
||||
self.year = date.today().year
|
||||
self.cal = self.cal_class()
|
|
@ -0,0 +1,39 @@
|
|||
from datetime import date
|
||||
from workalendar.tests import GenericCalendarTest
|
||||
from workalendar.america import UnitedStatesCalendar
|
||||
|
||||
|
||||
class UnitedStatesCalendarTest(GenericCalendarTest):
|
||||
|
||||
cal_class = UnitedStatesCalendar
|
||||
|
||||
def test_year_2013(self):
|
||||
holidays = self.cal.holidays(2013)
|
||||
self.assertIn(date(2013, 1, 1), holidays) # new year
|
||||
self.assertIn(date(2013, 7, 4), holidays) # Nation day
|
||||
self.assertIn(date(2013, 11, 11), holidays) # Armistice
|
||||
self.assertIn(date(2013, 12, 25), holidays) # Christmas
|
||||
# Variable days
|
||||
self.assertIn(date(2013, 1, 21), holidays) # Martin Luther King
|
||||
self.assertIn(date(2013, 2, 18), holidays) # Washington's bday
|
||||
self.assertIn(date(2013, 5, 27), holidays) # Memorial day
|
||||
self.assertIn(date(2013, 9, 2), holidays) # Labour day
|
||||
self.assertIn(date(2013, 10, 14), holidays) # Colombus
|
||||
self.assertIn(date(2013, 11, 28), holidays) # Thanskgiving
|
||||
|
||||
def test_presidential_year(self):
|
||||
self.assertTrue(UnitedStatesCalendar.is_presidential_year(2012))
|
||||
self.assertFalse(UnitedStatesCalendar.is_presidential_year(2013))
|
||||
self.assertFalse(UnitedStatesCalendar.is_presidential_year(2014))
|
||||
self.assertFalse(UnitedStatesCalendar.is_presidential_year(2015))
|
||||
self.assertTrue(UnitedStatesCalendar.is_presidential_year(2016))
|
||||
|
||||
def test_inauguration_day(self):
|
||||
holidays = self.cal.holidays(2008)
|
||||
self.assertNotIn(date(2008, 1, 20), holidays)
|
||||
holidays = self.cal.holidays(2009)
|
||||
self.assertIn(date(2009, 1, 20), holidays)
|
||||
# case when inauguration day is a sunday
|
||||
holidays = self.cal.holidays(1985)
|
||||
self.assertNotIn(date(1985, 1, 20), holidays)
|
||||
self.assertIn(date(1985, 1, 21), holidays)
|
|
@ -0,0 +1,67 @@
|
|||
from datetime import date
|
||||
from workalendar.tests import GenericCalendarTest
|
||||
from workalendar.core import Calendar, MON, TUE, THU
|
||||
|
||||
|
||||
class CalendarTest(GenericCalendarTest):
|
||||
|
||||
def test_private_variables(self):
|
||||
self.assertTrue(hasattr(self.cal, '_holidays'))
|
||||
private_holidays = self.cal._holidays
|
||||
self.assertTrue(isinstance(private_holidays, dict))
|
||||
self.cal.holidays(2011)
|
||||
self.cal.holidays(2012)
|
||||
private_holidays = self.cal._holidays
|
||||
self.assertTrue(isinstance(private_holidays, dict))
|
||||
self.assertIn(2011, self.cal._holidays)
|
||||
self.assertIn(2012, self.cal._holidays)
|
||||
|
||||
def test_year(self):
|
||||
holidays = self.cal.holidays()
|
||||
self.assertTrue(isinstance(holidays, set))
|
||||
self.assertEquals(self.cal._holidays[self.year], holidays)
|
||||
|
||||
def test_another_year(self):
|
||||
holidays = self.cal.holidays(2011)
|
||||
self.assertTrue(isinstance(holidays, set))
|
||||
self.assertEquals(self.cal._holidays[2011], holidays)
|
||||
|
||||
def test_is_workday(self):
|
||||
self.assertRaises(
|
||||
NotImplementedError,
|
||||
self.cal.is_workday, date(2012, 1, 1))
|
||||
|
||||
def test_nth_weekday(self):
|
||||
# first monday in january 2013
|
||||
self.assertEquals(
|
||||
Calendar.get_nth_weekday_in_month(2013, 1, MON),
|
||||
date(2013, 1, 7)
|
||||
)
|
||||
# second monday in january 2013
|
||||
self.assertEquals(
|
||||
Calendar.get_nth_weekday_in_month(2013, 1, MON, 2),
|
||||
date(2013, 1, 14)
|
||||
)
|
||||
# let's test the limits
|
||||
# Jan 1st is a TUE
|
||||
self.assertEquals(
|
||||
Calendar.get_nth_weekday_in_month(2013, 1, TUE),
|
||||
date(2013, 1, 1)
|
||||
)
|
||||
# There's no 6th MONday
|
||||
self.assertEquals(
|
||||
Calendar.get_nth_weekday_in_month(2013, 1, MON, 6),
|
||||
None
|
||||
)
|
||||
|
||||
def test_last_weekday(self):
|
||||
# last monday in january 2013
|
||||
self.assertEquals(
|
||||
Calendar.get_last_weekday_in_month(2013, 1, MON),
|
||||
date(2013, 1, 28)
|
||||
)
|
||||
# last thursday
|
||||
self.assertEquals(
|
||||
Calendar.get_last_weekday_in_month(2013, 1, THU),
|
||||
date(2013, 1, 31)
|
||||
)
|
|
@ -0,0 +1,35 @@
|
|||
from datetime import date
|
||||
from workalendar.tests import GenericCalendarTest
|
||||
from workalendar.europe import FranceCalendar
|
||||
|
||||
|
||||
class FranceCalendarTest(GenericCalendarTest):
|
||||
|
||||
cal_class = FranceCalendar
|
||||
|
||||
def test_year_2013(self):
|
||||
holidays = self.cal.holidays(2013)
|
||||
self.assertIn(date(2013, 1, 1), holidays) # new year
|
||||
self.assertIn(date(2013, 4, 1), holidays) # easter
|
||||
self.assertIn(date(2013, 5, 1), holidays) # labour day
|
||||
self.assertIn(date(2013, 5, 8), holidays) # 39-45
|
||||
self.assertIn(date(2013, 5, 9), holidays) # Ascension
|
||||
self.assertIn(date(2013, 5, 20), holidays) # Pentecote
|
||||
self.assertIn(date(2013, 7, 14), holidays) # Nation day
|
||||
self.assertIn(date(2013, 8, 15), holidays) # Assomption
|
||||
self.assertIn(date(2013, 11, 1), holidays) # Toussaint
|
||||
self.assertIn(date(2013, 11, 11), holidays) # Armistice
|
||||
self.assertIn(date(2013, 12, 25), holidays) # Christmas
|
||||
|
||||
def test_workdays(self):
|
||||
self.assertFalse(self.cal.is_workday(date(2013, 1, 1))) # holiday
|
||||
self.assertFalse(self.cal.is_workday(date(2013, 1, 5))) # saturday
|
||||
self.assertFalse(self.cal.is_workday(date(2013, 1, 6))) # sunday
|
||||
self.assertTrue(self.cal.is_workday(date(2013, 1, 7))) # monday
|
||||
|
||||
def test_business_days_computations(self):
|
||||
day = date(2013, 10, 30)
|
||||
self.assertEquals(self.cal.add_workdays(day, 0), date(2013, 10, 30))
|
||||
self.assertEquals(self.cal.add_workdays(day, 1), date(2013, 10, 31))
|
||||
self.assertEquals(self.cal.add_workdays(day, 2), date(2013, 11, 4))
|
||||
self.assertEquals(self.cal.add_workdays(day, 3), date(2013, 11, 5))
|
Loading…
Reference in New Issue