diff --git a/opening_hours/__init__.py b/opening_hours/__init__.py index 3dff038..550e881 100644 --- a/opening_hours/__init__.py +++ b/opening_hours/__init__.py @@ -1 +1,2 @@ -from .parse_opening_hours import OpeningHours, create_entry \ No newline at end of file +from .parse_opening_hours import OpeningHours, create_entry +from .models.times import Times \ No newline at end of file diff --git a/opening_hours/helpers.py b/opening_hours/helpers.py new file mode 100644 index 0000000..40b8483 --- /dev/null +++ b/opening_hours/helpers.py @@ -0,0 +1,6 @@ +import unicodedata + +def normalize_string(string): + string = unicodedata.normalize('NFC', string) + string = string.strip() + return string \ No newline at end of file diff --git a/opening_hours/models/times.py b/opening_hours/models/times.py index f969b2d..3608597 100644 --- a/opening_hours/models/times.py +++ b/opening_hours/models/times.py @@ -1,5 +1,7 @@ from opening_hours.models.time import Time, TimeType +from opening_hours.patterns import timerange import logging, os +from opening_hours.helpers import normalize_string logger = logging.getLogger(__name__) @@ -13,36 +15,28 @@ class Times(): end_time = None @classmethod - def from_shortcut_string(cls, times_string, assume_type=None): + def parse(cls, times_string, assume_type=None): """ - create a time object from a string + parse a time object from a string using the pyparsing patterns for a time range + This function will normalize the input value first and will raise a TypeError if None is given. """ - logger.debug("creating times object from shortcut: " + times_string) + logger.debug("creating times object from string: " + times_string) if times_string is None: raise TypeError("Cannot create Times Object from value None") - day = times_string.lower() + times_string = normalize_string(times_string) - # set up some shortcut ranges - allday = cls(Time(0, 0, TimeType.AM), Time(11, 59, TimeType.PM)) - workhours = cls(Time(9, 0, TimeType.AM), Time(5, 0, TimeType.PM)) + return cls.from_parse_results(timerange.parseString(times_string), assume_type=assume_type) - if "24" in day: - return allday - elif "business" in day: - return workhours - elif "work" in day: - return workhours - elif "all day" in day: - return allday - - raise ValueError("string '" + times_string + "' does not match a known pattern") - @classmethod def from_parse_results(cls, result, assume_type=None): - """ Takes values from the pyparsing results and converts them to the appropriate internal objects """ + """ + Takes values from pyparsing results and converts them to an instance of this object. This makes heavy use of the output names defined for certain patterns to help pick out only the relevant data. + This is primarily for internal use and is helpful when combined with the parse() functions of this or other objects. + """ # assumes that all three (hours, minutes, am_pm) are the same length res_dct = result.asDict() + logger.debug(res_dct) if "starttime" in res_dct and "endtime" in res_dct: logger.info("time range detected") start = res_dct.get("starttime")[0] @@ -74,33 +68,47 @@ def from_parse_results(cls, result, assume_type=None): @classmethod def from_shortcut_string(cls, times_shortcut, assume_type=None): """ - create a times object from a shortcut string + create a times object from a shortcut string, such as are used to represent time ranges such as "24 hours", or "work hours". + + This is primarily for internal use and is helpful when combined with the parse() functions of this or other objects. + """ - logger.debug("creating times object from shortcut: " + times_shortcut) + logger.debug("creating times object from shortcut: " + (times_shortcut or "None")) if times_shortcut is None: raise TypeError("Cannot create Times Object from value None") - day = times_shortcut.lower() + times = times_shortcut.lower() # set up some shortcut ranges allday = cls(Time(0, 0, TimeType.AM), Time(11, 59, TimeType.PM)) + workhours = cls(Time(9, 0, TimeType.AM), Time(5, 0, TimeType.PM)) + closed = cls(None, None) - if "all day" in day: - return allday - elif "24" in day: + if "24" in times: return allday - elif day == "": - # if no day is specified, assume the intention is all day + elif "business" in times: + return workhours + elif "work" in times: + return workhours + elif "all day" in times: return allday + elif "closed" in times: + return closed + elif "null" in times: + return closed - raise ValueError("string '" + times_shortcut + "' does not match a known pattern") + + raise ValueError("string '" + times_shortcut or "[NoneType]" + "' does not match a known pattern") def __init__(self, start_time, end_time): - if start_time is None or end_time is None: - raise TypeError("Cannot create Times Object from value None") + """ + Creates a Times object from two Time objects + """ + # if start_time is None or end_time is None: + # raise TypeError("Cannot create Times Object from value None") - logger.debug("creating times from " + str(start_time) + " and " + str(end_time)) + logger.debug("creating times from " + str(start_time or "None") + " and " + str(end_time or "None")) self.start_time = start_time self.end_time = end_time @@ -110,6 +118,11 @@ def get_start_time(self): def get_end_time(self): return self.end_time + + def is_closed(self): + has_none = self.start_time is None or self.end_time is None + times_match = self.start_time == self.end_time + return has_none or times_match #TODO: possibly add a function to see if a single Time is within the range # specified by this Times object @@ -118,7 +131,20 @@ def get_end_time(self): #TODO: getduration function def __str__(self): - return self.start_time + to + self.end_time + if self.is_closed(): + return "closed" + else: + return str(self.start_time) + " to " + str(self.end_time) + + + def json(self): + if self.is_closed(): + return {} + else: + return { + "opens": str(self.start_time.get_as_military_time()), + "closes": str(self.end_time.get_as_military_time()) + } def __eq__(self, other): diff --git a/opening_hours/parse_opening_hours.py b/opening_hours/parse_opening_hours.py index a6be245..d77a1b0 100644 --- a/opening_hours/parse_opening_hours.py +++ b/opening_hours/parse_opening_hours.py @@ -15,7 +15,7 @@ from opening_hours.models.days import Days from opening_hours.models.time import Time, TimeType from opening_hours.models.times import Times -import unicodedata +from opening_hours.helpers import normalize_string import os import logging @@ -33,7 +33,7 @@ class OpeningHours(): def parse(cls, hours_string, assume_type=None): """This parse function allows an OpeningHours instance to be created from most arbitrary strings representing opening hours using pyparsing.""" - hours_string = unicodedata.normalize('NFC', hours_string) + hours_string = normalize_string(hours_string) # TODO: handle unicode confuseables # TODO: handle special cases taht apply to beoth data and time, like "24/7" @@ -46,7 +46,6 @@ def parse(cls, hours_string, assume_type=None): for p in pattern.scanString(hours_string): logger.debug(p) - hours_string = hours_string.strip() return cls(opening_hours_format.parseString(hours_string), assume_type=assume_type) @@ -90,5 +89,4 @@ def create_entry(day, opening, closing, notes=None): entry["notes"] = notes return entry - # print(OpeningHours.parse("by appointment Sunday \u2013 Wednesday from 9 a.m. to 5 p.m.")) diff --git a/setup.py b/setup.py index 570f30b..12237a9 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ # This call to setup() does all the work setup( name="parse-opening-hours", - version="0.4.0", + version="0.4.1", description="Parses opening hours from various human-readable strings into a standard JSON format", long_description=README, long_description_content_type="text/markdown", diff --git a/tests/test_times.py b/tests/test_times.py index f58dabc..70d648f 100644 --- a/tests/test_times.py +++ b/tests/test_times.py @@ -10,16 +10,47 @@ class TestTimes(unittest.TestCase): + def test_create_closed(self): + closed_list = [ + Times(None, None), + Times(Time(12,0), Time(12,0)) + ] + for time in closed_list: + self.assertTrue(time.is_closed()) + def test_create_from_none(self): with self.assertRaises(TypeError): Times.from_shortcut_string(None) with self.assertRaises(TypeError): - Times(None, None) + Times.parse(None) def test_create_from_unknown(self): with self.assertRaises(ValueError): Times.from_shortcut_string("cheeseburger") + def test_parse_time_formats(self): + expected_value = Times(Time(7,0, TimeType.AM), Time(5,0, TimeType.PM)) + input_strings = [ + "700AM-500PM", + ] + + self.run_tests(input_strings, expected_value) + + # def test_shortcuts(self): + # "24/7", + # "Closed", + # for time in input_strings: + # self.assertTrue(Times.from_shortcut_string(time).is_closed()) + + + def test_is_closed(self): + input_strings = [ + "Closed", + "null" + ] + for time in input_strings: + self.assertTrue(Times.from_shortcut_string(time).is_closed()) + # def test_from_parse_regular(self): # test_dict = { # "hour": 5, @@ -128,8 +159,8 @@ def test_equals(self): def run_tests(self, input_strings, expected_result, **kwargs): for input_str in input_strings: - print("Testing String: '", input_str) - self.assertEqual(list(Times.from_shortcut_string(input_str, **kwargs)), expected_result) + print("Testing String: '"+ input_str + "'") + self.assertEqual(Times.parse(input_str, **kwargs), expected_result) if __name__ == '__main__': unittest.main() \ No newline at end of file