Skip to content

Commit 0076773

Browse files
committed
Added some new functions to dates, with appropriate tests.
1 parent d23526c commit 0076773

File tree

2 files changed

+207
-10
lines changed

2 files changed

+207
-10
lines changed

domdf_python_tools/dates.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
# stdlib
3535
import datetime
36+
from collections import OrderedDict
3637

3738
# 3rd party
3839
try:
@@ -153,6 +154,101 @@ def utc_timestamp_to_datetime(utc_timestamp, output_tz=None):
153154

154155
new_datetime = datetime.datetime.fromtimestamp(utc_timestamp, output_tz)
155156
return new_datetime.astimezone(output_tz)
157+
158+
159+
# List of months and their 3-character shortcodes.
160+
months = OrderedDict(
161+
Jan="January",
162+
Feb="February",
163+
Mar="March",
164+
Apr="April",
165+
May="May",
166+
Jun="June",
167+
Jul="July",
168+
Aug="August",
169+
Sep="September",
170+
Oct="October",
171+
Nov="November",
172+
Dec="December",
173+
)
174+
175+
176+
def parse_month(month):
177+
"""
178+
Converts an integer or shorthand month into the full month name
179+
180+
:param month:
181+
:type month: str or int
182+
183+
:return:
184+
:rtype: str
185+
"""
186+
187+
try:
188+
month = int(month)
189+
except ValueError:
190+
try:
191+
return months[month.capitalize()[:3]]
192+
except KeyError:
193+
raise ValueError("Unrecognised month value")
194+
195+
# Only get here if first try succeeded
196+
if 0 < month <= 12:
197+
return list(months.values())[month - 1]
198+
else:
199+
raise ValueError("Unrecognised month value")
200+
201+
202+
def get_month_number(month):
203+
"""
204+
Returns the number of the given month. If ``month`` is already a
205+
number between 1 and 12 it will be returned immediately.
206+
207+
:param month: The month to convert to a number
208+
:type month: str or int
209+
210+
:return: The number of the month
211+
:rtype:
212+
"""
213+
214+
if isinstance(month, int):
215+
if 0 < month <= 12:
216+
return month
217+
else:
218+
raise ValueError("The given month is not recognised.")
219+
else:
220+
month = parse_month(month)
221+
return list(months.values()).index(month) + 1
222+
223+
224+
def check_date(month, day, leap_year=True):
225+
"""
226+
Returns ``True`` if the day number is valid for the given month.
227+
Note that this will return ``True`` for the 29th Feb. If you don't
228+
want this behaviour set ``leap_year`` to ``False``.
229+
230+
:param month: The month to test
231+
:type month: str, int
232+
:param day: The day number to test
233+
:type day: int
234+
:param leap_year: Whether to return ``True`` for 29th Feb
235+
:type leap_year: bool
236+
237+
:return: ``True`` if the date is valid
238+
:rtype: bool
239+
"""
240+
241+
# Ensure day is an integer
242+
day = int(day)
243+
month = get_month_number(month)
244+
year = 2020 if leap_year else 2019
245+
246+
try:
247+
datetime.date(year, month, day)
248+
return True
249+
except ValueError:
250+
return False
251+
156252

157253
except ImportError:
158254
import warnings

tests/test_dates.py

Lines changed: 111 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212

1313
# 3rd party
1414
import pytz
15+
import pytest
1516

1617
# this package
1718
from domdf_python_tools import dates
1819

1920
test_date = datetime.datetime(1996, 10, 13, 2, 20).replace(tzinfo=pytz.utc)
2021
today = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) # make sure UTC
2122

23+
2224
# TODO: travis matrix to test without pytz installed
2325
# TODO: test get_timezone
2426

@@ -29,7 +31,7 @@ def test_utc_offset():
2931
assert dates.get_utc_offset("Europe/London", test_date) == datetime.timedelta(0, 3600)
3032
assert dates.get_utc_offset("Africa/Algiers", test_date) == datetime.timedelta(0, 3600)
3133
# TODO: Finish
32-
34+
3335
# Check that the correct UTC offsets are given for common timezones for today
3436
assert dates.get_utc_offset("US/Pacific", today) == datetime.timedelta(-1, 61200)
3537
assert dates.get_utc_offset("Europe/London", today) == datetime.timedelta(0, 3600)
@@ -39,36 +41,39 @@ def test_utc_offset():
3941
assert dates.get_utc_offset("US/Pacific") == datetime.timedelta(-1, 61200)
4042
assert dates.get_utc_offset("Europe/London") == datetime.timedelta(0, 3600)
4143
assert dates.get_utc_offset("Africa/Algiers") == datetime.timedelta(0, 3600)
42-
# TODO: Finish
44+
45+
46+
# TODO: Finish
4347

4448

4549
def test_converting_timezone():
4650
# No matter what timezone we convert to the timestamp should be the same
4751
for tz in pytz.all_timezones:
48-
assert test_date.astimezone(dates.get_timezone(tz, test_date)).timestamp() == test_date.timestamp() == 845173200.0
49-
52+
assert test_date.astimezone(
53+
dates.get_timezone(tz, test_date)).timestamp() == test_date.timestamp() == 845173200.0
54+
5055
if dates.get_utc_offset(tz, test_date): # otherwise the timezone stayed as UTC
5156
assert test_date.astimezone(dates.get_timezone(tz, test_date)).hour != test_date.hour
5257

5358
# And again with today's date
5459
assert today.astimezone(dates.get_timezone(tz, today)).timestamp() == today.timestamp()
5560
if dates.get_utc_offset(tz, today): # otherwise the timezone stayed as UTC
5661
assert today.astimezone(dates.get_timezone(tz, today)).hour != today.hour
57-
58-
62+
63+
5964
def test_utc_timestamp_to_datetime():
6065
# Going from a datetime object to timezone and back should give us the same object
6166
for tz in pytz.all_timezones:
6267
tzinfo = dates.get_timezone(tz, test_date)
6368
dt = test_date.astimezone(tzinfo)
6469
assert dates.utc_timestamp_to_datetime(dt.timestamp(), tzinfo) == dt
65-
70+
6671
# And again with today's date
6772
tzinfo = dates.get_timezone(tz, today)
6873
dt = today.astimezone(tzinfo)
6974
assert dates.utc_timestamp_to_datetime(dt.timestamp(), tzinfo) == dt
7075

71-
76+
7277
def test_set_timezone():
7378
# Setting the timezone should change the timestamp
7479
for tz in pytz.all_timezones:
@@ -79,7 +84,7 @@ def test_set_timezone():
7984

8085
# Difference between "today" and the new timezone should be the timezone difference
8186
assert \
82-
dates.set_timezone(today, dates.get_timezone(tz, today)).timestamp() +\
87+
dates.set_timezone(today, dates.get_timezone(tz, today)).timestamp() + \
8388
dates.get_utc_offset(tz, today).total_seconds() \
8489
== today.timestamp()
8590

@@ -104,6 +109,102 @@ def test_set_timezone():
104109
#
105110
# )
106111
assert \
107-
dates.set_timezone(test_date, dates.get_timezone(tz, test_date)).timestamp() +\
112+
dates.set_timezone(test_date, dates.get_timezone(tz, test_date)).timestamp() + \
108113
dates.get_utc_offset(tz, test_date).total_seconds() \
109114
== test_date.timestamp()
115+
116+
117+
months = [
118+
"January",
119+
"February",
120+
"March",
121+
"April",
122+
"May",
123+
"June",
124+
"July",
125+
"August",
126+
"September",
127+
"October",
128+
"November",
129+
"December",
130+
]
131+
132+
133+
def test_parse_month():
134+
for month_idx, month in enumerate(months):
135+
136+
month_idx += 1 # to make 1-indexed
137+
138+
for i in range(3, len(month)):
139+
assert dates.parse_month(month.lower()[:i]) == month
140+
assert dates.parse_month(month.upper()[:i]) == month
141+
assert dates.parse_month(month.capitalize()[:i]) == month
142+
143+
assert dates.parse_month(month_idx) == month
144+
145+
for value in ["abc", 0, "0", -1, "-1", 13, "13"]:
146+
with pytest.raises(ValueError):
147+
dates.parse_month(value)
148+
149+
150+
def test_get_month_number():
151+
for month_idx, month in enumerate(months):
152+
153+
month_idx += 1 # to make 1-indexed
154+
155+
for i in range(3, len(month)):
156+
assert dates.get_month_number(month.lower()[:i]) == month_idx
157+
assert dates.get_month_number(month.upper()[:i]) == month_idx
158+
assert dates.get_month_number(month.capitalize()[:i]) == month_idx
159+
160+
assert dates.get_month_number(month) == month_idx
161+
162+
for month_idx in range(1, 13):
163+
assert dates.get_month_number(month_idx) == month_idx
164+
165+
for value in ["abc", 0, "0", -1, "-1", 13, "13"]:
166+
with pytest.raises(ValueError):
167+
dates.get_month_number(value)
168+
169+
170+
def test_check_date():
171+
for month_idx, month in enumerate(months):
172+
173+
month_idx += 1 # to make 1-indexed
174+
175+
if month_idx in {9, 4, 6, 11}:
176+
max_day = 30
177+
elif month_idx == 2:
178+
max_day = 28
179+
else:
180+
max_day = 31
181+
182+
for day in range(-5, 36):
183+
if month_idx == 2 and day == 29:
184+
for i in range(3, len(month)):
185+
assert dates.check_date(month.lower()[:i], 29)
186+
assert dates.check_date(month.upper()[:i], 29)
187+
assert dates.check_date(month.capitalize()[:i], 29)
188+
189+
assert not dates.check_date(month.lower()[:i], 29, False)
190+
assert not dates.check_date(month.upper()[:i], 29, False)
191+
assert not dates.check_date(month.capitalize()[:i], 29, False)
192+
193+
assert dates.check_date(month, 29)
194+
assert not dates.check_date(month, 29, False)
195+
196+
elif 0 < day <= max_day:
197+
for i in range(3, len(month)):
198+
assert dates.check_date(month.lower()[:i], day)
199+
assert dates.check_date(month.upper()[:i], day)
200+
assert dates.check_date(month.capitalize()[:i], day)
201+
202+
assert dates.check_date(month, day)
203+
204+
else:
205+
for i in range(3, len(month)):
206+
assert not dates.check_date(month.lower()[:i], day)
207+
assert not dates.check_date(month.upper()[:i], day)
208+
assert not dates.check_date(month.capitalize()[:i], day)
209+
210+
assert not dates.check_date(month, day)

0 commit comments

Comments
 (0)