python-mylib/mylib/opening_hours.py

256 lines
11 KiB
Python
Raw Normal View History

2021-05-19 19:19:57 +02:00
# -*- coding: utf-8 -*-
""" Opening hours helpers """
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})?$')
2021-05-19 19:19:57 +02:00
def easter_date(year):
2023-01-06 19:36:14 +01:00
""" Compute easter date for the specified year """
2021-05-19 19:19:57 +02:00
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)
2021-05-19 19:19:57 +02:00
def nonworking_french_public_days_of_the_year(year=None):
2023-01-06 19:36:14 +01:00
""" Compute dict of nonworking french public days for the specified year """
if year is None:
2021-05-19 19:19:57 +02:00
year = datetime.date.today().year
dp = easter_date(year)
return {
'1janvier': datetime.date(year, 1, 1),
'paques': dp,
2021-05-19 19:19:57 +02:00
'lundi_paques': (dp + datetime.timedelta(1)),
'1mai': datetime.date(year, 5, 1),
'8mai': datetime.date(year, 5, 8),
2021-05-19 19:19:57 +02:00
'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),
}
2021-05-19 19:19:57 +02:00
def parse_exceptional_closures(values):
2023-01-06 19:36:14 +01:00
""" Parse exceptional closures values """
2021-05-19 19:19:57 +02:00
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:
2021-05-19 19:19:57 +02:00
# 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:
2021-05-19 19:19:57 +02:00
# ex: 18/12/2017-20/12/2017 ou 9h-10h30
if date_pattern.match(parts[0]) and date_pattern.match(parts[1]):
2021-05-19 19:19:57 +02:00
# ex: 18/12/2017-20/12/2017
pstart = time.strptime(parts[0], date_format)
pstop = time.strptime(parts[1], date_format)
if pstop <= pstart:
2023-01-06 22:13:28 +01:00
raise ValueError(f'Day {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:
2021-05-19 19:19:57 +02:00
# ex: 9h-10h30
mstart = time_pattern.match(parts[0])
mstop = time_pattern.match(parts[1])
if not mstart or not mstop:
2023-01-06 22:13:28 +01:00
raise ValueError(f'"{word}" is not a valid time period')
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:
2023-01-06 22:13:28 +01:00
raise ValueError(f'Time {parts[1]} <= {parts[0]}')
hours_periods.append({'start': hstart, 'stop': hstop})
else:
2023-01-06 22:13:28 +01:00
raise ValueError(f'Invalid number of part in this word: "{word}"')
if not days:
2023-01-06 22:13:28 +01:00
raise ValueError(f'No days found in value "{value}"')
exceptional_closures.append({'days': days, 'hours_periods': hours_periods})
return exceptional_closures
def parse_normal_opening_hours(values):
2023-01-06 19:36:14 +01:00
""" Parse normal opening hours """
2021-05-19 19:19:57 +02:00
normal_opening_hours = []
for value in values:
2021-05-19 19:19:57 +02:00
days = []
hours_periods = []
words = value.strip().split()
for word in words:
2021-05-19 19:19:57 +02:00
if not word:
continue
2021-05-19 19:19:57 +02:00
parts = word.split('-')
if len(parts) == 1:
# ex: jeudi
if word not in week_days:
2023-01-06 22:13:28 +01:00
raise ValueError(f'"{word}" is not a valid week day')
if word not in days:
days.append(word)
2021-05-19 19:19:57 +02:00
elif len(parts) == 2:
# ex: lundi-jeudi ou 9h-10h30
if parts[0] in week_days and parts[1] in week_days:
2021-05-19 19:19:57 +02:00
# ex: lundi-jeudi
if week_days.index(parts[1]) <= week_days.index(parts[0]):
2023-01-06 22:13:28 +01:00
raise ValueError(f'"{parts[1]}" is before "{parts[0]}"')
2021-05-19 19:19:57 +02:00
started = False
for d in week_days:
2021-05-19 19:19:57 +02:00
if not started and d != parts[0]:
continue
2021-05-19 19:19:57 +02:00
started = True
if d not in days:
days.append(d)
2021-05-19 19:19:57 +02:00
if d == parts[1]:
break
else:
2021-05-19 19:19:57 +02:00
# ex: 9h-10h30
mstart = time_pattern.match(parts[0])
mstop = time_pattern.match(parts[1])
if not mstart or not mstop:
2023-01-06 22:13:28 +01:00
raise ValueError(f'"{word}" is not a valid time period')
2021-05-19 19:19:57 +02:00
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:
2023-01-06 22:13:28 +01:00
raise ValueError(f'Time {parts[1]} <= {parts[0]}')
hours_periods.append({'start': hstart, 'stop': hstop})
else:
2023-01-06 22:13:28 +01:00
raise ValueError(f'Invalid number of part in this word: "{word}"')
if not days and not hours_periods:
2023-01-06 22:13:28 +01:00
raise ValueError(f'No days or hours period found in this value: "{value}"')
normal_opening_hours.append({'days': days, 'hours_periods': hours_periods})
return normal_opening_hours
2021-05-19 19:19:57 +02:00
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'
):
2023-01-06 19:36:14 +01:00
""" Check if closed """
if not when:
when = datetime.datetime.now()
2021-05-19 19:19:57 +02:00
when_date = when.date()
when_time = when.time()
when_weekday = week_days[when.timetuple().tm_wday]
on_error_result = None
if on_error == 'closed':
2023-01-06 22:13:28 +01:00
on_error_result = {
'closed': True, 'exceptional_closure': False,
'exceptional_closure_all_day': False}
2021-05-19 19:19:57 +02:00
elif on_error == 'opened':
2023-01-06 22:13:28 +01:00
on_error_result = {
'closed': False, 'exceptional_closure': False,
'exceptional_closure_all_day': False}
2023-01-06 22:13:28 +01:00
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)
2021-05-19 19:19:57 +02:00
nonworking_days = nonworking_french_public_days_of_the_year()
for day in nonworking_public_holidays_values:
2021-05-19 19:19:57 +02:00
if day in nonworking_days and when_date == nonworking_days[day]:
log.debug("Non working day: %s", day)
2023-01-06 22:13:28 +01:00
return {
'closed': True,
'exceptional_closure': exceptional_closure_on_nonworking_public_days,
'exceptional_closure_all_day': exceptional_closure_on_nonworking_public_days
}
2021-05-19 19:19:57 +02:00
if exceptional_closures_values:
try:
2021-05-19 19:19:57 +02:00
exceptional_closures = parse_exceptional_closures(exceptional_closures_values)
log.debug('Exceptional closures: %s', exceptional_closures)
2021-05-19 19:19:57 +02:00
except ValueError 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
2023-01-06 22:13:28 +01:00
return {
'closed': True, 'exceptional_closure': True,
'exceptional_closure_all_day': True}
for hp in cl['hours_periods']:
if hp['start'] <= when_time <= hp['stop']:
2023-01-06 22:13:28 +01:00
return {
'closed': True, 'exceptional_closure': True,
'exceptional_closure_all_day': False}
if normal_opening_hours_values:
try:
2021-05-19 19:19:57 +02:00
normal_opening_hours = parse_normal_opening_hours(normal_opening_hours_values)
log.debug('Normal opening hours: %s', normal_opening_hours)
2021-05-19 19:19:57 +02:00
except ValueError as e: # pylint: disable=broad-except
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
2023-01-06 22:13:28 +01:00
return {
'closed': False, 'exceptional_closure': False,
'exceptional_closure_all_day': False}
for hp in oh['hours_periods']:
if hp['start'] <= when_time <= hp['stop']:
2023-01-06 22:13:28 +01:00
return {
'closed': False, 'exceptional_closure': False,
'exceptional_closure_all_day': False}
log.debug("Not in normal opening hours => closed")
2023-01-06 22:13:28 +01:00
return {
'closed': True, 'exceptional_closure': False,
'exceptional_closure_all_day': False}
2023-01-06 22:13:28 +01:00
# 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}