218 lines
9.8 KiB
Python
218 lines
9.8 KiB
Python
|
import datetime
|
||
|
import re
|
||
|
import time
|
||
|
import logging
|
||
|
|
||
|
log = logging.getLogger(__name__)
|
||
|
|
||
|
week_days = ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']
|
||
|
date_format = '%d/%m/%Y'
|
||
|
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})?$')
|
||
|
|
||
|
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=[]
|
||
|
for value in values:
|
||
|
days = []
|
||
|
hours_periods = []
|
||
|
words = value.strip().split()
|
||
|
for word in words:
|
||
|
if not word:
|
||
|
continue
|
||
|
parts = word.split('-')
|
||
|
if len(parts) == 1:
|
||
|
# ex : 31/02/2017
|
||
|
ptime = time.strptime(word, date_format)
|
||
|
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], date_format)
|
||
|
pstop = time.strptime(parts[1], date_format)
|
||
|
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, pstop.tm_mon, pstop.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"' % value)
|
||
|
exceptional_closures.append({'days': days, 'hours_periods': hours_periods})
|
||
|
return exceptional_closures
|
||
|
|
||
|
|
||
|
def parse_normal_opening_hours(values):
|
||
|
normal_opening_hours=[]
|
||
|
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=None, exceptional_closures_values=None,
|
||
|
nonworking_public_holidays_values=None, exceptional_closure_on_nonworking_public_days=False,
|
||
|
when=None, on_error='raise'):
|
||
|
if not when:
|
||
|
when = datetime.datetime.now()
|
||
|
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}
|
||
|
|
||
|
log.debug("When = %s => date = %s / time = %s / week day = %s", when, when_date, when_time, when_weekday)
|
||
|
if nonworking_public_holidays_values:
|
||
|
log.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]:
|
||
|
log.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)
|
||
|
log.debug('Exceptional closures: %s', exceptional_closures)
|
||
|
except Exception as e:
|
||
|
log.error("Fail to parse exceptional closures, consider as closed", exc_info=True)
|
||
|
if on_error_result is None:
|
||
|
raise e from e
|
||
|
return on_error_result
|
||
|
for cl in exceptional_closures:
|
||
|
if when_date not in cl['days']:
|
||
|
log.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 <= hp['stop']:
|
||
|
return {'closed': True, 'exceptional_closure': True, 'exceptional_closure_all_day': False}
|
||
|
|
||
|
if normal_opening_hours_values:
|
||
|
try:
|
||
|
normal_opening_hours=parse_normal_opening_hours(normal_opening_hours_values)
|
||
|
log.debug('Normal opening hours: %s', normal_opening_hours)
|
||
|
except Exception:
|
||
|
log.error("Fail to parse normal opening hours, consider as closed", exc_info=True)
|
||
|
if on_error_result is None:
|
||
|
raise e from e
|
||
|
return on_error_result
|
||
|
for oh in normal_opening_hours:
|
||
|
if oh['days'] and when_weekday not in oh['days']:
|
||
|
log.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 <= hp['stop']:
|
||
|
return {'closed': False, 'exceptional_closure': False, 'exceptional_closure_all_day': False}
|
||
|
log.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}
|