Skip to content

fix: format_o (%o) now uses language-specific ordinal suffixes#107

Merged
atoomic merged 1 commit into
cpan-authors:mainfrom
Koan-Bot:koan.atoomic/fix-format-o-inheritance
Apr 26, 2026
Merged

fix: format_o (%o) now uses language-specific ordinal suffixes#107
atoomic merged 1 commit into
cpan-authors:mainfrom
Koan-Bot:koan.atoomic/fix-format-o-inheritance

Conversation

@Koan-Bot
Copy link
Copy Markdown

What

The %o format specifier now correctly uses each language module's @Dsuf array for ordinal suffixes, instead of always falling back to English (st/nd/rd/th).

Why

format_o in Generic.pm closed over a lexical my @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 @Dsuf arrays but these were silently ignored — users always got English suffixes.

How

Changed format_o to dynamically resolve @Dsuf from the object's package via symbolic reference, falling back to the English default when no package-level @Dsuf exists. This follows the same pattern already used by Date::Language::format_Z.

Testing

  • Extended t/time2str-lang.t from 8 to 20 tests covering %o across English, French, German, Spanish, Romanian, and Occitan
  • Full test suite passes (28 test files)

🤖 Generated with Claude Code

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>
Koan-Bot added a commit to Koan-Bot/perl-TimeDate that referenced this pull request Apr 18, 2026
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>
atoomic pushed a commit that referenced this pull request Apr 26, 2026
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>
@atoomic atoomic marked this pull request as ready for review April 26, 2026 12:58
@atoomic atoomic merged commit c5e036f into cpan-authors:main Apr 26, 2026
22 checks passed
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 26, 2026

Greptile Summary

This PR fixes format_o (%o) in Generic.pm to dynamically look up @Dsuf from the calling object's package via a symbolic reference (with a fallback to the English @Dsuf lexical in Generic.pm), mirroring the pattern already used by format_Z. The implementation is correct for all language modules that declare our @Dsuf, and the fix genuinely resolves the long-standing bug where Spanish, Romanian, Occitan, and similar languages always received English ordinal suffixes.

Confidence Score: 5/5

Safe 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 our @Dsuf language modules, the English fallback works properly, and the edge cases (empty $pkg, unknown package) are handled. All three comments are P2: test scope clarity, assertion strength, and a pre-existing uninitialized-value risk for day 31.

t/time2str-lang.t — the Spanish and Occitan assertions could be tightened to exact equality checks for better regression protection.

Important Files Changed

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
Loading

Reviews (1): Last reviewed commit: "fix: format_o (%o) now uses language-spe..." | Re-trigger Greptile

Comment thread t/time2str-lang.t
Comment on lines +41 to +46
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' );
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment thread t/time2str-lang.t
Comment on lines +50 to +52
{
my $result = time2str('%o', $jan1, 'GMT', 'Spanish');
unlike( $result, qr/st$/, 'Spanish %o day 1 is not English "st"' );
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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:

Suggested change
{
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').

Comment on lines +217 to +223
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]])
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 $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.

@Koan-Bot Koan-Bot deleted the koan.atoomic/fix-format-o-inheritance branch April 27, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants