Skip to content

Commit

Permalink
Add common_unit and make_common utilities (#362)
Browse files Browse the repository at this point in the history
We also add the "common point" versions of these.

The absence of `common_unit` and `common_point_unit` was mostly just an
oversight.  I recently tried to call one in Godbolt, and got surprised
when I couldn't.  And it turned out that I had already written this
function anyway, except that it was hidden away inside of a test file.

As I thought about these functions more, I thought it was interesting
that the inputs are unit slots, so we could have a wide variety of
things (simple units, quantity makers, unit symbols, etc.), but the
output would always be a simple unit.  Wouldn't it be nice if we could
also combine `meters` and `feet`, or `m` and `ft`, and have the output
act like its inputs?  This idea became the `make_common` and
`make_common_point` utilities.

Docs included (and rendered and tested).

---------

Co-authored-by: Michael Hordijk <[email protected]>
  • Loading branch information
chiphogg and hoffbrinkle authored Dec 18, 2024
1 parent 30e0b27 commit 3074486
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 7 deletions.
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);
EXPECT_THAT(feet(1u) % feeters(1u), Eq(ZERO));
EXPECT_THAT(meters(1u) % feeters(1u), Eq(ZERO));
EXPECT_THAT(detail::gcd(feet(1u).in(feeters), meters(1u).in(feeters)), Eq(1u));

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
105 changes: 104 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,96 @@ 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
// `meters` and `feet` are quantity makers: pass them a number, they make a quantity.
//
// `make_common(meters, feet)` is also a quantity maker, so we can pass it `18`.
constexpr auto x = make_common(meters, feet)(18);

// `m` and `ft` are unit symbols: multiply a number by them to make a quantity.
//
// `make_common(meters, feet)` is also a unit symbol, so we can multiply `9.5f` by it.
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, which is "common for points" (see [background
info](../discussion/concepts/common_unit.md#common-quantity-point)) with respect to all input units.
This means that its magnitude will be the largest-magnitude unit which evenly divides _both_ the
input units _and_ the units for any differences-of-origin. And its origin will be the lowest of all
input origins.

**Syntax:**

- `make_common_point(us...)`

??? example "Example"
```cpp
// `meters_pt` and `feet_pt` are quantity point makers: pass them a number, they make a quantity point.
//
// `make_common_point(meters_pt, feet_pt)` is also a quantity point maker, so we can pass it `10`.
constexpr auto temp = make_common_point(celsius_pt, fahrenheit_pt)(10);
```

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

0 comments on commit 3074486

Please sign in to comment.