Skip to content

Commit 99f35da

Browse files
authored
Merge pull request #2 from makermelissa/master
Added fromisoformat() to date and datetime
2 parents 70a4e69 + 898031f commit 99f35da

File tree

4 files changed

+156
-14
lines changed

4 files changed

+156
-14
lines changed

adafruit_datetime.py

+135
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
# pylint: disable=too-many-lines
3030
import time as _time
3131
import math as _math
32+
import re as _re
3233
from micropython import const
3334

3435
__version__ = "0.0.0-auto.0"
@@ -62,6 +63,8 @@
6263
)
6364
_DAYNAMES = (None, "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
6465

66+
_INVALID_ISO_ERROR = "Invalid isoformat string: '{}'"
67+
6568
# Utility functions - universal
6669
def _cmp(obj_x, obj_y):
6770
return 0 if obj_x == obj_y else 1 if obj_x > obj_y else -1
@@ -657,6 +660,20 @@ def fromordinal(cls, ordinal):
657660
y, m, d = _ord2ymd(ordinal)
658661
return cls(y, m, d)
659662

663+
@classmethod
664+
def fromisoformat(cls, date_string):
665+
"""Return a date object constructed from an ISO date format.
666+
Valid format is ``YYYY-MM-DD``
667+
668+
"""
669+
match = _re.match(
670+
r"([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$", date_string
671+
)
672+
if match:
673+
y, m, d = int(match.group(1)), int(match.group(2)), int(match.group(3))
674+
return cls(y, m, d)
675+
raise ValueError(_INVALID_ISO_ERROR.format(date_string))
676+
660677
@classmethod
661678
def today(cls):
662679
"""Return the current local date."""
@@ -907,6 +924,96 @@ def tzinfo(self):
907924
"""
908925
return self._tzinfo
909926

927+
@staticmethod
928+
def _parse_iso_string(string_to_parse, segments):
929+
results = []
930+
931+
remaining_string = string_to_parse
932+
for regex in segments:
933+
match = _re.match(regex, remaining_string)
934+
if match:
935+
for grp in range(regex.count("(")):
936+
results.append(int(match.group(grp + 1)))
937+
remaining_string = remaining_string[len(match.group(0)) :]
938+
elif remaining_string: # Only raise an error if we're not done yet
939+
raise ValueError()
940+
if remaining_string:
941+
raise ValueError()
942+
return results
943+
944+
# pylint: disable=too-many-locals
945+
@classmethod
946+
def fromisoformat(cls, time_string):
947+
"""Return a time object constructed from an ISO date format.
948+
Valid format is ``HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]``
949+
950+
"""
951+
# Store the original string in an error message
952+
original_string = time_string
953+
match = _re.match(r"(.*)[\-\+]", time_string)
954+
offset_string = None
955+
if match:
956+
offset_string = time_string[len(match.group(1)) :]
957+
time_string = match.group(1)
958+
959+
time_segments = (
960+
r"([0-9][0-9])",
961+
r":([0-9][0-9])",
962+
r":([0-9][0-9])",
963+
r"\.([0-9][0-9][0-9])",
964+
r"([0-9][0-9][0-9])",
965+
)
966+
offset_segments = (
967+
r"([\-\+][0-9][0-9]):([0-9][0-9])",
968+
r":([0-9][0-9])",
969+
r"\.([0-9][0-9][0-9][0-9][0-9][0-9])",
970+
)
971+
972+
try:
973+
results = cls._parse_iso_string(time_string, time_segments)
974+
if len(results) < 1:
975+
raise ValueError(_INVALID_ISO_ERROR.format(original_string))
976+
if len(results) < len(time_segments):
977+
results += [None] * (len(time_segments) - len(results))
978+
if offset_string:
979+
results += cls._parse_iso_string(offset_string, offset_segments)
980+
except ValueError as error:
981+
raise ValueError(_INVALID_ISO_ERROR.format(original_string)) from error
982+
983+
hh = results[0]
984+
mm = results[1] if len(results) >= 2 and results[1] is not None else 0
985+
ss = results[2] if len(results) >= 3 and results[2] is not None else 0
986+
us = 0
987+
if len(results) >= 4 and results[3] is not None:
988+
us += results[3] * 1000
989+
if len(results) >= 5 and results[4] is not None:
990+
us += results[4]
991+
tz = None
992+
if len(results) >= 7:
993+
offset_hh = results[5]
994+
multiplier = -1 if offset_hh < 0 else 1
995+
offset_mm = results[6] * multiplier
996+
offset_ss = (results[7] if len(results) >= 8 else 0) * multiplier
997+
offset_us = (results[8] if len(results) >= 9 else 0) * multiplier
998+
offset = timedelta(
999+
hours=offset_hh,
1000+
minutes=offset_mm,
1001+
seconds=offset_ss,
1002+
microseconds=offset_us,
1003+
)
1004+
tz = timezone(offset, name="utcoffset")
1005+
1006+
result = cls(
1007+
hh,
1008+
mm,
1009+
ss,
1010+
us,
1011+
tz,
1012+
)
1013+
return result
1014+
1015+
# pylint: enable=too-many-locals
1016+
9101017
# Instance methods
9111018
def isoformat(self, timespec="auto"):
9121019
"""Return a string representing the time in ISO 8601 format, one of:
@@ -1163,6 +1270,11 @@ def tzinfo(self):
11631270
"""
11641271
return self._tzinfo
11651272

1273+
@property
1274+
def fold(self):
1275+
"""Fold."""
1276+
return self._fold
1277+
11661278
# Class methods
11671279

11681280
# pylint: disable=protected-access
@@ -1206,6 +1318,29 @@ def _fromtimestamp(cls, t, utc, tz):
12061318
def fromtimestamp(cls, timestamp, tz=None):
12071319
return cls._fromtimestamp(timestamp, tz is not None, tz)
12081320

1321+
@classmethod
1322+
def fromisoformat(cls, date_string):
1323+
"""Return a datetime object constructed from an ISO date format.
1324+
Valid format is ``YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]``
1325+
1326+
"""
1327+
original_string = date_string
1328+
1329+
time_string = None
1330+
try:
1331+
if len(date_string) > 10:
1332+
time_string = date_string[11:]
1333+
date_string = date_string[:10]
1334+
dateval = date.fromisoformat(date_string)
1335+
timeval = time.fromisoformat(time_string)
1336+
else:
1337+
dateval = date.fromisoformat(date_string)
1338+
timeval = time()
1339+
except ValueError as error:
1340+
raise ValueError(_INVALID_ISO_ERROR.format(original_string)) from error
1341+
1342+
return cls.combine(dateval, timeval)
1343+
12091344
@classmethod
12101345
def now(cls, timezone=None):
12111346
"""Return the current local date and time."""

examples/datetime_simpletest.py

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# All rights reserved.
77
# SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved.
88
# SPDX-FileCopyrightText: 2021 Brent Rubell for Adafruit Industries
9+
# SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries
910
# SPDX-License-Identifier: Python-2.0
1011

1112
# Example of working with a `datetime` object
@@ -28,3 +29,8 @@
2829
print(it)
2930

3031
print("Today is: ", dt.ctime())
32+
33+
iso_date_string = "2020-04-05T05:04:45.752301"
34+
print("Creating new datetime from ISO Date:", iso_date_string)
35+
isodate = datetime.fromisoformat(iso_date_string)
36+
print("Formatted back out as ISO Date: ", isodate.isoformat())

tests/test_date.py

+13
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,19 @@ def test_fromtimestamp(self):
8888
self.assertEqual(d.month, month)
8989
self.assertEqual(d.day, day)
9090

91+
def test_fromisoformat(self):
92+
# Try an arbitrary fixed value.
93+
iso_date_string = "1999-09-19"
94+
d = cpy_date.fromisoformat(iso_date_string)
95+
self.assertEqual(d.year, 1999)
96+
self.assertEqual(d.month, 9)
97+
self.assertEqual(d.day, 19)
98+
99+
def test_fromisoformat_bad_formats(self):
100+
# Try an arbitrary fixed value.
101+
self.assertRaises(ValueError, cpy_date.fromisoformat, "99-09-19")
102+
self.assertRaises(ValueError, cpy_date.fromisoformat, "1999-13-19")
103+
91104
# TODO: Test this when timedelta is added in
92105
@unittest.skip("Skip for CircuitPython - timedelta() not yet implemented.")
93106
def test_today(self):

tests/test_datetime.py

+2-14
Original file line numberDiff line numberDiff line change
@@ -1033,8 +1033,6 @@ def __new__(cls, *args, **kwargs):
10331033
self.assertIsInstance(dt, DateTimeSubclass)
10341034
self.assertEqual(dt.extra, 7)
10351035

1036-
# TODO
1037-
@unittest.skip("timezone not implemented")
10381036
def test_fromisoformat_datetime(self):
10391037
# Test that isoformat() is reversible
10401038
base_dates = [(1, 1, 1), (1900, 1, 1), (2004, 11, 12), (2017, 5, 30)]
@@ -1097,8 +1095,6 @@ def test_fromisoformat_timezone(self):
10971095
dt_rt = self.theclass.fromisoformat(dtstr)
10981096
assert dt == dt_rt, dt_rt
10991097

1100-
# TODO
1101-
@unittest.skip("fromisoformat not implemented")
11021098
def test_fromisoformat_separators(self):
11031099
separators = [
11041100
" ",
@@ -1120,8 +1116,6 @@ def test_fromisoformat_separators(self):
11201116
dt_rt = self.theclass.fromisoformat(dtstr)
11211117
self.assertEqual(dt, dt_rt)
11221118

1123-
# TODO
1124-
@unittest.skip("fromisoformat not implemented")
11251119
def test_fromisoformat_ambiguous(self):
11261120
# Test strings like 2018-01-31+12:15 (where +12:15 is not a time zone)
11271121
separators = ["+", "-"]
@@ -1134,7 +1128,7 @@ def test_fromisoformat_ambiguous(self):
11341128
self.assertEqual(dt, dt_rt)
11351129

11361130
# TODO
1137-
@unittest.skip("fromisoformat not implemented")
1131+
@unittest.skip("_format_time not fully implemented")
11381132
def test_fromisoformat_timespecs(self):
11391133
datetime_bases = [(2009, 12, 4, 8, 17, 45, 123456), (2009, 12, 4, 8, 17, 45, 0)]
11401134

@@ -1161,8 +1155,6 @@ def test_fromisoformat_timespecs(self):
11611155
dt_rt = self.theclass.fromisoformat(dtstr)
11621156
self.assertEqual(dt, dt_rt)
11631157

1164-
# TODO
1165-
@unittest.skip("fromisoformat not implemented")
11661158
def test_fromisoformat_fails_datetime(self):
11671159
# Test that fromisoformat() fails on invalid values
11681160
bad_strs = [
@@ -1201,14 +1193,12 @@ def test_fromisoformat_fails_datetime(self):
12011193
with self.assertRaises(ValueError):
12021194
self.theclass.fromisoformat(bad_str)
12031195

1204-
# TODO
1205-
@unittest.skip("fromisoformat not implemented")
12061196
def test_fromisoformat_fails_surrogate(self):
12071197
# Test that when fromisoformat() fails with a surrogate character as
12081198
# the separator, the error message contains the original string
12091199
dtstr = "2018-01-03\ud80001:0113"
12101200

1211-
with self.assertRaisesRegex(ValueError, re.escape(repr(dtstr))):
1201+
with self.assertRaisesRegex(ValueError, repr(dtstr)):
12121202
self.theclass.fromisoformat(dtstr)
12131203

12141204
# TODO
@@ -1219,8 +1209,6 @@ def test_fromisoformat_utc(self):
12191209

12201210
self.assertIs(dt.tzinfo, timezone.utc)
12211211

1222-
# TODO
1223-
@unittest.skip("fromisoformat not implemented")
12241212
def test_fromisoformat_subclass(self):
12251213
class DateTimeSubclass(self.theclass):
12261214
pass

0 commit comments

Comments
 (0)