From 5f79db31b530391c1d27c50532745f52fe5b1e7f Mon Sep 17 00:00:00 2001 From: Chip Hogg Date: Mon, 28 Oct 2024 11:14:32 -0400 Subject: [PATCH] Eliminate redundant units from `CommonUnit<...>` (#310) The core here is the addition to `:quantity_test`. Making this pass is the goal of this PR. To get there, we need a new trait/metafunction that can take a pack of units, and eliminate all the "redundant" ones. This means we also need a definition of "redundant", where the canonical example would be that "feet" is redundant between "feet" and "inches". Here's what we came up with: - If two units are _identical_ (same type), then each is redundant w.r.t. the other. - If two units are _distinct_, but _quantity-equivalent_, then the redundant one is whichever comes later in the "standard pack ordering". (We try to give more "recognizable" units higher priority.) - Otherwise, for _quantity-inequivalent_ units, a redundant unit is an exact integer multiple of another unit. All of this assumes that the units have the same dimension. We simply wouldn't call this machinery in the first place if that weren't true. Finally, we need one more piece of machinery: a simple "drop all X" metafunction. This is because when an earlier unit makes later units redundant, we can't easily "reach into" that "tail pack" of units and remove them directly. Instead, we need to mark them with a "tombstone" (`void`), and then remove all the tombstoned units. Fixes #295. --- au/code/au/quantity_test.cc | 33 ++++++++++++- au/code/au/unit_of_measure.hh | 52 ++++++++++++++++++++- au/code/au/unit_of_measure_test.cc | 26 +++++++++++ au/code/au/utility/test/type_traits_test.cc | 20 ++++++-- au/code/au/utility/type_traits.hh | 23 +++++++++ 5 files changed, 149 insertions(+), 5 deletions(-) diff --git a/au/code/au/quantity_test.cc b/au/code/au/quantity_test.cc index e5f1d4e0..39d6d8ff 100644 --- a/au/code/au/quantity_test.cc +++ b/au/code/au/quantity_test.cc @@ -22,6 +22,8 @@ #include "gtest/gtest.h" using ::testing::DoubleEq; +using ::testing::Each; +using ::testing::Eq; using ::testing::StaticAssertTypeEq; namespace au { @@ -29,7 +31,10 @@ namespace au { struct Feet : UnitImpl {}; constexpr auto feet = QuantityMaker{}; -struct Miles : decltype(Feet{} * mag<5'280>()) {}; +struct Miles : decltype(Feet{} * mag<5'280>()) { + static constexpr const char label[] = "mi"; +}; +constexpr const char Miles::label[]; constexpr auto mile = SingularNameFor{}; constexpr auto miles = QuantityMaker{}; @@ -717,6 +722,32 @@ TEST(Quantity, MixedTypeSubtractionUsesCommonRepType) { EXPECT_THAT(feet(2.f) - feet(1.5), QuantityEquivalent(feet(0.5))); } +TEST(Quantity, CommonUnitAlwaysCompletelyIndependentOfOrder) { + auto check_units = [](auto unit_a, auto unit_b, auto unit_c) { + const auto a = unit_a(1LL); + const auto b = unit_b(1LL); + const auto c = unit_c(1LL); + auto stream_to_string = [](auto x) { + std::ostringstream oss; + oss << x; + return oss.str(); + }; + std::vector results = { + stream_to_string(a + b + c), + stream_to_string(a + c + b), + stream_to_string(b + a + c), + stream_to_string(b + c + a), + stream_to_string(c + a + b), + stream_to_string(c + b + a), + }; + EXPECT_THAT(results, Each(Eq(results[0]))) + << "Inconsistency found for (" << a << ", " << b << ", " << c << ")"; + }; + + check_units(centi(meters), miles, meters); + check_units(kilo(meters), miles, milli(meters)); +} + TEST(Quantity, CommonTypeRespectsImplicitRepSafetyChecks) { // The following test should fail to compile. Uncomment both lines to check. // constexpr auto feeters = QuantityMaker>{}; diff --git a/au/code/au/unit_of_measure.hh b/au/code/au/unit_of_measure.hh index 20b2c8de..d88f335d 100644 --- a/au/code/au/unit_of_measure.hh +++ b/au/code/au/unit_of_measure.hh @@ -19,6 +19,7 @@ #include "au/power_aliases.hh" #include "au/stdx/type_traits.hh" #include "au/utility/string_constant.hh" +#include "au/utility/type_traits.hh" #include "au/zero.hh" namespace au { @@ -533,10 +534,59 @@ struct FirstMatchingUnit> stdx::type_identity, FirstMatchingUnit>> {}; +// A "redundant" unit, among a list of units, is one that is an exact integer multiple of another. +// +// If two units are identical, then each is redundant with the other. +// +// If two units are distinct, but quantity-equivalent, then the unit that comes later in the +// standard unit ordering (i.e., `InOrderFor`) is the redundant one. +template +struct EliminateRedundantUnitsImpl; +template +using EliminateRedundantUnits = typename EliminateRedundantUnitsImpl::type; + +// Base case: no units to eliminate. +template