From 1ef16c07864e15ab0d4b7a7378cfff1118c53f37 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:39:42 +0900 Subject: [PATCH] Implement `quantity` --- include/iris/type_traits.hpp | 3 + include/iris/units/quantity.hpp | 627 +++++++++++++++++ test/CMakeLists.txt | 8 + test/units/quantity.cpp | 1153 +++++++++++++++++++++++++++++++ 4 files changed, 1791 insertions(+) create mode 100644 include/iris/units/quantity.hpp create mode 100644 test/units/quantity.cpp diff --git a/include/iris/type_traits.hpp b/include/iris/type_traits.hpp index 6f0a51f..016c670 100644 --- a/include/iris/type_traits.hpp +++ b/include/iris/type_traits.hpp @@ -37,6 +37,9 @@ concept unsigned_numeric_integral = template concept numeric_integral = signed_numeric_integral || unsigned_numeric_integral; +template +concept numeric_arithmetic = numeric_integral || std::floating_point; + template struct remove_cv { diff --git a/include/iris/units/quantity.hpp b/include/iris/units/quantity.hpp new file mode 100644 index 0000000..7f890b5 --- /dev/null +++ b/include/iris/units/quantity.hpp @@ -0,0 +1,627 @@ +#ifndef IRIS_ZZ_UNITS_QUANTITY_HPP +#define IRIS_ZZ_UNITS_QUANTITY_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include + +#include +#include +#include +#include +#include +#include +#include +#include // IWYU pragma: keep +#include + +#include // IWYU pragma: keep +#include // IWYU pragma: keep + +namespace iris::units { + +template +class quantity; + +template +struct quantity_traits; + +template class DerivedTT, numeric_arithmetic T, class... Rest> + requires + std::derived_from, quantity> && + (!std::same_as, quantity>) +struct quantity_traits> +{ + using value_type = T; + + template + using rebind = DerivedTT; +}; + +template +concept quantity_like = + requires { typename quantity_traits::value_type; } && + std::derived_from::value_type>>; + +namespace detail { + +template +concept same_quantity_family = + quantity_like && + (std::same_as< + typename quantity_traits::template rebind::value_type>, + Rest + > && ...); + +template +using common_quantity_value_t = std::common_type_t< + typename quantity_traits::value_type, + typename quantity_traits::value_type +>; + +} // detail + +// Declares the deduction guide for a quantity family, so that `DerivedClass{value}` +// deduces `DerivedClass`. +// Not required on compilers that implement . +#define IRIS_QUANTITY_DEDUCTION_GUIDE(class_name, ...) \ + template<::iris::numeric_arithmetic T> \ + class_name(T) -> class_name + +// Base class for defining a strongly-typed "quantity" class. +// +// The derived class must be a class template whose first parameter is the +// representation type (`T`), and must derive publicly from `quantity`. +// +// Marking the derived class `final` is strongly advised, since deriving from +// a derived quantity has no meaning in this model. +// +// For practical ergonomics, the derived class should provide the following: +// +// 1. `using iris::units::quantity::quantity;` +// Required. Without it, the class has no usable constructor and the +// operators fail to instantiate. +// +// 2. `IRIS_QUANTITY_DEDUCTION_GUIDE(class_name);` +// Enables construction with the syntax `DerivedClass{value}`. +// Not required on compilers that implement . +template +class quantity +{ + static_assert(numeric_arithmetic); + static_assert(std::same_as, T>); + +public: + using value_type = T; + + T value{}; + + constexpr quantity() noexcept = default; + +protected: + // Using `quantity` without deriving is prohibited as a "quantity" without + // target domain is meaningless + constexpr ~quantity() noexcept = default; + +public: + // We need to resurrect defaulted special members since we declare destructor + constexpr quantity(quantity const&) noexcept = default; + constexpr quantity(quantity&&) noexcept = default; + constexpr quantity& operator=(quantity const&) noexcept = default; + constexpr quantity& operator=(quantity&&) noexcept = default; + + // ---------------------------------------------------- + + constexpr explicit quantity(T value) noexcept + : value(value) + {} + + [[nodiscard]] constexpr explicit operator T() const noexcept + { + return value; + } + + template + requires detail::same_quantity_family && (!std::same_as) + [[nodiscard]] constexpr + explicit(!std::same_as< + detail::common_quantity_value_t, + typename quantity_traits::value_type + >) + operator Target(this Self const& self) noexcept + { + return Target{static_cast::value_type>(self.value)}; + } + + // ---------------------------------------------------- + + template + requires detail::same_quantity_family + [[nodiscard]] constexpr bool + operator==(this Self const& self, Other const& other) noexcept + { + using common = detail::common_quantity_value_t; + return static_cast(self.value) == static_cast(other.value); + } + + template + requires detail::same_quantity_family + [[nodiscard]] constexpr auto + operator<=>(this Self const& self, Other const& other) noexcept + { + using common = detail::common_quantity_value_t; + return static_cast(self.value) <=> static_cast(other.value); + } + + // ---------------------------------------------------- + + template + [[nodiscard]] constexpr Self operator+(this Self const& self) noexcept + { + return self; + } + + template + [[nodiscard]] constexpr Self operator-(this Self const& self) noexcept + { + return Self{static_cast::value_type>(-self.value)}; + } + + template + requires std::integral::value_type> + constexpr Self& operator++(this Self& self) noexcept + { + ++self.value; + return self; + } + + template + requires std::integral::value_type> + constexpr Self operator++(this Self& self, int) noexcept + { + return Self{self.value++}; + } + + template + requires std::integral::value_type> + constexpr Self& operator--(this Self& self) noexcept + { + --self.value; + return self; + } + + template + requires std::integral::value_type> + constexpr Self operator--(this Self& self, int) noexcept + { + return Self{self.value--}; + } + + // ---------------------------------------------------- + + template + requires detail::same_quantity_family + [[nodiscard]] constexpr std::common_type_t + operator+(this Self const& self, Other const& other) noexcept + { + using result = std::common_type_t; + return result{static_cast::value_type>(self.value + other.value)}; + } + + template + requires detail::same_quantity_family && + std::same_as, typename quantity_traits::value_type> + constexpr Self& operator+=(this Self& self, Other const& other) noexcept + { + self.value += other.value; + return self; + } + + template + requires detail::same_quantity_family + [[nodiscard]] constexpr std::common_type_t + operator-(this Self const& self, Other const& other) noexcept + { + using result = std::common_type_t; + return result{static_cast::value_type>(self.value - other.value)}; + } + + template + requires detail::same_quantity_family && + std::same_as, typename quantity_traits::value_type> + constexpr Self& operator-=(this Self& self, Other const& other) noexcept + { + self.value -= other.value; + return self; + } + + // ---------------------------------------------------- + + template + requires std::same_as::value_type, U>, typename quantity_traits::value_type> + constexpr Self& operator*=(this Self& self, U scalar) noexcept + { + self.value *= scalar; + return self; + } + + template + requires std::same_as::value_type, U>, typename quantity_traits::value_type> + constexpr Self& operator/=(this Self& self, U scalar) noexcept + { + self.value /= scalar; + return self; + } + + // ---------------------------------------------------- + + template + [[nodiscard]] constexpr auto operator*(this Self const& self, U scalar) noexcept + -> quantity_traits::template rebind::value_type, U>> + { + using common = std::common_type_t::value_type, U>; + return typename quantity_traits::template rebind{static_cast(self.value * scalar)}; + } + + // Scalar on the left (cannot use explicit object parameter) + template + requires std::derived_from + [[nodiscard]] friend constexpr auto operator*(U scalar, Q const& q) noexcept + -> quantity_traits::template rebind::value_type, U>> + { + using common = std::common_type_t::value_type, U>; + return typename quantity_traits::template rebind{static_cast(scalar * q.value)}; + } + + template + void operator*(this Self const&, Other const&) = delete; + + // ---------------------------------------------------- + + template + [[nodiscard]] constexpr auto operator/(this Self const& self, U scalar) noexcept + -> quantity_traits::template rebind::value_type, U>> + { + using common = std::common_type_t::value_type, U>; + return typename quantity_traits::template rebind{static_cast(self.value / scalar)}; + } + + template + requires detail::same_quantity_family + [[nodiscard]] constexpr detail::common_quantity_value_t + operator/(this Self const& self, Other const& other) noexcept + { + return static_cast>(self.value / other.value); + } + + template + requires std::derived_from + friend void operator/(U, Q const&) = delete; + + // ---------------------------------------------------- + + template + requires + std::integral::value_type> && std::integral && + std::same_as::value_type, U>, typename quantity_traits::value_type> + constexpr Self& operator%=(this Self& self, U scalar) noexcept + { + self.value %= scalar; + return self; + } + + template + requires + detail::same_quantity_family && + std::integral::value_type> && + std::integral::value_type> && + std::same_as, typename quantity_traits::value_type> + constexpr Self& operator%=(this Self& self, Other const& other) noexcept + { + self.value %= other.value; + return self; + } + + template + requires std::integral::value_type> && std::integral + [[nodiscard]] constexpr auto operator%(this Self const& self, U scalar) noexcept + -> quantity_traits::template rebind::value_type, U>> + { + using common = std::common_type_t::value_type, U>; + return typename quantity_traits::template rebind{static_cast(self.value % scalar)}; + } + + template + requires + detail::same_quantity_family && + std::integral::value_type> && + std::integral::value_type> + [[nodiscard]] constexpr std::common_type_t + operator%(this Self const& self, Other const& other) noexcept + { + using result = std::common_type_t; + return result{static_cast::value_type>(self.value % other.value)}; + } + + template + requires std::derived_from + friend void operator%(U, Q const&) = delete; +}; + +} // iris::units + +template< + template class DerivedTT, + iris::numeric_arithmetic T, iris::numeric_arithmetic U, class... Rest +> + requires + iris::units::quantity_like> && + iris::units::quantity_like> +struct std::common_type, DerivedTT> +{ + using type = DerivedTT, Rest...>; +}; + +// A plain quantity and a derived quantity have no common type +template + requires + iris::units::quantity_like && + std::derived_from> && + (!std::same_as>) +struct std::common_type, Derived> +{ + // No `::type` +}; + +// A plain quantity and a derived quantity have no common type +template + requires + iris::units::quantity_like && + std::derived_from> && + (!std::same_as>) +struct std::common_type> +{ + // No `::type` +}; + +template class DerivedTT, iris::numeric_arithmetic T, class... Rest> + requires iris::units::quantity_like> +class std::numeric_limits> : public std::numeric_limits +{ +public: + [[nodiscard]] static constexpr DerivedTT (min)() noexcept + { + return DerivedTT{(std::numeric_limits::min)()}; + } + [[nodiscard]] static constexpr DerivedTT (max)() noexcept + { + return DerivedTT{(std::numeric_limits::max)()}; + } + [[nodiscard]] static constexpr DerivedTT lowest() noexcept + { + return DerivedTT{std::numeric_limits::lowest()}; + } + [[nodiscard]] static constexpr DerivedTT epsilon() noexcept + { + return DerivedTT{std::numeric_limits::epsilon()}; + } + [[nodiscard]] static constexpr DerivedTT round_error() noexcept + { + return DerivedTT{std::numeric_limits::round_error()}; + } + [[nodiscard]] static constexpr DerivedTT infinity() noexcept + { + return DerivedTT{std::numeric_limits::infinity()}; + } + [[nodiscard]] static constexpr DerivedTT quiet_NaN() noexcept + { + return DerivedTT{std::numeric_limits::quiet_NaN()}; + } + [[nodiscard]] static constexpr DerivedTT signaling_NaN() noexcept + { + return DerivedTT{std::numeric_limits::signaling_NaN()}; + } + [[nodiscard]] static constexpr DerivedTT denorm_min() noexcept + { + return DerivedTT{std::numeric_limits::denorm_min()}; + } +}; + +namespace iris::units { + +template + requires std::floating_point::value_type> +[[nodiscard]] constexpr Q trunc(Q const& q) noexcept +{ + return Q{std::trunc(q.value)}; +} + +template + requires std::integral::value_type> +[[nodiscard]] constexpr Q trunc(Q const& q) noexcept +{ + return q; +} + +template + requires std::floating_point::value_type> +[[nodiscard]] constexpr Q floor(Q const& q) noexcept +{ + return Q{std::floor(q.value)}; +} + +template + requires std::integral::value_type> +[[nodiscard]] constexpr Q floor(Q const& q) noexcept +{ + return q; +} + +template + requires std::floating_point::value_type> +[[nodiscard]] constexpr Q ceil(Q const& q) noexcept +{ + return Q{std::ceil(q.value)}; +} + +template + requires std::integral::value_type> +[[nodiscard]] constexpr Q ceil(Q const& q) noexcept +{ + return q; +} + +template + requires std::floating_point::value_type> +[[nodiscard]] constexpr Q round(Q const& q) noexcept +{ + return Q{std::round(q.value)}; +} + +template + requires std::integral::value_type> +[[nodiscard]] constexpr Q round(Q const& q) noexcept +{ + return q; +} + +template +[[nodiscard]] constexpr Q abs(Q const& q) noexcept +{ + if constexpr (std::unsigned_integral::value_type>) { + return q; + } else { + return Q{static_cast::value_type>(std::abs(q.value))}; + } +} + +// -------------------------------------------------- + +// Returns by value, unlike `std::min`, since the result is in the common +// representation and may not be any of the arguments. `std::min` remains +// available when a reference is wanted, but only for a single representation. +template + requires detail::same_quantity_family +[[nodiscard]] constexpr std::common_type_t +(min)(A const& a, B const& b) noexcept +{ + using result = std::common_type_t; + return (std::min)(static_cast(a), static_cast(b)); +} + +// Returns by value, unlike `std::min`, since the result is in the common +// representation and may not be any of the arguments. `std::min` remains +// available when a reference is wanted, but only for a single representation. +template + requires + detail::same_quantity_family && + std::strict_weak_order const&, std::common_type_t const&> +[[nodiscard]] constexpr std::common_type_t +(min)(A const& a, B const& b, Comp&& comp) + noexcept(std::is_nothrow_invocable_v const&, std::common_type_t const&>) +{ + using result = std::common_type_t; + return (std::min)(static_cast(a), static_cast(b), std::forward(comp)); +} + +// Returns by value, unlike `std::max`, since the result is in the common +// representation and may not be any of the arguments. `std::max` remains +// available when a reference is wanted, but only for a single representation. +template + requires detail::same_quantity_family +[[nodiscard]] constexpr std::common_type_t +(max)(A const& a, B const& b) noexcept +{ + using result = std::common_type_t; + return (std::max)(static_cast(a), static_cast(b)); +} + +// Returns by value, unlike `std::max`, since the result is in the common +// representation and may not be any of the arguments. `std::max` remains +// available when a reference is wanted, but only for a single representation. +template + requires + detail::same_quantity_family && + std::strict_weak_order const&, std::common_type_t const&> +[[nodiscard]] constexpr std::common_type_t +(max)(A const& a, B const& b, Comp&& comp) + noexcept(std::is_nothrow_invocable_v const&, std::common_type_t const&>) +{ + using result = std::common_type_t; + return (std::max)(static_cast(a), static_cast(b), std::forward(comp)); +} + +// Returns by value, unlike `std::clamp`, since the result is in the common +// representation and may not be any of the arguments. `std::clamp` remains +// available when a reference is wanted, but only for a single representation. +template + requires detail::same_quantity_family +[[nodiscard]] constexpr std::common_type_t +clamp(Q const& v, Lo const& lo, Hi const& hi) +{ + using result = std::common_type_t; + return std::clamp(static_cast(v), static_cast(lo), static_cast(hi)); +} + +// Returns by value, unlike `std::clamp`, since the result is in the common +// representation and may not be any of the arguments. `std::clamp` remains +// available when a reference is wanted, but only for a single representation. +template + requires + detail::same_quantity_family && + std::strict_weak_order const&, std::common_type_t const&> +[[nodiscard]] constexpr std::common_type_t +clamp(Q const& v, Lo const& lo, Hi const& hi, Comp&& comp) + noexcept(std::is_nothrow_invocable_v const&, std::common_type_t const&>) +{ + using result = std::common_type_t; + return std::clamp(static_cast(v), static_cast(lo), static_cast(hi), std::forward(comp)); +} + +// --------------------------------------------------- + +template + requires detail::same_quantity_family +[[nodiscard]] constexpr std::common_type_t +midpoint(A a, B b) noexcept +{ + using result = std::common_type_t; + using common = quantity_traits::value_type; + return result{std::midpoint(static_cast(a.value), static_cast(b.value))}; +} + +template + requires + detail::same_quantity_family && + std::floating_point>::value_type> +[[nodiscard]] constexpr std::common_type_t +lerp(A a, B b, T t) noexcept +{ + using result = std::common_type_t; + using common = quantity_traits::value_type; + return result{std::lerp(static_cast(a.value), static_cast(b.value), static_cast(t))}; +} + +} // iris::units + +template +struct std::hash +{ + [[nodiscard]] static std::size_t operator()(Q const& q) noexcept + { + return std::hash::value_type>{}(q.value); + } +}; + +template +struct std::formatter : std::formatter::value_type, CharT> +{ + auto format(Q const& q, auto& ctx) const + { + return std::formatter::value_type, CharT>::format(q.value, ctx); + } +}; + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 136577c..7e2c38c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -207,6 +207,14 @@ if(PROJECT_IS_TOP_LEVEL) target_link_libraries(iris_marshal_test PRIVATE Iris::Marshal) + set( + IRIS_TEST_UNITS_TESTS + quantity + ) + foreach(test_name IN LISTS IRIS_TEST_UNITS_TESTS) + iris_define_internal_subdir_test(units ${test_name} ${test_name}.cpp) + endforeach() + set( IRIS_TEST_NGRAM_TESTS ngram diff --git a/test/units/quantity.cpp b/test/units/quantity.cpp new file mode 100644 index 0000000..00f073e --- /dev/null +++ b/test/units/quantity.cpp @@ -0,0 +1,1153 @@ +// SPDX-License-Identifier: MIT + +#include "iris_test.hpp" + +#include +#include + +#include + +#include +#include +#include +#include +#include + +//using iris::units::quantity; +using iris::units::quantity_like; +using iris::units::quantity_traits; + +template +concept has_plus = requires(A a, B b) { a + b; }; + +template +concept has_minus = requires(A a, B b) { a - b; }; + +template +concept has_multiply = requires(A a, B b) { a * b; }; + +template +concept has_divide = requires(A a, B b) { a / b; }; + +template +concept has_modulo = requires(A a, B b) { a % b; }; + +template +concept has_plus_assign = requires(A& a, B b) { a += b; }; + +template +concept has_minus_assign = requires(A& a, B b) { a -= b; }; + +template +concept has_multiply_assign = requires(A& a, B b) { a *= b; }; + +template +concept has_divide_assign = requires(A& a, B b) { a /= b; }; + +template +concept has_modulo_assign = requires(A& a, B b) { a %= b; }; + +template +concept has_equal = requires(A const& a, B const& b) { { a == b } -> iris::req::boolean_testable; }; + +template +concept has_less = requires(A const& a, B const& b) { { a < b } -> iris::req::boolean_testable; }; + +template +concept has_increment = requires(A& a) { ++a; a++; --a; a--; }; + +template +// ReSharper disable once CppUseTypeTraitAlias +concept has_common_type = requires { typename std::common_type::type; }; + +template +concept has_min = requires(A const& a, B const& b) { iris::units::min(a, b); }; + +template +concept has_max = requires(A const& a, B const& b) { iris::units::max(a, b); }; + +template +concept has_clamp = requires(Q const& q, Lo const& lo, Hi const& hi) { iris::units::clamp(q, lo, hi); }; + +template +concept has_clamp_comp = requires(A const& a, B const& b, C const& c, Comp comp) { iris::units::clamp(a, b, c, comp); }; + +template +concept has_midpoint = requires(A a, B b) { iris::units::midpoint(a, b); }; + +template +concept has_lerp = requires(A a, B b, T t) { iris::units::lerp(a, b, t); }; + +// --------------------------------------------------- + +template +struct my_quantity final : iris::units::quantity +{ + using iris::units::quantity::quantity; +}; +IRIS_QUANTITY_DEDUCTION_GUIDE(my_quantity); + +// --------------------------------------------------- + +TEST_CASE("basic type traits") +{ + STATIC_CHECK(!std::is_constructible_v>); // must be derived + + STATIC_CHECK(quantity_like>); + STATIC_CHECK(quantity_like>); + STATIC_CHECK(std::same_as>::value_type, float>); + STATIC_CHECK(std::same_as>::rebind, my_quantity>); + + STATIC_CHECK(!quantity_like); + STATIC_CHECK(!quantity_like); + STATIC_CHECK(!quantity_like*>); + STATIC_CHECK(!quantity_like const>); + STATIC_CHECK(!quantity_like&>); + + STATIC_CHECK(!quantity_like>); + + // ------------------------------------------------- + + STATIC_CHECK(std::same_as, my_quantity>, my_quantity>); + STATIC_CHECK(std::same_as, my_quantity>, my_quantity>); + STATIC_CHECK(std::same_as, my_quantity>, my_quantity>); + STATIC_CHECK(std::same_as, my_quantity>, my_quantity>); + STATIC_CHECK(std::same_as, my_quantity>, my_quantity>); + + STATIC_CHECK(std::same_as, my_quantity>, my_quantity>); + STATIC_CHECK(!std::is_convertible_v, my_quantity>); + STATIC_CHECK(!std::is_convertible_v, my_quantity>); + + STATIC_CHECK(std::same_as const, my_quantity&>, my_quantity>); + STATIC_CHECK(std::same_as&&, my_quantity const&>, my_quantity>); + + STATIC_CHECK(std::same_as< + std::common_type_t, my_quantity>, + decltype(my_quantity{} + my_quantity{}) + >); + STATIC_CHECK(std::same_as< + std::common_type_t, my_quantity>, + decltype(my_quantity{} + my_quantity{}) + >); + + STATIC_CHECK(std::common_with, my_quantity>); + STATIC_CHECK(std::common_with, my_quantity>); + STATIC_CHECK(std::common_with, my_quantity>); + STATIC_CHECK(std::same_as< + std::common_reference_t&, my_quantity&>, + my_quantity + >); + STATIC_CHECK(std::same_as< + std::common_reference_t const&, my_quantity const&>, + my_quantity + >); + + STATIC_CHECK(!std::common_with, int>); + STATIC_CHECK(!std::common_with, double>); +} + +TEMPLATE_TEST_CASE( + "type traits", "[units][quantity]", + int, long long, unsigned, float, double +) { + using Q = my_quantity; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(sizeof(Q) == sizeof(TestType)); + STATIC_CHECK(std::is_trivially_copyable_v); + STATIC_CHECK(std::is_standard_layout_v); + STATIC_CHECK(std::is_nothrow_default_constructible_v); + STATIC_CHECK(std::is_nothrow_copy_constructible_v); + STATIC_CHECK(std::is_nothrow_copy_assignable_v); + STATIC_CHECK(!std::is_aggregate_v); + STATIC_CHECK(std::regular); + STATIC_CHECK(std::totally_ordered); +} + +// --------------------------------------------------- + +TEMPLATE_TEST_CASE( + "construction (same type)", "[units][quantity]", + int, long long, unsigned, float, double +) { + using Q = my_quantity; + + { + constexpr Q q; + STATIC_CHECK(q.value == TestType{}); + } + { + constexpr Q q{TestType{7}}; + STATIC_CHECK(q.value == TestType{7}); + STATIC_CHECK(!std::is_convertible_v); + STATIC_CHECK(std::is_constructible_v); + } + { + constexpr auto q = my_quantity{TestType{7}}; + STATIC_CHECK(std::same_as const>); + } + { + constexpr auto q = my_quantity{Q{TestType{7}}}; + STATIC_CHECK(std::same_as); + } +} + +TEST_CASE("construction", "[units][quantity]") +{ + STATIC_CHECK(std::is_constructible_v, float>); + STATIC_CHECK(std::is_constructible_v, int>); + STATIC_CHECK(std::is_constructible_v, int>); + STATIC_CHECK(!std::is_convertible_v>); + STATIC_CHECK(!std::is_convertible_v>); + + STATIC_CHECK(std::is_constructible_v, double>); + STATIC_CHECK(std::is_constructible_v, double>); + STATIC_CHECK(std::is_constructible_v, long long>); + + STATIC_CHECK(std::is_constructible_v, int>); + STATIC_CHECK(std::is_constructible_v, unsigned>); + + STATIC_CHECK(std::is_constructible_v, bool>); + STATIC_CHECK(!std::is_constructible_v, std::nullptr_t>); + STATIC_CHECK(!std::is_constructible_v, my_quantity*>); + + { + constexpr my_quantity q{1.5f}; + STATIC_CHECK(q.value == 1.5); + } +} + +TEST_CASE("conversion", "[units][quantity]") +{ + STATIC_CHECK(std::is_convertible_v, my_quantity>); + STATIC_CHECK(std::is_convertible_v, my_quantity>); + STATIC_CHECK(std::is_convertible_v, my_quantity>); + STATIC_CHECK(std::is_convertible_v, my_quantity>); + + { + constexpr my_quantity d = my_quantity{1.5f}; + STATIC_CHECK(d.value == 1.5); + } + + STATIC_CHECK(!std::is_convertible_v, my_quantity>); + STATIC_CHECK(std::is_constructible_v, my_quantity>); + + STATIC_CHECK(!std::is_convertible_v, my_quantity>); + + STATIC_CHECK(std::is_constructible_v, my_quantity>); + STATIC_CHECK(!std::is_convertible_v, my_quantity>); + + { + constexpr my_quantity i{my_quantity{2.75}}; + STATIC_CHECK(i.value == 2); // static_cast truncates toward zero + } +} + +// --------------------------------------------------- + +TEST_CASE("comparison", "[units][quantity]") +{ + STATIC_CHECK(std::same_as{} <=> my_quantity{}), std::strong_ordering>); + STATIC_CHECK(std::same_as{} <=> my_quantity{}), std::partial_ordering>); + STATIC_CHECK(std::same_as{} <=> my_quantity{}), std::partial_ordering>); + STATIC_CHECK(std::same_as{} <=> my_quantity{}), std::strong_ordering>); +} + +// --------------------------------------------------- + +TEMPLATE_TEST_CASE( + "unary (same type)", "[units][quantity]", + short, int, long long, float, double +) { + using Q = my_quantity; + + constexpr Q q{TestType{3}}; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK((+q).value == TestType{3}); + STATIC_CHECK((-q).value == TestType{-3}); + STATIC_CHECK(-(-q) == q); +} + +TEMPLATE_TEST_CASE( + "increment / decrement", "[units][quantity]", + int, long long, unsigned +) { + using Q = my_quantity; + + STATIC_CHECK(has_increment); + + Q q{TestType{5}}; + { + Q& ref = ++q; + REQUIRE(&ref == &q); + REQUIRE(q.value == TestType{6}); + --q; + REQUIRE(q.value == TestType{5}); + } + { + Q const old = q++; + REQUIRE(old.value == TestType{5}); + REQUIRE(q.value == TestType{6}); + Q const older = q--; + REQUIRE(older.value == TestType{6}); + REQUIRE(q.value == TestType{5}); + } +} + +TEST_CASE("increment / decrement (floating point)", "[units][quantity]") +{ + STATIC_CHECK(!has_increment>); + STATIC_CHECK(!has_increment>); +} + +// --------------------------------------------------- + +TEMPLATE_TEST_CASE( + "addition / subtraction (same type)", "[units][quantity]", + int, long long, unsigned, float, double +) { + using Q = my_quantity; + + constexpr Q a{TestType{5}}; + constexpr Q b{TestType{3}}; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK((a + b).value == TestType{8}); + STATIC_CHECK((a - b).value == TestType{2}); + + { + Q q = a; + Q& ref = (q += b); + REQUIRE(&ref == &q); + REQUIRE(q.value == TestType{8}); + q -= b; + REQUIRE(q.value == TestType{5}); + } +} + +TEST_CASE("addition / subtraction", "[units][quantity]") +{ + STATIC_CHECK(std::same_as{} + my_quantity{}), my_quantity>); + STATIC_CHECK(std::same_as{} - my_quantity{}), my_quantity>); + STATIC_CHECK(std::same_as{} + my_quantity{}), my_quantity>); + STATIC_CHECK(std::same_as{} + my_quantity{}), my_quantity>); + + STATIC_CHECK((my_quantity{1.5f} + my_quantity{2.25}).value == 3.75); + STATIC_CHECK((my_quantity{1} - my_quantity{3}).value == -2LL); + + STATIC_CHECK(std::same_as{} + my_quantity{}), my_quantity>); + STATIC_CHECK(std::same_as{} - my_quantity{}), my_quantity>); + { + constexpr auto sum = my_quantity{short{1}} + my_quantity{short{2}}; + STATIC_CHECK(sum.value == short{3}); + } + + STATIC_CHECK(has_plus_assign, my_quantity>); + STATIC_CHECK(has_plus_assign, my_quantity>); + STATIC_CHECK(!has_plus_assign, my_quantity>); + STATIC_CHECK(!has_plus_assign, my_quantity>); + STATIC_CHECK(!has_minus_assign, my_quantity>); + { + my_quantity d{1.0}; + d += my_quantity{0.5f}; + REQUIRE(d.value == 1.5); + d -= my_quantity{1}; + REQUIRE(d.value == 0.5); + } + + STATIC_CHECK(!has_plus, int>); + STATIC_CHECK(!has_plus>); + STATIC_CHECK(!has_minus, double>); + STATIC_CHECK(!has_plus_assign, double>); +} + +// --------------------------------------------------- + +TEMPLATE_TEST_CASE( + "multiplication / division (scalar)", "[units][quantity]", + int, long long, unsigned, float, double +) { + using Q = my_quantity; + + constexpr Q q{TestType{6}}; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK((q * TestType{2}).value == TestType{12}); + STATIC_CHECK((TestType{2} * q).value == TestType{12}); + STATIC_CHECK((q / TestType{2}).value == TestType{3}); + + { + Q r = q; + r *= TestType{2}; + REQUIRE(r.value == TestType{12}); + r /= TestType{4}; + REQUIRE(r.value == TestType{3}); + } +} + +TEST_CASE("arithmetic ops (scalar)", "[units][quantity]") +{ + STATIC_CHECK(std::same_as{} * 2.5), my_quantity>); + STATIC_CHECK(std::same_as{}), my_quantity>); + STATIC_CHECK(std::same_as{} * 2), my_quantity>); + STATIC_CHECK(std::same_as{} / 2.0), my_quantity>); + STATIC_CHECK(std::same_as{} * short{2}), my_quantity>); + + STATIC_CHECK((my_quantity{3} * 2.5).value == 7.5); + STATIC_CHECK((my_quantity{7.5} / 2).value == 3.75); + STATIC_CHECK((my_quantity{7} / 2).value == 3); + + STATIC_CHECK(has_multiply_assign, int>); + STATIC_CHECK(has_multiply_assign, float>); + STATIC_CHECK(!has_multiply_assign, double>); + STATIC_CHECK(!has_multiply_assign, double>); + STATIC_CHECK(!has_divide_assign, float>); + { + my_quantity d{3.0}; + d *= 2; + REQUIRE(d.value == 6.0); + d /= 4.0f; + REQUIRE(d.value == 1.5); + } + + STATIC_CHECK(!has_divide>); + STATIC_CHECK(!has_divide>); + + STATIC_CHECK(!has_multiply, bool>); + STATIC_CHECK(!has_multiply>); + STATIC_CHECK(!has_divide, bool>); + STATIC_CHECK(!has_multiply_assign, bool>); +} + +// --------------------------------------------------- + +TEST_CASE("quantity / quantity", "[units][quantity]") +{ + STATIC_CHECK(std::same_as{} / my_quantity{}), int>); + STATIC_CHECK(std::same_as{} / my_quantity{}), double>); + STATIC_CHECK(std::same_as{} / my_quantity{}), double>); + STATIC_CHECK(std::same_as{} / my_quantity{}), short>); + + STATIC_CHECK(my_quantity{7.5} / my_quantity{2.5} == 3.0); + STATIC_CHECK(my_quantity{7} / my_quantity{2} == 3); + STATIC_CHECK(my_quantity{3} / my_quantity{2.0} == 1.5); + + STATIC_CHECK(!has_divide_assign, my_quantity>); +} + +TEST_CASE("quantity * quantity", "[units][quantity]") +{ + STATIC_CHECK(!has_multiply, my_quantity>); + STATIC_CHECK(!has_multiply, my_quantity>); + STATIC_CHECK(!has_multiply_assign, my_quantity>); +} + +// --------------------------------------------------- + +TEMPLATE_TEST_CASE( + "mod (integral)", "[units][quantity]", + int, long long, unsigned +) { + using Q = my_quantity; + + constexpr Q q{TestType{7}}; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK((q % TestType{3}).value == TestType{1}); + STATIC_CHECK((q % Q{TestType{3}}).value == TestType{1}); + + { + Q r = q; + r %= TestType{4}; + REQUIRE(r.value == TestType{3}); + r %= Q{TestType{2}}; + REQUIRE(r.value == TestType{1}); + } +} + +TEST_CASE("mod", "[units][quantity]") +{ + STATIC_CHECK((my_quantity{-7} % 3).value == -1); + STATIC_CHECK((my_quantity{7} % -3).value == 1); + STATIC_CHECK((my_quantity{-7} % my_quantity{3}).value == -1); + + STATIC_CHECK(std::same_as{} % 3LL), my_quantity>); + STATIC_CHECK(std::same_as{} % my_quantity{}), my_quantity>); + STATIC_CHECK(std::same_as{} % short{3}), my_quantity>); + STATIC_CHECK(has_modulo_assign, int>); + STATIC_CHECK(!has_modulo_assign, long long>); + + STATIC_CHECK(!has_modulo, int>); + STATIC_CHECK(!has_modulo, my_quantity>); + STATIC_CHECK(!has_modulo, double>); + STATIC_CHECK(!has_modulo, my_quantity>); + STATIC_CHECK(!has_modulo_assign, int>); + STATIC_CHECK(!has_modulo_assign, double>); + + STATIC_CHECK(!has_modulo>); + + STATIC_CHECK(!has_modulo, bool>); + STATIC_CHECK(!has_modulo_assign, bool>); +} + +// ----------------------------------------------- + +TEMPLATE_TEST_CASE("numeric_limits", "[units][quantity]", int, unsigned, double) +{ + using Q = my_quantity; + using limits = std::numeric_limits; + using base = std::numeric_limits; + + STATIC_CHECK(limits::is_specialized); + STATIC_CHECK(limits::digits == base::digits); + STATIC_CHECK(std::same_as); + STATIC_CHECK(limits::max().value == base::max()); + STATIC_CHECK(limits::lowest().value == base::lowest()); + STATIC_CHECK(std::numeric_limits::max() == limits::max()); +} + +// ----------------------------------------------- + +template +struct RelativeLength final : iris::units::quantity +{ + using iris::units::quantity::quantity; +}; +IRIS_QUANTITY_DEDUCTION_GUIDE(RelativeLength); + +template +struct AbsoluteLength final : iris::units::quantity +{ + using iris::units::quantity::quantity; +}; +IRIS_QUANTITY_DEDUCTION_GUIDE(AbsoluteLength); + +template +struct Count final : iris::units::quantity +{ + using iris::units::quantity::quantity; +}; +IRIS_QUANTITY_DEDUCTION_GUIDE(Count); + +TEST_CASE("derived: type traits", "[units][quantity]") +{ + STATIC_CHECK(quantity_like>); + STATIC_CHECK(quantity_like>); + STATIC_CHECK(std::same_as>::rebind, RelativeLength>); + STATIC_CHECK(std::same_as, RelativeLength>, RelativeLength>); + STATIC_CHECK(std::same_as, RelativeLength>, RelativeLength>); + STATIC_CHECK(std::same_as>::max()), RelativeLength>); + STATIC_CHECK(std::numeric_limits>::is_specialized); + STATIC_CHECK(std::numeric_limits>::max().value == std::numeric_limits::max()); + STATIC_CHECK(std::regular>); + STATIC_CHECK(std::totally_ordered>); + STATIC_CHECK(std::equality_comparable_with, RelativeLength>); + STATIC_CHECK(std::totally_ordered_with, RelativeLength>); + STATIC_CHECK(std::common_with, RelativeLength>); + STATIC_CHECK(sizeof(RelativeLength) == sizeof(double)); + STATIC_CHECK(std::is_trivially_copyable_v>); + STATIC_CHECK(!std::is_aggregate_v>); +} + +TEST_CASE("derived: construction / conversion", "[units][quantity]") +{ + constexpr auto c = RelativeLength{1.5}; + STATIC_CHECK(std::same_as const>); + STATIC_CHECK(std::same_as>); + + STATIC_CHECK(std::is_convertible_v, RelativeLength>); + STATIC_CHECK(!std::is_convertible_v, RelativeLength>); + STATIC_CHECK(std::is_constructible_v, RelativeLength>); + + // ------------------------------------------- + + // other family: never + STATIC_CHECK(!std::is_constructible_v, AbsoluteLength>); + STATIC_CHECK(!std::is_constructible_v, AbsoluteLength>); + + // within a family: common_type direction implicit, the other explicit + STATIC_CHECK(std::is_convertible_v, RelativeLength>); + STATIC_CHECK(!std::is_convertible_v, RelativeLength>); + STATIC_CHECK(std::is_constructible_v, RelativeLength>); +} + +TEST_CASE("derived: operators", "[units][quantity]") +{ + STATIC_CHECK(!has_plus, AbsoluteLength>); + STATIC_CHECK(!has_minus, RelativeLength>); + STATIC_CHECK(!has_equal, AbsoluteLength>); + STATIC_CHECK(!has_less, AbsoluteLength>); + STATIC_CHECK(!has_divide, AbsoluteLength>); + STATIC_CHECK(!has_plus_assign, AbsoluteLength>); + + STATIC_CHECK(!has_plus, my_quantity>); + STATIC_CHECK(!has_plus, RelativeLength>); + STATIC_CHECK(!has_equal, my_quantity>); + STATIC_CHECK(!has_equal, RelativeLength>); + STATIC_CHECK(!has_less, RelativeLength>); + STATIC_CHECK(!has_divide, RelativeLength>); + + STATIC_CHECK(!has_multiply, RelativeLength>); + STATIC_CHECK(!has_multiply, AbsoluteLength>); + STATIC_CHECK(!has_divide>); + STATIC_CHECK(!has_plus, double>); + STATIC_CHECK(!has_equal, double>); + + constexpr RelativeLength a{5.0}; + constexpr RelativeLength b{3.0}; + constexpr RelativeLength f{0.5f}; + + STATIC_CHECK(std::same_as>); + STATIC_CHECK(std::same_as>); + STATIC_CHECK((-a).value == -5.0); + + STATIC_CHECK(std::same_as>); + STATIC_CHECK(std::same_as>); + STATIC_CHECK(std::same_as>); + STATIC_CHECK(std::same_as>); + STATIC_CHECK((a + f).value == 5.5); + STATIC_CHECK((f - a).value == -4.5); + + STATIC_CHECK(std::same_as>); + STATIC_CHECK(std::same_as>); + STATIC_CHECK(std::same_as>); + STATIC_CHECK(std::same_as>); + STATIC_CHECK((a * 2).value == 10.0); + STATIC_CHECK((2 * a).value == 10.0); + STATIC_CHECK((a / 2).value == 2.5); + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(a / f == 10.0); + + STATIC_CHECK(a == RelativeLength{5.0}); + STATIC_CHECK(a > b); + STATIC_CHECK(f < a); + STATIC_CHECK(a > f); + STATIC_CHECK(std::same_as f), std::partial_ordering>); + + RelativeLength m = a; + STATIC_CHECK(std::same_as&>); + m += f; + REQUIRE(m.value == 5.5); + m *= 2; + REQUIRE(m.value == 11.0); + m /= 4.0f; + REQUIRE(m.value == 2.75); + STATIC_CHECK(!has_plus_assign, RelativeLength>); +} + +TEST_CASE("derived: integral", "[units][quantity]") +{ + constexpr Count n{7}; + STATIC_CHECK(has_increment>); + STATIC_CHECK(!has_increment>); + STATIC_CHECK(std::same_as>); + STATIC_CHECK(std::same_as{2}), Count>); + STATIC_CHECK((n % 3).value == 1); + STATIC_CHECK(!has_modulo>); + + Count m = n; + STATIC_CHECK(std::same_as&>); + STATIC_CHECK(std::same_as>); + ++m; + REQUIRE(m.value == 8); + m %= 3; + REQUIRE(m.value == 2); +} + +TEST_CASE("derived: common_type(quantity & derived)", "[units][quantity]") +{ + STATIC_CHECK(!has_common_type, RelativeLength>); + STATIC_CHECK(!has_common_type, my_quantity>); + STATIC_CHECK(!has_common_type const, RelativeLength&>); + STATIC_CHECK(!std::common_with, my_quantity>); + + STATIC_CHECK(std::same_as, my_quantity>, my_quantity>); + STATIC_CHECK(std::same_as, RelativeLength>, RelativeLength>); + STATIC_CHECK(std::common_with, my_quantity>); +} + +// ------------------------------------------------------- + +TEMPLATE_TEST_CASE("abs", "[units][quantity]", int, long long, float, double) +{ + using Q = my_quantity; + + STATIC_CHECK(std::same_as); + CHECK(abs(Q{TestType{3}}) == Q{TestType{3}}); + CHECK(abs(Q{TestType{-3}}) == Q{TestType{3}}); + CHECK(abs(Q{}) == Q{}); + CHECK(abs(std::numeric_limits::lowest() + Q{TestType{1}}) == std::numeric_limits::max()); +} + +TEMPLATE_TEST_CASE("abs (unsigned)", "[units][quantity]", unsigned, unsigned long long) +{ + using Q = my_quantity; + + CHECK(abs(Q{TestType{3}}) == Q{TestType{3}}); + CHECK(abs(std::numeric_limits::max()) == std::numeric_limits::max()); +} + +TEMPLATE_TEST_CASE("abs (integral promotion)", "[units][quantity]", short, unsigned short) +{ + using Q = my_quantity; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + CHECK(abs(Q{TestType{3}}) == Q{TestType{3}}); + CHECK(abs(std::numeric_limits::max()) == std::numeric_limits::max()); +} + +TEST_CASE("abs (short)", "[units][quantity]") +{ + using Q = my_quantity; + + CHECK(abs(Q{short{-3}}) == Q{short{3}}); + CHECK(abs(std::numeric_limits::lowest() + Q{short{1}}) == std::numeric_limits::max()); +} + +TEST_CASE("abs (floating point)", "[units][quantity]") +{ + using Q = my_quantity; + + CHECK(std::bit_cast(abs(Q{-0.0}).value) == 0); + CHECK(abs(std::numeric_limits::lowest()) == std::numeric_limits::max()); + CHECK(abs(-std::numeric_limits::infinity()) == std::numeric_limits::infinity()); + CHECK(std::isnan(abs(std::numeric_limits::quiet_NaN()).value)); + + STATIC_CHECK(std::same_as{})), RelativeLength>); + STATIC_CHECK(std::same_as{})), RelativeLength>); +} + +TEMPLATE_TEST_CASE("trunc (floating point)", "[units][quantity]", float, double) +{ + using Q = my_quantity; + using T = TestType; + + STATIC_CHECK(std::same_as); + + CHECK(trunc(Q{T{2.7}}) == Q{T{2}}); + CHECK(trunc(Q{T{2.2}}) == Q{T{2}}); + CHECK(trunc(Q{T{-2.7}}) == Q{T{-2}}); + CHECK(trunc(Q{T{-2.2}}) == Q{T{-2}}); + CHECK(trunc(Q{T{2}}) == Q{T{2}}); + CHECK(trunc(Q{}) == Q{}); + + CHECK(trunc(Q{T{-2.7}}) == Q{static_cast(static_cast>(Q{T{-2.7}}).value)}); + + CHECK(trunc(std::numeric_limits::infinity()) == std::numeric_limits::infinity()); + CHECK(trunc(-std::numeric_limits::infinity()) == -std::numeric_limits::infinity()); + CHECK(std::isnan(trunc(std::numeric_limits::quiet_NaN()).value)); +} + +TEST_CASE("trunc (negative zero)", "[units][quantity]") +{ + using Q = my_quantity; + + CHECK(std::bit_cast(trunc(Q{-0.5}).value) == std::bit_cast(-0.0)); + CHECK(std::bit_cast(trunc(Q{0.5}).value) == std::bit_cast(0.0)); +} + +TEMPLATE_TEST_CASE("trunc (integral)", "[units][quantity]", int, long long, unsigned, short) +{ + using Q = my_quantity; + using T = TestType; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(trunc(Q{T{7}}) == Q{T{7}}); + STATIC_CHECK(trunc(std::numeric_limits::max()) == std::numeric_limits::max()); + STATIC_CHECK(trunc(std::numeric_limits::lowest()) == std::numeric_limits::lowest()); +} + +TEST_CASE("trunc (qualified)", "[units][quantity]") +{ + using Q = my_quantity; + + CHECK(iris::units::trunc(Q{1.9}) == Q{1.0}); +} + +TEMPLATE_TEST_CASE("floor / ceil / round (floating point)", "[units][quantity]", float, double) +{ + using Q = my_quantity; + using T = TestType; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + + CHECK(floor(Q{T{2.5}}) == Q{T{2}}); + CHECK(floor(Q{T{-2.5}}) == Q{T{-3}}); + CHECK(floor(Q{T{2}}) == Q{T{2}}); + + CHECK(ceil(Q{T{2.5}}) == Q{T{3}}); + CHECK(ceil(Q{T{-2.5}}) == Q{T{-2}}); + CHECK(ceil(Q{T{2}}) == Q{T{2}}); + + CHECK(round(Q{T{2.5}}) == Q{T{3}}); + CHECK(round(Q{T{-2.5}}) == Q{T{-3}}); + CHECK(round(Q{T{2.4}}) == Q{T{2}}); + CHECK(round(Q{T{-2.6}}) == Q{T{-3}}); + CHECK(round(Q{T{2}}) == Q{T{2}}); + + CHECK(floor(Q{}) == Q{}); + CHECK(ceil(Q{}) == Q{}); + CHECK(round(Q{}) == Q{}); + + CHECK(floor(std::numeric_limits::infinity()) == std::numeric_limits::infinity()); + CHECK(ceil(-std::numeric_limits::infinity()) == -std::numeric_limits::infinity()); + CHECK(std::isnan(round(std::numeric_limits::quiet_NaN()).value)); +} + +TEMPLATE_TEST_CASE("floor / ceil / round (integral)", "[units][quantity]", int, long long, unsigned, short) +{ + using Q = my_quantity; + using T = TestType; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + + STATIC_CHECK(floor(Q{T{7}}) == Q{T{7}}); + STATIC_CHECK(ceil(Q{T{7}}) == Q{T{7}}); + STATIC_CHECK(round(Q{T{7}}) == Q{T{7}}); + STATIC_CHECK(floor(std::numeric_limits::max()) == std::numeric_limits::max()); + STATIC_CHECK(ceil(std::numeric_limits::lowest()) == std::numeric_limits::lowest()); +} + +TEST_CASE("floor / ceil / round (qualified)", "[units][quantity]") +{ + using Q = my_quantity; + + CHECK(iris::units::floor(Q{1.5}) == Q{1.0}); + CHECK(iris::units::ceil(Q{1.5}) == Q{2.0}); + CHECK(iris::units::round(Q{1.5}) == Q{2.0}); +} + +// ------------------------------------------------------- + +TEMPLATE_TEST_CASE("min / max", "[units][quantity]", int, long long, unsigned, float, double) +{ + using Q = my_quantity; + using T = TestType; + + constexpr Q a{T{2}}; + constexpr Q b{T{5}}; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(min(a, b) == a); + STATIC_CHECK(min(b, a) == a); + STATIC_CHECK(max(a, b) == b); + STATIC_CHECK(max(b, a) == b); + STATIC_CHECK(min(a, a) == a); + STATIC_CHECK(max(a, a) == a); + + // std::min / std::max keep working for a single representation. + STATIC_CHECK(std::min(a, b) == a); + STATIC_CHECK(std::max(a, b) == b); +} + +TEST_CASE("min / max (mixed representations)", "[units][quantity]") +{ + using F = my_quantity; + using D = my_quantity; + using I = my_quantity; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(min(I{3}, D{2.5}) == D{2.5}); + STATIC_CHECK(max(I{3}, D{2.5}) == D{3.0}); + STATIC_CHECK(min(F{1.5f}, D{2.0}) == D{1.5}); + STATIC_CHECK(max(D{2.0}, F{1.5f}) == D{2.0}); +} + +TEST_CASE("min / max (ties)", "[units][quantity]") +{ + using Q = my_quantity; + + // Same as std::min / std::max: the first argument is returned on a tie. + STATIC_CHECK(std::bit_cast(min(Q{-0.0}, Q{0.0}).value) == std::bit_cast(-0.0)); + STATIC_CHECK(std::bit_cast(min(Q{0.0}, Q{-0.0}).value) == std::bit_cast(0.0)); + STATIC_CHECK(std::bit_cast(max(Q{-0.0}, Q{0.0}).value) == std::bit_cast(-0.0)); + STATIC_CHECK(std::bit_cast(max(Q{0.0}, Q{-0.0}).value) == std::bit_cast(0.0)); + + // NaN: comparisons are false, so the first argument is returned. + constexpr auto nan = std::numeric_limits::quiet_NaN(); + CHECK(std::isnan(min(nan, Q{1.0}).value)); + CHECK(min(Q{1.0}, nan) == Q{1.0}); + CHECK(std::isnan(max(nan, Q{1.0}).value)); + CHECK(max(Q{1.0}, nan) == Q{1.0}); +} + +TEST_CASE("min / max (comp)", "[units][quantity]") +{ + using Q = my_quantity; + using D = my_quantity; + + // Qualified: a std:: comparator makes std an associated namespace, and for a + // single representation std::min / std::max would win overload resolution. + STATIC_CHECK(iris::units::min(Q{2}, Q{5}, std::greater<>{}) == Q{5}); + STATIC_CHECK(iris::units::max(Q{2}, Q{5}, std::greater<>{}) == Q{2}); + + // The comparator receives the common representation. + constexpr auto by_magnitude = [](D const& a, D const& b) noexcept { return abs(a) < abs(b); }; + CHECK(min(D{-7.0}, D{2.0}, by_magnitude) == D{2.0}); + CHECK(max(D{-7.0}, D{2.0}, by_magnitude) == D{-7.0}); + CHECK(max(Q{-7}, D{2.0}, by_magnitude) == D{-7.0}); +} + +TEST_CASE("min / max (families)", "[units][quantity]") +{ + STATIC_CHECK(has_min, my_quantity>); + STATIC_CHECK(!has_min, RelativeLength>); + STATIC_CHECK(!has_max, my_quantity>); + STATIC_CHECK(!has_max, double>); + STATIC_CHECK(iris::units::max(my_quantity{1}, my_quantity{2}) == my_quantity{2}); +} + +// ------------------------------------------------------- + +TEMPLATE_TEST_CASE("clamp", "[units][quantity]", int, long long, unsigned, float, double) +{ + using Q = my_quantity; + using T = TestType; + + constexpr Q lo{T{2}}; + constexpr Q hi{T{5}}; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(clamp(Q{T{1}}, lo, hi) == lo); + STATIC_CHECK(clamp(Q{T{2}}, lo, hi) == lo); + STATIC_CHECK(clamp(Q{T{3}}, lo, hi) == Q{T{3}}); + STATIC_CHECK(clamp(Q{T{5}}, lo, hi) == hi); + STATIC_CHECK(clamp(Q{T{7}}, lo, hi) == hi); + STATIC_CHECK(clamp(Q{T{3}}, lo, lo) == lo); + + STATIC_CHECK(std::clamp(Q{T{7}}, lo, hi) == hi); +} + +TEST_CASE("clamp (mixed)", "[units][quantity]") +{ + using F = my_quantity; + using D = my_quantity; + using I = my_quantity; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(clamp(I{1}, D{2.5}, D{4.5}) == D{2.5}); + STATIC_CHECK(clamp(F{3.0f}, D{2.5}, D{4.5}) == D{3.0}); + STATIC_CHECK(clamp(D{9.0}, F{2.5f}, I{4}) == D{4.0}); +} + +TEST_CASE("clamp (floating point)", "[units][quantity]") +{ + using Q = my_quantity; + + constexpr Q lo{-1.0}; + constexpr Q hi{1.0}; + CHECK(std::isnan(clamp(std::numeric_limits::quiet_NaN(), lo, hi).value)); + STATIC_CHECK(clamp(std::numeric_limits::infinity(), lo, hi) == hi); + STATIC_CHECK(clamp(-std::numeric_limits::infinity(), lo, hi) == lo); +} + +TEST_CASE("clamp (families)", "[units][quantity]") +{ + STATIC_CHECK(has_clamp, my_quantity, my_quantity>); + STATIC_CHECK(!has_clamp, RelativeLength, my_quantity>); + STATIC_CHECK(!has_clamp, my_quantity, RelativeLength>); + STATIC_CHECK(!has_clamp, double, double>); + STATIC_CHECK(iris::units::clamp(my_quantity{9}, my_quantity{0}, my_quantity{5}) == my_quantity{5}); +} + +TEMPLATE_TEST_CASE("clamp (comp)", "[units][quantity]", int, long long, float, double) +{ + using Q = my_quantity; + using T = TestType; + + constexpr Q lo{T{2}}; + constexpr Q hi{T{5}}; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(iris::units::clamp(Q{T{1}}, hi, lo, std::greater{}) == lo); + STATIC_CHECK(iris::units::clamp(Q{T{3}}, hi, lo, std::greater{}) == Q{T{3}}); + STATIC_CHECK(iris::units::clamp(Q{T{7}}, hi, lo, std::greater{}) == hi); + + constexpr auto by_magnitude = [](Q const& a, Q const& b) { return abs(a) < abs(b); }; + CHECK(clamp(Q{T{-7}}, Q{T{-2}}, Q{T{5}}, by_magnitude) == Q{T{5}}); + CHECK(clamp(Q{T{-1}}, Q{T{-2}}, Q{T{5}}, by_magnitude) == Q{T{-2}}); + CHECK(clamp(Q{T{3}}, Q{T{-2}}, Q{T{5}}, by_magnitude) == Q{T{3}}); +} + +TEST_CASE("clamp (comp, mixed)", "[units][quantity]") +{ + using F = my_quantity; + using D = my_quantity; + using I = my_quantity; + + constexpr auto less_d = [](D const& a, D const& b) { return a < b; }; + STATIC_CHECK(std::same_as); + STATIC_CHECK(clamp(I{1}, F{2.5f}, D{4.5}, less_d) == D{2.5}); + STATIC_CHECK(clamp(I{9}, F{2.5f}, D{4.5}, less_d) == D{4.5}); + STATIC_CHECK(has_clamp_comp>); + STATIC_CHECK(!has_clamp_comp, std::less<>>); + STATIC_CHECK(!has_clamp_comp); +} + +// ------------------------------------------------------- + +TEMPLATE_TEST_CASE("midpoint", "[units][quantity]", int, long long, unsigned, short, float, double) +{ + using Q = my_quantity; + using T = TestType; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(midpoint(Q{T{2}}, Q{T{6}}) == Q{T{4}}); + STATIC_CHECK(midpoint(Q{T{6}}, Q{T{2}}) == Q{T{4}}); + STATIC_CHECK(midpoint(Q{T{3}}, Q{T{3}}) == Q{T{3}}); + STATIC_CHECK(midpoint(Q{}, Q{}) == Q{}); + + STATIC_CHECK(midpoint(std::numeric_limits::max(), std::numeric_limits::max()) == std::numeric_limits::max()); + STATIC_CHECK(midpoint(std::numeric_limits::lowest(), std::numeric_limits::lowest()) == std::numeric_limits::lowest()); +} + +TEMPLATE_TEST_CASE("midpoint (integral halfway)", "[units][quantity]", int, long long, short) +{ + using Q = my_quantity; + using T = TestType; + + STATIC_CHECK(midpoint(Q{T{2}}, Q{T{5}}) == Q{T{3}}); + STATIC_CHECK(midpoint(Q{T{5}}, Q{T{2}}) == Q{T{4}}); + STATIC_CHECK(midpoint(Q{T{-2}}, Q{T{-5}}) == Q{T{-3}}); + STATIC_CHECK(midpoint(Q{T{-5}}, Q{T{-2}}) == Q{T{-4}}); + STATIC_CHECK(midpoint(std::numeric_limits::lowest(), std::numeric_limits::max()) == Q{T{-1}}); + STATIC_CHECK(midpoint(std::numeric_limits::max(), std::numeric_limits::lowest()) == Q{}); +} + +TEST_CASE("midpoint (mixed)", "[units][quantity]") +{ + using F = my_quantity; + using D = my_quantity; + using I = my_quantity; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(midpoint(I{2}, D{5.0}) == D{3.5}); + STATIC_CHECK(midpoint(F{1.0f}, D{2.0}) == D{1.5}); + + STATIC_CHECK(has_midpoint); + STATIC_CHECK(!has_midpoint>); + STATIC_CHECK(!has_midpoint); + STATIC_CHECK(iris::units::midpoint(I{1}, I{3}) == I{2}); +} + +TEMPLATE_TEST_CASE("lerp", "[units][quantity]", float, double) +{ + using Q = my_quantity; + using T = TestType; + + constexpr Q a{T{1}}; + constexpr Q b{T{5}}; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(lerp(a, b, T{0}) == a); + STATIC_CHECK(lerp(a, b, T{1}) == b); + STATIC_CHECK(lerp(a, b, T{0.5}) == Q{T{3}}); + STATIC_CHECK(lerp(a, b, T{0.25}) == Q{T{2}}); + STATIC_CHECK(lerp(b, a, T{0.25}) == Q{T{4}}); + + STATIC_CHECK(lerp(a, b, T{2}) == Q{T{9}}); + STATIC_CHECK(lerp(a, b, T{-1}) == Q{T{-3}}); + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(lerp(a, b, 1) == b); +} + +TEST_CASE("lerp (mixed)", "[units][quantity]") +{ + using F = my_quantity; + using D = my_quantity; + using I = my_quantity; + + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(lerp(I{0}, D{10.0}, 0.5) == D{5.0}); + STATIC_CHECK(lerp(F{0.0f}, D{10.0}, 0.25) == D{2.5}); + + STATIC_CHECK(has_lerp); + STATIC_CHECK(!has_lerp); + STATIC_CHECK(!has_lerp, my_quantity, float>); + STATIC_CHECK(!has_lerp, double>); + STATIC_CHECK(!has_lerp); + STATIC_CHECK(iris::units::lerp(D{0.0}, D{2.0}, 0.5) == D{1.0}); +} + +TEST_CASE("lerp (floating point)", "[units][quantity]") +{ + using Q = my_quantity; + CHECK(lerp(Q{1.0}, std::numeric_limits::infinity(), 0.5) == std::numeric_limits::infinity()); +} + +// ------------------------------------------------------- + +TEST_CASE("hash", "[units][quantity]") +{ + using Q = my_quantity; + CHECK(std::hash{}(Q{3.14}) == std::hash{}(3.14)); +} + +// ------------------------------------------------------- + +TEMPLATE_TEST_CASE("format", "[units][quantity]", int, long long, unsigned, short, float, double) +{ + using Q = my_quantity; + + STATIC_CHECK(std::formattable); + STATIC_CHECK(std::formattable); + + CHECK(std::format("{}", Q{TestType{7}}) == std::format("{}", TestType{7})); + CHECK(std::format("{:>6}", Q{TestType{7}}) == std::format("{:>6}", TestType{7})); + CHECK(std::format("{:<6}|", Q{TestType{7}}) == std::format("{:<6}|", TestType{7})); + CHECK(std::format("{:+}", Q{TestType{7}}) == std::format("{:+}", TestType{7})); + CHECK(std::format(L"{}", Q{TestType{7}}) == std::format(L"{}", TestType{7})); +} + +TEST_CASE("format (integral specs)", "[units][quantity]") +{ + using Q = my_quantity; + + CHECK(std::format("{:04}", Q{7}) == "0007"); + CHECK(std::format("{:x}", Q{255}) == "ff"); + CHECK(std::format("{:#b}", Q{5}) == "0b101"); + CHECK(std::format("{}", Q{-3}) == "-3"); +} + +TEST_CASE("format (floating point specs)", "[units][quantity]") +{ + using Q = my_quantity; + + CHECK(std::format("{}", Q{1.5}) == "1.5"); + CHECK(std::format("{:.2f}", Q{1.5}) == "1.50"); + CHECK(std::format("{:e}", Q{1.5}) == "1.500000e+00"); + CHECK(std::format("{:8.3f}|", Q{1.5}) == " 1.500|"); + CHECK(std::format("{}", Q{-0.0}) == "-0"); +} + +TEST_CASE("format (argument forms)", "[units][quantity]") +{ + using Q = my_quantity; + + Q const q{2.5}; + CHECK(std::format("{0} {0}", q) == "2.5 2.5"); + CHECK(std::format("{:.1f} and {}", q, Q{1.0}) == "2.5 and 1"); + CHECK(std::vformat("{:.1f}", std::make_format_args(q)) == "2.5"); +}