Skip to content

Recognize localized AM/PM markers in parse_time() - #1426

Open
jeff-grimes-pplx wants to merge 1 commit into
python-babel:masterfrom
jeff-grimes-pplx:fix/parse-time-localized-am-pm
Open

jeff-grimes-pplx wants to merge 1 commit into
python-babel:masterfrom
jeff-grimes-pplx:fix/parse-time-localized-am-pm

Conversation

@jeff-grimes-pplx

Copy link
Copy Markdown

Fixes #1168.

Problem

babel.dates.parse_time() decides between AM and PM by searching the input for the English substrings "am" / "pm". Babel's own format_time() output for locales whose CLDR day-period names aren't English (Korean 오후, Spanish p.m., Greek μ.μ., Arabic م, Chinese 下午, Albanian m.d., Afar carra, ...) is therefore always parsed as AM, so every afternoon time comes back 12 hours early. No error is raised.

>>> from babel.dates import format_time, parse_time
>>> import datetime
>>> s = format_time(datetime.time(15, 30), 'short', locale='ko_KR')
>>> s
'오후 3:30'
>>> parse_time(s, locale='ko_KR', format='short')
datetime.time(3, 30)        # expected datetime.time(15, 30)

When I round-tripped format_time() through parse_time() for every CLDR locale whose short/medium time pattern uses the a period field, every hour 0–23, 4,038 of 17,040 round-trips were wrong, across 171 locales.

Root cause

parse_time() (babel/dates.py, the if 'a' in format_str: block) hardcodes English markers:

if 'pm' in string.lower(): ...
elif 'am' in string.lower() and hour == 12: ...

It never reads the locale's day_periods data, even though the same data drives format_time()'s a field.

Fix

A small helper, _parse_period(), works out the marker from the locale's own AM/PM day-period names:

  • it checks every context (format / stand-alone) and every width (abbreviated / wide / narrow);
  • it tries the longest name first, so a short name like p can't match inside a longer one (dop. in Slovenian, for example);
  • it compares case- and whitespace-insensitively, because CLDR names contain U+00A0 / U+202F (Catalan p. m., for example);
  • it falls back to the English am/pm, so existing callers that parse English markers with another locale keep working.

The 12 AM / 12 PM normalisation is unchanged.

Verification

  • New regression test tests/test_dates.py::test_parse_time_localized_am_pm: it fails on master (assert datetime.time(3, 30) == datetime.time(15, 30)) and passes with the fix.
  • Full suite: 7939 passed, 8 skipped, 2 xfailed.
  • ruff check is clean.
  • An exhaustive sweep over all locales, five 12-hour patterns (h:mm:ss a, h:mm:ss aaaa, h:mm:ss aaaaa, a h:mm, K:mm a) and all 24 hours: correct round-trips go from 99,075 to 134,476 out of 134,520, with 0 regressions. The 44 that still fail are all Anii (blo), whose period names contain digits (1ka / 2ja) and confuse the existing digit extraction. That's a separate, pre-existing limitation.
  • Out of scope: flexible day periods (pattern B, used by zh_Hant, for example).

parse_time() decided between AM and PM by searching the input for the
English substrings "am" and "pm". Times formatted by Babel itself for
locales whose CLDR day-period names are not English - Korean "오후",
Spanish "p.m.", Greek "μ.μ.", Arabic "م", Chinese "下午", Albanian
"m.d." and many more - were therefore parsed as AM, silently shifting
every afternoon time back by 12 hours.

Look the marker up in the locale's own AM/PM day-period names (all
contexts and widths, longest name first, ignoring case and whitespace)
and keep the English markers as a fallback.
@akx

akx commented Nov 3, 2026

Copy link
Copy Markdown
Member

I'm sorry, how does #1168 relate to this?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants