morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
quantity.hpp File Reference

Unit-tagged, optionally-empty exact values with a derivation trail. More...

#include <array>
#include <compare>
#include <cstdint>
#include <format>
#include <glaze/glaze.hpp>
#include <limits>
#include <morph/core/payload_shape_tag.hpp>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
#include "../attributes.hpp"
#include "../detail/fixed_string.hpp"
#include "rational.hpp"

Go to the source code of this file.

Classes

struct  morph::units::UnitMeta
 Static description of one unit, supplied by UnitTraits. More...
struct  morph::units::UnitRelation< E >
 One exact within-dimension conversion ratio between two peer units. More...
struct  morph::units::UnitAlternative< E >
 A derived per-unit view of one convertible display/entry unit. More...
struct  morph::units::QuantityBounds
 Optional physical/wire bounds for one unit: the inclusive range a decoded value must fall within to be accepted. Returned by the optional UnitTraits<E>::bounds(E) customisation point (detected by HasUnitBounds) — e.g. a percentage unit bounding itself to [0, 100], or a mass unit rejecting a negative reading no scale can physically produce. Absent by default: a unit with no declared bounds accepts any value its precision allows, exactly as before this feature existed. More...
struct  morph::units::Quantity< U, DeclaredDecimals >
 A unit-tagged, optionally-empty exact value with a declared precision in its type and (optionally) a recorded derivation. More...
struct  morph::units::NamedQuantity< Name, U >
 A Quantity that names itself on construction and slices losslessly back to a plain Quantity. The name lives in the shared history. More...
struct  morph::model::PayloadShapeTag< morph::units::Quantity< U, Dec > >
 Stable shape tag for Quantity<U, Dec>, carrying the unit's ascii id and the declared decimals. More...

Concepts

concept  morph::units::UnitEnum
 Concept: an enum with a UnitTraits specialisation (template param E).
concept  morph::units::HasUnitRelations
 Concept: UnitTraits<E> also declares within-dimension relations (template param E).
concept  morph::units::HasUnitBounds
 Concept: UnitTraits<E> declares an optional static constexpr QuantityBounds bounds(E) — the pre-decode validation seam a field's unit opts into. A unit enum with no bounds() simply has none: every value its precision allows is accepted, unchanged from before this feature existed. E is the application's unit enum type.
concept  morph::units::RatioConvertible
 Concept: the auto-generated ratio convert applies to From → To (template params From, To).
concept  morph::units::HasUserConvert
 Concept: the application supplied a UnitTraits<E>::convert static for this pair (a non-ratio override, e.g. °C → °F) (template params From, To).
concept  morph::units::Convertible
 Concept: From → To is convertible — a user override takes precedence over an auto-generated ratio path (template params From, To).

Macros

#define MORPH_QUANTITY_PROVENANCE   1
 Build-wide provenance toggle. Default on; define to 0 to compile the derivation DAG out (no ASTNode allocations).

Functions

template<auto From, auto To>
requires RatioConvertible<From, To>
void morph::units::convert (const Quantity< From > &in, Quantity< To > &out)
 Auto-generated within-dimension conversion (ratio + chaining).
template<auto U, std::uint32_t DecA, std::uint32_t DecB>
constexpr bool morph::units::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; empty==empty is true.
template<auto U, std::uint32_t DecA, std::uint32_t DecB>
constexpr std::strong_ordering morph::units::operator<=> (const Quantity< U, DecA > &lhs, const Quantity< U, DecB > &rhs)
 Ordering across same-unit quantities. Precondition: both operands are engaged; comparing an empty operand throws std::logic_error (a defined diagnostic — ordering an absent value is a programming error).
template<auto U, std::uint32_t DecA, std::uint32_t DecB>
Quantity< U > morph::units::operator+ (const Quantity< U, DecA > &lhs, const Quantity< U, DecB > &rhs)
 Same-unit sum across any declared precisions.
template<auto U, std::uint32_t DecA, std::uint32_t DecB>
Quantity< U > morph::units::operator- (const Quantity< U, DecA > &lhs, const Quantity< U, DecB > &rhs)
 Same-unit difference across any declared precisions.
template<auto A, std::uint32_t DecA, auto B, std::uint32_t DecB>
requires (detail::SameEnumDistinct<A, B> && requires(Quantity<B, DecB> value) { static_cast<Quantity<A>>(value); })
Quantity< A > morph::units::operator+ (const Quantity< A, DecA > &lhs, const Quantity< B, DecB > &rhs)
 Mixed-unit sum: converts rhs to lhs's unit first.
template<auto A, std::uint32_t DecA, auto B, std::uint32_t DecB>
requires (detail::SameEnumDistinct<A, B> && requires(Quantity<B, DecB> value) { static_cast<Quantity<A>>(value); })
Quantity< A > morph::units::operator- (const Quantity< A, DecA > &lhs, const Quantity< B, DecB > &rhs)
 Mixed-unit difference: converts rhs to lhs's unit first.
template<auto U, std::uint32_t Dec>
Quantity< U, Dec > morph::units::operator- (const Quantity< U, Dec > &operand)
 Negation. Empty stays empty; the declared precision is kept.
template<auto A, std::uint32_t DecA, auto B, std::uint32_t DecB>
requires requires { typename std::type_identity_t<Quantity<A * B>>; }
Quantity< A *B > morph::units::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. Removed from overload resolution when the algebra rejects the combination.
template<auto A, std::uint32_t DecA, auto B, std::uint32_t DecB>
requires requires { typename std::type_identity_t<Quantity<A / B>>; }
Quantity< A/B > morph::units::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. Empty when either side is empty or the divisor is zero.
template<auto U, std::uint32_t Dec>
Quantity< U, Dec > morph::units::operator* (const Quantity< U, Dec > &lhs, const math::Rational &factor)
 Scales by a dimensionless rational (unit and declared precision kept).
template<auto U, std::uint32_t Dec>
Quantity< U, Dec > morph::units::operator* (const math::Rational &factor, const Quantity< U, Dec > &rhs)
 Scales by a dimensionless rational (unit and declared precision kept).
template<auto U, std::uint32_t Dec>
Quantity< U, Dec > morph::units::operator/ (const Quantity< U, Dec > &lhs, const math::Rational &divisor)
 Divides by a dimensionless rational (unit and declared precision kept). Empty when the quantity is empty or the divisor is zero.
template<auto U, std::uint32_t Dec>
std::string morph::units::toDecimalString (const Quantity< U, Dec > &quantity)
 Renders quantity's exact decimal alone, with no unit suffix ("5.2", "6", "N/A").
template<auto U, std::uint32_t Dec>
std::string morph::units::toString (const Quantity< U, Dec > &quantity)
 Renders quantity as value + unit (5.2kW, N/A%) — the same text std::format("{}", quantity) produces via the std::formatter specialization just below, exposed as a plain function so a caller can render a Quantity without going through std::format itself.

Variables

constexpr std::size_t morph::units::kDefaultEquationSteps = 100
 How many derivation steps equation() writes out by default.
constexpr std::size_t morph::units::kEquationStepsUnlimited = std::numeric_limits<std::size_t>::max()
 Pass as equation()'s limit to write the derivation out in full.
template<typename T>
constexpr bool morph::units::isQuantity = detail::IsQuantity<std::remove_cvref_t<T>>::value
 true when T (cvref-stripped) is a morph::units::Quantity.

Detailed Description

Unit-tagged, optionally-empty exact values with a derivation trail.

morph::units::Quantity<U> wraps an optional morph::math::Rational and knows three things about itself: its unit (a compile-time template tag), its exactness (an exact Rational, with a declared and a runtime notion of precision, and std::nullopt as a real "not entered" state), and how it was computed — its derivation, exposed through equation().

See docs/spec/util/quantity_type.md for the full design. The essentials:

  • Units are types. Quantity<Unit::kg> and Quantity<Unit::m3> cannot be mixed; operator* / operator/ deduce the result unit from the application-defined consteval algebra on the unit enum. Within-dimension scaling is declared as UnitTraits<E>::relations (a flat list of exact UnitRelation ratios), which drive convert, conversion chaining, and the schema's display-unit selector.
  • Precision is declared in the type; actual precision is runtime data. Quantity<Unit::m3, 4> overrides the unit default; the value's actual precision is the runtime DecimalPlaces tag inside the Rational and max-propagates through arithmetic. Magnitude is bounded by int64; overflow is a documented limitation, only division-by-zero yields empty.
  • Empty propagates (SQL-NULL semantics). == is total; relational ordering has a precondition that both operands are engaged.
  • Provenance is a build-wide toggle, MORPH_QUANTITY_PROVENANCE (default 1). With it 0, no derivation nodes are allocated and the provenance API returns empty results; Quantity<U> is the same type either way.
  • Wire. On the morph JSON wire a quantity is its nullable Rational payload; the unit lives in generated schemas (ExtUnits) and C++ types.

Definition in file quantity.hpp.

Macro Definition Documentation

◆ MORPH_QUANTITY_PROVENANCE

#define MORPH_QUANTITY_PROVENANCE   1

Build-wide provenance toggle. Default on; define to 0 to compile the derivation DAG out (no ASTNode allocations).

The default is 1 deliberately: the toggle changes observable behaviour, not just cost — with it 0, equation() collapses to the bare value and named() discards the name — so a build that never set it must not have that output emptied underneath it. The cost is real and measured, though: a 200,000-iteration running total took 54,056 KB and 0.034 s with provenance against 12,236 KB and 0.006 s without (clang 22, -O2). A bulk path that never calls equation() should set this to 0. See docs/spec/util/quantity_type.md, Limitations.

Definition at line 48 of file quantity.hpp.

Function Documentation

◆ convert()

template<auto From, auto To>
requires RatioConvertible<From, To>
void morph::units::convert ( const Quantity< From > & in,
Quantity< To > & out )

Auto-generated within-dimension conversion (ratio + chaining).

A constrained template; a UnitTraits<E>::convert static takes precedence for the pairs it names. Performs the value math only, on an engaged operand — empty-propagation and provenance are added by the caller (the conversion operator).

Template Parameters
FromSource unit enumerator.
ToTarget unit enumerator.
Parameters
inEngaged source quantity.
outReceives the converted value.

Definition at line 956 of file quantity.hpp.

◆ operator==()

template<auto U, std::uint32_t DecA, std::uint32_t DecB>
bool morph::units::operator== ( const Quantity< U, DecA > & lhs,
const Quantity< U, DecB > & rhs )
nodiscardconstexprnoexcept

Equality across same-unit quantities of any declared precision. Ignores the runtime precision tag; empty==empty is true.

Parameters
lhsLeft operand.
rhsRight operand.
Returns
true when both empty or both engaged with equal value.

Definition at line 973 of file quantity.hpp.

◆ operator<=>()

template<auto U, std::uint32_t DecA, std::uint32_t DecB>
std::strong_ordering morph::units::operator<=> ( const Quantity< U, DecA > & lhs,
const Quantity< U, DecB > & rhs )
nodiscardconstexpr

Ordering across same-unit quantities. Precondition: both operands are engaged; comparing an empty operand throws std::logic_error (a defined diagnostic — ordering an absent value is a programming error).

Parameters
lhsLeft operand.
rhsRight operand.
Returns
The ordering of the two exact values.
Exceptions
std::logic_errorwhen either operand is empty.

Definition at line 985 of file quantity.hpp.

◆ operator+() [1/2]

template<auto U, std::uint32_t DecA, std::uint32_t DecB>
Quantity< U > morph::units::operator+ ( const Quantity< U, DecA > & lhs,
const Quantity< U, DecB > & rhs )
nodiscard

Same-unit sum across any declared precisions.

Parameters
lhsLeft operand.
rhsRight operand.
Returns
The exact sum, or empty when either operand is empty.

Definition at line 1003 of file quantity.hpp.

◆ operator-() [1/3]

template<auto U, std::uint32_t DecA, std::uint32_t DecB>
Quantity< U > morph::units::operator- ( const Quantity< U, DecA > & lhs,
const Quantity< U, DecB > & rhs )
nodiscard

Same-unit difference across any declared precisions.

Parameters
lhsLeft operand.
rhsRight operand.
Returns
The exact difference, or empty when either operand is empty.

Definition at line 1017 of file quantity.hpp.

◆ operator+() [2/2]

template<auto A, std::uint32_t DecA, auto B, std::uint32_t DecB>
requires (detail::SameEnumDistinct<A, B> && requires(Quantity<B, DecB> value) { static_cast<Quantity<A>>(value); })
Quantity< A > morph::units::operator+ ( const Quantity< A, DecA > & lhs,
const Quantity< B, DecB > & rhs )
nodiscard

Mixed-unit sum: converts rhs to lhs's unit first.

Parameters
lhsLeft operand (its unit is the result unit).
rhsRight operand (a convertible unit).
Returns
The exact sum in A's unit, or empty.

Definition at line 1032 of file quantity.hpp.

◆ operator-() [2/3]

template<auto A, std::uint32_t DecA, auto B, std::uint32_t DecB>
requires (detail::SameEnumDistinct<A, B> && requires(Quantity<B, DecB> value) { static_cast<Quantity<A>>(value); })
Quantity< A > morph::units::operator- ( const Quantity< A, DecA > & lhs,
const Quantity< B, DecB > & rhs )
nodiscard

Mixed-unit difference: converts rhs to lhs's unit first.

Parameters
lhsLeft operand (its unit is the result unit).
rhsRight operand (a convertible unit).
Returns
The exact difference in A's unit, or empty.

Definition at line 1042 of file quantity.hpp.

◆ operator-() [3/3]

template<auto U, std::uint32_t Dec>
Quantity< U, Dec > morph::units::operator- ( const Quantity< U, Dec > & operand)
nodiscard

Negation. Empty stays empty; the declared precision is kept.

Parameters
operandValue to negate.
Returns
The negated value, or empty.

Definition at line 1050 of file quantity.hpp.

◆ operator*() [1/3]

template<auto A, std::uint32_t DecA, auto B, std::uint32_t DecB>
requires requires { typename std::type_identity_t<Quantity<A * B>>; }
Quantity< A *B > morph::units::operator* ( const Quantity< A, DecA > & lhs,
const Quantity< B, DecB > & rhs )
nodiscard

Cross-unit product; the result unit is A * B per the application's consteval unit algebra. Removed from overload resolution when the algebra rejects the combination.

Parameters
lhsLeft operand.
rhsRight operand.
Returns
The exact product in the deduced unit, or empty.

Definition at line 1067 of file quantity.hpp.

◆ operator/() [1/2]

template<auto A, std::uint32_t DecA, auto B, std::uint32_t DecB>
requires requires { typename std::type_identity_t<Quantity<A / B>>; }
Quantity< A/B > morph::units::operator/ ( const Quantity< A, DecA > & lhs,
const Quantity< B, DecB > & rhs )
nodiscard

Cross-unit quotient; the result unit is A / B per the application's consteval unit algebra. Empty when either side is empty or the divisor is zero.

Parameters
lhsDividend.
rhsDivisor.
Returns
The exact quotient in the deduced unit, or empty.

Definition at line 1084 of file quantity.hpp.

◆ operator*() [2/3]

template<auto U, std::uint32_t Dec>
Quantity< U, Dec > morph::units::operator* ( const Quantity< U, Dec > & lhs,
const math::Rational & factor )
nodiscard

Scales by a dimensionless rational (unit and declared precision kept).

Parameters
lhsQuantity to scale.
factorDimensionless factor.
Returns
The scaled quantity, or empty.

Definition at line 1102 of file quantity.hpp.

◆ operator*() [3/3]

template<auto U, std::uint32_t Dec>
Quantity< U, Dec > morph::units::operator* ( const math::Rational & factor,
const Quantity< U, Dec > & rhs )
nodiscard

Scales by a dimensionless rational (unit and declared precision kept).

Parameters
factorDimensionless factor.
rhsQuantity to scale.
Returns
The scaled quantity, or empty.

Definition at line 1116 of file quantity.hpp.

◆ operator/() [2/2]

template<auto U, std::uint32_t Dec>
Quantity< U, Dec > morph::units::operator/ ( const Quantity< U, Dec > & lhs,
const math::Rational & divisor )
nodiscard

Divides by a dimensionless rational (unit and declared precision kept). Empty when the quantity is empty or the divisor is zero.

Parameters
lhsQuantity to divide.
divisorDimensionless divisor.
Returns
The divided quantity, or empty.

Definition at line 1126 of file quantity.hpp.

◆ toDecimalString()

template<auto U, std::uint32_t Dec>
std::string morph::units::toDecimalString ( const Quantity< U, Dec > & quantity)
inlinenodiscard

Renders quantity's exact decimal alone, with no unit suffix ("5.2", "6", "N/A").

The numeric half of toString, and the only public way to obtain it. A view that places the number and the unit separately — a table with the unit in its column header, a right-aligned suffix, an editable field whose adjacent label carries the unit — asks for the two halves independently instead of concatenating them and then chopping the suffix back off. The unit half is Quantity::unitMeta().display.

This is the same exact-decimal path std::format("{}", quantity) takes: an integer long division of the canonical Rational at its runtime DecimalPlaces, trailing zeros trimmed. It is deliberately not std::format("{}", *quantity.value()) (which prints a num/den fraction) nor std::format("{:.Nf}", *quantity.value()) (which delegates to std::formatter<double> via toDouble() and so leaves the exact domain — 9007199254740993 renders as …992 there and as itself here).

An empty quantity renders as the fixed literal "N/A", not as an empty string. That keeps the identity toString(q) == toDecimalString(q) + unitMeta().display true for every value, engaged or not, so toString can be — and is — implemented in terms of this function rather than growing a second rendering path that can drift from it. Rendering an absent value as blank is a presentation policy and belongs to the layer that has a layout to decide it for; the value type only reports the fact that nothing is there. A caller that wants a blank writes q.hasValue() ? toDecimalString(q) : "".

Template Parameters
UUnit enumerator.
DecDeclared decimals.
Parameters
quantityThe value to render.
Returns
The exact decimal text, or "N/A" when the quantity is empty.

Definition at line 1215 of file quantity.hpp.

◆ toString()

template<auto U, std::uint32_t Dec>
std::string morph::units::toString ( const Quantity< U, Dec > & quantity)
inlinenodiscard

Renders quantity as value + unit (5.2kW, N/A%) — the same text std::format("{}", quantity) produces via the std::formatter specialization just below, exposed as a plain function so a caller can render a Quantity without going through std::format itself.

Exists because Emscripten's bundled libc++ (older than the Linux/Windows standard library this project otherwise builds against — see .github/workflows/wasm-demo.yml's EMSDK_VERSION) has a known limitation recognising std::formatter partial specializations parameterized over an auto non-type template parameter (U here) for std::format's compile-time formattability check — std::format("{}", someQuantity) fails to compile there with "the supplied type is not formattable" even though the specialization is valid and the identical call compiles and runs correctly on every other toolchain this project targets. toString() bypasses that check entirely: it calls the same underlying logic directly instead of through std::format's trait machinery, so it works identically everywhere, WASM included.

Defined as toDecimalString(quantity) + unitMeta().display — the number and the unit are one concatenation with a single implementation of each half, so a caller that needs them apart calls toDecimalString rather than undoing the join.

Template Parameters
UUnit enumerator.
DecDeclared decimals.
Parameters
quantityThe value to render.
Returns
The formatted text.

Definition at line 1246 of file quantity.hpp.

Variable Documentation

◆ kDefaultEquationSteps

std::size_t morph::units::kDefaultEquationSteps = 100
inlineconstexpr

How many derivation steps equation() writes out by default.

A derivation has no bound — total = total + row over a data-driven loop records one step per iteration — so without a limit equation() renders every one of them into a single line — measured at 100,000 steps: a 500,001-character line in 58.8 s. This is where a rendered formula stops being something a person reads, not where the cost stops being tolerable: a caller that wants more passes its own limit, so erring low costs one argument while erring high costs an unreadable line nobody asked for.

Definition at line 483 of file quantity.hpp.

◆ kEquationStepsUnlimited

std::size_t morph::units::kEquationStepsUnlimited = std::numeric_limits<std::size_t>::max()
inlineconstexpr

Pass as equation()'s limit to write the derivation out in full.

No step is elided, and the cost is the caller's, taken deliberately. Used by the depth regression tests, which exist precisely to walk a derivation deeper than any limit would render.

Definition at line 490 of file quantity.hpp.

◆ isQuantity

template<typename T>
bool morph::units::isQuantity = detail::IsQuantity<std::remove_cvref_t<T>>::value
inlineconstexpr

true when T (cvref-stripped) is a morph::units::Quantity.

Template Parameters
TCandidate type.

Definition at line 950 of file quantity.hpp.