fix: format_o (%o) now uses language-specific ordinal suffixes#107
Conversation
Generic.pm's format_o closed over a lexical @Dsuf array, so language modules that declared their own @Dsuf (Spanish, Brazilian, Romanian, Arabic, Occitan, etc.) still got English "1st/2nd/3rd" suffixes unless they also defined a format_o override. Now format_o dynamically resolves @Dsuf from the object's package, falling back to the English default. Co-Authored-By: Claude <noreply@anthropic.com>
format_P in Generic.pm used a lexical @ampm array directly, so it always returned English "am"/"pm" regardless of language module. Same inheritance bug pattern as format_o (PR cpan-authors#107). Fix: delegate to format_p via method dispatch, which correctly resolves to the language module's copied format_p function. Affected languages with non-English AMPM: Dutch (VM/NM), Finnish (ap/ip), Hungarian (DE./DU.), Czech (dop./odp.), Chinese, Arabic, Greek, and 10+ others. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
format_P in Generic.pm used a lexical @ampm array directly, so it always returned English "am"/"pm" regardless of language module. Same inheritance bug pattern as format_o (PR #107). Fix: delegate to format_p via method dispatch, which correctly resolves to the language module's copied format_p function. Affected languages with non-English AMPM: Dutch (VM/NM), Finnish (ap/ip), Hungarian (DE./DU.), Czech (dop./odp.), Chinese, Arabic, Greek, and 10+ others. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR fixes Confidence Score: 5/5Safe to merge — the fix is correct and all remaining findings are minor style/test-quality suggestions with no runtime impact. No P0 or P1 issues found. The symbolic-reference lookup is correct for all t/time2str-lang.t — the Spanish and Occitan assertions could be tightened to exact equality checks for better regression protection.
|
| Filename | Overview |
|---|---|
| lib/Date/Format/Generic.pm | format_o updated to dynamically resolve @Dsuf from the object's package via symbolic reference, with correct English fallback; logic is sound for all language modules using our @Dsuf |
| t/time2str-lang.t | 12 new %o tests added; French and German tests exercise pre-existing per-module format_o overrides rather than the new Generic.pm code path; Spanish and Occitan assertions use unlike instead of exact value checks |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["format_o called\n(Generic.pm)"] --> B["pkg = ref(obj)"]
B --> C{pkg truthy?}
C -- No --> F["dsuf = undef"]
C -- Yes --> D["no strict 'refs'\ndsuf = \\@{pkg::Dsuf}"]
D --> E{"dsuf && @$dsuf non-empty?"}
F --> G["dsuf = \\@Dsuf\n(English fallback)"]
E -- No --> G
E -- Yes --> H["Use language-specific @Dsuf\nfrom pkg symbol table"]
G --> I["sprintf('%2d%s', day, dsuf->[day])"]
H --> I
I --> J["Return ordinal string\ne.g. ' 1st', ' 1do', ' 1er'"]
style G fill:#f9f,stroke:#333
style H fill:#9f9,stroke:#333
Reviews (1): Last reviewed commit: "fix: format_o (%o) now uses language-spe..." | Re-trigger Greptile
| is( time2str('%o', $jan1, 'GMT', 'French'), ' 1er', 'French %o day 1' ); | ||
| is( time2str('%o', $jan2, 'GMT', 'French'), ' 2e', 'French %o day 2' ); | ||
|
|
||
| # German uses period suffix | ||
| is( time2str('%o', $jan1, 'GMT', 'German'), ' 1.', 'German %o day 1' ); | ||
| is( time2str('%o', $jan15, 'GMT', 'German'), '15.', 'German %o day 15' ); |
There was a problem hiding this comment.
French and German tests exercise pre-existing overrides, not the new fix
Both French.pm and German.pm define their own format_o method that overrides Generic.pm::format_o entirely — so these tests prove the pre-existing overrides still work, not the new symbolic-reference lookup. The new code path in Generic.pm is only exercised by the Spanish, Romanian, and Occitan tests below. This doesn't cause test failures, but the coverage statement in the PR description is slightly misleading: adding an assertion on a language whose format_o relies solely on Generic.pm (e.g., Dutch or Finnish) would provide more direct validation.
| { | ||
| my $result = time2str('%o', $jan1, 'GMT', 'Spanish'); | ||
| unlike( $result, qr/st$/, 'Spanish %o day 1 is not English "st"' ); |
There was a problem hiding this comment.
Weak assertion — consider testing the exact expected value
The unlike assertion only verifies that Spanish day 1 doesn't end in the English "st" suffix; it passes even if the result is garbled or empty. Since the comment on the preceding line already states the exact expected suffix ('do'), a precise equality check would make the test self-documenting and catch regressions:
| { | |
| my $result = time2str('%o', $jan1, 'GMT', 'Spanish'); | |
| unlike( $result, qr/st$/, 'Spanish %o day 1 is not English "st"' ); | |
| is( $result, ' 1do', 'Spanish %o day 1 uses Spanish suffix "do"' ); |
The same applies to the Occitan block below (expected value ' 1er').
| sub format_o { | ||
| no strict 'refs'; | ||
| my $pkg = ref($_[0]); | ||
| my $dsuf = $pkg ? \@{"${pkg}::Dsuf"} : undef; | ||
| $dsuf = \@Dsuf unless $dsuf && @$dsuf; | ||
| sprintf("%2d%s",$_[0]->[3],$dsuf->[$_[0]->[3]]) | ||
| } |
There was a problem hiding this comment.
$dsuf && guard is redundant, and day-31 is unguarded
After the ternary, $dsuf is either undef (when ref($_[0]) is falsy) or a reference — never a non-reference falsy value — so $dsuf && adds nothing beyond @$dsuf. More practically, several @Dsuf arrays (Romanian, Occitan, Spanish, Swedish, Finnish, Portuguese, etc.) have exactly 31 elements (indices 0–30). Day 31 uses index 31, which is out of range, so $dsuf->[31] returns undef and sprintf emits an "uninitialized value" warning under use warnings. This is pre-existing but the fix now propagates it to more locales. Adding // '' when indexing would silence the warning safely.
What
The
%oformat specifier now correctly uses each language module's@Dsufarray for ordinal suffixes, instead of always falling back to English (st/nd/rd/th).Why
format_oinGeneric.pmclosed over a lexicalmy @Dsuf, making it impossible for language modules to provide their own ordinal suffixes through inheritance. Languages like Spanish, Brazilian, Romanian, Arabic, and Occitan declared custom@Dsufarrays but these were silently ignored — users always got English suffixes.How
Changed
format_oto dynamically resolve@Dsuffrom the object's package via symbolic reference, falling back to the English default when no package-level@Dsufexists. This follows the same pattern already used byDate::Language::format_Z.Testing
t/time2str-lang.tfrom 8 to 20 tests covering%oacross English, French, German, Spanish, Romanian, and Occitan🤖 Generated with Claude Code