diff --git a/AK/Time.h b/AK/Time.h index 0137de2030..d301f5b6e3 100644 --- a/AK/Time.h +++ b/AK/Time.h @@ -66,14 +66,46 @@ constexpr int days_in_year(int year) return 365 + (is_leap_year(year) ? 1 : 0); } +namespace Detail { +// Integer division rounding towards negative infinity. +// TODO: This feels like there should be an easier way to do this. +template +constexpr int floor_div_by(int dividend) +{ + static_assert(divisor >= 1); + int is_negative = dividend < 0; + return (dividend + is_negative) / divisor - is_negative; +} + +// Counts how many integers n are in the interval [begin, end) with n % positive_mod == 0. +// NOTE: "end" is not considered to be part of the range, hence "[begin, end)". +template +constexpr int mod_zeros_in_range(int begin, int end) +{ + return floor_div_by(end - 1) - floor_div_by(begin - 1); +} +} + constexpr int years_to_days_since_epoch(int year) { - int days = 0; - for (int current_year = 1970; current_year < year; ++current_year) - days += days_in_year(current_year); - for (int current_year = year; current_year < 1970; ++current_year) - days -= days_in_year(current_year); - return days; + int begin_year, end_year, leap_sign; + if (year < 1970) { + begin_year = year; + end_year = 1970; + leap_sign = -1; + } else { + begin_year = 1970; + end_year = year; + leap_sign = +1; + } + // This duplicates the logic of 'is_leap_year', with the advantage of not needing any loops. + // Given that the definition of leap years is not expected to change, this should be a good trade-off. + int days = 365 * (year - 1970); + int extra_leap_days = 0; + extra_leap_days += Detail::mod_zeros_in_range<4>(begin_year, end_year); + extra_leap_days -= Detail::mod_zeros_in_range<100>(begin_year, end_year); + extra_leap_days += Detail::mod_zeros_in_range<400>(begin_year, end_year); + return days + extra_leap_days * leap_sign; } constexpr int days_since_epoch(int year, int month, int day) diff --git a/Tests/AK/TestTime.cpp b/Tests/AK/TestTime.cpp index e124d98052..94ad5bbcb3 100644 --- a/Tests/AK/TestTime.cpp +++ b/Tests/AK/TestTime.cpp @@ -317,8 +317,7 @@ TEST_CASE(years_to_days_since_epoch_points) BENCHMARK_CASE(years_to_days_since_epoch_benchmark) { - // This benchmark takes consistently about 295±1 ms on Linux, and roughly 2300 ms on Serenity. - // TODO: Computing the amount of days should never take dozens of milliseconds. + // This benchmark takes consistently "0ms" on Linux, and "0ms" on Serenity. for (size_t i = 0; i < 100; ++i) { int actual_days = years_to_days_since_epoch(-5877640); (void)actual_days; @@ -326,6 +325,70 @@ BENCHMARK_CASE(years_to_days_since_epoch_benchmark) } } +TEST_CASE(div_floor_by) +{ + EXPECT_EQ(AK::Detail::floor_div_by<4>(-5), -2); + EXPECT_EQ(AK::Detail::floor_div_by<4>(-4), -1); + EXPECT_EQ(AK::Detail::floor_div_by<4>(-3), -1); + EXPECT_EQ(AK::Detail::floor_div_by<4>(-2), -1); + EXPECT_EQ(AK::Detail::floor_div_by<4>(-1), -1); + EXPECT_EQ(AK::Detail::floor_div_by<4>(+0), +0); + EXPECT_EQ(AK::Detail::floor_div_by<4>(+1), +0); + EXPECT_EQ(AK::Detail::floor_div_by<4>(+2), +0); + EXPECT_EQ(AK::Detail::floor_div_by<4>(+3), +0); + EXPECT_EQ(AK::Detail::floor_div_by<4>(+4), +1); + EXPECT_EQ(AK::Detail::floor_div_by<4>(+5), +1); + EXPECT_EQ(AK::Detail::floor_div_by<4>(+6), +1); + EXPECT_EQ(AK::Detail::floor_div_by<4>(+7), +1); + EXPECT_EQ(AK::Detail::floor_div_by<4>(+8), +2); +} + +TEST_CASE(mod_zeros_in_range) +{ + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(0, 0), 0); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(0, 1), 1); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(0, 2), 1); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(0, 3), 1); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(0, 4), 1); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(0, 5), 2); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(0, 6), 2); + + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(1, 1), 0); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(1, 2), 0); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(1, 3), 0); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(1, 4), 0); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(1, 5), 1); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(1, 6), 1); + + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(2, 2), 0); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(2, 3), 0); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(2, 4), 0); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(2, 5), 1); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(2, 6), 1); + + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(3, 3), 0); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(3, 4), 0); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(3, 5), 1); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(3, 6), 1); + + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(4, 4), 0); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(4, 5), 1); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(4, 6), 1); + + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(5, 5), 0); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(5, 6), 0); + + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(6, 6), 0); + + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(-5, 3), 2); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(-4, 3), 2); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(-3, 3), 1); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(-2, 3), 1); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(-1, 3), 1); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(-0, 3), 1); + EXPECT_EQ(AK::Detail::mod_zeros_in_range<4>(+1, 3), 0); +} + TEST_CASE(years_to_days_since_epoch_span) { auto test_data_start_year = 1900;