Source code for Goulib.workdays

#!/usr/bin/env python
# coding: utf8
"""
WorkCalendar class with datetime operations on working hours, handling holidays
merges and improves `BusinessHours <http://pypi.python.org/pypi/BusinessHours/>`_ and `workdays <http://pypi.python.org/pypi/workdays/>`_ packages
"""
__author__ = "Philippe Guglielmetti"
__copyright__ = "Copyright 2012, Philippe Guglielmetti"
__credits__ = ["http://pypi.python.org/pypi/BusinessHours",
               "http://pypi.python.org/pypi/workdays/",
               ]
__license__ = "LGPL"

from datetime import *
import collections
import logging

from .datetime2 import *
from .interval import *

[docs]class WorkCalendar(object): """WorkCalendar class with datetime operations on working hours""" # Define the weekday mnemonics to match the date.weekday function (MON, TUE, WED, THU, FRI, SAT, SUN) = list(range(7))
[docs] def __init__(self,worktime=[time.min,time.max],parent=[], weekends=(SAT,SUN), holidays=set()): self.weekends=weekends self.holidays=set(holidays) if isinstance(parent, collections.Iterable): self.parents=parent else: self.parents=[parent] self.setworktime(worktime)
start = property(fget=lambda self: self._worktime[0]) end = property(fget=lambda self: self._worktime[1])
[docs] def setworktime(self,worktime): self._worktime=list(map(timef,worktime)) for p in self.parents: self._worktime=intersection(self._worktime,p._worktime) self.delta=timedelta(minutes=(self.end.hour-self.start.hour)*60+(self.end.minute-self.start.minute)) if self.start==time.min and self.end==time.max: #we have a microsecond delay self.delta=timedelta(hours=24) #make it perfect
[docs] def addholidays(self,days): """add day(s) to to known holidays. dates with year==4 (to allow Feb 29th) apply every year note : holidays set may contain weekends too.""" try: #iterable for day in days: self.holidays.add(day) except: self.holidays.add(days) return self
[docs] def isworkday(self,day): """@return True if day is a work day""" if day.weekday() in self.weekends: return False if date(year=4,month=day.month,day=day.day) in self.holidays: return False if datef(day) in self.holidays: return False for p in self.parents: if not p.isworkday(day): return False return True
[docs] def isworktime(self,time): """@return True if you're supposed to work at that time""" if not self.isworkday(time): return False return in_interval(self.workdatetime(time),time)
[docs] def nextworkday(self,day): """@return next work day""" res=day while True: res=res+oneday if self.isworkday(res): break return res
[docs] def prevworkday(self,day): """@return previous work day""" res=day while True: res=res-oneday if self.isworkday(res): break return res
[docs] def range(self,start,end): """range of workdays between start (included) and end (not included)""" if start>end: return self.range(end,start) res=[] day=start if not self.isworkday(day): day=self.nextworkday(start) while day<end: res.append(day) day=self.nextworkday(day) return res
[docs] def workdays(self,start_date,ndays): """list of ndays workdays from start""" day=start_date res=[day] while ndays>0: day=self.nextworkday(day) ndays=ndays-1 res.append(day) while ndays<0: day=self.prevworkday(day) ndays=ndays+1 res.insert(0,day) return res
[docs] def workday(self,start_date,ndays): '''Same as Excel WORKDAY function. Returns a date that is the indicated number of working days before or after the starting date. Working days exclude weekends and any dates identified as holidays. Use WORKDAY to exclude weekends or holidays when you calculate invoice due dates, expected delivery times, or the number of days of work performed. ''' if ndays>0: return self.workdays(start_date,ndays)[-1] else: return self.workdays(start_date,ndays)[0]
[docs] def cast(self,time,retro=False): '''force time to be in workhours''' if self.isworktime(time): return time #ok if retro: if not self.isworkday(time) or time.time()<self.start: return datetimef(self.prevworkday(time.date()),self.end) #only remaining case is time>self.end on a work day return datetimef(time.date(),self.end) else: if not self.isworkday(time) or time.time()>self.end: return datetimef(self.nextworkday(time.date()),self.start) #only remaining case is time<self.start on a work day return datetimef(time.date(),self.start)
[docs] def worktime(self,day): '''@return interval of time worked a given day''' if not self.isworkday(day): return None return (self.start,self.end)
[docs] def workdatetime(self,day): '''@return interval of datetime worked a given day''' if not self.isworkday(day): return None day=datef(day) return (datetimef(day,self.start),datetimef(day,self.end))
[docs] def diff(self,t1,t2): '''@return timedelta worktime between t1 and t2 (= t2-t1)''' t1=datetimef(t1,self.start) t2=datetimef(t2,self.start) if t1>t2: return -self.diff(t2,t1) fulldays=max(0,self.networkdays(t1, t2)-2) res=timedelta_mul(fulldays,self.delta) w1=self.workdatetime(t1) if w1: res+=intersectlen(w1,(t1,t2),timedelta0) w2=self.workdatetime(t2) if w2: res+=intersectlen(w2,(t1,t2),timedelta0) return res
[docs] def gethours(self,t1,t2): '''@return fractional work hours between t1 and t2 (= t2-t1)''' return self.diff(t1,t2).total_seconds()/3600.
[docs] def plus(self,start,t): '''@return start time + t work time (positive or negative)''' start=datetimef(start,self.start) if not self.isworktime(start): logging.error('%s is not in worktime'%start) raise days=timedelta_div(t,self.delta) res=start while days>=1: res=self.nextworkday(res) days=days-1 while days<=-1: res=self.prevworkday(res) days=days+1 remaining=timedelta_mul(self.delta,days) #less than one day of work day=res.date() start=datetimef(day,self.start) end=datetimef(day,self.end) if (res+remaining)<start: # skip to previous day remaining=(res+remaining)-start #in full time res=datetimef(self.prevworkday(day),self.end) if (res+remaining)>end: # skip to next day remaining=(res+remaining)-end #in full time res=datetimef(self.nextworkday(day),self.start) return res+remaining
[docs] def minus(self,start,t): '''@return start time - t work time (positive or negative)''' return self.plus(start,-t)
[docs] def networkdays(self,start_date, end_date): '''Same as Excel NETWORKDAYS function. Returns the number of whole working days between start_date and end_date (inclusive of both start_date and end_date). Working days exclude weekends and any dates identified in holidays. Use NETWORKDAYS to calculate employee benefits that accrue based on the number of days worked during a specific term''' end_date=datef(end_date) start_date=datef(start_date) if end_date<start_date: return -self.networkdays(end_date,start_date) i=start_date res=0 while i<=end_date: if self.isworkday(i):res+=1 i=datef(i+oneday) return res
''' a 24/24 7/7 calendar is useful''' FullTime=WorkCalendar([time.min,time.max],holidays=[],weekends=[]) ''' compatibility with http://pypi.python.org/pypi/BusinessHours'''
[docs]def workday(start_date,ndays,holidays=[]): '''Same as Excel WORKDAY function. Returns a date that is the indicated number of working days before or after the starting date. Working days exclude weekends and any dates identified as holidays. Use WORKDAY to exclude weekends or holidays when you calculate invoice due dates, expected delivery times, or the number of days of work performed. ''' return WorkCalendar([8,16],holidays).workday(start_date,ndays)
[docs]def networkdays(start_date, end_date,holidays=[]): '''Same as Excel NETWORKDAYS function. Returns the number of whole working days between start_date and end_date (inclusive of both start_date and end_date). Working days exclude weekends and any dates identified in holidays. Use NETWORKDAYS to calculate employee benefits that accrue based on the number of days worked during a specific term''' return WorkCalendar([8,16],holidays).networkdays(start_date,end_date)