Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions au/constant.hh
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,35 @@ constexpr std::strong_ordering operator<=>(Constant<U1>, Constant<U2>) {
}
#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 <typename U1, typename U2>
constexpr auto operator%(Constant<U1>, Constant<U2>) {
// 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<U1, U2>;
using CommonUnscaled = CommonUnit<detail::UnscaledUnit<U1>, detail::UnscaledUnit<U2>>;
return make_constant(CommonUnscaled{}) * (UnitRatio<U1, U>{} % UnitRatio<U2, U>{}) *
UnitRatio<U, CommonUnscaled>{};
}

// Arithmetic operators mixing `Constant` with `Zero`.
template <typename U>
constexpr Zero operator%(Zero, Constant<U>) {
return {};
}

} // namespace au
85 changes: 85 additions & 0 deletions au/constant_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -45,6 +47,22 @@ using ::testing::StrEq;

namespace {

template <typename U>
std::string constant_label_for(Constant<U>) {
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<std::string>(inner_matcher)) {
const std::string label(constant_label_for(arg));
*result_listener << "whose unit label is \"" << label << "\"";
return ::testing::SafeMatcherCast<std::string>(inner_matcher).Matches(label);
}

constexpr auto PI = Magnitude<Pi>{};
constexpr auto m = symbol_for(meters);
constexpr auto s = symbol_for(seconds);
Expand Down Expand Up @@ -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<int>() % five_feet.as<int>()),

// 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<decltype(make_constant(feet * mag<21>()) % 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) {
Expand Down
41 changes: 41 additions & 0 deletions docs/reference/constant.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```