diff --git a/au/BUILD.bazel b/au/BUILD.bazel index ce69e047..7e945707 100644 --- a/au/BUILD.bazel +++ b/au/BUILD.bazel @@ -113,7 +113,10 @@ cc_test( cc_library( name = "apply_magnitude", hdrs = ["apply_magnitude.hh"], - deps = [":magnitude"], + deps = [ + ":apply_rational_magnitude_to_integral", + ":magnitude", + ], ) cc_test( @@ -128,6 +131,23 @@ cc_test( ], ) +cc_library( + name = "apply_rational_magnitude_to_integral", + hdrs = ["apply_rational_magnitude_to_integral.hh"], + deps = [":magnitude"], +) + +cc_test( + name = "apply_rational_magnitude_to_integral_test", + size = "small", + srcs = ["apply_rational_magnitude_to_integral_test.cc"], + deps = [ + ":apply_rational_magnitude_to_integral", + ":testing", + "@com_google_googletest//:gtest_main", + ], +) + cc_library( name = "chrono_interop", hdrs = ["chrono_interop.hh"], diff --git a/au/apply_magnitude.hh b/au/apply_magnitude.hh index 515a3da5..e0c580e9 100644 --- a/au/apply_magnitude.hh +++ b/au/apply_magnitude.hh @@ -14,6 +14,7 @@ #pragma once +#include "au/apply_rational_magnitude_to_integral.hh" #include "au/magnitude.hh" namespace au { @@ -132,6 +133,28 @@ struct ApplyMagnitudeImpl { } }; +template +struct RationalOverflowChecker; +template +struct RationalOverflowChecker { + static constexpr bool would_overflow(const T &x) { + static_assert(std::is_signed::value, + "Mismatched instantiation (should never be done manually)"); + const bool safe = (x <= MaxNonOverflowingValue::value()) && + (x >= MinNonOverflowingValue::value()); + return !safe; + } +}; +template +struct RationalOverflowChecker { + static constexpr bool would_overflow(const T &x) { + static_assert(!std::is_signed::value, + "Mismatched instantiation (should never be done manually)"); + const bool safe = (x <= MaxNonOverflowingValue::value()); + return !safe; + } +}; + // Applying a (non-integer, non-inverse-integer) rational, for any integral type T. template struct ApplyMagnitudeImpl { @@ -141,13 +164,13 @@ struct ApplyMagnitudeImpl { "Mismatched instantiation (should never be done manually)"); constexpr T operator()(const T &x) { - return x * get_value(numerator(Mag{})) / get_value(denominator(Mag{})); + using P = PromotedType; + return static_cast(x * get_value

(numerator(Mag{})) / + get_value

(denominator(Mag{}))); } static constexpr bool would_overflow(const T &x) { - constexpr auto mag_value_result = get_value_result(numerator(Mag{})); - return OverflowChecker:: - would_product_overflow(x, mag_value_result.value); + return RationalOverflowChecker::value>::would_overflow(x); } static constexpr bool would_truncate(const T &x) { diff --git a/au/apply_magnitude_test.cc b/au/apply_magnitude_test.cc index 0bfbae15..1f6fad24 100644 --- a/au/apply_magnitude_test.cc +++ b/au/apply_magnitude_test.cc @@ -90,6 +90,28 @@ TEST(ApplyMagnitude, MultipliesThenDividesForRationalMagnitudeOnInteger) { EXPECT_THAT(apply_magnitude(5, three_halves), SameTypeAndValue(7)); } +TEST(ApplyMagnitude, SupportsNumeratorThatFitsInPromotedTypeButNotOriginalType) { + using T = uint16_t; + using P = PromotedType; + ASSERT_TRUE((std::is_same::value)) + << "This test fails on architectures where `uint16_t` doesn't get promoted to `int32_t`"; + + // Choose a magnitude whose effect will basically be to divide by 2. (We make the denominator + // slightly _smaller_ than twice the numerator, rather than slightly _larger_, so that the + // division will end up on the "high" side of the target, and truncation will bring it down very + // slightly instead of going down a full integer.) + auto roughly_one_half = mag<100'000'000>() / mag<199'999'999>(); + + // The whole point of this test case is to apply a magnitude whose numerator fits in the + // promoted type, but does not fit in the target type itself. + ASSERT_EQ(get_value_result

(numerator(roughly_one_half)).outcome, + MagRepresentationOutcome::OK); + ASSERT_EQ(get_value_result(numerator(roughly_one_half)).outcome, + MagRepresentationOutcome::ERR_CANNOT_FIT); + + EXPECT_THAT(apply_magnitude(T{18}, roughly_one_half), SameTypeAndValue(T{9})); +} + TEST(ApplyMagnitude, MultipliesSingleNumberForRationalMagnitudeOnFloatingPoint) { // Helper similar to `std::transform`, but with more convenient interfaces. auto apply = [](std::vector vals, auto fun) { @@ -196,10 +218,8 @@ TEST(WouldOverflow, AlwaysFalseForIntegerDivide) { } TEST(WouldOverflow, UsesNumeratorWhenApplyingRationalMagnitudeToIntegralType) { - auto TWO_THIRDS = mag<2>() / mag<3>(); - { - using ApplyTwoThirdsToI32 = ApplyMagnitudeT; + using ApplyTwoThirdsToI32 = ApplyMagnitudeT() / mag<3>())>; EXPECT_TRUE(ApplyTwoThirdsToI32::would_overflow(2'147'483'647)); EXPECT_TRUE(ApplyTwoThirdsToI32::would_overflow(1'073'741'824)); @@ -215,14 +235,18 @@ TEST(WouldOverflow, UsesNumeratorWhenApplyingRationalMagnitudeToIntegralType) { } { - using ApplyTwoThirdsToU8 = ApplyMagnitudeT; + using ApplyRoughlyOneThirdToU8 = + ApplyMagnitudeT() / mag<300'000'001>())>; + + ASSERT_TRUE((std::is_same::value)) + << "This test fails on architectures where `uint8_t` doesn't get promoted to `int32_t`"; - EXPECT_TRUE(ApplyTwoThirdsToU8::would_overflow(255)); - EXPECT_TRUE(ApplyTwoThirdsToU8::would_overflow(128)); + EXPECT_TRUE(ApplyRoughlyOneThirdToU8::would_overflow(255)); + EXPECT_TRUE(ApplyRoughlyOneThirdToU8::would_overflow(22)); - EXPECT_FALSE(ApplyTwoThirdsToU8::would_overflow(127)); - EXPECT_FALSE(ApplyTwoThirdsToU8::would_overflow(1)); - EXPECT_FALSE(ApplyTwoThirdsToU8::would_overflow(0)); + EXPECT_FALSE(ApplyRoughlyOneThirdToU8::would_overflow(21)); + EXPECT_FALSE(ApplyRoughlyOneThirdToU8::would_overflow(1)); + EXPECT_FALSE(ApplyRoughlyOneThirdToU8::would_overflow(0)); } } diff --git a/au/apply_rational_magnitude_to_integral.hh b/au/apply_rational_magnitude_to_integral.hh new file mode 100644 index 00000000..111d0beb --- /dev/null +++ b/au/apply_rational_magnitude_to_integral.hh @@ -0,0 +1,251 @@ +// Copyright 2023 Aurora Operations, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "au/magnitude.hh" +#include "au/stdx/utility.hh" + +// This file exists to analyze one single calculation: `x * N / D`, where `x` is +// some integral type, and `N` and `D` are the numerator and denominator of a +// rational magnitude (and hence, are automatically in lowest terms), +// represented in that same type. We want to answer one single question: will +// this calculation overflow at any stage? +// +// Importantly, we need to produce correct answers even when `N` and/or `D` +// _cannot be represented_ in that type (because they would overflow). We also +// need to handle subtleties around integer promotion, where the type of `x * x` +// can be different from the type of `x` when those types are small. +// +// The goal for the final solution we produce is to be as fast and efficient as +// the best such function that an expert C++ engineer could produce by hand, for +// every combination of integral type and numerator and denominator magnitudes. + +namespace au { +namespace detail { + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// `PromotedType` is the result type for arithmetic operations involving `T`. Of course, this is +// normally just `T`, but integer promotion for small integral types can change this. +// +template +struct PromotedTypeImpl { + using type = decltype(std::declval() * std::declval()); + + static_assert(std::is_same::type>::value, + "We explicitly assume that promoted types are not again promotable"); +}; +template +using PromotedType = typename PromotedTypeImpl::type; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// `clamp_to_range_of(x)` returns `x` if it is in the range of `T`, and otherwise returns the +// maximum value representable in `T` if `x` is too large, or the minimum value representable in `T` +// if `x` is too small. +// + +template +constexpr T clamp_to_range_of(U x) { + return stdx::cmp_greater(x, std::numeric_limits::max()) + ? std::numeric_limits::max() + : (stdx::cmp_less(x, std::numeric_limits::lowest()) + ? std::numeric_limits::lowest() + : static_cast(x)); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// `is_known_to_be_less_than_one(MagT)` is true if the magnitude `MagT` is purely rational; its +// numerator is representable in `std::uintmax_t`; and, it is less than 1. +// + +template +constexpr bool is_known_to_be_less_than_one(Magnitude m) { + using MagT = Magnitude; + static_assert(is_rational(m), "Magnitude must be rational"); + + constexpr auto num_result = get_value_result(numerator(MagT{})); + static_assert(num_result.outcome == MagRepresentationOutcome::OK, + "Magnitude must be representable in std::uintmax_t"); + + constexpr auto den_result = get_value_result(denominator(MagT{})); + static_assert( + den_result.outcome == MagRepresentationOutcome::OK || + den_result.outcome == MagRepresentationOutcome::ERR_CANNOT_FIT, + "Magnitude must either be representable in std::uintmax_t, or fail due to overflow"); + + return den_result.outcome == MagRepresentationOutcome::OK ? num_result.value < den_result.value + : true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// `MaxNonOverflowingValue` is the maximum value of type `T` that can have `MagT` applied +// as numerator-and-denominator without overflowing. We require that `T` is some integral +// arithmetic type, and that `MagT` is a rational magnitude that is neither purely integral nor +// purely inverse-integral. +// +// If you are trying to understand these helpers, we suggest starting at the bottom with +// `MaxNonOverflowingValue`, and reading upwards. +// + +// +// Branch based on whether `MagT` is less than 1. +// +template +struct MaxNonOverflowingValueImplWhenNumFits; + +// If `MagT` is less than 1, then we only need to check for the limiting value where the _numerator +// multiplication step alone_ would overflow. +template +struct MaxNonOverflowingValueImplWhenNumFits { + using P = PromotedType; + + static constexpr T value() { + return clamp_to_range_of(std::numeric_limits

::max() / + get_value

(numerator(MagT{}))); + } +}; + +// If `MagT` is greater than 1, then we have two opportunities for overflow: the numerator +// multiplication step can overflow the promoted type; or, the denominator division step can fail to +// restore it to the original type's range. +template +struct MaxNonOverflowingValueImplWhenNumFits { + using P = PromotedType; + + static constexpr T value() { + constexpr auto num = get_value

(numerator(MagT{})); + constexpr auto den = get_value

(denominator(MagT{})); + constexpr auto t_max = std::numeric_limits::max(); + constexpr auto p_max = std::numeric_limits

::max(); + constexpr auto limit_to_avoid = (den > p_max / t_max) ? p_max : t_max * den; + return clamp_to_range_of(limit_to_avoid / num); + } +}; + +// +// Branch based on whether the numerator of `MagT` can fit in the promoted type of `T`. +// +template +struct MaxNonOverflowingValueImpl; + +// If the numerator fits in the promoted type of `T`, delegate further based on whether the +// denominator is bigger. +template +struct MaxNonOverflowingValueImpl + : MaxNonOverflowingValueImplWhenNumFits {}; + +// If `MagT` can't be represented in the promoted type of `T`, then the result is 0. +template +struct MaxNonOverflowingValueImpl { + static constexpr T value() { return T{0}; } +}; + +template +struct ValidateTypeAndMagnitude { + static_assert(std::is_integral::value, "Only designed for integral types"); + static_assert(is_rational(MagT{}), "Magnitude must be rational"); + static_assert(!is_integer(MagT{}), "Magnitude must not be purely integral"); + static_assert(!is_integer(inverse(MagT{})), "Magnitude must not be purely inverse-integral"); +}; + +template +struct MaxNonOverflowingValue + : ValidateTypeAndMagnitude, + MaxNonOverflowingValueImpl>(numerator(MagT{})).outcome> {}; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// `MinNonOverflowingValue` is the minimum (i.e., most-negative) value of type `T` that can +// have `MagT` applied as numerator-and-denominator without overflowing (i.e., becoming too-negative +// to represent). We require that `T` is some integral arithmetic type, and that `MagT` is a +// rational magnitude that is neither purely integral nor purely inverse-integral. +// +// If you are trying to understand these helpers, we suggest starting at the bottom with +// `MinNonOverflowingValue`, and reading upwards. +// + +// +// Branch based on whether `MagT` is less than 1. +// +template +struct MinNonOverflowingValueImplWhenNumFits; + +// If `MagT` is less than 1, then we only need to check for the limiting value where the _numerator +// multiplication step alone_ would overflow. +template +struct MinNonOverflowingValueImplWhenNumFits { + using P = PromotedType; + + static constexpr T value() { + return clamp_to_range_of(std::numeric_limits

::lowest() / + get_value

(numerator(MagT{}))); + } +}; + +// If `MagT` is greater than 1, then we have two opportunities for overflow: the numerator +// multiplication step can overflow the promoted type; or, the denominator division step can fail to +// restore it to the original type's range. +template +struct MinNonOverflowingValueImplWhenNumFits { + using P = PromotedType; + + static constexpr T value() { + constexpr auto num = get_value

(numerator(MagT{})); + constexpr auto den = get_value

(denominator(MagT{})); + constexpr auto t_min = std::numeric_limits::lowest(); + constexpr auto p_min = std::numeric_limits

::lowest(); + constexpr auto limit_to_avoid = (den > p_min / t_min) ? p_min : t_min * den; + return clamp_to_range_of(limit_to_avoid / num); + } +}; + +// +// Branch based on whether the denominator of `MagT` can fit in the promoted type of `T`. +// +template +struct MinNonOverflowingValueImpl; + +// If the numerator fits in the promoted type of `T`, delegate further based on whether the +// denominator is bigger. +template +struct MinNonOverflowingValueImpl + : MinNonOverflowingValueImplWhenNumFits {}; + +// If the numerator can't be represented in the promoted type of `T`, then the result is 0. +template +struct MinNonOverflowingValueImpl { + static constexpr T value() { return T{0}; } +}; + +template +struct MinNonOverflowingValue + : ValidateTypeAndMagnitude, + MinNonOverflowingValueImpl>(numerator(MagT{})).outcome> { + static_assert(std::is_signed::value, "Only designed for signed types"); + static_assert(std::is_signed>::value, + "We assume the promoted type is also signed"); +}; + +} // namespace detail +} // namespace au diff --git a/au/apply_rational_magnitude_to_integral_test.cc b/au/apply_rational_magnitude_to_integral_test.cc new file mode 100644 index 00000000..65ef663a --- /dev/null +++ b/au/apply_rational_magnitude_to_integral_test.cc @@ -0,0 +1,376 @@ +// Copyright 2023 Aurora Operations, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "au/apply_rational_magnitude_to_integral.hh" + +#include "au/testing.hh" +#include "gtest/gtest.h" + +using ::testing::AllOf; +using ::testing::Lt; +using ::testing::StaticAssertTypeEq; + +namespace au { +namespace detail { +namespace { + +template +constexpr void ensure_relevant_kind_of_magnitude(Magnitude m) { + static_assert(is_rational(m), "Magnitude must be rational"); + static_assert(!is_integer(m), "Magnitude must not be purely integer"); + static_assert(!is_integer(ONE / m), "Magnitude must not be purely inverse-integer"); +} + +TEST(PromotedType, IdentityForInt) { + // `int` does not undergo type promotion. + StaticAssertTypeEq>(); +} + +TEST(PromotedType, PromotesUint8TIntoLargerType) { + using PromotedU8 = PromotedType; + + // Technically, this need not be true on every conceivable architecture. However, it is true on + // the vast majority that are used in practice. Moreover, the failure mode if it's not is + // simply that a test would fail when run on some obscure architecture, and the failure would + // direct the user to this comment. This doesn't affect the actual library usage one way or + // another. + ASSERT_FALSE((std::is_same::value)); + + EXPECT_GT(sizeof(PromotedU8), sizeof(uint8_t)); +} + +TEST(IsKnownToBeLessThanOne, ProducesExpectedResultsForMagnitudesThatCanFitInUintmax) { + EXPECT_TRUE(is_known_to_be_less_than_one(mag<1>() / mag<2>())); + EXPECT_TRUE(is_known_to_be_less_than_one(mag<999'999>() / mag<1'000'000>())); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Test cases for maximum non-overflowing value. +// +// What are the canonical representative situations for the overflow boundary of `x * N / D`, where +// `x` has type `T`, `TM` is the maximum value of type `T`, and `PM` is the maximum value of type +// `PromotedType`? We list them here. Note that they're listed in a kind of "if/else-if" order: +// each number's condition assumes that all of the previous conditions are false. (So for a test +// case to fall into category "3", it can't be in category "1" or "2".) +// +// 1. `N` cannot fit inside `PromotedType`. This means any nonzero value always overflows: the +// max is 0. +// +// 2. `N < D` (whether or not `D` can fit inside `PromotedType`). This means that the final +// result will be smaller than the input, and therefore will always fit inside `T` --- as long as +// the intermediate calculation `(x * N)` doesn't overflow. So the maximum non-overflowing value +// is `min(PM / N, TM)`. +// +// 3. `N > D` (all other cases). The only step that can overflow is the multiplication with `N`. +// There are two conditions we must meet to avoid overflow. First, we must avoid going over the +// limit of the promoted type. Second, we must ensure that the product is within the limits of +// the original type after dividing by `D`. So the maximum non-overflowing value is the smaller +// of `PM` and `TM * D`, divided by `N`. (We'll have to be careful that the `TM * D` itself +// doesn't overflow! We'll cap it at `PM` if it does.) + +enum class IsPromotable { NO, YES }; +enum class NumFitsInPromotedType { NO, YES }; +enum class DenFitsInPromotedType { NO, YES }; +struct TestSpec { + IsPromotable is_promotable; + NumFitsInPromotedType num_fits; + DenFitsInPromotedType den_fits; +}; + +template +void validate_spec(TestSpec spec) { + using PromotedT = PromotedType; + const bool is_promotable = !std::is_same::value; + const bool is_expected_to_be_promotable = (spec.is_promotable == IsPromotable::YES); + ASSERT_EQ(is_promotable, is_expected_to_be_promotable) + << "Expected a type that " << (is_expected_to_be_promotable ? "is" : "is not") + << " promotable; got a type that " << (is_promotable ? "is" : "is not"); + + using Num = decltype(numerator(MagT{})); + constexpr auto num_value_result = get_value_result(Num{}); + const bool is_num_representable = (num_value_result.outcome == MagRepresentationOutcome::OK); + const bool is_num_expected_to_be_representable = (spec.num_fits == NumFitsInPromotedType::YES); + ASSERT_EQ(is_num_representable, is_num_expected_to_be_representable) + << "Expected numerator " << (is_num_expected_to_be_representable ? "to be" : "not to be") + << " representable in promoted type; it " << (is_num_representable ? "is" : "is not"); + + using Den = decltype(denominator(MagT{})); + constexpr auto den_value_result = get_value_result(Den{}); + const bool is_den_representable = (den_value_result.outcome == MagRepresentationOutcome::OK); + const bool is_den_expected_to_be_representable = (spec.den_fits == DenFitsInPromotedType::YES); + ASSERT_EQ(is_den_representable, is_den_expected_to_be_representable) + << "Expected denominator " << (is_den_expected_to_be_representable ? "to be" : "not to be") + << " representable in promoted type; it " << (is_den_representable ? "is" : "is not"); +} + +template +void populate_max_non_overflowing_value(TestSpec spec, MagT, T &result_out) { + validate_spec(spec); + result_out = MaxNonOverflowingValue::value(); +} + +TEST(MaxNonOverflowingValue, AlwaysZeroIfNumCannotFitInPromotedType) { + // Case "1" above. + constexpr auto huge = pow<400>(mag<10>()) / mag<3>(); + + { + int max_int = 123; + populate_max_non_overflowing_value( + {IsPromotable::NO, NumFitsInPromotedType::NO, DenFitsInPromotedType::YES}, + huge, + max_int); + EXPECT_EQ(max_int, 0); + } + { + uint16_t max_u16 = 123u; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::NO, DenFitsInPromotedType::YES}, + huge, + max_u16); + EXPECT_EQ(max_u16, 0); + } +} + +TEST(MaxNonOverflowingValue, IsMaxTDividedByNWhenTIsNotPromotableAndDenomOverflows) { + // Case "2" above. a) Overflowing denominator, non-promotable types only. + constexpr auto huge_denom = mag<3>() / pow<400>(mag<10>()); + + { + int max_int = 0; + populate_max_non_overflowing_value( + {IsPromotable::NO, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + huge_denom, + max_int); + EXPECT_EQ(max_int, std::numeric_limits::max() / 3); + } + + { + uint64_t max_u64 = 0; + populate_max_non_overflowing_value( + {IsPromotable::NO, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + huge_denom, + max_u64); + EXPECT_EQ(max_u64, std::numeric_limits::max() / 3); + } +} + +TEST(MaxNonOverflowingValue, + IsSmallerOfMaxPromotedTDividedByNAndMaxTWhenTIsPromotableAndDenomOverflows) { + // Case "2" above. b) Overflowing denominator, promotable types. + { + int8_t max_i8 = 0; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + mag<3>() / pow<400>(mag<10>()), + max_i8); + EXPECT_EQ(max_i8, 127); + } + + { + uint16_t max_u16 = 0; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + mag<1'000'000>() / pow<400>(mag<11>()), + max_u16); + + ASSERT_TRUE((std::is_same, int32_t>::value)) + << "This test will fail on architectures where uint16_t is not promoted to `int32_t`"; + EXPECT_EQ(max_u16, 2'147); + } +} + +TEST(MaxNonOverflowingValue, + IsSmallerOfMaxPromotedTDividedByNAndMaxTWhenTIsPromotableAndNLessThanD) { + // Case "2" above. c) Non-overflowing denominator. + + { + uint8_t max_u8 = 0; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<3>() / mag<10>(), + max_u8); + EXPECT_EQ(max_u8, 255); + } + + { + uint16_t max_u16 = 0; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<1'000'000>() / pow<6>(mag<11>()), + max_u16); + EXPECT_EQ(max_u16, 2'147); + } +} + +TEST(MaxNonOverflowingValue, IsPromotedMaxOverNWhenNIsLargeAndDIsSlightlySmaller) { + // Case 3 above (partial). + // + // When `N / D` is very slightly larger than 1, then (PM / N) can be the most constraining. + // We'll use a promoted type just to make this interesting by making PM different from TM. + uint16_t max_u16 = 0; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<1'000'000>() / mag<999'999>(), + max_u16); + ASSERT_TRUE((std::is_same, int32_t>::value)) + << "This test will fail on architectures where `uint16_t` is not promoted to `int32_t`"; + EXPECT_EQ(max_u16, 2'147); +} + +TEST(MaxNonOverflowingValue, IsTMaxOverNTimesDWhenMoreConstrainingThanPMaxOverN) { + // Case 3 above (partial). + // + // The goal is to engineer a test case where `(TM * D) / N` is more constraining than `PM / N`. + // Obviously, if `TM = PM`, then this can never be; therefore, we need to use a promotable type. + int16_t max_int16 = 0; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<1'000>() / mag<3>(), + max_int16); + ASSERT_TRUE((std::is_same, int32_t>::value)) + << "This test will fail on architectures where `int16_t` is not promoted to `int32_t`"; + ASSERT_EQ(98, 32'768 * 3 / 1'000); + EXPECT_EQ(max_int16, 98); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Test cases for minimum (i.e. most-negative) non-overflowing value. +// +// The test cases are the mirror image of the above, except that we only check signed types. + +template +void populate_min_non_overflowing_value(TestSpec spec, MagT, T &result_out) { + validate_spec(spec); + result_out = MinNonOverflowingValue::value(); +} + +TEST(MinNonOverflowingValue, AlwaysZeroIfNumCannotFitInPromotedType) { + constexpr auto huge = pow<400>(mag<10>()) / mag<3>(); + + { + int min_int = 123; + populate_min_non_overflowing_value( + {IsPromotable::NO, NumFitsInPromotedType::NO, DenFitsInPromotedType::YES}, + huge, + min_int); + EXPECT_EQ(min_int, 0); + } + + { + int16_t min_i16 = 123; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::NO, DenFitsInPromotedType::YES}, + huge, + min_i16); + EXPECT_EQ(min_i16, 0); + } +} + +TEST(MinNonOverflowingValue, IsMinTDividedByNWhenTIsNotPromotableAndDenomOverflows) { + constexpr auto huge_denom = mag<3>() / pow<400>(mag<10>()); + + { + int min_int = 0; + populate_min_non_overflowing_value( + {IsPromotable::NO, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + huge_denom, + min_int); + EXPECT_EQ(min_int, std::numeric_limits::lowest() / 3); + } + + { + int64_t min_i64 = 0; + populate_min_non_overflowing_value( + {IsPromotable::NO, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + huge_denom, + min_i64); + EXPECT_EQ(min_i64, std::numeric_limits::lowest() / 3); + } +} + +TEST(MinNonOverflowingValue, + IsSmallerOfMinPromotedTDividedByNAndMinTWhenTIsPromotableAndDenomOverflows) { + { + int8_t min_i8 = 0; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + mag<3>() / pow<400>(mag<10>()), + min_i8); + EXPECT_EQ(min_i8, -128); + } + + { + int16_t min_i16 = 0; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + mag<1'000'000>() / pow<400>(mag<11>()), + min_i16); + + ASSERT_TRUE((std::is_same, int32_t>::value)) + << "This test will fail on architectures where `int16_t` is not promoted to `int32_t`"; + EXPECT_EQ(min_i16, -2'147); + } +} + +TEST(MinNonOverflowingValue, + IsSmallerOfMinPromotedTDividedByNAndMinTWhenTIsPromotableAndNLessThanD) { + + { + int8_t min_i8 = 0; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<3>() / mag<10>(), + min_i8); + EXPECT_EQ(min_i8, -128); + } + + { + int16_t min_i16 = 0; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<1'000'000>() / pow<6>(mag<11>()), + min_i16); + EXPECT_EQ(min_i16, -2'147); + } +} + +TEST(MinNonOverflowingValue, IsPromotedMinOverNWhenNIsLargeAndDIsSlightlySmaller) { + // When `N / D` is very slightly larger than 1, then (PM / N) can be the most constraining. + // We'll use a promoted type just to make this interesting by making PM different from TM. + int16_t min_i16 = 0; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<1'000'000>() / mag<999'999>(), + min_i16); + + ASSERT_TRUE((std::is_same, int32_t>::value)) + << "This test will fail on architectures where `int16_t` is not promoted to `int32_t`"; + EXPECT_EQ(min_i16, -2'147); +} + +TEST(MinNonOverflowingValue, IsTMinOverNTimesDWhenMoreConstrainingThanPMinOverN) { + // The goal is to engineer a test case where `(TM * D) / N` is more constraining than `PM / N`. + // Obviously, if `TM = PM`, then this can never be; therefore, we need to use a promotable type. + int16_t min_int16 = 0; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<1'000>() / mag<3>(), + min_int16); + ASSERT_EQ(-98, -32'768 * 3 / 1'000); + EXPECT_EQ(min_int16, -98); +} + +} // namespace +} // namespace detail +} // namespace au diff --git a/au/quantity_test.cc b/au/quantity_test.cc index feebdf94..553ce3eb 100644 --- a/au/quantity_test.cc +++ b/au/quantity_test.cc @@ -33,10 +33,16 @@ constexpr auto miles = QuantityMaker{}; struct Inches : decltype(Feet{} / mag<12>()) {}; constexpr auto inches = QuantityMaker{}; -struct Yards : decltype(Feet{} * mag<3>()) {}; +struct Yards : decltype(Feet{} * mag<3>()) { + static constexpr const char label[] = "yd"; +}; +constexpr const char Yards::label[]; constexpr auto yards = QuantityMaker{}; -struct Meters : decltype(Inches{} * mag<100>() / mag<254>() * mag<100>()) {}; +struct Meters : decltype(Inches{} * mag<100>() / mag<254>() * mag<100>()) { + static constexpr const char label[] = "m"; +}; +constexpr const char Meters::label[]; static constexpr QuantityMaker meters{}; static_assert(are_units_quantity_equivalent(Centi{} * mag<254>(), Inches{} * mag<100>()), "Double-check this ad hoc definition of meters"); @@ -750,7 +756,29 @@ TEST(IsConversionLossy, CorrectlyDiscriminatesBetweenLossyAndLosslessConversions ASSERT_FALSE(is_inverse_lossy); } - EXPECT_EQ(is_lossy, did_value_change); + std::string reason{}; + if (is_lossy) { + const bool truncates = will_conversion_truncate(original, target_units); + const bool overflows = will_conversion_overflow(original, target_units); + ASSERT_TRUE(truncates || overflows); + reason = std::string{" ("} + [&] { + if (truncates && overflows) { + return "truncates and overflows"; + } else if (truncates) { + return "truncates"; + } else if (overflows) { + return "overflows"; + } else { + return ""; + } + }() + ")"; + } + + EXPECT_EQ(is_lossy, did_value_change) + << "Conversion " << (is_lossy ? "is" : "is not") << " lossy" << reason + << ", but round-trip conversion " << (did_value_change ? "did" : "did not") + << " change the value. original: " << original << ", converted: " << converted + << ", round_trip: " << round_trip; } }; @@ -759,6 +787,10 @@ TEST(IsConversionLossy, CorrectlyDiscriminatesBetweenLossyAndLosslessConversions // Feet-to-inches tests overflow. test_round_trip_for_every_uint16_value(feet, inches); + + // Yards-to-meters (and vice versa) tests truncation and overflow. + test_round_trip_for_every_uint16_value(yards, meters); + test_round_trip_for_every_uint16_value(meters, yards); } TEST(AreQuantityTypesEquivalent, RequiresSameRepAndEquivalentUnits) {