morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
rational.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file util/rational.hpp
6/// @brief Exact rational arithmetic for morph model/action values.
7///
8/// `morph::math::Rational` is a small, value-semantic, trivially-copyable
9/// struct representing the rational number `numerator/denominator` with
10/// `std::int64_t` components. It carries a runtime decimal-precision tag,
11/// `decimalPlaces`, as a strong type (`DecimalPlaces`). Arithmetic is
12/// *exact* — sums, differences, products, and quotients are reduced to
13/// canonical form with no floating-point rounding error. The precision tag
14/// affects only decimal scaling (`Rational::fromFloat`) and rounding
15/// (`Rational::toDouble`, formatting); it never changes a stored value.
16///
17/// Adapted from LASTRADA `JPMath/Rational.hpp`, with the `boxed` strong-type
18/// dependency replaced by a self-contained `DecimalPlaces` and a Glaze wire
19/// codec added so the type round-trips through the morph JSON wire with its
20/// invariants restored on read.
21///
22/// The precision is supplied at construction. There is intentionally no
23/// default — every call site states the precision it intends, e.g.
24/// `Rational{1, 3, DecimalPlaces{9}}`. Precision is capped at
25/// `kMaxDecimalPlaces` (18, the largest `k` for which `10^k` fits in
26/// `int64_t`); out-of-range values assert in debug and clamp into
27/// `[0, kMaxDecimalPlaces]` in release. Zero decimal places is a legal
28/// precision — a whole-number quantity (e.g. a JPY/KRW amount) carries no
29/// fractional digit at all.
30///
31/// @par Cross-precision arithmetic
32/// Binary arithmetic propagates `std::max` of the two operands'
33/// `decimalPlaces` — the wider precision wins. Comparison (`<=>`, `==`) is
34/// purely value-based on the canonical `(numerator, denominator)` pair and
35/// ignores `decimalPlaces` entirely.
36///
37/// @par Invariants
38/// Every public operation that produces a `Rational` restores:
39/// - `denominator > 0` (strictly positive — never zero, never negative)
40/// - `gcd(|numerator|, denominator) == 1`
41/// - canonical zero is `0/1`
42/// - `0 <= decimalPlaces.value <= kMaxDecimalPlaces`
43///
44/// All sign lives in the numerator.
45///
46/// @par Error handling
47/// The struct never throws. Operations that may fail (zero divisor,
48/// non-finite floating-point input, overflow during decimal scaling) return
49/// `std::expected<Rational, RationalError>`. `operator/` and `reciprocal`
50/// return `expected`; `operator+`, `operator-`, `operator*` on plain
51/// `Rational` pairs cannot fail and return a bare `Rational`.
52///
53/// Overflow is *not* one of the conditions those channels report: all four
54/// operations saturate at `±INT64_MAX/1` and log, and `operator/`'s `expected`
55/// reports a zero divisor only — an overflowing quotient still comes back as a
56/// success holding a clamped value. `checkedAdd`, `checkedSub`, `checkedMul`
57/// and `checkedDiv` are the exact-or-nothing forms for callers that must
58/// branch on it.
59///
60/// @par Mixed-type expressions
61/// Whenever an arithmetic expression contains an
62/// `std::expected<Rational, RationalError>` sub-expression or a
63/// floating-point operand, the whole expression evaluates to
64/// `std::expected<Rational, RationalError>`. The float operand is lifted via
65/// `Rational::fromFloat` — its precision is taken from the Rational
66/// operand's `getDecimalPlaces()`. Errors short-circuit left to right.
67///
68/// @code{.cpp}
69/// using morph::math::DecimalPlaces;
70/// using morph::math::Rational;
71/// auto const a = Rational{7, 2, DecimalPlaces{9}};
72/// auto const b = Rational{2, DecimalPlaces{9}};
73/// auto const c = Rational{1, 2, DecimalPlaces{9}};
74/// auto result = a / b + c * 3.5;
75/// // decltype(result) == std::expected<Rational, RationalError>
76/// @endcode
77///
78/// @par Formatting
79/// `std::format` support is split by the supplied spec:
80/// - empty spec `"{}"` -> exact rational form (`"n/d"`, or `"n"` when integer)
81/// - non-empty spec `"{:.3f}"` -> delegated to `std::formatter<double>` on `toDouble()`
82///
83/// @par Wire format
84/// Over the morph JSON wire a `Rational` travels as the object
85/// `{"num":617,"den":50,"dp":2}`. Reading goes through the canonicalising
86/// constructor, so a non-canonical payload (`1234/100`) or a hostile one
87/// (`den == 0`, out-of-range `dp`) always lands as a valid, reduced value.
88///
89/// @note Negating a `Rational` built from `INT64_MIN` overflows; avoid that
90/// extreme value in code (the wire codec clamps it away for untrusted
91/// input).
92/// @note Comparison is exact over the full int64 range (128-bit cross
93/// products). Arithmetic, however, has the usual fixed-width envelope:
94/// `+`, `-`, `*` overflow int64 when reduced cross terms exceed ~2^63
95/// (e.g. sums over large coprime denominators). Keep operands within
96/// the decimal-scaled ranges the precision tags imply.
97
98#include <cassert>
99#include <cmath>
100#include <compare>
101#include <concepts>
102#include <cstdint>
103#include <expected>
104#include <format>
105#include <glaze/glaze.hpp>
106#include <limits>
107#include <morph/core/logger.hpp>
109#include <numeric>
110#include <string>
111#include <string_view>
112#include <type_traits>
113#include <utility>
114
115namespace morph::math {
116
117/// @brief Strong type for a decimal-precision count.
118///
119/// Prevents the precision from being confused with a numerator or a
120/// denominator at a call site: `Rational{1, 3, DecimalPlaces{9}}`.
121struct DecimalPlaces {
122 /// @brief Raw digit count. Rational's invariant keeps it in [0, kMaxDecimalPlaces].
123 std::uint32_t value{};
124
125 constexpr DecimalPlaces() noexcept = default;
126
127 /// @brief Explicit so a bare integer never silently becomes a precision.
128 /// @param raw The digit count to box.
129 constexpr explicit DecimalPlaces(std::uint32_t raw) noexcept : value{raw} {}
130
131 /// @brief Value ordering/equality on the raw digit count.
132 /// @param other Precision to compare against.
133 /// @return The ordering of the two raw counts.
134 [[nodiscard]] constexpr auto operator<=>(const DecimalPlaces& other) const noexcept = default;
135};
136
137/// @brief Strong type for a Rational numerator.
138///
139/// Prevents numerator/denominator argument swapping at construction sites.
140struct Numerator {
141 /// @brief Raw signed integer value.
142 std::int64_t value{};
143
144 constexpr Numerator() noexcept = default;
145
146 /// @brief Explicit so the type must be spelled out.
147 /// @param raw The integer to box as a numerator.
148 constexpr explicit Numerator(std::int64_t raw) noexcept : value{raw} {}
149};
150
151/// @brief Strong type for a Rational denominator.
152///
153/// Prevents numerator/denominator argument swapping at construction sites.
154struct Denominator {
155 /// @brief Raw signed integer value; must never be zero after canonicalisation.
156 std::int64_t value{};
157
158 constexpr Denominator() noexcept = default;
159
160 /// @brief Explicit so the type must be spelled out.
161 /// @param raw The integer to box as a denominator.
162 constexpr explicit Denominator(std::int64_t raw) noexcept : value{raw} {}
163};
164
165/// @brief Largest decimal precision the type supports: `10^18` is the
166/// greatest power of ten that fits in `std::int64_t`.
167inline constexpr std::uint32_t kMaxDecimalPlaces = 18;
168
169/// @brief Error states reachable through `Rational` operations.
170enum class RationalError : std::uint8_t {
171 DivisionByZero, ///< Divisor numerator is zero (operator/, reciprocal, From).
172 NotFinite, ///< Floating-point input was NaN or +/-Inf (fromFloat only).
173 Overflow, ///< Result or an intermediate term exceeds int64_t range
174 ///< (`fromFloat`, and the `checked*` arithmetic helpers
175 ///< `checkedAdd`/`checkedSub`/`checkedMul`/`checkedDiv`).
176 ///< Never produced by `dividedBy`/`operator/`, which
177 ///< saturate on overflow and report only `DivisionByZero`.
178};
179
180/// @brief Rule for resolving a value sitting exactly halfway between the two
181/// nearest results when rounding to a decimal scale.
182///
183/// morph's decimal *display* path (`morph::units::detail::formatRationalDecimal`)
184/// rounds half away from zero, and that is why it is the default here: a value
185/// reconciled to its declared precision with `HalfAwayFromZero` renders as the
186/// same digits the submitter was shown before sending it. `HalfEven` exists for
187/// a caller bound to banker's rounding (the default in IEEE 754-2008 decimal,
188/// Python `decimal` and JSR-354, because half-away-from-zero biases a long sum
189/// upward) and knowingly trades that agreement away.
190enum class RoundingMode : std::uint8_t {
191 HalfAwayFromZero, ///< Ties go to the neighbour of larger magnitude: `1.25 -> 1.3`, `-1.25 -> -1.3`.
192 HalfEven, ///< Ties go to the neighbour whose last kept digit is even: `1.25 -> 1.2`, `1.35 -> 1.4`.
193};
194
195namespace detail {
196
197/// @brief Per-thread clamp counter, scoped by `WireClampScope`.
198/// @return Reference to this thread's counter.
199[[nodiscard]] inline std::size_t& wireClampCounter() noexcept {
200 static thread_local std::size_t clamped = 0;
201 return clamped;
202}
203
204} // namespace detail
205
206/// @brief Observes whether any `Rational` had to be clamped while decoding
207/// inside this scope.
208///
209/// Decoding a `Rational` cannot fail. `setWire` rebuilds through the
210/// canonicalising constructor, which clamps rather than rejects, so
211/// `{"num":5,"den":0,"dp":2}` becomes a perfectly plausible `5/1` and nothing
212/// downstream can tell the value was altered.
213///
214/// This makes that observable at the point where it matters -- around a decode
215/// -- without giving `Rational` an opinion about what should happen next.
216/// Whether a clamped value is a protocol violation to reject or a harmless
217/// normalisation depends on whether the caller is reading a trusted local
218/// value or an untrusted payload off a socket, and only the decoding layer
219/// knows which:
220///
221/// @code
222/// morph::math::WireClampScope clamps;
223/// if (auto err = glz::read<opts>(action, json)) { ... }
224/// if (clamps.clamped() != 0) {
225/// // reject the payload
226/// }
227/// @endcode
228///
229/// Thread-local and scoped: a decode is synchronous on one thread, and a
230/// nested decode must not steal its parent's count.
232public:
233 /// @brief Starts a fresh count, saving any enclosing scope's.
234 WireClampScope() noexcept : _saved{detail::wireClampCounter()} { detail::wireClampCounter() = 0; }
235
236 WireClampScope(const WireClampScope&) = delete;
237 WireClampScope& operator=(const WireClampScope&) = delete;
238 WireClampScope(WireClampScope&&) = delete;
239 WireClampScope& operator=(WireClampScope&&) = delete;
240
241 /// @brief Folds this scope's count back into the enclosing one.
242 ~WireClampScope() { detail::wireClampCounter() += _saved; }
243
244 /// @brief How many `Rational` values were clamped so far in this scope.
245 /// @return The clamp count.
246 [[nodiscard]] std::size_t clamped() const noexcept { return detail::wireClampCounter(); }
247
248private:
249 std::size_t _saved;
250};
251
252namespace detail {
253
254/// @brief Clamps a raw precision into `[0, kMaxDecimalPlaces]` silently — for
255/// untrusted wire input.
256///
257/// The lower bound is `0`, not `1`: zero decimal places is a legal precision
258/// (a whole-number quantity, e.g. a JPY/KRW amount with no fractional
259/// subunit). `rawDecimalPlaces` is unsigned, so there is no below-zero case
260/// to clamp; only the upper bound can ever fire.
261[[nodiscard]] constexpr std::uint32_t clampWireDecimalPlaces(std::uint32_t rawDecimalPlaces) noexcept {
262 if (rawDecimalPlaces > kMaxDecimalPlaces) {
263 return kMaxDecimalPlaces;
264 }
265 return rawDecimalPlaces;
266}
267
268/// @brief Clamps like `clampWireDecimalPlaces` but asserts in debug — for
269/// call sites that state a precision in code, where out-of-range is a bug.
270[[nodiscard]] constexpr std::uint32_t clampDecimalPlaces(std::uint32_t rawDecimalPlaces) noexcept {
271 assert(rawDecimalPlaces <= kMaxDecimalPlaces);
272 return clampWireDecimalPlaces(rawDecimalPlaces);
273}
274
275/// @brief Whether `lhs + rhs` would overflow `std::int64_t`.
276///
277/// Asks *before* performing the addition. Detecting overflow by doing it and
278/// inspecting the result is undefined behaviour for signed types, so the
279/// question has to be answered from the operands alone.
280/// @param lhs Left addend.
281/// @param rhs Right addend.
282/// @return `true` if the sum is not representable.
283[[nodiscard]] constexpr bool addOverflows(std::int64_t lhs, std::int64_t rhs) noexcept {
284 constexpr auto maxValue = std::numeric_limits<std::int64_t>::max();
285 constexpr auto minValue = std::numeric_limits<std::int64_t>::min();
286 if (rhs > 0 && lhs > maxValue - rhs) {
287 return true;
288 }
289 return rhs < 0 && lhs < minValue - rhs;
290}
291
292/// @brief Whether `lhs - rhs` would overflow `std::int64_t`.
293/// @param lhs Minuend.
294/// @param rhs Subtrahend.
295/// @return `true` if the difference is not representable.
296[[nodiscard]] constexpr bool subOverflows(std::int64_t lhs, std::int64_t rhs) noexcept {
297 constexpr auto maxValue = std::numeric_limits<std::int64_t>::max();
298 constexpr auto minValue = std::numeric_limits<std::int64_t>::min();
299 if (rhs < 0 && lhs > maxValue + rhs) {
300 return true;
301 }
302 return rhs > 0 && lhs < minValue + rhs;
303}
304
305/// @brief Whether `lhs * rhs` would overflow `std::int64_t`.
306///
307/// Division-based rather than a wide-product comparison so it stays valid in a
308/// constant expression on every supported compiler (MSVC has no `__int128`).
309/// @param lhs Left factor.
310/// @param rhs Right factor.
311/// @return `true` if the product is not representable.
312[[nodiscard]] constexpr bool mulOverflows(std::int64_t lhs, std::int64_t rhs) noexcept {
313 constexpr auto maxValue = std::numeric_limits<std::int64_t>::max();
314 constexpr auto minValue = std::numeric_limits<std::int64_t>::min();
315 if (lhs == 0 || rhs == 0) {
316 return false;
317 }
318 // -1 is the one factor whose product can overflow without either operand
319 // being large: -1 * INT64_MIN has no positive counterpart.
320 if (lhs == -1) {
321 return rhs == minValue;
322 }
323 if (rhs == -1) {
324 return lhs == minValue;
325 }
326 if (lhs > 0) {
327 return rhs > 0 ? lhs > maxValue / rhs : rhs < minValue / lhs;
328 }
329 return rhs > 0 ? lhs < minValue / rhs : lhs < maxValue / rhs;
330}
331
332/// @brief A 64x64 -> 128-bit unsigned product, comparable high-word-first.
333struct U128 {
334 std::uint64_t hi{};
335 std::uint64_t lo{};
336 [[nodiscard]] constexpr auto operator<=>(const U128&) const noexcept = default;
337};
338
339/// @brief Multiplies two u64 exactly into 128 bits (for exact fraction comparison).
340[[nodiscard]] constexpr U128 mulU64(std::uint64_t lhs, std::uint64_t rhs) noexcept {
341#ifdef __SIZEOF_INT128__
342#if defined(__GNUC__) && !defined(__clang__)
343#pragma GCC diagnostic push
344#pragma GCC diagnostic ignored "-Wpedantic"
345#endif
346 auto const product = static_cast<unsigned __int128>(lhs) * rhs;
347#if defined(__GNUC__) && !defined(__clang__)
348#pragma GCC diagnostic pop
349#endif
350 return U128{.hi = static_cast<std::uint64_t>(product >> 64), .lo = static_cast<std::uint64_t>(product)};
351#else
352 // Portable 32-bit limb multiplication (MSVC has no __int128).
353 constexpr std::uint64_t mask = 0xffffffffULL;
354 auto const lhsLow = lhs & mask;
355 auto const lhsHigh = lhs >> 32;
356 auto const rhsLow = rhs & mask;
357 auto const rhsHigh = rhs >> 32;
358 auto const lowLow = lhsLow * rhsLow;
359 auto const lowHigh = lhsLow * rhsHigh;
360 auto const highLow = lhsHigh * rhsLow;
361 auto const highHigh = lhsHigh * rhsHigh;
362 auto const mid = (lowLow >> 32) + (lowHigh & mask) + (highLow & mask);
363 return U128{.hi = highHigh + (lowHigh >> 32) + (highLow >> 32) + (mid >> 32), .lo = (mid << 32) | (lowLow & mask)};
364#endif
365}
366
367/// @brief Magnitude of a signed 64-bit value as u64; well-defined for INT64_MIN.
368[[nodiscard]] constexpr std::uint64_t absU64(std::int64_t value) noexcept {
369 return value < 0 ? 0ULL - static_cast<std::uint64_t>(value) : static_cast<std::uint64_t>(value);
370}
371
372} // namespace detail
373
374/// @brief Exact rational number with a runtime decimal-precision tag.
375struct Rational {
376 /// @brief Signed numerator. Carries the sign of the rational value.
377 std::int64_t numerator{0};
378
379 /// @brief Strictly positive denominator. Never zero, never negative.
380 std::int64_t denominator{1};
381
382 /// @brief Decimal-precision tag. Invariant: 0 <= value <= kMaxDecimalPlaces.
384
385 /// @brief Default-constructs the canonical zero (0/1) at precision 1.
386 constexpr Rational() noexcept = default;
387
388 /// @brief Constructs from a whole integer at the given precision.
389 /// @param whole The integer value; stored as `whole/1`.
390 /// @param wantedPrecision Decimal precision; clamped to [0, kMaxDecimalPlaces].
391 constexpr Rational(std::int64_t whole, DecimalPlaces wantedPrecision) noexcept
392 : numerator{whole}, decimalPlaces{detail::clampDecimalPlaces(wantedPrecision.value)} {}
393
394 /// @brief Constructs from explicit numerator/denominator, then canonicalises.
395 ///
396 /// A negative @p wantedDenominator flips the sign of @p wantedNumerator,
397 /// the pair is reduced by `gcd`, and a denominator of `0` is clamped to
398 /// `1`. Use `from` for explicit zero-denominator detection.
399 ///
400 /// @param wantedNumerator Signed numerator.
401 /// @param wantedDenominator Denominator. May be negative or zero on input.
402 /// @param wantedPrecision Decimal precision; clamped to [0, kMaxDecimalPlaces].
403 constexpr Rational(Numerator wantedNumerator, Denominator wantedDenominator,
404 DecimalPlaces wantedPrecision) noexcept
405 : numerator{wantedNumerator.value},
406 denominator{wantedDenominator.value},
407 decimalPlaces{detail::clampDecimalPlaces(wantedPrecision.value)} {
408 canonicalise();
409 }
410
411 /// @brief Validating factory.
412 /// @param wantedNumerator Signed numerator.
413 /// @param wantedDenominator Denominator; `0` is rejected instead of clamped.
414 /// @param wantedPrecision Decimal precision; clamped to [0, kMaxDecimalPlaces].
415 /// @return The canonical rational, or `DivisionByZero` if @p wantedDenominator is 0.
416 [[nodiscard]] static constexpr std::expected<Rational, RationalError> from(
417 Numerator wantedNumerator, Denominator wantedDenominator, DecimalPlaces wantedPrecision) noexcept {
418 if (wantedDenominator.value == 0) {
419 return std::unexpected(RationalError::DivisionByZero);
420 }
421 return Rational{wantedNumerator, wantedDenominator, wantedPrecision};
422 }
423
424 /// @brief Converts a `double` to a Rational scaled to @p wantedPrecision.
425 /// @param value Source value.
426 /// @param wantedPrecision Decimal precision to scale to.
427 /// @return A canonical Rational, or an error (NotFinite / Overflow).
428 /// @note `noexcept` but not `constexpr` — uses `std::llround` / `std::isfinite`.
429 [[nodiscard]] static std::expected<Rational, RationalError> fromFloat(double value,
430 DecimalPlaces wantedPrecision) noexcept;
431
432 /// @brief Converts a `float` to a Rational scaled to @p wantedPrecision.
433 /// @param value Source value.
434 /// @param wantedPrecision Decimal precision to scale to.
435 /// @return A canonical Rational, or an error (NotFinite / Overflow).
436 [[nodiscard]] static std::expected<Rational, RationalError> fromFloat(float value,
437 DecimalPlaces wantedPrecision) noexcept;
438
439 /// @brief Converts a `long double` to a Rational scaled to @p wantedPrecision.
440 /// @param value Source value.
441 /// @param wantedPrecision Decimal precision to scale to.
442 /// @return A canonical Rational, or an error (NotFinite / Overflow).
443 [[nodiscard]] static std::expected<Rational, RationalError> fromFloat(long double value,
444 DecimalPlaces wantedPrecision) noexcept;
445
446 /// @brief Canonical zero (`0/1`) at the given precision.
447 /// @param wantedPrecision Decimal precision of the returned value.
448 /// @return `0/1` tagged with @p wantedPrecision.
449 [[nodiscard]] static constexpr Rational zero(DecimalPlaces wantedPrecision) noexcept {
450 return Rational{Numerator{0}, Denominator{1}, wantedPrecision};
451 }
452
453 /// @brief Canonical one (`1/1`) at the given precision.
454 /// @param wantedPrecision Decimal precision of the returned value.
455 /// @return `1/1` tagged with @p wantedPrecision.
456 [[nodiscard]] static constexpr Rational one(DecimalPlaces wantedPrecision) noexcept {
457 return Rational{Numerator{1}, Denominator{1}, wantedPrecision};
458 }
459
460 /// @brief The value's current decimal precision.
461 /// @return The `decimalPlaces` tag.
462 [[nodiscard]] constexpr DecimalPlaces getDecimalPlaces() const noexcept { return decimalPlaces; }
463
464 /// @brief Whether the value equals `0/1`.
465 /// @return `true` if the numerator is zero.
466 [[nodiscard]] constexpr bool isZero() const noexcept { return numerator == 0; }
467
468 /// @brief Whether the value is an integer.
469 /// @return `true` if the denominator is 1.
470 [[nodiscard]] constexpr bool isInteger() const noexcept { return denominator == 1; }
471
472 /// @brief Whether the value is strictly less than zero.
473 /// @return `true` if the numerator is negative.
474 [[nodiscard]] constexpr bool isNegative() const noexcept { return numerator < 0; }
475
476 /// @brief Converts to `double`, rounded to this value's `decimalPlaces`.
477 /// @return The rounded floating-point reading.
478 [[nodiscard]] double toDouble() const noexcept { return toDouble(decimalPlaces.value); }
479
480 /// @brief Converts to `double`, rounded to @p requestedDecimalPlaces.
481 /// @param requestedDecimalPlaces Number of decimal digits to keep. Values
482 /// `> 18` fall back to unrounded conversion.
483 /// @return The rounded floating-point reading.
484 [[nodiscard]] double toDouble(std::uint32_t requestedDecimalPlaces) const noexcept;
485
486 /// @brief Negates. @note Negating a Rational built from `INT64_MIN` overflows.
487 /// @return The value with the numerator's sign flipped.
488 [[nodiscard]] constexpr Rational operator-() const noexcept {
490 }
491
492 /// @brief Multiplicative inverse.
493 /// @return `denominator/numerator`, or `unexpected(DivisionByZero)` if zero.
494 [[nodiscard]] constexpr std::expected<Rational, RationalError> reciprocal() const noexcept {
495 if (numerator == 0) {
496 return std::unexpected(RationalError::DivisionByZero);
497 }
498 if (numerator < 0) {
500 }
502 }
503
504 /// @brief Three-way comparison. Value-only: ignores `decimalPlaces`.
505 ///
506 /// Exact for the full int64 range: cross-products are computed in 128
507 /// bits, so the ordering is always consistent with `operator==`.
508 /// @param other Value to compare against.
509 /// @return The ordering of the two exact values.
510 [[nodiscard]] constexpr std::strong_ordering operator<=>(const Rational& other) const noexcept {
511 auto const leftSign = static_cast<int>(numerator > 0) - static_cast<int>(numerator < 0);
512 auto const rightSign = static_cast<int>(other.numerator > 0) - static_cast<int>(other.numerator < 0);
513 if (leftSign != rightSign) {
514 return leftSign <=> rightSign;
515 }
516 if (leftSign == 0) {
517 return std::strong_ordering::equal;
518 }
519
520 auto const leftProduct =
521 detail::mulU64(detail::absU64(numerator), static_cast<std::uint64_t>(other.denominator));
522 auto const rightProduct =
523 detail::mulU64(detail::absU64(other.numerator), static_cast<std::uint64_t>(denominator));
524 auto const magnitude = leftProduct <=> rightProduct;
525 if (leftSign > 0) {
526 return magnitude;
527 }
528 if (std::is_lt(magnitude)) {
529 return std::strong_ordering::greater;
530 }
531 if (std::is_gt(magnitude)) {
532 return std::strong_ordering::less;
533 }
534 return std::strong_ordering::equal;
535 }
536
537 /// @brief Equality. Value-only: ignores `decimalPlaces`.
538 /// @param other Value to compare against.
539 /// @return `true` when the canonical pairs are identical.
540 [[nodiscard]] constexpr bool operator==(const Rational& other) const noexcept {
541 return numerator == other.numerator && denominator == other.denominator;
542 }
543
544 /// @brief In-place addition. Result precision becomes `max` of the two.
545 /// Uses reduce-before-multiply to extend the safe int64 range (Knuth 4.5.1).
546 ///
547 /// **Saturates rather than overflowing.** If the exact sum -- or any
548 /// intermediate cross-term needed to reach it -- is not representable, this
549 /// logs at `error` and clamps to the largest magnitude of the correct sign
550 /// instead of committing signed-overflow undefined behaviour. See
551 /// `saturateToward` for why a defined wrong answer beats UB here.
552 /// @param rhs Value to add.
553 /// @return `*this`.
554 constexpr Rational& operator+=(const Rational& rhs) noexcept {
555 if (addWouldOverflow(rhs)) {
556 reportOverflow("operator+=", "checkedAdd");
557 saturateToward(compareForSaturation(rhs, true), rhs.decimalPlaces);
558 return *this;
559 }
560 addAssignUnchecked(rhs);
561 return *this;
562 }
563
564 /// @brief In-place subtraction. Result precision becomes `max` of the two.
565 ///
566 /// Saturates rather than overflowing; see `operator+=`.
567 /// @param rhs Value to subtract.
568 /// @return `*this`.
569 constexpr Rational& operator-=(const Rational& rhs) noexcept {
570 if (subWouldOverflow(rhs)) {
571 reportOverflow("operator-=", "checkedSub");
572 saturateToward(compareForSaturation(rhs, false), rhs.decimalPlaces);
573 return *this;
574 }
575 subAssignUnchecked(rhs);
576 return *this;
577 }
578
579 /// @brief In-place multiplication. Result precision becomes `max` of the two.
580 /// Cross-cancels common factors before multiplying.
581 /// @param rhs Value to multiply by.
582 /// @return `*this`.
583 ///
584 /// Saturates rather than overflowing; see `operator+=`.
585 constexpr Rational& operator*=(const Rational& rhs) noexcept {
586 mulAssignSaturating(rhs, "operator*=", "checkedMul");
587 return *this;
588 }
589
590 /// @brief Non-throwing division. Result precision becomes `max` of the two.
591 ///
592 /// **Reports a zero divisor; saturates on overflow.** The `expected` says
593 /// *only* whether @p rhs was zero. Division is multiplication by the
594 /// divisor's reciprocal, and that product saturates exactly like
595 /// `operator*=`: a quotient too large for `int64_t` clamps to
596 /// `±INT64_MAX/1`, logs at `error`, and is still returned as a
597 /// **successful** `expected`. `INT64_MAX / (1/1000000)` is such a case.
598 /// A caller that must not absorb a clamped quotient uses `checkedDiv`,
599 /// which routes both failure modes through the one channel.
600 /// @param rhs Divisor.
601 /// @return `*this / rhs` — possibly saturated — or
602 /// `unexpected(DivisionByZero)` if @p rhs is zero.
603 [[nodiscard]] constexpr std::expected<Rational, RationalError> dividedBy(const Rational& rhs) const noexcept {
604 auto const divisorReciprocal = rhs.reciprocal();
605 if (!divisorReciprocal.has_value()) {
606 return std::unexpected(divisorReciprocal.error());
607 }
608 auto leftCopy = *this;
609 // Named for the caller's site, not `operator*=`: the divide is an
610 // implementation detail the caller never invoked, and `checkedMul` is
611 // not the remedy they can reach for.
612 leftCopy.mulAssignSaturating(*divisorReciprocal, "dividedBy", "checkedDiv");
613 return leftCopy;
614 }
615
616 /// @brief Flat JSON representation used by the Glaze codec below.
617 ///
618 /// Field names are the wire contract: `{"num":..,"den":..,"dp":..}`.
619 struct Wire {
620 std::int64_t num{0}; ///< Signed numerator as sent/received.
621 std::int64_t den{1}; ///< Denominator as sent/received; may be non-canonical.
622 std::uint32_t dp{1}; ///< Decimal-precision tag as sent/received.
623
624 /// @brief Whether these raw values decode without being clamped.
625 ///
626 /// Names the three clamps `setWire` would otherwise apply silently, so
627 /// "was this value altered on the way in?" is answerable *before* the
628 /// canonicalising constructor has already hidden the answer. A caller
629 /// that has its own decoded `Wire` can ask directly; the wire codec
630 /// asks on its behalf, and a `WireClampScope` around the decode acts on it.
631 ///
632 /// Note that a non-canonical but representable denominator (`4/8`, or
633 /// a negative `den`) is *valid*: reducing and sign-normalising it is
634 /// canonicalisation, not clamping, and round-trips the same value.
635 ///
636 /// @return `true` if the values survive decoding unaltered in magnitude.
637 [[nodiscard]] constexpr bool validate() const noexcept {
638 // `std::in_range` on the magnitude, rather than `!= INT64_MIN`:
639 // the actual requirement is that the component can be negated,
640 // which is to say its magnitude is representable as int64. That is
641 // what canonicalising needs, and it says so directly.
642 return den != 0 && dp <= kMaxDecimalPlaces && std::in_range<std::int64_t>(detail::absU64(num)) &&
643 std::in_range<std::int64_t>(detail::absU64(den));
644 }
645 };
646
647 /// @brief Wire-codec entry (Glaze read side): rebuilds through the
648 /// canonicalising constructor, clamping what it cannot represent.
649 ///
650 /// `den == 0`, an out-of-range `dp`, or a component whose magnitude does
651 /// not fit are clamped rather than rejected, so this never fails — which
652 /// means `{"num":5,"den":0,"dp":2}` decodes to a perfectly plausible
653 /// `5/1`, and nothing downstream can tell the value was altered.
654 ///
655 /// Rejecting here would be the wrong layer's call. Whether a clamped value
656 /// is a protocol violation or a harmless normalisation depends on where
657 /// the bytes came from, and this function cannot know. It records the fact
658 /// instead, for a `WireClampScope` around the decode to act on.
659 ///
660 /// @param wire Raw values decoded from JSON.
661 void setWire(Wire wire) noexcept {
662 if (!wire.validate()) {
663 ++detail::wireClampCounter();
664 }
665 constexpr auto int64Min = std::numeric_limits<std::int64_t>::min();
666 constexpr auto negatableMin = -std::numeric_limits<std::int64_t>::max();
667 *this = Rational{Numerator{wire.num == int64Min ? negatableMin : wire.num},
668 Denominator{wire.den == int64Min ? negatableMin : wire.den},
669 DecimalPlaces{detail::clampWireDecimalPlaces(wire.dp)}};
670 }
671
672 /// @brief Wire-codec exit (Glaze write side).
673 /// @return The canonical members, ready for JSON encoding.
674 [[nodiscard]] Wire getWire() const noexcept {
675 return Wire{.num = numerator, .den = denominator, .dp = decimalPlaces.value};
676 }
677
678private:
679 /// @brief Logs an overflow that `operator+=`/`-=`/`*=` saturated instead
680 /// of committing.
681 ///
682 /// Skipped during constant evaluation: `log` is not `constexpr`, and a
683 /// `constexpr` arithmetic expression that saturates should still compile.
684 ///
685 /// No local `try`/`catch` is needed around the logging call: `morph::log`
686 /// is `noexcept` (`docs/spec/core/logger.md`), so an arithmetic operator
687 /// cannot start failing because logging failed. This function carried that
688 /// workaround until morph#158 moved the guarantee into the logging layer.
689 /// The site and the remedy are both supplied by the caller rather than
690 /// hardcoded: `dividedBy` saturates *through* `operator*=`'s arithmetic,
691 /// and naming that as the site pointed a division caller at a function it
692 /// never called, alongside three remedies none of which is a division
693 /// (morph#206).
694 /// @param where Which operation saturated, as the caller spells it.
695 /// @param remedy The `checked*` helper that reports this case instead.
696 static constexpr void reportOverflow(std::string_view where, std::string_view remedy) noexcept {
697 if (!std::is_constant_evaluated()) {
698 ::morph::log::logError(
699 "[Rational] {} overflowed int64 and saturated; the result is clamped, "
700 "not exact. Use {} to detect this instead.",
701 where, remedy);
702 }
703 }
704
705 /// @brief `operator*=`'s arithmetic, saturating and logging under a
706 /// caller-chosen site and remedy name.
707 ///
708 /// Shared by `operator*=` and `dividedBy` so the two cannot drift: one
709 /// overflow predicate, one saturation rule, two log attributions.
710 /// @param rhs Value to multiply by.
711 /// @param where Operation name for the overflow log.
712 /// @param remedy The `checked*` helper named as the remedy in that log.
713 constexpr void mulAssignSaturating(const Rational& rhs, std::string_view where, std::string_view remedy) noexcept {
714 if (mulWouldOverflow(rhs)) {
715 reportOverflow(where, remedy);
716 // Sign of a product is the product of the signs; zero operands
717 // cannot overflow, so neither sign is zero here.
718 const bool negative = (numerator < 0) != (rhs.numerator < 0);
719 saturateToward(negative ? -1 : 1, rhs.decimalPlaces);
720 return;
721 }
722 mulAssignUnchecked(rhs);
723 }
724
725 /// @brief Logs an `INT64_MIN` component clamped by `canonicalise`.
726 ///
727 /// Skipped during constant evaluation, and non-throwing, like
728 /// `reportOverflow` -- see that function for why no local try/catch is
729 /// needed (morph#158 moved that guarantee into the logging layer).
730 static constexpr void reportClamp() noexcept {
731 if (!std::is_constant_evaluated()) {
732 ::morph::log::logError(
733 "[Rational] an INT64_MIN component was clamped to -INT64_MAX; "
734 "canonicalising it would require negating a value with no positive "
735 "counterpart.");
736 }
737 }
738
739 /// @brief The sign the saturated result should carry, for `+=` / `-=`.
740 ///
741 /// Determined by exact comparison rather than by computing the true sum,
742 /// which by definition does not fit. `a + b` has the sign of `a` versus
743 /// `-b`; `a - b` has the sign of `a` versus `b`. Both use the type's own
744 /// exact 128-bit-wide comparison, so a mixed-sign case whose cross-terms
745 /// overflow still saturates in the mathematically correct direction.
746 /// @param rhs The other operand.
747 /// @param addition `true` for `+=`, `false` for `-=`.
748 /// @return `-1`, `0` or `1`.
749 [[nodiscard]] constexpr int compareForSaturation(const Rational& rhs, bool addition) const noexcept {
750 const auto ordering = addition ? (*this <=> -rhs) : (*this <=> rhs);
751 // std::is_lt/is_gt, not `ordering < 0`: comparing an ordering against
752 // the literal 0 trips -Wzero-as-null-pointer-constant under
753 // -Weverything, which this repository builds with.
754 if (std::is_lt(ordering)) {
755 return -1;
756 }
757 return std::is_gt(ordering) ? 1 : 0;
758 }
759
760 /// @brief Clamps this value to the largest representable magnitude with
761 /// sign @p sign, at `max(decimalPlaces, other)`.
762 ///
763 /// Saturation, not an exception and not UB. `operator+` and friends are
764 /// used inside strand-bound model code and in `constexpr` expressions;
765 /// throwing would change their contract for every existing caller, while
766 /// leaving the overflow undefined is what this whole change exists to
767 /// stop. A clamped value is wrong, but it is *defined* wrong, it is
768 /// logged, and `checkedAdd`/`checkedSub`/`checkedMul`/`checkedDiv` remain
769 /// available for callers that need to detect the condition rather than
770 /// absorb it.
771 /// @param sign Direction to clamp toward; `0` yields zero.
772 /// @param other The other operand's precision, folded in as usual.
773 constexpr void saturateToward(int sign, DecimalPlaces other) noexcept {
774 constexpr auto maxValue = std::numeric_limits<std::int64_t>::max();
775 numerator = sign == 0 ? 0 : (sign < 0 ? -maxValue : maxValue);
776 denominator = 1;
777 widenPrecisionTo(other);
778 }
779
780 /// @brief The factors that scale `*this` and @p rhs's numerators (and
781 /// `*this`'s denominator) to their least common denominator.
782 struct DenominatorScale {
783 std::int64_t leftScaled;
784 std::int64_t rightScaled;
785 };
786
787 /// @brief Computes the scale factors for `a/b op c/d == (a*d' op c*b')/(b*d')`,
788 /// where `b' = b/gcd(b,d)`, `d' = d/gcd(b,d)`.
789 ///
790 /// The reduced cross-multiplication both `+=`/`-=`'s arithmetic and their
791 /// overflow checks need identically. Returns the bare factors rather than
792 /// the scaled numerators themselves: `addWouldOverflow`/`subWouldOverflow`
793 /// must run `detail::mulOverflows` on `numerator * rightScaled` *before*
794 /// forming that product, so a helper that returns the already-multiplied
795 /// values would have to compute the very product being checked for.
796 /// @param rhs The other operand.
797 /// @return `rightScaled` multiplies `this->numerator` and
798 /// `this->denominator`; `leftScaled` multiplies `rhs.numerator`
799 /// (see `addAssignUnchecked`, which applies them).
800 [[nodiscard]] constexpr DenominatorScale scaleFactorsFor(const Rational& rhs) const noexcept {
801 auto const denominatorGcd = std::gcd(denominator, rhs.denominator);
802 return DenominatorScale{
803 .leftScaled = denominator / denominatorGcd,
804 .rightScaled = rhs.denominator / denominatorGcd,
805 };
806 }
807
808 /// @brief `operator+=`'s arithmetic, without the overflow check.
809 /// @param rhs Value to add.
810 constexpr void addAssignUnchecked(const Rational& rhs) noexcept {
811 auto const scale = scaleFactorsFor(rhs);
812 numerator = (numerator * scale.rightScaled) + (rhs.numerator * scale.leftScaled);
813 denominator = denominator * scale.rightScaled;
814 widenPrecisionTo(rhs.decimalPlaces);
815 canonicalise();
816 }
817
818 /// @brief `operator-=`'s arithmetic, without the overflow check.
819 /// @param rhs Value to subtract.
820 constexpr void subAssignUnchecked(const Rational& rhs) noexcept {
821 auto const scale = scaleFactorsFor(rhs);
822 numerator = (numerator * scale.rightScaled) - (rhs.numerator * scale.leftScaled);
823 denominator = denominator * scale.rightScaled;
824 widenPrecisionTo(rhs.decimalPlaces);
825 canonicalise();
826 }
827
828 /// @brief `operator*=`'s arithmetic, without the overflow check.
829 /// @param rhs Value to multiply by.
830 constexpr void mulAssignUnchecked(const Rational& rhs) noexcept {
831 auto const absoluteLeftNumerator = numerator < 0 ? -numerator : numerator;
832 auto const absoluteRightNumerator = rhs.numerator < 0 ? -rhs.numerator : rhs.numerator;
833 auto const crossDivisorOne = std::gcd(absoluteLeftNumerator, rhs.denominator);
834 auto const crossDivisorTwo = std::gcd(absoluteRightNumerator, denominator);
835 auto const reducedLeftNumerator = numerator / crossDivisorOne;
836 auto const reducedRightNumerator = rhs.numerator / crossDivisorTwo;
837 auto const reducedLeftDenominator = denominator / crossDivisorTwo;
838 auto const reducedRightDenominator = rhs.denominator / crossDivisorOne;
839 numerator = reducedLeftNumerator * reducedRightNumerator;
840 denominator = reducedLeftDenominator * reducedRightDenominator;
841 widenPrecisionTo(rhs.decimalPlaces);
842 canonicalise();
843 }
844
845public:
846 /// @brief Applies `+=`'s arithmetic, having already proven it cannot overflow.
847 ///
848 /// For `checkedAdd`, which has just run `addWouldOverflow` and must not
849 /// re-enter `operator+=`'s saturating path. Calling this without that
850 /// check is undefined on overflow -- the check is the precondition.
851 /// @param rhs Value to add.
852 constexpr void addAssignChecked(const Rational& rhs) noexcept { addAssignUnchecked(rhs); }
853
854 /// @brief Applies `-=`'s arithmetic, having already proven it cannot overflow.
855 /// @param rhs Value to subtract.
856 constexpr void subAssignChecked(const Rational& rhs) noexcept { subAssignUnchecked(rhs); }
857
858 /// @brief Applies `*=`'s arithmetic, having already proven it cannot overflow.
859 /// @param rhs Value to multiply by.
860 constexpr void mulAssignChecked(const Rational& rhs) noexcept { mulAssignUnchecked(rhs); }
861
862 /// @brief Whether `*this + rhs` would overflow any intermediate or the result.
863 /// @param rhs The addend.
864 /// @return `true` if the addition cannot be performed exactly.
865 [[nodiscard]] constexpr bool addWouldOverflow(const Rational& rhs) const noexcept {
866 auto const scale = scaleFactorsFor(rhs);
867 if (detail::mulOverflows(numerator, scale.rightScaled) ||
868 detail::mulOverflows(rhs.numerator, scale.leftScaled) ||
869 detail::mulOverflows(denominator, scale.rightScaled)) {
870 return true;
871 }
872 return detail::addOverflows(numerator * scale.rightScaled, rhs.numerator * scale.leftScaled);
873 }
874
875 /// @brief Whether `*this - rhs` would overflow any intermediate or the result.
876 /// @param rhs The subtrahend.
877 /// @return `true` if the subtraction cannot be performed exactly.
878 [[nodiscard]] constexpr bool subWouldOverflow(const Rational& rhs) const noexcept {
879 auto const scale = scaleFactorsFor(rhs);
880 if (detail::mulOverflows(numerator, scale.rightScaled) ||
881 detail::mulOverflows(rhs.numerator, scale.leftScaled) ||
882 detail::mulOverflows(denominator, scale.rightScaled)) {
883 return true;
884 }
885 return detail::subOverflows(numerator * scale.rightScaled, rhs.numerator * scale.leftScaled);
886 }
887
888 /// @brief Whether `*this * rhs` would overflow, after cross-cancelling.
889 ///
890 /// Checks the cross-cancelled factors `operator*=` actually multiplies:
891 /// cross-cancelling is what keeps most products in range, so checking the
892 /// raw operands would reject pairs that multiply perfectly well.
893 /// @param rhs The factor.
894 /// @return `true` if the product cannot be represented.
895 [[nodiscard]] constexpr bool mulWouldOverflow(const Rational& rhs) const noexcept {
896 auto const absoluteLeftNumerator = numerator < 0 ? -numerator : numerator;
897 auto const absoluteRightNumerator = rhs.numerator < 0 ? -rhs.numerator : rhs.numerator;
898 auto const crossDivisorOne = std::gcd(absoluteLeftNumerator, rhs.denominator);
899 auto const crossDivisorTwo = std::gcd(absoluteRightNumerator, denominator);
900 if (crossDivisorOne == 0 || crossDivisorTwo == 0) {
901 return false; // a zero numerator: the product is zero
902 }
903 return detail::mulOverflows(numerator / crossDivisorOne, rhs.numerator / crossDivisorTwo) ||
904 detail::mulOverflows(denominator / crossDivisorTwo, rhs.denominator / crossDivisorOne);
905 }
906
907private:
908 /// @brief Restores the canonical-form invariants in place (denominator > 0,
909 /// gcd reduced, zero denominator clamped to 1). Leaves `decimalPlaces`
910 /// untouched (it is not a value property).
911 constexpr void canonicalise() noexcept {
912 if (denominator == 0) {
913 denominator = 1;
914 return;
915 }
916 constexpr auto minValue = std::numeric_limits<std::int64_t>::min();
917 constexpr auto maxValue = std::numeric_limits<std::int64_t>::max();
918
919 // Neither component may be INT64_MIN past this point: canonicalising
920 // needs `|value|` and a sign flip, and `-INT64_MIN` is not
921 // representable -- negating it is undefined behaviour. INT64_MIN is
922 // reachable two ways: constructing a Rational with such a numerator
923 // directly, and *ordinary arithmetic* landing on it exactly
924 // (`-INT64_MAX - 1` is a perfectly legal subtraction whose result is
925 // INT64_MIN).
926 //
927 // Clamped to the adjacent representable magnitude, matching what
928 // `setWire` does for the same values arriving off the wire. The value
929 // is off by one ulp; it is not undefined.
930 if (numerator == minValue || denominator == minValue) {
931 reportClamp();
932 numerator = numerator == minValue ? -maxValue : numerator;
933 denominator = denominator == minValue ? -maxValue : denominator;
934 }
935
936 // Flips the sign via unsigned subtraction from zero rather than
937 // signed negation: both components are already clamped off
938 // INT64_MIN above, so `-numerator`/`-denominator` would be
939 // well-defined here, but it is exactly the negate-under-sign-check
940 // shape GCC's `-Wstrict-overflow` flags once this function is
941 // inlined at `-O3`.
942 if (denominator < 0) {
943 numerator = static_cast<std::int64_t>(0ULL - static_cast<std::uint64_t>(numerator));
944 denominator = static_cast<std::int64_t>(0ULL - static_cast<std::uint64_t>(denominator));
945 }
946 // Magnitudes via `absU64`, so the gcd never negates either component.
947 // The result cannot exceed INT64_MAX: gcd(a, b) <= min(a, b) and the
948 // denominator is at most INT64_MAX here.
949 auto const divisor =
950 static_cast<std::int64_t>(std::gcd(detail::absU64(numerator), detail::absU64(denominator)));
951 if (divisor > 1) {
952 numerator /= divisor;
953 denominator /= divisor;
954 }
955 }
956
957 /// @brief Sets `decimalPlaces` to `max(this, other)`.
958 constexpr void widenPrecisionTo(DecimalPlaces other) noexcept {
959 if (other.value > decimalPlaces.value) {
960 decimalPlaces = other;
961 }
962 }
963};
964
965// Sanity checks: trivially copyable and standard layout.
966static_assert(std::is_trivially_copyable_v<Rational>);
967static_assert(std::is_standard_layout_v<Rational>);
968
969// ---------------------------------------------------------------------------
970// Free-function rounding helpers (live next to Rational so ADL finds them).
971// ---------------------------------------------------------------------------
972
973/// @brief Absolute value of a rational. Precision is preserved.
974/// @param value Value to take the absolute value of.
975/// @return The non-negative value with the same magnitude.
976[[nodiscard]] constexpr Rational abs(const Rational& value) noexcept {
977 return value.numerator < 0
978 ? Rational{Numerator{-value.numerator}, Denominator{value.denominator}, value.decimalPlaces}
979 : value;
980}
981
982/// @brief Rounds toward positive infinity.
983/// @param value Value to round.
984/// @return The smallest integer `>= value`.
985[[nodiscard]] constexpr std::int64_t ceil(const Rational& value) noexcept {
986 auto const quotient = value.numerator / value.denominator;
987 auto const remainder = value.numerator % value.denominator;
988 if (remainder != 0 && value.numerator > 0) {
989 return quotient + 1;
990 }
991 return quotient;
992}
993
994/// @brief Rounds toward negative infinity.
995/// @param value Value to round.
996/// @return The largest integer `<= value`.
997[[nodiscard]] constexpr std::int64_t floor(const Rational& value) noexcept {
998 auto const quotient = value.numerator / value.denominator;
999 auto const remainder = value.numerator % value.denominator;
1000 if (remainder != 0 && value.numerator < 0) {
1001 return quotient - 1;
1002 }
1003 return quotient;
1004}
1005
1006/// @brief Truncates toward zero.
1007/// @param value Value to truncate.
1008/// @return The integer part of the value.
1009[[nodiscard]] constexpr std::int64_t trunc(const Rational& value) noexcept {
1010 return value.numerator / value.denominator;
1011}
1012
1013/// @brief Rounds to a fixed number of decimal places **exactly, without leaving
1014/// the `Rational` domain** -- the operation `ceil`/`floor`/`trunc` do not
1015/// offer, since those return `std::int64_t` and so only ever round to
1016/// zero decimal places.
1017///
1018/// This changes the **value**, not merely the `decimalPlaces` tag: the result is
1019/// the multiple of `10^-places` nearest @p value, ties resolved by @p mode, and
1020/// it is tagged at @p places so display and storage agree digit for digit. A
1021/// value already representable at @p places decimals is returned unchanged
1022/// except for that tag, so the common case is exact and cannot overflow.
1023///
1024/// **Saturates rather than overflowing**, like every other `Rational`
1025/// arithmetic path: rounding scales by `10^places`, so a value whose scaled form
1026/// exceeds `int64` range clamps to `+/-INT64_MAX` and logs at `error` (see
1027/// `operator*=`). A caller who must detect that has to range-check first --
1028/// there is no `checkedRound`.
1029///
1030/// @param value Value to round.
1031/// @param places Target decimal places; silently clamped to `[0, kMaxDecimalPlaces]`.
1032/// @param mode Tie-breaking rule. Defaults to half away from zero, matching
1033/// morph's display path.
1034/// @return The rounded value, tagged at @p places.
1035[[nodiscard]] constexpr Rational roundToDecimalPlaces(const Rational& value, DecimalPlaces places,
1036 RoundingMode mode = RoundingMode::HalfAwayFromZero) noexcept;
1037
1038// ---------------------------------------------------------------------------
1039// Plain Rational arithmetic (max-precision propagation).
1040// ---------------------------------------------------------------------------
1041
1042/// @brief Adds two Rationals. Result precision is `max` of the two.
1043/// @param lhs Left operand.
1044/// @param rhs Right operand.
1045/// @return The exact sum.
1046[[nodiscard]] constexpr Rational operator+(const Rational& lhs, const Rational& rhs) noexcept {
1047 auto result = lhs;
1048 result += rhs;
1049 return result;
1050}
1051
1052/// @brief Subtracts two Rationals. Result precision is `max` of the two.
1053/// @param lhs Left operand.
1054/// @param rhs Right operand.
1055/// @return The exact difference.
1056[[nodiscard]] constexpr Rational operator-(const Rational& lhs, const Rational& rhs) noexcept {
1057 auto result = lhs;
1058 result -= rhs;
1059 return result;
1060}
1061
1062/// @brief Multiplies two Rationals. Result precision is `max` of the two.
1063/// @param lhs Left operand.
1064/// @param rhs Right operand.
1065/// @return The exact product.
1066[[nodiscard]] constexpr Rational operator*(const Rational& lhs, const Rational& rhs) noexcept {
1067 auto result = lhs;
1068 result *= rhs;
1069 return result;
1070}
1071
1072/// @brief Divides two Rationals. Result precision is `max` of the two.
1073///
1074/// Saturates on overflow and still reports success; see `Rational::dividedBy`,
1075/// and `checkedDiv` for the exact-or-nothing form.
1076/// @param lhs Dividend.
1077/// @param rhs Divisor.
1078/// @return `lhs / rhs` — possibly saturated — or `unexpected(DivisionByZero)`
1079/// if @p rhs is zero.
1080[[nodiscard]] constexpr std::expected<Rational, RationalError> operator/(const Rational& lhs,
1081 const Rational& rhs) noexcept {
1082 return lhs.dividedBy(rhs);
1083}
1084
1085/// @brief Adds two Rationals, reporting overflow instead of saturating.
1086///
1087/// `operator+` saturates and logs when the exact sum does not fit, so it is
1088/// never undefined -- but it is also silently inexact. This returns the
1089/// overflow instead, for callers that must not absorb it: a ledger totalling
1090/// rows needs to *stop*, not to carry on with a clamped balance.
1091///
1092/// Every intermediate is checked before any is formed, since detecting signed
1093/// overflow by performing it is itself undefined. Note the cross-terms are the
1094/// tighter bound: they can overflow while the final result would have been
1095/// perfectly representable, which is exactly the case a caller cannot spot by
1096/// inspecting the answer.
1097///
1098/// @param lhs Left addend.
1099/// @param rhs Right addend.
1100/// @return The exact sum, or `unexpected(RationalError::Overflow)`.
1101[[nodiscard]] constexpr std::expected<Rational, RationalError> checkedAdd(const Rational& lhs,
1102 const Rational& rhs) noexcept {
1103 if (lhs.addWouldOverflow(rhs)) {
1104 return std::unexpected(RationalError::Overflow);
1105 }
1106 auto result = lhs;
1107 result.addAssignChecked(rhs);
1108 return result;
1109}
1110
1111/// @brief Subtracts two Rationals, reporting overflow instead of saturating.
1112///
1113/// The `checkedAdd` counterpart; see that function.
1114/// @param lhs Minuend.
1115/// @param rhs Subtrahend.
1116/// @return The exact difference, or `unexpected(RationalError::Overflow)`.
1117[[nodiscard]] constexpr std::expected<Rational, RationalError> checkedSub(const Rational& lhs,
1118 const Rational& rhs) noexcept {
1119 if (lhs.subWouldOverflow(rhs)) {
1120 return std::unexpected(RationalError::Overflow);
1121 }
1122 auto result = lhs;
1123 result.subAssignChecked(rhs);
1124 return result;
1125}
1126
1127/// @brief Multiplies two Rationals, reporting overflow instead of saturating.
1128///
1129/// Checks the cross-cancelled factors `operator*` actually multiplies, not the
1130/// raw operands -- see `Rational::mulWouldOverflow`.
1131/// @param lhs Left factor.
1132/// @param rhs Right factor.
1133/// @return The exact product, or `unexpected(RationalError::Overflow)`.
1134[[nodiscard]] constexpr std::expected<Rational, RationalError> checkedMul(const Rational& lhs,
1135 const Rational& rhs) noexcept {
1136 if (lhs.mulWouldOverflow(rhs)) {
1137 return std::unexpected(RationalError::Overflow);
1138 }
1139 auto result = lhs;
1140 result.mulAssignChecked(rhs);
1141 return result;
1142}
1143
1144/// @brief Divides two Rationals, reporting overflow instead of saturating.
1145///
1146/// The escape hatch division was missing (morph#206). `dividedBy` and
1147/// `operator/` already return `std::expected`, but they spend it on the zero
1148/// divisor alone: an overflowing quotient saturates and comes back as a
1149/// *successful* `expected` holding a clamp, so a caller doing the right thing
1150/// and checking the result is told the division succeeded. This reports both
1151/// failure modes through the one channel.
1152///
1153/// Division is multiplication by the divisor's reciprocal, so this is exactly
1154/// `checkedMul` against `rhs.reciprocal()` — the same operand pair
1155/// `dividedBy` forms internally, checked by the same `mulWouldOverflow`
1156/// predicate the operator uses. The operators and the checked forms therefore
1157/// cannot disagree about which quotients fit.
1158///
1159/// @param lhs Dividend.
1160/// @param rhs Divisor.
1161/// @return The exact quotient; `unexpected(RationalError::DivisionByZero)` if
1162/// @p rhs is zero; `unexpected(RationalError::Overflow)` if the
1163/// quotient is not representable.
1164[[nodiscard]] constexpr std::expected<Rational, RationalError> checkedDiv(const Rational& lhs,
1165 const Rational& rhs) noexcept {
1166 auto const divisorReciprocal = rhs.reciprocal();
1167 if (!divisorReciprocal.has_value()) {
1168 return std::unexpected(divisorReciprocal.error());
1169 }
1170 return checkedMul(lhs, *divisorReciprocal);
1171}
1172
1173// ---------------------------------------------------------------------------
1174// Mixed-type arithmetic with automatic expected-propagation.
1175// ---------------------------------------------------------------------------
1176
1177namespace detail {
1178
1179using ExpectedRational = std::expected<Rational, RationalError>;
1180
1181/// @brief Concept: matches `Rational` (after cvref-stripping).
1182template <typename T>
1183concept IsRational = std::same_as<std::remove_cvref_t<T>, Rational>;
1184
1185/// @brief Concept: matches `std::expected<Rational, RationalError>`.
1186template <typename T>
1187concept IsExpectedRational = std::same_as<std::remove_cvref_t<T>, ExpectedRational>;
1188
1189/// @brief Concept: matches any standard floating-point type.
1190template <typename T>
1191concept IsFloat = std::floating_point<std::remove_cvref_t<T>>;
1192
1193/// @brief Concept: a Rational-family operand (Rational or ExpectedRational).
1194template <typename T>
1195concept RationalLike = IsRational<T> || IsExpectedRational<T>;
1196
1197/// @brief Concept: an operand the mixed-type overload accepts.
1198template <typename T>
1199concept LiftableOperand = RationalLike<T> || IsFloat<T>;
1200
1201/// @brief Concept: an operand whose presence forces the mixed-type overload to fire.
1202template <typename T>
1203concept NeedsLifting = IsExpectedRational<T> || IsFloat<T>;
1204
1205/// @brief Lifts a `Rational` to `ExpectedRational` (always succeeds).
1206[[nodiscard]] constexpr ExpectedRational lift(const Rational& value, DecimalPlaces /*wantedPrecision*/) noexcept {
1207 return ExpectedRational{value};
1208}
1209
1210/// @brief Forwards an `ExpectedRational` unchanged.
1211[[nodiscard]] constexpr ExpectedRational lift(const ExpectedRational& value,
1212 DecimalPlaces /*wantedPrecision*/) noexcept {
1213 return value;
1214}
1215
1216/// @brief Lifts a floating-point value via `Rational::fromFloat` at @p fallback precision.
1217template <std::floating_point Float>
1218[[nodiscard]] inline ExpectedRational lift(Float value, DecimalPlaces fallback) noexcept {
1219 return Rational::fromFloat(value, fallback);
1220}
1221
1222/// @brief The precision to lift a float operand to: the Rational-family
1223/// operand's precision. Picks whichever side carries one (at least one
1224/// always does).
1225template <typename Left, typename Right>
1226[[nodiscard]] constexpr DecimalPlaces liftPrecision(const Left& lhs, const Right& rhs) noexcept {
1227 if constexpr (IsRational<Left>) {
1228 return lhs.decimalPlaces;
1229 } else if constexpr (IsExpectedRational<Left>) {
1230 return lhs ? lhs->decimalPlaces : DecimalPlaces{1};
1231 } else if constexpr (IsRational<Right>) {
1232 return rhs.decimalPlaces;
1233 } else if constexpr (IsExpectedRational<Right>) {
1234 return rhs ? rhs->decimalPlaces : DecimalPlaces{1};
1235 } else {
1236 return DecimalPlaces{1};
1237 }
1238}
1239
1240/// @brief Lifts both mixed-type operands to `Rational`, at their shared precision.
1241///
1242/// Shared by the free `+`/`-`/`*`/`/` operators below: each lifts `lhs` then
1243/// `rhs` to `liftPrecision(lhs, rhs)`, propagating the first error encountered
1244/// left to right, and differs only in which operator it applies to the two
1245/// lifted values afterward.
1246/// @param lhs Left operand (Rational, expected-Rational, or floating point).
1247/// @param rhs Right operand (Rational, expected-Rational, or floating point).
1248/// @return Both operands as `Rational`, or the first error encountered left to right.
1249template <typename Left, typename Right>
1250[[nodiscard]] inline std::expected<std::pair<Rational, Rational>, RationalError> liftBoth(const Left& lhs,
1251 const Right& rhs) noexcept {
1252 auto const precision = liftPrecision(lhs, rhs);
1253 auto const leftExpected = lift(lhs, precision);
1254 if (!leftExpected) {
1255 return std::unexpected(leftExpected.error());
1256 }
1257 auto const rightExpected = lift(rhs, precision);
1258 if (!rightExpected) {
1259 return std::unexpected(rightExpected.error());
1260 }
1261 return std::pair<Rational, Rational>{*leftExpected, *rightExpected};
1262}
1263
1264} // namespace detail
1265
1266/// @brief Mixed-type addition with automatic expected-propagation.
1267/// @param lhs Left operand (Rational, expected-Rational, or floating point).
1268/// @param rhs Right operand (Rational, expected-Rational, or floating point).
1269/// @return The exact sum, or the first error encountered left to right.
1270template <typename Left, typename Right>
1271 requires(detail::RationalLike<Left> || detail::RationalLike<Right>) &&
1272 (detail::NeedsLifting<Left> || detail::NeedsLifting<Right>) &&
1273 (detail::LiftableOperand<Left> && detail::LiftableOperand<Right>)
1274[[nodiscard]] inline detail::ExpectedRational operator+(const Left& lhs, const Right& rhs) noexcept {
1275 auto const lifted = detail::liftBoth(lhs, rhs);
1276 if (!lifted) {
1277 return std::unexpected(lifted.error());
1278 }
1279 return lifted->first + lifted->second;
1280}
1281
1282/// @brief Mixed-type subtraction with automatic expected-propagation.
1283/// @param lhs Left operand (Rational, expected-Rational, or floating point).
1284/// @param rhs Right operand (Rational, expected-Rational, or floating point).
1285/// @return The exact difference, or the first error encountered left to right.
1286template <typename Left, typename Right>
1287 requires(detail::RationalLike<Left> || detail::RationalLike<Right>) &&
1288 (detail::NeedsLifting<Left> || detail::NeedsLifting<Right>) &&
1289 (detail::LiftableOperand<Left> && detail::LiftableOperand<Right>)
1290[[nodiscard]] inline detail::ExpectedRational operator-(const Left& lhs, const Right& rhs) noexcept {
1291 auto const lifted = detail::liftBoth(lhs, rhs);
1292 if (!lifted) {
1293 return std::unexpected(lifted.error());
1294 }
1295 return lifted->first - lifted->second;
1296}
1297
1298/// @brief Mixed-type multiplication with automatic expected-propagation.
1299/// @param lhs Left operand (Rational, expected-Rational, or floating point).
1300/// @param rhs Right operand (Rational, expected-Rational, or floating point).
1301/// @return The exact product, or the first error encountered left to right.
1302template <typename Left, typename Right>
1303 requires(detail::RationalLike<Left> || detail::RationalLike<Right>) &&
1304 (detail::NeedsLifting<Left> || detail::NeedsLifting<Right>) &&
1305 (detail::LiftableOperand<Left> && detail::LiftableOperand<Right>)
1306[[nodiscard]] inline detail::ExpectedRational operator*(const Left& lhs, const Right& rhs) noexcept {
1307 auto const lifted = detail::liftBoth(lhs, rhs);
1308 if (!lifted) {
1309 return std::unexpected(lifted.error());
1310 }
1311 return lifted->first * lifted->second;
1312}
1313
1314/// @brief Mixed-type division with automatic expected-propagation.
1315/// @param lhs Dividend (Rational, expected-Rational, or floating point).
1316/// @param rhs Divisor (Rational, expected-Rational, or floating point).
1317/// @return The exact quotient, or the first error encountered left to right.
1318template <typename Left, typename Right>
1319 requires(detail::RationalLike<Left> || detail::RationalLike<Right>) &&
1320 (detail::NeedsLifting<Left> || detail::NeedsLifting<Right>) &&
1321 (detail::LiftableOperand<Left> && detail::LiftableOperand<Right>)
1322[[nodiscard]] inline detail::ExpectedRational operator/(const Left& lhs, const Right& rhs) noexcept {
1323 auto const lifted = detail::liftBoth(lhs, rhs);
1324 if (!lifted) {
1325 return std::unexpected(lifted.error());
1326 }
1327 return lifted->first / lifted->second;
1328}
1329
1330// ---------------------------------------------------------------------------
1331// fromFloat and toDouble out-of-class implementations.
1332// ---------------------------------------------------------------------------
1333
1334namespace detail {
1335
1336/// @brief Computes `10^decimalPlaces` as `std::int64_t`. Returns `0` on overflow.
1337[[nodiscard]] constexpr std::int64_t powerOfTen(std::uint32_t decimalPlaces) noexcept {
1338 if (decimalPlaces > 18) {
1339 return 0;
1340 }
1341 auto result = std::int64_t{1};
1342 for (std::uint32_t digit = 0; digit < decimalPlaces; ++digit) {
1343 result *= 10;
1344 }
1345 return result;
1346}
1347
1348/// @brief Shared implementation of the three `Rational::fromFloat` overloads.
1349template <std::floating_point Float>
1350[[nodiscard]] inline ExpectedRational fromFloatImpl(Float value, DecimalPlaces wantedPrecision) noexcept {
1351 if (!std::isfinite(value)) {
1352 return std::unexpected(RationalError::NotFinite);
1353 }
1354
1355 auto const rawPrecision = clampDecimalPlaces(wantedPrecision.value);
1356 // rawPrecision is clamped to <= kMaxDecimalPlaces, so powerOfTen cannot
1357 // return its overflow sentinel here.
1358 auto const scale = powerOfTen(rawPrecision);
1359
1360 auto const scaled = static_cast<long double>(value) * static_cast<long double>(scale);
1361 // Bound against 2^63 (representable in every long double format), minus
1362 // the half-ulp llround adds: values in [2^63 - 0.5, 2^63) round *up* to
1363 // 2^63 and would overflow int64. Casting INT64_MAX instead would itself
1364 // round up to 2^63 where long double == double. The negative bound is
1365 // asymmetric because llround maps (-2^63 - 0.5, -2^63] onto INT64_MIN,
1366 // hence `<` against -2^63 exactly. Note the accepted INT64_MIN does not
1367 // survive as such: the constructor below canonicalises, and canonicalise()
1368 // clamps it to -INT64_MAX with an error log (see reportClamp).
1369 constexpr auto twoPow63 = 0x1p63L;
1370 if (scaled >= twoPow63 - 0.5L || scaled < -twoPow63) {
1371 return std::unexpected(RationalError::Overflow);
1372 }
1373
1374 auto const scaledNumerator = static_cast<std::int64_t>(std::llround(scaled));
1375 return Rational{Numerator{scaledNumerator}, Denominator{scale}, DecimalPlaces{rawPrecision}};
1376}
1377
1378} // namespace detail
1379
1380constexpr Rational roundToDecimalPlaces(const Rational& value, DecimalPlaces places, RoundingMode mode) noexcept {
1381 auto const wanted = detail::clampWireDecimalPlaces(places.value);
1382 // `wanted <= kMaxDecimalPlaces` after the clamp, so this never returns the
1383 // overflow sentinel and `scale >= 1`.
1384 auto const scale = detail::powerOfTen(wanted);
1385
1386 // Fast, exact, overflow-free path: `value * 10^wanted` is an integer exactly
1387 // when the canonical denominator divides `10^wanted`. Nothing to round --
1388 // only the tag moves. This also covers every integer-valued Rational, which
1389 // is what keeps a large `n/1` from saturating on the scale-up below.
1390 if (scale % value.denominator == 0) {
1391 auto exact = value;
1392 exact.decimalPlaces = DecimalPlaces{wanted};
1393 return exact;
1394 }
1395
1396 auto const scaled = value * Rational{Numerator{scale}, Denominator{1}, DecimalPlaces{wanted}};
1397 auto const whole = trunc(scaled);
1398 auto const fraction = abs(scaled - Rational{whole, DecimalPlaces{wanted}});
1399 auto const tie = fraction <=> Rational{Numerator{1}, Denominator{2}, DecimalPlaces{wanted}};
1400
1401 bool roundAway = std::is_gt(tie);
1402 if (std::is_eq(tie)) {
1403 // `whole` is the scaled integer, so its parity is the parity of the last
1404 // digit that survives the rounding.
1405 roundAway = mode == RoundingMode::HalfAwayFromZero || (whole % 2) != 0;
1406 }
1407
1408 auto stepped = whole;
1409 if (roundAway) {
1410 auto const step = scaled.numerator < 0 ? std::int64_t{-1} : std::int64_t{1};
1411 // Unreachable unless the scale-up already saturated; stepping past
1412 // INT64_MAX would be UB, so decline rather than commit it.
1413 if (!detail::addOverflows(whole, step)) {
1414 stepped = whole + step;
1415 }
1416 }
1417 return Rational{Numerator{stepped}, Denominator{scale}, DecimalPlaces{wanted}};
1418}
1419
1420inline std::expected<Rational, RationalError> Rational::fromFloat(double value,
1421 DecimalPlaces wantedPrecision) noexcept {
1422 return detail::fromFloatImpl(value, wantedPrecision);
1423}
1424
1425inline std::expected<Rational, RationalError> Rational::fromFloat(float value,
1426 DecimalPlaces wantedPrecision) noexcept {
1427 return detail::fromFloatImpl(value, wantedPrecision);
1428}
1429
1430inline std::expected<Rational, RationalError> Rational::fromFloat(long double value,
1431 DecimalPlaces wantedPrecision) noexcept {
1432 return detail::fromFloatImpl(value, wantedPrecision);
1433}
1434
1435inline double Rational::toDouble(std::uint32_t requestedDecimalPlaces) const noexcept {
1436 auto const raw = static_cast<double>(numerator) / static_cast<double>(denominator);
1437 auto const scale = detail::powerOfTen(requestedDecimalPlaces);
1438 if (scale == 0) {
1439 return raw;
1440 }
1441 auto const scaledDouble = static_cast<double>(scale);
1442 return std::round(raw * scaledDouble) / scaledDouble;
1443}
1444
1445} // namespace morph::math
1446
1447// ---------------------------------------------------------------------------
1448// Glaze wire codec: {"num":..,"den":..,"dp":..}, canonicalised on read.
1449// ---------------------------------------------------------------------------
1450
1451/// @brief Routes Rational serialisation through `setWire`/`getWire` so every
1452/// deserialised value passes the canonicalising constructor.
1453template <>
1454struct glz::meta<morph::math::Rational> {
1455 static constexpr auto value = glz::custom<&morph::math::Rational::setWire, &morph::math::Rational::getWire>;
1456 static constexpr std::string_view name = "Rational";
1457};
1458
1459namespace glz::detail {
1460
1461/// @brief Schema for Rational: the `Wire` object shape. Without this the
1462/// `glz::custom` codec above would degrade the generated schema to
1463/// "accepts anything".
1464template <>
1465struct to_json_schema<morph::math::Rational> {
1466 template <auto Opts>
1467 static void op(auto& outSchema, auto& defs) {
1468 to_json_schema<morph::math::Rational::Wire>::template op<Opts>(outSchema, defs);
1469 }
1470};
1471
1472} // namespace glz::detail
1473
1474/// @brief Stable shape tag for `Rational`, so a payload fingerprint can tell a
1475/// `Rational` field from a `Timestamp` or a `Quantity` one.
1476///
1477/// Its `glz::meta` above routes serialisation through `setWire`/`getWire`, so
1478/// there are no reflected members for `payloadShape` to decompose and the
1479/// field would otherwise render as the bare opaque tag — making a retype
1480/// between two such types invisible to `journal::replay()`'s mismatch gate.
1481/// The name is spelled here rather than derived from `glz::name_v`, which is
1482/// compiler-dependent. See `morph/core/payload_shape_tag.hpp`.
1483template <>
1485 /// @brief This type's stable shape name.
1486 /// @return `"rational"`.
1487 static constexpr std::string_view name() noexcept { return "rational"; }
1488};
1489
1490// ---------------------------------------------------------------------------
1491// std::format support.
1492// ---------------------------------------------------------------------------
1493
1494/// @brief std::format support for `morph::math::Rational`.
1495///
1496/// Empty specs print the exact rational form (`n/d`, or `n` when integer).
1497/// Non-empty specs are forwarded to `std::formatter<double>` on `toDouble()`.
1498template <>
1499struct std::formatter<morph::math::Rational> {
1500 bool delegateToDouble{false};
1501 std::formatter<double> doubleFormatter;
1502
1503 constexpr auto parse(std::format_parse_context& ctx) {
1504 if (ctx.begin() == ctx.end() || *ctx.begin() == '}') {
1505 return ctx.begin();
1506 }
1507 delegateToDouble = true;
1508 return doubleFormatter.parse(ctx);
1509 }
1510
1511 auto format(const morph::math::Rational& value, std::format_context& ctx) const {
1512 if (delegateToDouble) {
1513 return doubleFormatter.format(value.toDouble(), ctx);
1514 }
1515 if (value.denominator == 1) {
1516 return std::format_to(ctx.out(), "{}", value.numerator);
1517 }
1518 return std::format_to(ctx.out(), "{}/{}", value.numerator, value.denominator);
1519 }
1520};
Handle< std::string > format(AnyHandle const &pattern, std::vector< AnyHandle > args)
format: a template with {0}, {1}, … placeholders.
Definition author.hpp:970
WireClampScope() noexcept
Starts a fresh count, saving any enclosing scope's.
Definition rational.hpp:234
~WireClampScope()
Folds this scope's count back into the enclosing one.
Definition rational.hpp:242
std::size_t clamped() const noexcept
How many Rational values were clamped so far in this scope.
Definition rational.hpp:246
Opt-in, compiler-independent shape tag for a type with a custom Glaze codec.
constexpr std::expected< Rational, RationalError > checkedMul(const Rational &lhs, const Rational &rhs) noexcept
Multiplies two Rationals, reporting overflow instead of saturating.
constexpr std::expected< Rational, RationalError > checkedAdd(const Rational &lhs, const Rational &rhs) noexcept
Adds two Rationals, reporting overflow instead of saturating.
constexpr Rational operator*(const Rational &lhs, const Rational &rhs) noexcept
Multiplies two Rationals. Result precision is max of the two.
constexpr std::int64_t trunc(const Rational &value) noexcept
Truncates toward zero.
constexpr Rational roundToDecimalPlaces(const Rational &value, DecimalPlaces places, RoundingMode mode=RoundingMode::HalfAwayFromZero) noexcept
Rounds to a fixed number of decimal places exactly, without leaving the Rational domain – the operati...
constexpr std::int64_t ceil(const Rational &value) noexcept
Rounds toward positive infinity.
Definition rational.hpp:985
constexpr std::expected< Rational, RationalError > checkedDiv(const Rational &lhs, const Rational &rhs) noexcept
Divides two Rationals, reporting overflow instead of saturating.
RoundingMode
Rule for resolving a value sitting exactly halfway between the two nearest results when rounding to a...
Definition rational.hpp:190
@ HalfEven
Ties go to the neighbour whose last kept digit is even: 1.25 -> 1.2, 1.35 -> 1.4.
Definition rational.hpp:192
@ HalfAwayFromZero
Ties go to the neighbour of larger magnitude: 1.25 -> 1.3, -1.25 -> -1.3.
Definition rational.hpp:191
constexpr Rational operator-(const Rational &lhs, const Rational &rhs) noexcept
Subtracts two Rationals. Result precision is max of the two.
constexpr std::uint32_t kMaxDecimalPlaces
Largest decimal precision the type supports: 10^18 is the greatest power of ten that fits in std::int...
Definition rational.hpp:167
constexpr std::expected< Rational, RationalError > operator/(const Rational &lhs, const Rational &rhs) noexcept
Divides two Rationals. Result precision is max of the two.
constexpr std::expected< Rational, RationalError > checkedSub(const Rational &lhs, const Rational &rhs) noexcept
Subtracts two Rationals, reporting overflow instead of saturating.
RationalError
Error states reachable through Rational operations.
Definition rational.hpp:170
@ DivisionByZero
Divisor numerator is zero (operator/, reciprocal, From).
Definition rational.hpp:171
@ NotFinite
Floating-point input was NaN or +/-Inf (fromFloat only).
Definition rational.hpp:172
constexpr std::int64_t floor(const Rational &value) noexcept
Rounds toward negative infinity.
Definition rational.hpp:997
constexpr Rational operator+(const Rational &lhs, const Rational &rhs) noexcept
Adds two Rationals. Result precision is max of the two.
constexpr Rational abs(const Rational &value) noexcept
Absolute value of a rational. Precision is preserved.
Definition rational.hpp:976
Strong type for a decimal-precision count.
Definition rational.hpp:121
constexpr auto operator<=>(const DecimalPlaces &other) const noexcept=default
Value ordering/equality on the raw digit count.
std::uint32_t value
Raw digit count. Rational's invariant keeps it in [0, kMaxDecimalPlaces].
Definition rational.hpp:123
Strong type for a Rational denominator.
Definition rational.hpp:154
std::int64_t value
Raw signed integer value; must never be zero after canonicalisation.
Definition rational.hpp:156
Strong type for a Rational numerator.
Definition rational.hpp:140
std::int64_t value
Raw signed integer value.
Definition rational.hpp:142
Flat JSON representation used by the Glaze codec below.
Definition rational.hpp:619
constexpr bool validate() const noexcept
Whether these raw values decode without being clamped.
Definition rational.hpp:637
std::int64_t den
Denominator as sent/received; may be non-canonical.
Definition rational.hpp:621
std::uint32_t dp
Decimal-precision tag as sent/received.
Definition rational.hpp:622
std::int64_t num
Signed numerator as sent/received.
Definition rational.hpp:620
Exact rational number with a runtime decimal-precision tag.
Definition rational.hpp:375
constexpr bool isZero() const noexcept
Whether the value equals 0/1.
Definition rational.hpp:466
constexpr std::strong_ordering operator<=>(const Rational &other) const noexcept
Three-way comparison. Value-only: ignores decimalPlaces.
Definition rational.hpp:510
constexpr void subAssignChecked(const Rational &rhs) noexcept
Applies -='s arithmetic, having already proven it cannot overflow.
Definition rational.hpp:856
double toDouble() const noexcept
Converts to double, rounded to this value's decimalPlaces.
Definition rational.hpp:478
constexpr bool isNegative() const noexcept
Whether the value is strictly less than zero.
Definition rational.hpp:474
static constexpr std::expected< Rational, RationalError > from(Numerator wantedNumerator, Denominator wantedDenominator, DecimalPlaces wantedPrecision) noexcept
Validating factory.
Definition rational.hpp:416
constexpr std::expected< Rational, RationalError > dividedBy(const Rational &rhs) const noexcept
Non-throwing division. Result precision becomes max of the two.
Definition rational.hpp:603
constexpr bool mulWouldOverflow(const Rational &rhs) const noexcept
Whether *this * rhs would overflow, after cross-cancelling.
Definition rational.hpp:895
constexpr bool isInteger() const noexcept
Whether the value is an integer.
Definition rational.hpp:470
constexpr std::expected< Rational, RationalError > reciprocal() const noexcept
Multiplicative inverse.
Definition rational.hpp:494
constexpr bool addWouldOverflow(const Rational &rhs) const noexcept
Whether *this + rhs would overflow any intermediate or the result.
Definition rational.hpp:865
Wire getWire() const noexcept
Wire-codec exit (Glaze write side).
Definition rational.hpp:674
DecimalPlaces decimalPlaces
Decimal-precision tag. Invariant: 0 <= value <= kMaxDecimalPlaces.
Definition rational.hpp:383
static constexpr Rational zero(DecimalPlaces wantedPrecision) noexcept
Canonical zero (0/1) at the given precision.
Definition rational.hpp:449
constexpr DecimalPlaces getDecimalPlaces() const noexcept
The value's current decimal precision.
Definition rational.hpp:462
constexpr bool operator==(const Rational &other) const noexcept
Equality. Value-only: ignores decimalPlaces.
Definition rational.hpp:540
constexpr void addAssignChecked(const Rational &rhs) noexcept
Applies +='s arithmetic, having already proven it cannot overflow.
Definition rational.hpp:852
constexpr Rational(Numerator wantedNumerator, Denominator wantedDenominator, DecimalPlaces wantedPrecision) noexcept
Constructs from explicit numerator/denominator, then canonicalises.
Definition rational.hpp:403
constexpr Rational & operator*=(const Rational &rhs) noexcept
In-place multiplication. Result precision becomes max of the two. Cross-cancels common factors before...
Definition rational.hpp:585
static std::expected< Rational, RationalError > fromFloat(double value, DecimalPlaces wantedPrecision) noexcept
Converts a double to a Rational scaled to wantedPrecision.
constexpr Rational & operator-=(const Rational &rhs) noexcept
In-place subtraction. Result precision becomes max of the two.
Definition rational.hpp:569
constexpr bool subWouldOverflow(const Rational &rhs) const noexcept
Whether *this - rhs would overflow any intermediate or the result.
Definition rational.hpp:878
void setWire(Wire wire) noexcept
Wire-codec entry (Glaze read side): rebuilds through the canonicalising constructor,...
Definition rational.hpp:661
constexpr Rational operator-() const noexcept
Negates.
Definition rational.hpp:488
std::int64_t numerator
Signed numerator. Carries the sign of the rational value.
Definition rational.hpp:377
std::int64_t denominator
Strictly positive denominator. Never zero, never negative.
Definition rational.hpp:380
constexpr void mulAssignChecked(const Rational &rhs) noexcept
Applies *='s arithmetic, having already proven it cannot overflow.
Definition rational.hpp:860
static constexpr Rational one(DecimalPlaces wantedPrecision) noexcept
Canonical one (1/1) at the given precision.
Definition rational.hpp:456
constexpr Rational() noexcept=default
Default-constructs the canonical zero (0/1) at precision 1.
constexpr Rational & operator+=(const Rational &rhs) noexcept
In-place addition. Result precision becomes max of the two. Uses reduce-before-multiply to extend the...
Definition rational.hpp:554
static constexpr std::string_view name() noexcept
This type's stable shape name.
Declares a stable shape tag for a type T whose JSON shape payloadShape cannot decompose.