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}