Skip to content

Clamp rounded fractional seconds to the field width - #1292

Open
vineethsaivs wants to merge 1 commit into
python-babel:masterfrom
vineethsaivs:fix/frac-seconds-field-overflow
Open

Clamp rounded fractional seconds to the field width#1292
vineethsaivs wants to merge 1 commit into
python-babel:masterfrom
vineethsaivs:fix/frac-seconds-field-overflow

Conversation

@vineethsaivs

Copy link
Copy Markdown
Contributor

DateTimeFormat.format_frac_seconds can render a fractional-seconds field that is wider than its declared number of digits.

The method rounds the microseconds to the requested precision:

value = self.value.microsecond / 1000000
return self.format(round(value, num) * 10**num, num)

round(value, num) can carry up to a whole second (for example 0.999 rounds to 1.0). Then 1.0 * 10**num == 10**num, which has num + 1 digits, and self.format ('%0*d' % (num, value)) prints all of them. Babel intentionally does not carry into the seconds field, so the fractional field simply overflows its width.

from datetime import time
from babel.dates import DateTimeFormat

DateTimeFormat(time(1, 2, 3, 990000), locale='en_US')['S']    # '10'  (expected '9')
DateTimeFormat(time(1, 2, 3, 999500), locale='en_US')['SS']   # '100' (expected '99')

A single S digit must be 0 to 9, but it can render 10.

Fix: clamp the rounded value to the largest in-field value so it stays within the declared width:

frac = min(int(round(value, num) * 10**num), 10**num - 1)
return self.format(frac, num)

This only changes the overflow case (the value is clamped to all nines, the closest representation without carrying into seconds). Every existing test_fractional_seconds expectation, including the deliberately rounded ['SSSS'] == '0346', is unchanged. Added a regression test covering the S, SS, and SSSS overflow inputs.

DateTimeFormat rounds the fraction to the requested number of digits, so a
microsecond value close enough to a whole second rounds up to a value one digit
too wide: time(1, 2, 3, 990000) formatted as 'S' gave '10' rather than '9', and
999999 as 'SSSS' gave '10000'. Clamp the rounded value to the largest the field
can hold.

Rebased onto current master, where the DateTimeFormat tests were moved out of
tests/test_dates.py into tests/test_date_time_format.py and flattened from
methods into module-level functions; the regression test follows them.

Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
@vineethsaivs
vineethsaivs force-pushed the fix/frac-seconds-field-overflow branch from d4e3c2b to dbd44a5 Compare August 27, 2026 17:15
@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.37%. Comparing base (6ba6701) to head (dbd44a5).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1292   +/-   ##
=======================================
  Coverage   93.37%   93.37%           
=======================================
  Files          27       27           
  Lines        4919     4920    +1     
=======================================
+ Hits         4593     4594    +1     
  Misses        326      326           
Flag Coverage Δ
macos-14-3.10 92.45% <100.00%> (+<0.01%) ⬆️
macos-14-3.11 92.39% <100.00%> (+<0.01%) ⬆️
macos-14-3.12 92.60% <100.00%> (+<0.01%) ⬆️
macos-14-3.13 92.60% <100.00%> (+<0.01%) ⬆️
macos-14-3.14 92.58% <100.00%> (+<0.01%) ⬆️
macos-14-3.15 92.58% <100.00%> (+<0.01%) ⬆️
macos-14-3.8 92.33% <100.00%> (+<0.01%) ⬆️
macos-14-3.9 92.39% <100.00%> (+<0.01%) ⬆️
macos-14-pypy3.10 92.45% <100.00%> (+<0.01%) ⬆️
ubuntu-24.04-3.10 92.47% <100.00%> (+<0.01%) ⬆️
ubuntu-24.04-3.11 92.41% <100.00%> (+<0.01%) ⬆️
ubuntu-24.04-3.12 92.62% <100.00%> (+<0.01%) ⬆️
ubuntu-24.04-3.13 92.62% <100.00%> (+<0.01%) ⬆️
ubuntu-24.04-3.14 92.60% <100.00%> (+<0.01%) ⬆️
ubuntu-24.04-3.15 92.60% <100.00%> (+<0.01%) ⬆️
ubuntu-24.04-3.8 92.35% <100.00%> (+<0.01%) ⬆️
ubuntu-24.04-3.9 92.41% <100.00%> (+<0.01%) ⬆️
ubuntu-24.04-pypy3.10 92.47% <100.00%> (+<0.01%) ⬆️
windows-2022-3.10 92.47% <100.00%> (+<0.01%) ⬆️
windows-2022-3.11 92.41% <100.00%> (+<0.01%) ⬆️
windows-2022-3.12 92.61% <100.00%> (+<0.01%) ⬆️
windows-2022-3.13 92.61% <100.00%> (+<0.01%) ⬆️
windows-2022-3.14 92.59% <100.00%> (+<0.01%) ⬆️
windows-2022-3.15 92.59% <100.00%> (+<0.01%) ⬆️
windows-2022-3.8 92.44% <100.00%> (+<0.01%) ⬆️
windows-2022-3.9 92.40% <100.00%> (+<0.01%) ⬆️
windows-2022-pypy3.10 92.47% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq

codspeed-hq Bot commented Aug 27, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 88 untouched benchmarks


Comparing vineethsaivs:fix/frac-seconds-field-overflow (dbd44a5) with master (6ba6701)

Open in CodSpeed

@jun66j5

jun66j5 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

According to https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-second, it says Fractional Second (numeric). Truncates, like other numeric time fields, but in this case to the number of digits specified by the field length. So, the format_frac_seconds should truncate the fractional second, not round.

The document of Date and Time in Babel, https://babel.pocoo.org/en/latest/dates.html#time-fields, says Fractional second, rounds to the count of letters, and the document should be fixed too.

@vineethsaivs

Copy link
Copy Markdown
Contributor Author

You are right, and that is the better fix: TR35 says the field truncates, and truncation makes the overflow impossible rather than clamping it after the fact. round(0.999, 1) * 10 can reach 10; 0.999 truncated to one digit is 9 and never can.

Two things worth deciding before I switch this PR over, since truncating is a wider change than the overflow repair.

It changes three expectations that test_fractional_seconds currently pins:

input field today truncated
time(8, 3, 1, 799) SSSS 0008 0007
time(8, 3, 1, 34567) SSSS 0346 0345
time(8, 3, 1, 799) SSSSS 00080 00079

The other two cases in that test, and test_fractional_seconds_zero, are unchanged. The docstring on format_frac_seconds says "Rounds the time's microseconds", so it would move too.

The arithmetic is also better done on the integer microseconds than on the float:

return self.format(self.value.microsecond // 10 ** (6 - num), num)   # num <= 6

int(self.value.microsecond / 1000000 * 10**num) is exact on every case I checked, but it is a float round-trip that does not need to happen, and // on the microsecond field cannot land a digit low.

Happy to push that as this PR, updating the three expectations and the docstring and dropping the clamp, if you would rather have the spec-correct behaviour than the narrower overflow fix. Say the word and I will rework it.

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.

2 participants