├── requirements.txt ├── requirements-dev.txt ├── humanized_opening_hours ├── locales │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── hoh.mo │ │ │ └── hoh.pot │ ├── nl │ │ └── LC_MESSAGES │ │ │ ├── hoh.mo │ │ │ └── hoh.pot │ ├── pl │ │ └── LC_MESSAGES │ │ │ ├── hoh.mo │ │ │ └── hoh.pot │ ├── pt │ │ └── LC_MESSAGES │ │ │ ├── hoh.mo │ │ │ └── hoh.pot │ ├── fr_FR │ │ └── LC_MESSAGES │ │ │ ├── hoh.mo │ │ │ └── hoh.pot │ ├── it_IT │ │ └── LC_MESSAGES │ │ │ ├── hoh.mo │ │ │ └── hoh.pot │ └── ru_RU │ │ └── LC_MESSAGES │ │ ├── hoh.mo │ │ └── hoh.pot ├── __init__.py ├── exceptions.py ├── rendering.py ├── field.ebnf ├── frequent_fields.py ├── field_parser.py ├── temporal_objects.py └── main.py ├── MANIFEST.in ├── .flake8 ├── .coveragerc ├── setup.py ├── .gitignore ├── Makefile ├── README.md └── LICENCE /requirements.txt: -------------------------------------------------------------------------------- 1 | astral==1.7.1 2 | Babel==2.6.0 3 | pytz==2018.7 4 | lark-parser==0.6.5 5 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | astral==1.7.1 2 | Babel==2.6.0 3 | flake8==3.6.0 4 | lark-parser==0.6.5 5 | pyflakes==2.0.0 6 | pytz==2018.7 7 | twine==1.12.1 8 | requests==2.20.1 9 | -------------------------------------------------------------------------------- /humanized_opening_hours/locales/de/LC_MESSAGES/hoh.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezemika/humanized_opening_hours/HEAD/humanized_opening_hours/locales/de/LC_MESSAGES/hoh.mo -------------------------------------------------------------------------------- /humanized_opening_hours/locales/nl/LC_MESSAGES/hoh.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezemika/humanized_opening_hours/HEAD/humanized_opening_hours/locales/nl/LC_MESSAGES/hoh.mo -------------------------------------------------------------------------------- /humanized_opening_hours/locales/pl/LC_MESSAGES/hoh.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezemika/humanized_opening_hours/HEAD/humanized_opening_hours/locales/pl/LC_MESSAGES/hoh.mo -------------------------------------------------------------------------------- /humanized_opening_hours/locales/pt/LC_MESSAGES/hoh.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezemika/humanized_opening_hours/HEAD/humanized_opening_hours/locales/pt/LC_MESSAGES/hoh.mo -------------------------------------------------------------------------------- /humanized_opening_hours/locales/fr_FR/LC_MESSAGES/hoh.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezemika/humanized_opening_hours/HEAD/humanized_opening_hours/locales/fr_FR/LC_MESSAGES/hoh.mo -------------------------------------------------------------------------------- /humanized_opening_hours/locales/it_IT/LC_MESSAGES/hoh.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezemika/humanized_opening_hours/HEAD/humanized_opening_hours/locales/it_IT/LC_MESSAGES/hoh.mo -------------------------------------------------------------------------------- /humanized_opening_hours/locales/ru_RU/LC_MESSAGES/hoh.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezemika/humanized_opening_hours/HEAD/humanized_opening_hours/locales/ru_RU/LC_MESSAGES/hoh.mo -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include humanized_opening_hours *.po 2 | recursive-include humanized_opening_hours *.mo 3 | include humanized_opening_hours/*.ebnf 4 | include *.txt 5 | include *.md 6 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | 3 | max_line_length = 80 4 | ignore = 5 | W293, 6 | E226 7 | exclude = 8 | .git, 9 | __pycache__, 10 | dist, 11 | build 12 | builtins= _ 13 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | pragma: no cover 4 | def __str__ 5 | def __repr__ 6 | dt = datetime.date(time)?.today 7 | 8 | [run] 9 | include = 10 | humanized_opening_hours/main.py 11 | humanized_opening_hours/frequent_fields.py 12 | humanized_opening_hours/temporal_objects.py 13 | humanized_opening_hours/field_parser.py 14 | humanized_opening_hours/rendering.py 15 | humanized_opening_hours/exceptions.py 16 | -------------------------------------------------------------------------------- /humanized_opening_hours/__init__.py: -------------------------------------------------------------------------------- 1 | """A parser for the opening_hours fields from OpenStreetMap. 2 | 3 | Provides an OHParser object with the most useful methods 4 | (`is_open()`, `next_change()`, etc). Allows you to set 5 | public and school holidays. Provides a `description()` method 6 | to get a human-readable describing of the opening hours. 7 | 8 | Automatically sanitizes the fields to prevent some common mistakes. 9 | 10 | To get started, simply do: 11 | >>> import humanized_opening_hours as hoh 12 | >>> oh = hoh.OHParser("Mo-Sa 10:00-19:00") 13 | """ 14 | # flake8: noqa 15 | 16 | __version__ = "1.0.0b3" 17 | __appname__ = "osm_humanized_opening_hours" 18 | __author__ = "rezemika " 19 | __licence__ = "AGPLv3" 20 | 21 | import os as _os 22 | import gettext as _gettext 23 | _gettext.install("HOH", 24 | _os.path.join( 25 | _os.path.dirname(_os.path.realpath(__file__)), "locales" 26 | ) 27 | ) 28 | 29 | from humanized_opening_hours.main import OHParser, sanitize, days_of_week 30 | from humanized_opening_hours.temporal_objects import easter_date 31 | from humanized_opening_hours.rendering import AVAILABLE_LOCALES 32 | from humanized_opening_hours import exceptions 33 | -------------------------------------------------------------------------------- /humanized_opening_hours/exceptions.py: -------------------------------------------------------------------------------- 1 | class HOHError(Exception): 2 | """Base class for HOH errors.""" 3 | pass 4 | 5 | 6 | class ParseError(HOHError): 7 | """ 8 | Raised when field parsing fails. 9 | """ 10 | pass 11 | 12 | 13 | class SolarHoursError(HOHError): 14 | """ 15 | Raised when trying to get a time from a solar hour 16 | without having defined them. 17 | """ 18 | pass 19 | 20 | 21 | class CommentOnlyField(ParseError): 22 | """ 23 | Raised when a field contains only a comment. 24 | The comment is accessible via the 'comment' attribute. 25 | """ 26 | def __init__(self, message, comment): 27 | super().__init__(message) 28 | self.comment = comment 29 | 30 | 31 | class AlwaysClosed(ParseError): 32 | """ 33 | Raised when trying to parse a field which only indicates "closed" or "off". 34 | """ 35 | pass 36 | 37 | 38 | class NextChangeRecursionError(HOHError): 39 | """ 40 | Raised when reaching the maximum recursion in 41 | the 'OHParser.next_change()' method. 42 | """ 43 | def __init__(self, message, last_change): 44 | super().__init__(message) 45 | self.last_change = last_change 46 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os 3 | 4 | import humanized_opening_hours 5 | 6 | BASE_DIR = os.path.dirname(os.path.realpath(__file__)) 7 | 8 | setup( 9 | name="osm_humanized_opening_hours", 10 | version=humanized_opening_hours.__version__, 11 | packages=find_packages(exclude=["doc", "tests"]), 12 | author="rezemika", 13 | author_email="reze.mika@gmail.com", 14 | description="A parser for the opening_hours fields from OpenStreetMap.", 15 | long_description=open(BASE_DIR + "/README.md", 'r').read(), 16 | install_requires=["lark-parser", "babel", "astral"], 17 | include_package_data=True, 18 | url='http://github.com/rezemika/humanized_opening_hours', 19 | keywords="openstreetmap opening_hours parser", 20 | classifiers=[ 21 | "Programming Language :: Python", 22 | "Development Status :: 3 - Alpha", 23 | "Intended Audience :: Developers", 24 | "License :: OSI Approved :: GNU Affero General Public License v3", 25 | "Natural Language :: English", 26 | "Natural Language :: French", 27 | "Operating System :: OS Independent", 28 | "Programming Language :: Python :: 3", 29 | "Topic :: Utilities", 30 | "Topic :: Other/Nonlisted Topic", 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Django stuff: 50 | *.log 51 | local_settings.py 52 | 53 | # Flask stuff: 54 | instance/ 55 | .webassets-cache 56 | 57 | # Scrapy stuff: 58 | .scrapy 59 | 60 | # Sphinx documentation 61 | docs/_build/ 62 | 63 | # PyBuilder 64 | target/ 65 | 66 | # Jupyter Notebook 67 | .ipynb_checkpoints 68 | 69 | # pyenv 70 | .python-version 71 | 72 | # celery beat schedule file 73 | celerybeat-schedule 74 | 75 | # SageMath parsed files 76 | *.sage.py 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | .venv 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | .spyproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | 93 | # mkdocs documentation 94 | /site 95 | 96 | # mypy 97 | .mypy_cache/ 98 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | freeze: 2 | pip3 freeze | grep -Ev "pkg-resources|twine|osm-humanized-opening-hours|mccabe|pycodestyle|pyflakes|flake8|requests|idna|pkginfo|tqdm|urllib3|chardet|certifi" > requirements.txt 3 | 4 | freeze-dev: 5 | pip3 freeze | grep -Ev "pkg-resources|osm-humanized-opening-hours" > requirements-dev.txt 6 | 7 | tests: 8 | python3 tests.py 9 | 10 | flake8: 11 | python3 -m flake8 humanized_opening_hours 12 | 13 | benchmark-simple: 14 | @echo "=== Time for a single field:" 15 | @python3 -m timeit -v -r 5 -u sec -n 1 -s 'import humanized_opening_hours as hoh' 'oh = hoh.OHParser("Mo-Fr 08:00-19:00")' 16 | @echo "=== Time for 10 fields:" 17 | @python3 -m timeit -v -r 5 -u sec -n 10 -s 'import humanized_opening_hours as hoh' 'oh = hoh.OHParser("Mo-Fr 08:00-19:00")' 18 | @echo "=== Time for 100 fields:" 19 | @python3 -m timeit -v -r 5 -u sec -n 100 -s 'import humanized_opening_hours as hoh' 'oh = hoh.OHParser("Mo-Fr 08:00-19:00")' 20 | @echo "=== Time for 1000 fields:" 21 | @python3 -m timeit -v -r 5 -u sec -n 1000 -s 'import humanized_opening_hours as hoh' 'oh = hoh.OHParser("Mo-Fr 08:00-19:00")' 22 | 23 | benchmark-complex: 24 | @echo "=== Time for a single field:" 25 | @python3 -m timeit -v -r 5 -u sec -n 1 -s 'import humanized_opening_hours as hoh' 'oh = hoh.OHParser("Jan-Feb Mo-Fr 08:00-19:00")' 26 | @echo "=== Time for 10 fields:" 27 | @python3 -m timeit -v -r 5 -u sec -n 10 -s 'import humanized_opening_hours as hoh' 'oh = hoh.OHParser("Jan-Feb Mo-Fr 08:00-19:00")' 28 | @echo "=== Time for 100 fields:" 29 | @python3 -m timeit -v -r 5 -u sec -n 100 -s 'import humanized_opening_hours as hoh' 'oh = hoh.OHParser("Jan-Feb Mo-Fr 08:00-19:00")' 30 | @echo "=== Time for 1000 fields:" 31 | @python3 -m timeit -v -r 5 -u sec -n 1000 -s 'import humanized_opening_hours as hoh' 'oh = hoh.OHParser("Jan-Feb Mo-Fr 08:00-19:00")' 32 | 33 | coverage: 34 | @coverage erase 35 | coverage run tests.py 36 | @clear 37 | coverage report -m 38 | 39 | help: 40 | @echo "Available commands:" 41 | @echo " freeze Updates 'requirements.txt'" 42 | @echo " freeze-dev Updates 'requirements-dev.txt'" 43 | @echo " tests Runs unit tests" 44 | @echo " flake8 Runs flake8 tests" 45 | @echo " benchmark Runs benchmark for 1, 10, 100 and 1000 fields" 46 | -------------------------------------------------------------------------------- /humanized_opening_hours/rendering.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | import os 3 | 4 | import babel.lists 5 | 6 | AVAILABLE_LOCALES = ["en", "fr", "de", "ru", "nl", "pt", "it"] 7 | 8 | BASE_DIR = os.path.dirname(os.path.realpath(__file__)) 9 | 10 | 11 | def set_locale(babel_locale): 12 | try: 13 | lang = gettext.translation( 14 | 'hoh', 15 | localedir=os.path.join(BASE_DIR, "locales"), 16 | languages=[babel_locale.language] 17 | ) 18 | except FileNotFoundError: 19 | lang = gettext.NullTranslations() 20 | lang.install() 21 | 22 | 23 | def join_list(l: list, babel_locale) -> str: # pragma: no cover 24 | """Returns a string from a list and a locale.""" 25 | if not l: 26 | return '' 27 | values = [str(value) for value in l] 28 | return babel.lists.format_list(values, locale=babel_locale) 29 | 30 | 31 | def translate_open_closed(babel_locale): 32 | set_locale(babel_locale) 33 | return (_("open"), _("closed")) 34 | 35 | 36 | def translate_colon(babel_locale): 37 | set_locale(babel_locale) 38 | return _("{}: {}") 39 | 40 | 41 | # TODO : Put these functions into a unique class? 42 | # TODO : Handle "datetime.time.max" (returns "23:59" instead of "24:00"). 43 | def render_time(time, babel_locale): 44 | """Returns a string from a Time object.""" 45 | set_locale(babel_locale) 46 | if time.t[0] == "normal": 47 | return babel.dates.format_time( 48 | time.t[1], locale=babel_locale, format="short" 49 | ) 50 | if time.t[2].total_seconds() == 0: 51 | return { 52 | "sunrise": _("sunrise"), 53 | "sunset": _("sunset"), 54 | "dawn": _("dawn"), 55 | "dusk": _("dusk") 56 | }.get(time.t[0]) 57 | if time.t[1] == 1: 58 | delta_str = babel.dates.format_timedelta( 59 | time.t[2], locale=babel_locale, format="long", threshold=2 60 | ) 61 | return { 62 | "sunrise": _("{time} after sunrise"), 63 | "sunset": _("{time} after sunset"), 64 | "dawn": _("{time} after dawn"), 65 | "dusk": _("{time} after dusk") 66 | }.get(time.t[0]).format(time=delta_str) 67 | else: 68 | delta_str = babel.dates.format_timedelta( 69 | time.t[2], locale=babel_locale, format="long", threshold=2 70 | ) 71 | return { 72 | "sunrise": _("{time} before sunrise"), 73 | "sunset": _("{time} before sunset"), 74 | "dawn": _("{time} before dawn"), 75 | "dusk": _("{time} before dusk") 76 | }.get(time.t[0]).format(time=delta_str) 77 | 78 | 79 | def render_timespan(timespan, babel_locale): 80 | """Returns a string from a TimeSpan object and a locale.""" 81 | return babel_locale.interval_formats[None].format( 82 | render_time(timespan.beginning, babel_locale), 83 | render_time(timespan.end, babel_locale) 84 | ) 85 | -------------------------------------------------------------------------------- /humanized_opening_hours/field.ebnf: -------------------------------------------------------------------------------- 1 | WDAY : "Mo" | "Tu" | "We" | "Th" | "Fr" | "Sa" | "Su" 2 | MONTH : "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" | "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec" 3 | PUBLIC_HOLIDAY : "PH" 4 | SCHOOL_HOLIDAY : "SH" 5 | EVENT : "dawn" | "sunrise" | "sunset" | "dusk" 6 | EASTER : "easter" 7 | OPEN : " open" 8 | CLOSED : " closed" | " off" 9 | ALWAYS_OPEN.2 : "24/7" 10 | WEEK.2 : "week " 11 | 12 | TWO_DIGITS.1 : /[0-9][0-9]+/ 13 | YEAR.3 : /([12][0-9]{3})+/ 14 | 15 | PLUS_OR_MINUS : "+" | "-" 16 | 17 | // 18 | 19 | // The support of the comma as rule separator is not supported yet 20 | // in the parsing code (comma and semicolons are treated as equals), 21 | // but the current implementation of rules is not perfect and doesn't 22 | // perfectly fits the specifications, so it makes no difference yet. 23 | time_domain : rule_sequence (("; "|", ") rule_sequence)* 24 | 25 | rule_sequence : ALWAYS_OPEN -> always_open_rule 26 | | range_selectors " " time_selector 27 | | range_selectors " " time_selector rule_modifier 28 | | time_selector 29 | | time_selector rule_modifier -> time_modifier_rule 30 | | range_selectors rule_modifier -> range_modifier_rule 31 | // | rule_modifier -> modifier_only_rule // TODO: Allow only "off". 32 | 33 | rule_modifier : OPEN -> rule_modifier_open 34 | | CLOSED -> rule_modifier_closed 35 | 36 | // 37 | 38 | %ignore " " 39 | range_selectors : [year_selector] [monthday_selector] [week_selector] [":"] [weekday_selector] 40 | 41 | // 42 | 43 | weekday_selector : (weekday_sequence | holiday ("," holiday)*) -> weekday_or_holiday_sequence_selector 44 | | holiday ("," holiday)* "," weekday_sequence -> holiday_and_weekday_sequence_selector 45 | | weekday_sequence "," holiday ("," holiday)* -> holiday_and_weekday_sequence_selector // Not valid but frequent pattern. 46 | | holiday ("," holiday)* " " weekday_sequence -> holiday_in_weekday_sequence_selector 47 | weekday_sequence : weekday_range ("," weekday_range)* 48 | weekday_range : WDAY 49 | | WDAY "-" WDAY 50 | holiday : PUBLIC_HOLIDAY 51 | | SCHOOL_HOLIDAY 52 | 53 | // 54 | 55 | %import common.INT 56 | //day_offset : " " PLUS_OR_MINUS INT " day" ["s"] 57 | 58 | // 59 | 60 | time_selector : timespan ("," timespan)* 61 | timespan : time "-" time 62 | time : hour_minutes | variable_time 63 | hour_minutes : TWO_DIGITS ":" TWO_DIGITS 64 | variable_time : EVENT 65 | | "(" EVENT PLUS_OR_MINUS hour_minutes ")" 66 | 67 | // 68 | 69 | year_selector : year_range ("," year_range)* 70 | year_range : year 71 | | year "-" year 72 | | year "-" year "/" INT 73 | year : YEAR 74 | 75 | // 76 | 77 | // TODO : Prevent case like "Jan 1-5-Feb 1-5" (monthday_date - monthday_date) 78 | monthday_selector : monthday_range ("," monthday_range)* 79 | monthday_range : monthday_date // "Dec 25" 80 | | monthday_date "-" monthday_date // "Jan 1-Feb 1" 81 | monthday_date : [year " "] MONTH " " INT -> monthday_date_monthday 82 | | [year " "] MONTH " " INT "-" INT -> monthday_date_day_to_day 83 | | [year " "] MONTH -> monthday_date_month 84 | | [year " "] EASTER -> monthday_date_easter 85 | 86 | // 87 | 88 | week_selector : WEEK week ("," week)* // TODO 89 | week : weeknum 90 | | weeknum "-" weeknum 91 | | weeknum "-" weeknum "/" INT 92 | weeknum : INT 93 | -------------------------------------------------------------------------------- /humanized_opening_hours/locales/de/LC_MESSAGES/hoh.pot: -------------------------------------------------------------------------------- 1 | # German translation of HOH. 2 | # Copyright (C) 2018 3 | # FELIX KLEMENT mail@felix-klement.de, 2018. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2019-05-13 09:40+0200\n" 10 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 11 | "Last-Translator: FELIX KLEMENT mail@felix-klement.de\n" 12 | "Language-Team: LANGUAGE \n" 13 | "Language: de_DE\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | 18 | #: rendering.py:33 19 | msgid "open" 20 | msgstr "offen" 21 | 22 | #: rendering.py:33 temporal_objects.py:221 23 | msgid "closed" 24 | msgstr "geschlossen" 25 | 26 | #: rendering.py:38 temporal_objects.py:230 27 | msgid "{}: {}" 28 | msgstr "{}: {}" 29 | 30 | #: rendering.py:52 31 | msgid "sunrise" 32 | msgstr "Sonnenaufgang" 33 | 34 | #: rendering.py:53 35 | msgid "sunset" 36 | msgstr "Sonnenuntergang" 37 | 38 | #: rendering.py:54 39 | msgid "dawn" 40 | msgstr "Morgendämmerung" 41 | 42 | #: rendering.py:55 43 | msgid "dusk" 44 | msgstr "Abenddämmerung" 45 | 46 | #: rendering.py:62 47 | msgid "{time} after sunrise" 48 | msgstr "{time} nach Sonnenaufgang" 49 | 50 | #: rendering.py:63 51 | msgid "{time} after sunset" 52 | msgstr "{time} nach Sonnenuntergang" 53 | 54 | #: rendering.py:64 55 | msgid "{time} after dawn" 56 | msgstr "{time} nach der Morgendämmerung" 57 | 58 | #: rendering.py:65 59 | msgid "{time} after dusk" 60 | msgstr "{time} nach Einbruch der Dunkelheit" 61 | 62 | #: rendering.py:72 63 | msgid "{time} before sunrise" 64 | msgstr "{time} vor Sonnenaufgang" 65 | 66 | #: rendering.py:73 67 | msgid "{time} before sunset" 68 | msgstr "{time} vor Sonnenuntergang" 69 | 70 | #: rendering.py:74 71 | msgid "{time} before dawn" 72 | msgstr "{time} vor der Morgendämmerung" 73 | 74 | #: rendering.py:75 75 | msgid "{time} before dusk" 76 | msgstr "{time} vor Einbruch der Dunkelheit" 77 | 78 | #: temporal_objects.py:210 79 | msgid "Open 24 hours a day and 7 days a week." 80 | msgstr "24 Stunden am Tag und 7 Tage die Woche geöffnet." 81 | 82 | #: temporal_objects.py:219 83 | msgid "every days" 84 | msgstr "jeden Tag" 85 | 86 | #: temporal_objects.py:339 temporal_objects.py:410 87 | msgid "on {weekday}" 88 | msgstr "am {weekday}" 89 | 90 | #: temporal_objects.py:343 temporal_objects.py:414 91 | msgid "from {weekday1} to {weekday2}" 92 | msgstr "von {weekday1} bis {weekday2}" 93 | 94 | #: temporal_objects.py:348 temporal_objects.py:426 95 | msgid "on public and school holidays" 96 | msgstr "an Feiertagen und Schulferien" 97 | 98 | #: temporal_objects.py:349 temporal_objects.py:427 99 | msgid "on public holidays" 100 | msgstr "an Feiertagen" 101 | 102 | #: temporal_objects.py:350 temporal_objects.py:428 103 | msgid "on school holidays" 104 | msgstr "in den Schulferien" 105 | 106 | #: temporal_objects.py:453 107 | msgid "in week {week}" 108 | msgstr "in Woche {week}" 109 | 110 | #: temporal_objects.py:455 111 | msgid "from week {week1} to week {week2}" 112 | msgstr "von Woche {week1} bis Woche {week2}" 113 | 114 | #: temporal_objects.py:461 115 | msgid "from week {week1} to week {week2}, every {n} weeks" 116 | msgstr "von Woche {week1} bis Woche {week2}, alle {n} Wochen" 117 | 118 | #: temporal_objects.py:486 119 | msgid "in {year}" 120 | msgstr "in {year}" 121 | 122 | #: temporal_objects.py:488 123 | msgid "from {year1} to {year2}" 124 | msgstr "von {year1} bis {year2}" 125 | 126 | #: temporal_objects.py:494 127 | msgid "from {year1} to {year2}, every {n} years" 128 | msgstr "von {year1} bis {year2}, alle {n} Jahre" 129 | 130 | #: temporal_objects.py:535 131 | msgid "from {monthday1} to {monthday2}" 132 | msgstr "von {monthday1} bis {monthday2}" 133 | 134 | #: temporal_objects.py:614 135 | msgid "on easter" 136 | msgstr "an Ostern" 137 | 138 | #: temporal_objects.py:619 139 | msgid "{month} {day1} to {day2}, {year}" 140 | msgstr "{month} {day1} bis {day2}, {year}" 141 | 142 | #: temporal_objects.py:626 143 | msgid "from {month} {day1} to {day2}" 144 | msgstr "von {month} {day1} bis {day2}" 145 | 146 | #: temporal_objects.py:637 147 | msgid "%B %-d" 148 | msgstr "%B %-d" 149 | -------------------------------------------------------------------------------- /humanized_opening_hours/frequent_fields.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from lark.lark import Tree 4 | from lark.lexer import Token 5 | 6 | from humanized_opening_hours.temporal_objects import WEEKDAYS 7 | 8 | # flake8: noqa 9 | 10 | FREQUENT_FIELDS = { 11 | "24/7": Tree("time_domain", [Tree("always_open_rule", [Token("ALWAYS_OPEN", '24/7')])]), 12 | "sunrise-sunset": Tree("time_domain", [Tree("rule_sequence", [Tree("time_selector", [Tree("timespan", [Tree("time", [Tree("variable_time", [Token("EVENT", 'sunrise')])]), Tree("time", [Tree("variable_time", [Token("EVENT", 'sunset')])])])])])]), 13 | "sunset-sunrise": Tree("time_domain", [Tree("rule_sequence", [Tree("time_selector", [Tree("timespan", [Tree("time", [Tree("variable_time", [Token("EVENT", 'sunset')])]), Tree("time", [Tree("variable_time", [Token("EVENT", 'sunrise')])])])])])]), 14 | } 15 | 16 | 17 | RE_WDAY_OFF = re.compile("^[A-Z][a-z] off$") 18 | RE_WDAY_TIMESPAN = re.compile("^[A-Z][a-z] [0-9]{2}:[0-9]{2}-[0-9]{2}:[0-9]{2}$") 19 | RE_WDAY_WDAY_TIMESPAN = re.compile("^[A-Z][a-z]-[A-Z][a-z] [0-9]{2}:[0-9]{2}-[0-9]{2}:[0-9]{2}$") 20 | RE_TIMESPAN = re.compile("^[0-9]{2}:[0-9]{2}-[0-9]{2}:[0-9]{2}$") 21 | RE_TIMESPANS = re.compile("([0-9]{2}):([0-9]{2})-([0-9]{2}):([0-9]{2})") 22 | 23 | 24 | def parse_simple_field(field): 25 | """Returns None or a tree if the field is simple enough. 26 | 27 | Simple field example: "Mo-Fr 08:00-20:00; Sa 08:00-12:00" 28 | """ 29 | # It's about 12 times faster than with Lark. 30 | # Effective for a bit more than 35% of OSM fields. 31 | splited_field = [ 32 | part.strip() for part in field.strip(' \n\t;').split(';') 33 | ] 34 | parsed_parts = [] 35 | for part in splited_field: 36 | if RE_WDAY_OFF.match(part): 37 | wday = part[:2] 38 | if wday not in WEEKDAYS: 39 | return None 40 | parsed_parts.append( 41 | Tree("range_modifier_rule", [Tree("range_selectors", [Tree("weekday_or_holiday_sequence_selector", [Tree("weekday_sequence", [Tree("weekday_range", [Token("WDAY", wday)])])])]), Tree("rule_modifier_closed", [Token("CLOSED", ' off')])]) 42 | ) 43 | elif RE_WDAY_TIMESPAN.match(part): 44 | wday = part[:2] 45 | if wday not in WEEKDAYS: 46 | return None 47 | timespans = [] 48 | for timespan in RE_TIMESPANS.findall(part): 49 | from_h, from_m, to_h, to_m = timespan 50 | timespans.append( 51 | Tree("timespan", [Tree("time", [Tree("hour_minutes", [Token("TWO_DIGITS", from_h), Token("TWO_DIGITS", from_m)])]), Tree("time", [Tree("hour_minutes", [Token("TWO_DIGITS", to_h), Token("TWO_DIGITS", to_m)])])]) 52 | ) 53 | parsed_parts.append( 54 | Tree("rule_sequence", [Tree("range_selectors", [Tree("weekday_or_holiday_sequence_selector", [Tree("weekday_sequence", [Tree("weekday_range", [Token("WDAY", wday)])])])]), Tree("time_selector", timespans)]) 55 | ) 56 | elif RE_WDAY_WDAY_TIMESPAN.match(part): 57 | wday_from, wday_to = part[:5].split('-') 58 | if wday_from not in WEEKDAYS or wday_to not in WEEKDAYS: 59 | return None 60 | timespans = [] 61 | for timespan in RE_TIMESPANS.findall(part): 62 | from_h, from_m, to_h, to_m = timespan 63 | timespans.append( 64 | Tree("timespan", [Tree("time", [Tree("hour_minutes", [Token("TWO_DIGITS", from_h), Token("TWO_DIGITS", from_m)])]), Tree("time", [Tree("hour_minutes", [Token("TWO_DIGITS", to_h), Token("TWO_DIGITS", to_m)])])]) 65 | ) 66 | parsed_parts.append( 67 | Tree("rule_sequence", [Tree("range_selectors", [Tree("weekday_or_holiday_sequence_selector", [Tree("weekday_sequence", [Tree("weekday_range", [Token("WDAY", wday_from), Token("WDAY", wday_to)])])])]), Tree("time_selector", timespans)]) 68 | ) 69 | elif RE_TIMESPAN.match(part): 70 | from_h, from_m = part[:5].split(':') 71 | to_h, to_m = part[6:].split(':') 72 | parsed_parts.append( 73 | Tree("rule_sequence", [Tree("time_selector", [Tree("timespan", [Tree("time", [Tree("hour_minutes", [Token("TWO_DIGITS", from_h), Token("TWO_DIGITS", from_m)])]), Tree("time", [Tree("hour_minutes", [Token("TWO_DIGITS", to_h), Token("TWO_DIGITS", to_m)])])])])]) 74 | ) 75 | else: 76 | return None 77 | return Tree("time_domain", parsed_parts) 78 | -------------------------------------------------------------------------------- /humanized_opening_hours/locales/nl/LC_MESSAGES/hoh.pot: -------------------------------------------------------------------------------- 1 | # Dutch translation of HOH. 2 | # Copyright (C) 2018 3 | # TIJS B tijskeb@gmail.com, 2018. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2018-10-06 16:55+0200\n" 10 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 11 | "Last-Translator: FULL NAME \n" 12 | "Language-Team: LANGUAGE \n" 13 | "Language: nl_NL\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=utf-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | 18 | #: temporal_objects.py:210 19 | msgid "Open 24 hours a day and 7 days a week." 20 | msgstr "Open 24 uur per dag en 7 dagen per week." 21 | 22 | #: temporal_objects.py:219 23 | msgid "every days" 24 | msgstr "iedere dag" 25 | 26 | #: temporal_objects.py:221 rendering.py:34 27 | msgid "closed" 28 | msgstr "gesloten" 29 | 30 | #: temporal_objects.py:230 rendering.py:39 31 | msgid "{}: {}" 32 | msgstr "{}: {}" 33 | 34 | #: temporal_objects.py:339 temporal_objects.py:410 35 | #, python-brace-format 36 | msgid "on {weekday}" 37 | msgstr "op {weekday}" 38 | 39 | #: temporal_objects.py:343 temporal_objects.py:414 40 | #, python-brace-format 41 | msgid "from {weekday1} to {weekday2}" 42 | msgstr "van {weekday1} tot {weekday2}" 43 | 44 | #: temporal_objects.py:348 temporal_objects.py:426 45 | msgid "on public and school holidays" 46 | msgstr "op schoolvakanties en feestdagen" 47 | 48 | #: temporal_objects.py:349 temporal_objects.py:427 49 | msgid "on public holidays" 50 | msgstr "op feestdagen" 51 | 52 | #: temporal_objects.py:350 temporal_objects.py:428 53 | msgid "on school holidays" 54 | msgstr "op schoolvakanties" 55 | 56 | #: temporal_objects.py:453 57 | #, python-brace-format 58 | msgid "in week {week}" 59 | msgstr "in week {week}" 60 | 61 | #: temporal_objects.py:455 62 | #, python-brace-format 63 | msgid "from week {week1} to week {week2}" 64 | msgstr "van week {week1} tot week {week2}" 65 | 66 | #: temporal_objects.py:462 67 | #, python-brace-format 68 | msgid "from week {week1} to week {week2}, every {n} weeks" 69 | msgstr "van week {week1} tot week {week2}, iedere {n} weken" 70 | 71 | #: temporal_objects.py:486 72 | #, python-brace-format 73 | msgid "in {year}" 74 | msgstr "in {year}" 75 | 76 | #: temporal_objects.py:488 77 | #, python-brace-format 78 | msgid "from {year1} to {year2}" 79 | msgstr "van {year1} tot {year2}" 80 | 81 | #: temporal_objects.py:495 82 | #, python-brace-format 83 | msgid "from {year1} to {year2}, every {n} years" 84 | msgstr "van {year1} tot {year2}, iedere {n} jaren" 85 | 86 | #: temporal_objects.py:528 87 | #, python-brace-format 88 | msgid "from {monthday1} to {monthday2}" 89 | msgstr "van {monthday1} tot {monthday2}" 90 | 91 | #: temporal_objects.py:607 92 | msgid "on easter" 93 | msgstr "op Pasen" 94 | 95 | #: temporal_objects.py:612 96 | #, python-brace-format 97 | msgid "{month} {day1} to {day2}, {year}" 98 | msgstr "{day1} tot {day2} {month} {year}" 99 | 100 | #: temporal_objects.py:619 101 | #, python-brace-format 102 | msgid "from {month} {day1} to {day2}" 103 | msgstr "van {day1} tot {day2} {month}" 104 | 105 | #: temporal_objects.py:630 106 | msgid "%B %-d" 107 | msgstr "%B %-d" 108 | 109 | #: rendering.py:34 110 | msgid "open" 111 | msgstr "open" 112 | 113 | #: rendering.py:53 114 | msgid "sunrise" 115 | msgstr "zonsopgang" 116 | 117 | #: rendering.py:54 118 | msgid "sunset" 119 | msgstr "zonsondergang" 120 | 121 | #: rendering.py:55 122 | msgid "dawn" 123 | msgstr "zonsopgang" 124 | 125 | #: rendering.py:56 126 | msgid "dusk" 127 | msgstr "zonsondergang" 128 | 129 | #: rendering.py:63 130 | #, python-brace-format 131 | msgid "{time} after sunrise" 132 | msgstr "{time} na zonsopgang" 133 | 134 | #: rendering.py:64 135 | #, python-brace-format 136 | msgid "{time} after sunset" 137 | msgstr "{time} na zonsondergang" 138 | 139 | #: rendering.py:65 140 | #, python-brace-format 141 | msgid "{time} after dawn" 142 | msgstr "{time} na zonsopgang" 143 | 144 | #: rendering.py:66 145 | #, python-brace-format 146 | msgid "{time} after dusk" 147 | msgstr "{time} na zonsondergang" 148 | 149 | #: rendering.py:73 150 | #, python-brace-format 151 | msgid "{time} before sunrise" 152 | msgstr "{time} voor zonsopgang" 153 | 154 | #: rendering.py:74 155 | #, python-brace-format 156 | msgid "{time} before sunset" 157 | msgstr "{time} voor zonsondergang" 158 | 159 | #: rendering.py:75 160 | #, python-brace-format 161 | msgid "{time} before dawn" 162 | msgstr "{time} voor zonsopgang" 163 | 164 | #: rendering.py:76 165 | #, python-brace-format 166 | msgid "{time} before dusk" 167 | msgstr "{time} voor zonsondergang" 168 | -------------------------------------------------------------------------------- /humanized_opening_hours/locales/it_IT/LC_MESSAGES/hoh.pot: -------------------------------------------------------------------------------- 1 | # ITALIAN TRANSLATION FOR HOH. 2 | # Copyright (C) 2018 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2018-10-06 15:07+0200\n" 10 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 11 | "Last-Translator: FULL NAME \n" 12 | "Language-Team: LANGUAGE \n" 13 | "Language: it_IT\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=utf-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | 18 | #: temporal_objects.py:210 19 | msgid "Open 24 hours a day and 7 days a week." 20 | msgstr "Aperto 24 ore al giorno e 7 giorni alla settimana." 21 | 22 | #: temporal_objects.py:219 23 | msgid "every days" 24 | msgstr "ogni giorno" 25 | 26 | #: temporal_objects.py:221 rendering.py:34 27 | msgid "closed" 28 | msgstr "chiuso" 29 | 30 | #: temporal_objects.py:230 rendering.py:39 31 | msgid "{}: {}" 32 | msgstr "{}: {}" 33 | 34 | #: temporal_objects.py:339 temporal_objects.py:410 35 | #, python-brace-format 36 | msgid "on {weekday}" 37 | msgstr "di {weekday}" 38 | 39 | #: temporal_objects.py:343 temporal_objects.py:414 40 | #, python-brace-format 41 | msgid "from {weekday1} to {weekday2}" 42 | msgstr "da {weekday1} a {weekday2}" 43 | 44 | #: temporal_objects.py:348 temporal_objects.py:426 45 | msgid "on public and school holidays" 46 | msgstr "nei giorni festivi e nelle vacanze scolastiche" 47 | 48 | #: temporal_objects.py:349 temporal_objects.py:427 49 | msgid "on public holidays" 50 | msgstr "nei giorni festivi" 51 | 52 | #: temporal_objects.py:350 temporal_objects.py:428 53 | msgid "on school holidays" 54 | msgstr "nelle vacanze scolastiche" 55 | 56 | #: temporal_objects.py:453 57 | #, python-brace-format 58 | msgid "in week {week}" 59 | msgstr "nella settimana {week}" 60 | 61 | #: temporal_objects.py:455 62 | #, python-brace-format 63 | msgid "from week {week1} to week {week2}" 64 | msgstr "dalla settimana {week1} alla settimana {week2}" 65 | 66 | #: temporal_objects.py:462 67 | #, python-brace-format 68 | msgid "from week {week1} to week {week2}, every {n} weeks" 69 | msgstr "dalla settimana {week1} alla settimana {week2}, ogni {n} settimane" 70 | 71 | #: temporal_objects.py:486 72 | #, python-brace-format 73 | msgid "in {year}" 74 | msgstr "nel {year}" 75 | 76 | #: temporal_objects.py:488 77 | #, python-brace-format 78 | msgid "from {year1} to {year2}" 79 | msgstr "dal {year1} al {year2}" 80 | 81 | #: temporal_objects.py:495 82 | #, python-brace-format 83 | msgid "from {year1} to {year2}, every {n} years" 84 | msgstr "dal {year1} al {year2}, ogni {n} anni" 85 | 86 | #: temporal_objects.py:528 87 | #, python-brace-format 88 | msgid "from {monthday1} to {monthday2}" 89 | msgstr "da {monthday1} a {monthday2}" 90 | 91 | #: temporal_objects.py:607 92 | msgid "on easter" 93 | msgstr "a Pasqua" 94 | 95 | #: temporal_objects.py:612 96 | #, python-brace-format 97 | msgid "{month} {day1} to {day2}, {year}" 98 | msgstr "dal {day1} al {day2} {month}, {year}" 99 | 100 | #: temporal_objects.py:619 101 | #, python-brace-format 102 | msgid "from {month} {day1} to {day2}" 103 | msgstr "dal {day1} al {day2} {month}" 104 | 105 | #: temporal_objects.py:630 106 | msgid "%B %-d" 107 | msgstr "%-d %B" 108 | 109 | #: rendering.py:34 110 | msgid "open" 111 | msgstr "aperto" 112 | 113 | #: rendering.py:53 114 | msgid "sunrise" 115 | msgstr "sorgere del sole" 116 | 117 | #: rendering.py:54 118 | msgid "sunset" 119 | msgstr "tramonto" 120 | 121 | #: rendering.py:55 122 | msgid "dawn" 123 | msgstr "alba" 124 | 125 | #: rendering.py:56 126 | msgid "dusk" 127 | msgstr "crepuscolo" 128 | 129 | #: rendering.py:63 130 | #, python-brace-format 131 | msgid "{time} after sunrise" 132 | msgstr "{time} dopo il sorgere del sole" 133 | 134 | #: rendering.py:64 135 | #, python-brace-format 136 | msgid "{time} after sunset" 137 | msgstr "{time} dopo il tramonto" 138 | 139 | #: rendering.py:65 140 | #, python-brace-format 141 | msgid "{time} after dawn" 142 | msgstr "{time} dopo l'alba" 143 | 144 | #: rendering.py:66 145 | #, python-brace-format 146 | msgid "{time} after dusk" 147 | msgstr "{time} dopo il crepuscolo" 148 | 149 | #: rendering.py:73 150 | #, python-brace-format 151 | msgid "{time} before sunrise" 152 | msgstr "{time} prima del sorgere del sole" 153 | 154 | #: rendering.py:74 155 | #, python-brace-format 156 | msgid "{time} before sunset" 157 | msgstr "{time} prima del tramonto" 158 | 159 | #: rendering.py:75 160 | #, python-brace-format 161 | msgid "{time} before dawn" 162 | msgstr "{time} prima dell'alba" 163 | 164 | #: rendering.py:76 165 | #, python-brace-format 166 | msgid "{time} before dusk" 167 | msgstr "{time} prima del crepuscolo" 168 | -------------------------------------------------------------------------------- /humanized_opening_hours/locales/fr_FR/LC_MESSAGES/hoh.pot: -------------------------------------------------------------------------------- 1 | # French translation for HOH. 2 | # Copyright (C) 2018 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2018-10-06 14:03+0200\n" 10 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 11 | "Last-Translator: FULL NAME \n" 12 | "Language-Team: LANGUAGE \n" 13 | "Language: fr_FR\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=utf-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | 18 | #: temporal_objects.py:210 19 | msgid "Open 24 hours a day and 7 days a week." 20 | msgstr "Ouvert 24 heures sur 24 et 7 jours sur 7." 21 | 22 | #: temporal_objects.py:219 23 | msgid "every days" 24 | msgstr "tous les jours" 25 | 26 | #: temporal_objects.py:221 rendering.py:34 27 | msgid "closed" 28 | msgstr "fermé" 29 | 30 | #: temporal_objects.py:230 rendering.py:39 31 | msgid "{}: {}" 32 | msgstr "{} : {}" 33 | 34 | #: temporal_objects.py:339 temporal_objects.py:410 35 | #, python-brace-format 36 | msgid "on {weekday}" 37 | msgstr "le {weekday}" 38 | 39 | #: temporal_objects.py:343 temporal_objects.py:414 40 | #, python-brace-format 41 | msgid "from {weekday1} to {weekday2}" 42 | msgstr "du {weekday1} au {weekday2}" 43 | 44 | #: temporal_objects.py:348 temporal_objects.py:426 45 | msgid "on public and school holidays" 46 | msgstr "pendant les vacances scolaires et jours fériés" 47 | 48 | #: temporal_objects.py:349 temporal_objects.py:427 49 | msgid "on public holidays" 50 | msgstr "pendant les jours fériés" 51 | 52 | #: temporal_objects.py:350 temporal_objects.py:428 53 | msgid "on school holidays" 54 | msgstr "pendant les vacances scolaires" 55 | 56 | #: temporal_objects.py:453 57 | #, python-brace-format 58 | msgid "in week {week}" 59 | msgstr "semaine {week}" 60 | 61 | #: temporal_objects.py:455 62 | #, python-brace-format 63 | msgid "from week {week1} to week {week2}" 64 | msgstr "de la semaine {week1} à la semaine {week2}" 65 | 66 | #: temporal_objects.py:462 67 | #, python-brace-format 68 | msgid "from week {week1} to week {week2}, every {n} weeks" 69 | msgstr "de la semaine {week1} à la semaine {week2}, toutes les {n} weeks" 70 | 71 | #: temporal_objects.py:486 72 | #, python-brace-format 73 | msgid "in {year}" 74 | msgstr "en {year}" 75 | 76 | #: temporal_objects.py:488 77 | #, python-brace-format 78 | msgid "from {year1} to {year2}" 79 | msgstr "de {year1} à {year2}" 80 | 81 | #: temporal_objects.py:495 82 | #, python-brace-format 83 | msgid "from {year1} to {year2}, every {n} years" 84 | msgstr "de {year1} à {year2}, tous les {n} ans" 85 | 86 | #: temporal_objects.py:528 87 | #, python-brace-format 88 | msgid "from {monthday1} to {monthday2}" 89 | msgstr "du {monthday1} au {monthday2}" 90 | 91 | #: temporal_objects.py:607 92 | msgid "on easter" 93 | msgstr "à pâques" 94 | 95 | #: temporal_objects.py:612 96 | #, python-brace-format 97 | msgid "{month} {day1} to {day2}, {year}" 98 | msgstr "{day1} au {day2} {month} {year}" 99 | 100 | #: temporal_objects.py:619 101 | #, python-brace-format 102 | msgid "from {month} {day1} to {day2}" 103 | msgstr "du {day1} au {day2} {month}" 104 | 105 | #: temporal_objects.py:630 106 | msgid "%B %-d" 107 | msgstr "%-d %B" 108 | 109 | #: rendering.py:34 110 | msgid "open" 111 | msgstr "ouvert" 112 | 113 | #: rendering.py:53 114 | msgid "sunrise" 115 | msgstr "lever du soleil" 116 | 117 | #: rendering.py:54 118 | msgid "sunset" 119 | msgstr "coucher du soleil" 120 | 121 | #: rendering.py:55 122 | msgid "dawn" 123 | msgstr "aurore" 124 | 125 | #: rendering.py:56 126 | msgid "dusk" 127 | msgstr "crépuscule" 128 | 129 | #: rendering.py:63 130 | #, python-brace-format 131 | msgid "{time} after sunrise" 132 | msgstr "{time} après le lever du soleil" 133 | 134 | #: rendering.py:64 135 | #, python-brace-format 136 | msgid "{time} after sunset" 137 | msgstr "{time} après le coucher du soleil" 138 | 139 | #: rendering.py:65 140 | #, python-brace-format 141 | msgid "{time} after dawn" 142 | msgstr "{time} après l'aurore" 143 | 144 | #: rendering.py:66 145 | #, python-brace-format 146 | msgid "{time} after dusk" 147 | msgstr "{time} après le crépuscule" 148 | 149 | #: rendering.py:73 150 | #, python-brace-format 151 | msgid "{time} before sunrise" 152 | msgstr "{time} avant le lever du soleil" 153 | 154 | #: rendering.py:74 155 | #, python-brace-format 156 | msgid "{time} before sunset" 157 | msgstr "{time} avant le coucher du soleil" 158 | 159 | #: rendering.py:75 160 | #, python-brace-format 161 | msgid "{time} before dawn" 162 | msgstr "{time} avant l'aurore" 163 | 164 | #: rendering.py:76 165 | #, python-brace-format 166 | msgid "{time} before dusk" 167 | msgstr "{time} avant le crépuscule" 168 | -------------------------------------------------------------------------------- /humanized_opening_hours/locales/pt/LC_MESSAGES/hoh.pot: -------------------------------------------------------------------------------- 1 | # Brazilian portuguese translation for HOH. 2 | # Copyright (C) 2018 3 | # GABRIEL LUIZ FREITAS ALMEIDA , 2018. 4 | # 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PACKAGE VERSION\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2018-10-06 10:59-0300\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: GABRIEL LUIZ FREITAS ALMEIDA \n" 13 | "Language-Team: LANGUAGE \n" 14 | "Language: pt-br\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: temporal_objects.py:210 20 | msgid "Open 24 hours a day and 7 days a week." 21 | msgstr "Aberto 24 horas por dia e 7 dias por semana." 22 | 23 | #: temporal_objects.py:219 24 | msgid "every days" 25 | msgstr "todos os dias" 26 | 27 | #: temporal_objects.py:221 rendering.py:34 28 | msgid "closed" 29 | msgstr "fechado" 30 | 31 | #: temporal_objects.py:230 rendering.py:39 32 | msgid "{}: {}" 33 | msgstr "{}: {}" 34 | 35 | #: temporal_objects.py:339 temporal_objects.py:410 36 | #, python-brace-format 37 | msgid "on {weekday}" 38 | msgstr "na {weekday}" 39 | 40 | #: temporal_objects.py:343 temporal_objects.py:414 41 | #, python-brace-format 42 | msgid "from {weekday1} to {weekday2}" 43 | msgstr "de {weekday1} a {weekday2}" 44 | 45 | #: temporal_objects.py:348 temporal_objects.py:426 46 | msgid "on public and school holidays" 47 | msgstr "em feriados e férias escolares" 48 | 49 | #: temporal_objects.py:349 temporal_objects.py:427 50 | msgid "on public holidays" 51 | msgstr "em feriados" 52 | 53 | #: temporal_objects.py:350 temporal_objects.py:428 54 | msgid "on school holidays" 55 | msgstr "em férias escolares" 56 | 57 | #: temporal_objects.py:453 58 | #, python-brace-format 59 | msgid "in week {week}" 60 | msgstr "na semana {week}" 61 | 62 | #: temporal_objects.py:455 63 | #, python-brace-format 64 | msgid "from week {week1} to week {week2}" 65 | msgstr "da semana {week1} à semana {week2}" 66 | 67 | #: temporal_objects.py:462 68 | #, python-brace-format 69 | msgid "from week {week1} to week {week2}, every {n} weeks" 70 | msgstr "da semana {week1} à semana {week2}, a cada {n} semanas" 71 | 72 | #: temporal_objects.py:486 73 | #, python-brace-format 74 | msgid "in {year}" 75 | msgstr "em {year}" 76 | 77 | #: temporal_objects.py:488 78 | #, python-brace-format 79 | msgid "from {year1} to {year2}" 80 | msgstr "de {year1} a {year2}" 81 | 82 | #: temporal_objects.py:495 83 | #, python-brace-format 84 | msgid "from {year1} to {year2}, every {n} years" 85 | msgstr "de {year1} a {year2}, a cada {n} anos" 86 | 87 | #: temporal_objects.py:528 88 | #, python-brace-format 89 | msgid "from {monthday1} to {monthday2}" 90 | msgstr "de {monthday1} a {monthday2}" 91 | 92 | #: temporal_objects.py:607 93 | msgid "on easter" 94 | msgstr "na páscoa" 95 | 96 | #: temporal_objects.py:612 97 | #, python-brace-format 98 | msgid "{month} {day1} to {day2}, {year}" 99 | msgstr "{day1} a {day2} de {month}, {year}" 100 | 101 | #: temporal_objects.py:619 102 | #, python-brace-format 103 | msgid "from {month} {day1} to {day2}" 104 | msgstr "de {day1} a {day2} de {month}" 105 | 106 | #: temporal_objects.py:630 107 | msgid "%B %-d" 108 | msgstr "%B %-d" 109 | 110 | #: rendering.py:34 111 | msgid "open" 112 | msgstr "aberto" 113 | 114 | #: rendering.py:53 115 | msgid "sunrise" 116 | msgstr "nascer do sol" 117 | 118 | #: rendering.py:54 119 | msgid "sunset" 120 | msgstr "pôr do sol" 121 | 122 | #: rendering.py:55 123 | msgid "dawn" 124 | msgstr "amanhecer" 125 | 126 | #: rendering.py:56 127 | msgid "dusk" 128 | msgstr "anoitecer" 129 | 130 | #: rendering.py:63 131 | #, python-brace-format 132 | msgid "{time} after sunrise" 133 | msgstr "{time} após o nascer do sol" 134 | 135 | #: rendering.py:64 136 | #, python-brace-format 137 | msgid "{time} after sunset" 138 | msgstr "{time} após o pôr do sol" 139 | 140 | #: rendering.py:65 141 | #, python-brace-format 142 | msgid "{time} after dawn" 143 | msgstr "{time} após o amanhecer" 144 | 145 | #: rendering.py:66 146 | #, python-brace-format 147 | msgid "{time} after dusk" 148 | msgstr "{time} após o anoitecer" 149 | 150 | #: rendering.py:73 151 | #, python-brace-format 152 | msgid "{time} before sunrise" 153 | msgstr "{time} antes do nascer do sol" 154 | 155 | #: rendering.py:74 156 | #, python-brace-format 157 | msgid "{time} before sunset" 158 | msgstr "{time} antes do pôr do sol" 159 | 160 | #: rendering.py:75 161 | #, python-brace-format 162 | msgid "{time} before dawn" 163 | msgstr "{time} antes do amanhecer" 164 | 165 | #: rendering.py:76 166 | #, python-brace-format 167 | msgid "{time} before dusk" 168 | msgstr "{time} antes do anoitecer" 169 | -------------------------------------------------------------------------------- /humanized_opening_hours/locales/ru_RU/LC_MESSAGES/hoh.pot: -------------------------------------------------------------------------------- 1 | # Russian translation for HOH. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2018-10-06 14:06+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: ru_RU\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: temporal_objects.py:210 21 | msgid "Open 24 hours a day and 7 days a week." 22 | msgstr "Открыто 24 часа в сутки и 7 дней в неделю." 23 | 24 | #: temporal_objects.py:219 25 | msgid "every days" 26 | msgstr "каждый день" 27 | 28 | #: temporal_objects.py:221 rendering.py:34 29 | msgid "closed" 30 | msgstr "закрыто" 31 | 32 | #: temporal_objects.py:230 rendering.py:39 33 | msgid "{}: {}" 34 | msgstr "{}: {}" 35 | 36 | #: temporal_objects.py:339 temporal_objects.py:410 37 | #, python-brace-format 38 | msgid "on {weekday}" 39 | msgstr "в {weekday}" 40 | 41 | #: temporal_objects.py:343 temporal_objects.py:414 42 | #, python-brace-format 43 | msgid "from {weekday1} to {weekday2}" 44 | msgstr "с {weekday1} по {weekday2}" 45 | 46 | #: temporal_objects.py:348 temporal_objects.py:426 47 | msgid "on public and school holidays" 48 | msgstr "на общественные и школьные каникулы" 49 | 50 | #: temporal_objects.py:349 temporal_objects.py:427 51 | msgid "on public holidays" 52 | msgstr "на общественные каникулы" 53 | 54 | #: temporal_objects.py:350 temporal_objects.py:428 55 | msgid "on school holidays" 56 | msgstr "на школьные каникулы" 57 | 58 | #: temporal_objects.py:453 59 | #, python-brace-format 60 | msgid "in week {week}" 61 | msgstr "в неделю {n}" 62 | 63 | #: temporal_objects.py:455 64 | #, python-brace-format 65 | msgid "from week {week1} to week {week2}" 66 | msgstr "от недели {week1} до недели {week2}" 67 | 68 | #: temporal_objects.py:462 69 | #, python-brace-format 70 | msgid "from week {week1} to week {week2}, every {n} weeks" 71 | msgstr "от недели {week1} до недели {week2}, каждые {n} недель" 72 | 73 | #: temporal_objects.py:486 74 | #, python-brace-format 75 | msgid "in {year}" 76 | msgstr "в год {year}" 77 | 78 | #: temporal_objects.py:488 79 | #, python-brace-format 80 | msgid "from {year1} to {year2}" 81 | msgstr "от года {year1} до года {year2}" 82 | 83 | #: temporal_objects.py:495 84 | #, python-brace-format 85 | msgid "from {year1} to {year2}, every {n} years" 86 | msgstr "от года {year1} до года {year2}, каждые {n} года" 87 | 88 | #: temporal_objects.py:528 89 | #, python-brace-format 90 | msgid "from {monthday1} to {monthday2}" 91 | msgstr "от {monthday1} до {monthday2}" 92 | 93 | #: temporal_objects.py:607 94 | msgid "on easter" 95 | msgstr "на Пасху" 96 | 97 | #: temporal_objects.py:612 98 | #, python-brace-format 99 | msgid "{month} {day1} to {day2}, {year}" 100 | msgstr "от {month} {day1} до {day2}, {year}" 101 | 102 | #: temporal_objects.py:619 103 | #, python-brace-format 104 | msgid "from {month} {day1} to {day2}" 105 | msgstr "от {month} {day1} до {day2}" 106 | 107 | #: temporal_objects.py:630 108 | msgid "%B %-d" 109 | msgstr "%B %-d" 110 | 111 | #: rendering.py:34 112 | msgid "open" 113 | msgstr "открыто" 114 | 115 | #: rendering.py:53 116 | msgid "sunrise" 117 | msgstr "восход" 118 | 119 | #: rendering.py:54 120 | msgid "sunset" 121 | msgstr "закат" 122 | 123 | #: rendering.py:55 124 | msgid "dawn" 125 | msgstr "рассвет" 126 | 127 | #: rendering.py:56 128 | msgid "dusk" 129 | msgstr "сумерки" 130 | 131 | #: rendering.py:63 132 | #, python-brace-format 133 | msgid "{time} after sunrise" 134 | msgstr "{time} после восхода солнца" 135 | 136 | #: rendering.py:64 137 | #, python-brace-format 138 | msgid "{time} after sunset" 139 | msgstr "{time} после заката солнца" 140 | 141 | #: rendering.py:65 142 | #, python-brace-format 143 | msgid "{time} after dawn" 144 | msgstr "{time} после рассвета" 145 | 146 | #: rendering.py:66 147 | #, python-brace-format 148 | msgid "{time} after dusk" 149 | msgstr "{time} после сумерек" 150 | 151 | #: rendering.py:73 152 | #, python-brace-format 153 | msgid "{time} before sunrise" 154 | msgstr "{time} до восхода солнца" 155 | 156 | #: rendering.py:74 157 | #, python-brace-format 158 | msgid "{time} before sunset" 159 | msgstr "{time} до восхода заката солнца" 160 | 161 | #: rendering.py:75 162 | #, python-brace-format 163 | msgid "{time} before dawn" 164 | msgstr "{time} до рассвета" 165 | 166 | #: rendering.py:76 167 | #, python-brace-format 168 | msgid "{time} before dusk" 169 | msgstr "{time} до сумерек" 170 | -------------------------------------------------------------------------------- /humanized_opening_hours/locales/pl/LC_MESSAGES/hoh.pot: -------------------------------------------------------------------------------- 1 | # Polish translation of HOH. 2 | # Copyright (C) 2018 3 | # ADAM KULESZA adam.kule@gmail.com, 2018. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2018-10-06 15:30+0200\n" 10 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 11 | "Last-Translator: ADAM KULESZA adam.kule@gmail.com\n" 12 | "Language-Team: LANGUAGE \n" 13 | "Language: pl\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=utf-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | 18 | #: temporal_objects.py:210 19 | msgid "Open 24 hours a day and 7 days a week." 20 | msgstr "Otwarte 24 godziny na dobę 7 dni w tygodniu." 21 | 22 | #: temporal_objects.py:219 23 | msgid "every days" 24 | msgstr "codziennie" 25 | 26 | #: temporal_objects.py:221 rendering.py:34 27 | msgid "closed" 28 | msgstr "zamknięte" 29 | 30 | #: temporal_objects.py:230 rendering.py:39 31 | msgid "{}: {}" 32 | msgstr "{}: {}" 33 | 34 | #: temporal_objects.py:339 temporal_objects.py:410 35 | #, python-brace-format 36 | msgid "on {weekday}" 37 | msgstr "w {weekday}" 38 | 39 | #: temporal_objects.py:343 temporal_objects.py:414 40 | #, python-brace-format 41 | msgid "from {weekday1} to {weekday2}" 42 | msgstr "od {weekday1} do {weekday2}" 43 | 44 | #: temporal_objects.py:348 temporal_objects.py:426 45 | msgid "on public and school holidays" 46 | msgstr "w święta państwowe i wakacje" 47 | 48 | #: temporal_objects.py:349 temporal_objects.py:427 49 | msgid "on public holidays" 50 | msgstr "w święta państwowe" 51 | 52 | #: temporal_objects.py:350 temporal_objects.py:428 53 | msgid "on school holidays" 54 | msgstr "w wakacje" 55 | 56 | #: temporal_objects.py:453 57 | #, python-brace-format 58 | msgid "in week {week}" 59 | msgstr "w tygodniu {week}" 60 | 61 | #: temporal_objects.py:455 62 | #, python-brace-format 63 | msgid "from week {week1} to week {week2}" 64 | msgstr "od {week1} do {week2} tygodnia" 65 | 66 | #: temporal_objects.py:462 67 | #, python-brace-format 68 | msgid "from week {week1} to week {week2}, every {n} weeks" 69 | msgstr "od {week1} do {week2} tygodnia, co {n} tygodni" 70 | #Rules of polish grammar might cause the translation to be imperfect in some cases 71 | 72 | #: temporal_objects.py:486 73 | #, python-brace-format 74 | msgid "in {year}" 75 | msgstr "w {year} roku" 76 | 77 | #: temporal_objects.py:488 78 | #, python-brace-format 79 | msgid "from {year1} to {year2}" 80 | msgstr "od {year1} do {year2} roku" 81 | #Rules of polish grammar might cause the translation to be imperfect in some cases 82 | 83 | #: temporal_objects.py:495 84 | #, python-brace-format 85 | msgid "from {year1} to {year2}, every {n} years" 86 | msgstr "od {year1} do {year2} roku, co {n} lat" 87 | #Rules of polish grammar might cause the translation to be imperfect in some cases 88 | 89 | #: temporal_objects.py:528 90 | #, python-brace-format 91 | msgid "from {monthday1} to {monthday2}" 92 | msgstr "od {monthday1} do {monthday2}" 93 | 94 | #: temporal_objects.py:607 95 | msgid "on easter" 96 | msgstr "w wielkanoc" 97 | 98 | #: temporal_objects.py:612 99 | #, python-brace-format 100 | msgid "{month} {day1} to {day2}, {year}" 101 | msgstr "od {day1} do {day2} {month}, {year} roku" 102 | 103 | #: temporal_objects.py:619 104 | #, python-brace-format 105 | msgid "from {month} {day1} to {day2}" 106 | msgstr "od {day1} do {day2} {month}" 107 | 108 | #: temporal_objects.py:630 109 | msgid "%B %-d" 110 | msgstr "%B %-d" 111 | 112 | #: rendering.py:34 113 | msgid "open" 114 | msgstr "otwarte" 115 | 116 | #: rendering.py:53 117 | msgid "sunrise" 118 | msgstr "wschód słońca" 119 | 120 | #: rendering.py:54 121 | msgid "sunset" 122 | msgstr "zachód słońca" 123 | 124 | #: rendering.py:55 125 | msgid "dawn" 126 | msgstr "świt" 127 | 128 | #: rendering.py:56 129 | msgid "dusk" 130 | msgstr "zmierzch" 131 | 132 | #: rendering.py:63 133 | #, python-brace-format 134 | msgid "{time} after sunrise" 135 | msgstr "{time} po wschodzie słońca" 136 | 137 | #: rendering.py:64 138 | #, python-brace-format 139 | msgid "{time} after sunset" 140 | msgstr "{time} po zachodzie słońca" 141 | 142 | #: rendering.py:65 143 | #, python-brace-format 144 | msgid "{time} after dawn" 145 | msgstr "{time} po świcie" 146 | 147 | #: rendering.py:66 148 | #, python-brace-format 149 | msgid "{time} after dusk" 150 | msgstr "{time} po zmierzchu" 151 | 152 | #: rendering.py:73 153 | #, python-brace-format 154 | msgid "{time} before sunrise" 155 | msgstr "{time} przed wchodem słońca" 156 | 157 | #: rendering.py:74 158 | #, python-brace-format 159 | msgid "{time} before sunset" 160 | msgstr "{time} przed zachodem słońca" 161 | 162 | #: rendering.py:75 163 | #, python-brace-format 164 | msgid "{time} before dawn" 165 | msgstr "{time} przed świtem" 166 | 167 | #: rendering.py:76 168 | #, python-brace-format 169 | msgid "{time} before dusk" 170 | msgstr "{time} przed zmierzchem" 171 | -------------------------------------------------------------------------------- /humanized_opening_hours/field_parser.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | import lark 5 | 6 | from humanized_opening_hours.temporal_objects import ( 7 | WEEKDAYS, MONTHS, 8 | Rule, RangeSelector, AlwaysOpenSelector, 9 | MonthDaySelector, WeekdayHolidaySelector, 10 | WeekdayInHolidaySelector, WeekSelector, 11 | YearSelector, MonthDayRange, MonthDayDate, 12 | TimeSpan, Time, TIMESPAN_ALL_THE_DAY 13 | ) 14 | from humanized_opening_hours.exceptions import ParseError 15 | from humanized_opening_hours.frequent_fields import ( 16 | FREQUENT_FIELDS, parse_simple_field 17 | ) 18 | 19 | 20 | BASE_DIR = os.path.dirname(os.path.realpath(__file__)) 21 | 22 | 23 | def cycle_slice(l, start_index, end_index): 24 | """Allows to do a cyclical slicing on any iterable. 25 | It's like a regular slicing, but it allows the start index 26 | to be greater than the end index. 27 | 28 | Parameters 29 | ---------- 30 | iterable 31 | The object on which to iterate. 32 | int 33 | The start index. 34 | int 35 | The end index (can be lower than the start index). 36 | 37 | Returns 38 | ------- 39 | list 40 | The objects between the start and the end index (inclusive). 41 | """ 42 | if start_index <= end_index: 43 | return l[start_index:end_index+1] 44 | return l[start_index:] + l[:end_index+1] 45 | 46 | 47 | class MainTransformer(lark.Transformer): 48 | def time_domain(self, args): 49 | return args 50 | 51 | # Rule 52 | def rule_sequence(self, args): 53 | if len(args) == 1: # "time_selector" 54 | return Rule(AlwaysOpenSelector(), args[0]) 55 | if len(args) == 2: # "range_selectors time_selector" 56 | return Rule(args[0], args[1]) 57 | else: # "range_selectors time_selector rule_modifier" 58 | time_selector = args[1] 59 | status = args[2] 60 | if status == "closed" and time_selector: 61 | raise ParseError( 62 | "Handling of ' closed' is not available yet." 63 | ) 64 | return Rule(args[0], time_selector, status=status) 65 | 66 | def always_open_rule(self, args): # "ALWAYS_OPEN" 67 | return Rule(AlwaysOpenSelector(), [TIMESPAN_ALL_THE_DAY]) 68 | 69 | def time_modifier_rule(self, args): # "time_selector rule_modifier" 70 | return Rule(AlwaysOpenSelector(), args[0], status=args[1]) 71 | 72 | def range_modifier_rule(self, args): # "range_selectors rule_modifier" 73 | modifier = args[1] 74 | if modifier == "open": 75 | raise lark.exceptions.ParseError() 76 | return Rule(args[0], [], status=modifier) 77 | 78 | # def modifier_only_rule(self, args): # "rule_modifier" 79 | # pass 80 | 81 | # Main selectors 82 | def range_selectors(self, args): 83 | return RangeSelector(args) 84 | 85 | def time_selector(self, args): 86 | return args 87 | 88 | # Dates 89 | def monthday_selector(self, args): 90 | return MonthDaySelector(args) 91 | 92 | def monthday_range(self, args): 93 | # Prevent case like "Jan 1-5-Feb 1-5" (monthday_date - monthday_date). 94 | return MonthDayRange(args) 95 | 96 | def monthday_date_monthday(self, args): 97 | year = args.pop(0) if len(args) == 3 else None 98 | month = MONTHS.index(args[0].value) + 1 99 | monthday = int(args[1].value) 100 | return MonthDayDate( 101 | "monthday", year=year, month=month, monthday=monthday 102 | ) 103 | 104 | def monthday_date_day_to_day(self, args): 105 | year = args.pop(0) if len(args) == 4 else None 106 | month = MONTHS.index(args[0].value) + 1 107 | monthday_from = int(args[1].value) 108 | monthday_to = int(args[2].value) 109 | return MonthDayDate( 110 | "monthday-day", year=year, month=month, 111 | monthday=monthday_from, monthday_to=monthday_to 112 | ) 113 | 114 | def monthday_date_month(self, args): 115 | year = args[0] if len(args) == 2 else None 116 | if year: 117 | args.pop(0) 118 | month = MONTHS.index(args[0]) + 1 119 | return MonthDayDate("month", year=year, month=month) 120 | 121 | def monthday_date_easter(self, args): 122 | year = args[0] if len(args) == 3 else None 123 | return MonthDayDate("easter", year=year) 124 | 125 | def day_offset(self, args): # TODO : Make usable. 126 | # TODO : Review. 127 | # [Token(DAY_OFFSET, ' +2 days')] 128 | offset_sign, days = args[0].value.strip("days ") 129 | offset_sign = 1 if offset_sign == '+' else -1 130 | days = int(days) 131 | return (offset_sign, days) 132 | 133 | # Holidays 134 | def holiday(self, args): 135 | return set([args[0].value]) 136 | 137 | # weekday_selector 138 | def weekday_or_holiday_sequence_selector(self, args): 139 | args = set([item for sublist in args for item in sublist]) 140 | SH, PH = 'SH' in args, 'PH' in args 141 | if SH: 142 | args.remove('SH') 143 | if PH: 144 | args.remove('PH') 145 | return WeekdayHolidaySelector(args, SH, PH) 146 | 147 | def holiday_and_weekday_sequence_selector(self, args): 148 | args = set([item for sublist in args for item in sublist]) 149 | SH, PH = 'SH' in args, 'PH' in args 150 | if SH: 151 | args.remove('SH') 152 | if PH: 153 | args.remove('PH') 154 | return WeekdayHolidaySelector(args, SH, PH) 155 | 156 | def holiday_in_weekday_sequence_selector(self, args): 157 | if len(args) == 2: # TODO : Clean. 158 | holiday, weekday = args 159 | else: 160 | holiday = set(("PH", "SH")) 161 | weekday = args[-1] 162 | return WeekdayInHolidaySelector(weekday, holiday) 163 | 164 | # Weekdays 165 | def weekday_sequence(self, args): 166 | return set([item for sublist in args for item in sublist]) 167 | 168 | def weekday_range(self, args): 169 | if len(args) == 1: 170 | return [args[0].value] 171 | first_day = WEEKDAYS.index(args[0]) 172 | last_day = WEEKDAYS.index(args[1]) 173 | return set(cycle_slice(WEEKDAYS, first_day, last_day)) 174 | 175 | # Year 176 | def year(self, args): 177 | return int(args[0].value) 178 | 179 | def year_range(self, args): 180 | if len(args) == 1: 181 | return ( 182 | (args[0],), 183 | set([args[0]]) 184 | ) 185 | elif len(args) == 2: 186 | return ( 187 | (args[0], args[1]), 188 | set(range(args[0], args[1]+1)) 189 | ) 190 | else: 191 | return ( 192 | (args[0], args[1], int(args[2].value)), 193 | set(range(args[0], args[1]+1, int(args[2].value))) 194 | ) 195 | 196 | def year_selector(self, args): 197 | years = set() 198 | rendering_data = [] 199 | for (arg_rendering_data, arg_years) in args: 200 | years = years.union(arg_years) 201 | rendering_data.append(arg_rendering_data) 202 | ys = YearSelector(years) 203 | ys.rendering_data = rendering_data 204 | return ys 205 | 206 | # Week 207 | def week_selector(self, args): 208 | args = args[1:] 209 | weeks = set() 210 | rendering_data = [] 211 | for (arg_rendering_data, arg_weeks) in args: 212 | weeks = weeks.union(arg_weeks) 213 | rendering_data.append(arg_rendering_data) 214 | ws = WeekSelector(weeks) 215 | ws.rendering_data = rendering_data 216 | return ws 217 | 218 | def week(self, args): 219 | if len(args) == 1: 220 | return ( 221 | (args[0],), 222 | set([args[0]]) 223 | ) 224 | elif len(args) == 2: 225 | return ( 226 | (args[0], args[1]), 227 | set(range(args[0], args[1]+1)) 228 | ) 229 | else: 230 | return ( 231 | (args[0], args[1], int(args[2].value)), 232 | set(range(args[0], args[1]+1, int(args[2].value))) 233 | ) 234 | 235 | def weeknum(self, args): 236 | return int(args[0].value) 237 | 238 | # Time 239 | def timespan(self, args): 240 | return TimeSpan(*args) 241 | 242 | def time(self, args): 243 | return Time(args[0]) 244 | 245 | def hour_minutes(self, args): 246 | h, m = int(args[0].value), int(args[1].value) 247 | if m >= 60: 248 | raise ParseError( 249 | "Minutes must be in 0..59 (got {!r}).".format( 250 | args[0].value + ':' + args[1].value 251 | ) 252 | ) 253 | if (h, m) == (24, 0): 254 | dt = datetime.time.max 255 | else: 256 | dt = datetime.time(h % 24, m) # Converts "26:00" to "02:00". 257 | return ("normal", dt) 258 | 259 | def variable_time(self, args): 260 | # ("event", "offset_sign", "hour_minutes") 261 | kind = args[0].value 262 | if len(args) == 1: 263 | return (kind, 1, datetime.timedelta(0)) 264 | offset_sign = 1 if args[1].value == '+' else -1 265 | delta = ( # Because "datetime.time" cannot be substracted. 266 | datetime.datetime.combine(datetime.date(1, 1, 1), args[2][1]) - 267 | datetime.datetime.combine(datetime.date(1, 1, 1), datetime.time.min) 268 | ) 269 | return (kind, offset_sign, delta) 270 | 271 | def rule_modifier_open(self, args): 272 | return "open" 273 | 274 | def rule_modifier_closed(self, args): 275 | return "closed" 276 | 277 | 278 | def get_parser(): 279 | """ 280 | Returns a Lark parser able to parse a valid field. 281 | """ 282 | base_dir = os.path.dirname(os.path.realpath(__file__)) 283 | with open(os.path.join(base_dir, "field.ebnf"), 'r') as f: 284 | grammar = f.read() 285 | return lark.Lark(grammar, start="time_domain", parser="earley") 286 | 287 | 288 | def get_tree_and_rules(field, optimize=True): 289 | # If the field is in FREQUENT_FIELDS, returns directly its tree. 290 | tree = None 291 | if optimize: 292 | tree = FREQUENT_FIELDS.get(field) 293 | if not tree: 294 | tree = parse_simple_field(field) 295 | if not tree: 296 | tree = PARSER.parse(field) 297 | rules = MainTransformer().transform(tree) 298 | return (tree, rules) 299 | 300 | 301 | PARSER = get_parser() 302 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Humanized Opening Hours - A parser for the opening_hours fields from OSM 2 | ======================================================================== 3 | 4 | **Humanized Opening Hours** is a Python 3 module allowing a simple usage of the opening_hours fields used in OpenStreetMap. 5 | 6 | **Due to a lack of free time, the developpement of this module is paused. You can of course use it, but its features won't evolve before a (long) moment. If you want to become maintainer, don't hesitate to create an issue!** 7 | 8 | ```python 9 | >>> import humanized_opening_hours as hoh 10 | >>> field = "Mo-Fr 06:00-21:00; Sa,Su 08:00-12:00" 11 | >>> oh = hoh.OHParser(field, locale="en") 12 | >>> oh.is_open() 13 | True 14 | >>> oh.next_change() 15 | datetime.datetime(2017, 12, 24, 12, 0) 16 | >>> print('\n'.join(oh.description())) 17 | """ 18 | From Monday to Friday: 6:00 AM – 9:00 PM. 19 | From Saturday to Sunday: 8:00 AM – 12:00 PM. 20 | """ 21 | ``` 22 | 23 | **This module is in beta. It should be production ready, but some bugs or minor modifications are still possible. Don't hesitate to create an issue!** 24 | 25 | # Table of contents 26 | 27 | - [Installation](#installation) 28 | - [Dependencies](#dependencies) 29 | - [How to use it](#how-to-use-it) 30 | - [Basic methods](#basic-methods) 31 | - [Solar hours](#solar-hours) 32 | - [Have nice schedules](#have-nice-schedules) 33 | - [Supported field formats](#supported-field-formats) 34 | - [Alternatives](#alternatives) 35 | - [Performances](#performances) 36 | - [Licence](#licence) 37 | 38 | # Installation 39 | 40 | This library is so small, you can include it directly into your project. 41 | Also, it is available on PyPi. 42 | 43 | $ pip3 install osm-humanized-opening-hours 44 | 45 | ## Dependencies 46 | 47 | This module requires the following modules, which should be automatically installed when installing HOH with `pip`. 48 | 49 | ```python 50 | lark-parser 51 | babel 52 | astral 53 | ``` 54 | 55 | # How to use it 56 | 57 | The only mandatory argument to give to the constructor is the field, which must be a string. 58 | It can also take a `locale` argument, which can be any valid locale name. You can change it later by changing the `locale` attribute (which is, in fact, a `property`). 59 | However, to be able to use the most of the rendering methods, it must be in `hoh.AVAILABLE_LOCALES` (a warning will be printed otherwise). 60 | 61 | ```python 62 | >>> import humanized_opening_hours as hoh 63 | >>> field = "Mo-Fr 06:00-21:00; Sa,Su 07:00-21:00" 64 | >>> oh = hoh.OHParser(field) 65 | ``` 66 | 67 | If you have a GeoJSON, you can use a dedicated classmethod: `from_geojson()`, which returns an `OHParser` instance. 68 | It takes the GeoJSON, and optionally the following arguments: 69 | 70 | - `timezone_getter` (callable): A function to call, which takes two arguments (latitude and longitude, as floats), and returns a timezone name or None, allowing to get solar hours for the facility; 71 | - `locale` (str): the locale to use ("en" default). 72 | 73 | ## Basic methods 74 | 75 | To know if the facility is open at the present time. Returns a boolean. 76 | Can take a datetime.datetime moment to check for another time. 77 | 78 | ```python 79 | >>> oh.is_open() 80 | True 81 | ``` 82 | 83 | ----- 84 | 85 | To know at which time the facility status (open / closed) will change. 86 | Returns a datetime.datetime object. 87 | It can take a datetime.datetime moment to get next change from another time. 88 | If we are on December 24 before 21:00 / 09:00PM... 89 | 90 | ```python 91 | >>> oh.next_change() 92 | datetime.datetime(2017, 12, 24, 21, 0) 93 | ``` 94 | 95 | For fields with consecutive days fully open, `next_change()` will try to get the true next change by recursion. 96 | You can change this behavior with the `max_recursion` argument, which is set to `31` default, meaning `next_change()` will try a maximum of 31 recursions (*i.e.* 31 days, or a month) to get the true next change. 97 | If this limit is reached, a `NextChangeRecursionError` will be raised. 98 | You can deny recursion by setting the `max_recursion` argument to `0`. 99 | 100 | The `NextChangeRecursionError` has a `last_change` attribute, containing the last change got just before raising of the exception. 101 | You can get it with a `except NextChangeRecursionError as e:` block. 102 | 103 | ```python 104 | >>> oh = hoh.OHParser("Mo-Fr 00:00-24:00") 105 | >>> oh.next_change(dt=datetime.datetime(2018, 1, 8, 0, 0)) 106 | datetime.datetime(2018, 1, 11, 23, 59, 59, 999999) 107 | ``` 108 | 109 | ----- 110 | 111 | To get a list of the opening periods between to dates, you can the use `opening_periods_between()` method. 112 | It takes two arguments, which can be `datetime.date` or `datetime.datetime` objects. 113 | If you pass `datetime.date` objects, it will return all opening periods between these dates (inclusive). 114 | If you pass `datetime.datetime`, the returned opening periods will be "choped" on these times. 115 | 116 | The returned opening periods are tuples of two `datetime.datetime` objects, representing the beginning and the end of the period. 117 | 118 | ```python 119 | >>> oh = hoh.OHParser("Mo-Fr 06:00-21:00; Sa,Su 07:00-21:00") 120 | >>> oh.opening_periods_between(datetime.date(2018, 1, 1), datetime.date(2018, 1, 7)) 121 | [ 122 | (datetime.datetime(2018, 1, 1, 6, 0), datetime.datetime(2018, 1, 1, 21, 0)), 123 | (datetime.datetime(2018, 1, 2, 6, 0), datetime.datetime(2018, 1, 2, 21, 0)), 124 | (datetime.datetime(2018, 1, 3, 6, 0), datetime.datetime(2018, 1, 3, 21, 0)), 125 | (datetime.datetime(2018, 1, 4, 6, 0), datetime.datetime(2018, 1, 4, 21, 0)), 126 | (datetime.datetime(2018, 1, 5, 6, 0), datetime.datetime(2018, 1, 5, 21, 0)), 127 | (datetime.datetime(2018, 1, 6, 7, 0), datetime.datetime(2018, 1, 6, 21, 0)), 128 | (datetime.datetime(2018, 1, 7, 7, 0), datetime.datetime(2018, 1, 7, 21, 0)) 129 | ] 130 | ``` 131 | 132 | You can also set the `merge` parameter to True, to merge continuous opening periods. 133 | 134 | ----- 135 | 136 | You can get a sanitized version of the field given to the constructor with the `sanitize()` function or the `field` attribute. 137 | 138 | ```python 139 | >>> field = "mo-su 09:30-20h;jan off" 140 | >>> print(hoh.sanitize(field)) 141 | "Mo-Su 09:30-20:00; Jan off" 142 | ``` 143 | 144 | If sanitization is the only thing you need, use HOH for this is probably overkill. 145 | You might be interested in the [OH Sanitizer](https://github.com/rezemika/oh_sanitizer) module, or you can copy directly the code of the sanitize function in your project. 146 | 147 | ----- 148 | 149 | If you try to parse a field which is invalid or contains a pattern which is not supported, an `humanized_opening_hours.exceptions.ParseError` (inheriting from `humanized_opening_hours.exceptions.HOHError`) will be raised. 150 | 151 | If a field contains only a comment (like `"on appointment"`), a `CommentOnlyField` exception (inheriting from `ParseError`) will be raised. 152 | It contains a `comment` attribute, allowing you to display it instead of the opening hours. 153 | 154 | The `OHParser` contains an `is_24_7` attribute, which is true if the field is simply `24/7` or `00:00-24:00`, and false either. 155 | The `next_change()` method won't try recursion if this attribute is true and will directly raise a `NextChangeRecursionError` (except if you set `max_recursion` to zero, in this case it will just return the last time of the current day). 156 | 157 | You can check equality between two `OHParser` instances. 158 | It will be true if both have the same field and the same location. 159 | 160 | ```python 161 | >>> import humanized_opening_hours as hoh 162 | >>> 163 | >>> oh1 = hoh.OHParser("Mo 10:00-20:00") 164 | >>> oh2 = hoh.OHParser("Mo 10:00-20:00") 165 | >>> oh3 = hoh.OHParser("Mo 09:00-21:00") 166 | >>> oh1 == oh2 167 | True 168 | >>> oh1 == oh3 169 | False 170 | ``` 171 | 172 | ----- 173 | 174 | The `OHParser` object contains two other attributes: `PH_dates` and `SH_dates`, which are empty lists default. 175 | To indicate a date is a public or a school holiday, you can pass its `datetime.date` into these lists. 176 | You can also use the [python-holidays](https://github.com/dr-prodigy/python-holidays) module to get dynamic dictionnary (which updates the year) to replace these lists. 177 | In fact, any iterable object with a `__contains__` method (receiving `datetime.date` objects) will work. 178 | If you have GPS coordinates and want to have a country name, you can use the [countries](https://github.com/che0/countries) module. 179 | 180 | ## Solar hours 181 | 182 | If the field contains solar hours, here is how to deal with them. 183 | 184 | First of all, you can easily know if you need to set them by checking the `OHParser.needs_solar_hours_setting` variable. 185 | If one of its values is `True`, it appears in the field and you should give to HOH a mean to retrive its time. 186 | 187 | You have to ways to do this. 188 | The first is to give to the `OHParser` the location of the facility, to allow it to calculate solar hours. 189 | The second is to use the `SolarHours` object (which inherits from `dict`), *via* the `OHParser.solar_hours` attribute. 190 | 191 | ```python 192 | # First method. You can use either an 'astral.Location' object or a tuple. 193 | location = astral.Location(["Greenwich", "England", 51.168, 0.0, "Europe/London", 24]) 194 | location = (51.168, 0.0, "Europe/London", 24) 195 | oh = hoh.OHParser(field, location=location) 196 | 197 | # Second method. 198 | solar_hours = { 199 | "sunrise": datetime.time(8, 0), "sunset": datetime.time(20, 0), 200 | "dawn": datetime.time(7, 30), "dusk": datetime.time(20, 30) 201 | } 202 | oh.solar_hours[datetime.date.today()] = solar_hours 203 | ``` 204 | 205 | Attention, except if the facility is on the equator, this setting will be valid only for a short period (except if you provide coordinates, because they will be automatically updated). 206 | 207 | If you try to do something with a field containing solar hours without providing a location, a `humanized_opening_hours.exceptions.SolarHoursError` exception will be raised. 208 | 209 | In some very rare cases, it might be impossible to get solar hours. 210 | For example, in Antactica, the sun may never reach the dawn / dusk location in the sky, so the `astral` module can't return the down time. 211 | So, if you try to get, for example, the next change with a field containing solar hours and located in such location, a `humanized_opening_hours.exceptions.SolarHoursError` exception will also be raised. 212 | 213 | ----- 214 | 215 | Sometimes, especially if you work with numerous fields, you may want to apply the same methods to the same field but for different locations. 216 | To do so, you can use a dedicated method called `this_location()`, which is intended to be used as a context manager. 217 | It allows you to temporarily set a specific location to the OHParser instance. 218 | 219 | ```python 220 | oh = hoh.OHParser( 221 | "Mo-Fr sunrise-sunset", 222 | location=(51.168, 0.0, "Europe/London", 24) 223 | ) 224 | 225 | str(oh.solar_hours.location) == 'Location/Region, tz=Europe/London, lat=51.17, lon=0.00' 226 | 227 | with oh.temporary_location("Paris"): 228 | str(oh.solar_hours.location) == 'Paris/France, tz=Europe/Paris, lat=48.83, lon=2.33' 229 | 230 | str(oh.solar_hours.location) == 'Location/Region, tz=Europe/London, lat=51.17, lon=0.00' 231 | ``` 232 | 233 | ## Have nice schedules 234 | 235 | You can pass any valid locale name to `OHParser`, it will work for the majority of methods, cause they only need Babel's translations. 236 | However, the `description()` and `plaintext_week_description()` methods need more translations, so it works only with a few locales, whose list is available with `hoh.AVAILABLE_LOCALES`. 237 | Use another one will make methods return inconsistent sentences. 238 | 239 | Currently, the following locales are supported: 240 | 241 | - `en`: english (default); 242 | - `fr_FR`: french; 243 | - `de`: deutsch; 244 | - `nl`: dutch; 245 | - `pl`: polish; 246 | - `pt`: portuguese; 247 | - `it`: italian; 248 | - `ru_RU`: russian. 249 | 250 | ----- 251 | 252 | The `get_localized_names()` method returns a dict of lists with the names of months and weekdays in the current locale. 253 | 254 | Example: 255 | 256 | ```python 257 | >>> oh.get_localized_names() 258 | { 259 | 'months': [ 260 | 'January', 'February', 'March', 261 | 'April', 'May', 'June', 'July', 262 | 'August', 'September', 'October', 263 | 'November', 'December' 264 | ], 265 | 'days': [ 266 | 'Monday', 'Tuesday', 'Wednesday', 267 | 'Thursday', 'Friday', 'Saturday', 268 | 'Sunday' 269 | ] 270 | } 271 | ``` 272 | 273 | ----- 274 | 275 | `time_before_next_change()` returns a humanized delay before the next change in opening status. 276 | Like `next_change()`, it can take a `datetime.datetime` moment to get next change from another time. 277 | 278 | ```python 279 | >>> oh.time_before_next_change() 280 | "in 3 hours" 281 | >>> oh.time_before_next_change(word=False) 282 | "3 hours" 283 | ``` 284 | 285 | ----- 286 | 287 | `description()` returns a list of strings (sentences) describing the whole field. 288 | 289 | ```python 290 | # Field: "Mo-Fr 10:00-19:00; Sa 10:00-12:00; Dec 25 off" 291 | >>> print(oh.description()) 292 | ['From Monday to Friday: 10:00 AM – 7:00 PM.', 'On Saturday: 10:00 AM – 12:00 PM.', 'December 25: closed.'] 293 | >>> print('\n'.join(oh.description())) 294 | """ 295 | From Monday to Friday: 10:00 AM – 7:00 PM. 296 | On Saturday: 10:00 AM – 12:00 PM. 297 | December 25: closed. 298 | """ 299 | ``` 300 | 301 | ----- 302 | 303 | `plaintext_week_description()` returns a plaintext description of the opening periods of a week. 304 | This method takes a `year` and a `weeknumber` (both `int`). 305 | You can also specify the first day of the week with the `first_weekday` parameter (as `int`). 306 | Its default value is `0`, meaning "Monday". 307 | 308 | It can also take no parameter, so the described week will be the current one. 309 | 310 | ```python 311 | >>> print(oh.plaintext_week_description(year=2018, weeknumber=1, first_weekday=0)) 312 | """ 313 | Monday: 8:00 AM – 7:00 PM 314 | Tuesday: 8:00 AM – 7:00 PM 315 | Wednesday: 8:00 AM – 7:00 PM 316 | Thursday: 8:00 AM – 7:00 PM 317 | Friday: 8:00 AM – 7:00 PM 318 | Saturday: 8:00 AM – 12:00 PM 319 | Sunday: closed 320 | """ 321 | ``` 322 | 323 | This method uses the `days_of_week()` function to get the datetimes of the days of the requested week. 324 | It is accessible directly through the HOH namespace, and takes the same parameters. 325 | 326 | ----- 327 | 328 | `get_day()` returns a `Day` object, which contains opening periods and useful methods for a day. 329 | It can take a `datetime.date` argument to get the day you want. 330 | 331 | The returned object contains the following attributes. 332 | 333 | - `ohparser` (OHParser) : the OHParser instance where the object come from; 334 | - `date` (datetime.date) : the date of the day; 335 | - `weekday_name` (str) : the name of the day (ex: "Monday"); 336 | - `timespans` : (list[ComputedTimeSpan]) : the computed timespans of the day (containing `datetime.datetime` objects); 337 | - `locale` (babel.Locale) : the locale given to OHParser. 338 | 339 | Attention, the `datetime.datetime` objects in the computed timespans may be in another day, if it contains a period which spans over midnight (like `Mo-Fr 20:00-02:00`). 340 | 341 | # Supported field formats 342 | 343 | Here are the field formats officialy supported and tested (examples). 344 | 345 | ``` 346 | 24/7 347 | Mo 10:00-20:00 348 | Mo-Fr 10:00-20:00 349 | Sa,Su 10:00-20:00 350 | Su,PH off # or "closed" 351 | 10:00-20:00 352 | 20:00-02:00 353 | sunrise-sunset # or "dawn" / "dusk" 354 | (sunrise+01:00)-20:00 355 | Jan 10:00-20:00 356 | Jan-Feb 10:00-20:00 357 | Jan,Dec 10:00-20:00 358 | Jan Mo 10:00-20:00 359 | Jan,Feb Mo 10:00-20:00 360 | Jan-Feb Mo 10:00-20:00 361 | Jan Mo-Fr 10:00-20:00 362 | Jan,Feb Mo-Fr 10:00-20:00 363 | Jan-Feb Mo-Fr 10:00-20:00 364 | SH Mo 10:00-20:00 365 | SH Mo-Fr 10:00-20:00 366 | easter 10:00-20:00 367 | SH,PH Mo-Fr 10:00-20:00 368 | SH,PH Mo-Fr,Su 10:00-20:00 369 | Jan-Feb,Aug Mo-Fr,Su 10:00-20:00 370 | week 1 Mo 09:00-12:00 371 | week 1-10 Su 09:00-12:00 372 | week 1-10/2 Sa-Su 09:00-12:00 373 | 2018 Mo-Fr 10:00-20:00 374 | 2018-2022 Mo-Fr 10:00-20:00 375 | 2018-2022/2 Mo-Fr 10:00-20:00 376 | ``` 377 | 378 | The following formats are NOT supported yet and their parsing will raise a ParseError. 379 | 380 | ``` 381 | Su[1] 10:00-20:00 382 | easter +1 day 10:00-20:00 383 | easter +2 days 10:00-20:00 384 | Mo-Fr 10:00+ 385 | Mo-Fr 10:00,12:00,20:00 # Does not support points in time. 386 | ``` 387 | 388 | For fields like `24/7; Su 10:00-13:00 off`, Sundays are considered as entirely closed. 389 | This should be fixed in a later version. 390 | 391 | # Alternatives 392 | 393 | If you want to parse `opening_hours` fields but HOH doesn't fit your needs, here are a few other libraries which might interest you. 394 | 395 | - [opening_hours.js](https://github.com/opening-hours/opening_hours.js/tree/master): The main library to parse these fields, but written in JS. 396 | - [pyopening_hours](https://github.com/opening-hours/pyopening_hours): A Python implementation of the previous library. 397 | - [simple-opening-hours](https://github.com/ubahnverleih/simple-opening-hours): Another small JS library which can parse simple fields. 398 | 399 | # Performances 400 | 401 | HOH uses the module [Lark](https://github.com/erezsh/lark) (with the Earley parser) to parse the fields. 402 | 403 | It is very optimized (about 20 times faster) for the simplest fields (like `Mo-Fr 10:00-20:00`), so their parsing will be very fast: 404 | 405 | - 0.0002 seconds for a single field; 406 | - 0.023 seconds for a hundred; 407 | - 0.23 seconds for a thousand. 408 | 409 | For more complex fields (like `Jan-Feb Mo-Fr 08:00-19:00`), the parsing is slower: 410 | 411 | - 0.006 seconds for a single field; 412 | - 0.57 seconds for a hundred; 413 | - 5.7 seconds for a thousand. 414 | 415 | # Licence 416 | 417 | This module is published under the AGPLv3 license, the terms of which can be found in the [LICENCE](LICENCE) file. 418 | -------------------------------------------------------------------------------- /humanized_opening_hours/temporal_objects.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import calendar 3 | import gettext 4 | from itertools import groupby 5 | from operator import itemgetter 6 | 7 | import babel 8 | import babel.dates 9 | 10 | from humanized_opening_hours.rendering import ( 11 | set_locale, join_list, render_timespan, render_time, translate_open_closed 12 | ) 13 | from humanized_opening_hours.exceptions import SolarHoursError 14 | 15 | 16 | gettext.install("hoh", "locales") 17 | 18 | 19 | WEEKDAYS = ( 20 | "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" 21 | ) 22 | MONTHS = ( 23 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", 24 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" 25 | ) 26 | 27 | 28 | def consecutive_groups(iterable, ordering=lambda x: x): 29 | """Yields groups of consecutive items using 'itertools.groupby'. 30 | 31 | The *ordering* function determines whether two items are adjacent by 32 | returning their position. 33 | 34 | By default, the ordering function is the identity function. This is 35 | suitable for finding runs of numbers: 36 | 37 | >>> iterable = [1, 10, 11, 12, 20, 30, 31, 32, 33, 40] 38 | >>> for group in consecutive_groups(iterable): 39 | ... print(list(group)) 40 | [1] 41 | [10, 11, 12] 42 | [20] 43 | [30, 31, 32, 33] 44 | [40] 45 | """ 46 | # Code from https://more-itertools.readthedocs.io/en/latest/api.html#more_itertools.consecutive_groups # noqa 47 | for k, g in groupby( 48 | enumerate(iterable), key=lambda x: x[0] - ordering(x[1]) 49 | ): 50 | yield map(itemgetter(1), g) 51 | 52 | 53 | def easter_date(year): 54 | """Returns the datetime.date of easter for a given year (int).""" 55 | # Code from https://github.com/ActiveState/code/tree/master/recipes/Python/576517_Calculate_Easter_Western_given # noqa 56 | a = year % 19 57 | b = year // 100 58 | c = year % 100 59 | d = (19 * a + b - b // 4 - ((b - (b + 8) // 25 + 1) // 3) + 15) % 30 60 | e = (32 + 2 * (b % 4) + 2 * (c // 4) - d - (c % 4)) % 7 61 | f = d + e - 7 * ((a + 11 * d + 22 * e) // 451) + 114 62 | month = f // 31 63 | day = f % 31 + 1 64 | return datetime.date(year, month, day) 65 | 66 | 67 | class Day: 68 | def __init__(self, ohparser, date, computed_timespans): 69 | """A representation of a day and its opening periods. 70 | 71 | Parameters 72 | ---------- 73 | OHParser instance 74 | The instance where the field come from. 75 | datetime.datetime 76 | The date of the day. 77 | list[ComputedTimeSpan] 78 | The opening periods of the day. 79 | 80 | Attributes 81 | ---------- 82 | ohparser 83 | The instance where the field come from. 84 | date 85 | The date of the day. 86 | weekday_name 87 | The name of the weekday, in the locale given to OHParser. 88 | timespans 89 | The opening periods of the day. 90 | locale 91 | The Babel locale given to OHParser. 92 | """ 93 | self.ohparser = ohparser 94 | self.date = date 95 | self.locale = self.ohparser.locale 96 | self.weekday_name = babel.dates.get_day_names( 97 | locale=self.locale 98 | )[date.weekday()] 99 | self.timespans = computed_timespans 100 | 101 | def opens_today(self): 102 | """Returns whether there is at least one opening period on this day.""" 103 | return bool(self.timespans) 104 | 105 | def opening_periods(self): 106 | """ 107 | Returns the opening periods of the day as tuples of the shape 108 | '(beginning, end)' (represented by datetime.datetime objects). 109 | """ 110 | return [ts.to_tuple() for ts in self.timespans] 111 | 112 | def total_duration(self): 113 | """ 114 | Returns the total duration of the opening periods of the day, 115 | as a datetime.timedelta object. 116 | """ 117 | return sum( 118 | [p[1] - p[0] for p in self.opening_periods()], 119 | datetime.timedelta() 120 | ) 121 | 122 | def render_periods(self, join=True): 123 | """ 124 | Returns a list of translated strings 125 | describing the opening periods of the day. 126 | """ 127 | if self.opens_today(): 128 | rendered_periods = [ 129 | render_timespan(ts.timespan, self.locale) 130 | for ts in self.timespans 131 | ] 132 | else: 133 | closed_word = translate_open_closed(self.locale)[1] 134 | rendered_periods = [closed_word] 135 | if join: 136 | return join_list(rendered_periods, self.locale) 137 | else: 138 | return rendered_periods 139 | 140 | def tomorrow(self): 141 | """ 142 | Returns a Day object representing the next day. 143 | 144 | You can also use additions or subtractions with datetime.timedelta 145 | objects, like this. 146 | 147 | >>> seven_days_later = day + datetime.timedelta(7) 148 | >>> type(seven_days_later) == Day 149 | """ 150 | return self + datetime.timedelta(1) 151 | 152 | def yersterday(self): 153 | """ 154 | Returns a Day object representing the eve. 155 | 156 | You can also use additions or subtractions with datetime.timedelta 157 | objects, like this. 158 | 159 | >>> seven_days_before = day - datetime.timedelta(7) 160 | >>> type(seven_days_before) == Day 161 | """ 162 | return self - datetime.timedelta(1) 163 | 164 | def __add__(self, other): 165 | if isinstance(other, datetime.timedelta): 166 | return self.ohparser.get_day(self.date + other) 167 | return NotImplemented 168 | 169 | def __sub__(self, other): 170 | if isinstance(other, datetime.timedelta): 171 | return self.ohparser.get_day(self.date - other) 172 | return NotImplemented 173 | 174 | def __repr__(self): 175 | return str(self) 176 | 177 | def __str__(self): 178 | return "".format(self.date, len(self.timespans)) 179 | 180 | 181 | class Rule: 182 | def __init__(self, range_selectors, time_selectors, status="open"): 183 | self.range_selectors = range_selectors 184 | self.time_selectors = time_selectors 185 | self.status = status 186 | 187 | for timespan in self.time_selectors: 188 | timespan.status = status == "open" 189 | 190 | self.priority = sum( 191 | [sel.priority for sel in self.range_selectors.selectors] 192 | ) 193 | 194 | def get_status_at(self, dt: datetime.datetime, solar_hours): 195 | # TODO: Remove? 196 | for timespan in self.time_selectors: 197 | if timespan.compute(dt, solar_hours).is_open(dt): 198 | if self.status == "open": 199 | return True 200 | else: # self.status == "closed" 201 | return False 202 | return False 203 | 204 | def description(self, localized_names, babel_locale): 205 | set_locale(babel_locale) 206 | if ( 207 | isinstance(self.range_selectors, AlwaysOpenSelector) and 208 | self.time_selectors == [TIMESPAN_ALL_THE_DAY] 209 | ): 210 | return _("Open 24 hours a day and 7 days a week.") 211 | 212 | range_selectors_description = ', '.join( 213 | [ 214 | sel.description(localized_names, babel_locale) 215 | for sel in self.range_selectors.selectors 216 | ] 217 | ) 218 | if not range_selectors_description: 219 | range_selectors_description = _("every days") 220 | if not self.time_selectors: 221 | time_selectors_description = _("closed") 222 | else: 223 | time_selectors_description = join_list( 224 | [ 225 | timespan.description(localized_names, babel_locale) 226 | for timespan in self.time_selectors 227 | ], 228 | babel_locale 229 | ) 230 | full_description = _("{}: {}").format( 231 | range_selectors_description, 232 | time_selectors_description 233 | ) + '.' 234 | return full_description[0].upper() + full_description[1:] 235 | 236 | def __repr__(self): 237 | return str(self) 238 | 239 | def __str__(self): 240 | return "".format( 241 | self.range_selectors, 242 | self.time_selectors, 243 | self.priority 244 | ) 245 | 246 | 247 | # Selectors 248 | 249 | 250 | class BaseSelector: # pragma: no cover 251 | priority = 1 252 | rendering_data = () 253 | 254 | def __init__(self, selectors): 255 | self.selectors = selectors 256 | 257 | def is_included(self, dt, SH_dates, PH_dates): 258 | pass 259 | 260 | def description(self, localized_names, babel_locale): 261 | pass 262 | 263 | def __repr__(self): 264 | return str(self) 265 | 266 | def __str__(self): 267 | return "<{name} {selectors}>".format( 268 | name=self.__class__.__name__, 269 | selectors=str(self.selectors) 270 | ) 271 | 272 | 273 | class RangeSelector(BaseSelector): 274 | def is_included(self, dt, SH_dates, PH_dates): 275 | for selector in self.selectors: 276 | if selector.is_included(dt, SH_dates, PH_dates): 277 | continue 278 | else: 279 | return False 280 | return True 281 | 282 | 283 | class AlwaysOpenSelector(BaseSelector): 284 | def __init__(self): 285 | self.selectors = [] 286 | 287 | def is_included(self, dt, SH_dates, PH_dates): 288 | return True 289 | 290 | 291 | class MonthDaySelector(BaseSelector): 292 | priority = 5 293 | 294 | def is_included(self, dt, SH_dates, PH_dates): 295 | for selector in self.selectors: 296 | if selector.is_included(dt, SH_dates, PH_dates): 297 | return True 298 | return False 299 | 300 | def description(self, localized_names, babel_locale): 301 | set_locale(babel_locale) 302 | rendered_selectors = [ 303 | sel.description(localized_names, babel_locale) 304 | for sel in self.selectors 305 | ] 306 | return join_list(rendered_selectors, babel_locale) 307 | 308 | 309 | class WeekdayHolidaySelector(BaseSelector): 310 | def __init__(self, selectors, SH, PH): 311 | self.selectors = selectors 312 | self.SH = SH # Boolean 313 | self.PH = PH # Boolean 314 | 315 | def is_included(self, dt: datetime.datetime, SH_dates, PH_dates): 316 | if dt in SH_dates: 317 | return self.SH or WEEKDAYS[dt.weekday()] in self.selectors 318 | elif dt in PH_dates: 319 | return self.PH or WEEKDAYS[dt.weekday()] in self.selectors 320 | else: 321 | wd = WEEKDAYS[dt.weekday()] 322 | return wd in self.selectors 323 | 324 | def description(self, localized_names, babel_locale): 325 | # TODO: SH and PH 326 | set_locale(babel_locale) 327 | day_groups = [] 328 | for group in consecutive_groups( 329 | sorted(self.selectors, key=WEEKDAYS.index), ordering=WEEKDAYS.index 330 | ): 331 | group = list(group) 332 | if len(group) == 1: 333 | day_groups.append((group[0],)) 334 | else: 335 | day_groups.append((group[0], group[-1])) 336 | output = [] 337 | for group in day_groups: 338 | if len(group) == 1: 339 | output.append(_("on {weekday}").format( 340 | weekday=localized_names["days"][WEEKDAYS.index(group[0])] 341 | )) 342 | else: 343 | output.append(_("from {weekday1} to {weekday2}").format( 344 | weekday1=localized_names["days"][WEEKDAYS.index(group[0])], 345 | weekday2=localized_names["days"][WEEKDAYS.index(group[1])] 346 | )) 347 | holidays_description = { 348 | (True, True): _("on public and school holidays"), 349 | (True, False): _("on public holidays"), 350 | (False, True): _("on school holidays") 351 | }.get((self.PH, self.SH)) 352 | if holidays_description: 353 | return babel.lists.format_list( 354 | [holidays_description] + output, 355 | locale=babel_locale 356 | ) 357 | else: 358 | return join_list(output, babel_locale) 359 | 360 | def __str__(self): 361 | return "".format( 362 | str(self.selectors), 363 | self.SH, 364 | self.PH 365 | ) 366 | 367 | 368 | ''' 369 | class HolidaySelector(BaseSelector): 370 | def is_included(self, dt: datetime.datetime, SH_dates, PH_dates): 371 | if dt in SH_dates: 372 | return self.SH or WEEKDAYS[dt.weekday()] in self.selectors 373 | elif dt in PH_dates: 374 | return self.PH or WEEKDAYS[dt.weekday()] in self.selectors 375 | return False 376 | ''' 377 | 378 | 379 | class WeekdayInHolidaySelector(BaseSelector): 380 | priority = 3 381 | 382 | def __init__(self, weekdays, holidays): 383 | self.weekdays = weekdays 384 | self.holidays = holidays 385 | 386 | def is_included(self, dt: datetime.datetime, SH_dates, PH_dates): 387 | if ( 388 | ( 389 | (dt in SH_dates and 'SH' in self.holidays) or 390 | (dt in PH_dates and 'PH' in self.holidays) 391 | ) and WEEKDAYS[dt.weekday()] in self.weekdays 392 | ): 393 | return True 394 | return False 395 | 396 | def _weekdays_description(self, localized_names, babel_locale): 397 | set_locale(babel_locale) 398 | day_groups = [] 399 | for group in consecutive_groups( 400 | sorted(self.weekdays, key=WEEKDAYS.index), ordering=WEEKDAYS.index 401 | ): 402 | group = list(group) 403 | if len(group) == 1: 404 | day_groups.append((group[0],)) 405 | else: 406 | day_groups.append((group[0], group[-1])) 407 | output = [] 408 | for group in day_groups: 409 | if len(group) == 1: 410 | output.append(_("on {weekday}").format( 411 | weekday=localized_names["days"][WEEKDAYS.index(group[0])] 412 | )) 413 | else: 414 | output.append(_("from {weekday1} to {weekday2}").format( 415 | weekday1=localized_names["days"][WEEKDAYS.index(group[0])], 416 | weekday2=localized_names["days"][WEEKDAYS.index(group[1])] 417 | )) 418 | return output 419 | 420 | def description(self, localized_names, babel_locale): 421 | set_locale(babel_locale) 422 | weekdays_description = self._weekdays_description( 423 | localized_names, babel_locale 424 | ) 425 | holidays_description = { 426 | (True, True): _("on public and school holidays"), 427 | (True, False): _("on public holidays"), 428 | (False, True): _("on school holidays") 429 | }.get(('PH' in self.holidays, 'SH' in self.holidays)) 430 | return ', '.join([holidays_description] + weekdays_description) 431 | 432 | def __str__(self): 433 | return "".format( 434 | str(self.weekdays), str(self.holidays) 435 | ) 436 | 437 | 438 | class WeekSelector(BaseSelector): 439 | priority = 3 440 | 441 | def __init__(self, week_numbers): 442 | self.week_numbers = week_numbers 443 | 444 | def is_included(self, dt: datetime.datetime, SH_dates, PH_dates): 445 | week_number = dt.isocalendar()[1] 446 | return week_number in self.week_numbers 447 | 448 | def description(self, localized_names, babel_locale): 449 | set_locale(babel_locale) 450 | output = [] 451 | for week_range in self.rendering_data: 452 | if len(week_range) == 1: 453 | output.append(_("in week {week}").format(week=week_range[0])) 454 | elif len(week_range) == 2: 455 | output.append(_("from week {week1} to week {week2}").format( 456 | week1=week_range[0], 457 | week2=week_range[1] 458 | )) 459 | else: 460 | output.append( 461 | _( 462 | "from week {week1} to week {week2}, every {n} weeks" 463 | ).format( 464 | week1=week_range[0], 465 | week2=week_range[1], 466 | n=week_range[2] 467 | ) 468 | ) 469 | return join_list(output, babel_locale) 470 | 471 | def __str__(self): 472 | return '' 473 | 474 | 475 | class YearSelector(BaseSelector): 476 | priority = 4 477 | 478 | def is_included(self, dt: datetime.datetime, SH_dates, PH_dates): 479 | return dt.year in self.selectors 480 | 481 | def description(self, localized_names, babel_locale): 482 | set_locale(babel_locale) 483 | output = [] 484 | for year_range in self.rendering_data: 485 | if len(year_range) == 1: 486 | output.append(_("in {year}").format(year=year_range[0])) 487 | elif len(year_range) == 2: 488 | output.append(_("from {year1} to {year2}").format( 489 | year1=year_range[0], 490 | year2=year_range[1] 491 | )) 492 | else: 493 | output.append( 494 | _( 495 | "from {year1} to {year2}, every {n} years" 496 | ).format( 497 | year1=year_range[0], 498 | year2=year_range[1], 499 | n=year_range[2] 500 | ) 501 | ) 502 | return join_list(output, babel_locale) 503 | 504 | 505 | # Ranges 506 | 507 | 508 | class MonthDayRange: 509 | def __init__(self, monthday_dates): 510 | # TODO: Prevent case like "Jan 1-5-Feb 1-5" 511 | # (monthday_date - monthday_date). 512 | self.date_from = monthday_dates[0] 513 | self.date_to = monthday_dates[1] if len(monthday_dates) == 2 else None 514 | 515 | def is_included(self, dt: datetime.date, SH_dates, PH_dates): 516 | if not self.date_to: 517 | return dt in self.date_from.get_dates(dt) 518 | else: 519 | dt_from = sorted(self.date_from.get_dates(dt))[0] 520 | dt_to = sorted(self.date_to.get_dates(dt))[-1] 521 | if dt_to < dt_from <= dt: # TODO: Fix this in parsing. 522 | # When 'dt_to' is "before" 'dt_from' 523 | # (ex: 'Oct-Mar 07:30-19:30; Apr-Sep 07:00-21:00'), 524 | # it returns False. It shoud fix this bug. 525 | dt_to += datetime.timedelta(weeks=52) 526 | if dt <= dt_to < dt_from: 527 | dt_from -= datetime.timedelta(weeks=52) 528 | return dt_from <= dt <= dt_to 529 | 530 | def description(self, localized_names, babel_locale): 531 | set_locale(babel_locale) 532 | if not self.date_to: 533 | return self.date_from.description(localized_names, babel_locale) 534 | else: 535 | return _("from {monthday1} to {monthday2}").format( 536 | monthday1=self.date_from.description( 537 | localized_names, babel_locale 538 | ), 539 | monthday2=self.date_to.description( 540 | localized_names, babel_locale 541 | ) 542 | ) 543 | 544 | def __repr__(self): 545 | return str(self) 546 | 547 | def __str__(self): 548 | return "".format( 549 | date_from=self.date_from, 550 | date_to=self.date_to 551 | ) 552 | 553 | 554 | class MonthDayDate: 555 | def __init__( 556 | self, kind, year=None, month=None, monthday=None, monthday_to=None 557 | ): 558 | self.kind = kind # "monthday", "monthday-day", "month" or "easter" 559 | self.year = year 560 | self.month = month # int between 1 and 12 561 | self.monthday = monthday 562 | self.monthday_to = monthday_to 563 | 564 | def safe_monthrange(self, year, month): 565 | start, end = calendar.monthrange(year, month) 566 | if start == 0: 567 | start = 1 568 | return (start, end) 569 | 570 | def get_dates(self, dt: datetime.date): 571 | # Returns a set of days covered by the object. 572 | if self.kind == "easter": 573 | return set([easter_date(dt.year)]) 574 | elif self.kind == "month": 575 | first_monthday = datetime.date( 576 | self.year or dt.year, 577 | self.month, 578 | 1 579 | ) 580 | last_monthday = datetime.date( 581 | self.year or dt.year, 582 | self.month, 583 | self.safe_monthrange(self.year or dt.year, self.month)[1] 584 | ) 585 | dates = [] 586 | for i in range((last_monthday - first_monthday).days + 1): 587 | dates.append(first_monthday+datetime.timedelta(i)) 588 | return set(dates) 589 | elif self.kind == "monthday-day": 590 | first_day = datetime.date( 591 | self.year or dt.year, 592 | self.month, 593 | self.monthday 594 | ) 595 | last_day = datetime.date( 596 | self.year or dt.year, 597 | self.month, 598 | self.monthday_to 599 | ) 600 | dates = [] 601 | for i in range((last_day - first_day).days + 1): 602 | dates.append(first_day+datetime.timedelta(i)) 603 | return set(dates) 604 | else: # self.kind == "monthday" 605 | return set([datetime.date( 606 | self.year or dt.year, 607 | self.month, 608 | self.monthday 609 | )]) 610 | 611 | def description(self, localized_names, babel_locale): 612 | set_locale(babel_locale) 613 | if self.kind == "easter": 614 | return _("on easter") 615 | elif self.kind == "month": 616 | return localized_names["months"][self.month-1] 617 | elif self.kind == "monthday-day": 618 | if self.year: 619 | return _("{month} {day1} to {day2}, {year}").format( 620 | month=localized_names["months"][self.month-1], 621 | day1=self.monthday, 622 | day2=self.monthday_to, 623 | year=self.year 624 | ) 625 | else: 626 | return _("from {month} {day1} to {day2}").format( 627 | month=localized_names["months"][self.month-1], 628 | day1=self.monthday, 629 | day2=self.monthday_to 630 | ) 631 | else: # self.kind == "monthday" 632 | if self.year: 633 | date = datetime.date(self.year, self.month, self.monthday) 634 | return babel.dates.format_date(date, format="long") 635 | else: 636 | date = datetime.date(2000, self.month, self.monthday) 637 | return date.strftime(_("%B %-d")) 638 | 639 | def __repr__(self): 640 | return str(self) 641 | 642 | def __str__(self): 643 | if self.kind == "easter": 644 | return "" 645 | elif self.kind == "month": 646 | return "".format( 647 | year=(self.year if self.year else '') + ' ', 648 | month=MONTHS[self.month-1] 649 | ) 650 | elif self.kind == "monthday-day": 651 | return "".format( 652 | year=(self.year if self.year else '') + ' ', 653 | month=MONTHS[self.month-1], 654 | day_from=self.monthday, 655 | day_to=self.monthday_to 656 | ) 657 | else: # self.kind == "monthday" 658 | return "".format( 659 | year=(self.year if self.year else '') + ' ', 660 | month=MONTHS[self.month-1], 661 | day=self.monthday 662 | ) 663 | 664 | 665 | class ComputedTimeSpan: 666 | def __init__(self, beginning, end, status, timespan): 667 | # 'beginning' and 'end' are 'datetime.datetime' objects. 668 | self.beginning = beginning 669 | self.end = end 670 | self.status = status 671 | self.timespan = timespan 672 | 673 | def spans_over_midnight(self): 674 | """Returns whether the TimeSpan spans over midnight.""" 675 | return self.beginning.day != self.end.day 676 | 677 | def __contains__(self, dt): 678 | if ( 679 | not isinstance(dt, datetime.date) or 680 | not isinstance(dt, datetime.datetime) 681 | ): 682 | return NotImplemented 683 | return self.beginning <= dt < self.end 684 | 685 | def __lt__(self, other): 686 | if isinstance(other, ComputedTimeSpan): 687 | return self.beginning < other.beginning 688 | elif isinstance(other, datetime.datetime): 689 | return self.beginning < other 690 | return NotImplemented 691 | 692 | def to_tuple(self): 693 | return (self.beginning, self.end) 694 | 695 | def is_open(self, dt): 696 | return (dt in self) and self.status 697 | 698 | def __repr__(self): 699 | return "".format( 700 | self.beginning.strftime("%H:%M"), self.end.strftime("%H:%M") 701 | ) 702 | 703 | def __str__(self): 704 | return "{} - {}".format( 705 | self.beginning.strftime("%H:%M"), 706 | self.end.strftime("%H:%M") 707 | ) 708 | 709 | 710 | class TimeSpan: 711 | def __init__(self, beginning, end): 712 | self.beginning = beginning 713 | self.end = end 714 | self.status = True # False if closed period. 715 | 716 | def spans_over_midnight(self): 717 | """Returns whether the TimeSpan spans over midnight.""" 718 | if ( 719 | self.beginning.t[0] == self.end.t[0] == "normal" and 720 | self.beginning.t[1] > self.end.t[1] 721 | ): 722 | return True 723 | elif any(( 724 | self.beginning.t[0] == "sunset" and self.end.t[0] == "sunrise", 725 | self.beginning.t[0] == "sunset" and self.end.t[0] == "dawn", 726 | self.beginning.t[0] == "sunrise" and self.end.t[0] == "dawn", 727 | self.beginning.t[0] == "dusk" 728 | )): 729 | return True 730 | else: 731 | return False 732 | 733 | def compute(self, date, solar_hours): 734 | """Returns a 'ComputedTimeSpan' object.""" 735 | return ComputedTimeSpan( 736 | *self.get_times(date, solar_hours), 737 | self.status, 738 | self 739 | ) 740 | 741 | def get_times(self, date, solar_hours): 742 | """Returns the beginning and the end of the TimeSpan. 743 | 744 | Note 745 | ---- 746 | If the TimeSpan spans over midnight, the second datetime of the 747 | returned tuple will be one day later than the first. 748 | 749 | Parameters 750 | ---------- 751 | date: datetime.date 752 | The day to use for returned datetimes. If the timespan spans 753 | over midnight, it will be the date of the first day. 754 | solar_hours: dict{str: datetime.time} 755 | A dict containing hours of sunrise, sunset, dawn and dusk. 756 | 757 | Returns 758 | ------- 759 | tuple[datetime.datetime] 760 | A tuple containing the beginning and the end of the TimeSpan. 761 | """ 762 | beginning_time = self.beginning.get_time(solar_hours, date) 763 | end_time = self.end.get_time(solar_hours, date) 764 | if self.spans_over_midnight(): 765 | end_time += datetime.timedelta(1) 766 | return (beginning_time, end_time) 767 | 768 | def description(self, localized_names, babel_locale): 769 | set_locale(babel_locale) 770 | return render_timespan(self, babel_locale) 771 | 772 | def __repr__(self): 773 | return "".format( 774 | str(self.beginning), str(self.end) 775 | ) 776 | 777 | def __str__(self): 778 | return "{} - {}".format(str(self.beginning), str(self.end)) 779 | 780 | 781 | class Time: 782 | def __init__(self, t): 783 | # ("normal", datetime.time) / ("name", "offset_sign", "delta_seconds") 784 | self.t = t 785 | self.is_min_time = ( 786 | self.t[0] == "normal" and self.t[1] == datetime.time.min 787 | ) 788 | self.is_max_time = ( 789 | self.t[0] == "normal" and self.t[1] == datetime.time.max 790 | ) 791 | # TODO: Set only two attributes: "kind" (str) and "offset" (signed int). 792 | 793 | def get_time(self, solar_hours, date): 794 | """Returns the corresponding datetime.datetime. 795 | 796 | Parameters 797 | ---------- 798 | solar_hours: dict{str: datetime.time} 799 | A dict containing hours of sunrise, sunset, dawn and dusk. 800 | datetime.datetime 801 | The day to use for the returned datetime. 802 | 803 | Returns 804 | ------- 805 | datetime.datetime 806 | The datetime of the Time. 807 | """ 808 | if self.t[0] == "normal": 809 | return datetime.datetime.combine(date, self.t[1]) 810 | solar_hour = solar_hours[self.t[0]] 811 | if solar_hour is None: 812 | raise SolarHoursError() 813 | if self.t[1] == 1: 814 | return datetime.datetime.combine( 815 | date, 816 | ( 817 | datetime.datetime.combine( 818 | datetime.date(1, 1, 1), solar_hour 819 | ) + self.t[2] 820 | ).time() 821 | ) 822 | else: 823 | return datetime.datetime.combine( 824 | date, 825 | ( 826 | datetime.datetime.combine( 827 | datetime.date(1, 1, 1), solar_hour 828 | ) - 829 | self.t[2] 830 | ).time() 831 | ) 832 | 833 | def description(self, localized_names, babel_locale): 834 | set_locale(babel_locale) 835 | return render_time(self, babel_locale) 836 | 837 | def __repr__(self): 838 | return "