Skip to content

Commit

Permalink
Types.format_datetime() - fix fractional timezones to ISO 8601 semantics
Browse files Browse the repository at this point in the history
  • Loading branch information
csingley committed Feb 2, 2021
1 parent a170a4b commit 8bee7fe
Show file tree
Hide file tree
Showing 2 changed files with 21 additions and 13 deletions.
30 changes: 19 additions & 11 deletions ofxtools/Types.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ def __init__(self, *args, **kwargs):
# Rewrite ``self.scale`` from # of digits to a ``decimal.Decimal`` instance
# That can be directly fed into ``decimal.Decimal.quantize()``
if self.scale is not None:
self.scale = decimal.Decimal("0.{}1".format("0" * (self.scale - 1)))
self.scale = decimal.Decimal(f"0.{'0' * (self.scale - 1)}1")

@singledispatchmethod
def convert(self, value):
Expand Down Expand Up @@ -527,17 +527,25 @@ def format_datetime(format: str, value: datetime.datetime) -> str:
raise ValueError(f"{value} is not timezone-aware")

# Round to nearest millisecond by adding 500 us and truncating.
value += datetime.timedelta(microseconds=500)
ms = value.microsecond // 1000
# N.B. the value being increased by half a millisecond is
# carried forward to this function's return value, to ensure that
# the rounded time has the seconds dial bumped if necessary.
value_bumped = value + datetime.timedelta(microseconds=500)
ms = value_bumped.microsecond // 1000

# OFX takes the UTC offset in hours, preferably as a whole number.
hours = utcoffset.total_seconds() / 3600
if hours == int(hours):
tz = "{:+d}".format(int(hours))
else:
tz = "{:+.2f}".format(hours)
# OFX takes the UTC offset formatted as +h[.mm].
offset_mins = utcoffset // datetime.timedelta(minutes=1)
hours, mins = divmod(offset_mins, 60)
tz = f"{hours:+d}"
if mins != 0:
tz += f".{abs(mins):02d}"

return "{}.{:03d}[{}:{}]".format(value.strftime(format), ms, tz, value.tzname())
# Note that tzname() is permitted to return None.
tzname = value.tzname()
if tzname is not None:
tz += ":" + tzname

return f"{value_bumped.strftime(format)}.{ms:03d}[{tz}]"


class DateTime(Element):
Expand Down Expand Up @@ -779,7 +787,7 @@ def _convert_none(self, value: None):

# This doesn't get used
# def __repr__(self):
# return "<{}>".format(self.__type__.__name__)
# return f"<{self.__type__.__name__}>"


class ListAggregate(SubAggregate):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ def test_unconvert(self):
self.assertEqual(t.unconvert(check), "20070101000000.000[-6:CST]")

check = datetime.datetime(2007, 1, 1, tzinfo=IST)
self.assertEqual(t.unconvert(check), "20070101000000.000[+5.50:IST]")
self.assertEqual(t.unconvert(check), "20070101000000.000[+5.30:IST]")

def test_unconvert_round_microseconds(self):
# Round up microseconds above 999499; increment seconds (Issue #80)
Expand Down Expand Up @@ -629,7 +629,7 @@ def test_unconvert(self):
self.assertEqual(t.unconvert(check), "010203.000[-6:CST]")

check = datetime.time(1, 2, 3, tzinfo=IST)
self.assertEqual(t.unconvert(check), "010203.000[+5.50:IST]")
self.assertEqual(t.unconvert(check), "010203.000[+5.30:IST]")

def test_unconvert_round_microseconds(self):
# Round up microseconds above 999499; increment seconds (Issue #80)
Expand Down

0 comments on commit 8bee7fe

Please sign in to comment.