Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions arrow/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,12 +662,21 @@ def _parse_token(

# floating-point (IEEE-754) defaults to half-to-even rounding
seventh_digit = int(value[6])
if seventh_digit == 5:
rounding = int(value[5]) % 2
elif seventh_digit > 5:
if seventh_digit > 5:
# Strictly above the half-way point, always round up.
rounding = 1
else:
elif seventh_digit < 5:
# Strictly below the half-way point, always round down.
rounding = 0
else:
# Exactly at the half-way point: only use round-half-to-even
# (banker's rounding) when the trailing digits are all zero.
# If anything non-zero follows the 5, the truncated value is
# strictly above the midpoint and we must round up.
if any(ch != "0" for ch in value[7:]):
rounding = 1
else:
rounding = int(value[5]) % 2

parts["microsecond"] = int(value[:6]) + rounding

Expand Down
37 changes: 34 additions & 3 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,16 +393,29 @@ def test_parse_subsecond_rounding(self):
assert self.parser.parse(string, datetime_format) == self.expected
assert self.parser.parse_iso(string) == self.expected

# round half-up
# round up (non-zero trailing past 5)
string = "2013-01-01 12:30:45.987653521"
assert self.parser.parse(string, datetime_format) == self.expected
assert self.parser.parse_iso(string) == self.expected

# round half-down
# round up (6th=4, non-zero trailing past 5) - 6th=4, 7th=5, trailing "210" non-zero,
# so the value is strictly above the half-way point and must round up
string = "2013-01-01 12:30:45.9876545210"
expected_round_up = datetime(2013, 1, 1, 12, 30, 45, 987655)
assert self.parser.parse(string, datetime_format) == expected_round_up
assert self.parser.parse_iso(string) == expected_round_up

# round half-to-even (6th=3, exact .5) - 6th is odd, so round up to even
string = "2013-01-01 12:30:45.9876535"
assert self.parser.parse(string, datetime_format) == self.expected
assert self.parser.parse_iso(string) == self.expected

# round half-to-even (6th=4, exact .5) - 6th is even, so round down to even
string = "2013-01-01 12:30:45.9876545"
expected_half_to_even = datetime(2013, 1, 1, 12, 30, 45, 987654)
assert self.parser.parse(string, datetime_format) == expected_half_to_even
assert self.parser.parse_iso(string) == expected_half_to_even

# overflow (zero out the subseconds and increment the seconds)
# regression tests for issue #636
def test_parse_subsecond_rounding_overflow(self):
Expand Down Expand Up @@ -1058,6 +1071,22 @@ def test_YYYY_MM_DDTHH_mm_ss_S(self):
2013, 2, 3, 4, 5, 6, 789124
)

def test_YYYY_MM_DDTHH_mm_ss_S_round_half_up(self):
# When the 7th digit is 5, the truncated value sits at the half-way
# point of the 6th digit. The value is strictly greater than the
# midpoint iff at least one trailing digit is non-zero, in which
# case we must round up rather than apply banker's rounding to
# the 6th digit.
# 6th digit even, 7th == 5, nothing trailing -> banker's rounds down
assert self.parser.parse_iso("2013-02-03T04:05:06.789124500") == datetime(
2013, 2, 3, 4, 5, 6, 789124
)
# 6th digit even, 7th == 5, non-zero digit after -> strictly above
# midpoint, must round up (was rounding down before the fix)
assert self.parser.parse_iso("2013-02-03T04:05:06.789124501") == datetime(
2013, 2, 3, 4, 5, 6, 789125
)

def test_YYYY_MM_DDTHH_mm_ss_SZ(self):
assert self.parser.parse_iso("2013-02-03T04:05:06.7+01:00") == datetime(
2013, 2, 3, 4, 5, 6, 700000, tzinfo=tz.tzoffset(None, 3600)
Expand Down Expand Up @@ -1144,8 +1173,10 @@ def test_parse_subsecond(self):
def test_gnu_date(self):
"""Regression tests for parsing output from GNU date."""
# date -Ins
# 7th digit is 5, trailing "57" is non-zero, so the value is strictly
# above the half-way point and must round up from 895636 to 895637
assert self.parser.parse_iso("2016-11-16T09:46:30,895636557-0800") == datetime(
2016, 11, 16, 9, 46, 30, 895636, tzinfo=tz.tzoffset(None, -3600 * 8)
2016, 11, 16, 9, 46, 30, 895637, tzinfo=tz.tzoffset(None, -3600 * 8)
)

# date --rfc-3339=ns
Expand Down
Loading