From ebc208b5c0896870f42f5d578b25c926d986ca2a Mon Sep 17 00:00:00 2001 From: simpleqt <89645338+simpleqt@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:17:54 +0800 Subject: [PATCH 1/2] dates: extract era and day-period fields for interval formatting format_interval() raises NotImplementedError for any skeleton whose CLDR interval pattern defines the era ('G') or day period ('B') field, because DateTimeFormat.extract() only understood y M d H h m a. The affected skeletons (Gy, GyM, GyMd, GyMEd, GyMMM, GyMMMd, GyMMMEd, GyMMMM, GyMMMMd, GyMMMMEd, GyMMMEEEEd, Bh, Bhm) are defined in interval_formats for every locale, so format_interval(datetime(2020, 1, 1), datetime(2021, 1, 1), 'Gy', locale='en') failed with "Not implemented: extracting 'G' ...". Extract those two fields, mirroring how they are rendered: the era index from format_era() and the day period identifier from format_period(). Sweeping all locales, interval skeletons and instants (230,698 cases) shows 20,220 NotImplementedError crashes on the base, 0 after the fix, with no case that previously produced output changing its output. --- babel/dates.py | 8 +++++++- tests/test_date_intervals.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/babel/dates.py b/babel/dates.py index 7b626aa11..d0372d143 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -1480,7 +1480,7 @@ def __getitem__(self, name: str) -> str: else: raise KeyError(f"Unsupported date/time field {char!r}") - def extract(self, char: str) -> int: + def extract(self, char: str) -> int | str: char = str(char)[0] if char == 'y': return self.value.year @@ -1496,6 +1496,12 @@ def extract(self, char: str) -> int: return self.value.minute elif char == 'a': return int(self.value.hour >= 12) # 0 for am, 1 for pm + elif char == 'G': + return int(self.value.year >= 0) # 0 for BCE, 1 for CE + elif char == 'B': + # The day period is rendered from the same identifier, + # so the extracted value matches what `format_period` renders. + return get_period_id(self.value, locale=self.locale) else: raise NotImplementedError( f"Not implemented: extracting {char!r} from {self.value!r}", diff --git a/tests/test_date_intervals.py b/tests/test_date_intervals.py index 33a85d38a..ee52b75c1 100644 --- a/tests/test_date_intervals.py +++ b/tests/test_date_intervals.py @@ -61,3 +61,32 @@ def test_issue_825(): locale='pt', ) assert formatted == '1 h' + + +@pytest.mark.parametrize( + ("start", "end", "skeleton", "expected"), + [ + # Era ('G') interval formats used to raise NotImplementedError when + # computing the greatest difference between the two instants. + # A year difference must select the 'y' pattern of the "Gy" interval format. + (TEST_DT, TEST_DT.replace(year=2017), "Gy", "2016\u2009\u2013\u20092017 AD"), + ( + TEST_DT, + TEST_DT.replace(year=2017, month=3), + "GyM", + "1/2016\u2009\u2013\u20093/2017 AD", + ), + # Day period ('B') interval formats used to raise NotImplementedError, too. + # Differing day periods must select the 'B' pattern of the "Bh" interval format. + ( + TEST_DT, + TEST_DT.replace(hour=16), + "Bh", + "11 in the morning\u2009\u2013\u20094 in the afternoon", + ), + # ... and a shared day period must fall through to the 'h' pattern. + (TEST_DT, TEST_DT.replace(hour=10), "Bh", "11\u2009\u2013\u200910 in the morning"), + ], +) +def test_format_interval_era_and_day_period(start, end, skeleton, expected): + assert dates.format_interval(start, end, skeleton, locale="en") == expected From 8a5ffe940892cc90c30aee7e36e5ae56078330d0 Mon Sep 17 00:00:00 2001 From: simpleqt <89645338+simpleqt@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:17:06 +0800 Subject: [PATCH 2/2] dates: return a location without a region when the locale has no name for it get_timezone_location() falls back to the CLDR region 'ZZ' ('unknown region') when the zone's territory has no name in the locale, but it then looked that name up with [] instead of .get(), so every locale that does not translate 'ZZ' -- 173 of the 1,121 locales -- raised KeyError: 'ZZ' for a perfectly valid zone. get_timezone_name() reaches the same code when no zone variant is given, so the crash surfaced through the public name lookup too. Leave the region out of the result in that case, which is what the location format degenerates to when the locale cannot name the region. --- babel/dates.py | 8 ++++++-- tests/test_dates.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/babel/dates.py b/babel/dates.py index d0372d143..b2244466c 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -528,10 +528,10 @@ def get_timezone_location( territory = get_global('zone_territories').get(zone) if territory not in locale.territories: territory = 'ZZ' # invalid/unknown - territory_name = locale.territories[territory] + territory_name = locale.territories.get(territory) if ( not return_city - and territory + and territory_name and len(get_global('territory_zones').get(territory, [])) == 1 ): return region_format % territory_name @@ -552,6 +552,10 @@ def get_timezone_location( if return_city: return city_name + if territory_name is None: + # The locale has no name for the zone's region, not even for the + # "unknown region" fallback, so the region is left out. + return region_format % city_name return region_format % ( fallback_format % { diff --git a/tests/test_dates.py b/tests/test_dates.py index 4f8c73907..2297db18c 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -284,6 +284,17 @@ def test_get_timezone_location(timezone_getter): 'Deutschland (Berlin) (Ortszeit)') +def test_get_timezone_location_without_a_region_name(timezone_getter): + # Locales without a name for the "unknown region" (CLDR "ZZ") used to + # raise a KeyError instead of falling back to the location without region. + tz = timezone_getter('America/St_Johns') + assert dates.get_timezone_location(tz, locale='aa') == 'St. John’s' + assert dates.get_timezone_location(tz, locale='aa', return_city=True) == 'St. John’s' + assert dates.get_timezone_name(tz, locale='aa') == 'St. John’s' + # Locales that do have a name for it are unchanged. + assert dates.get_timezone_location(tz, locale='en') == 'Canada (St. John’s) Time' + + @pytest.mark.parametrize( ('tzname', 'params', 'expected'), [