Skip to content

Added fromisoformat() to date and datetime #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 19, 2021
135 changes: 135 additions & 0 deletions adafruit_datetime.py
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@
# pylint: disable=too-many-lines
import time as _time
import math as _math
import re as _re
from micropython import const

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

_INVALID_ISO_ERROR = "Invalid isoformat string: '{}'"

# Utility functions - universal
def _cmp(obj_x, obj_y):
return 0 if obj_x == obj_y else 1 if obj_x > obj_y else -1
@@ -657,6 +660,20 @@ def fromordinal(cls, ordinal):
y, m, d = _ord2ymd(ordinal)
return cls(y, m, d)

@classmethod
def fromisoformat(cls, date_string):
"""Return a date object constructed from an ISO date format.
Valid format is ``YYYY-MM-DD``
"""
match = _re.match(
r"([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$", date_string
)
if match:
y, m, d = int(match.group(1)), int(match.group(2)), int(match.group(3))
return cls(y, m, d)
raise ValueError(_INVALID_ISO_ERROR.format(date_string))

@classmethod
def today(cls):
"""Return the current local date."""
@@ -907,6 +924,96 @@ def tzinfo(self):
"""
return self._tzinfo

@staticmethod
def _parse_iso_string(string_to_parse, segments):
results = []

remaining_string = string_to_parse
for regex in segments:
match = _re.match(regex, remaining_string)
if match:
for grp in range(regex.count("(")):
results.append(int(match.group(grp + 1)))
remaining_string = remaining_string[len(match.group(0)) :]
elif remaining_string: # Only raise an error if we're not done yet
raise ValueError()
if remaining_string:
raise ValueError()
return results

# pylint: disable=too-many-locals
@classmethod
def fromisoformat(cls, time_string):
"""Return a time object constructed from an ISO date format.
Valid format is ``HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]``
"""
# Store the original string in an error message
original_string = time_string
match = _re.match(r"(.*)[\-\+]", time_string)
offset_string = None
if match:
offset_string = time_string[len(match.group(1)) :]
time_string = match.group(1)

time_segments = (
r"([0-9][0-9])",
r":([0-9][0-9])",
r":([0-9][0-9])",
r"\.([0-9][0-9][0-9])",
r"([0-9][0-9][0-9])",
)
offset_segments = (
r"([\-\+][0-9][0-9]):([0-9][0-9])",
r":([0-9][0-9])",
r"\.([0-9][0-9][0-9][0-9][0-9][0-9])",
)

try:
results = cls._parse_iso_string(time_string, time_segments)
if len(results) < 1:
raise ValueError(_INVALID_ISO_ERROR.format(original_string))
if len(results) < len(time_segments):
results += [None] * (len(time_segments) - len(results))
if offset_string:
results += cls._parse_iso_string(offset_string, offset_segments)
except ValueError as error:
raise ValueError(_INVALID_ISO_ERROR.format(original_string)) from error

hh = results[0]
mm = results[1] if len(results) >= 2 and results[1] is not None else 0
ss = results[2] if len(results) >= 3 and results[2] is not None else 0
us = 0
if len(results) >= 4 and results[3] is not None:
us += results[3] * 1000
if len(results) >= 5 and results[4] is not None:
us += results[4]
tz = None
if len(results) >= 7:
offset_hh = results[5]
multiplier = -1 if offset_hh < 0 else 1
offset_mm = results[6] * multiplier
offset_ss = (results[7] if len(results) >= 8 else 0) * multiplier
offset_us = (results[8] if len(results) >= 9 else 0) * multiplier
offset = timedelta(
hours=offset_hh,
minutes=offset_mm,
seconds=offset_ss,
microseconds=offset_us,
)
tz = timezone(offset, name="utcoffset")

result = cls(
hh,
mm,
ss,
us,
tz,
)
return result

# pylint: enable=too-many-locals

# Instance methods
def isoformat(self, timespec="auto"):
"""Return a string representing the time in ISO 8601 format, one of:
@@ -1163,6 +1270,11 @@ def tzinfo(self):
"""
return self._tzinfo

@property
def fold(self):
"""Fold."""
return self._fold

# Class methods

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

@classmethod
def fromisoformat(cls, date_string):
"""Return a datetime object constructed from an ISO date format.
Valid format is ``YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]``
"""
original_string = date_string

time_string = None
try:
if len(date_string) > 10:
time_string = date_string[11:]
date_string = date_string[:10]
dateval = date.fromisoformat(date_string)
timeval = time.fromisoformat(time_string)
else:
dateval = date.fromisoformat(date_string)
timeval = time()
except ValueError as error:
raise ValueError(_INVALID_ISO_ERROR.format(original_string)) from error

return cls.combine(dateval, timeval)

@classmethod
def now(cls, timezone=None):
"""Return the current local date and time."""
6 changes: 6 additions & 0 deletions examples/datetime_simpletest.py
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
# All rights reserved.
# SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved.
# SPDX-FileCopyrightText: 2021 Brent Rubell for Adafruit Industries
# SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries
# SPDX-License-Identifier: Python-2.0

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

print("Today is: ", dt.ctime())

iso_date_string = "2020-04-05T05:04:45.752301"
print("Creating new datetime from ISO Date:", iso_date_string)
isodate = datetime.fromisoformat(iso_date_string)
print("Formatted back out as ISO Date: ", isodate.isoformat())
13 changes: 13 additions & 0 deletions tests/test_date.py
Original file line number Diff line number Diff line change
@@ -88,6 +88,19 @@ def test_fromtimestamp(self):
self.assertEqual(d.month, month)
self.assertEqual(d.day, day)

def test_fromisoformat(self):
# Try an arbitrary fixed value.
iso_date_string = "1999-09-19"
d = cpy_date.fromisoformat(iso_date_string)
self.assertEqual(d.year, 1999)
self.assertEqual(d.month, 9)
self.assertEqual(d.day, 19)

def test_fromisoformat_bad_formats(self):
# Try an arbitrary fixed value.
self.assertRaises(ValueError, cpy_date.fromisoformat, "99-09-19")
self.assertRaises(ValueError, cpy_date.fromisoformat, "1999-13-19")

# TODO: Test this when timedelta is added in
@unittest.skip("Skip for CircuitPython - timedelta() not yet implemented.")
def test_today(self):
16 changes: 2 additions & 14 deletions tests/test_datetime.py
Original file line number Diff line number Diff line change
@@ -1033,8 +1033,6 @@ def __new__(cls, *args, **kwargs):
self.assertIsInstance(dt, DateTimeSubclass)
self.assertEqual(dt.extra, 7)

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

# TODO
@unittest.skip("fromisoformat not implemented")
def test_fromisoformat_separators(self):
separators = [
" ",
@@ -1120,8 +1116,6 @@ def test_fromisoformat_separators(self):
dt_rt = self.theclass.fromisoformat(dtstr)
self.assertEqual(dt, dt_rt)

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

# TODO
@unittest.skip("fromisoformat not implemented")
@unittest.skip("_format_time not fully implemented")
def test_fromisoformat_timespecs(self):
datetime_bases = [(2009, 12, 4, 8, 17, 45, 123456), (2009, 12, 4, 8, 17, 45, 0)]

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

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

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

with self.assertRaisesRegex(ValueError, re.escape(repr(dtstr))):
with self.assertRaisesRegex(ValueError, repr(dtstr)):
self.theclass.fromisoformat(dtstr)

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

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

# TODO
@unittest.skip("fromisoformat not implemented")
def test_fromisoformat_subclass(self):
class DateTimeSubclass(self.theclass):
pass