diff --git a/src/include/units/bits/customization_points.h b/src/include/units/bits/customization_points.h new file mode 100644 index 00000000..acc33c3d --- /dev/null +++ b/src/include/units/bits/customization_points.h @@ -0,0 +1,115 @@ +// 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. + +#pragma once + +#include +#include + +namespace units { + + // treat_as_floating_point + + template // TODO Conceptify that + inline constexpr bool treat_as_floating_point = std::is_floating_point_v; + + // isnan + namespace isnan_impl { + + // non-ADL lookup block + void isnan(); // undefined + + template + inline constexpr bool has_customization = false; + + template + requires requires(const T& t) { + { isnan(t) } -> bool; + } + inline constexpr bool has_customization = true; + + struct fn { + template + constexpr bool operator()(const T&) const + { + return false; + } + + template + requires treat_as_floating_point + constexpr bool operator()(const T& value) const + { + return std::isnan(value); + } + + template + requires treat_as_floating_point && has_customization + constexpr bool operator()(const T& value) const + { + return isnan(value); // uses ADL + } + }; + } + + inline constexpr isnan_impl::fn isnan{}; + + // isfinite + namespace isfinite_impl { + + // non-ADL lookup block + void isfinite(); // undefined + + template + inline constexpr bool has_customization = false; + + template + requires requires(const T& t) { + { isfinite(t) } -> bool; + } + inline constexpr bool has_customization = true; + + struct fn { + template + constexpr bool operator()(const T&) const + { + return true; + } + + template + requires treat_as_floating_point + constexpr bool operator()(const T& value) const + { + return std::isfinite(value); + } + + template + requires treat_as_floating_point && has_customization + constexpr bool operator()(const T& value) const + { + return isfinite(value); // uses ADL + } + }; + } + + inline constexpr isfinite_impl::fn isfinite{}; + +} diff --git a/src/include/units/format.h b/src/include/units/format.h index d12cdb1a..df2d3d79 100644 --- a/src/include/units/format.h +++ b/src/include/units/format.h @@ -22,63 +22,347 @@ #pragma once +#include #include #include -// units-format-spec: -// fill-and-align[opt] width[opt] precision[opt] units-specs[opt] -// units-specs: -// conversion-spec -// units-specs conversion-spec -// units-specs literal-char -// literal-char: -// any character other than { or } -// conversion-spec: -// % modifier[opt] type -// modifier: one of -// E O -// type: one of -// q Q % +namespace units { + namespace detail { -template -struct fmt::formatter, CharT> { + // units-format-spec: + // fill-and-align[opt] width[opt] precision[opt] units-specs[opt] + // units-specs: + // conversion-spec + // units-specs conversion-spec + // units-specs literal-char + // literal-char: + // any character other than { or } + // conversion-spec: + // % modifier[opt] type + // modifier: one of + // E O + // type: one of + // n q Q t % + + template + constexpr const CharT* parse_units_format(const CharT* begin, const CharT* end, Handler&& handler) + { + auto ptr = begin; + while(ptr != end) { + auto c = *ptr; + if(c == '}') + break; + if(c != '%') { + ++ptr; + continue; + } + if (begin != ptr) + handler.on_text(begin, ptr); + ++ptr; // consume '%' + if(ptr == end) + throw fmt::format_error("invalid format"); + c = *ptr++; + switch(c) { + case '%': + handler.on_text(ptr - 1, ptr); + break; + case 'n': { + const char newline[] = "\n"; + handler.on_text(newline, newline + 1); + break; + } + case 't': { + const char tab[] = "\t"; + handler.on_text(tab, tab + 1); + break; + } + case 'Q': + handler.on_quantity_value(); + break; + case 'q': + handler.on_quantity_unit(); + break; + default: + throw fmt::format_error("invalid format"); + } + begin = ptr; + } + if(begin != ptr) + handler.on_text(begin, ptr); + return ptr; + } + + struct units_format_checker { + template + void on_text(const Char*, const Char*) {} + void on_quantity_value() {} + void on_quantity_unit() {} + }; + + template + inline OutputIt format_units_quantity_value(OutputIt out, const Rep& val, int precision) + { + if(precision >= 0) + return format_to(out, "{:.{}f}", val, precision); + return format_to(out, treat_as_floating_point ? "{:g}" : "{}", val); + } + + template + inline static OutputIt format_units_quantity_unit(OutputIt out) + { + return format_to(out, "{}", unit_text().c_str()); + } + + // If T is an integral type, maps T to its unsigned counterpart, otherwise + // leaves it unchanged (unlike std::make_unsigned). + template> + struct make_unsigned_or_unchanged { + using type = T; + }; + + template + struct make_unsigned_or_unchanged { + using type = std::make_unsigned_t; + }; + + // converts value to int and checks that it's in the range [0, upper). + template + requires std::is_integral_v + inline int to_nonnegative_int(T value, int upper) + { + FMT_ASSERT(value >= 0 && value <= upper, "invalid value"); + (void)upper; + return static_cast(value); + } + + template + inline int to_nonnegative_int(T value, int upper) + { + FMT_ASSERT(units::isnan(value) || (value >= 0 && value <= static_cast(upper)), "invalid value"); + (void)upper; + return static_cast(value); + } + + template + struct units_formatter { + FormatContext& context; + OutputIt out; + // rep is unsigned to avoid overflow. + using rep = conditional && sizeof(Rep) < sizeof(int), + unsigned, typename make_unsigned_or_unchanged::type>; + bool negative = false; + rep val; + int precision; + + using char_type = FormatContext::char_type; + + explicit units_formatter(FormatContext& ctx, OutputIt o, quantity q, int prec): + context(ctx), out(o), negative(q.count() < 0), val(negative ? -q.count() : q.count()), precision(prec) + { + } + + // returns true if nan or inf, writes to out. + bool handle_nan_inf() + { + if(units::isfinite(val)) { + return false; + } + if(units::isnan(val)) { + write_nan(); + return true; + } + // must be +-inf + if(val > 0) { + write_pinf(); + } + else { + write_ninf(); + } + return true; + } + + void write_sign() + { + if(negative) { + *out++ = '-'; + negative = false; + } + } + + void write(Rep value, int width) + { + write_sign(); + if(units::isnan(value)) + return write_nan(); + fmt::internal::uint32_or_64_t n = fmt::internal::to_unsigned(to_nonnegative_int(value, std::numeric_limits::max())); + int num_digits = fmt::internal::count_digits(n); + if(width > num_digits) + out = std::fill_n(out, width - num_digits, '0'); + out = fmt::internal::format_decimal(out, n, num_digits); + } + + void write_nan() { std::copy_n("nan", 3, out); } + void write_pinf() { std::copy_n("inf", 3, out); } + void write_ninf() { std::copy_n("-inf", 4, out); } + + void on_text(const char_type* begin, const char_type* end) + { + std::copy(begin, end, out); + } + + void on_quantity_value() + { + if(handle_nan_inf()) + return; + write_sign(); + out = format_units_quantity_value(out, val, precision); + } + + void on_quantity_unit() + { + out = format_units_quantity_unit(out); + } + }; + + } // namespace detail + +} // namespace units + +template +struct fmt::formatter, CharT> { private: + using quantity = units::quantity; + using iterator = fmt::basic_parse_context::iterator; + using arg_ref_type = fmt::internal::arg_ref; + fmt::basic_format_specs specs; int precision = -1; - using arg_ref_type = fmt::internal::arg_ref; arg_ref_type width_ref; arg_ref_type precision_ref; - mutable basic_string_view format_str; - using quantity = units::quantity; + fmt::basic_string_view format_str; - // auto parse_unit_format() { - // if (s != ctx.end() && *s == 'q') { - // quantity = true; - // return ++s; - // } - // } + struct spec_handler { + formatter& f; + fmt::basic_parse_context& context; + fmt::basic_string_view format_str; + + template + constexpr arg_ref_type make_arg_ref(Id arg_id) + { + context.check_arg_id(arg_id); + return arg_ref_type(arg_id); + } + + constexpr arg_ref_type make_arg_ref(fmt::basic_string_view arg_id) + { + context.check_arg_id(arg_id); + const auto str_val = fmt::internal::string_view_metadata(format_str, arg_id); + return arg_ref_type(str_val); + } + + constexpr arg_ref_type make_arg_ref(internal::auto_id) + { + return arg_ref_type(context.next_arg_id()); + } + + void on_error(const char* msg) { throw fmt::format_error(msg); } + void on_fill(CharT fill) { f.specs.fill[0] = fill; } + void on_align(align_t align) { f.specs.align = align; } + void on_width(unsigned width) { f.specs.width = width; } + void on_precision(unsigned precision) { f.precision = precision; } + void end_precision() {} + + template + void on_dynamic_width(Id arg_id) + { + f.width_ref = make_arg_ref(arg_id); + } + + template void on_dynamic_precision(Id arg_id) + { + f.precision_ref = make_arg_ref(arg_id); + } + }; + + struct parse_range { + iterator begin; + iterator end; + }; + + constexpr parse_range do_parse(fmt::basic_parse_context& ctx) + { + auto begin = ctx.begin(), end = ctx.end(); + if(begin == end || *begin == '}') + return {begin, begin}; + + // handler to assign parsed data to formatter data members + spec_handler handler{*this, ctx, format_str}; + + // parse alignment + begin = fmt::internal::parse_align(begin, end, handler); + if(begin == end) + return {begin, begin}; + + // parse width + begin = fmt::internal::parse_width(begin, end, handler); + if(begin == end) + return {begin, begin}; + + // parse precision if a floating point + if(*begin == '.') { + if constexpr(units::treat_as_floating_point) + begin = fmt::internal::parse_precision(begin, end, handler); + else + handler.on_error("precision not allowed for integral quantity representation"); + } + + end = parse_units_format(begin, end, units::detail::units_format_checker()); + + return {begin, end}; + } public: constexpr auto parse(fmt::basic_parse_context& ctx) { - // [ctx.begin(), ctx.end()) is a range of CharTs containing format-specs, - // e.g. in format("{:%Q %q}", ...) it is "%Q %q}" (format string after ':') - // auto begin = ctx.begin(), end = ctx.end(); - // Look at do_parse in fmt/chrono.h and provide replacement for parse_chrono_format. - // fill-and-align_opt ... - // begin = fmt::internal::parse_align(begin, end, handler); - // parse_unit_format(); - return ctx.end(); + auto range = do_parse(ctx); + format_str = fmt::basic_string_view(&*range.begin, fmt::internal::to_unsigned(range.end - range.begin)); + return range.end; } - // format("{:{}}", 'x', 10) template - auto format(const units::quantity& q, FormatContext& ctx) + auto format(const units::quantity& q, FormatContext& ctx) { - // ctx.out() - output iterator you write to. - // auto s = format("{0:.{1}} {2}", q.count(), precision, unit(q)); - // return format_to(ctx.out(), "{:{}}", s, width); - return format_to(ctx.out(), "{} {}", q.count(), units::detail::unit_text().c_str()); + auto begin = format_str.begin(), end = format_str.end(); + + // TODO Avoid extra copying if width is not specified + fmt::basic_memory_buffer buf; + auto out = std::back_inserter(buf); + + // process dynamic width and precision + fmt::internal::handle_dynamic_spec(specs.width, width_ref, ctx, format_str.begin()); + fmt::internal::handle_dynamic_spec(precision, precision_ref, ctx, format_str.begin()); + + // deal with quantity content + if(begin == end || *begin == '}') { + // default format should print value followed by the unit separeted with 1 space + out = units::detail::format_units_quantity_value(out, q.count(), precision); + *out++ = CharT(' '); + units::detail::format_units_quantity_unit(out); + } + else { + // user provided format + units::detail::units_formatter f(ctx, out, q, precision); + parse_units_format(begin, end, f); + } + + // form a final text + using range = fmt::internal::output_range; + fmt::internal::basic_writer w(range(ctx.out())); + w.write(buf.data(), buf.size(), specs); + + return w.out(); + +// return format_to(ctx.out(), "{} {}", q.count(), units::detail::unit_text().c_str()); } }; diff --git a/test/unit_test/runtime/text_test.cpp b/test/unit_test/runtime/text_test.cpp index af522a8b..5a698e45 100644 --- a/test/unit_test/runtime/text_test.cpp +++ b/test/unit_test/runtime/text_test.cpp @@ -31,6 +31,7 @@ #include using namespace units; +using namespace Catch::Matchers; TEST_CASE("operator<< on a quantity", "[text][ostream][fmt]") { @@ -562,6 +563,180 @@ TEST_CASE("operator<< on a quantity", "[text][ostream][fmt]") } } +TEST_CASE("format string with only %Q should print quantity value only", "[text][fmt]") +{ + SECTION("integral representation") + { + REQUIRE(fmt::format("{:%Q}", 123kmph) == "123"); + } + + SECTION("floating-point representation") + { + SECTION("no precision specification") + { + REQUIRE(fmt::format("{:%Q}", 221.km / 2h) == "110.5"); + } + } +} + +TEST_CASE("format string with only %q should print quantity unit symbol only", "[text][fmt]") +{ + REQUIRE(fmt::format("{:%q}", 123kmph) == "km/h"); +} + +TEST_CASE("%q an %Q can be put anywhere in a format string", "[text][fmt]") +{ + SECTION("no space") + { + REQUIRE(fmt::format("{:%Q%q}", 123kmph) == "123km/h"); + } + + SECTION("separator") + { + REQUIRE(fmt::format("{:%Q###%q}", 123kmph) == "123###km/h"); + } + + SECTION("opposite order") + { + REQUIRE(fmt::format("{:%q %Q}", 123kmph) == "km/h 123"); + } +} + +TEST_CASE("precision specification", "[text][fmt]") +{ + SECTION("default format {} on a quantity") + { + SECTION("0") + { + REQUIRE(fmt::format("{:.0}", 1.2345m) == "1 m"); + } + + SECTION("1") + { + REQUIRE(fmt::format("{:.1}", 1.2345m) == "1.2 m"); + } + + SECTION("2") + { + REQUIRE(fmt::format("{:.2}", 1.2345m) == "1.23 m"); + } + + SECTION("3") + { + REQUIRE(fmt::format("{:.3}", 1.2345m) == "1.235 m"); + } + + SECTION("4") + { + REQUIRE(fmt::format("{:.4}", 1.2345m) == "1.2345 m"); + } + + SECTION("5") + { + REQUIRE(fmt::format("{:.5}", 1.2345m) == "1.23450 m"); + } + + SECTION("10") + { + REQUIRE(fmt::format("{:.10}", 1.2345m) == "1.2345000000 m"); + } + } + + SECTION("full format {:%Q %q} on a quantity") + { + SECTION("0") + { + REQUIRE(fmt::format("{:.0%Q %q}", 1.2345m) == "1 m"); + } + + SECTION("1") + { + REQUIRE(fmt::format("{:.1%Q %q}", 1.2345m) == "1.2 m"); + } + + SECTION("2") + { + REQUIRE(fmt::format("{:.2%Q %q}", 1.2345m) == "1.23 m"); + } + + SECTION("3") + { + REQUIRE(fmt::format("{:.3%Q %q}", 1.2345m) == "1.235 m"); + } + + SECTION("4") + { + REQUIRE(fmt::format("{:.4%Q %q}", 1.2345m) == "1.2345 m"); + } + + SECTION("5") + { + REQUIRE(fmt::format("{:.5%Q %q}", 1.2345m) == "1.23450 m"); + } + + SECTION("10") + { + REQUIRE(fmt::format("{:.10%Q %q}", 1.2345m) == "1.2345000000 m"); + } + } + + SECTION("value only format {:%Q} on a quantity") + { + SECTION("0") + { + REQUIRE(fmt::format("{:.0%Q}", 1.2345m) == "1"); + } + + SECTION("1") + { + REQUIRE(fmt::format("{:.1%Q}", 1.2345m) == "1.2"); + } + + SECTION("2") + { + REQUIRE(fmt::format("{:.2%Q}", 1.2345m) == "1.23"); + } + + SECTION("3") + { + REQUIRE(fmt::format("{:.3%Q}", 1.2345m) == "1.235"); + } + + SECTION("4") + { + REQUIRE(fmt::format("{:.4%Q}", 1.2345m) == "1.2345"); + } + + SECTION("5") + { + REQUIRE(fmt::format("{:.5%Q}", 1.2345m) == "1.23450"); + } + + SECTION("10") + { + REQUIRE(fmt::format("{:.10%Q}", 1.2345m) == "1.2345000000"); + } + } +} + +TEST_CASE("precision specification for integral representation should throw", "[text][fmt][exception]") +{ + SECTION("default format {} on a quantity") + { + REQUIRE_THROWS_MATCHES(fmt::format("{:.1}", 1m), fmt::format_error, Message("precision not allowed for integral quantity representation")); + } + + SECTION("full format {:%Q %q} on a quantity") + { + REQUIRE_THROWS_MATCHES(fmt::format("{:.1%Q %q}", 1m), fmt::format_error, Message("precision not allowed for integral quantity representation")); + } + + SECTION("value only format {:%Q} on a quantity") + { + REQUIRE_THROWS_MATCHES(fmt::format("{:.1%Q}", 1m), fmt::format_error, Message("precision not allowed for integral quantity representation")); + } +} + // Restate operator<< definitions in terms of std::format to make I/O manipulators apply to whole objects // rather than their parts