From 4a0d92d55fa34a348d5bbb60c4516ebde90e3282 Mon Sep 17 00:00:00 2001 From: Vineeth Sai Date: Tue, 3 Nov 2026 13:04:54 -0800 Subject: [PATCH] Truncate fractional seconds per UTS 35 format_frac_seconds rounded microseconds, which could carry to a whole second and overflow the field (S gave 10). UTS #35 says to truncate to the field length, so it now truncates with integer math. Docs and expectations updated. --- babel/dates.py | 9 +++++---- docs/dates.rst | 2 +- tests/test_date_time_format.py | 18 +++++++++++++++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/babel/dates.py b/babel/dates.py index 7f3371248..e6393c4c8 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -1636,11 +1636,12 @@ def format_period(self, char: str, num: int) -> str: def format_frac_seconds(self, num: int) -> str: """ Return fractional seconds. - Rounds the time's microseconds to the precision given by the number \ - of digits passed in. + Truncates the time's microseconds to the number of digits passed in, + as specified by UTS #35. """ - value = self.value.microsecond / 1000000 - return self.format(round(value, num) * 10**num, num) + micro = self.value.microsecond + frac = micro // 10**(6 - num) if num <= 6 else micro * 10**(num - 6) + return self.format(frac, num) def format_milliseconds_in_day(self, num): msecs = ( diff --git a/docs/dates.rst b/docs/dates.rst index 18b4f1f59..d1a735523 100644 --- a/docs/dates.rst +++ b/docs/dates.rst @@ -206,7 +206,7 @@ Time Fields +----------+--------+--------------------------------------------------------+ | Second | ``s`` | Use one or two for zero places padding. | | +--------+--------------------------------------------------------+ - | | ``S`` | Fractional second, rounds to the count of letters. | + | | ``S`` | Fractional second, truncates to the count of letters. | | +--------+--------------------------------------------------------+ | | ``A`` | Milliseconds in day. | +----------+--------+--------------------------------------------------------+ diff --git a/tests/test_date_time_format.py b/tests/test_date_time_format.py index 5ca620879..47e50c0c5 100644 --- a/tests/test_date_time_format.py +++ b/tests/test_date_time_format.py @@ -166,13 +166,13 @@ def test_fractional_seconds(): t = time(8, 3, 9, 799) assert DateTimeFormat(t, locale='en_US')['S'] == '0' t = time(8, 3, 1, 799) - assert DateTimeFormat(t, locale='en_US')['SSSS'] == '0008' + assert DateTimeFormat(t, locale='en_US')['SSSS'] == '0007' t = time(8, 3, 1, 34567) - assert DateTimeFormat(t, locale='en_US')['SSSS'] == '0346' + assert DateTimeFormat(t, locale='en_US')['SSSS'] == '0345' t = time(8, 3, 1, 345678) assert DateTimeFormat(t, locale='en_US')['SSSSSS'] == '345678' t = time(8, 3, 1, 799) - assert DateTimeFormat(t, locale='en_US')['SSSSS'] == '00080' + assert DateTimeFormat(t, locale='en_US')['SSSSS'] == '00079' def test_fractional_seconds_zero(): @@ -180,6 +180,18 @@ def test_fractional_seconds_zero(): assert DateTimeFormat(t, locale='en_US')['SSSS'] == '0000' +def test_fractional_seconds_truncation_does_not_carry(): + t = time(1, 2, 3, 990000) + assert DateTimeFormat(t, locale='en_US')['S'] == '9' + t = time(1, 2, 3, 999500) + assert DateTimeFormat(t, locale='en_US')['SS'] == '99' + t = time(1, 2, 3, 999999) + assert DateTimeFormat(t, locale='en_US')['S'] == '9' + assert DateTimeFormat(t, locale='en_US')['SS'] == '99' + assert DateTimeFormat(t, locale='en_US')['SSSS'] == '9999' + assert DateTimeFormat(t, locale='en_US')['SSSSSSSS'] == '99999900' + + def test_milliseconds_in_day(): t = time(15, 30, 12, 345000) assert DateTimeFormat(t, locale='en_US')['AAAA'] == '55812345'