diff --git a/docs/users_guide/framework_basics/systems_of_units.md b/docs/users_guide/framework_basics/systems_of_units.md index 7b1e3c10..9e1cf9f0 100644 --- a/docs/users_guide/framework_basics/systems_of_units.md +++ b/docs/users_guide/framework_basics/systems_of_units.md @@ -260,3 +260,31 @@ This is why we provide both versions of identifiers for such units. quantity resistance = 60 * kΩ; quantity capacitance = 100 * µF; ``` + + +## Common units + +Adding or subtracting two quantities of different units will force the library to find a common +unit for those. This is to prevent data truncation. For the cases when one of the units is an +integral multiple of the another, the resulting quantity will use a "smaller" one in its result. +For example: + +```cpp +static_assert((1 * kg + 1 * g).unit == g); +static_assert((1 * km + 1 * mm).unit == mm); +static_assert((1 * yd + 1 * mi).unit == yd); +``` + +However, in many cases an arithmetic on quantities of different units will result in a yet another +unit. This happens when none of the source units is an integral multiple of another. In such cases, +the library returns a special type that denotes that we are dealing with a common unit of such +an equation: + +```cpp +quantity q = 1 * km + 1 * mi; // quantity>{}, int> +``` + +!!! note + + A user should never explicitly instantiate a `common_unit` class template. The library's + framework will do it based on the provided quantity equation. diff --git a/docs/users_guide/framework_basics/text_output.md b/docs/users_guide/framework_basics/text_output.md index 17a45a4f..9dfee718 100644 --- a/docs/users_guide/framework_basics/text_output.md +++ b/docs/users_guide/framework_basics/text_output.md @@ -268,6 +268,31 @@ The above prints: kg⋅m⋅s⁻² ``` +## Symbols of common units + +Some [common units](systems_of_units.md#common-units) expressed with a specialization of the +`common_unit` class template need special printing rules for their symbols. As they represent +a minimum set of common units resulting from the addition or subtraction of multiple quantities, +we print all of them as a scaled version of the source unit. For example the following: + +```cpp +std::cout << 1 * km + 1 * mi << "\n"; +std::cout << 1 * nmi + 1 * mi << "\n"; +std::cout << 1 * km / h + 1 * m / s << "\n"; +``` + +will print: + +```text +40771 ([1/25146] mi = [1/15625] km) +108167 ([1/50292] mi = [1/57875] nmi) +23 ([1/5] km/h = [1/18] m/s) +``` + +Thanks to the above, it might be easier for the user to reason about the magnitude of the resulting +unit and its impact on the value stored in the quantity. + + ## `space_before_unit_symbol` customization point The [SI Brochure](../../appendix/references.md#SIBrochure) says: diff --git a/src/core/include/mp-units/framework/unit.h b/src/core/include/mp-units/framework/unit.h index ed79be9d..46b56436 100644 --- a/src/core/include/mp-units/framework/unit.h +++ b/src/core/include/mp-units/framework/unit.h @@ -50,6 +50,7 @@ import std; #include #include #include +#include #if MP_UNITS_HOSTED #include #endif @@ -404,6 +405,46 @@ struct prefixed_unit : decltype(M * U)::_base_type_ { namespace detail { +template + requires(convertible(U1{}, U2{})) +[[nodiscard]] consteval Unit auto get_common_scaled_unit(U1, U2) +{ + constexpr auto canonical_lhs = get_canonical_unit(U1{}); + constexpr auto canonical_rhs = get_canonical_unit(U2{}); + constexpr auto common_magnitude = _common_magnitude(canonical_lhs.mag, canonical_rhs.mag); + return scaled_unit{}; +} + +[[nodiscard]] consteval Unit auto get_common_scaled_unit(Unit auto u1, Unit auto u2, Unit auto u3, Unit auto... rest) + requires requires { get_common_scaled_unit(get_common_scaled_unit(u1, u2), u3, rest...); } +{ + return get_common_scaled_unit(get_common_scaled_unit(u1, u2), u3, rest...); +} + +} // namespace detail + +/** + * @brief Measurement unit for an accumulation of two quantities of different units + * + * While adding two quantities of different units we can often identify which of those unit should be used + * to prevent data truncation. For example, adding `1 * m + 1 * mm` will end up in a quantity expressed in + * millimeters. However, for some cases this is not possible. Choosing any of the units from the arguments + * of the addition would result in a data truncation. For example, a common unit for `1 * km + 1 * mi` is + * `[8/125] m`. Instead of returning such a complex unit type the library will return a `common_unit`. + * This type is convertible to both `mi` and `km` without risking data truncation, but is not equal to any + * of them. + * + * @note User should not instantiate this type! It is not exported from the C++ module. The library will + * instantiate this type automatically based on the unit arithmetic equation provided by the user. + */ +template +struct common_unit final : decltype(detail::get_common_scaled_unit(U1{}, U2{}, Rest{}...))::_base_type_ +{ + using _base_type_ = common_unit; // exposition only +}; + +namespace detail { + template struct is_one : std::false_type {}; @@ -648,12 +689,62 @@ template else if constexpr (is_integral(canonical_rhs.mag / canonical_lhs.mag)) return u1; else { - constexpr auto common_magnitude = _common_magnitude(canonical_lhs.mag, canonical_rhs.mag); - return scaled_unit{}; + if constexpr (detail::unit_less::value) + return common_unit{}; + else + return common_unit{}; } } } +namespace detail { + +template +struct collapse_common_unit_impl; + +template +struct collapse_common_unit_impl { + using cu = decltype(get_common_unit(NewUnit{}, Front{})); + using type = + conditional, + typename collapse_common_unit_impl, NewUnit, Included, Rest...>::type, + typename collapse_common_unit_impl, NewUnit, true, Rest...>::type>; +}; + +template +struct collapse_common_unit_impl { + using type = type_list_push_back; +}; + +template +struct collapse_common_unit_impl { + using type = List; +}; + +template +using collapse_common_unit = type_list_unique< + type_list_sort, NewUnit, false, Us...>::type, type_list_of_unit_less>>; + +} // namespace detail + +template + requires(convertible(common_unit{}, NewUnit{})) +[[nodiscard]] consteval Unit auto get_common_unit(common_unit, NewUnit) +{ + using type = detail::collapse_common_unit; + if constexpr (detail::type_list_size == 1) + return detail::type_list_front{}; + else + return detail::type_list_map{}; +} + +template + requires(convertible(common_unit{}, NewUnit{})) +[[nodiscard]] consteval Unit auto get_common_unit(NewUnit nu, common_unit cu) +{ + return get_common_unit(cu, nu); +} + [[nodiscard]] consteval Unit auto get_common_unit(Unit auto u1, Unit auto u2, Unit auto u3, Unit auto... rest) requires requires { get_common_unit(get_common_unit(u1, u2), u3, rest...); } { @@ -743,6 +834,33 @@ constexpr Out unit_symbol_impl(Out out, const scaled_unit_impl& u, const u } } +template +[[nodiscard]] consteval Unit auto get_common_unit_in(common_unit, U u) +{ + constexpr auto canonical_u = get_canonical_unit(u); + constexpr Magnitude auto mag = common_unit::mag / canonical_u.mag; + return scaled_unit{}; +} + +template Out, typename U, typename... Rest> +constexpr Out unit_symbol_impl(Out out, const common_unit&, const unit_symbol_formatting& fmt, + bool negative_power) +{ + constexpr std::string_view separator(" = "); + auto print_unit = [&](Arg) { + constexpr auto u = get_common_unit_in(common_unit{}, Arg{}); + unit_symbol_impl(out, u, fmt, negative_power); + }; + *out++ = '('; + print_unit(U{}); + for_each(std::tuple{}, [&](Arg) { + detail::copy(std::begin(separator), std::end(separator), out); + print_unit(Arg{}); + }); + *out++ = ')'; + return out; +} + template Out, typename F, int Num, int... Den> constexpr auto unit_symbol_impl(Out out, const power&, const unit_symbol_formatting& fmt, bool negative_power) diff --git a/test/static/unit_symbol_test.cpp b/test/static/unit_symbol_test.cpp index c87831be..ba3c37dc 100644 --- a/test/static/unit_symbol_test.cpp +++ b/test/static/unit_symbol_test.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #ifdef MP_UNITS_IMPORT_STD import std; @@ -34,6 +35,7 @@ namespace { using namespace mp_units; using namespace mp_units::si; using namespace mp_units::iec; +using namespace mp_units::international; using enum text_encoding; using enum unit_symbol_solidus; @@ -114,6 +116,14 @@ static_assert(unit_symbol(mag<60> * second) == "[6 × 10¹] s"); static_assert(unit_symbol(mag<60> * second) == "[6 x 10^1] s"); static_assert(unit_symbol(mag_ratio<1, 18> * metre / second) == "[1/18] m/s"); +// common units +static_assert(unit_symbol(get_common_unit(kilo, mile)) == "([1/25146] mi = [1/15625] km)"); +static_assert(unit_symbol(get_common_unit(kilo / hour, metre / second)) == "([1/5] km/h = [1/18] m/s)"); +static_assert(unit_symbol(get_common_unit(kilo / hour, metre / second) / second) == + "([1/5] km/h = [1/18] m/s)/s"); +static_assert(unit_symbol(get_common_unit(kilo / hour, metre / second) * second) == + "([1/5] km/h = [1/18] m/s) s"); + // derived units static_assert(unit_symbol(one) == ""); // NOLINT(readability-container-size-empty) static_assert(unit_symbol(percent) == "%"); diff --git a/test/static/unit_test.cpp b/test/static/unit_test.cpp index 9b417ea0..5a82aef3 100644 --- a/test/static/unit_test.cpp +++ b/test/static/unit_test.cpp @@ -51,11 +51,17 @@ QUANTITY_SPEC_(mass, dim_mass); QUANTITY_SPEC_(time, dim_time); QUANTITY_SPEC_(thermodynamic_temperature, dim_thermodynamic_temperature); +// prefixes +template struct milli_ final : prefixed_unit<"m", mag_power<10, -3>, U{}> {}; +template struct kilo_ final : prefixed_unit<"k", mag_power<10, 3>, U{}> {}; +template constexpr milli_ milli; +template constexpr kilo_ kilo; + // base units inline constexpr struct second_ final : named_unit<"s", kind_of