From 735b33d172214abf3b6c2057fc60b281c5e292e5 Mon Sep 17 00:00:00 2001 From: Chip Hogg Date: Wed, 27 Nov 2024 12:47:45 -0500 Subject: [PATCH] Gather factors and add labels for scaled units 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, 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`, 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. --- au/code/au/quantity_test.cc | 6 ++-- au/code/au/unit_of_measure.hh | 57 ++++++++++++++++++++++++++---- au/code/au/unit_of_measure_test.cc | 37 +++++++++++++------ 3 files changed, 80 insertions(+), 20 deletions(-) diff --git a/au/code/au/quantity_test.cc b/au/code/au/quantity_test.cc index 74513b9f..b9dce430 100644 --- a/au/code/au/quantity_test.cc +++ b/au/code/au/quantity_test.cc @@ -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::value)); - EXPECT_TRUE((AreQuantityTypesEquivalent::value)); + ASSERT_FALSE((std::is_same::value)); + EXPECT_TRUE((AreQuantityTypesEquivalent::value)); } TEST(UnblockIntDiv, EnablesTruncatingIntegerDivisionIntoQuantity) { diff --git a/au/code/au/unit_of_measure.hh b/au/code/au/unit_of_measure.hh index b224810c..b720046e 100644 --- a/au/code/au/unit_of_measure.hh +++ b/au/code/au/unit_of_measure.hh @@ -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 +struct ScaledUnit; + +template +struct ComputeScaledUnitImpl : stdx::type_identity> {}; +template +using ComputeScaledUnit = typename ComputeScaledUnitImpl::type; +template +struct ComputeScaledUnitImpl, ScaleFactor> + : ComputeScaledUnitImpl> {}; +template +struct ComputeScaledUnitImpl> : stdx::type_identity {}; +// Disambiguating specialization: +template +struct ComputeScaledUnitImpl, Magnitude<>> + : stdx::type_identity> {}; + template struct ScaledUnit : Unit { static_assert(IsValidPack::value, "Can only scale by a Magnitude<...> type"); using Dim = detail::DimT; using Mag = MagProductT, 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::value; }; // Type template to hold the product of powers of Units. @@ -321,13 +333,13 @@ using UnitQuotientT = UnitProductT>; // Scale this Unit by multiplying by a Magnitude. template ::value>, typename... BPs> -constexpr ScaledUnit> operator*(U, Magnitude) { +constexpr ComputeScaledUnit> operator*(U, Magnitude) { return {}; } // Scale this Unit by dividing by a Magnitude. template ::value>, typename... BPs> -constexpr ScaledUnit>> operator/(U, Magnitude) { +constexpr ComputeScaledUnit>> operator/(U, Magnitude) { return {}; } @@ -821,6 +833,22 @@ struct UnitLabel> detail::DenominatorPartT>, void> {}; +// Implementation for ScaledUnit: scaling unit U by M gets label `"[M U]"`. +template +struct UnitLabel> { + using MagLab = MagnitudeLabel; + using LabelT = detail:: + ExtendedLabel(MagLab::value).size() + 3u, U>; + static constexpr LabelT value = + detail::concatenate("[", + detail::parens_if(MagLab::value), + " ", + UnitLabel::value, + "]"); +}; +template +constexpr typename UnitLabel>::LabelT UnitLabel>::value; + // Implementation for CommonUnit: unite constituent labels. template struct UnitLabel> { @@ -859,6 +887,20 @@ struct OrderByDim : InStandardPackOrder, DimT> {}; template struct OrderByMag : InStandardPackOrder, MagT> {}; +// Order by "scaledness" of scaled units. This is always false unless BOTH are specializations of +// the `ScaledUnit` 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 +struct OrderByScaledness : std::false_type {}; +template +struct OrderByScaleFactor : std::false_type {}; +template +struct OrderByScaleFactor, ScaledUnit> : InStandardPackOrder {}; + +template +struct OrderByScaledness, ScaledUnit> + : LexicographicTotalOrdering, ScaledUnit, OrderByScaleFactor> {}; + // OrderAsUnitProduct 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. @@ -913,6 +955,7 @@ struct InOrderFor : LexicographicTotalOrdering {}; diff --git a/au/code/au/unit_of_measure_test.cc b/au/code/au/unit_of_measure_test.cc index dfd452cf..72800487 100644 --- a/au/code/au/unit_of_measure_test.cc +++ b/au/code/au/unit_of_measure_test.cc @@ -89,7 +89,7 @@ struct SomePack {}; template struct InOrderFor : InOrderFor {}; -struct UnlabeledUnit : decltype(Feet{} * mag<9>()) {}; +struct UnlabeledUnit : UnitImpl {}; MATCHER_P(QuantityEquivalentToUnit, target, "") { return are_units_quantity_equivalent(arg, target); @@ -111,6 +111,11 @@ TEST(Unit, OriginRetainedForProductWithMagnitudeButNotWithUnit) { (stdx::experimental::is_detected{})); } +TEST(ScaledUnit, IsTypeIdentityWhenScalingByOne) { + StaticAssertTypeEq()), Feet>(); + StaticAssertTypeEq()) / mag<3>()), Feet>(); +} + TEST(IsUnit, TrueForUnitImpl) { EXPECT_TRUE(IsUnit>::value); } TEST(IsUnit, TrueForOpaqueTypedef) { EXPECT_TRUE(IsUnit::value); } @@ -452,15 +457,7 @@ TEST(CommonUnit, PrefersUnitFromListIfAnyIdentical) { } TEST(CommonUnit, DownranksAnonymousScaledUnits) { - StaticAssertTypeEq())>, Feet>(); - StaticAssertTypeEq>, Meters>(); - - using OpaqueFeetSquared = decltype(pow<2>(Feet{}) * ONE); - using OpaqueFeet = UnitProductT>; - ASSERT_FALSE((std::is_same::value)); - ASSERT_TRUE((AreUnitsQuantityEquivalent::value)); - ASSERT_TRUE((InOrderFor::value)); - StaticAssertTypeEq, Feet>(); + StaticAssertTypeEq())>, Yards>(); } // Four coprime units of the same dimension. @@ -483,6 +480,10 @@ TEST(CommonUnit, UnpacksTypesInNestedCommonUnit) { StaticAssertTypeEq>(); } +TEST(CommonUnit, CanCombineUnitsThatWouldBothBeAnonymousScaledUnits) { + EXPECT_EQ((feet / mag<3>())(1), (inches * mag<4>())(1)); +} + TEST(CommonPointUnit, FindsCommonMagnitude) { EXPECT_THAT((CommonPointUnitT{}), PointEquivalentToUnit(Feet{})); EXPECT_THAT((CommonPointUnitT{}), PointEquivalentToUnit(Inches{})); @@ -544,6 +545,22 @@ TEST(UnitLabel, PicksUpLabelForLabeledUnit) { EXPECT_EQ(sizeof(unit_label()), 3); } +TEST(UnitLabel, PrependsScaleFactorToLabelForScaledUnit) { + EXPECT_THAT(unit_label())>(), StrEq("[3 ft]")); + EXPECT_THAT(unit_label())>(), StrEq("[(1 / 12) ft]")); +} + +TEST(UnitLabel, ApplyingMultipleScaleFactorsComposesToOneSingleScaleFactor) { + EXPECT_THAT(unit_label() / mag<12>())>(), StrEq("[(7 / 12) ft]")); + EXPECT_THAT(unit_label() / mag<3>() * mag<5>() / mag<7>())>(), + StrEq("[(10 / 21) ft]")); +} + +TEST(UnitLabel, OmitsTrivialScaleFactor) { + EXPECT_THAT(unit_label())>(), StrEq("ft")); + EXPECT_THAT(unit_label()) / mag<3>())>(), StrEq("ft")); +} + TEST(UnitLabel, PrintsExponentForUnitPower) { EXPECT_THAT(unit_label(Pow{}), StrEq("min^2")); EXPECT_THAT(unit_label(Pow{}), StrEq("ft^33"));