diff --git a/src/core/include/units/magnitude.h b/src/core/include/units/magnitude.h index 94140397..eae55206 100644 --- a/src/core/include/units/magnitude.h +++ b/src/core/include/units/magnitude.h @@ -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 constexpr T get_value(const magnitude&) { // Force the expression to be evaluated in a constexpr context, to catch, e.g., overflow. - constexpr auto result = detail::checked_static_cast((detail::compute_base_power(BPs) * ...)); + constexpr auto result = detail::checked_static_cast((detail::compute_base_power(BPs) * ... * 1)); return result; } @@ -480,6 +483,53 @@ constexpr auto operator*(magnitude, magnitude) 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 +constexpr auto integer_part(magnitude) +{ + constexpr auto power_num = numerator(BP.power); + constexpr auto power_den = denominator(BP.power); + + if constexpr (std::is_integral_v && (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{}; + } else { + return magnitude<>{}; + } +} + +} // namespace detail + +template +constexpr auto numerator(magnitude) +{ + return (detail::integer_part(magnitude{}) * ... * 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 +constexpr magnitude::operator ratio() const +{ + static_assert(is_rational(magnitude{})); + + return ratio{ + get_value(numerator(*this)), + get_value(denominator(*this)), + }; +} + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // `as_magnitude()` implementation. diff --git a/src/core/include/units/ratio.h b/src/core/include/units/ratio.h index 1e35b47e..d892825a 100644 --- a/src/core/include/units/ratio.h +++ b/src/core/include/units/ratio.h @@ -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) diff --git a/test/unit_test/runtime/magnitude_test.cpp b/test/unit_test/runtime/magnitude_test.cpp index 9db7b696..b7a75b65 100644 --- a/test/unit_test/runtime/magnitude_test.cpp +++ b/test/unit_test/runtime/magnitude_test.cpp @@ -63,6 +63,18 @@ void check_same_type_and_value(T actual, U expected) CHECK(actual == expected); } +template +void check_ratio_round_trip_is_identity() +{ + constexpr Magnitude auto m = as_magnitude(); + constexpr ratio round_trip = ratio{ + get_value(numerator(m)), + get_value(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(); } + 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") { // 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(); + } + + SECTION("Rational magnitude implicitly converts to ratio") + { + constexpr ratio r = as_magnitude(); + CHECK(r == ratio{22, 7}); + } + + SECTION("Irrational magnitude does not convert to ratio") + { + // The following code should not compile. + // constexpr ratio radical = pow(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()) == magnitude<>{}); + } + + SECTION("integer_part of integer base to negative power is identity magnitude") + { + CHECK(integer_part(magnitude{}) == magnitude<>{}); + CHECK(integer_part(magnitude{}) == magnitude<>{}); + } + + SECTION("integer_part of integer base to fractional power is identity magnitude") + { + CHECK(integer_part(magnitude{}) == magnitude<>{}); + } + + SECTION("integer_part of integer base to power at least one takes integer part") + { + CHECK(integer_part(magnitude{}) == magnitude{}); + CHECK(integer_part(magnitude{}) == magnitude{}); + CHECK(integer_part(magnitude{}) == magnitude{}); + } +} + TEST_CASE("Prime helper functions") { SECTION("multiplicity") diff --git a/test/unit_test/static/ratio_test.cpp b/test/unit_test/static/ratio_test.cpp index 9eac9963..4abe5094 100644 --- a/test/unit_test/static/ratio_test.cpp +++ b/test/unit_test/static/ratio_test.cpp @@ -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