Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions babel/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,22 @@ def ordinal_form(self) -> PluralRule:
"""
return self._data.get('ordinal_form', _default_plural_rule)

@property
def ordinal_day_formats(self) -> localedata.LocaleDataDict:
"""Locale patterns for ordinal days of the month, e.g. "13th" in English.

The keys are `ordinal_form` plural categories, and ``{0}`` is replaced
with the day of the month.

Introduced in CLDR 49.

.. note:: The format of the value returned may change between
Babel versions.

.. versionadded:: 2.19
"""
return self._data.get('ordinal_day_formats', localedata.LocaleDataDict({}))

@property
def measurement_systems(self) -> localedata.LocaleDataDict:
"""Localized names for various measurement systems.
Expand Down
27 changes: 25 additions & 2 deletions babel/dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -1444,7 +1444,7 @@ def __getitem__(self, name: str) -> str:
elif char in ('w', 'W'):
return self.format_week(char, num)
elif char == 'd':
return self.format(self.value.day, num)
return self.format_day_of_month(num)
elif char == 'D':
return self.format_day_of_year(num)
elif char == 'F':
Expand Down Expand Up @@ -1580,6 +1580,29 @@ def format_weekday(self, char: str = 'E', num: int = 4) -> str:
context = "stand-alone" if char == "c" else "format"
return get_day_names(width, context, self.locale)[weekday]

def format_day_of_month(self, num: int) -> str:
"""
Return day of month from parsed datetime according to format pattern.

>>> from datetime import date
>>> format = DateTimeFormat(date(2016, 2, 28), Locale.parse('en_US'))
>>> format.format_day_of_month(1)
'28'

'd': Day of month - Use one or two for the minimum number of digits, or
three for the ordinal form (CLDR 49 and later), e.g. "28th" in
English. Locales without ordinal forms fall back to a single "d".
"""
if num == 3:
ordinal_pattern = self.locale.ordinal_day_formats.get(
self.locale.ordinal_form(self.value.day),
)
if ordinal_pattern:
return ordinal_pattern.replace('{0}', str(self.value.day))
# No ordinal forms for this locale; format like a single "d" would.
num = 1
return self.format(self.value.day, num)

def format_day_of_year(self, num: int) -> str:
return self.format(self.get_day_of_year(), num)

Expand Down Expand Up @@ -1753,7 +1776,7 @@ def get_week_number(self, day_of_period: int, day_of_week: int | None = None) ->
'Q': [1, 2, 3, 4, 5], 'q': [1, 2, 3, 4, 5], # quarter
'M': [1, 2, 3, 4, 5], 'L': [1, 2, 3, 4, 5], # month
'w': [1, 2], 'W': [1], # week
'd': [1, 2], 'D': [1, 2, 3], 'F': [1], 'g': None, # day
'd': [1, 2, 3], 'D': [1, 2, 3], 'F': [1], 'g': None, # day
'E': [1, 2, 3, 4, 5, 6], 'e': [1, 2, 3, 4, 5, 6], 'c': [1, 3, 4, 5, 6], # week day
'a': [1, 2, 3, 4, 5], 'b': [1, 2, 3, 4, 5], 'B': [1, 2, 3, 4, 5], # period
'h': [1, 2], 'H': [1, 2], 'K': [1, 2], 'k': [1, 2], # hour
Expand Down
20 changes: 20 additions & 0 deletions scripts/import_cldr.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ def _process_local_datas(sup, srcdir, destdir, force=True, dump_json=False):
parse_calendar_quarters(data, calendar)
parse_calendar_eras(data, calendar)
parse_calendar_periods(data, calendar)
parse_calendar_ordinal_day_formats(data, calendar)
parse_calendar_date_formats(data, calendar)
parse_calendar_time_formats(data, calendar)
parse_calendar_datetime_skeletons(data, calendar)
Expand Down Expand Up @@ -720,6 +721,25 @@ def parse_calendar_periods(data, calendar):
dest_dict[period_type] = str(day_period.text)


def parse_calendar_ordinal_day_formats(data, calendar):
# Ordinal days of the month, e.g. "1st" in English (CLDR 49 and later).
# These are keyed by the ordinal plural categories of the locale.
ordinal_day_formats = data.setdefault('ordinal_day_formats', {})
for day_of_month_context in calendar.findall('dayOfMonths/dayOfMonthContext'):
if day_of_month_context.attrib.get('type') != 'format':
continue
for day_of_month_width in day_of_month_context.findall('dayOfMonthWidth'):
if day_of_month_width.attrib.get('type') != 'abbreviated':
continue
for elem in day_of_month_width.findall('dayOfMonth'):
ordinal = elem.attrib.get('ordinal')
if ordinal is None or elem.text is None:
continue
if _should_skip_elem(elem, ordinal, ordinal_day_formats):
continue
ordinal_day_formats[ordinal] = str(elem.text)


def parse_calendar_date_formats(data, calendar):
date_formats = data.setdefault('date_formats', {})
for format in calendar.findall('dateFormats'):
Expand Down
43 changes: 43 additions & 0 deletions tests/test_dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,49 @@ def test_parse_pattern():
assert dates.parse_pattern("hh' o''clock'").format == "%(hh)s o'clock"


def test_parse_pattern_ordinal_day():
# CLDR 49 added the ordinal day of month field, e.g. "Feb 1st"
assert dates.parse_pattern("MMM ddd, y").format == '%(MMM)s %(ddd)s, %(y)s'


def test_parse_pattern_rejects_unsupported_day_length():
with pytest.raises(ValueError):
dates.parse_pattern("dddd")


def test_format_ordinal_day_of_month(monkeypatch):
# CLDR 49 and later provide one ordinal pattern per ordinal plural category
locale = Locale('en')
ordinal_day_formats = {
'one': '{0}st',
'two': '{0}nd',
'few': '{0}rd',
'other': '{0}th',
}
monkeypatch.setitem(locale._data, 'ordinal_day_formats', ordinal_day_formats)

def format_day(day: int) -> str:
return dates.format_datetime(datetime(2016, 2, day), "MMM ddd, y", locale=locale)

assert format_day(1) == 'Feb 1st, 2016'
assert format_day(2) == 'Feb 2nd, 2016'
assert format_day(3) == 'Feb 3rd, 2016'
assert format_day(4) == 'Feb 4th, 2016'
assert format_day(11) == 'Feb 11th, 2016'
assert format_day(22) == 'Feb 22nd, 2016'


def test_format_ordinal_day_of_month_falls_back_without_forms():
# Locales without ordinal forms format the day like a single "d" would,
# without zero padding (CLDR 49 has no forms for German, for instance).
day = datetime(2016, 2, 1)
assert (
dates.format_datetime(day, "MMM ddd, y", locale='de')
== dates.format_datetime(day, "MMM d, y", locale='de')
)
assert dates.format_datetime(day, "dd.MM.y", locale='de') == '01.02.2016'


def test_lithuanian_long_format():
assert (
dates.format_date(date(2015, 12, 10), locale='lt_LT', format='long') ==
Expand Down