From e60f6e06ba70128b1e7013b6e227d544bc2f0531 Mon Sep 17 00:00:00 2001 From: Chip Hogg Date: Sat, 24 Jan 2026 14:43:38 -0500 Subject: [PATCH] Support `%` for `Constant` inputs Getting a correct answer was easy, thanks to the underlying support for `%` in `Magnitude`. Just as with `Quantity`, we simply convert the inputs to their common unit, and then perform the usual `%` operation on those (necessarily integer) inputs. The label was unexpectedly subtle. I wrote the tests in terms of my human a priori expectations, which say, for example, that 40 inches mod 2 feet should get a label of `"[16 in]"`. Instead, the answer I got was `"[2 EQUIV{[(2 / 3) ft], [8 in]}]"`! Now, clearly this is equivalent, so the program was not "wrong"; the label was just strange and inconvenient. Ultimately, I realized that human readers will be looking at the _unscaled version_ of the units associated with each constant. So we should start by finding the common unit of the _unscaled_ input units. We still perform the modulus in the common unit of the _direct_ inputs, as we must. But then we make the answer right by multiplying by the ratio _between these different common units_. This way, the common _unscaled_ unit is what determines the label, and everything else is just a simple multiplicative `Magnitude` applied to it. Includes doc updates. Helps #607. --- au/constant.hh | 31 ++++++++++++++ au/constant_test.cc | 85 ++++++++++++++++++++++++++++++++++++++ docs/reference/constant.md | 41 ++++++++++++++++++ 3 files changed, 157 insertions(+) diff --git a/au/constant.hh b/au/constant.hh index 74f7d2db..efe6e9ea 100644 --- a/au/constant.hh +++ b/au/constant.hh @@ -180,4 +180,35 @@ constexpr std::strong_ordering operator<=>(Constant, Constant) { } #endif +// Arithmetic operators. +// +// Note that these inherit the limitations of the Magnitude comparisons: they will not work for +// every combination of Constant. Again, we decided that supporting many common use cases was worth +// this tradeoff. + +// Mod (%) for `Constant`. +template +constexpr auto operator%(Constant, Constant) { + // This slightly complicated dance tends to produce more intuitive, human-friendly labels. + // + // The basic idea for `%` with `Constant` is to perform the operation in the constants' common + // unit. But those constants' units may be scaled, and the scale factor is _part of the unit_ + // as far as the _library_ is concerned. Human readers, on the other hand, tend to look at the + // _unscaled_ unit. + // + // To bridge this gap, we make the actual constant (which determines the label) from the common + // unit among all _unscaled_ input units. Everything else is applied as a multiplicative + // magnitude against this. + using U = CommonUnit; + using CommonUnscaled = CommonUnit, detail::UnscaledUnit>; + return make_constant(CommonUnscaled{}) * (UnitRatio{} % UnitRatio{}) * + UnitRatio{}; +} + +// Arithmetic operators mixing `Constant` with `Zero`. +template +constexpr Zero operator%(Zero, Constant) { + return {}; +} + } // namespace au diff --git a/au/constant_test.cc b/au/constant_test.cc index da1ee89e..2fce943f 100644 --- a/au/constant_test.cc +++ b/au/constant_test.cc @@ -19,6 +19,8 @@ #include "au/chrono_interop.hh" #include "au/testing.hh" #include "au/units/degrees.hh" +#include "au/units/feet.hh" +#include "au/units/inches.hh" #include "au/units/joules.hh" #include "au/units/meters.hh" #include "au/units/newtons.hh" @@ -45,6 +47,22 @@ using ::testing::StrEq; namespace { +template +std::string constant_label_for(Constant) { + return unit_label(U{}); +} + +// Matcher for checking the unit label of a Constant. +// Usage: EXPECT_THAT(my_constant, ConstantLabelIs(StrEq("[21 in]"))); +MATCHER_P(ConstantLabelIs, + inner_matcher, + "is a Constant whose unit label " + + ::testing::DescribeMatcher(inner_matcher)) { + const std::string label(constant_label_for(arg)); + *result_listener << "whose unit label is \"" << label << "\""; + return ::testing::SafeMatcherCast(inner_matcher).Matches(label); +} + constexpr auto PI = Magnitude{}; constexpr auto m = symbol_for(meters); constexpr auto s = symbol_for(seconds); @@ -335,6 +353,73 @@ TEST(Constant, SupportsModWithQuantity) { EXPECT_THAT(degrees(300) % half_rev, SameTypeAndValue(degrees(120))); } +TEST(Constant, ModResultIsIntegerMultipleOfCommonUnitWhenRatioIsExactInteger) { + // 5 feet = 60 inches; 60 % 7 = 4 + constexpr auto five_feet = make_constant(feet * mag<5>()); + constexpr auto seven_inches = make_constant(inches * mag<7>()); + constexpr auto result = five_feet % seven_inches; + + EXPECT_THAT(result, AllOf(Eq(inches(4)), ConstantLabelIs(StrEq("[4 in]")))); +} + +TEST(Constant, ModResultIsIntegerMultipleOfCommonUnitWhenRatioIsExactInverseInteger) { + // 57 inches % 3 feet = 57 inches % 36 inches = 21 inches + constexpr auto fifty_seven_inches = make_constant(inches * mag<57>()); + constexpr auto three_feet = make_constant(feet * mag<3>()); + constexpr auto result = fifty_seven_inches % three_feet; + + EXPECT_THAT(result, AllOf(Eq(inches(21)), ConstantLabelIs(StrEq("[21 in]")))); +} + +TEST(Constant, ModResultIsIntegerMultipleOfCommonUnitWhenRatioIsRational) { + // The common unit of feet and meters is (1/1250) meters = (1/381) feet. + // + // 3 meters = 3750 common units; 5 feet = 1905 common units. + // + // 3750 % 1905 = 1845 common units + constexpr auto three_meters = make_constant(meters * mag<3>()); + constexpr auto five_feet = make_constant(feet * mag<5>()); + constexpr auto result = three_meters % five_feet; + + EXPECT_THAT(result, + AllOf( + + // Use well-tested `Quantity` results, with exact integer math, to be confident + // that `Constant` produces the correct quantity. + Eq(three_meters.as() % five_feet.as()), + + // We don't want to depend on which order the EQUIV label shows the units. + ConstantLabelIs(AnyOf(StrEq("[1845 EQUIV{[(1 / 1250) m], [(1 / 381) ft]}]"), + StrEq("[1845 EQUIV{[(1 / 381) ft], [(1 / 1250) m]}]"))))); +} + +TEST(Constant, ModWithFractionalScaledUnits) { + // Denominators 7 and 5 are coprime with each other, and with all factors of 12, so the common + // unit must be (1/35) inches. + // + // Input values are: + // (12/7) * (12 * 35 units) = 720 units + // (8/5) * ( 1 * 35 units) = 56 units + // + // Overall, 720 % 56 = 48, and "units" is most economically expressed as (1/35) inches. + constexpr auto twelve_sevenths_feet = make_constant(feet * mag<12>() / mag<7>()); + constexpr auto eight_fifths_inches = make_constant(inches * mag<8>() / mag<5>()); + constexpr auto result = twelve_sevenths_feet % eight_fifths_inches; + + EXPECT_THAT(result, + AllOf(Eq((inches / mag<35>())(48)), ConstantLabelIs(StrEq("[(48 / 35) in]")))); +} + +TEST(Constant, ModReturnsZeroWhenEvenlyDivisible) { + StaticAssertTypeEq()) % make_constant(inches * mag<7>())), + Zero>(); +} + +TEST(Constant, ModWithZeroDividendReturnsZero) { + constexpr auto result = make_constant(ZERO) % make_constant(feet); + EXPECT_THAT(result, Eq(ZERO)); +} + TEST(MakeConstant, IdentityForZero) { EXPECT_THAT(make_constant(ZERO), SameTypeAndValue(ZERO)); } TEST(CanStoreValueIn, ChecksRangeOfTypeForIntegers) { diff --git a/docs/reference/constant.md b/docs/reference/constant.md index f152f1be..3a8842fd 100644 --- a/docs/reference/constant.md +++ b/docs/reference/constant.md @@ -510,3 +510,44 @@ because quantity points do not support multiplication. [0.6.0]: https://github.com/aurora-opensource/au/milestone/9 [#481]: https://github.com/aurora-opensource/au/issues/481 + +### Modulo † + +The modulo operator computes the remainder after dividing one `Constant` by another, returning +another `Constant` (or `Zero`). The operands must have the same dimension. + +The result is well defined, independently of the units of the inputs --- that is, it is a sensible +"pure quantity" operation. For positive inputs, we can define it as the result of continually +subtracting the second argument from the first, until what remains is less than the second argument. +(This is not how we actually _implement_ it, but it is useful for understanding the semantics.) If +either or both of the inputs is signed, the result follows the same sign rules as the built-in C++ +`%` operator, since this is a C++ library. + +This operator interacts with `Zero` in several ways. + +- When the second argument exactly divides the first (no remainder), the result is `Zero`. +- `Zero` may be provided as the first argument, in which case the result is always `Zero`. +- `Zero` may **not** be provided as the second argument, because division by zero is undefined. + +† _This feature is subject to the same [compile-time arithmetic +limitations](./magnitude.md#compile-time-arithmetic-limitations) as `Magnitude` modulo, because the +computation is built on `Magnitude` modulo._ + +**Syntax:** + +For `Constant` instances `c1` and `c2` with the same dimension: + +- `c1 % c2` + +**Result:** A new `Constant` representing the remainder. The result's unit is based on the common +unit of the inputs, which generally produces human-friendly labels. + +**Example:** + +```cpp +constexpr auto five_feet = make_constant(feet * mag<5>()); +constexpr auto seven_inches = make_constant(inches * mag<7>()); + +// 5 feet = 60 inches; 60 % 7 = 4 +constexpr auto result = five_feet % seven_inches; // Constant representing 4 inches +```