Skip to content

fix(libc): compute strftime week numbers as the console's libc does - #11004

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
verusferro:fix/libc-strftime-iso-week
Oct 26, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
verusferro:fix/libc-strftime-iso-week

Conversation

@verusferro

@verusferro verusferro commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

What

strftime's %G, %g and %V threw for a negative tm_yday, a tm_yday past the year and a tm_wday below -6, because no rule fitted the console's results. They now follow the rule of the SDK libc.prx that PPSA06416 ships in sce_module/ (title SDK 7.00), read from its disassembly (REVERSE_ENGINEERING, source 2). strftime (Av3zjWi64Kw, 0x6d720) passes the C-locale time table to _Strftime (0x3ce10), whose conversion switch is at 0x3d2e0: %G 0x3d41d, %V 0x3d6bb, %g 0x3d9a6, %U 0x3d697, %W 0x3d7fa.

The rule, all in 32-bit arithmetic with C remainders:

  • 1 January falls on c = (tm_wday - tm_yday + 371) % 7 and the next one on n = (c + 365 + leap) % 7.

  • When n is 2, 3 or 4 and tm_yday - leap >= 366 - n, the date is week 1 of the next year.

  • Otherwise week = (tm_yday - (tm_wday + 6) % 7 + 7) / 7, plus 1 when c is 2, 3 or 4. A negative week is week 1 of the next year. Week 0 belongs to the previous year: %G prints that year, and %V prints its last week computed from its length, or 00 when its last days are week 1.

  • The leap test (0x3e830) is not Gregorian. A year divisible by 4 and not by 100 is leap, and of the years divisible by 100 only those with tm_year / 400 equal to 100 or -300:

    3e857: lea  -0x9c40(%rdi),%ecx    tm_year - 40000
    3e85d: cmp  $0x190,%ecx; jb       below 400 unsigned: leap
    3e86a: test %edi,%edi; jns        otherwise non-negative: not leap
    3e86e: add  $0x1d64f,%edi         tm_year + 120399
    3e876: cmp  $0x190,%edi; setb %al
    

For every real date from year 1 to 9999 this still gives ISO 8601, so valid dates print as before. Fields that are each in range but disagree with each other can change through the leap test: tm_year 100, tm_wday 1, tm_yday 362 prints 2001|01|01 as the module does, where the FreeBSD loop gave 2000|00|53.

tm_year + 1 and the other sums wrap at 32 bits as in the module; the code reproduces that through uint32_t without signed overflow. %U and %W keep their formula, (tm_yday + 7 - (tm_wday + shift) % 7) / 7, but now also wrap at 32 bits as the module does, so a field near INT_MAX matches it. The debt entry now records where the rule comes from and what is not measured on a console.

Tested

Ubuntu 24.04 (WSL2) x86-64, GCC 13.3, Release, full build with BUILD_TESTING=ON:

  • The title's libc.prx, converted by the relinker and loaded in a host process with ProcessParametersFixture, returns all 82 console results in guest_strftime (the zone conversions left out) and the console values the old debt entry listed: %V 01, 00, 00 and 52 for tm_yday -25, -13, -12 and -1; week 01 of 2027; 59; 53; 04.
  • This library's strftime and the module's print the same %G|%g|%V|%U|%W|%Y|%y for all 675,076 inputs of three random runs: fields up to INT_MIN and INT_MAX, every tm_wday from -14 to 14 around year ends, and the years the leap test treats differently.
  • guest_strftime gains the console's out-of-range cases, rows for each branch, the wrap at INT_MAX and INT_MIN, and two %U/%W rows. The old code throws on the first ISO row and prints 78|78 for tm_yday INT_MAX. A Gregorian leap test fails the tm_year 100 row.
  • ctest -R '^(guest_strftime|guest_time_conversion|guest_time_extras|guest_wide_formatting|patched_libraries|conventions_unknown_nids)$': 6/6 pass.
  • tools/progress.py: libc 1344 → 1345 of 1355 (libc_strftime implemented).
  • python3 tools/check_conventions.py --base origin/main: passed. python3 tools/pr_overlap.py: no conflicting lines or same exports. Of the pull requests it could not compare, feat(libc): add the std::__sce_v2 thread start, timed wait, clock and processor count calls #9447 changes _Xtime_get_ticks in Time.cpp and fix(libc): write a 36-byte struct tm as the console does #10409 the GuestTm layout; neither touches the week code. The rest share only docs/dev/debt/libc.md, in other entries.

Windows: not run by me. This head passed in runs by @ycbuzi and @wachi35676; the second also compared every date from year 1 to 9999 against ISO 8601 with no mismatch.

Not tested: macOS, a console run of the cases the old debt entry did not list.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: fix(libc): preserve partial wcsftime output on overflow #10895
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

%G, %g and %V no longer throw for a negative tm_yday, a tm_yday past the year or a tm_wday below -6. They follow the year and week rule of the SDK libc.prx, read from its disassembly and checked by running it, with its leap-year test and 32-bit arithmetic. %U and %W also wrap in 32 bits as that module does.
@github-actions

Copy link
Copy Markdown

Progress report (e05ba6f → c9df854)

📈 System libraries: 49.64% (+0.01%, +1 implemented functions)

✅ 1 implemented
Library Name
libc libc_strftime_nid_postfix

Changed libraries (implemented exports)

Library Before After Change
libc 1344/1355 (99.19%) 1345/1355 (99.26%) +0.07 pp

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the %G/%g/%V implementation and the cited console/SDK evidence. The new tests cover the reported negative and oversized fields, inconsistent year fields, and 32-bit boundary cases; the PR reports matching behavior against the SDK module over 675,076 inputs and six focused host tests. I independently verified the diff, conventions, and a local C++20 syntax check of Time.cpp. Hosted eligibility/conventions/report and non-product jobs pass; the PR does not claim a hosted full product build. Windows/macOS product tests and console measurements for untested cases remain outstanding as documented.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 5021 core/libs/prx/libc/src/Time.cpp
docs/dev/debt/libc.md
PR 5303 docs/dev/debt/libc.md
PR 6713 docs/dev/debt/libc.md
PR 7081 docs/dev/debt/libc.md
PR 7201 core/libs/prx/libc/src/Time.cpp
PR 7386 docs/dev/debt/libc.md
PR 7447 docs/dev/debt/libc.md
PR 7544 docs/dev/debt/libc.md
PR 7648 docs/dev/debt/libc.md
PR 7715 docs/dev/debt/libc.md
PR 8150 docs/dev/debt/libc.md
PR 9371 docs/dev/debt/libc.md
PR 9447 core/libs/prx/libc/src/Time.cpp
docs/dev/debt/libc.md
PR 9526 docs/dev/debt/libc.md
PR 9772 docs/dev/debt/libc.md
PR 9863 docs/dev/debt/libc.md
PR 9929 docs/dev/debt/libc.md
PR 9953 docs/dev/debt/libc.md
PR 9964 docs/dev/debt/libc.md
PR 10002 docs/dev/debt/libc.md
PR 10007 docs/dev/debt/libc.md
PR 10158 docs/dev/debt/libc.md
PR 10200 docs/dev/debt/libc.md
PR 10234 docs/dev/debt/libc.md
PR 10315 docs/dev/debt/libc.md
PR 10317 docs/dev/debt/libc.md
PR 10321 docs/dev/debt/libc.md
PR 10325 docs/dev/debt/libc.md
PR 10328 docs/dev/debt/libc.md
PR 10345 docs/dev/debt/libc.md
PR 10349 docs/dev/debt/libc.md
PR 10405 docs/dev/debt/libc.md
PR 10409 core/libs/prx/libc/src/Time.cpp
core/libs/tests/GuestStrftime.cpp
PR 10467 docs/dev/debt/libc.md
PR 10506 docs/dev/debt/libc.md
PR 10510 docs/dev/debt/libc.md
PR 10512 docs/dev/debt/libc.md
PR 10529 docs/dev/debt/libc.md
PR 10565 docs/dev/debt/libc.md
PR 10749 docs/dev/debt/libc.md
PR 11332 docs/dev/debt/libc.md
PR 11341 docs/dev/debt/libc.md

@ycbuzi

ycbuzi commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

A Windows run of this head: Windows 11 Home 10.0.26200, WinLibs GCC 15.2.0 r7 (MinGW-w64 UCRT, posix-seh), CMake 4.3.1, Ninja 1.13.2, Release. c9df854687 merged onto main d2b3ee3bcc with no conflicts.

  • guest_strftime_tests builds with no warnings or errors.
  • guest_strftime passes both runs of ctest --repeat until-fail:2.
  • Negative control: libc/src/Time.cpp taken from main, with this test. It fails with strftime: ISO week of a negative tm_yday not implemented.

Not run: Linux, macOS, the comparison against the title's libc.prx.

AI-assisted: yes (Claude Code)

@wachi35676

Copy link
Copy Markdown
Contributor

First Windows run of head c9df85468795 merged onto main 90f918fa7. Windows 11 10.0.26300, WinLibs GCC 15.2.0, Release.

  • guest_strftime passes 3/3. With Time.cpp from main, it aborts with strftime: ISO week of a negative tm_yday not implemented.
  • I checked the "every real date still gives ISO 8601" claim independently. For every day of years 1 to 9999 (3,652,059 dates, proleptic Gregorian, 1 Jan 0001 a Monday), I compared strftime("%G|%g|%V|%U|%W") against an ISO 8601 / C99 week calculation of my own. There are 0 mismatches once %G is printed with four digits, as the module does (0001). So the non-Gregorian leap test only affects inconsistent field combinations, never real dates.

AI-assisted: yes (Claude Code); results above were run on this machine.

@StoneDC

StoneDC commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

@verusferro Thanks for the careful work and reference-module evidence in this PR! I maintain #10895, which fixes partial output from wcsftime on overflow. Your week-number fix addresses a separate issue, but I compared our current heads (920903db9 and c9df854687) and confirmed merge conflicts in Time.cpp and docs/dev/debt/libc.md.

Would you be happy with #10895 landing first, followed by rebasing #11004 onto the updated main and recording Depends on: #10895 in your checklist? #10895 already has approvals, and we have agreed on that same first step with the author of #10987.

When rebasing, we would need to preserve the templated FormatBuffer declaration from #10895 alongside your week-number changes, retain both regression test sets, and keep both debt-entry updates. Rerunning guest_strftime and guest_wide_formatting after integration would help check the combined behavior. I have checked the textual conflicts, but have not run the combined code.

Thanks for coordinating! Please let me know if you would prefer a different order; the final merge decision remains with the maintainers.

@oneandonlydean oneandonlydean self-assigned this Oct 26, 2026
@verusferro

Copy link
Copy Markdown
Contributor Author

@StoneDC Agreed: #10895 first. I've set Depends on: #10895, and I'll rebase #11004 once #10895 is on main.

I tried that rebase locally, replaying c9df854687 onto your head 920903db93. Both conflicts are adjacent edits:

Both test sets merge without conflicts. On that combined tree, guest_strftime, guest_wide_formatting, guest_time_conversion, guest_time_extras, patched_libraries and conventions_unknown_nids pass (6/6, Linux), and so does check_conventions.py.

@oneandonlydean
oneandonlydean merged commit c980e6e into boykopovar:main Oct 26, 2026
22 checks passed
@verusferro
verusferro deleted the fix/libc-strftime-iso-week branch October 28, 2026 13:07
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.

6 participants