Support seamless interop between ratio and rational Magnitude

We provide two new functions, `numerator(m)` and `denominator(m)`, for a
Magnitude `m`.  They fulfill the following conditions:

1. `numerator(m)` and `denominator(m)` are always integer Magnitudes.
2. If `m` is rational, then `m == numerator(m) / denominator(m)`.

If `m` is _not_ rational, then the numerator and denominator are not
especially meaningful (there is no uniquely defined "leftover irrational
part").  However, we choose a convention that matches how humans would
write a mixed number.  For example, sqrt(27/16) would have a numerator
of 3, denominator of 4, and a "leftover part" of sqrt(3), matching the
"human" way of writing this as [(3 * sqrt(3)) / 4].  This has no use
yet, but it may later be useful in printing the Magnitude of an
anonymous Unit for end users.

To further reduce friction for the upcoming migration, we provide an
implicit conversion from a Magnitude to a `ratio`.  We restrict this
operation to rational Magnitudes, and guard this with a `static_assert`.
This commit is contained in:
Chip Hogg
2022-02-21 01:44:26 +00:00
parent b221dace3f
commit d7681e188e
4 changed files with 154 additions and 1 deletions

View File

@@ -368,6 +368,9 @@ struct magnitude {
// Whether this magnitude represents a rational number.
friend constexpr bool is_rational(const magnitude&) { return (detail::is_rational(BPs) && ...); }
// Implicit conversion to ratio.
constexpr explicit(false) operator ratio() const;
};
// Implementation for Magnitude concept (below).
@@ -392,7 +395,7 @@ template<typename T, BasePower auto... BPs>
constexpr T get_value(const magnitude<BPs...>&)
{
// Force the expression to be evaluated in a constexpr context, to catch, e.g., overflow.
constexpr auto result = detail::checked_static_cast<T>((detail::compute_base_power<T>(BPs) * ...));
constexpr auto result = detail::checked_static_cast<T>((detail::compute_base_power<T>(BPs) * ... * 1));
return result;
}
@@ -480,6 +483,53 @@ constexpr auto operator*(magnitude<H1, T1...>, magnitude<H2, T2...>)
constexpr auto operator/(Magnitude auto l, Magnitude auto r) { return l * pow<-1>(r); }
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Magnitude numerator and denominator implementation.
namespace detail {
// The largest integer which can be extracted from any magnitude with only a single basis vector.
template<BasePower auto BP>
constexpr auto integer_part(magnitude<BP>)
{
constexpr auto power_num = numerator(BP.power);
constexpr auto power_den = denominator(BP.power);
if constexpr (std::is_integral_v<decltype(BP.get_base())> && (power_num >= power_den)) {
constexpr auto largest_integer_power = [power_num, power_den](BasePower auto bp) {
bp.power = (power_num / power_den); // Note: integer division intended.
return bp;
}(BP); // Note: lambda is immediately invoked.
return magnitude<largest_integer_power>{};
} else {
return magnitude<>{};
}
}
} // namespace detail
template<BasePower auto... BPs>
constexpr auto numerator(magnitude<BPs...>)
{
return (detail::integer_part(magnitude<BPs>{}) * ... * magnitude<>{});
}
constexpr auto denominator(Magnitude auto m) { return numerator(pow<-1>(m)); }
// Implementation of implicit conversion to ratio goes here, because it needs `numerator()` and `denominator()`.
template<BasePower auto... BPs>
constexpr magnitude<BPs...>::operator ratio() const
{
static_assert(is_rational(magnitude<BPs...>{}));
return ratio{
get_value<std::intmax_t>(numerator(*this)),
get_value<std::intmax_t>(denominator(*this)),
};
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// `as_magnitude()` implementation.

View File

@@ -87,8 +87,27 @@ struct ratio {
}
[[nodiscard]] friend constexpr ratio operator/(const ratio& lhs, const ratio& rhs) { return lhs * inverse(rhs); }
[[nodiscard]] friend constexpr std::intmax_t numerator(const ratio& r)
{
std::intmax_t num = r.num;
for (auto i = r.exp; i > 0; --i) {
num *= 10;
}
return num;
}
[[nodiscard]] friend constexpr std::intmax_t denominator(const ratio& r)
{
std::intmax_t den = r.den;
for (auto i = r.exp; i < 0; ++i) {
den *= 10;
}
return den;
}
};
[[nodiscard]] constexpr ratio inverse(const ratio& r) { return ratio(r.den, r.num, -r.exp); }
[[nodiscard]] constexpr bool is_integral(const ratio& r)

View File

@@ -63,6 +63,18 @@ void check_same_type_and_value(T actual, U expected)
CHECK(actual == expected);
}
template<ratio R>
void check_ratio_round_trip_is_identity()
{
constexpr Magnitude auto m = as_magnitude<R>();
constexpr ratio round_trip = ratio{
get_value<std::intmax_t>(numerator(m)),
get_value<std::intmax_t>(denominator(m)),
};
CHECK(round_trip == R);
}
TEST_CASE("base_power")
{
SECTION("base rep deducible for integral base")
@@ -156,6 +168,15 @@ TEST_CASE("make_ratio performs prime factorization correctly")
as_magnitude<ratio(16'605'390'666'050, 10'000'000'000'000)>();
}
SECTION("Can handle prime number which would exceed GCC iteration limit")
{
// GCC 10 has a constexpr loop iteration limit of 262144. A naive algorithm, which performs trial division on 2 and
// all odd numbers up to sqrt(N), will exceed this limit for the following prime. Thus, for this test to pass, we
// need to be using a more efficient algorithm. (We could increase the limit, but we don't want users to have to
// mess with compiler flags just to compile the code.)
as_magnitude<334'524'384'739>();
}
SECTION("Can bypass computing primes by providing known_first_factor<N>")
{
// Sometimes, even wheel factorization isn't enough to handle the compilers' limits on constexpr steps and/or
@@ -351,6 +372,35 @@ TEST_CASE("can distinguish integral, rational, and irrational magnitudes")
}
}
TEST_CASE("Constructing ratio from rational magnitude")
{
SECTION("Round trip is identity")
{
// Note that not every Magnitude can be represented as a ratio. However, if we _start_ with a
// ratio, we must guarantee to recover the same ratio in a round trip.
check_ratio_round_trip_is_identity<1>();
check_ratio_round_trip_is_identity<9>();
check_ratio_round_trip_is_identity<ratio{5, 8}>();
}
SECTION("Rational magnitude implicitly converts to ratio")
{
constexpr ratio r = as_magnitude<ratio{22, 7}>();
CHECK(r == ratio{22, 7});
}
SECTION("Irrational magnitude does not convert to ratio")
{
// The following code should not compile.
// constexpr ratio radical = pow<ratio{1, 2}>(as_magnitude<2>());
// (void)radical;
// The following code should not compile.
// constexpr ratio degrees_per_radian = as_magnitude<180>() / pi_to_the<1>();
// (void)degrees_per_radian;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Detail function tests below.
@@ -374,6 +424,34 @@ TEST_CASE("int_power computes integer powers")
}
}
TEST_CASE("integer_part picks out integer part of single-basis magnitude")
{
SECTION("integer_part of non-integer base is identity magnitude")
{
CHECK(integer_part(pi_to_the<1>()) == magnitude<>{});
CHECK(integer_part(pi_to_the<-8>()) == magnitude<>{});
CHECK(integer_part(pi_to_the<ratio{3, 4}>()) == magnitude<>{});
}
SECTION("integer_part of integer base to negative power is identity magnitude")
{
CHECK(integer_part(magnitude<base_power{2, -8}>{}) == magnitude<>{});
CHECK(integer_part(magnitude<base_power{11, -1}>{}) == magnitude<>{});
}
SECTION("integer_part of integer base to fractional power is identity magnitude")
{
CHECK(integer_part(magnitude<base_power{2, ratio{1, 2}}>{}) == magnitude<>{});
}
SECTION("integer_part of integer base to power at least one takes integer part")
{
CHECK(integer_part(magnitude<base_power{2, 1}>{}) == magnitude<base_power{2, 1}>{});
CHECK(integer_part(magnitude<base_power{2, ratio{19, 10}}>{}) == magnitude<base_power{2, 1}>{});
CHECK(integer_part(magnitude<base_power{11, ratio{97, 9}}>{}) == magnitude<base_power{11, 10}>{});
}
}
TEST_CASE("Prime helper functions")
{
SECTION("multiplicity")

View File

@@ -102,4 +102,10 @@ static_assert(common_ratio(ratio(100, 1), ratio(1, 10)) == ratio(1, 10));
static_assert(common_ratio(ratio(1), ratio(1, 1, 3)) == ratio(1));
static_assert(common_ratio(ratio(10, 1, -1), ratio(1, 1, -3)) == ratio(1, 1, -3));
// numerator and denominator
static_assert(numerator(ratio(3, 4)) == 3);
static_assert(numerator(ratio(3, 7, 2)) == 300);
static_assert(denominator(ratio(3, 4)) == 4);
static_assert(denominator(ratio(3, 7, -2)) == 700);
} // namespace