From ef32805a2c77b99440c49e7a352311d7eb2cb53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Tue, 26 Sep 2023 21:01:30 +0200 Subject: refactor: DateTime: Simplify parsing code --- nbxmpp/modules/date_and_time.py | 124 ++++++++++++++++++------------------- test/unit/test_datetime_parsing.py | 9 ++- 2 files changed, 65 insertions(+), 68 deletions(-) diff --git a/nbxmpp/modules/date_and_time.py b/nbxmpp/modules/date_and_time.py index b96df25..4815d28 100644 --- a/nbxmpp/modules/date_and_time.py +++ b/nbxmpp/modules/date_and_time.py @@ -25,24 +25,15 @@ from datetime import tzinfo log = logging.getLogger('nbxmpp.m.date_and_time') -PATTERN_DATETIME = re.compile( - r'([0-9]{4}-[0-9]{2}-[0-9]{2})' - r'T' - r'([0-9]{2}:[0-9]{2}:[0-9]{2})' - r'(\.[0-9]{0,6})?' - r'(?:[0-9]+)?' - r'(?:(Z)|(?:([-+][0-9]{2}):([0-9]{2})))$' -) - -PATTERN_DELAY = re.compile( - r'([0-9]{4}-[0-9]{2}-[0-9]{2})' - r'T' - r'([0-9]{2}:[0-9]{2}:[0-9]{2})' - r'(\.[0-9]{0,6})?' - r'(?:[0-9]+)?' - r'(?:(Z)|(?:([-+][0]{2}):([0]{2})))$' -) - +PATTERN_DATETIME = re.compile(r''' + ([0-9]{4}-[0-9]{2}-[0-9]{2}) # Date + T # Separator + ([0-9]{2}:[0-9]{2}:[0-9]{2}) # Time + (?P\.[0-9]{0,6})? # Fractual Seconds + [0-9]* # lose everything > 6 + (Z|[-+][0-9]{2}:[0-9]{2}) # UTC Offset + $ # End of String +''', re.VERBOSE) ZERO = timedelta(0) HOUR = timedelta(hours=1) @@ -124,8 +115,12 @@ def create_tzinfo(hours=0, minutes=0, tz_string=None): return timezone(timedelta(hours=hours, minutes=minutes)) -def parse_datetime(timestring: str | None, check_utc=False, - convert='utc', epoch=False): +def parse_datetime( + timestring: str | None, + check_utc: bool = False, + convert: str | None = 'utc', + epoch: bool = False +) -> datetime | float | None: ''' Parse a XEP-0082 DateTime Profile String @@ -149,52 +144,51 @@ def parse_datetime(timestring: str | None, check_utc=False, return None if convert not in (None, 'utc', 'local'): raise TypeError('"%s" is not a valid value for convert') + + match = PATTERN_DATETIME.match(timestring) + if match is None: + return + + timestring = ''.join(match.groups('')) + strformat = '%Y-%m-%d%H:%M:%S%z' + if match.group('frac'): + # Fractional second addendum to Time + strformat = '%Y-%m-%d%H:%M:%S.%f%z' + + try: + date_time = datetime.strptime(timestring, strformat) + except ValueError: + return None + if check_utc: - match = PATTERN_DELAY.match(timestring) - else: - match = PATTERN_DATETIME.match(timestring) - - if match: - timestring = ''.join(match.groups('')) - strformat = '%Y-%m-%d%H:%M:%S%z' - if match.group(3): - # Fractional second addendum to Time - strformat = '%Y-%m-%d%H:%M:%S.%f%z' - if match.group(4): - # UTC string denoted by addition of the character 'Z' - timestring = timestring[:-1] + '+0000' - try: - date_time = datetime.strptime(timestring, strformat) - except ValueError: - pass - else: - if check_utc: - if convert != 'utc': - raise ValueError( - 'check_utc can only be used with convert="utc"') - date_time.replace(tzinfo=timezone.utc) - if epoch: - return date_time.timestamp() - return date_time - - if convert == 'utc': - date_time = date_time.astimezone(timezone.utc) - if epoch: - return date_time.timestamp() - return date_time - - if epoch: - # epoch is always UTC, use convert='utc' or check_utc=True - raise ValueError( - 'epoch not available while converting to local') - - if convert == 'local': - date_time = date_time.astimezone(LocalTimezone()) - return date_time - - # convert=None - return date_time - return None + if convert != 'utc': + raise ValueError( + 'check_utc can only be used with convert="utc"') + + if date_time.tzinfo != timezone.utc: + return None + + if epoch: + return date_time.timestamp() + return date_time + + if convert == 'utc': + date_time = date_time.astimezone(timezone.utc) + if epoch: + return date_time.timestamp() + return date_time + + if epoch: + # epoch is always UTC, use convert='utc' or check_utc=True + raise ValueError( + 'epoch not available while converting to local') + + if convert == 'local': + date_time = date_time.astimezone(LocalTimezone()) + return date_time + + # convert=None + return date_time def get_local_time(): diff --git a/test/unit/test_datetime_parsing.py b/test/unit/test_datetime_parsing.py index 7ef0aa5..8970611 100644 --- a/test/unit/test_datetime_parsing.py +++ b/test/unit/test_datetime_parsing.py @@ -33,8 +33,10 @@ class TestDateTime(unittest.TestCase): strings2 = { # Valid strings with offset - '2017-11-05T01:41:20-05:00': datetime(2017, 11, 5, 1, 41, 20, 0, create_tzinfo(hours=-5)), - '2017-11-05T01:41:20+05:00': datetime(2017, 11, 5, 1, 41, 20, 0, create_tzinfo(hours=5)), + '2017-11-05T07:41:20-05:00': datetime(2017, 11, 5, 12, 41, 20, 0, timezone.utc), + '2017-11-05T07:41:20+05:00': datetime(2017, 11, 5, 2, 41, 20, 0, timezone.utc), + '2017-11-05T01:41:20+00:00': datetime(2017, 11, 5, 1, 41, 20, 0, timezone.utc), + '2017-11-05T01:41:20Z': datetime(2017, 11, 5, 1, 41, 20, 0, timezone.utc), } for time_string, expected_value in strings.items(): @@ -43,7 +45,7 @@ class TestDateTime(unittest.TestCase): for time_string, expected_value in strings2.items(): result = parse_datetime(time_string, convert='utc') - self.assertEqual(result, expected_value.astimezone(timezone.utc)) + self.assertEqual(result, expected_value) def test_convert_to_local(self): @@ -79,6 +81,7 @@ class TestDateTime(unittest.TestCase): for time_string, expected_value in strings.items(): result = parse_datetime(time_string, convert=None) + assert result is not None self.assertEqual(result.utcoffset(), expected_value) def test_check_utc(self): -- cgit v1.2.3