Skip to content

Commit

Permalink
Gather factors and add labels for scaled units (#332)
Browse files Browse the repository at this point in the history
First, when multiplying a `ScaledUnit` by another magnitude, we now fold
it into the existing magnitude of the scaled unit.  Previously, we'd end
up with `ScaledUnit<ScaledUnit<U, M1>, M2>`, and so on.  We also now
omit any "trivial" scaling factors, whether because we're scaling by
`mag<1>()`, or (more commonly) whether we've applied a bunch of
different scale factors and they all cancel out.

(We did need to tweak a few cases that were relying on `U{} * mag<1>()`
being meaningfully different from `U{}`.)

Next, we now auto-generate labels for `ScaledUnit` specializations.  For
`ScaledUnit<U, M>`, if `U` has label `"U"`, and `M` has label `"M"`, we
generate a label `"[M U]"` --- or, if `"M"` contains an exposed slash
`"/"`, we'll generate the label `"[(M) U]"` for lack of ambiguity.  This
resolves the vast majority of `[UNLABELED_UNIT]` labels.  The remaining
work on #85 is simply to generate labels for a wider variety of
magnitude label categories.

Finally: we formerly had no way to decide ordering between units that
are _both_ specializations of `ScaledUnit`, which _do_ have identical
dimension _and_ magnitude, and yet are _not_ the same unit.  (For
example, something like `"[(1 / 4) ft]"` and `"[3 in]"`.)  This may have
been somewhat obscure in the past, but with the upcoming work on #105,
it's about to become very common.  We added a test case that exposes
this, and then updated our ordering code to handle this case.

Helps #85.  Unblocks #105.
  • Loading branch information
chiphogg authored Dec 2, 2024
1 parent 28a7d8c commit e88baac
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 20 deletions.
6 changes: 3 additions & 3 deletions au/code/au/quantity_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -916,10 +916,10 @@ TEST(IsConversionLossy, CorrectlyDiscriminatesBetweenLossyAndLosslessConversions

TEST(AreQuantityTypesEquivalent, RequiresSameRepAndEquivalentUnits) {
using IntQFeet = decltype(feet(1));
using IntQFeetTimesOne = decltype((feet * ONE)(1));
using IntQTwelveInches = decltype((inches * mag<12>())(1));

ASSERT_FALSE((std::is_same<IntQFeet, IntQFeetTimesOne>::value));
EXPECT_TRUE((AreQuantityTypesEquivalent<IntQFeet, IntQFeetTimesOne>::value));
ASSERT_FALSE((std::is_same<IntQFeet, IntQTwelveInches>::value));
EXPECT_TRUE((AreQuantityTypesEquivalent<IntQFeet, IntQTwelveInches>::value));
}

TEST(UnblockIntDiv, EnablesTruncatingIntegerDivisionIntoQuantity) {
Expand Down
57 changes: 50 additions & 7 deletions au/code/au/unit_of_measure.hh
Original file line number Diff line number Diff line change
Expand Up @@ -274,17 +274,29 @@ constexpr auto associated_unit_for_points(U) {
// one for the anonymous scaled unit (e.g., `Inches * mag<12>()`). We explicitly assume that this
// will not cause any performance problems, because these should all be empty classes anyway. If we
// find out we're mistaken, we'll need to revisit this idea.
template <typename Unit, typename ScaleFactor>
struct ScaledUnit;

template <typename Unit, typename ScaleFactor>
struct ComputeScaledUnitImpl : stdx::type_identity<ScaledUnit<Unit, ScaleFactor>> {};
template <typename Unit, typename ScaleFactor>
using ComputeScaledUnit = typename ComputeScaledUnitImpl<Unit, ScaleFactor>::type;
template <typename Unit, typename ScaleFactor, typename OldScaleFactor>
struct ComputeScaledUnitImpl<ScaledUnit<Unit, OldScaleFactor>, ScaleFactor>
: ComputeScaledUnitImpl<Unit, MagProductT<OldScaleFactor, ScaleFactor>> {};
template <typename Unit>
struct ComputeScaledUnitImpl<Unit, Magnitude<>> : stdx::type_identity<Unit> {};
// Disambiguating specialization:
template <typename Unit, typename OldScaleFactor>
struct ComputeScaledUnitImpl<ScaledUnit<Unit, OldScaleFactor>, Magnitude<>>
: stdx::type_identity<ScaledUnit<Unit, OldScaleFactor>> {};

template <typename Unit, typename ScaleFactor>
struct ScaledUnit : Unit {
static_assert(IsValidPack<Magnitude, ScaleFactor>::value,
"Can only scale by a Magnitude<...> type");
using Dim = detail::DimT<Unit>;
using Mag = MagProductT<detail::MagT<Unit>, ScaleFactor>;

// We must ensure we don't give this unit the same label as the unscaled version!
//
// Later on, we could try generating a new label by "pretty printing" the scale factor.
static constexpr auto &label = DefaultUnitLabel<void>::value;
};

// Type template to hold the product of powers of Units.
Expand Down Expand Up @@ -321,13 +333,13 @@ using UnitQuotientT = UnitProductT<U1, UnitInverseT<U2>>;

// Scale this Unit by multiplying by a Magnitude.
template <typename U, typename = std::enable_if_t<IsUnit<U>::value>, typename... BPs>
constexpr ScaledUnit<U, Magnitude<BPs...>> operator*(U, Magnitude<BPs...>) {
constexpr ComputeScaledUnit<U, Magnitude<BPs...>> operator*(U, Magnitude<BPs...>) {
return {};
}

// Scale this Unit by dividing by a Magnitude.
template <typename U, typename = std::enable_if_t<IsUnit<U>::value>, typename... BPs>
constexpr ScaledUnit<U, MagInverseT<Magnitude<BPs...>>> operator/(U, Magnitude<BPs...>) {
constexpr ComputeScaledUnit<U, MagInverseT<Magnitude<BPs...>>> operator/(U, Magnitude<BPs...>) {
return {};
}

Expand Down Expand Up @@ -821,6 +833,22 @@ struct UnitLabel<UnitProduct<Us...>>
detail::DenominatorPartT<UnitProduct<Us...>>,
void> {};

// Implementation for ScaledUnit: scaling unit U by M gets label `"[M U]"`.
template <typename U, typename M>
struct UnitLabel<ScaledUnit<U, M>> {
using MagLab = MagnitudeLabel<M>;
using LabelT = detail::
ExtendedLabel<detail::parens_if<MagLab::has_exposed_slash>(MagLab::value).size() + 3u, U>;
static constexpr LabelT value =
detail::concatenate("[",
detail::parens_if<MagLab::has_exposed_slash>(MagLab::value),
" ",
UnitLabel<U>::value,
"]");
};
template <typename U, typename M>
constexpr typename UnitLabel<ScaledUnit<U, M>>::LabelT UnitLabel<ScaledUnit<U, M>>::value;

// Implementation for CommonUnit: unite constituent labels.
template <typename... Us>
struct UnitLabel<CommonUnit<Us...>> {
Expand Down Expand Up @@ -859,6 +887,20 @@ struct OrderByDim : InStandardPackOrder<DimT<A>, DimT<B>> {};
template <typename A, typename B>
struct OrderByMag : InStandardPackOrder<MagT<A>, MagT<B>> {};

// Order by "scaledness" of scaled units. This is always false unless BOTH are specializations of
// the `ScaledUnit<U, M>` template. If they are, we *assume* we would never call this unless both
// `OrderByDim` and `OrderByMag` are tied. Therefore, we go by the _scale factor itself_.
template <typename A, typename B>
struct OrderByScaledness : std::false_type {};
template <typename A, typename B>
struct OrderByScaleFactor : std::false_type {};
template <typename U1, typename M1, typename U2, typename M2>
struct OrderByScaleFactor<ScaledUnit<U1, M1>, ScaledUnit<U2, M2>> : InStandardPackOrder<M1, M2> {};

template <typename U1, typename M1, typename U2, typename M2>
struct OrderByScaledness<ScaledUnit<U1, M1>, ScaledUnit<U2, M2>>
: LexicographicTotalOrdering<ScaledUnit<U1, M1>, ScaledUnit<U2, M2>, OrderByScaleFactor> {};

// OrderAsUnitProduct<A, B> can only be true if both A and B are unit products, _and_ they are in
// the standard pack order for unit products. This default case handles the usual case where either
// A or B (or both) is not a UnitProduct<...> in the first place.
Expand Down Expand Up @@ -913,6 +955,7 @@ struct InOrderFor<UnitProduct, A, B> : LexicographicTotalOrdering<A,
detail::OrderByUnitAvoidance,
detail::OrderByDim,
detail::OrderByMag,
detail::OrderByScaleFactor,
detail::OrderByOrigin,
detail::OrderAsUnitProduct> {};

Expand Down
37 changes: 27 additions & 10 deletions au/code/au/unit_of_measure_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ struct SomePack {};
template <typename A, typename B>
struct InOrderFor<SomePack, A, B> : InOrderFor<CommonUnit, A, B> {};

struct UnlabeledUnit : decltype(Feet{} * mag<9>()) {};
struct UnlabeledUnit : UnitImpl<Length> {};

MATCHER_P(QuantityEquivalentToUnit, target, "") {
return are_units_quantity_equivalent(arg, target);
Expand All @@ -111,6 +111,11 @@ TEST(Unit, OriginRetainedForProductWithMagnitudeButNotWithUnit) {
(stdx::experimental::is_detected<detail::OriginMemberType, decltype(scaled_by_unit)>{}));
}

TEST(ScaledUnit, IsTypeIdentityWhenScalingByOne) {
StaticAssertTypeEq<decltype(Feet{} * mag<1>()), Feet>();
StaticAssertTypeEq<decltype((Feet{} * mag<3>()) / mag<3>()), Feet>();
}

TEST(IsUnit, TrueForUnitImpl) { EXPECT_TRUE(IsUnit<UnitImpl<Length>>::value); }

TEST(IsUnit, TrueForOpaqueTypedef) { EXPECT_TRUE(IsUnit<Feet>::value); }
Expand Down Expand Up @@ -452,15 +457,7 @@ TEST(CommonUnit, PrefersUnitFromListIfAnyIdentical) {
}

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

using OpaqueFeetSquared = decltype(pow<2>(Feet{}) * ONE);
using OpaqueFeet = UnitProductT<OpaqueFeetSquared, UnitInverseT<Feet>>;
ASSERT_FALSE((std::is_same<OpaqueFeet, Feet>::value));
ASSERT_TRUE((AreUnitsQuantityEquivalent<OpaqueFeet, Feet>::value));
ASSERT_TRUE((InOrderFor<CommonUnit, Feet, OpaqueFeet>::value));
StaticAssertTypeEq<CommonUnitT<Feet, OpaqueFeet>, Feet>();
StaticAssertTypeEq<CommonUnitT<Yards, decltype(Feet{} * mag<3>())>, Yards>();
}

// Four coprime units of the same dimension.
Expand All @@ -483,6 +480,10 @@ TEST(CommonUnit, UnpacksTypesInNestedCommonUnit) {
StaticAssertTypeEq<Common, CommonUnitT<W, X, Y, Z>>();
}

TEST(CommonUnit, CanCombineUnitsThatWouldBothBeAnonymousScaledUnits) {
EXPECT_EQ((feet / mag<3>())(1), (inches * mag<4>())(1));
}

TEST(CommonPointUnit, FindsCommonMagnitude) {
EXPECT_THAT((CommonPointUnitT<Feet, Feet>{}), PointEquivalentToUnit(Feet{}));
EXPECT_THAT((CommonPointUnitT<Feet, Inches>{}), PointEquivalentToUnit(Inches{}));
Expand Down Expand Up @@ -544,6 +545,22 @@ TEST(UnitLabel, PicksUpLabelForLabeledUnit) {
EXPECT_EQ(sizeof(unit_label<Feet>()), 3);
}

TEST(UnitLabel, PrependsScaleFactorToLabelForScaledUnit) {
EXPECT_THAT(unit_label<decltype(Feet{} * mag<3>())>(), StrEq("[3 ft]"));
EXPECT_THAT(unit_label<decltype(Feet{} / mag<12>())>(), StrEq("[(1 / 12) ft]"));
}

TEST(UnitLabel, ApplyingMultipleScaleFactorsComposesToOneSingleScaleFactor) {
EXPECT_THAT(unit_label<decltype(Feet{} * mag<7>() / mag<12>())>(), StrEq("[(7 / 12) ft]"));
EXPECT_THAT(unit_label<decltype(Feet{} * mag<2>() / mag<3>() * mag<5>() / mag<7>())>(),
StrEq("[(10 / 21) ft]"));
}

TEST(UnitLabel, OmitsTrivialScaleFactor) {
EXPECT_THAT(unit_label<decltype(Feet{} * mag<1>())>(), StrEq("ft"));
EXPECT_THAT(unit_label<decltype((Feet{} * mag<3>()) / mag<3>())>(), StrEq("ft"));
}

TEST(UnitLabel, PrintsExponentForUnitPower) {
EXPECT_THAT(unit_label(Pow<Minutes, 2>{}), StrEq("min^2"));
EXPECT_THAT(unit_label(Pow<Feet, 33>{}), StrEq("ft^33"));
Expand Down

0 comments on commit e88baac

Please sign in to comment.