diff --git a/babel/dates.py b/babel/dates.py index 7b626aa11..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 % { @@ -1480,7 +1484,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 +1500,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 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'), [