Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add common_unit and make_common utilities #362

Merged
merged 4 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
20 changes: 20 additions & 0 deletions au/code/au/unit_of_measure.hh
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,26 @@ constexpr auto associated_unit_for_points(U) {
return AssociatedUnitForPointsT<U>{};
}

template <typename... Us>
constexpr auto common_unit(Us...) {
return CommonUnitT<AssociatedUnitT<Us>...>{};
}

template <typename... Us>
constexpr auto common_point_unit(Us...) {
return CommonPointUnitT<AssociatedUnitForPointsT<Us>...>{};
}

template <template <class> class Utility, typename... Us>
constexpr auto make_common(Utility<Us>...) {
return Utility<CommonUnitT<AssociatedUnitT<Us>...>>{};
}

template <template <class> class Utility, typename... Us>
constexpr auto make_common_point(Utility<Us>...) {
return Utility<CommonPointUnitT<AssociatedUnitForPointsT<Us>...>>{};
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Unit arithmetic traits: products, powers, and derived operations.

Expand Down
45 changes: 39 additions & 6 deletions au/code/au/unit_of_measure_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,14 @@ using ::testing::StaticAssertTypeEq;
using ::testing::StrEq;

namespace au {
namespace {
template <typename... Us>
constexpr auto common_unit(Us...) {
return CommonUnitT<Us...>{};
}
} // namespace

struct Celsius : Kelvins {
static constexpr auto origin() { return milli(kelvins)(273'150); }
static constexpr const char label[] = "deg_C";
};
constexpr const char Celsius::label[];
constexpr auto celsius = QuantityMaker<Celsius>{};
constexpr auto celsius_pt = QuantityPointMaker<Celsius>{};

struct AlternateCelsius : Kelvins {
static constexpr auto origin() { return micro(kelvins)(273'150'000); }
Expand Down Expand Up @@ -504,6 +499,10 @@ TEST(CommonUnit, CanCombineUnitsThatWouldBothBeAnonymousScaledUnits) {
EXPECT_EQ((feet / mag<3>())(1), (inches * mag<4>())(1));
}

TEST(CommonUnit, SupportsUnitSlots) {
StaticAssertTypeEq<decltype(common_unit(feet, meters)), CommonUnitT<Feet, Meters>>();
}

TEST(CommonPointUnit, FindsCommonMagnitude) {
EXPECT_THAT((CommonPointUnitT<Feet, Feet>{}), PointEquivalentToUnit(Feet{}));
EXPECT_THAT((CommonPointUnitT<Feet, Inches>{}), PointEquivalentToUnit(Inches{}));
Expand Down Expand Up @@ -555,6 +554,40 @@ TEST(CommonPointUnit, UnpacksTypesInNestedCommonUnit) {
StaticAssertTypeEq<Common, CommonPointUnitT<W, X, Y, Z>>();
}

TEST(CommonPointUnit, SupportsUnitSlots) {
StaticAssertTypeEq<decltype(common_point_unit(kelvins_pt, celsius_pt)),
CommonPointUnitT<Kelvins, Celsius>>();
}

TEST(MakeCommon, PreservesCategory) {
constexpr auto feeters = make_common(feet, meters);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feeters

😆

EXPECT_EQ(feet(1u) % feeters(1u), ZERO);
EXPECT_EQ(meters(1u) % feeters(1u), ZERO);
EXPECT_EQ(detail::gcd(feet(1u).in(feeters), meters(1u).in(feeters)), 1u);
chiphogg marked this conversation as resolved.
Show resolved Hide resolved

using symbols::ft;
using symbols::m;
EXPECT_THAT(123 * make_common(m, ft), SameTypeAndValue(feeters(123)));
}

TEST(MakeCommonPoint, PreservesCategory) {
constexpr auto celsenheit_pt = make_common_point(celsius_pt, fahrenheit_pt);

// The origin of the common point unit is the lowest origin among all input units.
EXPECT_EQ(celsenheit_pt(0), fahrenheit_pt(0));
EXPECT_LT(celsenheit_pt(0), celsius_pt(0));

// The common point unit should evenly divide both input units.
//
// (We can't necessarily say that it is the _largest_ such unit, as we could for the common
// unit, because we also have to accomodate the unit for the _difference of the origins_.)
constexpr auto one_f = fahrenheit_pt(1) - fahrenheit_pt(0);
constexpr auto one_c = celsius_pt(1) - celsius_pt(0);
constexpr auto one_ch = celsenheit_pt(1) - celsenheit_pt(0);
EXPECT_EQ(one_f % one_ch, ZERO);
EXPECT_EQ(one_c % one_ch, ZERO);
}

TEST(UnitLabel, DefaultsToUnlabeledUnit) {
EXPECT_THAT(unit_label<UnlabeledUnit>(), StrEq("[UNLABELED UNIT]"));
EXPECT_EQ(sizeof(unit_label<UnlabeledUnit>()), 17);
Expand Down
94 changes: 93 additions & 1 deletion docs/reference/unit.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ _instance_ `u` and magnitude instance `m`, this operation:

- `u * m`

## Traits
## Traits {#traits}

Because units are [monovalue types](./detail/monovalue_types.md), each trait has two forms: one for
_types_, and another for _instances_.
Expand Down Expand Up @@ -575,10 +575,26 @@ no uniquely defined answer, but the program should still produce _some_ answer.
the result is associative, and symmetric under any reordering of the input units. The specific
implementation choice will be driven by convenience and simplicity.

??? note "A note on inputs vs. outputs for the `common_unit(us...)` form"
The return value of the instance version is a _unit_, while the input parameters are
[unit _slots_](../discussion/idioms/unit-slots.md). This means that the return value will often
be a different _category of thing_ (i.e., always consistently a unit) than the inputs (which may
be quantity makers, unit symbols, or so on).

For example, consider `common_unit(meters, feet)`. Recall that the type of `meters` is
`QuantityMaker<Meters>`, and that of `feet` is `QuantityMaker<Feet>`. In this case, the return
value is an instance of `CommonUnitT<Meters, Feet>`, _not_
`QuantityMaker<CommonUnitT<Meters, Feet>>`.

If you want something that still computes the common unit, but preserves the _category_ of the
inputs, see [`make_common(us...)`](#make-common).

**Syntax:**

- For _types_ `Us...`:
- `CommonUnitT<Us...>`
- For _instances_ `us...`:
- `common_unit(us...)`

### Common point unit

Expand All @@ -603,9 +619,85 @@ A specialization will only exist if the inputs are all units, and will exist but
error if any two input units have different Dimensions. We also strive to keep the result
associative, and symmetric under interchange of any inputs.

??? note "A note on inputs vs. outputs for the `common_point_unit(us...)` form"
The return value of the instance version is a _unit_, while the input parameters are
[unit _slots_](../discussion/idioms/unit-slots.md). This means that the return value will often
be a different _category of thing_ (i.e., always consistently a unit) than the inputs (which may
be quantity point makers, unit symbols, or so on).

For example, consider `common_unit(meters, feet)`. Recall that the type of `meters` is
`QuantityMaker<Meters>`, and that of `feet` is `QuantityMaker<Feet>`. In this case, the return
value is an instance of `CommonUnitT<Meters, Feet>`, _not_
`QuantityMaker<CommonUnitT<Meters, Feet>>`.

If you want something that still computes the common unit, but preserves the _category_ of the
inputs, see [`make_common_point(us...)`](#make-common-point).

**Syntax:**

- For _types_ `Us...`:
- `CommonPointUnitT<Us...>`
- For _instances_ `us...`:
- `common_point_unit(us...)`

## Category-preserving unit slot operations

A [unit slot](../discussion/idioms/unit-slots.md) API can take a variety of "categories" of input.
Prominent examples include:

- Simple unit types (`Meters{}`, ...)
- Quantity makers (`meters`, ...)
- Unit symbols (`symbols::m`, ...)
- Constants (`SPEED_OF_LIGHT`, ...)

The [previous section](#traits) demonstrated various traits that can be applied to units. Some of
these traits (such as `common_unit(...)`) produce a new unit as their output type. This will always
be a _simple_ unit, even though the inputs are unit _slots_: that is, these traits change the
_category_ of the output.

This section describes a few special operations that _preserve_ that category. So for example,
suppose we had an operation `op` of this type. Let's call the result of `op(Meters{}, Seconds{})`
as `U{}`. Then we have:

- `op(meters, seconds)` produces `QuantityMaker<U>`, because its inputs are `QuantityMaker<Meters>`
and `QuantityMaker<Seconds>`.
- `op(m, s)` produces `UnitSymbol<U>`, because its inputs are `UnitSymbol<Meters>` and
`UnitSymbol<Seconds>`.
- ... and so on.

Here are the category-preserving operations we provide.

### Making common units {#make-common}

**Result:** A new unit: the largest unit that evenly divides its input units. (Read more about the
concept of [common units](../discussion/concepts/common_unit.md).)

**Syntax:**

- `make_common(us...)`

??? example "Examples"
```cpp
constexpr auto x = make_common(meters, feet)(18);
geoffviola marked this conversation as resolved.
Show resolved Hide resolved

using symbols::m;
using symbols::ft;
constexpr auto y = 9.5f * make_common(m, ft);
```

### Making common point units {#make-common-point}

**Result:** A new unit: the largest-magnitude, highest-origin unit which is "common" to the input
geoffviola marked this conversation as resolved.
Show resolved Hide resolved
units. (Read more about the concept of [common units for
`QuantityPoint`](../discussion/concepts/common_unit.md#common-quantity-point).)

**Syntax:**

- `make_common_point(us...)`

??? example "Example"
```cpp
constexpr auto temp = make_common_point(celsius_pt, fahrenheit_pt)(10);
```

<script src="../assets/hrh4.js" async=false defer=false></script>
Loading