Created
December 15, 2015 08:35
-
-
Save jayme-github/3bd6e8e5614a52f2e608 to your computer and use it in GitHub Desktop.
BusinessHours
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Based on BusinessHours written by | |
# Antano Solar John "solar_ant" <solar345@gmail.com> | |
# Sponsored by Ma Foi | |
# | |
# (C)2006-2007 All Rights Reserved, | |
# | |
# This library is free software; you can redistribute it and/or | |
# modify it under the terms of the GNU Lesser General Public | |
# License as published by the Free Software Foundation; either | |
# version 2.1 of the License, or (at your option) any later version. | |
# | |
# This library is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
# Lesser General Public License for more details. | |
# | |
# You should have received a copy of the GNU Lesser General Public | |
# License along with this library; if not, write to the Free Software | |
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | |
import datetime | |
from dateutil.easter import easter | |
def get_public_holiday(year): | |
easterDay = easter(year) | |
publicHoliday = (datetime.date(year, 01, 01), # Neujahr | |
easterDay + datetime.timedelta(-2), # Karfreitag | |
easterDay + datetime.timedelta(1), # Ostermontag | |
datetime.date(year, 05, 01), # Tag der Arbeit | |
easterDay + datetime.timedelta(39), # Christi Himmelfahrt | |
easterDay + datetime.timedelta(50), # Pfingstmontag | |
datetime.date(year, 10, 03), # Tag der Deutschen Einheit | |
datetime.date(year, 12, 25), # 1. Weihnachtstag | |
datetime.date(year, 12, 26) # 2. Weihnachtstag | |
) | |
return publicHoliday | |
def get_half_holiday(year): | |
halfDays = (datetime.date(year, 12, 24), # Heiligabend | |
datetime.date(year, 12, 31) # Sylvester | |
) | |
return halfDays | |
def daterange(start_date, end_date): | |
if isinstance(start_date, datetime.datetime): | |
start_date = start_date.date() | |
if isinstance(end_date, datetime.datetime): | |
end_date = end_date.date() | |
# yield even if this is no range | |
day_count = (end_date - start_date).days | |
for n in range(day_count if day_count else 1): | |
yield start_date + datetime.timedelta(n) | |
def business_daterange(start_date, end_date, weekend_days=[6, 7]): | |
""" | |
Iterate over a date range yielding only business days | |
""" | |
for day in daterange(start_date, end_date): | |
if day in get_public_holiday(day.year): | |
# Skip holiday | |
continue | |
if day.isoweekday() in weekend_days: | |
# Skip weekend_days | |
continue | |
yield day | |
class BusinessHours(object): | |
def __init__(self, start, end, worktime_start=9, worktime_end=17, weekend_days=[6, 7]): | |
if not end >= start: | |
raise ValueError('End should end after start') | |
self.start = start | |
self.end = end | |
self.worktime_start = worktime_start | |
self.worktime_end = worktime_end | |
self.weekend_days = weekend_days | |
def __repr__(self): | |
return u'<BusinessHours %s - %s>' % (self.start, self.end) | |
def get_worktime_for(self, day): | |
# FIXME: Returns full worktime for weekends and holiday | |
if isinstance(day, datetime.datetime): | |
day = day.date() | |
worktime = (self.worktime_start, self.worktime_end) | |
if day in get_half_holiday(day.year): | |
worktime = (self.worktime_start, self.worktime_start + ((self.worktime_end - self.worktime_start) / 2)) | |
return worktime | |
def get_days(self): | |
""" | |
Return the difference in days. | |
""" | |
days = 0.0 | |
for day in business_daterange(self.start, self.end, self.weekend_days): | |
if day in get_half_holiday(day.year): | |
days += 0.5 | |
else: | |
days += 1 | |
return days | |
def get_hours(self): | |
""" | |
Return the difference in hours | |
""" | |
return self.get_minutes() / 60 | |
def get_minutes(self): | |
""" | |
Return the difference in minutes | |
""" | |
return self.get_seconds() / 60 | |
def get_seconds(self): | |
""" | |
Return the difference in seconds | |
""" | |
dt_start = self.start | |
dt_end = self.end | |
if dt_start.date() == dt_end.date(): | |
# Range is on the same day | |
if self.is_free_day(dt_start): | |
return 0 | |
else: | |
wt_start, wt_end = self.get_worktime_for(dt_start) | |
if dt_start.hour >= wt_end or \ | |
dt_end.hour < wt_start: | |
# Range is not within work hours | |
return 0 | |
if dt_start.hour < wt_start: | |
# starts prior to work hours | |
# so overwrite start with work hours | |
dt_start = dt_start.replace(hour=wt_start, minute=0, second=0) | |
if dt_end.hour >= wt_end: | |
# Ends after work hours | |
# so oberwrite end with work hours | |
dt_end = dt_end.replace(hour=wt_end, minute=0, second=0) | |
return (dt_end-dt_start).total_seconds() | |
# Start and end are on differnt days | |
total_worktime = 0 | |
current_day = dt_start # marker for counting workdays | |
for current_day in business_daterange(dt_start, dt_end, self.weekend_days): | |
# Get work hour start and end for the current day | |
wt_start, wt_end = self.get_worktime_for(current_day) | |
# Worktime (in seconds) for this day | |
worktime = datetime.timedelta(hours=(wt_end - wt_start)).total_seconds() | |
if current_day == dt_start.date(): | |
# The first day | |
if dt_start.hour < wt_start: | |
# starts prior to work hours | |
# add the complete day | |
total_worktime += worktime | |
elif dt_start.hour >= wt_end: | |
# Starts after work hours, | |
# no worktime for this day | |
continue | |
else: | |
# Fix end time to wt_end | |
dt_currentday_close = dt_start.replace(hour=wt_end, | |
minute=0, | |
second=0) | |
total_worktime += (dt_currentday_close - dt_start).total_seconds() | |
elif current_day == dt_end.date(): | |
# The last day | |
if dt_end.hour >= wt_end: | |
# ends after work hours | |
# add the complete day | |
total_worktime += worktime | |
elif dt_end.hour < wt_start: | |
# ends before work hours | |
# no work time for this day | |
continue | |
else: | |
# Fix sart time ti wt_start | |
dt_currentday_open = dt_end.replace(hour=wt_start, | |
minute=0, | |
second=0) | |
total_worktime += (dt_end - dt_currentday_open).total_seconds() | |
else: | |
# increment one full day | |
total_worktime += worktime | |
return total_worktime | |
def is_free_day(self, day): | |
if self.is_weekend(day): | |
return True | |
if day.date() in get_public_holiday(day.year): | |
return True | |
return False | |
def is_weekend(self, day): | |
""" | |
Returns True if day lands on a weekend. | |
""" | |
for weekend in self.weekend_days: | |
if day.isoweekday() == weekend: | |
return True | |
return False |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment