{"id":15202931,"url":"https://github.com/rezemika/humanized_opening_hours","last_synced_at":"2025-10-28T23:31:29.143Z","repository":{"id":55354358,"uuid":"99339960","full_name":"rezemika/humanized_opening_hours","owner":"rezemika","description":"A parser for the opening_hours fields from OpenStreetMap","archived":false,"fork":false,"pushed_at":"2021-10-21T17:51:37.000Z","size":341,"stargazers_count":26,"open_issues_count":7,"forks_count":20,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-02-01T19:39:20.783Z","etag":null,"topics":["lark","opening-hours","openstreetmap","openstreetmap-data","parser","python3"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rezemika.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-08-04T12:16:16.000Z","updated_at":"2024-03-06T08:52:04.000Z","dependencies_parsed_at":"2022-08-14T22:02:47.800Z","dependency_job_id":null,"html_url":"https://github.com/rezemika/humanized_opening_hours","commit_stats":null,"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezemika%2Fhumanized_opening_hours","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezemika%2Fhumanized_opening_hours/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezemika%2Fhumanized_opening_hours/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezemika%2Fhumanized_opening_hours/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rezemika","download_url":"https://codeload.github.com/rezemika/humanized_opening_hours/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238738044,"owners_count":19522299,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["lark","opening-hours","openstreetmap","openstreetmap-data","parser","python3"],"created_at":"2024-09-28T04:07:22.769Z","updated_at":"2025-10-28T23:31:28.728Z","avatar_url":"https://github.com/rezemika.png","language":"Python","funding_links":[],"categories":["Libraries"],"sub_categories":["Python"],"readme":"Humanized Opening Hours - A parser for the opening_hours fields from OSM\n========================================================================\n\n**Humanized Opening Hours** is a Python 3 module allowing a simple usage of the opening_hours fields used in OpenStreetMap.\n\n**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!**\n\n```python\n\u003e\u003e\u003e import humanized_opening_hours as hoh\n\u003e\u003e\u003e field = \"Mo-Fr 06:00-21:00; Sa,Su 08:00-12:00\"\n\u003e\u003e\u003e oh = hoh.OHParser(field, locale=\"en\")\n\u003e\u003e\u003e oh.is_open()\nTrue\n\u003e\u003e\u003e oh.next_change()\ndatetime.datetime(2017, 12, 24, 12, 0)\n\u003e\u003e\u003e print('\\n'.join(oh.description()))\n\"\"\"\nFrom Monday to Friday: 6:00 AM – 9:00 PM.\nFrom Saturday to Sunday: 8:00 AM – 12:00 PM.\n\"\"\"\n```\n\n**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!**\n\n# Table of contents\n\n- [Installation](#installation)\n  - [Dependencies](#dependencies)\n- [How to use it](#how-to-use-it)\n  - [Basic methods](#basic-methods)\n  - [Solar hours](#solar-hours)\n  - [Have nice schedules](#have-nice-schedules)\n- [Supported field formats](#supported-field-formats)\n- [Alternatives](#alternatives)\n- [Performances](#performances)\n- [Licence](#licence)\n\n# Installation\n\nThis library is so small, you can include it directly into your project.\nAlso, it is available on PyPi.\n\n    $ pip3 install osm-humanized-opening-hours\n\n## Dependencies\n\nThis module requires the following modules, which should be automatically installed when installing HOH with `pip`.\n\n```python\nlark-parser\nbabel\nastral\n```\n\n# How to use it\n\nThe only mandatory argument to give to the constructor is the field, which must be a string.\nIt 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`).\nHowever, to be able to use the most of the rendering methods, it must be in `hoh.AVAILABLE_LOCALES` (a warning will be printed otherwise).\n\n```python\n\u003e\u003e\u003e import humanized_opening_hours as hoh\n\u003e\u003e\u003e field = \"Mo-Fr 06:00-21:00; Sa,Su 07:00-21:00\"\n\u003e\u003e\u003e oh = hoh.OHParser(field)\n```\n\nIf you have a GeoJSON, you can use a dedicated classmethod: `from_geojson()`, which returns an `OHParser` instance.\nIt takes the GeoJSON, and optionally the following arguments:\n\n- `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;\n- `locale` (str): the locale to use (\"en\" default).\n\n## Basic methods\n\nTo know if the facility is open at the present time. Returns a boolean.\nCan take a datetime.datetime moment to check for another time.\n\n```python\n\u003e\u003e\u003e oh.is_open()\nTrue\n```\n\n-----\n\nTo know at which time the facility status (open / closed) will change.\nReturns a datetime.datetime object.\nIt can take a datetime.datetime moment to get next change from another time.\nIf we are on December 24 before 21:00 / 09:00PM...\n\n```python\n\u003e\u003e\u003e oh.next_change()\ndatetime.datetime(2017, 12, 24, 21, 0)\n```\n\nFor fields with consecutive days fully open, `next_change()` will try to get the true next change by recursion.\nYou 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.\nIf this limit is reached, a `NextChangeRecursionError` will be raised.\nYou can deny recursion by setting the `max_recursion` argument to `0`.\n\nThe `NextChangeRecursionError` has a `last_change` attribute, containing the last change got just before raising of the exception.\nYou can get it with a `except NextChangeRecursionError as e:` block.\n\n```python\n\u003e\u003e\u003e oh = hoh.OHParser(\"Mo-Fr 00:00-24:00\")\n\u003e\u003e\u003e oh.next_change(dt=datetime.datetime(2018, 1, 8, 0, 0))\ndatetime.datetime(2018, 1, 11, 23, 59, 59, 999999)\n```\n\n-----\n\nTo get a list of the opening periods between to dates, you can the use `opening_periods_between()` method.\nIt takes two arguments, which can be `datetime.date` or `datetime.datetime` objects.\nIf you pass `datetime.date` objects, it will return all opening periods between these dates (inclusive).\nIf you pass `datetime.datetime`, the returned opening periods will be \"choped\" on these times.\n\nThe returned opening periods are tuples of two `datetime.datetime` objects, representing the beginning and the end of the period.\n\n```python\n\u003e\u003e\u003e oh = hoh.OHParser(\"Mo-Fr 06:00-21:00; Sa,Su 07:00-21:00\")\n\u003e\u003e\u003e oh.opening_periods_between(datetime.date(2018, 1, 1), datetime.date(2018, 1, 7))\n[\n    (datetime.datetime(2018, 1, 1, 6, 0), datetime.datetime(2018, 1, 1, 21, 0)),\n    (datetime.datetime(2018, 1, 2, 6, 0), datetime.datetime(2018, 1, 2, 21, 0)),\n    (datetime.datetime(2018, 1, 3, 6, 0), datetime.datetime(2018, 1, 3, 21, 0)),\n    (datetime.datetime(2018, 1, 4, 6, 0), datetime.datetime(2018, 1, 4, 21, 0)),\n    (datetime.datetime(2018, 1, 5, 6, 0), datetime.datetime(2018, 1, 5, 21, 0)),\n    (datetime.datetime(2018, 1, 6, 7, 0), datetime.datetime(2018, 1, 6, 21, 0)),\n    (datetime.datetime(2018, 1, 7, 7, 0), datetime.datetime(2018, 1, 7, 21, 0))\n]\n```\n\nYou can also set the `merge` parameter to True, to merge continuous opening periods.\n\n-----\n\nYou can get a sanitized version of the field given to the constructor with the `sanitize()` function or the `field` attribute.\n\n```python\n\u003e\u003e\u003e field = \"mo-su 09:30-20h;jan off\"\n\u003e\u003e\u003e print(hoh.sanitize(field))\n\"Mo-Su 09:30-20:00; Jan off\"\n```\n\nIf sanitization is the only thing you need, use HOH for this is probably overkill.\nYou 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.\n\n-----\n\nIf 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.\n\nIf a field contains only a comment (like `\"on appointment\"`), a `CommentOnlyField` exception (inheriting from `ParseError`) will be raised.\nIt contains a `comment` attribute, allowing you to display it instead of the opening hours.\n\nThe `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.\nThe `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).\n\nYou can check equality between two `OHParser` instances.\nIt will be true if both have the same field and the same location.\n\n```python\n\u003e\u003e\u003e import humanized_opening_hours as hoh\n\u003e\u003e\u003e \n\u003e\u003e\u003e oh1 = hoh.OHParser(\"Mo 10:00-20:00\")\n\u003e\u003e\u003e oh2 = hoh.OHParser(\"Mo 10:00-20:00\")\n\u003e\u003e\u003e oh3 = hoh.OHParser(\"Mo 09:00-21:00\")\n\u003e\u003e\u003e oh1 == oh2\nTrue\n\u003e\u003e\u003e oh1 == oh3\nFalse\n```\n\n-----\n\nThe `OHParser` object contains two other attributes: `PH_dates` and `SH_dates`, which are empty lists default.\nTo indicate a date is a public or a school holiday, you can pass its `datetime.date` into these lists.\nYou 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.\nIn fact, any iterable object with a `__contains__` method (receiving `datetime.date` objects) will work.\nIf you have GPS coordinates and want to have a country name, you can use the [countries](https://github.com/che0/countries) module.\n\n## Solar hours\n\nIf the field contains solar hours, here is how to deal with them.\n\nFirst of all, you can easily know if you need to set them by checking the `OHParser.needs_solar_hours_setting` variable.\nIf one of its values is `True`, it appears in the field and you should give to HOH a mean to retrive its time.\n\nYou have to ways to do this.\nThe first is to give to the `OHParser` the location of the facility, to allow it to calculate solar hours.\nThe second is to use the `SolarHours` object (which inherits from `dict`), *via* the `OHParser.solar_hours` attribute.\n\n```python\n# First method. You can use either an 'astral.Location' object or a tuple.\nlocation = astral.Location([\"Greenwich\", \"England\", 51.168, 0.0, \"Europe/London\", 24])\nlocation = (51.168, 0.0, \"Europe/London\", 24)\noh = hoh.OHParser(field, location=location)\n\n# Second method.\nsolar_hours = {\n    \"sunrise\": datetime.time(8, 0), \"sunset\": datetime.time(20, 0),\n    \"dawn\": datetime.time(7, 30), \"dusk\": datetime.time(20, 30)\n}\noh.solar_hours[datetime.date.today()] = solar_hours\n```\n\nAttention, 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).\n\nIf 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.\n\nIn some very rare cases, it might be impossible to get solar hours.\nFor 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.\nSo, 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.\n\n-----\n\nSometimes, especially if you work with numerous fields, you may want to apply the same methods to the same field but for different locations.\nTo do so, you can use a dedicated method called `this_location()`, which is intended to be used as a context manager.\nIt allows you to temporarily set a specific location to the OHParser instance.\n\n```python\noh = hoh.OHParser(\n    \"Mo-Fr sunrise-sunset\",\n    location=(51.168, 0.0, \"Europe/London\", 24)\n)\n\nstr(oh.solar_hours.location) == 'Location/Region, tz=Europe/London, lat=51.17, lon=0.00'\n\nwith oh.temporary_location(\"Paris\"):\n    str(oh.solar_hours.location) == 'Paris/France, tz=Europe/Paris, lat=48.83, lon=2.33'\n\nstr(oh.solar_hours.location) == 'Location/Region, tz=Europe/London, lat=51.17, lon=0.00'\n```\n\n## Have nice schedules\n\nYou can pass any valid locale name to `OHParser`, it will work for the majority of methods, cause they only need Babel's translations.\nHowever, 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`.\nUse another one will make methods return inconsistent sentences.\n\nCurrently, the following locales are supported:\n\n- `en`: english (default);\n- `fr_FR`: french;\n- `de`: deutsch;\n- `nl`: dutch;\n- `pl`: polish;\n- `pt`: portuguese;\n- `it`: italian;\n- `ru_RU`: russian.\n\n-----\n\nThe `get_localized_names()` method returns a dict of lists with the names of months and weekdays in the current locale.\n\nExample:\n\n```python\n\u003e\u003e\u003e oh.get_localized_names()\n{\n    'months': [\n        'January', 'February', 'March',\n        'April', 'May', 'June', 'July',\n        'August', 'September', 'October',\n        'November', 'December'\n    ],\n    'days': [\n        'Monday', 'Tuesday', 'Wednesday',\n        'Thursday', 'Friday', 'Saturday',\n        'Sunday'\n    ]\n}\n```\n\n-----\n\n`time_before_next_change()` returns a humanized delay before the next change in opening status.\nLike `next_change()`, it can take a `datetime.datetime` moment to get next change from another time.\n\n```python\n\u003e\u003e\u003e oh.time_before_next_change()\n\"in 3 hours\"\n\u003e\u003e\u003e oh.time_before_next_change(word=False)\n\"3 hours\"\n```\n\n-----\n\n`description()` returns a list of strings (sentences) describing the whole field.\n\n```python\n# Field: \"Mo-Fr 10:00-19:00; Sa 10:00-12:00; Dec 25 off\"\n\u003e\u003e\u003e print(oh.description())\n['From Monday to Friday: 10:00 AM – 7:00 PM.', 'On Saturday: 10:00 AM – 12:00 PM.', 'December 25: closed.']\n\u003e\u003e\u003e print('\\n'.join(oh.description()))\n\"\"\"\nFrom Monday to Friday: 10:00 AM – 7:00 PM.\nOn Saturday: 10:00 AM – 12:00 PM.\nDecember 25: closed.\n\"\"\"\n```\n\n-----\n\n`plaintext_week_description()` returns a plaintext description of the opening periods of a week.\nThis method takes a `year` and a `weeknumber` (both `int`).\nYou can also specify the first day of the week with the `first_weekday` parameter (as `int`).\nIts default value is `0`, meaning \"Monday\".\n\nIt can also take no parameter, so the described week will be the current one.\n\n```python\n\u003e\u003e\u003e print(oh.plaintext_week_description(year=2018, weeknumber=1, first_weekday=0))\n\"\"\"\nMonday: 8:00 AM – 7:00 PM\nTuesday: 8:00 AM – 7:00 PM\nWednesday: 8:00 AM – 7:00 PM\nThursday: 8:00 AM – 7:00 PM\nFriday: 8:00 AM – 7:00 PM\nSaturday: 8:00 AM – 12:00 PM\nSunday: closed\n\"\"\"\n```\n\nThis method uses the `days_of_week()` function to get the datetimes of the days of the requested week.\nIt is accessible directly through the HOH namespace, and takes the same parameters.\n\n-----\n\n`get_day()` returns a `Day` object, which contains opening periods and useful methods for a day.\nIt can take a `datetime.date` argument to get the day you want.\n\nThe returned object contains the following attributes.\n\n- `ohparser` (OHParser) : the OHParser instance where the object come from;\n- `date` (datetime.date) : the date of the day;\n- `weekday_name` (str) : the name of the day (ex: \"Monday\");\n- `timespans` : (list[ComputedTimeSpan]) : the computed timespans of the day (containing `datetime.datetime` objects);\n- `locale` (babel.Locale) : the locale given to OHParser.\n\nAttention, 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`).\n\n# Supported field formats\n\nHere are the field formats officialy supported and tested (examples).\n\n```\n24/7\nMo 10:00-20:00\nMo-Fr 10:00-20:00\nSa,Su 10:00-20:00\nSu,PH off  # or \"closed\"\n10:00-20:00\n20:00-02:00\nsunrise-sunset  # or \"dawn\" / \"dusk\"\n(sunrise+01:00)-20:00\nJan 10:00-20:00\nJan-Feb 10:00-20:00\nJan,Dec 10:00-20:00\nJan Mo 10:00-20:00\nJan,Feb Mo 10:00-20:00\nJan-Feb Mo 10:00-20:00\nJan Mo-Fr 10:00-20:00\nJan,Feb Mo-Fr 10:00-20:00\nJan-Feb Mo-Fr 10:00-20:00\nSH Mo 10:00-20:00\nSH Mo-Fr 10:00-20:00\neaster 10:00-20:00\nSH,PH Mo-Fr 10:00-20:00\nSH,PH Mo-Fr,Su 10:00-20:00\nJan-Feb,Aug Mo-Fr,Su 10:00-20:00\nweek 1 Mo 09:00-12:00\nweek 1-10 Su 09:00-12:00\nweek 1-10/2 Sa-Su 09:00-12:00\n2018 Mo-Fr 10:00-20:00\n2018-2022 Mo-Fr 10:00-20:00\n2018-2022/2 Mo-Fr 10:00-20:00\n```\n\nThe following formats are NOT supported yet and their parsing will raise a ParseError.\n\n```\nSu[1] 10:00-20:00\neaster +1 day 10:00-20:00\neaster +2 days 10:00-20:00\nMo-Fr 10:00+\nMo-Fr 10:00,12:00,20:00  # Does not support points in time.\n```\n\nFor fields like `24/7; Su 10:00-13:00 off`, Sundays are considered as entirely closed.\nThis should be fixed in a later version.\n\n# Alternatives\n\nIf you want to parse `opening_hours` fields but HOH doesn't fit your needs, here are a few other libraries which might interest you.\n\n- [opening_hours.js](https://github.com/opening-hours/opening_hours.js/tree/master): The main library to parse these fields, but written in JS.\n- [pyopening_hours](https://github.com/opening-hours/pyopening_hours): A Python implementation of the previous library.\n- [simple-opening-hours](https://github.com/ubahnverleih/simple-opening-hours): Another small JS library which can parse simple fields.\n\n# Performances\n\nHOH uses the module [Lark](https://github.com/erezsh/lark) (with the Earley parser) to parse the fields.\n\nIt 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:\n\n- 0.0002 seconds for a single field;\n- 0.023 seconds for a hundred;\n- 0.23 seconds for a thousand.\n\nFor more complex fields (like `Jan-Feb Mo-Fr 08:00-19:00`), the parsing is slower:\n\n- 0.006 seconds for a single field;\n- 0.57 seconds for a hundred;\n- 5.7 seconds for a thousand.\n\n# Licence\n\nThis module is published under the AGPLv3 license, the terms of which can be found in the [LICENCE](LICENCE) file.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frezemika%2Fhumanized_opening_hours","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frezemika%2Fhumanized_opening_hours","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frezemika%2Fhumanized_opening_hours/lists"}