Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
======
Expand Down
34 changes: 33 additions & 1 deletion khal/parse_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Comment on lines +58 to +61

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also handle %z?


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

Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions tests/parse_datetime_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime as dt
import locale
from collections import OrderedDict

import pytest
Expand Down Expand Up @@ -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):
Expand Down