diff --git a/AUTHORS.txt b/AUTHORS.txt index 5c8a9faa9..cb92f4870 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -59,3 +59,4 @@ Pi R Alnoman Kamil - noman [at] kamil [dot] gr - https://kamil.gr Leonardo Taccari - iamleot [at] gmail [dot] com Jorenar - dev [at] jorenar [dot] com - https://jorenar.com +Khanh Le diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 264b6d94b..a3c2b956d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,7 @@ may want to subscribe to `GitHub's tag feed ====== unreleased * FIX add timezone when assigning start/end time +* FIX parse ISO-like locale datetime formats with an empty timezone field 0.14.0 ====== diff --git a/khal/parse_datetime.py b/khal/parse_datetime.py index 38adbfff7..8e91392f7 100644 --- a/khal/parse_datetime.py +++ b/khal/parse_datetime.py @@ -23,6 +23,7 @@ strings to date(time) or event objects""" import datetime as dt +import locale import logging import re from calendar import isleap @@ -39,6 +40,29 @@ logger = logging.getLogger("khal") +def _locale_format_candidates(dateformat: str) -> list[str]: + replacements = { + "%c": locale.D_T_FMT, + "%x": locale.D_FMT, + "%X": locale.T_FMT, + } + expanded_format = dateformat + for directive, nl_item in replacements.items(): + if directive in expanded_format: + expanded_format = expanded_format.replace(directive, locale.nl_langinfo(nl_item)) + + candidates = [dateformat] + if expanded_format != dateformat: + candidates.append(expanded_format) + + if "%Z" in expanded_format: + without_timezone = expanded_format.replace(" %Z", "").replace("%Z", "").strip() + if without_timezone not in candidates: + candidates.append(without_timezone) + + return candidates + + def timefstr(dtime_list: list[str], timeformat: str) -> dt.datetime: """converts the first item of a list (a time as a string) to a datetimeobject @@ -80,7 +104,15 @@ def datetimefstr( parts = dateformat.count(" ") + 1 dtstring = " ".join(dtime_list[0:parts]) # only time.strptime can parse the 29th of Feb. if no year is given - dtstart_struct = strptime(dtstring, dateformat) + for candidate_format in _locale_format_candidates(dateformat): + try: + dtstart_struct = strptime(dtstring, candidate_format) + except ValueError: + pass + else: + break + else: + raise ValueError if ( infer_year and dtstart_struct.tm_mon == 2 diff --git a/tests/parse_datetime_test.py b/tests/parse_datetime_test.py index 5cee0346f..5027356b4 100644 --- a/tests/parse_datetime_test.py +++ b/tests/parse_datetime_test.py @@ -1,4 +1,5 @@ import datetime as dt +import locale from collections import OrderedDict import pytest @@ -206,6 +207,27 @@ def test_short_format_contains_year(self): "2017-1-1 16:30".split(), locale=locale, default_day=dt.datetime.today() ) + def test_locale_datetime_with_empty_timezone(self, monkeypatch): + def nl_langinfo(item): + return { + locale.D_T_FMT: "%Y-%m-%dT%T %Z", + locale.D_FMT: "%Y-%m-%d", + locale.T_FMT: "%T", + }[item] + + monkeypatch.setattr(locale, "nl_langinfo", nl_langinfo) + parsed = guessdatetimefstr( + ["2026-05-14T12:00:00"], + { + "timeformat": "%X", + "dateformat": "%x", + "longdateformat": "%x", + "datetimeformat": "%c", + "longdatetimeformat": "%c", + }, + ) + assert parsed == (dt.datetime(2026, 5, 14, 12), False) + class TestGuessTimedeltafstr: def test_single(self):