morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
quantity.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file util/quantity.hpp
6/// @brief Unit-tagged, optionally-empty exact values with a derivation trail.
7///
8/// `morph::units::Quantity<U>` wraps an *optional* `morph::math::Rational` and
9/// knows three things about itself: its **unit** (a compile-time template tag),
10/// its **exactness** (an exact `Rational`, with a declared and a runtime notion
11/// of precision, and `std::nullopt` as a real "not entered" state), and **how
12/// it was computed** — its derivation, exposed through `equation()`.
13///
14/// See `docs/spec/util/quantity_type.md` for the full design. The essentials:
15///
16/// - **Units are types.** `Quantity<Unit::kg>` and `Quantity<Unit::m3>` cannot
17/// be mixed; `operator*` / `operator/` deduce the result unit from the
18/// application-defined `consteval` algebra on the unit enum. Within-dimension
19/// scaling is declared as `UnitTraits<E>::relations` (a flat list of exact
20/// `UnitRelation` ratios), which drive `convert`, conversion *chaining*, and
21/// the schema's display-unit selector.
22/// - **Precision is declared in the type; actual precision is runtime data.**
23/// `Quantity<Unit::m3, 4>` overrides the unit default; the value's actual
24/// precision is the runtime `DecimalPlaces` tag inside the `Rational` and
25/// max-propagates through arithmetic. Magnitude is bounded by `int64`;
26/// overflow is a documented limitation, only division-by-zero yields empty.
27/// - **Empty propagates** (SQL-NULL semantics). `==` is total; relational
28/// ordering has a precondition that both operands are engaged.
29/// - **Provenance** is a build-wide toggle, `MORPH_QUANTITY_PROVENANCE`
30/// (default `1`). With it `0`, no derivation nodes are allocated and the
31/// provenance API returns empty results; `Quantity<U>` is the same type
32/// either way.
33/// - **Wire.** On the morph JSON wire a quantity is its nullable `Rational`
34/// payload; the unit lives in generated schemas (`ExtUnits`) and C++ types.
35
36/// @brief Build-wide provenance toggle. Default on; define to `0` to compile
37/// the derivation DAG out (no `ASTNode` allocations).
38///
39/// The default is `1` deliberately: the toggle changes observable behaviour,
40/// not just cost — with it `0`, `equation()` collapses to the bare value and
41/// `named()` discards the name — so a build that never set it must not have
42/// that output emptied underneath it. The cost is real and measured, though:
43/// a 200,000-iteration running total took 54,056 KB and 0.034 s with
44/// provenance against 12,236 KB and 0.006 s without (clang 22, `-O2`).
45/// **A bulk path that never calls `equation()` should set this to `0`.**
46/// See `docs/spec/util/quantity_type.md`, *Limitations*.
47#ifndef MORPH_QUANTITY_PROVENANCE
48#define MORPH_QUANTITY_PROVENANCE 1
49#endif
50
51#include <array>
52#include <compare>
53#include <cstdint>
54#include <format>
55#include <glaze/glaze.hpp>
56#include <limits>
58#include <optional>
59#include <span>
60#include <stdexcept>
61#include <string>
62#include <string_view>
63#include <type_traits>
64#include <utility>
65#include <vector>
66
67#if MORPH_QUANTITY_PROVENANCE
68#include <memory>
69#include <unordered_map>
70#endif
71
72#include "../attributes.hpp"
74#include "rational.hpp"
75
76namespace morph::units {
77
78namespace detail {
79
80/// @brief Renders a `Rational` as a shortest exact decimal at its own
81/// `DecimalPlaces` (trailing zeros and a bare point trimmed) — the form
82/// `Quantity` formatting and `equation()` use everywhere.
83///
84/// The decimal is produced by **exact integer long division** of the canonical
85/// `numerator/denominator` to `decimalPlaces` digits — the value never passes
86/// through `double`, so both large exact integers (beyond the 2^53 double
87/// mantissa) and non-terminating quotients render without floating-point drift.
88/// Non-terminating quotients are rounded **half away from zero** at
89/// `decimalPlaces` (matching `Rational::toDouble`'s `std::round`), and the
90/// rounding carry is allowed to propagate into the integer part. Trailing zeros
91/// and a bare decimal point are then trimmed to the shortest form.
92///
93/// The division is a **digit-at-a-time** long division: the integer part comes
94/// from `|numerator| / denominator` in 64-bit, and each fractional digit from
95/// `remainder * 10 / denominator`, carrying the exact integer remainder forward.
96/// `remainder * 10` can exceed 64 bits when the denominator is near `INT64_MAX`,
97/// so each fractional step is computed with `mulTenDivMod` — a portable helper
98/// that never forms `remainder * 10` at all: since the quotient digit is in
99/// 0..9, it accumulates `remainder` ten times, reducing modulo the denominator
100/// as it goes, so every step stays within 64 bits (no 128-bit type, no
101/// `__udivti3` runtime dependency) and behaves identically under MSVC and clang-cl.
102/// @param value The rational to render.
103/// @return The decimal string (`"2"`, `"0.3"`, `"1.36"`, `"-4.2"`).
104[[nodiscard]] inline std::string formatRationalDecimal(const morph::math::Rational& value) {
105 // Work on magnitudes; the sign is reattached at the end. The canonical
106 // invariant guarantees denominator > 0, so only the numerator carries sign.
107 bool const negative = value.numerator < 0;
108 // Negate in unsigned arithmetic: `-INT64_MIN` is undefined as a signed
109 // operation, and INT64_MIN reaches here through the whole-integer
110 // `Rational{value, DecimalPlaces{n}}` constructor, which does not
111 // canonicalise (and `numerator` is public). `absU64` is the shared helper
112 // that gets this right.
113 auto const num = ::morph::math::detail::absU64(value.numerator);
114 auto const den = static_cast<std::uint64_t>(value.denominator);
115 auto const places = static_cast<std::uint32_t>(value.decimalPlaces.value);
116
117 // Integer part and the exact running remainder (both < den, so 64-bit safe).
118 std::uint64_t const integerPart = num / den;
119 std::uint64_t remainder = num % den;
120
121 // floor(rem*10 / den) and (rem*10 % den) with `rem < den`. `rem*10` can need
122 // up to 67 bits, but we never form it: the digit q = floor(rem*10/den) is in
123 // 0..9 (because rem < den), so we compute it by adding `rem` ten times and
124 // reducing modulo `den` as we go. The running accumulator stays below `den`
125 // before each add and below `2*den <= 2^64` after, so every step is exact in
126 // 64-bit with no overflow and no 128-bit type — identical under MSVC and
127 // clang-cl (whose MSVC runtime lacks the `__udivti3` 128-bit divide helper).
128 auto const mulTenDivMod = [den](std::uint64_t rem, std::uint64_t& quotientDigit) -> std::uint64_t {
129 std::uint64_t acc = 0;
130 std::uint64_t q = 0;
131 for (int k = 0; k < 10; ++k) {
132 acc += rem; // acc < den before the add (rem < den), so acc < 2*den <= 2^64 after
133 while (acc >= den) {
134 acc -= den;
135 ++q;
136 }
137 }
138 quotientDigit = q;
139 return acc;
140 };
141
142 std::string frac;
143 frac.reserve(places);
144 for (std::uint32_t i = 0; i < places; ++i) {
145 std::uint64_t digit = 0;
146 remainder = mulTenDivMod(remainder, digit);
147 frac.push_back(static_cast<char>('0' + static_cast<int>(digit)));
148 }
149
150 // Round half away from zero at `places`: if twice the leftover remainder is
151 // >= den, the truncated result rounds up. Propagate the carry through the
152 // fractional digits and, if it reaches the top, into the integer part.
153 std::uint64_t roundedInteger = integerPart;
154 if (remainder * 2 >= den) {
155 bool carry = true;
156 for (std::size_t i = frac.size(); i-- > 0 && carry;) {
157 if (frac[i] == '9') {
158 frac[i] = '0';
159 } else {
160 ++frac[i];
161 carry = false;
162 }
163 }
164 if (carry) {
165 ++roundedInteger;
166 }
167 }
168
169 std::string text = std::to_string(roundedInteger);
170 if (places > 0) {
171 text.push_back('.');
172 text += frac;
173 }
174
175 // Trim trailing zeros and a now-bare decimal point (shortest form).
176 if (auto const dot = text.find('.'); dot != std::string::npos) {
177 std::size_t last = text.find_last_not_of('0');
178 if (last == dot) {
179 --last;
180 }
181 text.erase(last + 1);
182 }
183
184 // A rounded-to-zero magnitude must not print as "-0".
185 if (negative && text != "0") {
186 text.insert(text.begin(), '-');
187 }
188 return text;
189}
190
191/// @brief Formats an optional payload as `equation()` and the formatter print
192/// it. The shared implementation behind `units::toDecimalString` (and so
193/// behind `toString` and `std::formatter<Quantity>`), which is why the
194/// empty case is the `"N/A"` literal rather than an empty string.
195/// @param value The optional payload.
196/// @return The decimal string, or `"N/A"` when empty.
197[[nodiscard]] inline std::string formatOptionalDecimal(const std::optional<morph::math::Rational>& value) {
198 return value ? formatRationalDecimal(*value) : std::string{"N/A"};
199}
200
201} // namespace detail
202
203/// @brief Static description of one unit, supplied by `UnitTraits`.
204struct UnitMeta {
205 /// @brief Stable ascii identifier, e.g. `"kg_per_m3"`. Appears in JSON
206 /// Schemas (`ExtUnits.unitAscii`); part of the protocol vocabulary.
207 std::string_view id;
208
209 /// @brief Human display text, e.g. `"kg/m³"` (`ExtUnits.unitUnicode`).
210 std::string_view display;
211
212 /// @brief Default decimal places for parsing/formatting values of this
213 /// unit; surfaces in schemas as `x-decimalPlaces`.
214 std::uint32_t defaultDecimals{3};
215};
216
217/// @brief One exact within-dimension conversion ratio between two peer units.
218///
219/// `1` unit of `from` equals `fromTo` units of `to`. `fromTo` must be strictly
220/// positive; `from` and `to` must differ. Declared in `UnitTraits<E>::relations`
221/// and used in both directions (the reverse multiplies by the reciprocal).
222/// @tparam E The application's unit enum type.
223template <typename E>
225 /// @brief Source unit of the ratio.
226 E from{};
227
228 /// @brief Target unit of the ratio.
229 E to{};
230
231 /// @brief Exact ratio: `1 from == fromTo · to`. Strictly positive.
233};
234
235/// @brief A derived per-unit view of one convertible display/entry unit.
236///
237/// Not declared by the application — computed from `UnitTraits<E>::relations`
238/// by `Quantity::unitAlternatives()`. `value_in_canonical = value_in_unit *
239/// num/den`, where the canonical unit is the field's own unit.
240/// @tparam E The application's unit enum type.
241template <typename E>
243 /// @brief The alternative (convertible) unit.
244 E unit{};
245
246 /// @brief Numerator of the exact alternative-to-canonical ratio.
247 std::int64_t num{1};
248
249 /// @brief Denominator of the exact alternative-to-canonical ratio.
250 std::int64_t den{1};
251};
252
253/// @brief Optional physical/wire bounds for one unit: the inclusive range a
254/// decoded value must fall within to be accepted. Returned by the
255/// optional `UnitTraits<E>::bounds(E)` customisation point (detected
256/// by `HasUnitBounds`) — e.g. a percentage unit bounding itself to
257/// `[0, 100]`, or a mass unit rejecting a negative reading no scale
258/// can physically produce. Absent by default: a unit with no declared
259/// bounds accepts any value its precision allows, exactly as before
260/// this feature existed.
262 /// @brief Inclusive lower bound.
264 /// @brief Inclusive upper bound.
266};
267
268/// @brief Customisation point: the application specialises this for its unit
269/// enum, returning a `UnitMeta` per enumerator and (optionally) a
270/// `relations` array of `UnitRelation` entries and/or a `bounds(E)`
271/// static method.
272/// @tparam E The application's unit enum type.
273template <typename E>
275
276/// @brief Concept: an enum with a `UnitTraits` specialisation (template param `E`).
277template <typename E>
278concept UnitEnum = std::is_enum_v<E> && requires(E unit) {
279 { UnitTraits<E>::meta(unit) } -> std::convertible_to<UnitMeta>;
280};
281
282/// @brief Concept: `UnitTraits<E>` also declares within-dimension `relations` (template param `E`).
283template <typename E>
284concept HasUnitRelations = requires {
286};
287
288/// @brief Concept: `UnitTraits<E>` declares an optional `static constexpr
289/// QuantityBounds bounds(E)` — the pre-decode validation seam a field's
290/// unit opts into. A unit enum with no `bounds()` simply has none:
291/// every value its precision allows is accepted, unchanged from
292/// before this feature existed. `E` is the application's unit enum type.
293template <typename E>
294concept HasUnitBounds = requires(E unit) {
295 { UnitTraits<E>::bounds(unit) } -> std::convertible_to<QuantityBounds>;
296};
297
298namespace detail {
299
300/// @brief Result of a compile-time conversion-ratio search.
301/// @tparam E The unit enum type.
302template <typename E>
303struct RatioResult {
304 /// @brief Whether a connecting path exists in the relation graph.
305 bool found{false};
306
307 /// @brief Composed ratio `k` such that `value_to = value_from · k`.
308 morph::math::Rational ratio{};
309};
310
311/// @brief Compile-time guard that a `UnitRelation::fromTo` ratio is strictly
312/// positive (both numerator and denominator > 0).
313///
314/// A `UnitRelation` declares `1 from == fromTo · to`; a zero or negative ratio
315/// is physically meaningless and, worse, silently corrupts conversions: a zero
316/// numerator makes the reverse edge's denominator `0`, which `Rational` clamps
317/// to `1` (see rational.hpp), and a negative ratio inverts the direction of the
318/// conversion — either way `den:0`/garbage leaks into `x-unitAlternatives`.
319///
320/// This is invoked from the `consteval` `conversionRatio` search for every
321/// relation edge it touches, so a bad ratio is a **compile error** (the
322/// `throw` in a constant-evaluation context is ill-formed) rather than silent
323/// runtime corruption. `fromTo` is a canonicalised `Rational` whose denominator
324/// is already `> 0` by the type invariant, so the failing case in practice is a
325/// numerator `<= 0` (a `0`/negative ratio the application declared).
326/// @param ratio The declared `fromTo` ratio to validate.
327/// @return `true` when @p ratio is strictly positive; otherwise the call is
328/// ill-formed in a constant-evaluation context (a compile error).
329[[nodiscard]] consteval bool requirePositiveRatio(const morph::math::Rational& ratio) {
330 // Rational canonicalises so denominator > 0 always; the numerator carries
331 // the sign and can be zero/negative if the application declared a bad ratio.
332 if (ratio.numerator <= 0 || ratio.denominator <= 0) {
333 throw "UnitRelation::fromTo must be strictly positive (numerator > 0 and denominator > 0)";
334 }
335 return true;
336}
337
338/// @brief Reciprocal of a strictly-positive rational (swaps numerator and
339/// denominator), keeping its precision tag.
340///
341/// Guards, at compile time, that @p value is strictly positive: a zero or
342/// negative input would otherwise produce a degenerate reciprocal (a `0`
343/// denominator that `Rational` clamps to `1`, or a sign-flipped ratio). Because
344/// this is `consteval`, a non-positive @p value is a **compile error** at the
345/// relation-consuming call site rather than silent corruption.
346/// @param value A strictly-positive rational.
347/// @return `1 / value`.
348[[nodiscard]] consteval morph::math::Rational reciprocal(const morph::math::Rational& value) {
349 static_cast<void>(requirePositiveRatio(value)); // compile error on a bad ratio
350 return morph::math::Rational{morph::math::Numerator{value.denominator}, morph::math::Denominator{value.numerator},
351 value.decimalPlaces};
352}
353
354/// @brief Breadth-first search of the relation graph for a `from → to` ratio.
355///
356/// Treats `UnitTraits<E>::relations` as an undirected graph (each entry usable
357/// forward with `fromTo` and backward with its reciprocal), returning the
358/// fewest-hops composed ratio. Ties break by declaration order.
359/// @tparam E The unit enum type (must satisfy `HasUnitRelations`).
360/// @param from Source unit.
361/// @param to Target unit.
362/// @return The composed ratio and whether a path was found.
363template <typename E>
364[[nodiscard]] consteval RatioResult<E> conversionRatio(E from, E to) {
365 using morph::math::Rational;
366 if (from == to) {
367 return RatioResult<E>{true, Rational::one(morph::math::DecimalPlaces{1})};
368 }
369 constexpr std::size_t relationCount = UnitTraits<E>::relations.size();
370 constexpr std::size_t capacity = 2 * relationCount + 1;
371 std::array<E, capacity> nodes{};
372 std::array<Rational, capacity> ratios{};
373 std::size_t count = 0;
374 std::size_t head = 0;
375 nodes[count] = from;
376 ratios[count] = Rational::one(morph::math::DecimalPlaces{1});
377 ++count;
378 while (head < count) {
379 E const current = nodes[head];
380 Rational const currentRatio = ratios[head];
381 ++head;
382 for (auto const& relation : UnitTraits<E>::relations) {
383 E neighbour{};
384 Rational edge{};
385 bool touches = false;
386 if (relation.from == current) {
387 neighbour = relation.to;
388 static_cast<void>(requirePositiveRatio(relation.fromTo)); // compile error on a bad ratio
389 edge = relation.fromTo;
390 touches = true;
391 } else if (relation.to == current) {
392 neighbour = relation.from;
393 edge = reciprocal(relation.fromTo); // reciprocal also guards positivity
394 touches = true;
395 }
396 if (!touches) {
397 continue;
398 }
399 bool seen = false;
400 for (std::size_t i = 0; i < count; ++i) {
401 if (nodes[i] == neighbour) {
402 seen = true;
403 break;
404 }
405 }
406 if (seen) {
407 continue;
408 }
409 Rational const neighbourRatio = currentRatio * edge;
410 if (neighbour == to) {
411 return RatioResult<E>{true, neighbourRatio};
412 }
413 nodes[count] = neighbour;
414 ratios[count] = neighbourRatio;
415 ++count;
416 }
417 }
418 return RatioResult<E>{false, Rational{}};
419}
420
421/// @brief Number of `UnitAlternative`s directly convertible to unit `U`.
422/// @tparam U The canonical unit enumerator.
423/// @return Count of relation entries touching `U`.
424template <auto U>
425[[nodiscard]] consteval std::size_t alternativeCount() {
426 using E = decltype(U);
427 std::size_t total = 0;
428 if constexpr (HasUnitRelations<E>) {
429 for (auto const& relation : UnitTraits<E>::relations) {
430 if (relation.from == U || relation.to == U) {
431 ++total;
432 }
433 }
434 }
435 return total;
436}
437
438/// @brief Builds the derived `UnitAlternative` view for unit `U` from
439/// `relations` (direct neighbours, both directions).
440/// @tparam U The canonical unit enumerator.
441/// @return The alternatives array (empty when the unit system declares none).
442template <auto U>
443[[nodiscard]] consteval std::array<UnitAlternative<decltype(U)>, alternativeCount<U>()> makeAlternatives() {
444 using E = decltype(U);
445 std::array<UnitAlternative<E>, alternativeCount<U>()> result{};
446 if constexpr (HasUnitRelations<E>) {
447 std::size_t index = 0;
448 for (auto const& relation : UnitTraits<E>::relations) {
449 if (relation.to == U) {
450 result[index] =
451 UnitAlternative<E>{relation.from, relation.fromTo.numerator, relation.fromTo.denominator};
452 ++index;
453 } else if (relation.from == U) {
454 result[index] =
455 UnitAlternative<E>{relation.to, relation.fromTo.denominator, relation.fromTo.numerator};
456 ++index;
457 }
458 }
459 }
460 return result;
461}
462
463/// @brief Stable storage for unit `U`'s derived alternatives.
464/// @tparam U The canonical unit enumerator.
465template <auto U>
466inline constexpr auto kUnitAlternatives = makeAlternatives<U>();
467
468/// @brief Concept: two enumerators of the same enum with distinct values (template params `A`, `B`).
469template <auto A, auto B>
470concept SameEnumDistinct = std::same_as<decltype(A), decltype(B)> && (A != B);
471
472} // namespace detail
473
474/// @brief How many derivation steps `equation()` writes out by default.
475///
476/// A derivation has no bound — `total = total + row` over a data-driven loop
477/// records one step per iteration — so without a limit `equation()` renders
478/// every one of them into a single line — measured at 100,000 steps: a
479/// 500,001-character line in 58.8 s. This is where a *rendered* formula stops
480/// being something a person reads, not where the cost stops being tolerable:
481/// a caller that wants more passes its own limit, so erring low costs one
482/// argument while erring high costs an unreadable line nobody asked for.
483inline constexpr std::size_t kDefaultEquationSteps = 100;
484
485/// @brief Pass as `equation()`'s limit to write the derivation out in full.
486///
487/// No step is elided, and the cost is the caller's, taken deliberately. Used
488/// by the depth regression tests, which
489/// exist precisely to walk a derivation deeper than any limit would render.
490inline constexpr std::size_t kEquationStepsUnlimited = std::numeric_limits<std::size_t>::max();
491
492// Forward declaration (carrying the default arg) so `convert` and the
493// conversion operator can name `Quantity<From>` before the full definition.
494template <auto U, std::uint32_t DeclaredDecimals = UnitTraits<decltype(U)>::meta(U).defaultDecimals>
495 requires UnitEnum<decltype(U)>
496struct Quantity;
497
498/// @brief Concept: the auto-generated ratio `convert` applies to `From → To` (template params `From`, `To`).
499template <auto From, auto To>
500concept RatioConvertible = detail::SameEnumDistinct<From, To> && UnitEnum<decltype(From)> &&
501 HasUnitRelations<decltype(From)> && detail::conversionRatio(From, To).found;
502
503/// @brief Auto-generated within-dimension conversion (ratio + chaining).
504///
505/// A **constrained template**; a `UnitTraits<E>::convert` static takes
506/// precedence for the pairs it names. Performs the value math only, on an
507/// **engaged** operand — empty-propagation and provenance are added by the
508/// caller (the conversion operator).
509/// @tparam From Source unit enumerator.
510/// @tparam To Target unit enumerator.
511/// @param in Engaged source quantity.
512/// @param out Receives the converted value.
513template <auto From, auto To>
515void convert(const Quantity<From>& in, Quantity<To>& out);
516
517/// @brief Concept: the application supplied a `UnitTraits<E>::convert` static
518/// for this pair (a non-ratio override, e.g. °C → °F) (template params `From`, `To`).
519template <auto From, auto To>
521 requires(Quantity<From> from, Quantity<To>& to) { UnitTraits<decltype(From)>::convert(from, to); };
522
523/// @brief Concept: `From → To` is convertible — a user override takes
524/// precedence over an auto-generated ratio path (template params `From`, `To`).
525template <auto From, auto To>
526concept Convertible = detail::SameEnumDistinct<From, To> && (HasUserConvert<From, To> || RatioConvertible<From, To>);
527
528#if MORPH_QUANTITY_PROVENANCE
529
530namespace detail {
531
532/// @brief One recorded step of a derivation.
533struct ASTUnit {
534 /// @brief Operator token (`"+"`, `"-"`, `"*"`, `"/"`, `"convert a -> b"`);
535 /// empty for a plain leaf.
536 std::string operation;
537
538 /// @brief First operand value (the leaf value for a leaf node).
539 std::optional<morph::math::Rational> lhs;
540
541 /// @brief Second operand value; empty for single-operand steps.
542 std::optional<morph::math::Rational> rhs;
543
544 /// @brief The step's result value (stored, not recomputed).
545 std::optional<morph::math::Rational> result;
546};
547
548/// @brief A node in the (immutable, shared) derivation DAG.
549struct ASTNode {
550 /// @brief This node's own step.
551 ASTUnit current;
552
553 /// @brief When set, an opaque named symbol: `equation()` shows the name
554 /// instead of expanding the derivation below it.
555 std::optional<std::string> name;
556
557 /// @brief Left operand's derivation (shared).
558 std::shared_ptr<ASTNode> left;
559
560 /// @brief Right operand's derivation (shared); null for unary/scalar steps.
561 std::shared_ptr<ASTNode> right;
562
563 /// @brief Default-constructs an empty node.
564 ASTNode() = default;
565
566 /// @brief Copies a node (the children stay shared).
567 ASTNode(const ASTNode&) = default;
568
569 /// @brief Copy-assigns a node (the children stay shared).
570 /// @return `*this`.
571 ASTNode& operator=(const ASTNode&) = default;
572
573 /// @brief Moves a node, leaving the source's children null.
574 ASTNode(ASTNode&&) = default;
575
576 /// @brief Move-assigns a node, leaving the source's children null.
577 /// @return `*this`.
578 ASTNode& operator=(ASTNode&&) = default;
579
580 /// @brief Releases the sub-derivation **iteratively**, so chain depth
581 /// cannot overflow the stack.
582 ///
583 /// The running-total pattern (`total = total + x` in a loop) builds a
584 /// derivation that is a linear chain down `left`, one node per iteration.
585 /// The compiler-generated destructor releases that chain recursively —
586 /// `~shared_ptr` -> `~ASTNode` -> `~shared_ptr` -> ... — one stack frame
587 /// per node, and a long enough chain runs the stack out. Measured with
588 /// clang 22, `-O0`, an 8 MiB stack: a 21,000-node chain
589 /// segfaults on destruction, while an optimised build survives 200,000
590 /// because clang turns the same chain into a loop. "Crashes in Debug,
591 /// survives in Release" is the worst signature a defect can have, so the
592 /// flattening is written down here rather than left to the optimiser.
593 ///
594 /// Children are detached into a local worklist and released one at a time.
595 /// A node is only unlinked when this pop holds its last reference; when it
596 /// does not, dropping the handle cannot destroy it and its children stay
597 /// where they are. Every `~ASTNode` reached from the loop therefore runs
598 /// with both children already null, so it cannot recurse.
599 ~ASTNode() {
600 std::vector<std::shared_ptr<ASTNode>> pending;
601 if (left) {
602 pending.push_back(std::move(left));
603 }
604 if (right) {
605 pending.push_back(std::move(right));
606 }
607 while (!pending.empty()) {
608 std::shared_ptr<ASTNode> const node = std::move(pending.back());
609 pending.pop_back();
610 if (node.use_count() == 1) {
611 if (node->left) {
612 pending.push_back(std::move(node->left));
613 }
614 if (node->right) {
615 pending.push_back(std::move(node->right));
616 }
617 }
618 // `node` goes out of scope here: either it was not the last owner
619 // (nothing happens) or it was, and its children are already null.
620 }
621 }
622};
623
624/// @brief The per-`Quantity` handle onto the root of its derivation.
625struct Context {
626 /// @brief Root node; null means "no recorded derivation".
627 std::shared_ptr<ASTNode> node;
628};
629
630} // namespace detail
631
632#endif
633
634/// @cond INTERNAL
635#if MORPH_QUANTITY_PROVENANCE
636#define MORPH_Q_NODE(quantity) (quantity)._ctx.node
637// clang-format off -- public macro surface; see CONTRIBUTING.md, "Formatting/linting".
638#define MORPH_Q_BUILD(out, op, lhsValue, rhsValue, resultValue, leftNode, rightNode) \
639 do { \
640 auto morphProvNode = std::make_shared<::morph::units::detail::ASTNode>(); \
641 morphProvNode->current.operation = (op); \
642 morphProvNode->current.lhs = (lhsValue); \
643 morphProvNode->current.rhs = (rhsValue); \
644 morphProvNode->current.result = (resultValue); \
645 morphProvNode->left = (leftNode); \
646 morphProvNode->right = (rightNode); \
647 (out)._ctx.node = std::move(morphProvNode); \
648 } while (0)
649// clang-format on
650
651#else
652
653#define MORPH_Q_NODE(quantity) nullptr
654// clang-format off -- public macro surface; see CONTRIBUTING.md, "Formatting/linting".
655#define MORPH_Q_BUILD(out, op, lhsValue, rhsValue, resultValue, leftNode, rightNode) \
656 do { \
657 } while (0)
658// clang-format on
659
660#endif
661/// @endcond
662
663/// @brief A unit-tagged, optionally-empty exact value with a declared precision
664/// in its type and (optionally) a recorded derivation.
665/// @tparam U An enumerator of an application unit enum
666/// satisfying `UnitEnum`.
667/// @tparam DeclaredDecimals Declared decimal places of the field; defaults to
668/// the unit's `UnitMeta::defaultDecimals`.
669template <auto U, std::uint32_t DeclaredDecimals>
670 requires UnitEnum<decltype(U)>
671struct Quantity {
672 static_assert(DeclaredDecimals <= math::kMaxDecimalPlaces,
673 "declared decimals must be within [0, kMaxDecimalPlaces]");
674
675 /// @brief The payload; `std::nullopt` means "not entered / not measured".
676 std::optional<math::Rational> payload;
677
678#if MORPH_QUANTITY_PROVENANCE
679 /// @brief Handle onto this value's derivation (internal).
680 detail::Context _ctx;
681#endif
682
683 /// @brief Constructs the empty state (no recorded derivation).
684 constexpr Quantity() noexcept = default;
685
686 /// @brief Engages with @p engaged (which keeps its own runtime precision).
687 /// @param engaged The exact value to hold.
688 Quantity(math::Rational engaged) : payload{engaged} { recordLeaf(); }
689
690 /// @brief Adopts an optional payload as-is.
691 /// @param adopted Engaged or empty payload.
692 Quantity(std::optional<math::Rational> adopted) : payload{std::move(adopted)} { recordLeaf(); }
693
694 /// @brief Converts from the same unit at another declared precision; the
695 /// value, its runtime tag, and its derivation carry over unchanged.
696 /// @param other Same-unit quantity with a different declared precision.
697 template <std::uint32_t OtherDecimals>
698 requires(OtherDecimals != DeclaredDecimals)
700#if MORPH_QUANTITY_PROVENANCE
701 _ctx = other._ctx;
702#endif
703 }
704
705 /// @brief The unit this quantity is denominated in (compile-time).
706 static constexpr auto unit = U;
707
708 /// @brief Declared decimal places of this field (compile-time).
709 static constexpr std::uint32_t declaredDecimals = DeclaredDecimals;
710
711 /// @brief The unit's static metadata (id, display text, default decimals).
712 /// @return The `UnitMeta` from the application's `UnitTraits` specialisation.
713 [[nodiscard]] static constexpr UnitMeta unitMeta() noexcept { return UnitTraits<decltype(U)>::meta(U); }
714
715 /// @brief The declared precision as a `DecimalPlaces` strong type.
716 /// @return `DecimalPlaces{declaredDecimals}`.
717 [[nodiscard]] static constexpr math::DecimalPlaces declaredPrecision() noexcept {
718 return math::DecimalPlaces{DeclaredDecimals};
719 }
720
721 /// @brief The convertible display/entry units for this field's unit,
722 /// derived from `UnitTraits::relations`.
723 /// @return Exact-ratio alternatives (empty when the unit system declares none).
724 [[nodiscard]] static constexpr std::span<const UnitAlternative<decltype(U)>> unitAlternatives() noexcept {
725 return std::span<const UnitAlternative<decltype(U)>>{detail::kUnitAlternatives<U>};
726 }
727
728 /// @brief Converts a floating-point reading at the declared precision.
729 /// @param raw The value to convert.
730 /// @return The engaged quantity, or empty when @p raw is not finite or does
731 /// not fit `int64` (empty-propagation philosophy: no error channel).
732 [[nodiscard]] static Quantity fromDouble(double raw) noexcept {
733 if (auto converted = math::Rational::fromFloat(raw, declaredPrecision()); converted.has_value()) {
734 return Quantity{*converted};
735 }
736 return {};
737 }
738
739 /// @brief Wraps an optional payload, preserving the declared precision.
740 /// @param adopted Engaged or empty payload.
741 /// @return The quantity (empty in → empty out).
742 [[nodiscard]] static Quantity fromOptional(std::optional<math::Rational> adopted) {
743 return Quantity{std::move(adopted)};
744 }
745
746 /// @brief Whether a value has been entered/measured.
747 /// @return `true` if the payload is engaged.
748 [[nodiscard]] constexpr bool hasValue() const noexcept { return payload.has_value(); }
749
750 /// @brief Pre-decode validation seam: whether the current payload falls
751 /// within this field's unit-declared bounds (`UnitTraits<E>::bounds`),
752 /// when the unit declares any. An empty payload, or a unit with no
753 /// declared `bounds()` (`HasUnitBounds` not satisfied), is always
754 /// within bounds — this is an *opt-in* check, not a new default
755 /// restriction, so a unit system that declares no bounds behaves
756 /// exactly as it did before this feature existed.
757 ///
758 /// Comparison is on the exact `Rational` value only (never a lossy
759 /// `double`), consistent with every other exact comparison in this
760 /// header.
761 /// @return `true` when empty, when the unit declares no bounds, or when
762 /// the engaged value satisfies `min <= value <= max`.
763 [[nodiscard]] constexpr bool withinDeclaredBounds() const noexcept {
764 if constexpr (HasUnitBounds<decltype(U)>) {
765 if (!payload.has_value()) {
766 return true;
767 }
768 auto const bounds = UnitTraits<decltype(U)>::bounds(U);
769 return (*payload <=> bounds.min) != std::strong_ordering::less &&
770 (*payload <=> bounds.max) != std::strong_ordering::greater;
771 } else {
772 return true;
773 }
774 }
775
776 /// @brief The payload, for pattern-matching / `->` access.
777 /// @return Const reference to the optional payload — into this `Quantity`,
778 /// so valid only for as long as it is.
779 [[nodiscard]] constexpr const std::optional<math::Rational>& value() const noexcept MORPH_LIFETIMEBOUND {
780 return payload;
781 }
782
783 /// @brief The payload if engaged, otherwise a caller-supplied fallback.
784 /// @param fallback Value to return when empty.
785 /// @return The engaged value or @p fallback.
786 [[nodiscard]] constexpr math::Rational value_or(const math::Rational& fallback) const noexcept {
787 return payload.value_or(fallback);
788 }
789
790 /// @brief Unchecked access to the engaged value (UB when empty, exactly like
791 /// `std::optional::operator*`).
792 /// @return The engaged exact value — a reference into this `Quantity`,
793 /// valid only for as long as it is.
794 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
795 [[nodiscard]] constexpr const math::Rational& operator*() const noexcept MORPH_LIFETIMEBOUND { return *payload; }
796
797 /// @brief Retags the value's *runtime* precision (the exact value itself is
798 /// unchanged — precision only affects rounding and formatting).
799 ///
800 /// This is the **display-hint** half of the pair: after it, the value shown
801 /// at @p newPrecision and the value stored may differ. Use
802 /// `roundedToDecimalPlaces` when the stored value must be the one displayed.
803 /// @param newPrecision Runtime precision; silently clamped to the valid range.
804 /// @return The retagged quantity, or empty if this is empty.
805 [[nodiscard]] Quantity withDecimalPlaces(math::DecimalPlaces newPrecision) const {
806 if (!payload) {
807 return *this;
808 }
809 auto adjusted = *payload;
810 adjusted.decimalPlaces = math::DecimalPlaces{math::detail::clampWireDecimalPlaces(newPrecision.value)};
811 Quantity out;
812 out.payload = adjusted;
813 // Not `out._ctx = _ctx`: that would copy this quantity's derivation
814 // node verbatim, so equation() and std::formatter<Quantity> would
815 // disagree -- the node's own recorded `result` is still *this*
816 // quantity's old payload/precision, but `out.payload` is the
817 // retagged one. A fresh node (same convention as operator
818 // Quantity<To>()'s unit conversion, below) keeps the two consistent.
819 MORPH_Q_BUILD(out, "retag decimal places", payload, std::nullopt, out.payload, MORPH_Q_NODE(*this), nullptr);
820 return out;
821 }
822
823 /// @brief Rounds the value to a runtime precision, **changing the exact
824 /// value** as well as the tag — unlike `withDecimalPlaces`, which
825 /// retags only and leaves display and storage free to disagree.
826 ///
827 /// Delegates to `math::roundToDecimalPlaces`, and so shares its saturation
828 /// behaviour: a value whose scaled form leaves `int64` range clamps and logs
829 /// instead of overflowing. A value already representable at @p newPrecision
830 /// is unchanged apart from its tag.
831 /// @param newPrecision Runtime precision; silently clamped to the valid range.
832 /// @param mode Tie-breaking rule; defaults to half away from zero, matching
833 /// the decimal display path.
834 /// @return The rounded quantity, or empty if this is empty.
837 if (!payload) {
838 return *this;
839 }
840 Quantity out;
841 out.payload = math::roundToDecimalPlaces(*payload, newPrecision, mode);
842 // Same reasoning as withDecimalPlaces() above: this call changes the
843 // exact value, not just its tag, so copying _ctx verbatim would leave
844 // the derivation node's recorded result pointing at the pre-rounding
845 // payload while out.payload holds the rounded one. Stamp a fresh node
846 // instead.
847 MORPH_Q_BUILD(out, "round to decimal places", payload, std::nullopt, out.payload, MORPH_Q_NODE(*this),
848 nullptr);
849 return out;
850 }
851
852 /// @brief Rounds the value to this field's **declared** precision — the one
853 /// the generated schema advertises as `x-decimalPlaces`.
854 ///
855 /// The enforcement half of the `x-decimalPlaces` contract: the stored value
856 /// becomes the value that renders at the declared precision, so a handler
857 /// never persists digits the form does not show. Rounds half away from zero,
858 /// the rule the decimal formatter uses.
859 /// @return The rounded quantity, or empty if this is empty.
861
862 /// @brief Seals this value with a name: `equation()` shows the name instead
863 /// of expanding its derivation. Builds a fresh node (never mutates a
864 /// shared one); a no-op returning empty on empty or with tracing off.
865 /// @param label The symbol name.
866 /// @return A same-unit quantity carrying the name.
867 [[nodiscard]] Quantity named([[maybe_unused]] std::string label) const {
868 Quantity out;
869 out.payload = payload;
870#if MORPH_QUANTITY_PROVENANCE
871 if (payload) {
872 auto node = std::make_shared<detail::ASTNode>();
873 node->name = std::move(label);
874 node->current.lhs = payload;
875 node->current.result = payload;
876 node->left = _ctx.node;
877 out._ctx.node = std::move(node);
878 }
879#endif
880 return out;
881 }
882
883 /// @brief The worked derivation as print-ready lines (see `docs/spec/util/quantity_type.md`).
884 /// @param maxSteps How many derivation steps to write out, across the
885 /// formula and the legend together. Past it a sub-derivation is
886 /// elided: it renders as `eK` and its value appears in the `where`
887 /// legend. `kEquationStepsUnlimited` writes the whole derivation
888 /// out (unbounded, quadratic on a right-leaning chain); `0` returns
889 /// the formatted value alone. Ignored with tracing off, which has
890 /// no derivation to render either way.
891 /// @return `[0]` formula, `[1]` substitution, `[2]` result, `[3..]` `where`
892 /// legend; a single formatted-value element for a degenerate root
893 /// (empty, named root, bare leaf, tracing off, or `maxSteps == 0`).
894 [[nodiscard]] std::vector<std::string> equation(
895 [[maybe_unused]] std::size_t maxSteps = kDefaultEquationSteps) const
896#if MORPH_QUANTITY_PROVENANCE
897 ;
898#else
899 {
900 return {detail::formatOptionalDecimal(payload)};
901 }
902#endif
903
904 /// @brief Implicit within-dimension conversion to another unit; delegates to
905 /// `convert`, propagates empty, and records a provenance step.
906 /// @tparam To Target unit enumerator (same enum, different value, convertible).
907 /// @return The converted quantity in unit `To`.
908 template <auto To>
909 requires Convertible<U, To>
910 [[nodiscard]] operator Quantity<To>() const {
911 Quantity<To> out;
912 if (payload) {
913 Quantity<U> self{payload};
914 if constexpr (HasUserConvert<U, To>) {
915 UnitTraits<decltype(U)>::convert(self, out);
916 } else {
917 convert(self, out);
918 }
919 MORPH_Q_BUILD(out,
920 std::string{"convert "} + std::string{unitMeta().id} + " -> " +
921 std::string{Quantity<To>::unitMeta().id},
922 payload, std::nullopt, out.payload, MORPH_Q_NODE(*this), nullptr);
923 }
924 return out;
925 }
926
927private:
928 /// @brief Records a leaf node for this value (no-op with tracing off).
929 void recordLeaf() {
930#if MORPH_QUANTITY_PROVENANCE
931 auto node = std::make_shared<detail::ASTNode>();
932 node->current.lhs = payload;
933 _ctx.node = std::move(node);
934#endif
935 }
936};
937
938/// @brief Trait: is `T` some `Quantity<U, Decimals>`?
939namespace detail {
940template <typename T>
941struct IsQuantity : std::false_type {};
942
943template <auto U, std::uint32_t Decimals>
944struct IsQuantity<Quantity<U, Decimals>> : std::true_type {};
945} // namespace detail
946
947/// @brief `true` when `T` (cvref-stripped) is a `morph::units::Quantity`.
948/// @tparam T Candidate type.
949template <typename T>
950inline constexpr bool isQuantity = detail::IsQuantity<std::remove_cvref_t<T>>::value;
951
952// Definition of the auto-generated ratio convert (declared above; defined here
953// where `Quantity` is complete).
954template <auto From, auto To>
956void convert(const Quantity<From>& in, Quantity<To>& out) {
957 constexpr auto ratioResult = detail::conversionRatio(From, To);
958 auto scaled = *in.value() * ratioResult.ratio;
959 scaled.decimalPlaces = in.value()->decimalPlaces;
960 out = Quantity<To>{scaled};
961}
962
963// ---------------------------------------------------------------------------
964// Comparison. `==` is total; ordering requires both operands engaged.
965// ---------------------------------------------------------------------------
966
967/// @brief Equality across same-unit quantities of any declared precision.
968/// Ignores the runtime precision tag; empty==empty is `true`.
969/// @param lhs Left operand.
970/// @param rhs Right operand.
971/// @return `true` when both empty or both engaged with equal value.
972template <auto U, std::uint32_t DecA, std::uint32_t DecB>
973[[nodiscard]] constexpr bool operator==(const Quantity<U, DecA>& lhs, const Quantity<U, DecB>& rhs) noexcept {
974 return lhs.payload == rhs.payload;
975}
976
977/// @brief Ordering across same-unit quantities. Precondition: both operands are
978/// engaged; comparing an empty operand throws `std::logic_error` (a
979/// defined diagnostic — ordering an absent value is a programming error).
980/// @param lhs Left operand.
981/// @param rhs Right operand.
982/// @return The ordering of the two exact values.
983/// @throws std::logic_error when either operand is empty.
984template <auto U, std::uint32_t DecA, std::uint32_t DecB>
985[[nodiscard]] constexpr std::strong_ordering operator<=>(const Quantity<U, DecA>& lhs, const Quantity<U, DecB>& rhs) {
986 if (!lhs.payload || !rhs.payload) {
987 throw std::logic_error{"morph::units::Quantity: relational comparison requires engaged operands"};
988 }
989 return *lhs.payload <=> *rhs.payload;
990}
991
992// ---------------------------------------------------------------------------
993// Arithmetic. Empty propagates; division by zero yields empty. Binary results
994// carry the unit's default declared precision (the value's runtime precision
995// still max-propagates inside Rational).
996// ---------------------------------------------------------------------------
997
998/// @brief Same-unit sum across any declared precisions.
999/// @param lhs Left operand.
1000/// @param rhs Right operand.
1001/// @return The exact sum, or empty when either operand is empty.
1002template <auto U, std::uint32_t DecA, std::uint32_t DecB>
1003[[nodiscard]] Quantity<U> operator+(const Quantity<U, DecA>& lhs, const Quantity<U, DecB>& rhs) {
1004 std::optional<math::Rational> result =
1005 (lhs.payload && rhs.payload) ? std::optional<math::Rational>{*lhs.payload + *rhs.payload} : std::nullopt;
1006 Quantity<U> out;
1007 out.payload = result;
1008 MORPH_Q_BUILD(out, "+", lhs.payload, rhs.payload, result, MORPH_Q_NODE(lhs), MORPH_Q_NODE(rhs));
1009 return out;
1010}
1011
1012/// @brief Same-unit difference across any declared precisions.
1013/// @param lhs Left operand.
1014/// @param rhs Right operand.
1015/// @return The exact difference, or empty when either operand is empty.
1016template <auto U, std::uint32_t DecA, std::uint32_t DecB>
1017[[nodiscard]] Quantity<U> operator-(const Quantity<U, DecA>& lhs, const Quantity<U, DecB>& rhs) {
1018 std::optional<math::Rational> result =
1019 (lhs.payload && rhs.payload) ? std::optional<math::Rational>{*lhs.payload - *rhs.payload} : std::nullopt;
1020 Quantity<U> out;
1021 out.payload = result;
1022 MORPH_Q_BUILD(out, "-", lhs.payload, rhs.payload, result, MORPH_Q_NODE(lhs), MORPH_Q_NODE(rhs));
1023 return out;
1024}
1025
1026/// @brief Mixed-unit sum: converts @p rhs to @p lhs's unit first.
1027/// @param lhs Left operand (its unit is the result unit).
1028/// @param rhs Right operand (a convertible unit).
1029/// @return The exact sum in `A`'s unit, or empty.
1030template <auto A, std::uint32_t DecA, auto B, std::uint32_t DecB>
1031 requires(detail::SameEnumDistinct<A, B> && requires(Quantity<B, DecB> value) { static_cast<Quantity<A>>(value); })
1032[[nodiscard]] Quantity<A> operator+(const Quantity<A, DecA>& lhs, const Quantity<B, DecB>& rhs) {
1033 return lhs + static_cast<Quantity<A>>(rhs);
1034}
1035
1036/// @brief Mixed-unit difference: converts @p rhs to @p lhs's unit first.
1037/// @param lhs Left operand (its unit is the result unit).
1038/// @param rhs Right operand (a convertible unit).
1039/// @return The exact difference in `A`'s unit, or empty.
1040template <auto A, std::uint32_t DecA, auto B, std::uint32_t DecB>
1041 requires(detail::SameEnumDistinct<A, B> && requires(Quantity<B, DecB> value) { static_cast<Quantity<A>>(value); })
1042[[nodiscard]] Quantity<A> operator-(const Quantity<A, DecA>& lhs, const Quantity<B, DecB>& rhs) {
1043 return lhs - static_cast<Quantity<A>>(rhs);
1044}
1045
1046/// @brief Negation. Empty stays empty; the declared precision is kept.
1047/// @param operand Value to negate.
1048/// @return The negated value, or empty.
1049template <auto U, std::uint32_t Dec>
1050[[nodiscard]] Quantity<U, Dec> operator-(const Quantity<U, Dec>& operand) {
1051 std::optional<math::Rational> result =
1052 operand.payload ? std::optional<math::Rational>{-*operand.payload} : std::nullopt;
1053 Quantity<U, Dec> out;
1054 out.payload = result;
1055 MORPH_Q_BUILD(out, "-", operand.payload, std::nullopt, result, MORPH_Q_NODE(operand), nullptr);
1056 return out;
1057}
1058
1059/// @brief Cross-unit product; the result unit is `A * B` per the application's
1060/// consteval unit algebra. Removed from overload resolution when the
1061/// algebra rejects the combination.
1062/// @param lhs Left operand.
1063/// @param rhs Right operand.
1064/// @return The exact product in the deduced unit, or empty.
1065template <auto A, std::uint32_t DecA, auto B, std::uint32_t DecB>
1066 requires requires { typename std::type_identity_t<Quantity<A * B>>; }
1067[[nodiscard]] Quantity<A * B> operator*(const Quantity<A, DecA>& lhs, const Quantity<B, DecB>& rhs) {
1068 std::optional<math::Rational> result =
1069 (lhs.payload && rhs.payload) ? std::optional<math::Rational>{*lhs.payload * *rhs.payload} : std::nullopt;
1070 Quantity<A * B> out;
1071 out.payload = result;
1072 MORPH_Q_BUILD(out, "*", lhs.payload, rhs.payload, result, MORPH_Q_NODE(lhs), MORPH_Q_NODE(rhs));
1073 return out;
1074}
1075
1076/// @brief Cross-unit quotient; the result unit is `A / B` per the application's
1077/// consteval unit algebra. Empty when either side is empty or the
1078/// divisor is zero.
1079/// @param lhs Dividend.
1080/// @param rhs Divisor.
1081/// @return The exact quotient in the deduced unit, or empty.
1082template <auto A, std::uint32_t DecA, auto B, std::uint32_t DecB>
1083 requires requires { typename std::type_identity_t<Quantity<A / B>>; }
1084[[nodiscard]] Quantity<A / B> operator/(const Quantity<A, DecA>& lhs, const Quantity<B, DecB>& rhs) {
1085 std::optional<math::Rational> result;
1086 if (lhs.payload && rhs.payload) {
1087 if (auto quotient = lhs.payload->dividedBy(*rhs.payload); quotient.has_value()) {
1088 result = *quotient;
1089 }
1090 }
1091 Quantity<A / B> out;
1092 out.payload = result;
1093 MORPH_Q_BUILD(out, "/", lhs.payload, rhs.payload, result, MORPH_Q_NODE(lhs), MORPH_Q_NODE(rhs));
1094 return out;
1095}
1096
1097/// @brief Scales by a dimensionless rational (unit and declared precision kept).
1098/// @param lhs Quantity to scale.
1099/// @param factor Dimensionless factor.
1100/// @return The scaled quantity, or empty.
1101template <auto U, std::uint32_t Dec>
1102[[nodiscard]] Quantity<U, Dec> operator*(const Quantity<U, Dec>& lhs, const math::Rational& factor) {
1103 std::optional<math::Rational> result =
1104 lhs.payload ? std::optional<math::Rational>{*lhs.payload * factor} : std::nullopt;
1105 Quantity<U, Dec> out;
1106 out.payload = result;
1107 MORPH_Q_BUILD(out, "*", lhs.payload, std::optional<math::Rational>{factor}, result, MORPH_Q_NODE(lhs), nullptr);
1108 return out;
1109}
1110
1111/// @brief Scales by a dimensionless rational (unit and declared precision kept).
1112/// @param factor Dimensionless factor.
1113/// @param rhs Quantity to scale.
1114/// @return The scaled quantity, or empty.
1115template <auto U, std::uint32_t Dec>
1116[[nodiscard]] Quantity<U, Dec> operator*(const math::Rational& factor, const Quantity<U, Dec>& rhs) {
1117 return rhs * factor;
1118}
1119
1120/// @brief Divides by a dimensionless rational (unit and declared precision
1121/// kept). Empty when the quantity is empty or the divisor is zero.
1122/// @param lhs Quantity to divide.
1123/// @param divisor Dimensionless divisor.
1124/// @return The divided quantity, or empty.
1125template <auto U, std::uint32_t Dec>
1126[[nodiscard]] Quantity<U, Dec> operator/(const Quantity<U, Dec>& lhs, const math::Rational& divisor) {
1127 std::optional<math::Rational> result;
1128 if (lhs.payload) {
1129 if (auto quotient = lhs.payload->dividedBy(divisor); quotient.has_value()) {
1130 result = *quotient;
1131 }
1132 }
1133 Quantity<U, Dec> out;
1134 out.payload = result;
1135 MORPH_Q_BUILD(out, "/", lhs.payload, std::optional<math::Rational>{divisor}, result, MORPH_Q_NODE(lhs), nullptr);
1136 return out;
1137}
1138
1139namespace detail {
1140
1141/// @brief Fixed-capacity string usable as a non-type template parameter.
1142///
1143/// An alias for the shared `morph::detail::FixedString` — the same underlying
1144/// type the forms layer's `Choice` uses (`morph::forms::FixedString`), so there
1145/// is one definition, not two look-alikes.
1146/// @tparam N Buffer length including the terminating null.
1147template <std::size_t N>
1148using FixedString = ::morph::detail::FixedString<N>;
1149
1150} // namespace detail
1151
1152/// @brief A `Quantity` that names itself on construction and slices losslessly
1153/// back to a plain `Quantity`. The name lives in the shared history.
1154/// @tparam Name Compile-time symbol name.
1155/// @tparam U Unit enumerator.
1156template <detail::FixedString Name, auto U>
1158 /// @brief The plain base quantity type.
1160
1161 /// @brief Constructs empty, then names.
1162 NamedQuantity() : Base{Base{}.named(std::string{Name.view()})} {}
1163
1164 /// @brief Constructs from a payload, then names.
1165 /// @param adopted Engaged or empty payload.
1166 NamedQuantity(std::optional<math::Rational> adopted)
1167 : Base{Base{std::move(adopted)}.named(std::string{Name.view()})} {}
1168
1169 /// @brief Constructs from a plain quantity, then names.
1170 /// @param quantity The source quantity.
1171 NamedQuantity(Base quantity) : Base{quantity.named(std::string{Name.view()})} {}
1172
1173 /// @brief Names a floating-point reading.
1174 ///
1175 /// Not `noexcept`: unlike `Base::fromDouble`, this wraps the result in a
1176 /// `NamedQuantity`, whose construction calls `named()` — and with
1177 /// `MORPH_QUANTITY_PROVENANCE` enabled `named()` allocates an `ASTNode`
1178 /// (`std::make_shared`), which can throw `std::bad_alloc`.
1179 /// @param raw The value to convert.
1180 /// @return The named quantity.
1181 [[nodiscard]] static NamedQuantity fromDouble(double raw) { return NamedQuantity{Base::fromDouble(raw)}; }
1182};
1183
1184/// @brief Renders @p quantity's exact decimal **alone**, with no unit suffix
1185/// (`"5.2"`, `"6"`, `"N/A"`).
1186///
1187/// The numeric half of `toString`, and the only public way to obtain it. A view
1188/// that places the number and the unit separately — a table with the unit in
1189/// its column header, a right-aligned suffix, an editable field whose adjacent
1190/// label carries the unit — asks for the two halves independently instead of
1191/// concatenating them and then chopping the suffix back off. The unit half is
1192/// `Quantity::unitMeta().display`.
1193///
1194/// This is the same exact-decimal path `std::format("{}", quantity)` takes: an
1195/// integer long division of the canonical `Rational` at its **runtime**
1196/// `DecimalPlaces`, trailing zeros trimmed. It is deliberately **not**
1197/// `std::format("{}", *quantity.value())` (which prints a `num/den` fraction)
1198/// nor `std::format("{:.Nf}", *quantity.value())` (which delegates to
1199/// `std::formatter<double>` via `toDouble()` and so leaves the exact domain —
1200/// `9007199254740993` renders as `…992` there and as itself here).
1201///
1202/// **An empty quantity renders as the fixed literal `"N/A"`, not as an empty
1203/// string.** That keeps the identity `toString(q) == toDecimalString(q) +
1204/// unitMeta().display` true for every value, engaged or not, so `toString` can
1205/// be — and is — implemented in terms of this function rather than growing a
1206/// second rendering path that can drift from it. Rendering an absent value as
1207/// blank is a presentation policy and belongs to the layer that has a layout to
1208/// decide it for; the value type only reports the fact that nothing is there.
1209/// A caller that wants a blank writes `q.hasValue() ? toDecimalString(q) : ""`.
1210/// @tparam U Unit enumerator.
1211/// @tparam Dec Declared decimals.
1212/// @param quantity The value to render.
1213/// @return The exact decimal text, or `"N/A"` when the quantity is empty.
1214template <auto U, std::uint32_t Dec>
1215[[nodiscard]] inline std::string toDecimalString(const Quantity<U, Dec>& quantity) {
1216 return detail::formatOptionalDecimal(quantity.value());
1217}
1218
1219/// @brief Renders @p quantity as value + unit (`5.2kW`, `N/A%`) — the same
1220/// text `std::format("{}", quantity)` produces via the `std::formatter`
1221/// specialization just below, exposed as a plain function so a caller
1222/// can render a `Quantity` without going through `std::format` itself.
1223///
1224/// Exists because Emscripten's bundled libc++ (older than the Linux/Windows
1225/// standard library this project otherwise builds against — see
1226/// `.github/workflows/wasm-demo.yml`'s `EMSDK_VERSION`) has a known
1227/// limitation recognising `std::formatter` partial specializations
1228/// parameterized over an `auto` non-type template parameter (`U` here) for
1229/// `std::format`'s compile-time formattability check — `std::format("{}",
1230/// someQuantity)` fails to compile there with "the supplied type is not
1231/// formattable" even though the specialization is valid and the identical
1232/// call compiles and runs correctly on every other toolchain this project
1233/// targets. `toString()` bypasses that check entirely: it calls the same
1234/// underlying logic directly instead of through `std::format`'s trait
1235/// machinery, so it works identically everywhere, WASM included.
1236///
1237/// Defined as `toDecimalString(quantity) + unitMeta().display` — the number and
1238/// the unit are one concatenation with a single implementation of each half, so
1239/// a caller that needs them apart calls `toDecimalString` rather than undoing
1240/// the join.
1241/// @tparam U Unit enumerator.
1242/// @tparam Dec Declared decimals.
1243/// @param quantity The value to render.
1244/// @return The formatted text.
1245template <auto U, std::uint32_t Dec>
1246[[nodiscard]] inline std::string toString(const Quantity<U, Dec>& quantity) {
1247 constexpr auto display = UnitTraits<decltype(U)>::meta(U).display;
1248 return toDecimalString(quantity) + std::string{display};
1249}
1250
1251} // namespace morph::units
1252
1253#if MORPH_QUANTITY_PROVENANCE
1255#endif
1256
1257/// @brief Renders value + unit (`5.2kW`, `N/A%`); no `operator<<` is provided.
1258/// Delegates to `morph::units::toString` so the two never drift.
1259/// @tparam U Unit enumerator.
1260/// @tparam Dec Declared decimals.
1261template <auto U, std::uint32_t Dec>
1262struct std::formatter<morph::units::Quantity<U, Dec>> {
1263 /// @brief Accepts the empty format spec.
1264 /// @param ctx Parse context.
1265 /// @return Iterator past the parsed spec.
1266 constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }
1267
1268 /// @brief Formats the quantity.
1269 /// @param quantity The value to format.
1270 /// @param ctx Format context.
1271 /// @return Output iterator past the written text.
1272 auto format(const morph::units::Quantity<U, Dec>& quantity, std::format_context& ctx) const {
1273 return std::format_to(ctx.out(), "{}", morph::units::toString(quantity));
1274 }
1275};
1276
1277/// @brief Forwards to the `Quantity` formatter.
1278/// @tparam Name Symbol name.
1279/// @tparam U Unit enumerator.
1280template <morph::units::detail::FixedString Name, auto U>
1281struct std::formatter<morph::units::NamedQuantity<Name, U>> : std::formatter<morph::units::Quantity<U>> {
1282 /// @brief Formats via the base `Quantity` formatter.
1283 /// @param quantity The named value.
1284 /// @param ctx Format context.
1285 /// @return Output iterator past the written text.
1286 auto format(const morph::units::NamedQuantity<Name, U>& quantity, std::format_context& ctx) const {
1287 return std::formatter<morph::units::Quantity<U>>::format(quantity, ctx);
1288 }
1289};
1290
1291// ---------------------------------------------------------------------------
1292// Glaze integration: wire shape + schema units.
1293// ---------------------------------------------------------------------------
1294
1295/// @brief On the wire a Quantity is its nullable Rational payload — neither the
1296/// unit nor the declared precision travels.
1297/// @tparam U Unit enumerator.
1298/// @tparam Dec Declared decimals.
1299template <auto U, std::uint32_t Dec>
1300struct glz::meta<morph::units::Quantity<U, Dec>> {
1301 /// @brief The single wire field: the payload.
1302 static constexpr auto value = &morph::units::Quantity<U, Dec>::payload;
1303
1304 /// @brief The unit's ascii id as the type name.
1305 static constexpr std::string_view name = morph::units::UnitTraits<decltype(U)>::meta(U).id;
1306};
1307
1308namespace glz::detail {
1309
1310/// @brief Schema generation for `Quantity<U, Dec>`: the nullable-Rational schema
1311/// with the unit stamped on as `ExtUnits`, sourced from `UnitTraits`.
1312/// @tparam U Unit enumerator.
1313/// @tparam Dec Declared decimals.
1314template <auto U, std::uint32_t Dec>
1315struct to_json_schema<morph::units::Quantity<U, Dec>> {
1316 /// @brief Emits the schema.
1317 /// @tparam Opts Glaze options.
1318 /// @param outSchema Schema being built.
1319 /// @param defs Schema definitions.
1320 template <auto Opts>
1321 static void op(auto& outSchema, auto& defs) {
1322 to_json_schema<std::optional<morph::math::Rational>>::template op<Opts>(outSchema, defs);
1323 constexpr auto unitMeta = morph::units::UnitTraits<decltype(U)>::meta(U);
1324 outSchema.ExtUnits = ExtUnits{.unitAscii = unitMeta.id, .unitUnicode = unitMeta.display};
1325 }
1326};
1327
1328} // namespace glz::detail
1329
1330/// @brief Stable shape tag for `Quantity<U, Dec>`, carrying the unit's ascii id
1331/// and the declared decimals.
1332///
1333/// This is the one place a `Quantity` retype can be caught at all. Neither the
1334/// unit nor the declared precision travels on the wire (see the `glz::meta`
1335/// above — a `Quantity` *is* its nullable `Rational` payload), so swapping
1336/// `Quantity<Unit::Gram>` for `Quantity<Unit::Litre>` in a recorded action
1337/// produces byte-identical JSON: no decode can notice, and before this tag no
1338/// fingerprint could either. `UnitMeta::id` is an author-declared ascii
1339/// identifier that is already part of the protocol vocabulary, so it is stable
1340/// across compilers in a way `glz::name_v` is not. See
1341/// `morph/core/payload_shape_tag.hpp`.
1342/// @tparam U Unit enumerator.
1343/// @tparam Dec Declared decimals.
1344template <auto U, std::uint32_t Dec>
1346 /// @brief This type's stable shape name, e.g. `"quantity.gram.3"`.
1347 /// @return The name; the referenced storage lives for the whole process.
1348 static std::string_view name() {
1349 static const std::string kName =
1350 "quantity." + std::string{morph::units::UnitTraits<decltype(U)>::meta(U).id} + '.' + std::to_string(Dec);
1351 return kName;
1352 }
1353};
Handle< std::string > format(AnyHandle const &pattern, std::vector< AnyHandle > args)
format: a template with {0}, {1}, … placeholders.
Definition author.hpp:970
Handle< std::int64_t > count(AnyHandle const &list)
count: a list's length.
Definition author.hpp:1000
Concept: From → To is convertible — a user override takes precedence over an auto-generated ratio pat...
Definition quantity.hpp:526
Concept: UnitTraits<E> declares an optional static constexpr QuantityBounds bounds(E) — the pre-decod...
Definition quantity.hpp:294
Concept: UnitTraits<E> also declares within-dimension relations (template param E).
Definition quantity.hpp:284
Concept: the application supplied a UnitTraits<E>::convert static for this pair (a non-ratio override...
Definition quantity.hpp:520
Concept: the auto-generated ratio convert applies to From → To (template params From,...
Definition quantity.hpp:500
Concept: an enum with a UnitTraits specialisation (template param E).
Definition quantity.hpp:278
@ Quantity
An exact {num, den, dp} in a unit, with optional convertible alternatives.
Shared compile-time, NTTP-capable fixed string.
Opt-in, compiler-independent shape tag for a type with a custom Glaze codec.
constexpr bool operator==(const Quantity< U, DecA > &lhs, const Quantity< U, DecB > &rhs) noexcept
Equality across same-unit quantities of any declared precision. Ignores the runtime precision tag; em...
Definition quantity.hpp:973
Quantity< A *B > operator*(const Quantity< A, DecA > &lhs, const Quantity< B, DecB > &rhs)
Cross-unit product; the result unit is A * B per the application's consteval unit algebra....
std::string toString(const Quantity< U, Dec > &quantity)
Renders quantity as value + unit (5.2kW, N/A%) — the same text std::format("{}", quantity) produces v...
std::string toDecimalString(const Quantity< U, Dec > &quantity)
Renders quantity's exact decimal alone, with no unit suffix ("5.2", "6", "N/A").
constexpr std::size_t kDefaultEquationSteps
How many derivation steps equation() writes out by default.
Definition quantity.hpp:483
constexpr bool isQuantity
true when T (cvref-stripped) is a morph::units::Quantity.
Definition quantity.hpp:950
constexpr std::strong_ordering operator<=>(const Quantity< U, DecA > &lhs, const Quantity< U, DecB > &rhs)
Ordering across same-unit quantities. Precondition: both operands are engaged; comparing an empty ope...
Definition quantity.hpp:985
Quantity< A/B > operator/(const Quantity< A, DecA > &lhs, const Quantity< B, DecB > &rhs)
Cross-unit quotient; the result unit is A / B per the application's consteval unit algebra....
Quantity< U > operator+(const Quantity< U, DecA > &lhs, const Quantity< U, DecB > &rhs)
Same-unit sum across any declared precisions.
constexpr std::size_t kEquationStepsUnlimited
Pass as equation()'s limit to write the derivation out in full.
Definition quantity.hpp:490
Quantity< U > operator-(const Quantity< U, DecA > &lhs, const Quantity< U, DecB > &rhs)
Same-unit difference across any declared precisions.
void convert(const Quantity< From > &in, Quantity< To > &out)
Auto-generated within-dimension conversion (ratio + chaining).
Definition quantity.hpp:956
Quantity::equation() renderer (provenance builds only).
Exact rational arithmetic for morph model/action values.
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...
RoundingMode
Rule for resolving a value sitting exactly halfway between the two nearest results when rounding to a...
Definition rational.hpp:190
@ HalfAwayFromZero
Ties go to the neighbour of larger magnitude: 1.25 -> 1.3, -1.25 -> -1.3.
Definition rational.hpp:191
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
A structural, NTTP-capable compile-time string.
Strong type for a decimal-precision count.
Definition rational.hpp:121
std::uint32_t value
Raw digit count. Rational's invariant keeps it in [0, kMaxDecimalPlaces].
Definition rational.hpp:123
Exact rational number with a runtime decimal-precision tag.
Definition rational.hpp:375
DecimalPlaces decimalPlaces
Decimal-precision tag. Invariant: 0 <= value <= kMaxDecimalPlaces.
Definition rational.hpp:383
static std::expected< Rational, RationalError > fromFloat(double value, DecimalPlaces wantedPrecision) noexcept
Converts a double to a Rational scaled to wantedPrecision.
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
static std::string_view name()
This type's stable shape name, e.g. "quantity.gram.3".
Declares a stable shape tag for a type T whose JSON shape payloadShape cannot decompose.
NamedQuantity()
Constructs empty, then names.
NamedQuantity(Base quantity)
Constructs from a plain quantity, then names.
static NamedQuantity fromDouble(double raw)
Names a floating-point reading.
NamedQuantity(std::optional< math::Rational > adopted)
Constructs from a payload, then names.
Quantity< U > Base
The plain base quantity type.
Optional physical/wire bounds for one unit: the inclusive range a decoded value must fall within to b...
Definition quantity.hpp:261
morph::math::Rational max
Inclusive upper bound.
Definition quantity.hpp:265
morph::math::Rational min
Inclusive lower bound.
Definition quantity.hpp:263
A unit-tagged, optionally-empty exact value with a declared precision in its type and (optionally) a ...
Definition quantity.hpp:671
Quantity(std::optional< math::Rational > adopted)
Adopts an optional payload as-is.
Definition quantity.hpp:692
Quantity named(std::string label) const
Seals this value with a name: equation() shows the name instead of expanding its derivation....
Definition quantity.hpp:867
constexpr math::Rational value_or(const math::Rational &fallback) const noexcept
The payload if engaged, otherwise a caller-supplied fallback.
Definition quantity.hpp:786
constexpr Quantity() noexcept=default
Constructs the empty state (no recorded derivation).
constexpr bool withinDeclaredBounds() const noexcept
Pre-decode validation seam: whether the current payload falls within this field's unit-declared bound...
Definition quantity.hpp:763
static constexpr std::uint32_t declaredDecimals
Definition quantity.hpp:709
Quantity atDeclaredPrecision() const
Rounds the value to this field's declared precision — the one the generated schema advertises as x-de...
Definition quantity.hpp:860
static Quantity fromOptional(std::optional< math::Rational > adopted)
Wraps an optional payload, preserving the declared precision.
Definition quantity.hpp:742
Quantity(const Quantity< U, OtherDecimals > &other)
Converts from the same unit at another declared precision; the value, its runtime tag,...
Definition quantity.hpp:699
std::optional< math::Rational > payload
Definition quantity.hpp:676
static constexpr math::DecimalPlaces declaredPrecision() noexcept
The declared precision as a DecimalPlaces strong type.
Definition quantity.hpp:717
static constexpr UnitMeta unitMeta() noexcept
The unit's static metadata (id, display text, default decimals).
Definition quantity.hpp:713
static constexpr std::span< const UnitAlternative< decltype(U)> > unitAlternatives() noexcept
The convertible display/entry units for this field's unit, derived from UnitTraits::relations.
Definition quantity.hpp:724
static constexpr auto unit
Definition quantity.hpp:706
constexpr const std::optional< math::Rational > & value() const noexcept
The payload, for pattern-matching / -> access.
Definition quantity.hpp:779
Quantity withDecimalPlaces(math::DecimalPlaces newPrecision) const
Retags the value's runtime precision (the exact value itself is unchanged — precision only affects ro...
Definition quantity.hpp:805
std::vector< std::string > equation(std::size_t maxSteps=kDefaultEquationSteps) const
The worked derivation as print-ready lines (see docs/spec/util/quantity_type.md).
Definition quantity.hpp:894
Quantity roundedToDecimalPlaces(math::DecimalPlaces newPrecision, math::RoundingMode mode=math::RoundingMode::HalfAwayFromZero) const
Rounds the value to a runtime precision, changing the exact value as well as the tag — unlike withDec...
Definition quantity.hpp:835
static Quantity fromDouble(double raw) noexcept
Converts a floating-point reading at the declared precision.
Definition quantity.hpp:732
constexpr bool hasValue() const noexcept
Whether a value has been entered/measured.
Definition quantity.hpp:748
constexpr const math::Rational & operator*() const noexcept
Unchecked access to the engaged value (UB when empty, exactly like std::optional::operator*).
Definition quantity.hpp:795
A derived per-unit view of one convertible display/entry unit.
Definition quantity.hpp:242
std::int64_t den
Denominator of the exact alternative-to-canonical ratio.
Definition quantity.hpp:250
std::int64_t num
Numerator of the exact alternative-to-canonical ratio.
Definition quantity.hpp:247
E unit
The alternative (convertible) unit.
Definition quantity.hpp:244
Static description of one unit, supplied by UnitTraits.
Definition quantity.hpp:204
std::uint32_t defaultDecimals
Default decimal places for parsing/formatting values of this unit; surfaces in schemas as x-decimalPl...
Definition quantity.hpp:214
std::string_view display
Human display text, e.g. "kg/m³" (ExtUnits.unitUnicode).
Definition quantity.hpp:210
std::string_view id
Stable ascii identifier, e.g. "kg_per_m3". Appears in JSON Schemas (ExtUnits.unitAscii); part of the ...
Definition quantity.hpp:207
One exact within-dimension conversion ratio between two peer units.
Definition quantity.hpp:224
E from
Source unit of the ratio.
Definition quantity.hpp:226
E to
Target unit of the ratio.
Definition quantity.hpp:229
morph::math::Rational fromTo
Exact ratio: 1 from == fromTo · to. Strictly positive.
Definition quantity.hpp:232
Customisation point: the application specialises this for its unit enum, returning a UnitMeta per enu...
Definition quantity.hpp:274