From 3cc6ba9a664af32839b7860d87ad13e1ab6317a1 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 17 Apr 2024 21:23:20 -0400 Subject: [PATCH] xo-ratio: initial commit --- CMakeLists.txt | 57 +++ LICENSE | 29 ++ README.md | 26 ++ cmake/xo-bootstrap-macros.cmake | 15 + cmake/xo_ratioConfig.cmake.in | 17 + example/CMakeLists.txt | 3 + example/ex1/CMakeLists.txt | 15 + example/ex1/ex1.cpp | 194 ++++++++++ include/xo/ratio/numeric_concept.hpp | 39 ++ include/xo/ratio/ratio.hpp | 547 +++++++++++++++++++++++++++ include/xo/ratio/ratio_concept.hpp | 29 ++ include/xo/ratio/ratio_iostream.hpp | 42 ++ 12 files changed, 1013 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmake/xo-bootstrap-macros.cmake create mode 100644 cmake/xo_ratioConfig.cmake.in create mode 100644 example/CMakeLists.txt create mode 100644 example/ex1/CMakeLists.txt create mode 100644 example/ex1/ex1.cpp create mode 100644 include/xo/ratio/numeric_concept.hpp create mode 100644 include/xo/ratio/ratio.hpp create mode 100644 include/xo/ratio/ratio_concept.hpp create mode 100644 include/xo/ratio/ratio_iostream.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..077f1267 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,57 @@ +# xo-ratio/CMakeLists.txt + +cmake_minimum_required(VERSION 3.10) + +project(xo_ratio VERSION 1.0) +enable_language(CXX) + +# common XO cmake macros (see proj/xo-cmake) +include(cmake/xo-bootstrap-macros.cmake) + +# ---------------------------------------------------------------- +# unit test setup + +enable_testing() +# activate code coverage for all executables + libraries (when configured with -DCODE_COVERAGE=ON) +add_code_coverage() + +# 1. assuming that /nix/store/ prefixes .hpp files belonging to gcc, catch2 etc. +# we're not interested in code coverage for these sources. +# 2. exclude the utest/ subdir, we don't need coverage on the unit tests themselves; +# rather, want coverage on the code that the unit tests exercise. +# +# NOTE: this seems to work only with the 'ccov-all' target. In particular, doesn't seem to do anything with the 'ccov' target +# +add_code_coverage_all_targets(EXCLUDE /nix/store/* ${PROJECT_SOURCE_DIR}/utest/* ${PROJECT_BINARY_DIR}/local/* ${PROJECT_SOURCE_DIR}/repo/*) + +# ---------------------------------------------------------------- +# c++ settings + +# one-time project-specific c++ flags. usually empty +set(PROJECT_CXX_FLAGS "") +#set(PROJECT_CXX_FLAGS "-fconcepts-diagnostics-depth=2") +add_definitions(${PROJECT_CXX_FLAGS}) + +xo_toplevel_compile_options() + +# ---------------------------------------------------------------- + +add_subdirectory(example) +#add_subdirectory(utest) +#add_subdirectory(docs) + +# ---------------------------------------------------------------- +# provide find_package() support for projects using this library + +set(SELF_LIB xo_ratio) +xo_add_headeronly_library(${SELF_LIB}) +xo_install_library4(${SELF_LIB} ${PROJECT_NAME}Targets) +xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) + +# ---------------------------------------------------------------- +# dependencies + +#xo_headeronly_dependency(${SELF_LIB} randomgen) +# etc.. + +# end CMakeLists.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..cae3cb5d --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2024 Roland Conybeare , All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Please also refer to the file .github/CONTRIBUTING.md, which clarifies licensing of +external contributions to this project including patches, pull requests, etc. diff --git a/README.md b/README.md new file mode 100644 index 00000000..722ea8bf --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# ratio library + +Header-only, constexpr library providing exact representation for rational numbers. + +Relative to `std::ratio`: +1. Uses `constexpr` instead of creating new types. + This means it can be used seamlessly at runtime. +2. Supports a few more arithmetic operations, + for example exponentiation to integer powers. +3. Provides concept support (with c++20) +4. Requires modern (c++17) support to achieve this + +## Getting Started + +### install dependencies + +- [github/Rconybea/xo-cmake](https://github.com/Rconybea/xo-cmake) cmake macros + +### build + install +``` +$ cd xo-ratio +$ PREFIX=/usr/local # for example +$ BUILDDIR=.build # for example +$ make ${BUILDDIR} +$ cmake -DCMAKE_PREFIX_PATH=${PREFIX} -B ${BUILDDIR} +``` diff --git a/cmake/xo-bootstrap-macros.cmake b/cmake/xo-bootstrap-macros.cmake new file mode 100644 index 00000000..936a1810 --- /dev/null +++ b/cmake/xo-bootstrap-macros.cmake @@ -0,0 +1,15 @@ +if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL "prefix")) + # default to typical install location for xo-project-macros + set(CMAKE_MODULE_PATH ${CMAKE_INSTALL_PREFIX}/share/cmake) +endif() + +if (NOT XO_SUBMODULE_BUILD) + message("-- GUESSED_CMAKE_CMD=cmake -DCMAKE_MODULE_PATH=${CMAKE_MODULE_PATH} -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -B ${CMAKE_BINARY_DIR}") + message("-- CMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}") + message("-- CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}") +endif() + +# needs to have been installed somewhere on CMAKE_MODULE_PATH, +# (e.g. from xo-cmake with the same value for CMAKE_INSTALL_PREFIX) +# +include(xo_macros/xo-project-macros) diff --git a/cmake/xo_ratioConfig.cmake.in b/cmake/xo_ratioConfig.cmake.in new file mode 100644 index 00000000..e5ee1778 --- /dev/null +++ b/cmake/xo_ratioConfig.cmake.in @@ -0,0 +1,17 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +# note: changes to find_dependency() calls here +# must coordinate with xo_dependency() calls +# in xo-reactor/src/reactor/CMakeLists.txt +# +#find_dependency(reflect) +#find_dependency(subsys) +#find_dependency(Eigen3) +#find_dependency(webutil) +#find_dependency(printjson) +#find_dependency(callback) + +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +check_required_components("@PROJECT_NAME@") diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt new file mode 100644 index 00000000..36200e18 --- /dev/null +++ b/example/CMakeLists.txt @@ -0,0 +1,3 @@ +# xo-ratio/example/CMakeLists.txt + +add_subdirectory(ex1) diff --git a/example/ex1/CMakeLists.txt b/example/ex1/CMakeLists.txt new file mode 100644 index 00000000..b7050cfc --- /dev/null +++ b/example/ex1/CMakeLists.txt @@ -0,0 +1,15 @@ +# xo-ratio/example/ex1/CMakeLists.txt + +set(SELF_EXE xo_ratio_ex1) +set(SELF_SRCS ex1.cpp) + +add_executable(${SELF_EXE} ${SELF_SRCS}) +xo_include_options2(${SELF_EXE}) + +# ---------------------------------------------------------------- +# dependencies.. + +xo_self_dependency(${SELF_EXE} xo_ratio) +xo_dependency(${SELF_EXE} reflect) + +# end CMakeLists.txt diff --git a/example/ex1/ex1.cpp b/example/ex1/ex1.cpp new file mode 100644 index 00000000..8aa3c82d --- /dev/null +++ b/example/ex1/ex1.cpp @@ -0,0 +1,194 @@ +/** @file ex1.cpp **/ + +#include "xo/ratio/ratio_iostream.hpp" +#include + +int +main() { + using xo::ratio::make_ratio; + using xo::ratio::ratio; + using xo::ratio::ratio_concept; + using namespace std; + + constexpr auto r1 = make_ratio(2, 3); + cerr << "r1=make_ratio(2,3): " << r1 << endl; // output + + static_assert(r1.num() == 2); + static_assert(r1.den() == 3); + + static_assert(r1.compare(r1, r1) == 0); + static_assert(xo::ratio::detail::op_aux_type::compare(r1,r1) == 0); + + static_assert(r1 == r1); + static_assert(!(r1 != r1)); + static_assert(r1 != 0); + static_assert(r1 != 1); + static_assert(r1 != 2); + static_assert(r1 != 3); + + static_assert(r1 >= r1); + static_assert(r1 <= r1); + + static_assert(r1 > 0); + static_assert(r1 >= 0); + static_assert(r1 < 1); + static_assert(r1 <= 1); + + constexpr auto r2 = make_ratio(2, 4); + cerr << "r2=make_ratio(2,4): " << r2 << endl; // output + + static_assert(r2.num() == 1); + static_assert(r2.den() == 2); + + static_assert(r2 == r2); + static_assert(r2 != r1); + static_assert(!(r2 > r1)); + static_assert(!(r2 >= r1)); + static_assert(r2 <= r1); + static_assert(r2 < r1); + static_assert(r1 > r2); + static_assert(r1 >= r2); + static_assert(!(r1 < r2)); + static_assert(!(r1 <= r2)); + + constexpr auto r3 = make_ratio(2, 3) - make_ratio(1, 2); + cerr << "r3=r1-r2: " << r1 - r2 << endl; // output + + static_assert(r3.num() == 1); + static_assert(r3.den() == 6); + + static_assert(r3 == r3); + static_assert(r3 != 0); + static_assert(r3 != 1); + static_assert(r3 < r2); + static_assert(r3 <= r2); + static_assert(r3 < r1); + static_assert(r3 <= r1); + + constexpr auto r4 = r1 + r2; + cerr << "r4=r1+r2: " << r1 + r2 << endl; // output + + static_assert(r4.num() == 7); + static_assert(r4.den() == 6); + static_assert(r4 > 1); + static_assert(r4 < 2); + + constexpr auto r5 = r1 + 3; + cerr << "r5=r1+3: " << r5 << endl; // output + + static_assert(r5.num() == 11); + static_assert(r5.den() == 3); + + constexpr auto r6 = 3 + r1; + cerr << "r5=3+r1: " << r6 << endl; // output + + static_assert(r6.num() == 11); + static_assert(r6.den() == 3); + + static_assert(r5 == r6); + static_assert(r5 > 3); + static_assert(r5 < 4); + static_assert(r5 > r1); + + constexpr auto r7 = r6 - 3; + cerr << "r7=r6-3: " << r7 << endl; // output + + static_assert(r7 == r1); + static_assert(r7 >= r1); + static_assert(r7 <= r1); + + constexpr auto r8 = 3 - r6; + cerr << "r8=3-r6: " << r8 << endl; // output + + static_assert(r8 == r8); + static_assert(r8 > -1); + static_assert(r8 < 0); + static_assert(-1 < r8); + static_assert(-1 <= r8); + static_assert(0 >= r8); + + constexpr auto r9 = r8 * r8; + cerr << "r9=r8*r8: " << r9 << endl; // output + + static_assert(r9 == make_ratio(4, 9)); + + constexpr auto r10 = r9 * 9; + cerr << "r10=r9*9: " << r10 << endl; // output + + static_assert(r10 == 4); + static_assert(r10.to() == 4); + + constexpr auto r11 = r9 * 3; + cerr << "r11=r9*3: " << r11 << endl; // output + + static_assert(r11 == make_ratio(4, 3)); + static_assert(r11.to() == 1); + + constexpr auto r12 = 9 * r9; + cerr << "r12=9*r9: " << r12 << endl; + + static_assert(r12 == r10); + static_assert(r12 == make_ratio(4, 1)); + static_assert(r12.to() == 4); + + constexpr auto r13 = 3 * r9; + cerr << "r13=3*r9: " << r13 << endl; // output + + static_assert(r13 == make_ratio(4, 3)); + static_assert(r13 == make_ratio(-4, -3)); + static_assert(r13 == r11); + static_assert(r13.to() == 1); + + constexpr auto r14 = r9 / r9; + cerr << "r14=r9/r9: " << r14 << endl; // output + + static_assert(r14 == 1); + static_assert(r14.to() == 1); + + constexpr auto r15 = r9 / r8; + cerr << "r15=r9/r8: " << r15 << endl; // (4/9) / (-2/3) = (4/9) * (3/-2) = 12/-18 = -2/3 + + static_assert(r15 == make_ratio(-2, 3)); + static_assert(r15 == make_ratio(2, -3)); + + constexpr auto r16 = r9 / 2; + cerr << "r16=r9/2: " << r16 << endl; + + static_assert(r16 == ratio(2, 9)); + static_assert(!r16.is_integer()); + + constexpr auto r17 = 2 / r9; + cerr << "r17=2/r9: " << r17 << endl; + + static_assert(r17 == ratio(9, 2)); + static_assert(!r17.is_integer()); + + constexpr auto r18 = r12 / r8; + cerr << "r18=r12/r8: " << r12/r8 << endl; + + static_assert(r18.is_integer()); + + constexpr auto r19 = r18.power(2); + cerr << "r19=r18^2: " << r19 << endl; + + static_assert(r19.is_integer()); + static_assert(r19 == ratio(36, 1)); + + constexpr auto r20 = r17.power(-3); + cerr << "r20=r17^-3: " << r20 << endl; + + static_assert(!r20.is_integer()); + static_assert(r20 == ratio(8, 729)); + + /* verify constexpr working */ + static_assert(ratio(2,3).num() == 2); + static_assert(ratio(2,3).den() == 3); + static_assert(make_ratio(-1,2).num() == -1); + static_assert(make_ratio(-1,2).den() == 2); + static_assert(make_ratio(-2,4).num() == -1); + static_assert(make_ratio(-2,4).den() == 2); + static_assert(make_ratio(1,-2).num() == -1); + static_assert(make_ratio(1,-2).den() == 2); + static_assert(make_ratio(-4,-6).num() == 2); + static_assert(make_ratio(-4,-6).den() == 3); +} diff --git a/include/xo/ratio/numeric_concept.hpp b/include/xo/ratio/numeric_concept.hpp new file mode 100644 index 00000000..02ad5894 --- /dev/null +++ b/include/xo/ratio/numeric_concept.hpp @@ -0,0 +1,39 @@ +/** @file numeric_concept.hpp **/ + +#pragma once + +#include + +namespace xo { + namespace ratio { + /** @concept numeric_concept + * @brief Concept for values that participate in arithmetic operations (+,-,*,/) and comparisons + * + * Intended to include at least: + * - built-in integral and floating-point types + * - big_int from ctbignum + * - boost::rational + * - std::complex + * - xo::unit::quantity + * + * Accepting complex numbers --> we don't require T to be totally ordered, + * and don't require (<,<=,>=,>) operators. + * + * Intend numeric_concept to apply to types T suitable for + * xo::ratio::ratio + **/ + template + concept numeric_concept = requires(T x, U y) + { + { -x }; + { x - y }; + { x + y }; + { x * y }; + { x / y }; + { x == y }; + { x != y }; + }; + } /*namespace ratio*/ +} /*namespace xo*/ + +/* end numeric_concept.hpp */ diff --git a/include/xo/ratio/ratio.hpp b/include/xo/ratio/ratio.hpp new file mode 100644 index 00000000..60aad611 --- /dev/null +++ b/include/xo/ratio/ratio.hpp @@ -0,0 +1,547 @@ +/** @file ratio.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "ratio_concept.hpp" +#include +#include +//#include + +namespace xo { + namespace ratio { + namespace detail { + /** @brief converts ratio to lowest terms when feasible + * + * Falls back to identity function for non-totally-ordered Ratio::component_type + */ + template > + struct reducer_type; + + /** @brief promote value to ratio type **/ + template > + struct promoter_type; + } + + /** @brief represent a ratio of two Int values. **/ + template + requires std::totally_ordered + struct ratio + { + public: + using component_type = Int; + + public: + constexpr ratio(Int n, Int d) : num_{n}, den_{d} {} + + /** @brief ratio in lowest commono terms + * + **/ + static constexpr ratio reduce(Int n, Int d) { + return ratio(n, d).reduce(); + } + + /** @brief add two ratios **/ + static constexpr ratio add(const ratio & x, + const ratio & y) { + /* (a/b) + (c/d) + * = a.d / (b.d) + b.c / (b.d) + * = (a.d + b.c) / (b.d) + */ + + auto a = x.num(); + auto b = x.den(); + auto c = y.num(); + auto d = y.den(); + + auto num = a*d + b*c; + auto den = b*d; + + return ratio(num, den).maybe_reduce(); + } + + /** @brief subtract two ratios **/ + static constexpr ratio subtract(const ratio & x, + const ratio & y) { + return add(x, y.negate()); + } + + /** @brief multiply two ratios **/ + static constexpr ratio multiply(const ratio & x, + const ratio & y) { + /* (a/b) * (c/d) = a.c / b.d */ + + /* if x,y normalized, + * opportunity to cancel common factor between (a, d) or (c, b) + * + * want to do this before multiplying to avoid overflow involving intermediate terms + */ + + auto a1 = x.num(); + auto b1 = x.den(); + auto c1 = y.num(); + auto d1 = y.den(); + + auto ad_gcf = std::gcd(a1, d1); + auto bc_gcf = std::gcd(b1, c1); + + auto a = a1 / ad_gcf; + auto b = b1 / bc_gcf; + auto c = c1 / bc_gcf; + auto d = d1 / ad_gcf; + + auto num = a*c; + auto den = b*d; + + return ratio(num, den).maybe_reduce(); + } + + /** @brief divide two ratios **/ + static constexpr ratio divide(const ratio & x, + const ratio & y) { + return multiply(x, y.reciprocal()); + } + + /** @brief compute integer power of a ratio **/ + constexpr ratio power(int p) const { + + constexpr ratio retval = ratio(1, 1); + + if (p == 0) + return ratio(1, 1); + + if (p < 0) + return this->reciprocal().power(-p); + + /* inv: x^p = aj.xj^pj */ + ratio aj = ratio(1, 1); + ratio xj = *this; + int pj = p; + + while (pj > 0) { + if (pj % 2 == 0) { + /* a.x^(2q) = a.(x^2)^q */ + xj = xj * xj; + pj = pj / 2; + } else { + /* a.x^(2q+1) = (a.x).x^(2q) */ + aj = aj * xj; + pj = (pj - 1); + } + } + + /* pj = 0, so: x^p = aj.xj^pj = aj.xj^0 = aj */ + return aj; + } + + /** @brief 3-way compare two ratios **/ + static constexpr auto compare(ratio x, ratio y) { + /* ensure minus signs in numerators only */ + if (x.den() < 0) + return compare_aux(ratio(-x.num(), -x.den()), y); + if (y.den() < 0) + return compare_aux(x, ratio(-y.num(), -y.den())); + + return compare_aux(x, y); + } + + constexpr Int num() const { return num_; } + constexpr Int den() const { return den_; } + + constexpr bool is_integer() const { return den_ == 1 || den_ == -1; } + + constexpr ratio negate() const { return ratio(-num_, den_); } + constexpr ratio reciprocal() const { return ratio(den_, num_); } + + /** @brief requires component_type is totally ordered **/ + constexpr Int floor() const { return (num_ / den_); } + + /** @brief requires component_type is totally ordered **/ + constexpr Int ceil() const { return floor() + 1; } + + /** @brief reduce to lowest terms + * + * @pre @c Int type must be totally ordered + **/ + constexpr ratio reduce() const requires std::totally_ordered { + if (den_ < 0) + return ratio(-num_, -den_).reduce(); + + auto factor = std::gcd(num_, den_); + + return ratio(num_ / factor, + den_ / factor); + } + + /** @brief reduce to lowest terms, if Int representation admits + * + * Otherwise fallback to identity function + **/ + constexpr ratio maybe_reduce() const { + return detail::reducer_type::attempt_reduce(*this); + } + + /** @brief return fractional part of this ratio + * + * @pre @c Int type must be totally ordered + **/ + constexpr ratio frac() const requires std::totally_ordered { + return ratio::subtract(*this, this->floor()); + } + + /** @brief convert to non-ratio representation + * + * For example: to int or double + **/ + template + constexpr Repr to() const { return num_ / static_cast(den_); } + + /** @brief convert to representation using different integer types **/ + template + constexpr operator Ratio2 () const requires ratio_concept { + return Ratio2(num_, den_); + } + + private: + /** @brief 3-way compare auxiliary function. + * + * @pre @p x, @p y have non-negative denominator + **/ + static constexpr auto compare_aux(ratio x, ratio y) { + /* control here: b>=0, d>=0 */ + + /* (a/b) <=> (c/d) + * (a.d/b) <=> c no sign change, since d >= 0 + * (a.d) <=> (b.c) no sign change, since b >= 0 + */ + auto a = x.num(); + auto b = x.den(); + auto c = y.num(); + auto d = y.den(); + + auto lhs = a*d; + auto rhs = b*c; + + return lhs <=> rhs; + } + + private: + /** @brief numerator **/ + Int num_; + /** @brief denominator **/ + Int den_; + }; + + namespace detail { + template + struct reducer_type {}; + + template + struct reducer_type { + static constexpr Ratio attempt_reduce(Ratio x) { return x.reduce(); } + }; + + template + struct reducer_type { + static constexpr Ratio attempt_reduce(Ratio x) { return x; } + }; + } + + namespace detail { + template + struct promoter_type; + + template + struct promoter_type { + /* to 'promote' a ratio, rely on its conversion operator */ + static constexpr Ratio promote(FromType x) { return x; } + }; + + template + struct promoter_type { + /* to 'promote' a non-ratio, use denominator=1 */ + static constexpr Ratio promote(FromType x) { return Ratio(x, 1); } + }; + } + + template + constexpr auto + make_ratio (Int1 n, Int2 d = 1) -> ratio> + { + return ratio>(n, d).maybe_reduce(); + } + + namespace detail { + /** @brief auxiliary function for binary ratio operations + * + * Support binary ratio operations on combinations: + * - (ratio, ratio) + * - (ratio, U) // where U is not a ratio + * - (T, ratio(U)) // where T is not a ratio + * + * Goals: + * + * 1. Support expressions like + * + * @code + * auto x = 1 + make_ratio(2,3); + * @endcode + * + * 2. promote to wider types as needed + * + * @code + * auto x = make_ratio(2,3) + make_ratio(1ul,2ul); + * static_assert(std::same_as); + * @endcode + * + * 3. avoid interfering with other templates that may overload operator+ + * + * @pre at least one of (Left,Right) must be known to be a ratio + **/ + template , + bool RightIsRatio = ratio_concept> + struct op_aux_type; + + /** @brief specialization for two ratio types **/ + template + requires (ratio_concept && ratio_concept) + struct op_aux_type { + using component_type = std::common_type_t; + + using ratio_type = ratio; + + static constexpr ratio_type add (const LeftRatio & x, + const RightRatio & y) + { + return ratio_type::add(x, y); + } + + static constexpr ratio_type subtract (const LeftRatio & x, + const RightRatio & y) + { + return ratio_type::subtract(x, y); + } + + static constexpr ratio_type multiply (const LeftRatio & x, + const RightRatio & y) + { + return ratio_type::multiply(x, y); + } + + static constexpr ratio_type divide (const LeftRatio & x, + const RightRatio & y) + { + return ratio_type::divide(x, y); + } + + static constexpr auto compare (const LeftRatio & x, + const RightRatio & y) + { + return ratio_type::compare(x, y); + } + }; + + /** @brief specialization for left-hand ratio and right-hand integer value **/ + template + requires (ratio_concept && !ratio_concept) + struct op_aux_type { + using component_type = std::common_type_t; + + using ratio_type = ratio; + + static constexpr ratio_type add (const LeftRatio & x, + const Right & y) + { + /* reminder: adding an integer can't introduce reduced terms */ + return ratio_type(x.num() + x.den() * y, x.den()); + } + + static constexpr ratio_type subtract (const LeftRatio & x, + const Right & y) + { + /* reminder: subtracting an integer can't introduce reduced terms */ + return ratio_type(x.num() - x.den() * y, x.den()); + } + + static constexpr ratio_type multiply (const LeftRatio & x, + const Right & yp) + { + auto gcf = std::gcd(x.den(), yp); + + auto a = x.num(); + auto b = x.den() / gcf; + auto y = yp / gcf; + + return ratio_type(a*y, b); + } + + static constexpr ratio_type divide (const LeftRatio & x, + const Right & yp) + { + auto gcf = std::gcd(x.num(), yp); + + auto a = x.num() / gcf; + auto b = x.den(); + auto y = yp / gcf; + + return ratio_type(a*y, b); + } + + static constexpr auto compare (const LeftRatio & x, + const Right & y) + { + /* note: in c++26 std::signof is constexpr, usable here */ + if (x.den() >= 0) + return compare_aux(x, y); + else + return compare_aux(LeftRatio(-x.num(), -x.den()), y); + } + + private: + static constexpr auto compare_aux (const LeftRatio & x, const Right & y) { + return (x.num() <=> x.den() * y); + } + }; + + /** @brief specialization for left-hand integer value and right-hand ratio **/ + template + requires (!ratio_concept && ratio_concept) + struct op_aux_type { + using component_type = std::common_type_t; + + using ratio_type = ratio; + + static constexpr ratio_type add(const Left & x, + const RightRatio & y) + { + /* reminder: adding an integer can't introduce reduced terms */ + return ratio_type(x * y.den() + y.num(), y.den()); + } + + static constexpr ratio_type subtract(const Left & x, + const RightRatio & y) + { + /* reminder: subtracting an integer can't introduce reduced terms */ + return ratio_type(x * y.den() - y.num(), y.den()); + } + + static constexpr ratio_type multiply (const Left & xp, + const RightRatio & y) + { + auto gcf = std::gcd(xp, y.den()); + + auto x = xp / gcf; + auto c = y.num(); + auto d = y.den() / gcf; + + return ratio_type(x*c, d); + } + + static constexpr ratio_type divide (const Left & x, + const RightRatio & y) + { + return multiply(x, y.reciprocal()); + } + + static constexpr auto compare(const Left & x, + const RightRatio & y) + { + if (y.den() >= 0) + return compare_aux(x, y); + else + return compare_aux(x, RightRatio(-y.num(), -y.den())); + } + + private: + static constexpr auto compare_aux (const Left & x, + const RightRatio & y) + { + return (x * y.den() <=> y.num()); + }; + }; + } /*namespace detail*/ + + /** @brief add two ratios. + * + * One argument may be a non-ratio type if it can be promoted to a ratio + **/ + template + inline constexpr auto + operator+ (const Ratio1 & x, const Ratio2 & y) + requires (ratio_concept || ratio_concept) + { + return detail::op_aux_type::add(x, y); + } + + /** @brief subtract two ratios. + * + * One argument may be a non-ratio type if it can be promoted to a ratio + **/ + template + inline constexpr auto + operator- (const Ratio1 & x, const Ratio2 & y) + requires (ratio_concept || ratio_concept) + { + return detail::op_aux_type::subtract(x, y); + } + + /** @brief multiply two ratios + * + * One argument may be a non-ratio type if it can be promoted to a ratio + **/ + template + inline constexpr auto + operator* (const Ratio1 & x, const Ratio2 & y) + requires (ratio_concept || ratio_concept) + { + return detail::op_aux_type::multiply(x, y); + } + + /** @brief divide two ratios + * + * One argument may be a non-ratio type if it can be promoted to a ratio + **/ + template + inline constexpr auto + operator/ (const Ratio1 & x, const Ratio2 & y) + requires (ratio_concept || ratio_concept) + { + return detail::op_aux_type::divide(x, y); + } + + /** @brief compare two ratios for equality + * + * One argument may be a non-ratio type if it can be promoted to a ratio + **/ + template + inline constexpr bool + operator== (const Ratio1 & x, const Ratio2 & y) + requires (ratio_concept || ratio_concept) + { + return (detail::op_aux_type::compare(x, y) == 0); + } + + /** @brief compare two ratios + * + * One argument may be a non-ratio type if it can be promoted to a ratio + **/ + template + inline constexpr auto + operator<=> (const Ratio1 & x, const Ratio2 & y) + requires (ratio_concept || ratio_concept) + { + return detail::op_aux_type::compare(x, y); + } + + } /*namespace ratio*/ +} /*namespace xo*/ + +/** end ratio.hpp **/ diff --git a/include/xo/ratio/ratio_concept.hpp b/include/xo/ratio/ratio_concept.hpp new file mode 100644 index 00000000..8ca1d438 --- /dev/null +++ b/include/xo/ratio/ratio_concept.hpp @@ -0,0 +1,29 @@ +/** @file ratio_concept.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "numeric_concept.hpp" + +namespace xo { + namespace ratio { + /* also expect: + * Ratio::num_type / Ratio::den_type rounds towards -inf + */ + template + concept ratio_concept = requires(Ratio ratio) + { + typename Ratio::component_type; + typename Ratio::component_type; + + { ratio.num() } -> std::same_as; + { ratio.den() } -> std::same_as; + } && numeric_concept; + + } /*namespace ratio*/ +} /*namespace xo*/ + + +/** end ratio_concept.hpp **/ diff --git a/include/xo/ratio/ratio_iostream.hpp b/include/xo/ratio/ratio_iostream.hpp new file mode 100644 index 00000000..fc898b36 --- /dev/null +++ b/include/xo/ratio/ratio_iostream.hpp @@ -0,0 +1,42 @@ +/** @file ratio_iostream.hpp + * + * Author: Roland Conybeare + **/ + +#pragma once + +#include "ratio.hpp" +#include + +namespace xo { + namespace ratio { + /** @brief print ratio x on stream os. + * + * Example: + * @code + * print_ratio(std::cerr, make_ratio(1,2); // outputs "" + * @endcode + **/ + template + void + print_ratio (std::ostream & os, const Ratio & x) { + os << ""; + } + + /** @brief print ratio x on stream os. + * + * Example: + * @code + * std::cout << make_ratio(2,3); // outputs "" + * @endcode + **/ + template + inline std::ostream & + operator<< (std::ostream & os, const Ratio & x) { + print_ratio(os, x); + return os; + } + } +} + +/** end ratio_iostream.hpp **/