AK+Tests: Add a Duration function to scale by a rational

This commit is contained in:
Zaggy1024 2026-05-06 21:13:51 -05:00 committed by Gregory Bertilson
parent 6063650261
commit 40d1957d6a
3 changed files with 88 additions and 0 deletions

View file

@ -95,6 +95,45 @@ Duration Duration::from_time_units(i64 time_units, u32 numerator, u32 denominato
return Duration(seconds, static_cast<u32>(nanoseconds));
}
Duration Duration::scaled_by(u32 numerator, u32 denominator) const
{
VERIFY(denominator != 0);
if (numerator == 0)
return Duration::zero();
if (numerator == denominator)
return *this;
if (Checked<i64>::multiplication_would_overflow(m_seconds, numerator))
return Duration(m_seconds < 0 ? NumericLimits<i64>::min() : NumericLimits<i64>::max(), 0);
auto seconds_product = m_seconds * static_cast<i64>(numerator);
auto seconds = seconds_product / static_cast<i64>(denominator);
auto seconds_remainder = seconds_product % static_cast<i64>(denominator);
if (seconds_remainder < 0) {
if (seconds == NumericLimits<i64>::min())
return Duration(NumericLimits<i64>::min(), 0);
seconds--;
seconds_remainder += denominator;
}
auto numerator_ns = seconds_remainder * 1'000'000'000;
numerator_ns += m_nanoseconds * static_cast<i64>(numerator);
auto total_ns = (numerator_ns + denominator / 2) / static_cast<i64>(denominator);
auto extra_seconds = total_ns / 1'000'000'000;
auto nanoseconds = total_ns % 1'000'000'000;
if (extra_seconds > 0) {
Checked<i64> total_seconds = seconds;
total_seconds += extra_seconds;
if (total_seconds.has_overflow())
return Duration(NumericLimits<i64>::max(), 999'999'999);
seconds = total_seconds.value();
}
VERIFY(nanoseconds >= 0);
VERIFY(nanoseconds < 1'000'000'000);
return Duration(seconds, static_cast<u32>(nanoseconds));
}
i64 Duration::to_truncated_seconds() const
{
VERIFY(m_nanoseconds < 1'000'000'000);

View file

@ -247,6 +247,7 @@ public:
[[nodiscard]] static Duration from_timespec(const struct timespec&);
[[nodiscard]] static Duration from_timeval(const struct timeval&);
[[nodiscard]] static Duration from_time_units(i64 units, u32 numerator, u32 denominator);
[[nodiscard]] Duration scaled_by(u32 numerator, u32 denominator) const;
// We don't pull in <stdint.h> for the pretty min/max definitions because this file is also included in the Kernel
[[nodiscard]] constexpr static Duration min() { return Duration(-__INT64_MAX__ - 1LL, 0); }
[[nodiscard]] constexpr static Duration zero() { return Duration(0, 0); }

View file

@ -991,3 +991,51 @@ TEST_CASE(time_units)
EXPECT_DEATH("From time units with zero numerator", (void)Duration::from_time_units(1, 0, 1));
EXPECT_DEATH("From time units with zero denominator", (void)Duration::from_time_units(1, 1, 0));
}
TEST_CASE(scaled_by)
{
// Identity and trivial cases.
EXPECT_EQ(Duration::zero().scaled_by(5, 7), Duration::zero());
EXPECT_EQ(Duration::from_seconds(7).scaled_by(0, 1), Duration::zero());
EXPECT_EQ(Duration::from_seconds(7).scaled_by(1, 1), Duration::from_seconds(7));
EXPECT_EQ(Duration::from_milliseconds(2'500).scaled_by(5, 5), Duration::from_milliseconds(2'500));
// Halving and doubling.
EXPECT_EQ(Duration::from_seconds(2).scaled_by(1, 2), Duration::from_seconds(1));
EXPECT_EQ(Duration::from_seconds(1).scaled_by(2, 1), Duration::from_seconds(2));
EXPECT_EQ(Duration::from_milliseconds(500).scaled_by(2, 1), Duration::from_seconds(1));
EXPECT_EQ(Duration::from_seconds(1).scaled_by(1, 2), Duration::from_milliseconds(500));
// Fractional scales using both seconds and nanoseconds parts.
EXPECT_EQ(Duration::from_milliseconds(2'500).scaled_by(1, 3), Duration::from_nanoseconds(833'333'333));
EXPECT_EQ(Duration::from_milliseconds(2'500).scaled_by(2, 3), Duration::from_nanoseconds(1'666'666'667));
EXPECT_EQ(Duration::from_seconds(3).scaled_by(7, 2), Duration::from_milliseconds(10'500));
EXPECT_EQ(Duration::from_milliseconds(1).scaled_by(48'000, 1'000), Duration::from_milliseconds(48));
// Negative durations: -1.5s × 2/3 → -1s exactly.
EXPECT_EQ(Duration::from_milliseconds(-1'500).scaled_by(2, 3), Duration::from_seconds(-1));
EXPECT_EQ(Duration::from_seconds(-3).scaled_by(2, 3), Duration::from_seconds(-2));
EXPECT_EQ(Duration::from_milliseconds(-500).scaled_by(3, 2), Duration::from_milliseconds(-750));
EXPECT_EQ(Duration::from_nanoseconds(-1).scaled_by(1, 1), Duration::from_nanoseconds(-1));
// Round-to-nearest at the nanosecond boundary.
EXPECT_EQ(Duration::from_nanoseconds(1).scaled_by(1, 2), Duration::from_nanoseconds(1)); // 0.5 ns rounds up
EXPECT_EQ(Duration::from_nanoseconds(1).scaled_by(1, 3), Duration::from_nanoseconds(0)); // 0.333 ns rounds down
EXPECT_EQ(Duration::from_nanoseconds(2).scaled_by(1, 3), Duration::from_nanoseconds(1)); // 0.667 ns rounds up
EXPECT_EQ(Duration::from_nanoseconds(3).scaled_by(2, 3), Duration::from_nanoseconds(2)); // 2.0 exact
EXPECT_EQ(Duration::from_nanoseconds(-1).scaled_by(1, 2), Duration::from_nanoseconds(0));
// Carrying nanoseconds past 1e9 into seconds.
EXPECT_EQ(Duration::from_nanoseconds(999'999'999).scaled_by(1, 1), Duration::from_nanoseconds(999'999'999));
EXPECT_EQ((Duration::from_seconds(0) + Duration::from_nanoseconds(999'999'999)).scaled_by(2, 1),
Duration::from_seconds(1) + Duration::from_nanoseconds(999'999'998));
// Saturation on m_seconds × numerator overflow.
EXPECT_EQ(Duration::from_seconds(NumericLimits<i64>::max()).scaled_by(2, 1),
Duration::from_seconds(NumericLimits<i64>::max()));
EXPECT_EQ(Duration::from_seconds(NumericLimits<i64>::min()).scaled_by(2, 1),
Duration::from_seconds(NumericLimits<i64>::min()));
// Death on zero denominator.
EXPECT_DEATH("Scaled by with zero denominator", (void)Duration::from_seconds(1).scaled_by(1, 0));
}