diff --git a/src/core/include/units/magnitude.h b/src/core/include/units/magnitude.h new file mode 100644 index 00000000..8e9c46ca --- /dev/null +++ b/src/core/include/units/magnitude.h @@ -0,0 +1,360 @@ +// The MIT License (MIT) +// +// Copyright (c) 2018 Mateusz Pusz +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include +#include +#include + +namespace units { + +/** + * @brief Any type which can be used as a basis vector in a BasePower. + * + * We have two categories. + * + * The first is just an `int`. This is for prime number bases. These can always be used directly as NTTPs. + * + * The second category is a _custom type_, which has a static member variable of type `long double` that holds its + * value. We choose `long double` to get the greatest degree of precision; users who need a different type can convert + * from this at compile time. This category is for any irrational base we admit into our representation (on which, more + * details below). + * + * The reason we can't hold the value directly for floating point bases is so that we can support some compilers (e.g., + * GCC 10) which don't yet permit floating point NTTPs. + */ +template +concept BaseRep = std::is_same_v || std::is_same_v, long double>; + +/** + * @brief A basis vector in our magnitude representation, raised to some rational power. + * + * The public API is that there is a `power` member variable (of type `ratio`), and a `get_base()` member function (of + * type either `int` or `long double`, as appropriate), for any specialization. + * + * These types exist to be used as NTTPs for the variadic `magnitude<...>` template. We represent a magnitude (which is + * a positive real number) as the product of rational powers of "basis vectors", where each "basis vector" is a positive + * real number. "Addition" in this vector space corresponds to _multiplying_ two real numbers. "Scalar multiplication" + * corresponds to _raising_ a real number to a _rational power_. Thus, this representation of positive real numbers + * maps them onto a vector space over the rationals, supporting the operations of products and rational powers. + * + * (Note that this is the same representation we already use for dimensions.) + * + * As in any vector space, the set of basis vectors must be linearly independent: that is, no product of basis powers + * can ever give the null vector (which in this case represents the number 1), unless all scalars (again, in this case, + * _powers_) are zero. To achieve this, we use the following kinds of basis vectors. + * - Prime numbers. (These are the only allowable values for `int` base.) + * - Certain selected irrational numbers, such as pi. + * + * Before allowing a new irrational base, make sure that it _cannot_ be represented as the product of rational powers of + * _existing_ bases, including both prime numbers and any other irrational bases. For example, even though `sqrt(2)` is + * irrational, we must not ever use it as a base; instead, we would use `base_power{2, ratio{1, 2}}`. + */ +template +struct base_power { + // The rational power to which the base is raised. + ratio power{1}; + + constexpr long double get_base() const { return T::value; } +}; + +/** + * @brief Specialization for prime number bases. + */ +template<> +struct base_power { + // The value of the basis "vector". Must be prime to be used with `magnitude` (below). + int base; + + // The rational power to which the base is raised. + ratio power{1}; + + constexpr int get_base() const { return base; } +}; + +/** + * @brief Deduction guides for base_power: only permit deducing integral bases. + */ +template U> +base_power(T, U) -> base_power; +template +base_power(T) -> base_power; + +// Implementation for BasePower concept (below). +namespace detail { +template +static constexpr bool is_base_power = false; +template +static constexpr bool is_base_power> = true; +} // namespace detail + +/** + * @brief Concept to detect whether a _type_ is a valid base power. + * + * Note that this is somewhat incomplete. We must also detect whether a _value_ of that type is valid for use with + * `magnitude<...>`. We will defer that second check to the constraints on the `magnitude` template. + */ +template +concept BasePower = detail::is_base_power; + +/** + * @brief Equality detection for two base powers. + */ +template +constexpr bool operator==(T t, U u) { + return std::is_same_v && (t.get_base() == u.get_base()) && (t.power == u.power); +} + +/** + * @brief A BasePower, raised to a rational power E. + */ +constexpr auto pow(BasePower auto bp, ratio p) { + bp.power = bp.power * p; + return bp; +} + +// A variety of implementation detail helpers. +namespace detail { + +// Find the smallest prime factor of `n`. +constexpr std::intmax_t find_first_factor(std::intmax_t n) +{ + for (std::intmax_t f = 2; f * f <= n; f += 1 + (f % 2)) + { + if (n % f == 0) { return f; } + } + return n; +} + +// The exponent of `factor` in the prime factorization of `n`. +constexpr std::intmax_t multiplicity(std::intmax_t factor, std::intmax_t n) +{ + std::intmax_t m = 0; + while (n % factor == 0) + { + n /= factor; + ++m; + } + return m; +} + +// Divide a number by a given base raised to some power. +// +// Undefined unless base > 1, pow >= 0, and (base ^ pow) evenly divides n. +constexpr std::intmax_t remove_power(std::intmax_t base, std::intmax_t pow, std::intmax_t n) +{ + while (pow-- > 0) { n /= base; } + return n; +} + +// A way to check whether a number is prime at compile time. +constexpr bool is_prime(std::intmax_t n) { return (n > 1) && (find_first_factor(n) == n); } + +constexpr bool is_valid_base_power(const BasePower auto &bp) { + if (bp.power == 0) { return false; } + + if constexpr (std::is_same_v) { return is_prime(bp.get_base()); } + else { return bp.get_base() > 0; } +} + +// A function object to apply a predicate to all consecutive pairs of values in a sequence. +template +struct pairwise_all { + Predicate predicate; + + template + constexpr bool operator()(Ts&&... ts) const { + // Carefully handle different sizes, avoiding unsigned integer underflow. + constexpr auto num_comparisons = [](auto num_elements) { + return (num_elements > 1) ? (num_elements - 1) : 0; + }(sizeof...(Ts)); + + // Compare zero or more pairs of neighbours as needed. + return [this](std::tuple &&t, std::index_sequence) { + return (predicate(std::get(t), std::get(t)) && ...); + }(std::make_tuple(std::forward(ts)...), std::make_index_sequence()); + } +}; + +// Deduction guide: permit constructions such as `pairwise_all{std::less{}}`. +template +pairwise_all(T) -> pairwise_all; + +// Check whether a sequence of (possibly heterogeneously typed) values are strictly increasing. +template + requires ((std::is_signed_v && ...)) +constexpr bool strictly_increasing(Ts&&... ts) { + return pairwise_all{std::less{}}(std::forward(ts)...); +} + +template +static constexpr bool all_base_powers_valid = (is_valid_base_power(BPs) && ...); + +template +static constexpr bool all_bases_in_order = strictly_increasing(BPs.get_base()...); + +template +static constexpr bool is_base_power_pack_valid = all_base_powers_valid && all_bases_in_order; +} // namespace detail + +/** + * @brief A representation for positive real numbers which optimizes taking products and rational powers. + * + * Magnitudes can be treated as values. Each type encodes exactly one value. Users can multiply, divide, raise to + * rational powers, and compare for equality. + */ +template + requires (detail::is_base_power_pack_valid) +struct magnitude {}; + +// Implementation for Magnitude concept (below). +namespace detail { +template +static constexpr bool is_magnitude = false; +template +static constexpr bool is_magnitude> = true; +} // namespace detail + +/** + * @brief Concept to detect whether T is a valid Magnitude. + */ +template +concept Magnitude = detail::is_magnitude; + +/** + * @brief Convert any positive integer to a Magnitude. + * + * This will be the main way end users create Magnitudes. They should rarely (if ever) create a magnitude<...> by + * manually adding base powers. + */ +template + requires (R.num > 0) +constexpr Magnitude auto as_magnitude(); + +/** + * @brief A base to represent pi. + */ +struct pi_base { + static constexpr long double value = std::numbers::pi_v; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Magnitude equality implementation. + +template +constexpr bool operator==(magnitude, magnitude) { + if constexpr (sizeof...(LeftBPs) == sizeof...(RightBPs)) { return ((LeftBPs == RightBPs) && ...); } + else { return false; } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Magnitude rational powers implementation. + +template +constexpr auto pow(magnitude) { + if constexpr (E == 0) { return magnitude<>{}; } + else { return magnitude{}; } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Magnitude product implementation. + +// Base cases, for when either (or both) inputs are the identity. +constexpr auto operator*(magnitude<>, magnitude<>) { return magnitude<>{}; } +constexpr auto operator*(magnitude<>, Magnitude auto m) { return m; } +constexpr auto operator*(Magnitude auto m, magnitude<>) { return m; } + +// Recursive case for the product of any two non-identity Magnitudes. +template +constexpr auto operator*(magnitude, magnitude) { + // Case for when H1 has the smaller base. + if constexpr(H1.get_base() < H2.get_base()){ + if constexpr (sizeof...(T1) == 0) { + // Shortcut for the "pure prepend" case, which makes it easier to implement some of the other cases. + return magnitude{}; + } else { + return magnitude

{} * (magnitude{} * magnitude{}); + } + } + + // Case for when H2 has the smaller base. + if constexpr(H1.get_base() > H2.get_base()){ + return magnitude

{} * (magnitude{} * magnitude{}); + } + + // "Same leading base" case. + if constexpr (H1.get_base() == H2.get_base()) { + constexpr auto partial_product = magnitude{} * magnitude{}; + + // Make a new base_power with the common base of H1 and H2, whose power is their powers' sum. + constexpr auto new_head = [&](auto head) { + head.power = H1.power + H2.power; + return head; + }(H1); + + if constexpr (new_head.power == 0) { + return partial_product; + } else { + return magnitude{} * partial_product; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Magnitude quotient implementation. + +constexpr auto operator/(Magnitude auto l, Magnitude auto r) { return l * pow<-1>(r); } + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// `as_magnitude()` implementation. + +namespace detail { +// Helper to perform prime factorization at compile time. +template + requires (N > 0) +struct prime_factorization { + static constexpr int first_base = find_first_factor(N); + static constexpr std::intmax_t first_power = multiplicity(first_base, N); + static constexpr std::intmax_t remainder = remove_power(first_base, first_power, N); + + static constexpr auto value = magnitude{} + * prime_factorization::value; +}; + +// Specialization for the prime factorization of 1 (base case). +template<> +struct prime_factorization<1> { static constexpr magnitude<> value{}; }; + +template +static constexpr auto prime_factorization_v = prime_factorization::value; +} // namespace detail + +template + requires (R.num > 0) +constexpr Magnitude auto as_magnitude() { + return pow(detail::prime_factorization_v<10>) + * detail::prime_factorization_v + / detail::prime_factorization_v; +} + +} // namespace units diff --git a/src/core/include/units/ratio.h b/src/core/include/units/ratio.h index dcb4956b..caf2571f 100644 --- a/src/core/include/units/ratio.h +++ b/src/core/include/units/ratio.h @@ -52,7 +52,7 @@ struct ratio { std::intmax_t den; std::intmax_t exp; - constexpr explicit ratio(std::intmax_t n, std::intmax_t d = 1, std::intmax_t e = 0): num(n), den(d), exp(e) + constexpr explicit(false) ratio(std::intmax_t n, std::intmax_t d = 1, std::intmax_t e = 0): num(n), den(d), exp(e) { gsl_Expects(den != 0); detail::normalize(num, den, exp); @@ -60,6 +60,27 @@ struct ratio { [[nodiscard]] friend constexpr bool operator==(const ratio&, const ratio&) = default; + [[nodiscard]] friend constexpr ratio operator-(const ratio& r) + { + return ratio(-r.num, r.den, r.exp); + } + + [[nodiscard]] friend constexpr ratio operator+(ratio lhs, ratio rhs) + { + // First, get the inputs into a common exponent. + const auto common_exp = std::min(lhs.exp, rhs.exp); + auto commonify = [common_exp](ratio &r) { + while (r.exp > common_exp) { + r.num *= 10; + --r.exp; + } + }; + commonify(lhs); + commonify(rhs); + + return ratio{lhs.num * rhs.den + lhs.den * rhs.num, lhs.den * rhs.den, common_exp}; + } + [[nodiscard]] friend constexpr ratio operator*(const ratio& lhs, const ratio& rhs) { const std::intmax_t gcd1 = std::gcd(lhs.num, rhs.den); diff --git a/test/unit_test/runtime/CMakeLists.txt b/test/unit_test/runtime/CMakeLists.txt index 4cbb821c..4ed735e7 100644 --- a/test/unit_test/runtime/CMakeLists.txt +++ b/test/unit_test/runtime/CMakeLists.txt @@ -27,6 +27,7 @@ find_package(Catch2 CONFIG REQUIRED) add_executable(unit_tests_runtime catch_main.cpp math_test.cpp + magnitude_test.cpp fmt_test.cpp fmt_units_test.cpp distribution_test.cpp diff --git a/test/unit_test/runtime/magnitude_test.cpp b/test/unit_test/runtime/magnitude_test.cpp new file mode 100644 index 00000000..653a6ef8 --- /dev/null +++ b/test/unit_test/runtime/magnitude_test.cpp @@ -0,0 +1,387 @@ +// The MIT License (MIT) +// +// Copyright (c) 2018 Mateusz Pusz +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include +#include +#include +#include + +namespace units { + +// A set of non-standard bases for testing purposes. +struct noninteger_base { static constexpr long double value = 1.234L; }; +struct noncanonical_two_base { static constexpr long double value = 2.0L; }; +struct other_noncanonical_two_base { static constexpr long double value = 2.0L; }; +struct invalid_zero_base { static constexpr long double value = 0.0L; }; +struct invalid_negative_base { static constexpr long double value = -1.234L; }; + +template +constexpr auto pi_to_the() { return magnitude{Power}>{}; } + +TEST_CASE("base_power") +{ + SECTION("base rep deducible for integral base") + { + CHECK(base_power{2} == base_power{2, ratio{1}}); + CHECK(base_power{2, 3} == base_power{2, ratio{3}}); + CHECK(base_power{2, ratio{3, 4}} == base_power{2, ratio{3, 4}}); + } + + SECTION("get_base retrieves base for integral base") + { + CHECK(base_power{2}.get_base() == 2); + CHECK(base_power{3, 5}.get_base() == 3); + CHECK(base_power{5, ratio{1, 3}}.get_base() == 5); + } + + SECTION("get_base retrieves member value for non-integer base") + { + CHECK(base_power{}.get_base() == 1.234L); + CHECK(base_power{2}.get_base() == 1.234L); + CHECK(base_power{ratio{5, 8}}.get_base() == 1.234L); + } + + SECTION("same-base values not equal if types are different") + { + const auto a = base_power{}; + const auto b = base_power{2}; + const auto c = base_power{}; + + REQUIRE(a.get_base() == b.get_base()); + CHECK(a != b); + + REQUIRE(a.get_base() == c.get_base()); + CHECK(a != c); + } + + SECTION("same-type values not equal if bases are different") + { + CHECK(base_power{2} != base_power{3}); + CHECK(base_power{2, ratio{5, 4}} != base_power{3, ratio{5, 4}}); + } + + SECTION("same-type, same-base values not equal if powers are different") + { + CHECK(base_power{2} != base_power{2, 2}); + CHECK(base_power{} != base_power{ratio{1, 3}}); + } + + SECTION("product with inverse equals identity") + { + auto check_product_with_inverse_is_identity = [] (auto x) { + CHECK(x * pow<-1>(x) == as_magnitude<1>()); + }; + + check_product_with_inverse_is_identity(as_magnitude<3>()); + check_product_with_inverse_is_identity(as_magnitude()); + check_product_with_inverse_is_identity(pi_to_the()); + } + + SECTION("pow() multiplies exponent") + { + CHECK(pow(base_power{2}, 0) == base_power{2, 0}); + CHECK(pow(base_power{2, 3}, ratio{-1, 2}) == base_power{2, ratio{-3, 2}}); + CHECK(pow(base_power{ratio{3, 2}}, ratio{1, 3}) == base_power{ratio{1, 2}}); + } +} + +TEST_CASE("make_ratio performs prime factorization correctly") +{ + SECTION("Performs prime factorization when denominator is 1") + { + CHECK(as_magnitude<1>() == magnitude<>{}); + CHECK(as_magnitude<2>() == magnitude{}); + CHECK(as_magnitude<3>() == magnitude{}); + CHECK(as_magnitude<4>() == magnitude{}); + + CHECK(as_magnitude<792>() == magnitude{}); + } + + SECTION("Supports fractions") + { + CHECK(as_magnitude() == magnitude{}); + } + + SECTION("Supports nonzero exp") + { + constexpr ratio r{3, 1, 2}; + REQUIRE(r.exp == 2); + CHECK(as_magnitude() == as_magnitude<300>()); + } +} + +TEST_CASE("Equality works for magnitudes") +{ + SECTION("Equivalent ratios are equal") + { + CHECK(as_magnitude<1>() == as_magnitude<1>()); + CHECK(as_magnitude<3>() == as_magnitude<3>()); + CHECK(as_magnitude() == as_magnitude()); + } + + SECTION("Different ratios are unequal") + { + CHECK(as_magnitude<3>() != as_magnitude<5>()); + CHECK(as_magnitude<3>() != as_magnitude()); + } + + SECTION("Supports constexpr") + { + constexpr auto eq = (as_magnitude() == as_magnitude()); + CHECK(!eq); + } +} + +TEST_CASE("Multiplication works for magnitudes") +{ + SECTION("Reciprocals reduce to null magnitude") + { + CHECK(as_magnitude() * as_magnitude() == as_magnitude<1>()); + } + + SECTION("Products work as expected") + { + CHECK(as_magnitude() * as_magnitude() == as_magnitude()); + } + + SECTION("Products handle pi correctly") + { + CHECK( + pi_to_the<1>() * as_magnitude() * pi_to_the() == + magnitude{ratio{1, 2}}>{}); + } + + SECTION("Supports constexpr") + { + constexpr auto p = as_magnitude() * as_magnitude(); + CHECK(p == as_magnitude()); + } +} + +TEST_CASE("Division works for magnitudes") +{ + SECTION("Dividing anything by itself reduces to null magnitude") + { + CHECK(as_magnitude() / as_magnitude() == as_magnitude<1>()); + CHECK(as_magnitude<15>() / as_magnitude<15>() == as_magnitude<1>()); + } + + SECTION("Quotients work as expected") + { + CHECK(as_magnitude() / as_magnitude() == as_magnitude()); + } + + SECTION("Supports constexpr") + { + constexpr auto q = as_magnitude() / as_magnitude(); + CHECK(q == as_magnitude()); + } +} + +TEST_CASE("Can raise Magnitudes to rational powers") +{ + SECTION("Anything to the 0 is 1") { + CHECK(pow<0>(as_magnitude<1>()) == as_magnitude<1>()); + CHECK(pow<0>(as_magnitude<123>()) == as_magnitude<1>()); + CHECK(pow<0>(as_magnitude()) == as_magnitude<1>()); + CHECK(pow<0>(pi_to_the()) == as_magnitude<1>()); + } + + SECTION("Anything to the 1 is itself") { + CHECK(pow<1>(as_magnitude<1>()) == as_magnitude<1>()); + CHECK(pow<1>(as_magnitude<123>()) == as_magnitude<123>()); + CHECK(pow<1>(as_magnitude()) == as_magnitude()); + CHECK(pow<1>(pi_to_the()) == pi_to_the()); + } + + SECTION("Can raise to arbitrary rational power") { + CHECK(pow(pi_to_the()) == pi_to_the()); + } +} + +namespace detail { + +TEST_CASE("Prime helper functions") +{ + SECTION("find_first_factor()") { + CHECK(find_first_factor(1) == 1); + CHECK(find_first_factor(2) == 2); + CHECK(find_first_factor(4) == 2); + CHECK(find_first_factor(6) == 2); + CHECK(find_first_factor(15) == 3); + CHECK(find_first_factor(17) == 17); + } + + SECTION("multiplicity") { + CHECK(multiplicity(2, 8) == 3); + CHECK(multiplicity(2, 1024) == 10); + CHECK(multiplicity(11, 6655) == 3); + } + + SECTION("remove_power()") { + CHECK(remove_power(17, 0, 5) == 5); + CHECK(remove_power(2, 3, 24) == 3); + CHECK(remove_power(11, 3, 6655) == 5); + } +} + +TEST_CASE("Prime factorization") +{ + SECTION("1 factors into the null magnitude") + { + CHECK(prime_factorization_v<1> == magnitude<>{}); + } + + SECTION("Prime numbers factor into themselves") + { + CHECK(prime_factorization_v<2> == magnitude{}); + CHECK(prime_factorization_v<3> == magnitude{}); + CHECK(prime_factorization_v<5> == magnitude{}); + CHECK(prime_factorization_v<7> == magnitude{}); + CHECK(prime_factorization_v<11> == magnitude{}); + + CHECK(prime_factorization_v<41> == magnitude{}); + } + + SECTION("Prime factorization finds factors and multiplicities") + { + CHECK(prime_factorization_v<792> == + magnitude{}); + } +} + +TEST_CASE("is_prime detects primes") +{ + SECTION("Non-positive numbers are not prime") + { + CHECK(!is_prime(-1328)); + CHECK(!is_prime(-1)); + CHECK(!is_prime(0)); + } + + SECTION("1 is not prime") + { + CHECK(!is_prime(1)); + } + + SECTION("Discriminates between primes and non-primes") + { + CHECK(is_prime(2)); + CHECK(is_prime(3)); + CHECK(!is_prime(4)); + CHECK(is_prime(5)); + CHECK(!is_prime(6)); + CHECK(is_prime(7)); + CHECK(!is_prime(8)); + CHECK(!is_prime(9)); + + CHECK(is_prime(7919)); + } +} + +TEST_CASE("is_valid_base_power") +{ + SECTION("0 power is invalid") { + REQUIRE(is_valid_base_power(base_power{2})); + CHECK(!is_valid_base_power(base_power{2, 0})); + + REQUIRE(is_valid_base_power(base_power{41})); + CHECK(!is_valid_base_power(base_power{41, 0})); + + REQUIRE(is_valid_base_power(base_power{})); + CHECK(!is_valid_base_power(base_power{0})); + } + + SECTION("non-prime integers are invalid") { + CHECK(!is_valid_base_power(base_power{-8})); + CHECK(!is_valid_base_power(base_power{0})); + CHECK(!is_valid_base_power(base_power{1})); + + CHECK(is_valid_base_power(base_power{2})); + CHECK(is_valid_base_power(base_power{3})); + + CHECK(!is_valid_base_power(base_power{4})); + } + + SECTION("non-positive floating point bases are invalid") { + CHECK(!is_valid_base_power(base_power{})); + CHECK(!is_valid_base_power(base_power{})); + } +} + +TEST_CASE("pairwise_all evaluates all pairs") +{ + const auto all_pairs_return_true = pairwise_all{[](auto, auto){ return true; }}; + const auto all_pairs_return_false = pairwise_all{[](auto, auto){ return false; }}; + const auto all_increasing = pairwise_all{std::less{}}; + + SECTION("always true for empty tuples") + { + CHECK(all_pairs_return_true()); + CHECK(all_pairs_return_false()); + } + + SECTION("always true for single-element tuples") + { + CHECK(all_pairs_return_true(1)); + CHECK(all_pairs_return_false(3.14)); + CHECK(all_pairs_return_true('x')); + } + + SECTION("true for longer tuples iff true for all neighbouring pairs") + { + CHECK(all_increasing(1, 1.5)); + CHECK(all_increasing(1, 1.5, 2)); + + CHECK(!all_increasing(1, 2.0, 2)); + CHECK(!all_increasing(1, 2.5, 2)); + + CHECK(all_pairs_return_true('c', 1, 8.9, 42u)); + CHECK(!all_pairs_return_false('c', 1, 8.9, 42u)); + } +} + +TEST_CASE("strictly_increasing") +{ + SECTION("Empty input is sorted") + { + CHECK(strictly_increasing()); + } + + SECTION("Single-element input is sorted") + { + CHECK(strictly_increasing(3)); + CHECK(strictly_increasing(15.42)); + CHECK(strictly_increasing('c')); + } + + SECTION("Multi-value inputs compare correctly") + { + CHECK(strictly_increasing(3, 3.14)); + CHECK(!strictly_increasing(3, 3.0)); + CHECK(!strictly_increasing(4, 3.0)); + } +} + +} // namespace detail + +} // namespace units diff --git a/test/unit_test/static/ratio_test.cpp b/test/unit_test/static/ratio_test.cpp index 5067d3fa..32de5f10 100644 --- a/test/unit_test/static/ratio_test.cpp +++ b/test/unit_test/static/ratio_test.cpp @@ -40,6 +40,13 @@ static_assert(ratio(4) * ratio(1, 2) == ratio(2)); static_assert(ratio(1, 8) * ratio(2) == ratio(1, 4)); static_assert(ratio(1, 2) * ratio(8) == ratio(4)); +// ratio negation +static_assert(-ratio(3, 8) == ratio(-3, 8)); + +// ratio addition +static_assert(ratio(1, 2) + ratio(1, 3) == ratio(5, 6)); +static_assert(ratio(1, 3, 2) + ratio(11, 6) == ratio(211, 6)); // 100/3 + 11/6 + // multiply with exponents static_assert(ratio(1, 8, 2) * ratio(2, 1, 4) == ratio(1, 4, 6)); static_assert(ratio(1, 2, -4) * ratio(8, 1, 3) == ratio(4, 1, -1));