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 88aaa5da..1879c4e6 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); @@ -345,6 +363,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 5340197b..ffe3191a 100644 --- a/docs/reference/constant.md +++ b/docs/reference/constant.md @@ -533,3 +533,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 +```