Simplify ISO week computation

Compute the ISO week-based year and week number from the day of the year
of the Thursday in the current week instead of the weekday of December 31.
This only needs a leap year check, which works for negative years without
floored division, and is about 3 times faster.

For std::tm values with tm_wday and tm_yday inconsistent with tm_year,
%G, %g and %V now give the same results as strftime.

Replace the negative year tests with a year_month_day test that also
covers the leap year check and days_from_civil for negative years.
This commit is contained in:
Victor Zverovich 2026-10-30 11:15:43 -07:00
parent f87a75e152
commit 97bd2ad530
2 changed files with 18 additions and 43 deletions

View File

@ -1151,42 +1151,29 @@ class tm_writer {
return static_cast<int>(l);
}
// Returns the weekday of December 31 of the given year (0 = Sunday). Uses
// floored division and a non-negative remainder so that it is also correct
// for negative years.
static auto dec31_weekday(long long year) noexcept -> long long {
auto floor_div = [](long long a, long long b) {
return a / b - (a % b < 0 ? 1 : 0);
};
auto p = (year + floor_div(year, 4) - floor_div(year, 100) +
floor_div(year, 400)) %
days_per_week;
return p < 0 ? p + days_per_week : p;
static auto days_in_year(long long year) noexcept -> int {
return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) ? 366 : 365;
}
// Algorithm: https://en.wikipedia.org/wiki/ISO_week_date.
auto iso_year_weeks(long long curr_year) const noexcept -> int {
auto curr_p = dec31_weekday(curr_year);
auto prev_p = dec31_weekday(curr_year - 1);
return 52 + ((curr_p == 4 || prev_p == 3) ? 1 : 0);
}
auto iso_week_num(int tm_yday, int tm_wday) const noexcept -> int {
return (tm_yday + 11 - (tm_wday == 0 ? days_per_week : tm_wday)) /
days_per_week;
// Returns the day of the year of the Thursday in the current week, which may
// be outside the current year. The ISO week-based year is the year of this
// Thursday: https://en.wikipedia.org/wiki/ISO_week_date.
auto thursday_yday() const noexcept -> int {
auto wday = tm_wday();
return tm_yday() + 4 - (wday == 0 ? days_per_week : wday);
}
auto tm_iso_week_year() const noexcept -> long long {
auto year = tm_year();
auto w = iso_week_num(tm_yday(), tm_wday());
if (w < 1) return year - 1;
if (w > iso_year_weeks(year)) return year + 1;
auto thursday = thursday_yday();
if (thursday < 0) return year - 1;
if (thursday >= days_in_year(year)) return year + 1;
return year;
}
auto tm_iso_week_of_year() const noexcept -> int {
auto year = tm_year();
auto w = iso_week_num(tm_yday(), tm_wday());
if (w < 1) return iso_year_weeks(year - 1);
if (w > iso_year_weeks(year)) return 1;
return w;
auto thursday = thursday_yday();
if (thursday >= days_in_year(tm_year())) return 1;
if (thursday < 0) thursday += days_in_year(tm_year() - 1);
return thursday / days_per_week + 1;
}
void write1(int value) {

View File

@ -123,21 +123,6 @@ TEST(chrono_test, format_tm) {
tm.tm_year = -2147483648;
EXPECT_EQ(fmt::format("{:%Y}", tm), "-2147481748");
// ISO week-based year and week number for negative years. The year -2 starts
// on a Thursday, so it has 53 ISO weeks.
tm = make_tm(-2, 12, 31, 0, 0, 0);
tm.tm_wday = 4;
tm.tm_yday = 364;
EXPECT_EQ(fmt::format("{:%G %V}", tm), "-002 53");
tm = make_tm(-1, 1, 1, 0, 0, 0);
tm.tm_wday = 5;
tm.tm_yday = 0;
EXPECT_EQ(fmt::format("{:%G %V}", tm), "-002 53");
tm = make_tm(-1, 1, 4, 0, 0, 0);
tm.tm_wday = 1;
tm.tm_yday = 3;
EXPECT_EQ(fmt::format("{:%G %V}", tm), "-001 01");
// for week on the year
// https://www.cl.cam.ac.uk/~mgk25/iso-time.html
std::vector<std::tm> tm_list = {
@ -1127,6 +1112,9 @@ TEST(chrono_test, year_month_day_weekday_and_day_of_year) {
EXPECT_EQ(fmt::format("{:%a %j %U %W}", ymd), "Fri 001 00 00");
EXPECT_EQ(fmt::format("{:%G-W%V-%u}", ymd), "2020-W53-5");
ymd = fmt::year_month_day(fmt::year(-23), fmt::month(1), fmt::day(1));
EXPECT_EQ(fmt::format("{:%G-W%V}", ymd), "-024-W53");
ymd = fmt::year_month_day(fmt::year(1969), fmt::month(12), fmt::day(31));
EXPECT_EQ(fmt::format("{:%A %j}", ymd), "Wednesday 365");
EXPECT_EQ(fmt::format("{:%G-W%V}", ymd), "1970-W01");