MathUtil: Add Rational class template.

This commit is contained in:
Jordan Woyak 2025-08-25 18:46:58 -05:00
parent 6edd00e55e
commit 27c7aa78c9
4 changed files with 454 additions and 0 deletions

View File

@ -124,6 +124,7 @@ add_library(common
QoSSession.h
Random.cpp
Random.h
Rational.h
Result.h
ScopeGuard.h
SDCardUtil.cpp

View File

@ -0,0 +1,312 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cmath>
#include <concepts>
#include <limits>
#include <numeric>
#include <utility>
#include "Common/MathUtil.h"
namespace MathUtil
{
template <std::integral T>
class Rational;
namespace detail
{
template <typename T>
concept RationalCompatible = requires(T x) { Rational(x); };
template <typename T>
concept RationalAdjacent = std::is_arithmetic_v<T>;
// Allows for class template argument deduction within the class body.
// Note: CTAD via alias template would be cleaner but was not implemented until Clang 19.
template <typename Other>
constexpr auto DeducedRational(Other other)
{
return Rational{other};
}
} // namespace detail
// Rational number class template
// Results are not currently automatically reduced.
// Watch out for integer overflow after repeated operations.
// Use Reduced / Approximated functions as needed to avoid overflow.
template <std::integral T>
class Rational final
{
public:
using ValueType = T;
ValueType numerator{0};
ValueType denominator{1};
// Conversion from integers
constexpr Rational(T num = 0, T den = 1) : numerator{num}, denominator{den} {}
// Conversion from floating point
// Properly converts nice values like 0.5f -> Rational(1, 2)
// Inexact values like 0.3f won't convert particularly nicely.
// But e.g. Rational(0.3f).Approximated(10) will produce Rational(3, 10)
constexpr explicit Rational(std::floating_point auto float_value)
{
constexpr int dest_exp = IntLog2(std::numeric_limits<T>::max()) - 1;
int exp = 0;
const auto norm_frac = std::frexp(float_value, &exp);
const int num_exp = std::max(exp, dest_exp);
numerator = SaturatingCast<T>(std::ldexp(norm_frac, num_exp));
const int zeros = std::countr_zero(std::make_unsigned_t<T>(numerator));
const int den_exp = num_exp - exp;
const int shift_right = std::min(den_exp, zeros);
denominator = SaturatingCast<T>(std::ldexp(1, den_exp - shift_right));
numerator >>= shift_right;
}
// Conversion from other Rational.
template <std::integral Other>
constexpr explicit Rational(Rational<Other> other)
{
if constexpr (std::is_unsigned_v<T> && std::is_signed_v<Other>)
other = other.Normalized();
numerator = SaturatingCast<T>(other.numerator);
denominator = SaturatingCast<T>(other.denominator);
}
// Potentially lossy conversion to int/float types
template <detail::RationalAdjacent Target>
constexpr explicit operator Target() const
{
return Target(numerator) / Target(denominator);
}
constexpr bool IsInteger() const { return (numerator % denominator) == 0; }
constexpr Rational Inverted() const { return Rational(denominator, numerator); }
// Returns a copy with a non-negative denominator.
constexpr Rational Normalized() const
{
return (denominator < T{0}) ? Rational(T{} - numerator, T{} - denominator) : *this;
}
// Returns a reduced fraction.
constexpr Rational Reduced() const
{
const auto gcd = std::gcd(numerator, denominator);
return {T(numerator / gcd), T(denominator / gcd)};
}
// Returns a reduced approximated fraction with a given maximum numerator/denominator.
constexpr Rational Approximated(T max_num_den) const
{
// This algorithm comes from FFmpeg's av_reduce function (LGPLv2.1+)
const auto reduced_normalized = Reduced().Normalized();
auto [num, den] = reduced_normalized;
const bool is_negative = num < T{0};
if (is_negative)
num *= -1;
if (num <= max_num_den && den <= max_num_den)
return reduced_normalized;
Rational a0{0, 1};
Rational a1{1, 0};
while (den != 0)
{
auto x = num / den;
const auto next_den = num - (den * x);
const Rational a2 = (Rational(x, x) * a1) & a0;
if (a2.numerator > max_num_den || a2.denominator > max_num_den)
{
if (a1.numerator != 0)
x = (max_num_den - a0.numerator) / a1.numerator;
if (a1.denominator != 0)
x = std::min(x, (max_num_den - a0.denominator) / a1.denominator);
if (den * (2 * x * a1.denominator + a0.denominator) > num * a1.denominator)
a1 = {(Rational(x, x) * a1) & a0};
break;
}
a0 = a1;
a1 = a2;
num = den;
den = next_den;
}
return is_negative ? -a1 : a1;
}
// Multiplication
constexpr auto& operator*=(detail::RationalCompatible auto rhs)
{
const auto r = CommonRational(rhs);
numerator *= r.numerator;
denominator *= r.denominator;
return *this;
}
constexpr friend auto operator*(detail::RationalCompatible auto lhs, Rational rhs)
{
return CommonRational(lhs) *= rhs;
}
constexpr friend auto operator*(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs * CommonRational(rhs);
}
// Division
constexpr auto& operator/=(detail::RationalCompatible auto rhs)
{
return *this *= CommonRational(rhs).Inverted();
}
constexpr friend auto operator/(detail::RationalCompatible auto lhs, Rational rhs)
{
return CommonRational(lhs) *= rhs.Inverted();
}
constexpr friend auto operator/(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs * CommonRational(rhs).Inverted();
}
// Modulo (behaves like fmod)
constexpr auto& operator%=(detail::RationalCompatible auto rhs)
{
const auto r = CommonRational(rhs);
return *this -= (r * T(*this / r));
}
constexpr friend auto operator%(detail::RationalCompatible auto lhs, Rational rhs)
{
return CommonRational(lhs) %= rhs;
}
constexpr friend auto operator%(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs % CommonRational(rhs);
}
// Addition
constexpr auto& operator+=(detail::RationalCompatible auto rhs)
{
const auto r = CommonRational(rhs);
numerator *= r.denominator;
numerator += r.numerator * denominator;
denominator *= r.denominator;
return *this;
}
constexpr friend auto operator+(detail::RationalCompatible auto lhs, Rational rhs)
{
return CommonRational(lhs) += rhs;
}
constexpr friend auto operator+(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs + CommonRational(rhs);
}
// Subtraction
constexpr auto& operator-=(detail::RationalCompatible auto rhs)
{
const auto r = CommonRational(rhs);
numerator *= r.denominator;
numerator -= r.numerator * denominator;
denominator *= r.denominator;
return *this;
}
constexpr friend auto operator-(detail::RationalCompatible auto lhs, Rational rhs)
{
return CommonRational(lhs) -= rhs;
}
constexpr friend auto operator-(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs - CommonRational(rhs);
}
// Mediant (n1+n2)/(d1+d2)
constexpr auto& operator&=(detail::RationalCompatible auto rhs)
{
const auto r = CommonRational(rhs);
numerator += r.numerator;
denominator += r.denominator;
return *this;
}
constexpr friend auto operator&(detail::RationalCompatible auto lhs, Rational rhs)
{
return CommonRational(lhs) &= rhs;
}
constexpr friend auto operator&(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs & CommonRational(rhs);
}
// Comparison
constexpr friend auto operator<=>(detail::RationalCompatible auto lhs, Rational rhs)
{
const auto left = detail::DeducedRational(lhs).Normalized();
const auto lhs_q = left.numerator / left.denominator;
const auto lhs_r = left.numerator % left.denominator;
rhs = rhs.Normalized();
const auto rhs_q = rhs.numerator / rhs.denominator;
const auto rhs_r = rhs.numerator % rhs.denominator;
// If integer division results differ we have a result.
if (const auto cmp = Compare3Way(lhs_q, rhs_q); std::is_neq(cmp))
return cmp;
// If at least one side has no remainder we have a result.
if (lhs_r == 0 || rhs_r == 0)
return Compare3Way(lhs_r, rhs_r);
// Recurse with inverted remainders.
return Rational(rhs.denominator, rhs_r) <=> decltype(left)(left.denominator, lhs_r);
}
constexpr friend auto operator<=>(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs <=> detail::DeducedRational(rhs);
}
// Equality
constexpr friend bool operator==(detail::RationalCompatible auto lhs, Rational rhs)
{
return std::is_eq(lhs <=> rhs);
}
constexpr friend bool operator==(Rational lhs, detail::RationalAdjacent auto rhs)
{
return std::is_eq(lhs <=> rhs);
}
// Unary operators
constexpr auto operator+() const { return *this; }
constexpr auto operator-() const { return Rational(T{} - numerator, denominator); }
constexpr auto& operator++() { return *this += 1; }
constexpr auto& operator--() { return *this -= 1; }
constexpr auto operator++(int) { return std::exchange(*this, *this + 1); }
constexpr auto operator--(int) { return std::exchange(*this, *this - 1); }
constexpr explicit operator bool() const { return numerator != 0; }
private:
// Constructs a common_type'd Rational<T> from an existing value.
static constexpr auto CommonRational(auto val)
{
return Rational<
std::common_type_t<T, typename decltype(detail::DeducedRational(val))::ValueType>>(val);
}
};
// Floating point deduction guides.
Rational(float) -> Rational<s32>;
Rational(double) -> Rational<s64>;
} // namespace MathUtil

View File

@ -152,6 +152,7 @@
<ClInclude Include="Common\Projection.h" />
<ClInclude Include="Common\QoSSession.h" />
<ClInclude Include="Common\Random.h" />
<ClInclude Include="Common\Rational.h" />
<ClInclude Include="Common\Result.h" />
<ClInclude Include="Common\scmrev.h" />
<ClInclude Include="Common\ScopeGuard.h" />

View File

@ -4,6 +4,7 @@
#include <gtest/gtest.h>
#include "Common/MathUtil.h"
#include "Common/Rational.h"
TEST(MathUtil, IntLog2)
{
@ -202,5 +203,144 @@ TEST(MathUtil, RectangleGetHeightUnsigned)
EXPECT_EQ(rect_e.GetHeight(), u32{0xFFFFFFF8});
}
TEST(MathUtil, Rational)
{
using MathUtil::Rational;
// Integer
const auto r5 = 65 * Rational{8, 13} / 8;
EXPECT_TRUE(r5.IsInteger());
EXPECT_EQ(r5, 5);
EXPECT_EQ(int(r5), 5);
EXPECT_EQ(r5, Rational{10} * 0.5f);
EXPECT_NE(r5, Rational(6));
// Non-Integer
const auto r5_2 = Rational(5, 2);
EXPECT_FALSE(r5_2.IsInteger());
EXPECT_EQ(int(r5_2), 2);
EXPECT_EQ(r5_2, 12.5f / r5);
// True/False
EXPECT_TRUE(r5_2);
EXPECT_FALSE(r5_2 * 0);
EXPECT_FALSE(!r5_2);
// Negative values
EXPECT_EQ(Rational(-4, -3), Rational(4, 3));
EXPECT_EQ(Rational(-1, 10), Rational(1, -10));
EXPECT_TRUE(Rational(-5, 1).IsInteger());
EXPECT_TRUE(Rational(5, -1).IsInteger());
EXPECT_NE(r5, -r5);
// Conversion to/from float
const Rational r3p5(3.5);
EXPECT_EQ(r3p5.numerator, 7);
EXPECT_EQ(r3p5.denominator, 2);
const Rational neg3p5(-3.5);
EXPECT_EQ(neg3p5.numerator, -7);
EXPECT_EQ(neg3p5.denominator, 2);
EXPECT_EQ(float(r5_2), 2.5);
EXPECT_EQ(float(-r5_2), -2.5f);
EXPECT_EQ(r5_2, Rational(2.5f));
EXPECT_EQ(-r5_2, Rational(-2.5));
EXPECT_NE(r5_2, Rational(2.500001f));
// Fraction reduction
const Rational r15_6{15, 6};
EXPECT_EQ(r15_6, r5_2);
const auto f15_6_reduced = r15_6.Reduced();
EXPECT_EQ(f15_6_reduced.numerator, 5);
EXPECT_EQ(f15_6_reduced.denominator, 2);
// Approximations
EXPECT_EQ(Rational(0.3).Approximated(1000'000), Rational(3, 10));
EXPECT_EQ(Rational(3, 10).Approximated(9), Rational(2, 7));
EXPECT_EQ(Rational(-33, 100).Approximated(20), Rational(-1, 3));
EXPECT_EQ(Rational(0.33).Approximated(10), Rational(1, 3));
EXPECT_EQ(Rational(1, -100).Approximated(10), Rational(0, 1));
EXPECT_EQ(Rational(6, -100).Approximated(10), Rational(-1, 10));
EXPECT_EQ(Rational(101).Approximated(20), Rational(20, 1));
EXPECT_EQ(Rational<s16>(std::numeric_limits<s16>::max()).Approximated(18), Rational(18, 1));
constexpr auto s64_max = std::numeric_limits<s64>::max();
EXPECT_EQ(Rational<s64>(-s64_max).Approximated(13), Rational(-13, 1));
EXPECT_EQ(Rational<s64>(1, s64_max).Approximated(s64_max), Rational<s64>(1, s64_max));
// Addition/Subtraction
EXPECT_EQ(-r5_2 + -r15_6, Rational(-5));
EXPECT_EQ(2.5f - -r15_6, Rational(5));
EXPECT_EQ(+r3p5 - 2, 1.5f);
EXPECT_EQ(r3p5 - 7, -3.5);
EXPECT_EQ(r3p5 - u8(2), 1.5);
EXPECT_EQ(r3p5 - 3ull, 0.5);
EXPECT_EQ(r3p5 - Rational<u64>(1), 2.5);
EXPECT_EQ(Rational<u8>(6) - 5.5f, 0.5);
EXPECT_EQ(Rational<u8>(6) + Rational(-5, -5), 7);
EXPECT_EQ(Rational<u8>(6) - Rational<s64>(3, -3), 7);
// Inc/Dec
Rational f7_3_inc{7, 3};
EXPECT_EQ(f7_3_inc++, 2 + Rational(1, 3));
EXPECT_EQ(++f7_3_inc, 4 + Rational(1, 3));
EXPECT_EQ(f7_3_inc--, Rational(1, 3) + 4);
EXPECT_EQ(--f7_3_inc, Rational(1, 3) + 2);
// Multiplication/Division
EXPECT_EQ(r5_2 * 3, Rational(7.5));
EXPECT_EQ(r5_2 * r5_2, 6.25);
EXPECT_EQ(7 / r15_6, 2 + Rational(8, 10));
EXPECT_EQ(r3p5 / r15_6, 12 - Rational(106, 10));
EXPECT_EQ(r3p5 / 2, 1.75);
EXPECT_EQ(int(r3p5 / 2), 1);
EXPECT_EQ(Rational(-1, -3) * 2ull, Rational(2, 3));
auto ru77 = Rational(77u);
ru77 /= Rational(-7, -1);
EXPECT_EQ(ru77, 11);
// Modulo
EXPECT_EQ(r3p5 % 2, 1.5f);
EXPECT_EQ(r3p5 % -2, Rational(3, 2));
EXPECT_EQ(-r3p5 % 2, Rational(3, -2));
EXPECT_EQ(-r3p5 % -2, -1.5f);
// Mediant
EXPECT_EQ(r5_2 & Rational(2, 1), Rational(7, 3));
EXPECT_EQ(Rational(11, 5) & Rational(3, -1), Rational(14, 4));
// Comparison
EXPECT_TRUE(Rational(-5, 101) < 0);
EXPECT_TRUE(Rational(5, -101) < 0u);
EXPECT_TRUE(Rational(-5, -101) > 0);
EXPECT_TRUE(Rational(7, 5) > Rational(7, 6));
EXPECT_TRUE(Rational(1, 3) < Rational(-2, -3));
EXPECT_TRUE(Rational(0.5) != Rational(2, 2));
EXPECT_TRUE(Rational(10, 3) == 3 + Rational(2, 6));
EXPECT_TRUE(3 >= Rational(6, 2));
EXPECT_TRUE(Rational(6, 2) < 4.0);
// Conversions use SaturatingCast
EXPECT_EQ(Rational<u32>(Rational(-5)), 0);
EXPECT_EQ(Rational<u8>(Rational(1, 1000)), Rational(1, 255));
EXPECT_EQ(Rational<u32>(-9.f), 0);
EXPECT_EQ(Rational<s16>(std::pow(2.1, 18.0)), std::numeric_limits<s16>::max());
EXPECT_EQ(Rational<s16>(-std::pow(2.1, 18.0)), std::numeric_limits<s16>::min());
// Smallest positive non-zero float produces the smallest positive non-zero Rational.
EXPECT_EQ(Rational<s16>(std::numeric_limits<double>::denorm_min()),
Rational<s16>(1, std::numeric_limits<s16>::max()));
// Mixing types uses common_type
const auto big_result = s64(-1000) * Rational<u16>{3000, 3};
static_assert(std::is_same_v<decltype(big_result), const Rational<s64>>);
EXPECT_TRUE(big_result == -1000'000);
EXPECT_EQ(Rational<u32>(Rational(-6, -2)), 3);
EXPECT_EQ(Rational(5u) * Rational(-1), 0);
EXPECT_EQ(Rational(5u) * Rational(-1ll), -5);
// Works at compile time
static_assert(Rational(8, 2) + Rational(-6, 123) + 4 == Rational(326, 41));
}
// TODO: Add unit test coverage for `Rectangle::ClampUL`. (And consider removing
// `Rectangle::ClampLL`, which does not have any callers.)