From e50d75a104893ae4a4a27e5d98240265d9526471 Mon Sep 17 00:00:00 2001 From: Mateusz Pusz Date: Mon, 26 Feb 2024 09:30:08 +0100 Subject: [PATCH] feat: dimension text output added Resolves #421 --- src/core/include/mp-units/bits/text_tools.h | 76 ++++++++++++- src/core/include/mp-units/dimension.h | 113 ++++++++++++++++++-- src/core/include/mp-units/format.h | 87 +++++++++++++-- src/core/include/mp-units/ostream.h | 67 ++++++------ src/core/include/mp-units/unit.h | 50 +-------- test/static/CMakeLists.txt | 1 + test/static/dimension_symbol_test.cpp | 52 +++++++++ test/static/unit_symbol_test.cpp | 5 +- 8 files changed, 349 insertions(+), 102 deletions(-) create mode 100644 test/static/dimension_symbol_test.cpp diff --git a/src/core/include/mp-units/bits/text_tools.h b/src/core/include/mp-units/bits/text_tools.h index 40fe0d62..4c61b7e5 100644 --- a/src/core/include/mp-units/bits/text_tools.h +++ b/src/core/include/mp-units/bits/text_tools.h @@ -23,9 +23,12 @@ #pragma once #include +#include #include -namespace mp_units::detail { +namespace mp_units { + +namespace detail { template requires(0 <= Value) && (Value < 10) @@ -84,4 +87,73 @@ template return regular() + regular(); } -} // namespace mp_units::detail +} // namespace detail + +enum class text_encoding : std::int8_t { + unicode, // m³; µs + ascii, // m^3; us + default_encoding = unicode +}; + +namespace detail { + +template Out> +constexpr Out copy(const basic_symbol_text& txt, text_encoding encoding, Out out) +{ + if (encoding == text_encoding::unicode) { + if constexpr (is_same_v) + return copy(txt.unicode(), out).out; + else if constexpr (is_same_v) { + for (char8_t ch : txt.unicode()) *out++ = static_cast(ch); + return out; + } else + throw std::invalid_argument("Unicode text can't be copied to CharT output"); + } else { + if constexpr (is_same_v) + return copy(txt.ascii(), out).out; + else + throw std::invalid_argument("ASCII text can't be copied to CharT output"); + } +} + +template Out> +constexpr Out copy_symbol(const basic_symbol_text& txt, text_encoding encoding, bool negative_power, Out out) +{ + out = copy(txt, encoding, out); + if (negative_power) { + constexpr auto exp = superscript<-1>(); + out = copy(exp, encoding, out); + } + return out; +} + +template Out> +constexpr Out copy_symbol_exponent(text_encoding encoding, bool negative_power, Out out) +{ + constexpr ratio r{Num, Den...}; + if constexpr (r.den != 1) { + // add root part + if (negative_power) { + constexpr auto txt = basic_symbol_text("^-(") + regular() + basic_symbol_text("/") + regular() + + basic_symbol_text(")"); + return copy(txt, encoding, out); + } else { + constexpr auto txt = + basic_symbol_text("^(") + regular() + basic_symbol_text("/") + regular() + basic_symbol_text(")"); + return copy(txt, encoding, out); + } + } else if constexpr (r.num != 1) { + // add exponent part + if (negative_power) { + constexpr auto txt = superscript<-r.num>(); + return copy(txt, encoding, out); + } else { + constexpr auto txt = superscript(); + return copy(txt, encoding, out); + } + } +} + +} // namespace detail + +} // namespace mp_units diff --git a/src/core/include/mp-units/dimension.h b/src/core/include/mp-units/dimension.h index 88d78792..4ca3b28b 100644 --- a/src/core/include/mp-units/dimension.h +++ b/src/core/include/mp-units/dimension.h @@ -27,6 +27,7 @@ #include #include #include +#include namespace mp_units { @@ -164,13 +165,15 @@ template } // namespace detail -MP_UNITS_EXPORT template +MP_UNITS_EXPORT_BEGIN + +template [[nodiscard]] consteval bool operator==(Lhs lhs, Rhs rhs) { return is_same_v || detail::derived_from_the_same_base_dimension(lhs, rhs); } -MP_UNITS_EXPORT [[nodiscard]] consteval Dimension auto inverse(Dimension auto d) { return dimension_one / d; } +[[nodiscard]] consteval Dimension auto inverse(Dimension auto d) { return dimension_one / d; } /** * @brief Computes the value of a dimension raised to the `Num/Den` power @@ -181,7 +184,7 @@ MP_UNITS_EXPORT [[nodiscard]] consteval Dimension auto inverse(Dimension auto d) * * @return Dimension The result of computation */ -MP_UNITS_EXPORT template +template requires detail::non_zero [[nodiscard]] consteval Dimension auto pow(D d) { @@ -202,7 +205,7 @@ MP_UNITS_EXPORT template * * @return Dimension The result of computation */ -MP_UNITS_EXPORT [[nodiscard]] consteval Dimension auto sqrt(Dimension auto d) { return pow<1, 2>(d); } +[[nodiscard]] consteval Dimension auto sqrt(Dimension auto d) { return pow<1, 2>(d); } /** * @brief Computes the cubic root of a dimension @@ -211,9 +214,107 @@ MP_UNITS_EXPORT [[nodiscard]] consteval Dimension auto sqrt(Dimension auto d) { * * @return Dimension The result of computation */ -MP_UNITS_EXPORT [[nodiscard]] consteval Dimension auto cbrt(Dimension auto d) { return pow<1, 3>(d); } +[[nodiscard]] consteval Dimension auto cbrt(Dimension auto d) { return pow<1, 3>(d); } -// TODO consider adding the support for text output of the dimensional equation +struct dimension_symbol_formatting { + text_encoding encoding = text_encoding::default_encoding; +}; + +MP_UNITS_EXPORT_END + +namespace detail { + +template Out, Dimension D> + requires requires { D::symbol; } +constexpr Out dimension_symbol_impl(Out out, D, dimension_symbol_formatting fmt, bool negative_power) +{ + return copy_symbol(D::symbol, fmt.encoding, negative_power, out); +} + +template Out, typename F, int Num, int... Den> +constexpr auto dimension_symbol_impl(Out out, const power&, dimension_symbol_formatting fmt, + bool negative_power) +{ + out = dimension_symbol_impl(out, F{}, fmt, false); // negative power component will be added below if needed + return copy_symbol_exponent(fmt.encoding, negative_power, out); +} + +template Out, DerivedDimensionExpr... Ms> +constexpr Out dimension_symbol_impl(Out out, const type_list&, dimension_symbol_formatting fmt, + bool negative_power) +{ + return (..., (out = dimension_symbol_impl(out, Ms{}, fmt, negative_power))); +} + +template Out, DerivedDimensionExpr... Nums, DerivedDimensionExpr... Dens> +constexpr Out dimension_symbol_impl(Out out, const type_list& nums, const type_list& dens, + dimension_symbol_formatting fmt) +{ + if constexpr (sizeof...(Nums) == 0 && sizeof...(Dens) == 0) { + // dimensionless quantity + *out++ = '1'; + return out; + } else if constexpr (sizeof...(Dens) == 0) { + // no denominator + return dimension_symbol_impl(out, nums, fmt, false); + } else { + if constexpr (sizeof...(Nums) > 0) out = dimension_symbol_impl(out, nums, fmt, false); + return dimension_symbol_impl(out, dens, fmt, true); + } +} + +template Out, typename... Expr> +constexpr Out dimension_symbol_impl(Out out, const derived_dimension&, dimension_symbol_formatting fmt, + bool negative_power) +{ + gsl_Expects(negative_power == false); + return dimension_symbol_impl(out, typename derived_dimension::_num_{}, + typename derived_dimension::_den_{}, fmt); +} + + +} // namespace detail + +MP_UNITS_EXPORT template Out, Dimension D> +constexpr Out dimension_symbol_to(Out out, D d, dimension_symbol_formatting fmt = dimension_symbol_formatting{}) +{ + return detail::dimension_symbol_impl(out, d, fmt, false); +} + +namespace detail { + +template +[[nodiscard]] consteval std::array get_symbol_buffer(D) +{ + std::array buffer{}; + dimension_symbol_to(buffer.begin(), D{}, fmt); + return buffer; +} + +} // namespace detail + + +// TODO Refactor to `dimension_symbol(D, fmt)` when P1045: constexpr Function Parameters is available +MP_UNITS_EXPORT template +[[nodiscard]] consteval auto dimension_symbol(D) +{ + auto get_size = []() consteval { + std::basic_string buffer; + dimension_symbol_to(std::back_inserter(buffer), D{}, fmt); + return buffer.size(); + }; + +#if __cpp_constexpr >= 202211L // Permitting static constexpr variables in constexpr functions + static constexpr std::size_t size = get_size(); + static constexpr auto buffer = detail::get_symbol_buffer(D{}); + return std::string_view(buffer.data(), size); +#else + constexpr std::size_t size = get_size(); + constexpr auto buffer = detail::get_symbol_buffer(D{}); + return basic_fixed_string(buffer.data(), std::integral_constant{}); +#endif +} } // namespace mp_units diff --git a/src/core/include/mp-units/format.h b/src/core/include/mp-units/format.h index 79195f2f..2d81027e 100644 --- a/src/core/include/mp-units/format.h +++ b/src/core/include/mp-units/format.h @@ -125,9 +125,80 @@ OutputIt format_global_buffer(OutputIt out, const fill_align_width_format_specs< // dimension-spec ::= [text-encoding] // text-encoding ::= 'U' | 'A' // +template +class MP_UNITS_STD_FMT::formatter { + struct format_specs : mp_units::detail::fill_align_width_format_specs, mp_units::dimension_symbol_formatting {}; -// template -// struct dimension_format_specs : fill_align_width_format_specs, dimension_symbol_formatting {}; + std::basic_string_view fill_align_width_format_str_; + std::basic_string_view modifiers_format_str_; + format_specs specs_{}; + + struct format_checker { + using enum mp_units::text_encoding; + mp_units::text_encoding encoding = unicode; + constexpr void on_text_encoding(Char val) { encoding = (val == 'U') ? unicode : ascii; } + }; + + struct unit_formatter { + format_specs specs; + using enum mp_units::text_encoding; + constexpr void on_text_encoding(Char val) { specs.encoding = (val == 'U') ? unicode : ascii; } + }; + + template + constexpr const Char* parse_dimension_specs(const Char* begin, const Char* end, Handler&& handler) const + { + auto it = begin; + if (it == end || *it == '}') return begin; + + constexpr auto valid_modifiers = std::string_view{"UA"}; + for (; it != end && *it != '}'; ++it) { + if (valid_modifiers.find(*it) == std::string_view::npos) + throw MP_UNITS_STD_FMT::format_error("invalid dimension modifier specified"); + } + end = it; + + if (it = mp_units::detail::at_most_one_of(begin, end, "UA"); it != end) handler.on_text_encoding(*it); + return end; + } + +public: + constexpr auto parse(MP_UNITS_STD_FMT::basic_format_parse_context& ctx) -> decltype(ctx.begin()) + { + const auto begin = ctx.begin(); + auto end = ctx.end(); + + auto it = parse_fill_align_width(ctx, begin, end, specs_); + fill_align_width_format_str_ = {begin, it}; + if (it == end) return it; + + format_checker checker; + end = parse_dimension_specs(it, end, checker); + modifiers_format_str_ = {it, end}; + return end; + } + + template + auto format(const D& d, FormatContext& ctx) const -> decltype(ctx.out()) + { + unit_formatter f{specs_}; + mp_units::detail::handle_dynamic_spec(f.specs.width, f.specs.width_ref, ctx); + + parse_dimension_specs(modifiers_format_str_.begin(), modifiers_format_str_.end(), f); + + if (f.specs.width == 0) { + // Avoid extra copying if width is not specified + return mp_units::dimension_symbol_to(ctx.out(), d, f.specs); + } else { + std::basic_string unit_buffer; + mp_units::dimension_symbol_to(std::back_inserter(unit_buffer), d, f.specs); + + std::basic_string global_format_buffer = "{:" + std::basic_string{fill_align_width_format_str_} + "}"; + return MP_UNITS_STD_FMT::vformat_to(ctx.out(), global_format_buffer, + MP_UNITS_STD_FMT::make_format_args(unit_buffer)); + } + } +}; // @@ -297,10 +368,9 @@ class MP_UNITS_STD_FMT::formatter, Char> { return on_replacement_field(begin); else if (id == "U") return on_replacement_field(begin); - else if (id == "D") { - return begin; - // on_replacement_field(begin); - } else + else if (id == "D") + return on_replacement_field(begin); + else throw MP_UNITS_STD_FMT::format_error("unknown replacement field '" + std::string(id) + "'"); } @@ -337,7 +407,10 @@ class MP_UNITS_STD_FMT::formatter, Char> { { out = MP_UNITS_STD_FMT::vformat_to(out, locale, format_str, MP_UNITS_STD_FMT::make_format_args(q.unit)); } - void on_dimension(std::basic_string_view) {} + void on_dimension(std::basic_string_view format_str) + { + out = MP_UNITS_STD_FMT::vformat_to(out, locale, format_str, MP_UNITS_STD_FMT::make_format_args(q.dimension)); + } void on_text(const Char* begin, const Char* end) const { std::copy(begin, end, out); } constexpr const Char* on_replacement_field(std::basic_string_view id, const Char* begin) diff --git a/src/core/include/mp-units/ostream.h b/src/core/include/mp-units/ostream.h index 4bf25669..cbd816d8 100644 --- a/src/core/include/mp-units/ostream.h +++ b/src/core/include/mp-units/ostream.h @@ -35,14 +35,21 @@ namespace mp_units { namespace detail { +template +void to_stream_impl(std::basic_ostream& os, D d) +{ + dimension_symbol_to(std::ostream_iterator(os), d); +} + template -void to_stream(std::basic_ostream& os, U u) +void to_stream_impl(std::basic_ostream& os, U u) { unit_symbol_to(std::ostream_iterator(os), u); } template -void to_stream(std::basic_ostream& os, const quantity& q) +void to_stream_impl(std::basic_ostream& os, const quantity& q) + requires requires { os << q.numerical_value_ref_in(q.unit); } { if constexpr (is_same_v || is_same_v) // promote the value to int @@ -50,46 +57,36 @@ void to_stream(std::basic_ostream& os, const quantity& q) else os << q.numerical_value_ref_in(q.unit); if constexpr (space_before_unit_symbol) os << " "; - to_stream(os, get_unit(R)); + to_stream_impl(os, get_unit(R)); +} + +template +std::basic_ostream& to_stream(std::basic_ostream& os, const T& v) + requires requires { detail::to_stream_impl(os, v); } +{ + if (os.width()) { + // std::setw() applies to the whole output so it has to be first put into std::string + std::basic_ostringstream oss; + oss.flags(os.flags()); + oss.imbue(os.getloc()); + oss.precision(os.precision()); + detail::to_stream_impl(oss, v); + return os << std::move(oss).str(); + } + + detail::to_stream_impl(os, v); + return os; } } // namespace detail MP_UNITS_EXPORT_BEGIN -template -std::basic_ostream& operator<<(std::basic_ostream& os, U u) +template +std::basic_ostream& operator<<(std::basic_ostream& os, const T& v) + requires requires { detail::to_stream_impl(os, v); } { - if (os.width()) { - // std::setw() applies to the whole uni output so it has to be first put into std::string - std::basic_ostringstream oss; - oss.flags(os.flags()); - oss.imbue(os.getloc()); - oss.precision(os.precision()); - detail::to_stream(oss, u); - return os << std::move(oss).str(); - } - - detail::to_stream(os, u); - return os; -} - -template -std::basic_ostream& operator<<(std::basic_ostream& os, const quantity& q) - requires requires { os << q.numerical_value_ref_in(q.unit); } -{ - if (os.width()) { - // std::setw() applies to the whole quantity output so it has to be first put into std::string - std::basic_ostringstream oss; - oss.flags(os.flags()); - oss.imbue(os.getloc()); - oss.precision(os.precision()); - detail::to_stream(oss, q); - return os << std::move(oss).str(); - } - - detail::to_stream(os, q); - return os; + return detail::to_stream(os, v); } MP_UNITS_EXPORT_END diff --git a/src/core/include/mp-units/unit.h b/src/core/include/mp-units/unit.h index b67f16e7..d6992469 100644 --- a/src/core/include/mp-units/unit.h +++ b/src/core/include/mp-units/unit.h @@ -682,12 +682,6 @@ inline constexpr bool space_before_unit_symbol = false; // get_unit_symbol -enum class text_encoding : std::int8_t { - unicode, // m³; µs - ascii, // m^3; us - default_encoding = unicode -}; - enum class unit_symbol_solidus : std::int8_t { one_denominator, // m/s; kg m⁻¹ s⁻¹ always, // m/s; kg/(m s) @@ -711,25 +705,6 @@ MP_UNITS_EXPORT_END namespace detail { -template Out> -constexpr Out copy(const basic_symbol_text& txt, text_encoding encoding, Out out) -{ - if (encoding == text_encoding::unicode) { - if constexpr (is_same_v) - return copy(txt.unicode(), out).out; - else if constexpr (is_same_v) { - for (char8_t ch : txt.unicode()) *out++ = static_cast(ch); - return out; - } else - throw std::invalid_argument("Unicode text can't be copied to CharT output"); - } else { - if constexpr (is_same_v) - return copy(txt.ascii(), out).out; - else - throw std::invalid_argument("ASCII text can't be copied to CharT output"); - } -} - template Out> constexpr Out print_separator(Out out, unit_symbol_formatting fmt) { @@ -748,12 +723,7 @@ template Out, Unit U> requires requires { U::symbol; } constexpr Out unit_symbol_impl(Out out, U, unit_symbol_formatting fmt, bool negative_power) { - out = copy(U::symbol, fmt.encoding, out); - if (negative_power) { - constexpr auto txt = superscript<-1>(); - out = copy(txt, fmt.encoding, out); - } - return out; + return copy_symbol(U::symbol, fmt.encoding, negative_power, out); } template Out, auto M, typename U> @@ -775,23 +745,7 @@ template Out, typename F, int Num, i constexpr auto unit_symbol_impl(Out out, const power&, unit_symbol_formatting fmt, bool negative_power) { out = unit_symbol_impl(out, F{}, fmt, false); // negative power component will be added below if needed - - constexpr ratio r = power::exponent; - if constexpr (r.den != 1) { - // add root part - constexpr auto txt = - basic_symbol_text("^(") + regular() + basic_symbol_text("/") + regular() + basic_symbol_text(")"); - return copy(txt, fmt.encoding, out); - } else if constexpr (r.num != 1) { - // add exponent part - if (negative_power) { - constexpr auto txt = superscript<-r.num>(); - return copy(txt, fmt.encoding, out); - } else { - constexpr auto txt = superscript(); - return copy(txt, fmt.encoding, out); - } - } + return copy_symbol_exponent(fmt.encoding, negative_power, out); } template Out, DerivedUnitExpr M> diff --git a/test/static/CMakeLists.txt b/test/static/CMakeLists.txt index cbfeb9ea..21fd9711 100644 --- a/test/static/CMakeLists.txt +++ b/test/static/CMakeLists.txt @@ -38,6 +38,7 @@ add_library( # custom_rep_test_min_expl.cpp custom_rep_test_min_impl.cpp dimension_test.cpp + dimension_symbol_test.cpp fixed_string_test.cpp fractional_exponent_quantity.cpp hep_test.cpp diff --git a/test/static/dimension_symbol_test.cpp b/test/static/dimension_symbol_test.cpp new file mode 100644 index 00000000..048ac692 --- /dev/null +++ b/test/static/dimension_symbol_test.cpp @@ -0,0 +1,52 @@ +// The MIT License (MIT) +// +// Copyright (c) 2018 Mateusz Pusz +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include + +namespace { + +using namespace mp_units; + +using enum text_encoding; + +static_assert(dimension_symbol(dimension_one) == "1"); + +// base dimensions +static_assert(dimension_symbol(isq::dim_length) == "L"); +static_assert(dimension_symbol(isq::dim_thermodynamic_temperature) == "Θ"); +static_assert(dimension_symbol(isq::dim_thermodynamic_temperature) == + "O"); + +// derived dimensions +static_assert(dimension_symbol(isq::speed.dimension) == "LT⁻¹"); +static_assert(dimension_symbol(isq::speed.dimension) == "LT^-1"); +static_assert(dimension_symbol(isq::power.dimension) == "L²MT⁻³"); +static_assert(dimension_symbol(isq::power.dimension) == "L^2MT^-3"); + +static_assert(dimension_symbol(pow<123>(isq::dim_length)) == "L¹²³"); +static_assert(dimension_symbol(pow<1, 2>(isq::dim_length)) == "L^(1/2)"); +static_assert(dimension_symbol(pow<3, 5>(isq::dim_length)) == "L^(3/5)"); +static_assert(dimension_symbol(pow<123>(isq::speed.dimension)) == "L¹²³T⁻¹²³"); +static_assert(dimension_symbol(pow<1, 2>(isq::speed.dimension)) == "L^(1/2)T^-(1/2)"); +static_assert(dimension_symbol(pow<3, 5>(isq::speed.dimension)) == "L^(3/5)T^-(3/5)"); + +} // namespace diff --git a/test/static/unit_symbol_test.cpp b/test/static/unit_symbol_test.cpp index c168cb14..df995d48 100644 --- a/test/static/unit_symbol_test.cpp +++ b/test/static/unit_symbol_test.cpp @@ -30,8 +30,6 @@ using namespace mp_units; using namespace mp_units::si; using namespace mp_units::iec80000; -#if __cpp_lib_constexpr_string && (!defined MP_UNITS_COMP_GCC || MP_UNITS_COMP_GCC > 11) - using enum text_encoding; using enum unit_symbol_solidus; using enum unit_symbol_separator; @@ -178,6 +176,7 @@ static_assert(unit_symbol(pow<123>(metre)) == "m¹²³"); static_assert(unit_symbol(pow<1, 2>(metre)) == "m^(1/2)"); static_assert(unit_symbol(pow<3, 5>(metre)) == "m^(3/5)"); static_assert(unit_symbol(pow<1, 2>(metre / second)) == "m^(1/2)/s^(1/2)"); +static_assert(unit_symbol(pow<1, 2>(metre / second)) == "m^(1/2) s^-(1/2)"); // dimensionless unit static_assert(unit_symbol(radian) == "rad"); @@ -188,6 +187,4 @@ static_assert(unit_symbol(gram * standard_gravity * si2019::speed_of_light_in_va static_assert(unit_symbol(gram / standard_gravity) == "g/g₀"); static_assert(unit_symbol(kilo / second / mega) == "km Mpc⁻¹ s⁻¹"); -#endif // __cpp_lib_constexpr_string - } // namespace