47#ifndef MORPH_QUANTITY_PROVENANCE
48#define MORPH_QUANTITY_PROVENANCE 1
55#include <glaze/glaze.hpp>
67#if MORPH_QUANTITY_PROVENANCE
69#include <unordered_map>
72#include "../attributes.hpp"
76namespace morph::units {
107 bool const negative = value.
numerator < 0;
113 auto const num = ::morph::math::detail::absU64(value.
numerator);
114 auto const den =
static_cast<std::uint64_t
>(value.
denominator);
118 std::uint64_t
const integerPart = num / den;
119 std::uint64_t remainder = num % den;
128 auto const mulTenDivMod = [den](std::uint64_t rem, std::uint64_t& quotientDigit) -> std::uint64_t {
129 std::uint64_t acc = 0;
131 for (
int k = 0; k < 10; ++k) {
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)));
153 std::uint64_t roundedInteger = integerPart;
154 if (remainder * 2 >= den) {
156 for (std::size_t i = frac.size(); i-- > 0 && carry;) {
157 if (frac[i] ==
'9') {
169 std::string text = std::to_string(roundedInteger);
176 if (
auto const dot = text.find(
'.'); dot != std::string::npos) {
177 std::size_t last = text.find_last_not_of(
'0');
181 text.erase(last + 1);
185 if (negative && text !=
"0") {
186 text.insert(text.begin(),
'-');
197[[nodiscard]]
inline std::string formatOptionalDecimal(
const std::optional<morph::math::Rational>& value) {
198 return value ? formatRationalDecimal(*value) : std::string{
"N/A"};
278concept UnitEnum = std::is_enum_v<E> &&
requires(E unit) {
329[[nodiscard]]
consteval bool requirePositiveRatio(
const morph::math::Rational& ratio) {
333 throw "UnitRelation::fromTo must be strictly positive (numerator > 0 and denominator > 0)";
348[[nodiscard]]
consteval morph::math::Rational reciprocal(
const morph::math::Rational& value) {
349 static_cast<void>(requirePositiveRatio(value));
350 return morph::math::Rational{morph::math::Numerator{value.
denominator}, morph::math::Denominator{value.
numerator},
364[[nodiscard]]
consteval RatioResult<E> conversionRatio(E from, E to) {
365 using morph::math::Rational;
367 return RatioResult<E>{
true, Rational::one(morph::math::DecimalPlaces{1})};
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;
376 ratios[
count] = Rational::one(morph::math::DecimalPlaces{1});
378 while (head < count) {
379 E
const current = nodes[head];
380 Rational
const currentRatio = ratios[head];
382 for (
auto const& relation : UnitTraits<E>::relations) {
385 bool touches =
false;
386 if (relation.from == current) {
387 neighbour = relation.to;
388 static_cast<void>(requirePositiveRatio(relation.fromTo));
389 edge = relation.fromTo;
391 }
else if (relation.to == current) {
392 neighbour = relation.from;
393 edge = reciprocal(relation.fromTo);
400 for (std::size_t i = 0; i <
count; ++i) {
401 if (nodes[i] == neighbour) {
409 Rational
const neighbourRatio = currentRatio * edge;
410 if (neighbour == to) {
411 return RatioResult<E>{
true, neighbourRatio};
413 nodes[
count] = neighbour;
414 ratios[
count] = neighbourRatio;
418 return RatioResult<E>{
false, Rational{}};
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) {
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) {
451 UnitAlternative<E>{relation.from, relation.fromTo.numerator, relation.fromTo.denominator};
453 }
else if (relation.from == U) {
455 UnitAlternative<E>{relation.to, relation.fromTo.denominator, relation.fromTo.numerator};
466inline constexpr auto kUnitAlternatives = makeAlternatives<U>();
469template <auto A, auto B>
470concept SameEnumDistinct = std::same_as<
decltype(A),
decltype(B)> && (A != B);
494template <auto U, std::u
int32_t DeclaredDecimals = UnitTraits<decltype(U)>::meta(U).defaultDecimals>
499template <auto From, auto To>
501 HasUnitRelations<
decltype(From)> && detail::conversionRatio(From, To).found;
513template <auto From, auto To>
519template <auto From, auto To>
525template <auto From, auto To>
528#if MORPH_QUANTITY_PROVENANCE
536 std::string operation;
539 std::optional<morph::math::Rational> lhs;
542 std::optional<morph::math::Rational> rhs;
545 std::optional<morph::math::Rational> result;
555 std::optional<std::string> name;
558 std::shared_ptr<ASTNode> left;
561 std::shared_ptr<ASTNode> right;
567 ASTNode(
const ASTNode&) =
default;
571 ASTNode& operator=(
const ASTNode&) =
default;
574 ASTNode(ASTNode&&) =
default;
578 ASTNode& operator=(ASTNode&&) =
default;
600 std::vector<std::shared_ptr<ASTNode>> pending;
602 pending.push_back(std::move(left));
605 pending.push_back(std::move(right));
607 while (!pending.empty()) {
608 std::shared_ptr<ASTNode>
const node = std::move(pending.back());
610 if (node.use_count() == 1) {
612 pending.push_back(std::move(node->left));
615 pending.push_back(std::move(node->right));
627 std::shared_ptr<ASTNode> node;
635#if MORPH_QUANTITY_PROVENANCE
636#define MORPH_Q_NODE(quantity) (quantity)._ctx.node
638#define MORPH_Q_BUILD(out, op, lhsValue, rhsValue, resultValue, leftNode, rightNode) \
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); \
653#define MORPH_Q_NODE(quantity) nullptr
655#define MORPH_Q_BUILD(out, op, lhsValue, rhsValue, resultValue, leftNode, rightNode) \
669template <auto U, std::u
int32_t DeclaredDecimals>
673 "declared decimals must be within [0, kMaxDecimalPlaces]");
678#if MORPH_QUANTITY_PROVENANCE
680 detail::Context _ctx;
692 Quantity(std::optional<math::Rational> adopted) :
payload{std::move(adopted)} { recordLeaf(); }
697 template <std::u
int32_t OtherDecimals>
698 requires(OtherDecimals != DeclaredDecimals)
700#if MORPH_QUANTITY_PROVENANCE
706 static constexpr auto unit = U;
725 return std::span<
const UnitAlternative<
decltype(U)>>{detail::kUnitAlternatives<U>};
743 return Quantity{std::move(adopted)};
748 [[nodiscard]]
constexpr bool hasValue() const noexcept {
return payload.has_value(); }
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;
779 [[nodiscard]]
constexpr const std::optional<math::Rational>&
value() const noexcept MORPH_LIFETIMEBOUND {
787 return payload.value_or(fallback);
819 MORPH_Q_BUILD(out,
"retag decimal places",
payload, std::nullopt, out.
payload, MORPH_Q_NODE(*
this),
nullptr);
847 MORPH_Q_BUILD(out,
"round to decimal places",
payload, std::nullopt, out.
payload, MORPH_Q_NODE(*
this),
870#if MORPH_QUANTITY_PROVENANCE
872 auto node = std::make_shared<detail::ASTNode>();
873 node->name = std::move(label);
875 node->current.result =
payload;
876 node->left = _ctx.node;
877 out._ctx.node = std::move(node);
896#if MORPH_QUANTITY_PROVENANCE
900 return {detail::formatOptionalDecimal(
payload)};
920 std::string{
"convert "} + std::string{
unitMeta().id} +
" -> " +
922 payload, std::nullopt, out.
payload, MORPH_Q_NODE(*
this),
nullptr);
930#if MORPH_QUANTITY_PROVENANCE
931 auto node = std::make_shared<detail::ASTNode>();
932 node->current.lhs = payload;
933 _ctx.node = std::move(node);
941struct IsQuantity : std::false_type {};
943template <auto U, std::u
int32_t Decimals>
944struct IsQuantity<
Quantity<U, Decimals>> : std::true_type {};
950inline constexpr bool isQuantity = detail::IsQuantity<std::remove_cvref_t<T>>::value;
954template <auto From, auto To>
957 constexpr auto ratioResult = detail::conversionRatio(From, To);
958 auto scaled = *in.
value() * ratioResult.ratio;
959 scaled.decimalPlaces = in.
value()->decimalPlaces;
972template <auto U, std::u
int32_t DecA, std::u
int32_t DecB>
974 return lhs.payload == rhs.payload;
984template <auto U, std::u
int32_t DecA, std::u
int32_t DecB>
987 throw std::logic_error{
"morph::units::Quantity: relational comparison requires engaged operands"};
1002template <auto U, std::u
int32_t DecA, std::u
int32_t DecB>
1004 std::optional<math::Rational> result =
1008 MORPH_Q_BUILD(out,
"+", lhs.
payload, rhs.
payload, result, MORPH_Q_NODE(lhs), MORPH_Q_NODE(rhs));
1016template <auto U, std::u
int32_t DecA, std::u
int32_t DecB>
1018 std::optional<math::Rational> result =
1022 MORPH_Q_BUILD(out,
"-", lhs.
payload, rhs.
payload, result, MORPH_Q_NODE(lhs), MORPH_Q_NODE(rhs));
1030template <auto A, std::u
int32_t DecA, auto B, std::u
int32_t DecB>
1031 requires(detail::SameEnumDistinct<A, B> &&
requires(Quantity<B, DecB> value) {
static_cast<Quantity<A>
>(value); })
1040template <auto A, std::u
int32_t DecA, auto B, std::u
int32_t DecB>
1041 requires(detail::SameEnumDistinct<A, B> &&
requires(Quantity<B, DecB> value) {
static_cast<Quantity<A>
>(value); })
1049template <auto U, std::u
int32_t Dec>
1051 std::optional<math::Rational> result =
1052 operand.
payload ? std::optional<math::Rational>{-*operand.
payload} : std::nullopt;
1055 MORPH_Q_BUILD(out,
"-", operand.
payload, std::nullopt, result, MORPH_Q_NODE(operand),
nullptr);
1065template <auto A, std::u
int32_t DecA, auto B, std::u
int32_t DecB>
1066 requires requires {
typename std::type_identity_t<Quantity<A * B>>; }
1068 std::optional<math::Rational> result =
1072 MORPH_Q_BUILD(out,
"*", lhs.
payload, rhs.
payload, result, MORPH_Q_NODE(lhs), MORPH_Q_NODE(rhs));
1082template <auto A, std::u
int32_t DecA, auto B, std::u
int32_t DecB>
1083 requires requires {
typename std::type_identity_t<Quantity<A / B>>; }
1085 std::optional<math::Rational> result;
1087 if (
auto quotient = lhs.
payload->dividedBy(*rhs.
payload); quotient.has_value()) {
1093 MORPH_Q_BUILD(out,
"/", lhs.
payload, rhs.
payload, result, MORPH_Q_NODE(lhs), MORPH_Q_NODE(rhs));
1101template <auto U, std::u
int32_t Dec>
1103 std::optional<math::Rational> result =
1104 lhs.
payload ? std::optional<math::Rational>{*lhs.
payload * factor} : std::nullopt;
1107 MORPH_Q_BUILD(out,
"*", lhs.
payload, std::optional<math::Rational>{factor}, result, MORPH_Q_NODE(lhs),
nullptr);
1115template <auto U, std::u
int32_t Dec>
1117 return rhs * factor;
1125template <auto U, std::u
int32_t Dec>
1127 std::optional<math::Rational> result;
1129 if (
auto quotient = lhs.
payload->dividedBy(divisor); quotient.has_value()) {
1135 MORPH_Q_BUILD(out,
"/", lhs.
payload, std::optional<math::Rational>{divisor}, result, MORPH_Q_NODE(lhs),
nullptr);
1147template <std::
size_t N>
1156template <detail::FixedString Name, auto U>
1167 :
Base{
Base{std::move(adopted)}.
named(std::string{Name.view()})} {}
1214template <auto U, std::u
int32_t Dec>
1216 return detail::formatOptionalDecimal(quantity.value());
1245template <auto U, std::u
int32_t Dec>
1247 constexpr auto display =
UnitTraits<
decltype(U)>::meta(U).display;
1253#if MORPH_QUANTITY_PROVENANCE
1261template <auto U, std::u
int32_t Dec>
1262struct std::formatter<morph::units::Quantity<U, Dec>> {
1266 constexpr auto parse(std::format_parse_context& ctx) {
return ctx.begin(); }
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));
1280template <morph::units::detail::FixedString Name, auto U>
1281struct std::formatter<morph::units::NamedQuantity<Name, U>> : std::formatter<morph::units::Quantity<U>> {
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);
1299template <auto U, std::u
int32_t Dec>
1300struct glz::meta<morph::units::Quantity<U, Dec>> {
1305 static constexpr std::string_view name = morph::units::UnitTraits<
decltype(U)>::meta(U).id;
1308namespace glz::detail {
1314template <auto U, std::u
int32_t Dec>
1315struct to_json_schema<morph::units::Quantity<U, Dec>> {
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};
1344template <auto U, std::u
int32_t Dec>
1349 static const std::string kName =
Handle< std::string > format(AnyHandle const &pattern, std::vector< AnyHandle > args)
format: a template with {0}, {1}, … placeholders.
Handle< std::int64_t > count(AnyHandle const &list)
count: a list's length.
Concept: From → To is convertible — a user override takes precedence over an auto-generated ratio pat...
Concept: UnitTraits<E> declares an optional static constexpr QuantityBounds bounds(E) — the pre-decod...
Concept: UnitTraits<E> also declares within-dimension relations (template param E).
Concept: the application supplied a UnitTraits<E>::convert static for this pair (a non-ratio override...
Concept: the auto-generated ratio convert applies to From → To (template params From,...
Concept: an enum with a UnitTraits specialisation (template param E).
@ 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...
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.
constexpr bool isQuantity
true when T (cvref-stripped) is a morph::units::Quantity.
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...
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.
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).
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...
@ HalfAwayFromZero
Ties go to the neighbour of larger magnitude: 1.25 -> 1.3, -1.25 -> -1.3.
constexpr std::uint32_t kMaxDecimalPlaces
Largest decimal precision the type supports: 10^18 is the greatest power of ten that fits in std::int...
A structural, NTTP-capable compile-time string.
Strong type for a decimal-precision count.
std::uint32_t value
Raw digit count. Rational's invariant keeps it in [0, kMaxDecimalPlaces].
Exact rational number with a runtime decimal-precision tag.
DecimalPlaces decimalPlaces
Decimal-precision tag. Invariant: 0 <= value <= kMaxDecimalPlaces.
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.
std::int64_t denominator
Strictly positive denominator. Never zero, never negative.
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...
morph::math::Rational max
Inclusive upper bound.
morph::math::Rational min
Inclusive lower bound.
A unit-tagged, optionally-empty exact value with a declared precision in its type and (optionally) a ...
Quantity(std::optional< math::Rational > adopted)
Adopts an optional payload as-is.
Quantity named(std::string label) const
Seals this value with a name: equation() shows the name instead of expanding its derivation....
constexpr math::Rational value_or(const math::Rational &fallback) const noexcept
The payload if engaged, otherwise a caller-supplied fallback.
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...
static constexpr std::uint32_t declaredDecimals
Quantity atDeclaredPrecision() const
Rounds the value to this field's declared precision — the one the generated schema advertises as x-de...
static Quantity fromOptional(std::optional< math::Rational > adopted)
Wraps an optional payload, preserving the declared precision.
Quantity(const Quantity< U, OtherDecimals > &other)
Converts from the same unit at another declared precision; the value, its runtime tag,...
std::optional< math::Rational > payload
static constexpr math::DecimalPlaces declaredPrecision() noexcept
The declared precision as a DecimalPlaces strong type.
static constexpr UnitMeta unitMeta() noexcept
The unit's static metadata (id, display text, default decimals).
static constexpr std::span< const UnitAlternative< decltype(U)> > unitAlternatives() noexcept
The convertible display/entry units for this field's unit, derived from UnitTraits::relations.
static constexpr auto unit
constexpr const std::optional< math::Rational > & value() const noexcept
The payload, for pattern-matching / -> access.
Quantity withDecimalPlaces(math::DecimalPlaces newPrecision) const
Retags the value's runtime precision (the exact value itself is unchanged — precision only affects ro...
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).
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...
static Quantity fromDouble(double raw) noexcept
Converts a floating-point reading at the declared precision.
constexpr bool hasValue() const noexcept
Whether a value has been entered/measured.
constexpr const math::Rational & operator*() const noexcept
Unchecked access to the engaged value (UB when empty, exactly like std::optional::operator*).
A derived per-unit view of one convertible display/entry unit.
std::int64_t den
Denominator of the exact alternative-to-canonical ratio.
std::int64_t num
Numerator of the exact alternative-to-canonical ratio.
E unit
The alternative (convertible) unit.
One exact within-dimension conversion ratio between two peer units.
E from
Source unit of the ratio.
E to
Target unit of the ratio.
morph::math::Rational fromTo
Exact ratio: 1 from == fromTo · to. Strictly positive.
Customisation point: the application specialises this for its unit enum, returning a UnitMeta per enu...