I see France, I see the USA, I see holidays!

This commit is contained in:
Bruno Bord 2013-11-20 15:16:27 +01:00
commit ed9a4b3f15
12 changed files with 386 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.pyc

19
LICENSE Normal file
View File

@ -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.

20
README.rst Normal file
View File

@ -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.

1
requirements.pip Normal file
View File

@ -0,0 +1 @@
python-dateutil

0
workalendar/__init__.py Normal file
View File

30
workalendar/america.py Normal file
View File

@ -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

132
workalendar/core.py Normal file
View File

@ -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

28
workalendar/europe.py Normal file
View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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)
)

View File

@ -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))