Skip to content

Commit 3804faf

Browse files
committed
Fix parsing of 29.02. etc. in leap years
Just in time for 2020.
1 parent acc28ab commit 3804faf

File tree

4 files changed

+60
-3
lines changed

4 files changed

+60
-3
lines changed

CHANGELOG.rst

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ not released yet
1414
* BREAKING CHANGE: python 2 is no longer supported (Hugo Osvaldo Barrera)
1515
* updated dependency: vdirsyncer >= 0.5.2
1616
* make tests work with icalendar 3.9.2 (no functional changes) (Christian Geier)
17+
* new dependency: freezegun (only for running the tests)
1718

1819
* support for showing the birthday of contacts with no FN property (Hugo
1920
Osvaldo Barrera)

khal/aux.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
"""this module contains some helper functions converting strings or list of
2323
strings to date(time) or event objects"""
2424

25+
from calendar import isleap
2526
from datetime import date, datetime, timedelta, time
2627
import random
2728
import string
29+
from time import strptime
2830

2931
import icalendar
3032
import pytz
@@ -145,14 +147,18 @@ def datetimefstr_weekday(dtime_list, timeformat):
145147
return dtime
146148

147149

148-
def guessdatetimefstr(dtime_list, locale, default_day=datetime.today()):
150+
def guessdatetimefstr(dtime_list, locale, default_day=None):
149151
"""
150152
:type dtime_list: list
151153
:type locale: dict
152154
:type default_day: datetime.datetime
153155
:rtype: datetime.datetime
154156
"""
157+
# if now() is called as default param, mocking with freezegun won't work
158+
if default_day is None:
159+
default_day = datetime.now().date()
155160
# TODO rename in guessdatetimefstrLIST or something saner altogether
161+
156162
def timefstr_day(dtime_list, timeformat):
157163
if locale['timeformat'] == '%H:%M' and dtime_list[0] == '24:00':
158164
a_date = datetime.combine(default_day, time(0))
@@ -163,8 +169,26 @@ def timefstr_day(dtime_list, timeformat):
163169
return a_date
164170

165171
def datefstr_year(dtime_list, dateformat):
166-
a_date = datetimefstr(dtime_list, dateformat)
167-
a_date = datetime(*(default_day.timetuple()[:1] + a_date.timetuple()[1:5]))
172+
"""should be used if a date(time) without year is given
173+
174+
we cannot use datetimefstr() here, because only time.strptime can
175+
parse the 29th of Feb. if no year is given
176+
177+
example: dtime_list = ['17.03.', 'description']
178+
dateformat = '%d.%m.'
179+
or : dtime_list = ['17.03.', '16:00', 'description']
180+
dateformat = '%d.%m. %H:%M'
181+
"""
182+
parts = dtformat.count(' ') + 1
183+
dtstring = ' '.join(dtime_list[0:parts])
184+
dtstart = strptime(dtstring, dtformat)
185+
if dtstart.tm_mon == 2 and dtstart.tm_mday == 29 and not isleap(default_day.year):
186+
raise ValueError
187+
188+
for _ in range(parts):
189+
dtime_list.pop(0)
190+
191+
a_date = datetime(*(default_day.timetuple()[:1] + dtstart[1:5]))
168192
return a_date
169193

170194
dtstart = None

setup.py

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
'tzlocal>=1.0',
1414
]
1515

16+
test_requirements = [
17+
'freezegun'
18+
]
19+
1620
extra_requirements = {
1721
'proctitle': ['setproctitle'],
1822
}
@@ -38,6 +42,7 @@
3842
},
3943
install_requires=requirements,
4044
extras_require=extra_requirements,
45+
tests_require=test_requirements,
4146
setup_requires=['setuptools_scm'], # not needed when using packages from PyPI
4247
use_scm_version={'write_to': 'khal/version.py'},
4348
classifiers=[

tests/aux_test.py

+27
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
import icalendar
66
import pytz
7+
from freezegun import freeze_time
78

89
from khal.aux import construct_event, guessdatetimefstr
910
from khal import aux
11+
from khal.exceptions import FatalError
12+
import pytest
1013

1114
from .aux import _get_all_vevents_file, _get_text, \
1215
normalize_component
@@ -260,6 +263,30 @@ def test_construct_event_format_de_complexer():
260263
assert _replace_uid(event).to_ical() == vevent
261264

262265

266+
test_set_leap_year = _create_testcases(
267+
('29.02. Äwesöme Event',
268+
['BEGIN:VEVENT',
269+
'SUMMARY:Äwesöme Event',
270+
'DTSTART;VALUE=DATE:20160229',
271+
'DTEND;VALUE=DATE:20160301',
272+
'DTSTAMP;VALUE=DATE-TIME:20140216T120000Z',
273+
'UID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA',
274+
'END:VEVENT']),
275+
)
276+
277+
278+
def test_leap_year():
279+
for data_list, vevent in test_set_leap_year:
280+
with freeze_time('1999-1-1'):
281+
with pytest.raises(FatalError):
282+
event = construct_event(
283+
data_list.split(), _now=_now, locale=locale_de)
284+
with freeze_time('2016-1-1'):
285+
event = construct_event(
286+
data_list.split(), _now=_now, locale=locale_de)
287+
assert _replace_uid(event).to_ical() == vevent
288+
289+
263290
test_set_description = _create_testcases(
264291
# now events where the start date has to be inferred, too
265292
# today

0 commit comments

Comments
 (0)