a/math: Add a rational number struct template.

This commit is contained in:
Ryan Pavlik 2022-01-28 17:49:38 -06:00
parent 4b94d83c11
commit 928254ffed
4 changed files with 358 additions and 1 deletions

View file

@ -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

View file

@ -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 <ryan.pavlik@collabora.com>
* @ingroup aux_math
*/
#pragma once
#ifndef __cplusplus
#error "This header is C++-only."
#endif
#include <type_traits>
#include <limits>
#include <stdexcept>
namespace xrt::auxiliary::math {
/*!
* A rational (fractional) number type.
*/
template <typename Scalar> struct Rational
{
static_assert(std::is_integral_v<Scalar>, "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<Scalar>
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<Scalar>) {
// 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 <typename Scalar>
constexpr Rational<Scalar>
operator*(const Rational<Scalar> &lhs, const Rational<Scalar> &rhs)
{
return Rational<Scalar>{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 <typename Scalar>
constexpr Rational<Scalar>
operator*(const Rational<Scalar> &lhs, const Scalar &rhs)
{
return Rational<Scalar>{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 <typename Scalar>
constexpr Rational<Scalar>
operator*(const Scalar &lhs, const Rational<Scalar> &rhs)
{
return (rhs * lhs).withNonNegativeDenominator();
}
/*!
* Equality comparison operator. Warning: does no simplification, looks for exact equality!
*
* @relates Rational
*/
template <typename Scalar>
constexpr bool
operator==(const Rational<Scalar> &lhs, const Rational<Scalar> &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 <typename Scalar>
constexpr Rational<Scalar>
operator/(const Rational<Scalar> &lhs, const Rational<Scalar> &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 <typename Scalar>
constexpr Rational<Scalar>
operator/(const Rational<Scalar> &lhs, Scalar rhs)
{
return (lhs * Rational<Scalar>{1, rhs});
}
} // namespace xrt::auxiliary::math

View file

@ -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)

145
tests/tests_rational.cpp Normal file
View file

@ -0,0 +1,145 @@
// Copyright 2022, Collabora, Ltd.
// SPDX-License-Identifier: BSL-1.0
/*!
* @file
* @brief Integer low pass filter tests.
* @author Ryan Pavlik <ryan.pavlik@collabora.com>
*/
#include <math/m_rational.hpp>
#include "catch/catch.hpp"
#include <iostream>
#include <chrono>
#include <sstream>
#include <iomanip>
#include <queue>
using xrt::auxiliary::math::Rational;
namespace Catch {
template <typename T> struct StringMaker<Rational<T>>
{
static std::string
convert(Rational<T> 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<TestType>;
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<TestType>::value) {
}
CHECK(R{5, 8}.withNonNegativeDenominator() == R{5, 8});
if constexpr (std::is_signed<TestType>::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<TestType>::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());
}
}