-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Eliminate redundant units from CommonUnit<...>
#310
Conversation
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.
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<std::string> 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)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to reviewer: if we added only this test case on main, without the fixes, here is what the failure would look like:
[ RUN ] Quantity.CommonUnitAlwaysCompletelyIndependentOfOrder
au/code/au/quantity_test.cc:743: Failure
Value of: results
Expected: only contains elements that is equal to "805177 COM[cm, m, mi]"
Actual: { "805177 COM[cm, m, mi]", "805177 COM[cm, mi]", "805177 COM[cm, m, mi]", "805177 COM[cm, m, mi]", "805177 COM[cm, mi]", "805177 COM[cm, m, mi]" }, whose element #1 doesn't match
Inconsistency found for (1 cm, 1 mi, 1 m)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of checking that they are all consistent with the first element, can we check that they are all equal to a specific unit? That might help test readibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see the appeal of this suggestion. I struggled with this for a little bit, but I couldn't see my way to an implementation that did this while keeping the test implementation clean.
One way would be to compare to a desired unit label. But the unit label for common units will change dramatically after 0.4.0 (#105), and I'd hate to have to update this test for that change.
We could also do it by providing the expected type of the common unit. But CommonUnit<...>
isn't supposed to be named directly by end users: we're supposed to use CommonUnitT<...>
, which performs the de-duping and simplification that is the very thing under test!
Ultimately, I think the current approach is the best option available. Phrasing it in terms of order independence, as mentioned in #295 (comment), is the most elegant way I know to express what we're really after here. The downside, as you point out, is that a test failure can't tell you which one is correct; all it can do is point out that they should all be the same, and they're not. But I think in practice this will be good enough to point maintainers in the right direction.
Note: compile time impact tests came back squeaky clean. Aurora-internal AV repo tests are still running but look very promising so far and I expect them to pass as well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great change! This should make common units more understandable.
//////////////////////////////////////////////////////////////////////////////////////////////////// | ||
|
||
//////////////////////////////////////////////////////////////////////////////////////////////////// | ||
// `Prepend` implementation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the point of this comment? Do we need a comment for all inline implementations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Often, the implementation for "public"-ish interface declared up top in a file will depend on new helpers, which I will declare-and-define right next to the "core" implementation. This can get a little messy and confusing, so I use these "section comments" to group things and clarify the purpose. That said, I'm certainly rather inconsistent about using them in practice.
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<std::string> 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)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of checking that they are all consistent with the first element, can we check that they are all equal to a specific unit? That might help test readibility.
The core here is the addition to
:quantity_test
. Making this pass isthe 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:
w.r.t. the other.
redundant one is whichever comes later in the "standard pack
ordering". (We try to give more "recognizable" units higher
priority.)
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.