[time] Use O(1) closed-form leap year math for epoch-to-year conversion (#15368)

This commit is contained in:
J. Nick Koston
2026-04-02 09:19:47 -10:00
committed by GitHub
parent e7e590b36f
commit da09e1e1ce
2 changed files with 144 additions and 28 deletions
+109
View File
@@ -758,6 +758,115 @@ TEST(PosixTzParser, EpochToLocalDstTransition) {
EXPECT_EQ(local.tm_isdst, 1);
}
// ============================================================================
// Leap year edge cases for closed-form year arithmetic
// ============================================================================
TEST(PosixTzParser, EpochToLocalLeapYear2000) {
// 2000 is a leap year (divisible by 400)
ParsedTimezone tz;
ASSERT_TRUE(parse_posix_tz("UTC0", tz));
// Feb 29, 2000 12:00:00 UTC
time_t epoch = make_utc(2000, 2, 29, 12);
struct tm local;
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
EXPECT_EQ(local.tm_year, 100); // 2000
EXPECT_EQ(local.tm_mon, 1); // February
EXPECT_EQ(local.tm_mday, 29);
EXPECT_EQ(local.tm_hour, 12);
}
TEST(PosixTzParser, EpochToLocalNonLeapYear2100) {
// 2100 is NOT a leap year (divisible by 100 but not 400)
ParsedTimezone tz;
ASSERT_TRUE(parse_posix_tz("UTC0", tz));
// Mar 1, 2100 00:00:00 UTC — the day after what would be Feb 29
time_t epoch = make_utc(2100, 3, 1);
struct tm local;
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
EXPECT_EQ(local.tm_year, 200); // 2100
EXPECT_EQ(local.tm_mon, 2); // March
EXPECT_EQ(local.tm_mday, 1);
// Feb 28, 2100 23:59:59 UTC — last second of February (no Feb 29)
epoch = make_utc(2100, 2, 28, 23, 59, 59);
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
EXPECT_EQ(local.tm_year, 200);
EXPECT_EQ(local.tm_mon, 1); // February
EXPECT_EQ(local.tm_mday, 28);
}
TEST(PosixTzParser, EpochToLocalLeapYear2400) {
// 2400 is a leap year (divisible by 400)
ParsedTimezone tz;
ASSERT_TRUE(parse_posix_tz("UTC0", tz));
time_t epoch = make_utc(2400, 2, 29, 6);
struct tm local;
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
EXPECT_EQ(local.tm_year, 500); // 2400
EXPECT_EQ(local.tm_mon, 1); // February
EXPECT_EQ(local.tm_mday, 29);
EXPECT_EQ(local.tm_hour, 6);
}
TEST(PosixTzParser, EpochToLocalNewYearBoundaries) {
// Test year boundary — last second of 2099 and first second of 2100
ParsedTimezone tz;
ASSERT_TRUE(parse_posix_tz("UTC0", tz));
struct tm local;
// Dec 31, 2099 23:59:59 UTC
time_t epoch = make_utc(2099, 12, 31, 23, 59, 59);
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
EXPECT_EQ(local.tm_year, 199); // 2099
EXPECT_EQ(local.tm_mon, 11); // December
EXPECT_EQ(local.tm_mday, 31);
// Jan 1, 2100 00:00:00 UTC
epoch = make_utc(2100, 1, 1);
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
EXPECT_EQ(local.tm_year, 200); // 2100
EXPECT_EQ(local.tm_mon, 0); // January
EXPECT_EQ(local.tm_mday, 1);
}
TEST(PosixTzParser, EpochToLocalDstAcrossCenturyBoundary) {
// DST transition in year 2100 (non-leap) with US Eastern rules
ParsedTimezone tz;
ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz));
// July 4, 2100 16:00 UTC = 12:00 EDT
time_t epoch = make_utc(2100, 7, 4, 16);
struct tm local;
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
EXPECT_EQ(local.tm_hour, 12);
EXPECT_EQ(local.tm_isdst, 1);
// Jan 15, 2100 10:00 UTC = 05:00 EST
epoch = make_utc(2100, 1, 15, 10);
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
EXPECT_EQ(local.tm_hour, 5);
EXPECT_EQ(local.tm_isdst, 0);
}
TEST(PosixTzParser, EpochToLocalFarFutureYear5000) {
// Year 5000 — days/365 estimate overshoots by ~2 years due to leap days,
// requiring multiple correction steps in days_to_year.
ParsedTimezone tz;
ASSERT_TRUE(parse_posix_tz("UTC0", tz));
time_t epoch = make_utc(5000, 6, 15, 12);
struct tm local;
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
EXPECT_EQ(local.tm_year, 3100); // 5000
EXPECT_EQ(local.tm_mon, 5); // June
EXPECT_EQ(local.tm_mday, 15);
EXPECT_EQ(local.tm_hour, 12);
}
// ============================================================================
// Verification against libc
// ============================================================================