Skip to content

parser: round microsecond half-up when truncated value has trailing non-zero digits#1323

Open
HrachShah wants to merge 1 commit into
arrow-py:masterfrom
HrachShah:fix/microsecond-round-half-up
Open

parser: round microsecond half-up when truncated value has trailing non-zero digits#1323
HrachShah wants to merge 1 commit into
arrow-py:masterfrom
HrachShah:fix/microsecond-round-half-up

Conversation

@HrachShah

Copy link
Copy Markdown

Bug

arrow.parser.DateTimeParser rounds the 'S' sub-second token to 6 digits using only the 7th digit. When the 7th digit is 5, it applies round-half-to-even (banker's rounding) based purely on the parity of the 6th digit, ignoring everything that follows.

A value like .789124501 therefore rounds to 789124 (banker's: 4 is even), but the true value is strictly above the half-way point between 789124 and 789125 and should round to 789125. The same is true for .789124511, .789123550001, and any other input where the 7th digit is 5 and a non-zero digit trails it.

Repro

>>> import arrow
>>> arrow.get(2013-02-03T04:05:06.789124501).format(S)
'789124'   # wrong, should be '789125'
>>> arrow.get(2013-02-03T04:05:06.789123500).format(S)
'789124'   # correct (banker's: 3 is odd, round up; trailing zeros make it exactly half-way)

Fix

Restrict the banker's-rounding branch to the case where the 7th digit is 5 and every following digit is zero. If any non-zero digit trails the 5, the truncated 6-digit value sits strictly above the midpoint, so round up regardless of parity:

if seventh_digit > 5:
    rounding = 1
elif seventh_digit < 5:
    rounding = 0
else:  # exactly 5
    if any(ch != "0" for ch in value[7:]):
        rounding = 1   # strictly above the midpoint
    else:
        rounding = int(value[5]) % 2   # banker's rounding

Tests

  • Updated test_parse_subsecond_rounding (the "round half-down" / "round half-up" cases were pinning the bug).
  • Updated test_gnu_date (date -Ins output that has 9 sub-second digits is now rounded correctly).
  • Added test_YYYY_MM_DDTHH_mm_ss_S_round_half_up covering all four corners: banker's with even 6th / trailing zeros, banker's with odd 6th / trailing zeros, strict half-up with non-zero trailing, and strict above/below the half-way point.

Full test suite: 1905 passed, 1 skipped (up from 1904 — the new test). The 2 pre-existing errors in tests/utils.py (assertion helpers being collected as test items) are unrelated.

…on-zero digits

The 'S' token rounds a sub-second value to the nearest 6-digit
microsecond, but the half-way case was unconditionally applying
round-half-to-even (banker's rounding) based purely on the 6th digit.
That ignored anything after the 7th digit, so a value like
.7891235001 lost the trailing '001' and was rounded down to .789123
even though the true value sits above the half-way point and should
round up to .789124.

Restrict the banker's-rounding branch to the case where the 7th digit
is exactly 5 and all following digits are zero. When non-zero digits
follow, the truncated 6-digit value is strictly greater than the
midpoint between two adjacent microseconds, so round up regardless of
the parity of the 6th digit.

Updated test_parse_subsecond_rounding and test_gnu_date which had
pinned the old behaviour, and added test_YYYY_MM_DDTHH_mm_ss_S_round_half_up
covering the banker's / strict half-up / strictly-above / strictly-below
corners.
@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (2224255) to head (0198620).

Additional details and impacted files
@@            Coverage Diff            @@
##            master     #1323   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           10        10           
  Lines         2315      2317    +2     
  Branches       358       359    +1     
=========================================
+ Hits          2315      2317    +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant