From 928254ffedbf0ecfa50c80c64fa55f8d99a738ef Mon Sep 17 00:00:00 2001 From: Ryan Pavlik Date: Fri, 28 Jan 2022 17:49:38 -0600 Subject: [PATCH] a/math: Add a rational number struct template. --- src/xrt/auxiliary/CMakeLists.txt | 3 +- src/xrt/auxiliary/math/m_rational.hpp | 209 ++++++++++++++++++++++++++ tests/CMakeLists.txt | 2 + tests/tests_rational.cpp | 145 ++++++++++++++++++ 4 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 src/xrt/auxiliary/math/m_rational.hpp create mode 100644 tests/tests_rational.cpp diff --git a/src/xrt/auxiliary/CMakeLists.txt b/src/xrt/auxiliary/CMakeLists.txt index 0880c28e1..d01836282 100644 --- a/src/xrt/auxiliary/CMakeLists.txt +++ b/src/xrt/auxiliary/CMakeLists.txt @@ -50,8 +50,9 @@ add_library( math/m_predict.c math/m_predict.h math/m_quatexpmap.cpp - math/m_relation_history.h + math/m_rational.hpp math/m_relation_history.cpp + math/m_relation_history.h math/m_space.cpp math/m_space.h math/m_vec2.h diff --git a/src/xrt/auxiliary/math/m_rational.hpp b/src/xrt/auxiliary/math/m_rational.hpp new file mode 100644 index 000000000..ab8d4a79b --- /dev/null +++ b/src/xrt/auxiliary/math/m_rational.hpp @@ -0,0 +1,209 @@ +// Copyright 2022, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief A very simple rational number type. + * @author Ryan Pavlik + * @ingroup aux_math + */ + +#pragma once + +#ifndef __cplusplus +#error "This header is C++-only." +#endif + +#include +#include +#include + +namespace xrt::auxiliary::math { + +/*! + * A rational (fractional) number type. + */ +template struct Rational +{ + static_assert(std::is_integral_v, "This type is only for use with integer components."); + + using value_type = Scalar; + value_type numerator; + value_type denominator; + + /*! + * Return the rational value 1/1, the simplest unity (== 1) value. + */ + static constexpr Rational + simplestUnity() noexcept + { + return {1, 1}; + } + + /*! + * Return the reciprocal of this value. + * + * Result will have a non-negative denominator. + */ + constexpr Rational + reciprocal() const noexcept + { + return Rational{denominator, numerator}.withNonNegativeDenominator(); + } + + /*! + * Return this value, with the denominator non-negative (0 or positive). + */ + constexpr Rational + withNonNegativeDenominator() const noexcept + { + if constexpr (std::is_unsigned_v) { + // unsigned means always non-negative + return *this; + } else { + return denominator < Scalar{0} ? Rational{-numerator, -denominator} : *this; + } + } + + /*! + * Does this rational number represent a value greater than 1, with a positive denominator? + * + */ + constexpr bool + isOverUnity() const noexcept + { + return numerator > denominator && denominator > Scalar{0}; + } + + /*! + * Does this rational number represent 1? + * + * @note false if denominator is 0, even if numerator is also 0. + */ + constexpr bool + isUnity() const noexcept + { + return numerator == denominator && denominator != Scalar{0}; + } + + /*! + * Does this rational number represent 0? + * + * @note false if denominator is 0, even if numerator is also 0. + */ + constexpr bool + isZero() const noexcept + { + return numerator == Scalar{0} && denominator != Scalar{0}; + } + + /*! + * Does this rational number represent a value between 0 and 1 (exclusive), and has a positive denominator? + * + * This is the most common useful range. + */ + constexpr bool + isBetweenZeroAndOne() const noexcept + { + return denominator > Scalar{0} && numerator > Scalar{0} && numerator < denominator; + } + + /*! + * Get the complementary fraction. + * + * Only really makes sense if isBetweenZeroAndOne() is true + * + * Result will have a non-negative denominator. + */ + constexpr Rational + complement() const noexcept + { + return Rational{denominator - numerator, denominator}.withNonNegativeDenominator(); + } +}; + +/*! + * Multiplication operator. Warning: does no simplification! + * + * Result will have a non-negative denominator. + * + * @relates Rational + */ +template +constexpr Rational +operator*(const Rational &lhs, const Rational &rhs) +{ + return Rational{lhs.numerator * rhs.numerator, lhs.denominator * rhs.denominator} + .withNonNegativeDenominator(); +} + +/*! + * Multiplication operator with a scalar. Warning: does no simplification! + * + * Result will have a non-negative denominator. + * + * @relates Rational + */ +template +constexpr Rational +operator*(const Rational &lhs, const Scalar &rhs) +{ + return Rational{lhs.numerator * rhs, lhs.denominator}.withNonNegativeDenominator(); +} + +/*! + * Multiplication operator with a scalar. Warning: does no simplification! + * + * Result will have a non-negative denominator. + * + * @relates Rational + */ +template +constexpr Rational +operator*(const Scalar &lhs, const Rational &rhs) +{ + return (rhs * lhs).withNonNegativeDenominator(); +} + +/*! + * Equality comparison operator. Warning: does no simplification, looks for exact equality! + * + * @relates Rational + */ +template +constexpr bool +operator==(const Rational &lhs, const Rational &rhs) +{ + return rhs.numerator == lhs.numerator && rhs.denominator == lhs.denominator; +} + +/*! + * Division operator. Warning: does no simplification! + * + * Result will have a non-negative denominator. + * + * @relates Rational + */ +template +constexpr Rational +operator/(const Rational &lhs, const Rational &rhs) +{ + return (lhs * rhs.reciprocal()).withNonNegativeDenominator(); +} + + +/*! + * Division operator by a scalar. Warning: does no simplification! + * + * Result will have a non-negative denominator. + * + * @relates Rational + */ +template +constexpr Rational +operator/(const Rational &lhs, Scalar rhs) +{ + return (lhs * Rational{1, rhs}); +} + + +} // namespace xrt::auxiliary::math diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4bdeb4764..39f8f725c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -19,6 +19,7 @@ foreach( tests_json tests_pacing tests_quatexpmap + tests_rational ) add_executable(${testname} ${testname}.cpp) @@ -33,3 +34,4 @@ target_link_libraries(tests_cxx_wrappers PRIVATE xrt-interfaces) target_link_libraries(tests_history_buf PRIVATE aux_math) target_link_libraries(tests_input_transform PRIVATE st_oxr xrt-interfaces xrt-external-openxr) target_link_libraries(tests_quatexpmap PRIVATE aux_math) +target_link_libraries(tests_rational PRIVATE aux_math) diff --git a/tests/tests_rational.cpp b/tests/tests_rational.cpp new file mode 100644 index 000000000..45bd3a51e --- /dev/null +++ b/tests/tests_rational.cpp @@ -0,0 +1,145 @@ +// Copyright 2022, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Integer low pass filter tests. + * @author Ryan Pavlik + */ + +#include + +#include "catch/catch.hpp" + +#include +#include +#include +#include +#include + +using xrt::auxiliary::math::Rational; +namespace Catch { +template struct StringMaker> +{ + static std::string + convert(Rational const &value) + { + return std::to_string(value.numerator) + "/" + std::to_string(value.denominator); + } +}; +} // namespace Catch + +TEMPLATE_TEST_CASE("Rational", "", int32_t, uint32_t) +{ + using R = Rational; + using T = TestType; + CHECK(R{1, 1} == R::simplestUnity()); + CHECK((R::simplestUnity() * T{1}) == R::simplestUnity()); + CHECK((T{1} * R::simplestUnity()) == R::simplestUnity()); + + CHECK(R{5, 8}.reciprocal() == R{8, 5}); + + CHECK(R{5, 8}.complement() == R{3, 8}); + CHECK(R{8, 8}.complement() == R{0, 8}); + + if constexpr (std::is_signed::value) { + } + + CHECK(R{5, 8}.withNonNegativeDenominator() == R{5, 8}); + + if constexpr (std::is_signed::value) { + CHECK(R{5, -8}.withNonNegativeDenominator() == R{-5, 8}); + CHECK(R{-5, 8}.withNonNegativeDenominator() == R{-5, 8}); + + CHECK(R{-5, 8}.reciprocal() == R{-8, 5}); + CHECK(R{5, -8}.complement() == R{8 + 5, 8}); + } + + { + R val{5, 8}; + CAPTURE(val); + CHECK((R::simplestUnity() * val) == val); + CHECK((val * R::simplestUnity()) == val); + CHECK((val * T{1}) == val); + CHECK((T{1} * val) == val); + + CHECK((val * val.reciprocal()).numerator == (val * val.reciprocal()).denominator); + CHECK((val * val.reciprocal()).isUnity()); + + CHECK((val / val).numerator == (val / val).denominator); + CHECK((val / val).isUnity()); + + CHECK((val / T{1}) == val); + } + + if constexpr (std::is_signed::value) { + R val{5, -8}; + R valNonNegativeDenominator = val.withNonNegativeDenominator(); + + CAPTURE(val); + CHECK((R::simplestUnity() * val) == valNonNegativeDenominator); + CHECK((val * R::simplestUnity()) == valNonNegativeDenominator); + CHECK((val * T{1}) == valNonNegativeDenominator); + CHECK((T{1} * val) == valNonNegativeDenominator); + + CHECK((val * val.reciprocal()).numerator == (val * val.reciprocal()).denominator); + CHECK((val * val.reciprocal()).isUnity()); + + CHECK((val / val).numerator == (val / val).denominator); + CHECK((val / val).isUnity()); + + CHECK((val / T{1}) == valNonNegativeDenominator); + } + + // Check all our predicates + { + // This is divide by zero error, all should be false + R val{0, 0}; + CAPTURE(val); + CHECK_FALSE(val.isZero()); + CHECK_FALSE(val.isBetweenZeroAndOne()); + CHECK_FALSE(val.isUnity()); + CHECK_FALSE(val.isOverUnity()); + } + { + R val{0, 8}; + CAPTURE(val); + CHECK(val.isZero()); + CHECK_FALSE(val.isBetweenZeroAndOne()); + CHECK_FALSE(val.isUnity()); + CHECK_FALSE(val.isOverUnity()); + } + + { + R val{5, 8}; + CAPTURE(val); + CHECK_FALSE(val.isZero()); + CHECK(val.isBetweenZeroAndOne()); + CHECK_FALSE(val.isUnity()); + CHECK_FALSE(val.isOverUnity()); + } + + { + R val{8, 8}; + CAPTURE(val); + CHECK_FALSE(val.isZero()); + CHECK_FALSE(val.isBetweenZeroAndOne()); + CHECK(val.isUnity()); + CHECK_FALSE(val.isOverUnity()); + } + { + R val = R::simplestUnity(); + CAPTURE(val); + CHECK_FALSE(val.isZero()); + CHECK_FALSE(val.isBetweenZeroAndOne()); + CHECK(val.isUnity()); + CHECK_FALSE(val.isOverUnity()); + } + { + R val{8, 5}; + CAPTURE(val); + CHECK_FALSE(val.isZero()); + CHECK_FALSE(val.isBetweenZeroAndOne()); + CHECK_FALSE(val.isUnity()); + CHECK(val.isOverUnity()); + } +}