Add opening_hours lib
This commit is contained in:
parent
f5771ca698
commit
764d92a40c
2 changed files with 262 additions and 0 deletions
209
opening_hours.py
Normal file
209
opening_hours.py
Normal file
|
@ -0,0 +1,209 @@
|
|||
import datetime, re, time, logging
|
||||
|
||||
week_days=['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']
|
||||
def easter_date(year):
|
||||
a=year//100
|
||||
b=year%100
|
||||
c=(3*(a+25))//4
|
||||
d=(3*(a+25))%4
|
||||
e=(8*(a+11))//25
|
||||
f=(5*a+b)%19
|
||||
g=(19*f+c-e)%30
|
||||
h=(f+11*g)//319
|
||||
j=(60*(5-d)+b)//4
|
||||
k=(60*(5-d)+b)%4
|
||||
m=(2*j-k-g+h)%7
|
||||
n=(g-h+m+114)//31
|
||||
p=(g-h+m+114)%31
|
||||
day=p+1
|
||||
month=n
|
||||
return datetime.date(year, month, day)
|
||||
|
||||
def nonworking_french_public_days_of_the_year(year=None):
|
||||
if year is None:
|
||||
year=datetime.date.today().year
|
||||
dp=easter_date(year)
|
||||
return {
|
||||
'1janvier': datetime.date(year, 1, 1),
|
||||
'paques': dp,
|
||||
'lundi_paques': (dp+datetime.timedelta(1)),
|
||||
'1mai': datetime.date(year, 5, 1),
|
||||
'8mai': datetime.date(year, 5, 8),
|
||||
'jeudi_ascension': (dp+datetime.timedelta(39)),
|
||||
'pentecote': (dp+datetime.timedelta(49)),
|
||||
'lundi_pentecote': (dp+datetime.timedelta(50)),
|
||||
'14juillet': datetime.date(year, 7, 14),
|
||||
'15aout': datetime.date(year, 8, 15),
|
||||
'1novembre': datetime.date(year, 11, 1),
|
||||
'11novembre': datetime.date(year, 11, 11),
|
||||
'noel': datetime.date(year, 12, 25),
|
||||
'saint_etienne': datetime.date(year, 12, 26),
|
||||
}
|
||||
|
||||
def parse_exceptional_closures(values):
|
||||
exceptional_closures=[]
|
||||
date_pattern=re.compile('^([0-9]{2})/([0-9]{2})/([0-9]{4})$')
|
||||
time_pattern=re.compile('^([0-9]{1,2})h([0-9]{2})?$')
|
||||
for value in values:
|
||||
days=[]
|
||||
hours_periods=[]
|
||||
words=value.strip().split()
|
||||
for word in words:
|
||||
if word=='':
|
||||
continue
|
||||
parts=word.split('-')
|
||||
if len(parts)==1:
|
||||
# ex : 31/02/2017
|
||||
ptime=time.strptime(word,'%d/%m/%Y')
|
||||
date=datetime.date(ptime.tm_year, ptime.tm_mon, ptime.tm_mday)
|
||||
if date not in days:
|
||||
days.append(date)
|
||||
elif len(parts)==2:
|
||||
# ex : 18/12/2017-20/12/2017 ou 9h-10h30
|
||||
if date_pattern.match(parts[0]) and date_pattern.match(parts[1]):
|
||||
# ex : 18/12/2017-20/12/2017
|
||||
pstart=time.strptime(parts[0],'%d/%m/%Y')
|
||||
pstop=time.strptime(parts[1],'%d/%m/%Y')
|
||||
if pstop<=pstart:
|
||||
raise ValueError('Day %s <= %s' % (parts[1],parts[0]))
|
||||
|
||||
date=datetime.date(pstart.tm_year, pstart.tm_mon, pstart.tm_mday)
|
||||
stop_date=datetime.date(pstop.tm_year, pstart.tm_mon, pstart.tm_mday)
|
||||
while date<=stop_date:
|
||||
if date not in days:
|
||||
days.append(date)
|
||||
date+=datetime.timedelta(days=1)
|
||||
else:
|
||||
# ex : 9h-10h30
|
||||
mstart=time_pattern.match(parts[0])
|
||||
mstop=time_pattern.match(parts[1])
|
||||
if not mstart or not mstop:
|
||||
raise ValueError('"%s" is not a valid time period' % word)
|
||||
hstart=datetime.time(int(mstart.group(1)), int(mstart.group(2) or 0))
|
||||
hstop=datetime.time(int(mstop.group(1)), int(mstop.group(2) or 0))
|
||||
if hstop<=hstart:
|
||||
raise ValueError('Time %s <= %s' % (parts[1],parts[0]))
|
||||
hours_periods.append({'start': hstart, 'stop': hstop})
|
||||
else:
|
||||
raise ValueError('Invalid number of part in this word : "%s"' % word)
|
||||
if not days:
|
||||
raise ValueError('No days found in value "%s"' % word)
|
||||
exceptional_closures.append({'days': days, 'hours_periods': hours_periods})
|
||||
return exceptional_closures
|
||||
|
||||
|
||||
def parse_normal_opening_hours(values):
|
||||
normal_opening_hours=[]
|
||||
time_pattern=re.compile('^([0-9]{1,2})h([0-9]{2})?$')
|
||||
for value in values:
|
||||
days=[]
|
||||
hours_periods=[]
|
||||
words=value.strip().split()
|
||||
for word in words:
|
||||
if word=='':
|
||||
continue
|
||||
parts=word.split('-')
|
||||
if len(parts)==1:
|
||||
# ex : jeudi
|
||||
if word not in week_days:
|
||||
raise ValueError('"%s" is not a valid week day' % word)
|
||||
if word not in days:
|
||||
days.append(word)
|
||||
elif len(parts)==2:
|
||||
# ex : lundi-jeudi ou 9h-10h30
|
||||
if parts[0] in week_days and parts[1] in week_days:
|
||||
# ex : lundi-jeudi
|
||||
if week_days.index(parts[1]) <= week_days.index(parts[0]):
|
||||
raise ValueError('"%s" is before "%s"' % (parts[1],parts[0]))
|
||||
started=False
|
||||
for d in week_days:
|
||||
if not started and d!=parts[0]:
|
||||
continue
|
||||
started=True
|
||||
if d not in days:
|
||||
days.append(d)
|
||||
if d==parts[1]:
|
||||
break
|
||||
else:
|
||||
#ex : 9h-10h30
|
||||
mstart=time_pattern.match(parts[0])
|
||||
mstop=time_pattern.match(parts[1])
|
||||
if not mstart or not mstop:
|
||||
raise ValueError('"%s" is not a valid time period' % word)
|
||||
hstart=datetime.time(int(mstart.group(1)), int(mstart.group(2) or 0))
|
||||
hstop=datetime.time(int(mstop.group(1)), int(mstop.group(2) or 0))
|
||||
if hstop<=hstart:
|
||||
raise ValueError('Time %s <= %s' % (parts[1],parts[0]))
|
||||
hours_periods.append({'start': hstart, 'stop': hstop})
|
||||
else:
|
||||
raise ValueError('Invalid number of part in this word : "%s"' % word)
|
||||
if not days and not hours_periods:
|
||||
raise ValueError('No days or hours period found in this value : "%s"' % value)
|
||||
normal_opening_hours.append({'days': days, 'hours_periods': hours_periods})
|
||||
return normal_opening_hours
|
||||
|
||||
def is_closed(normal_opening_hours_values=[],exceptional_closures_values=[],nonworking_public_holidays_values=[], when=datetime.datetime.now(), on_error='raise', exceptional_closure_on_nonworking_public_days=False):
|
||||
when_date=when.date()
|
||||
when_time=when.time()
|
||||
when_weekday=week_days[when.timetuple().tm_wday]
|
||||
on_error_result=None
|
||||
if on_error=='closed':
|
||||
on_error_result={'closed': True, 'exceptional_closure': False, 'exceptional_closure_all_day': False}
|
||||
elif on_error=='opened':
|
||||
on_error_result={'closed': False, 'exceptional_closure': False, 'exceptional_closure_all_day': False}
|
||||
|
||||
logging.debug("%s => %s / %s / %s" % (when, when_date, when_time, when_weekday))
|
||||
if len(nonworking_public_holidays_values)>0:
|
||||
logging.debug("Nonworking public holidays : %s" % nonworking_public_holidays_values)
|
||||
nonworking_days=nonworking_french_public_days_of_the_year()
|
||||
for day in nonworking_public_holidays_values:
|
||||
if day in nonworking_days and when_date==nonworking_days[day]:
|
||||
logging.debug("Non working day : %s" % day)
|
||||
return {'closed': True, 'exceptional_closure': exceptional_closure_on_nonworking_public_days, 'exceptional_closure_all_day': exceptional_closure_on_nonworking_public_days}
|
||||
|
||||
if len(exceptional_closures_values)>0:
|
||||
try:
|
||||
exceptional_closures=parse_exceptional_closures(exceptional_closures_values)
|
||||
logging.debug('Exceptional closures : %s' % exceptional_closures)
|
||||
except Exception, e:
|
||||
logging.error("%s => Not closed by default" % e)
|
||||
if on_error_result is None:
|
||||
raise e
|
||||
return on_error_result
|
||||
for cl in exceptional_closures:
|
||||
if when_date not in cl['days']:
|
||||
logging.debug("when_date (%s) no in days (%s)" % (when_date,cl['days']))
|
||||
continue
|
||||
if not cl['hours_periods']:
|
||||
# All day exceptional closure
|
||||
return {'closed': True, 'exceptional_closure': True, 'exceptional_closure_all_day': True}
|
||||
for hp in cl['hours_periods']:
|
||||
if hp['start']<=when_time and hp['stop']>= when_time:
|
||||
return {'closed': True, 'exceptional_closure': True, 'exceptional_closure_all_day': False}
|
||||
|
||||
if len(normal_opening_hours_values)>0:
|
||||
try:
|
||||
normal_opening_hours=parse_normal_opening_hours(normal_opening_hours_values)
|
||||
logging.debug('Normal opening hours : %s' % normal_opening_hours)
|
||||
except Exception, e:
|
||||
logging.error("%s => Not closed by default" % e)
|
||||
if on_error_result is None:
|
||||
raise e
|
||||
return on_error_result
|
||||
for oh in normal_opening_hours:
|
||||
if oh['days'] and when_weekday not in oh['days']:
|
||||
logging.debug("when_weekday (%s) no in days (%s)" % (when_weekday,oh['days']))
|
||||
continue
|
||||
if not oh['hours_periods']:
|
||||
# All day opened
|
||||
return {'closed': False, 'exceptional_closure': False, 'exceptional_closure_all_day': False}
|
||||
for hp in oh['hours_periods']:
|
||||
if hp['start']<=when_time and hp['stop']>= when_time:
|
||||
return {'closed': False, 'exceptional_closure': False, 'exceptional_closure_all_day': False}
|
||||
logging.debug("Not in normal opening hours => closed")
|
||||
return {'closed': True, 'exceptional_closure': False, 'exceptional_closure_all_day': False}
|
||||
|
||||
# Not a nonworking day, not during exceptional closure and no normal opening hours defined => Opened
|
||||
return {'closed': False, 'exceptional_closure': False, 'exceptional_closure_all_day': False}
|
||||
|
||||
|
53
tests/tests_opening_hours.py
Normal file
53
tests/tests_opening_hours.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
import os, sys
|
||||
sys.path.insert(0,os.path.dirname(os.path.dirname(os.path.abspath( __file__ ))))
|
||||
import opening_hours
|
||||
import datetime, logging
|
||||
|
||||
debug=True
|
||||
|
||||
if debug:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
exceptional_closures=["22/09/2017", "20/09/2017-22/09/2017", "20/09/2017-22/09/2017 18/09/2017","25/11/2017", "26/11/2017 9h30-12h30"]
|
||||
print "Raw exceptional closures value : %s" % exceptional_closures
|
||||
print "Parsed exceptional closures : %s" % opening_hours.parse_exceptional_closures(exceptional_closures)
|
||||
|
||||
normal_opening_hours=["lundi-mardi jeudi 9h30-12h30 14h-16h30", "mercredi vendredi 9h30-12h30 14h-17h"]
|
||||
print "Raw normal opening hours : %s" % normal_opening_hours
|
||||
print "Parsed normal opening hours : %s" % opening_hours.parse_normal_opening_hours(normal_opening_hours)
|
||||
|
||||
nonworking_public_holidays=[
|
||||
'1janvier',
|
||||
'paques',
|
||||
'lundi_paques',
|
||||
'1mai',
|
||||
'8mai',
|
||||
'jeudi_ascension',
|
||||
'lundi_pentecote',
|
||||
'14juillet',
|
||||
'15aout',
|
||||
'1novembre',
|
||||
'11novembre',
|
||||
'noel',
|
||||
]
|
||||
print "Raw nonworking_public_holidays values : %s" % nonworking_public_holidays
|
||||
|
||||
print "Is closed (now) : %s" % opening_hours.is_closed(normal_opening_hours,exceptional_closures,nonworking_public_holidays)
|
||||
|
||||
tests=[
|
||||
{ 'date_time': datetime.datetime(2017, 5, 1, 20, 15), 'result': {'exceptional_closure': False, 'closed': True, 'exceptional_closure_all_day': False} },
|
||||
{ 'date_time': datetime.datetime(2017, 5, 2, 15, 15), 'result': {'exceptional_closure': False, 'closed': False, 'exceptional_closure_all_day': False} },
|
||||
{ 'date_time': datetime.datetime(2017, 12, 25, 20, 15), 'result': {'exceptional_closure': False, 'closed': True, 'exceptional_closure_all_day': False} },
|
||||
{ 'date_time': datetime.datetime(2017, 9, 22, 15, 15), 'result': {'exceptional_closure': True, 'closed': True, 'exceptional_closure_all_day': True} },
|
||||
{ 'date_time': datetime.datetime(2017, 11, 25, 15, 15), 'result': {'exceptional_closure': True, 'closed': True, 'exceptional_closure_all_day': True} },
|
||||
{ 'date_time': datetime.datetime(2017, 11, 26, 11, 15), 'result': {'exceptional_closure': True, 'closed': True, 'exceptional_closure_all_day': False} },
|
||||
]
|
||||
for test in tests:
|
||||
result=opening_hours.is_closed(normal_opening_hours,exceptional_closures,nonworking_public_holidays, test['date_time'])
|
||||
if result == test['result']:
|
||||
status='OK'
|
||||
else:
|
||||
status='ERROR'
|
||||
print "Is closed (%s) : %s => %s" % (test['date_time'].isoformat(), result, status)
|
Loading…
Reference in a new issue