diff --git a/arrow/parser.py b/arrow/parser.py index fc3774b09..05b255212 100644 --- a/arrow/parser.py +++ b/arrow/parser.py @@ -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 diff --git a/tests/test_parser.py b/tests/test_parser.py index 7038d880f..8236c1c98 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -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): @@ -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) @@ -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