Skip to content

Commit

Permalink
Make most unit traits compatible with unit slots (#168)
Browse files Browse the repository at this point in the history
The motivating use case was `unit_ratio`.  I was working with an ad hoc
type for CPU frequency which _only_ had a quantity maker.  See the
example from the issue:

```cpp
constexpr auto cpu_ticks = hertz * mag<400'000'000>();
```

Right now, there's no good way to get the unit ratio between this and
`Nano<Seconds>`.  Enabling `unit_ratio(cpu_ticks, nano(seconds))` seems
like a slam dunk.

There are of course many other unit traits.  I decided to update them
all while I was in there.  `is_unit` felt like it should be an
exception, so I made it one, and explained why in detail in the docs.

I also added test cases to make sure we have the desired behaviour.  In
doing so, I replaced our ad hoc definitions with the direct unit
definitions.  Making a length system based on `Feet` was kind of cute,
but we don't need it.  This also brings in the quantity makers such as
`feet`, which was the real motivation for this change.  I kept `Celsius`
as-is because several tests depend on its exact choice of type for
origin, and they're easier to understand if the definition is in the
same file.

Fixes #167.
  • Loading branch information
chiphogg authored Aug 22, 2023
1 parent 02410d9 commit 28a72f8
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 43 deletions.
1 change: 1 addition & 0 deletions au/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ cc_test(
":prefix",
":testing",
":unit_of_measure",
":units",
"@com_google_googletest//:gtest_main",
],
)
Expand Down
22 changes: 14 additions & 8 deletions au/unit_of_measure.hh
Original file line number Diff line number Diff line change
Expand Up @@ -195,47 +195,53 @@ constexpr bool is_unit(T) {
return IsUnit<T>::value;
}

// Check whether two objects are Unit types of the same Dimension.
// `fits_in_unit_slot(T)`: check whether this value is valid for a unit slot.
template <typename T>
constexpr bool fits_in_unit_slot(T) {
return IsUnit<AssociatedUnitT<T>>::value;
}

// Check whether the units associated with these objects have the same Dimension.
template <typename... Us>
constexpr bool has_same_dimension(Us...) {
return HasSameDimension<Us...>::value;
return HasSameDimension<AssociatedUnitT<Us>...>::value;
}

// Check whether two Unit types are exactly quantity-equivalent.
template <typename U1, typename U2>
constexpr bool are_units_quantity_equivalent(U1, U2) {
return AreUnitsQuantityEquivalent<U1, U2>::value;
return AreUnitsQuantityEquivalent<AssociatedUnitT<U1>, AssociatedUnitT<U2>>::value;
}

// Check whether two Unit types are exactly point-equivalent.
template <typename U1, typename U2>
constexpr bool are_units_point_equivalent(U1, U2) {
return AreUnitsPointEquivalent<U1, U2>::value;
return AreUnitsPointEquivalent<AssociatedUnitT<U1>, AssociatedUnitT<U2>>::value;
}

// Check whether this value is an instance of a dimensionless Unit.
template <typename U>
constexpr bool is_dimensionless(U) {
return IsDimensionless<U>::value;
return IsDimensionless<AssociatedUnitT<U>>::value;
}

// Type trait to detect whether a Unit is "the unitless unit".
template <typename U>
constexpr bool is_unitless_unit(U) {
return IsUnitlessUnit<U>::value;
return IsUnitlessUnit<AssociatedUnitT<U>>::value;
}

// A Magnitude representing the ratio of two same-dimensioned units.
//
// Useful in doing unit conversions.
template <typename U1, typename U2>
constexpr UnitRatioT<U1, U2> unit_ratio(U1, U2) {
constexpr UnitRatioT<AssociatedUnitT<U1>, AssociatedUnitT<U2>> unit_ratio(U1, U2) {
return {};
}

template <typename U1, typename U2>
constexpr auto origin_displacement(U1, U2) {
return OriginDisplacement<U1, U2>::value();
return OriginDisplacement<AssociatedUnitT<U1>, AssociatedUnitT<U2>>::value();
}

template <typename U>
Expand Down
99 changes: 65 additions & 34 deletions au/unit_of_measure_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@

#include "au/prefix.hh"
#include "au/testing.hh"
#include "au/units/fahrenheit.hh"
#include "au/units/feet.hh"
#include "au/units/inches.hh"
#include "au/units/kelvins.hh"
#include "au/units/meters.hh"
#include "au/units/minutes.hh"
#include "au/units/yards.hh"
#include "au/utility/type_traits.hh"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
Expand All @@ -30,47 +37,15 @@ using ::testing::StrEq;

namespace au {

struct Feet : UnitImpl<Length> {
static constexpr const char label[] = "ft";
};
constexpr const char Feet::label[];

struct Yards : decltype(Feet{} * mag<3>()) {
static constexpr const char label[] = "yd";
};
constexpr const char Yards::label[];

struct Inches : decltype(Feet{} / mag<12>()) {
static constexpr const char label[] = "in";
};
constexpr const char Inches::label[];

struct Meters : decltype(Inches{} * (mag<100>() / mag<254>() * mag<100>())) {
static constexpr const char label[] = "m";
};
constexpr const char Meters::label[];

struct Minutes : UnitImpl<Time> {
static constexpr const char label[] = "min";
};
constexpr const char Minutes::label[];

struct Kelvins : UnitImpl<Temperature> {};
constexpr auto kelvins = QuantityMaker<Kelvins>{};

struct Celsius : Kelvins {
static constexpr auto origin() { return milli(kelvins)(273'150); }
};
constexpr auto celsius = QuantityMaker<Celsius>{};

struct AlternateCelsius : Kelvins {
static constexpr auto origin() { return micro(kelvins)(273'150'000); }
};

constexpr auto F_PER_C = mag<5>() / mag<9>();
struct Fahrenheit : decltype(Kelvins{} * F_PER_C) {
static constexpr auto origin() { return milli(kelvins * F_PER_C)(459'670); }
};

struct One : UnitImpl<Dimension<>> {};

// Test the ability to create labels non-intrusively, via a trait.
Expand Down Expand Up @@ -145,6 +120,15 @@ TEST(IsUnit, FalseIfDimOrMagHasWrongType) {
EXPECT_FALSE(is_unit(InvalidWrongMagType{}));
}

TEST(IsUnit, FunctionalFormFalseForQuantityMaker) { EXPECT_FALSE(is_unit(meters)); }

TEST(FitsInUnitSlot, TrueForUnitAndQuantityMaker) {
EXPECT_TRUE(fits_in_unit_slot(meters));
EXPECT_TRUE(fits_in_unit_slot(Meters{}));

EXPECT_FALSE(fits_in_unit_slot(1.2));
}

TEST(Product, IsUnitWithProductOfMagnitudesAndDimensions) {
constexpr auto foot_yards = Feet{} * Yards{};
EXPECT_TRUE(is_unit(foot_yards));
Expand Down Expand Up @@ -227,6 +211,13 @@ TEST(AssociatedUnitT, FunctionalInterfaceHandlesInstancesCorrectly) {
decltype(Feet{} / Minutes{})>();
}

TEST(AssociatedUnitT, IsIdentityForTypeWithNoAssociatedUnit) {
// We might have returned `void`, but this would require depending on `IsUnit`, which could slow
// down `AssociatedUnitT` because it's used so widely. It's simpler to think of it as a trait
// which "redirects" a type only when there is a definite, positive reason to do so.
StaticAssertTypeEq<AssociatedUnitT<double>, double>();
}

TEST(UnitInverseT, CommutesWithProduct) {
StaticAssertTypeEq<UnitInverseT<UnitProductT<Feet, Minutes>>,
UnitProductT<UnitInverseT<Feet>, UnitInverseT<Minutes>>>();
Expand Down Expand Up @@ -275,11 +266,21 @@ TEST(IsDimensionless, FunctionalInterfaceHandlesInstancesCorrectly) {
EXPECT_TRUE(is_dimensionless(AdHocSpeedUnit{} / Feet{} * Minutes{}));
}

TEST(IsDimensionless, FunctionalInterfaceHandlesQuantityMakersCorrectly) {
EXPECT_FALSE(is_dimensionless(feet));
EXPECT_TRUE(is_dimensionless(inches / yards));
}

TEST(IsUnitlessUnit, PicksOutUnitlessUnit) {
EXPECT_FALSE(is_unitless_unit(Inches{} / Yards{}));
EXPECT_TRUE(is_unitless_unit(Inches{} / Inches{}));
}

TEST(IsUnitlessUnit, FunctionalInterfaceHandlesQuantityMakersCorrectly) {
EXPECT_FALSE(is_unitless_unit(inches / yards));
EXPECT_TRUE(is_unitless_unit(inches / inches));
}

TEST(HasSameDimension, TrueForAnySingleDimension) {
EXPECT_TRUE(HasSameDimension<Feet>::value);
EXPECT_TRUE(HasSameDimension<Minutes>::value);
Expand All @@ -302,6 +303,14 @@ TEST(HasSameDimension, FunctionalInterfaceHandlesInstancesCorrectly) {
EXPECT_FALSE(has_same_dimension(Feet{}, Yards{}, Inches{}, Minutes{}));
}

TEST(HasSameDimension, FunctionalInterfaceHandlesQuantityMakersCorrectly) {
EXPECT_TRUE(has_same_dimension(feet, feet));
EXPECT_TRUE(has_same_dimension(feet, yards));

EXPECT_TRUE(has_same_dimension(feet, yards, inches, yards));
EXPECT_FALSE(has_same_dimension(feet, yards, inches, minutes));
}

TEST(UnitRatio, ComputesRatioForSameDimensionedUnits) {
StaticAssertTypeEq<UnitRatioT<Yards, Inches>, decltype(mag<36>())>();
StaticAssertTypeEq<UnitRatioT<Inches, Inches>, decltype(mag<1>())>();
Expand All @@ -314,6 +323,10 @@ TEST(UnitRatio, FunctionalInterfaceHandlesInstancesCorrectly) {
EXPECT_EQ(unit_ratio(Inches{}, Yards{}), mag<1>() / mag<36>());
}

TEST(UnitRatio, FunctionalInterfaceHandlesQuantityMakersCorrectly) {
EXPECT_EQ(unit_ratio(yards, inches), mag<36>());
}

TEST(AreUnitsQuantityEquivalent, UnitIsEquivalentToItself) {
EXPECT_TRUE((AreUnitsQuantityEquivalent<Feet, Feet>::value));
EXPECT_TRUE((AreUnitsQuantityEquivalent<Minutes, Minutes>::value));
Expand All @@ -331,6 +344,14 @@ TEST(AreUnitsQuantityEquivalent, DifferentDimensionedUnitsAreNotEquivalent) {
EXPECT_FALSE((AreUnitsQuantityEquivalent<Feet, Minutes>::value));
}

TEST(AreUnitsQuantityEquivalent, FunctionalInterfaceHandlesInstancesCorrectly) {
EXPECT_FALSE(are_units_quantity_equivalent(Feet{}, Minutes{}));
}

TEST(AreUnitsQuantityEquivalent, FunctionalInterfaceHandlesQuantityMakersCorrectly) {
EXPECT_FALSE(are_units_quantity_equivalent(feet, minutes));
}

TEST(AreUnitsPointEquivalent, UnitIsEquivalentToItself) {
EXPECT_TRUE((AreUnitsPointEquivalent<Feet, Feet>::value));
EXPECT_TRUE((AreUnitsPointEquivalent<Celsius, Celsius>::value));
Expand All @@ -353,6 +374,11 @@ TEST(AreUnitsPointEquivalent, UnitsWithDifferentOriginsAreNotPointEquivalent) {
EXPECT_FALSE(are_units_point_equivalent(Celsius{}, Kelvins{}));
}

TEST(AreUnitsPointEquivalent, FunctionalInterfaceHandlesQuantityMakersCorrectly) {
ASSERT_TRUE(are_units_quantity_equivalent(celsius, kelvins));
EXPECT_FALSE(are_units_point_equivalent(celsius, kelvins));
}

TEST(AreUnitsPointEquivalent, DifferentUnitsWithDifferentButEquivalentOriginsArePointEquivalent) {
// The origins of these units represent the same conceptual Quantity, although they are
// represented in quantity-inequivalent units.
Expand All @@ -377,6 +403,11 @@ TEST(OriginDisplacement, GivesDisplacementFromFirstToSecond) {
EXPECT_EQ(origin_displacement(Celsius{}, Kelvins{}), milli(kelvins)(-273'150));
}

TEST(OriginDisplacement, FunctionalInterfaceHandlesInstancesCorrectly) {
EXPECT_EQ(origin_displacement(kelvins, celsius), milli(kelvins)(273'150));
EXPECT_EQ(origin_displacement(celsius, kelvins), milli(kelvins)(-273'150));
}

struct OffsetCelsius : Celsius {
static constexpr auto origin() { return detail::OriginOf<Celsius>::value() + kelvins(10); }
};
Expand Down Expand Up @@ -406,7 +437,7 @@ TEST(CommonUnit, PrefersUnitFromListIfAnyIdentical) {

TEST(CommonUnit, DownranksAnonymousScaledUnits) {
StaticAssertTypeEq<CommonUnitT<Feet, decltype(Feet{} * mag<1>())>, Feet>();
StaticAssertTypeEq<CommonUnitT<Feet, UnitImpl<Length>>, Feet>();
StaticAssertTypeEq<CommonUnitT<Meters, UnitImpl<Length>>, Meters>();

using OpaqueFeetSquared = decltype(pow<2>(Feet{}) * ONE);
using OpaqueFeet = UnitProductT<OpaqueFeetSquared, UnitInverseT<Feet>>;
Expand Down
50 changes: 49 additions & 1 deletion docs/reference/unit.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,24 @@ _instance_ `u` and magnitude instance `m`, this operation:

## Traits

Because units are [monovalue types](./detail/monovalue_types.md), each trait has two forms: one for
_types_, and another for _instances_.

Additionally, the parameters in the _instance_ forms will usually act as [_unit
slots_](../discussion/idioms/unit-slots.md). This means you can, for example, write
`unit_ratio(feet, meters)`, which can be convenient.

!!! warning
The only unit trait whose parameters are _not_ unit slots is `is_unit(u)`. This is because of
its name. It will return `true` _only_ if you pass a unit type: passing the unit instance
`Meters{}` returns `true`, but _passing the quantity maker `meters` returns `false`_.

If you want to check whether your instance is compatible with a [unit
slot](../discussion/idioms/unit-slots.md), use `fits_in_unit_slot(u)`.

Sections describing `bool` traits will be indicated with a trailing question mark, `"?"`.

### Is unit?
### Is unit? {#is-unit}

**Result:** Indicates whether the argument is a valid unit.

Expand All @@ -217,6 +232,39 @@ Sections describing `bool` traits will be indicated with a trailing question mar
- For _instance_ `u`:
- `is_unit(u)`

!!! warning
This will only return true if `u` is an instance of a unit type, such as `Meters{}`. It will
return `false` for a quantity maker such as `meters`.

This is what you want if you are trying to figure out whether the type of your instance would be
suitable as the first template parameter for `Quantity` or `QuantityPoint`.

If you are trying to figure out whether your instance `u` is suitable for a [unit
slot](../discussion/idioms/unit-slots.md), call [`fits_in_unit_slot(u)`](#fits-in-unit-slot)
instead.

### Fits in unit slot? {#fits-in-unit-slot}

**Result:** Indicates whether the argument can be validly passed to a [unit
slot](../discussion/idioms/unit-slots.md) in an API.

This trait is _instance-only_: there is no reason to apply this to types, so we do not provide
a type-based API.

**Syntax:**

- For _instance_ `u`:
- `fits_in_unit_slot(u)`

!!! warning
This can return true even if `u` is _not_ an instance of a unit type. For example,
`fits_in_unit_slot(meters)` returns true, even though the type of `meters` is
`QuantityMaker<Meters>`, and thus, not a unit.

If you want to stringently check whether `u` is a _unit_ --- say, to determine whether its type
is suitable as the first template parameter of `Quantity` --- then call [`is_unit(u)`](#is-unit)
instead.

### Has same dimension?

**Result:** Indicates whether two units have the same dimension.
Expand Down

0 comments on commit 28a72f8

Please sign in to comment.