From 21ef5d530404aff3c87d6bbc0cfc2890cb1045d8 Mon Sep 17 00:00:00 2001 From: Markus Mittendrein Date: Tue, 11 Jan 2022 02:55:55 +0100 Subject: Add ' flag for thousands / byte separator for integral and floating point types --- include/cxxformat/core.hpp | 32 +++++--- include/cxxformat/formatters.hpp | 161 +++++++++++++++++++++++++++++++++++---- main.cpp | 20 ++++- 3 files changed, 186 insertions(+), 27 deletions(-) diff --git a/include/cxxformat/core.hpp b/include/cxxformat/core.hpp index acf8613..fdddece 100644 --- a/include/cxxformat/core.hpp +++ b/include/cxxformat/core.hpp @@ -137,12 +137,15 @@ namespace format { char conversion; char addSign; char padding; + Length length; struct Flags { - bool leftJustified : 1; - bool alternative : 1; - bool widthAsArg : 1; - bool precisionAsArg : 1; - Length length : 4; + // following after % or %$ + bool leftJustified : 1; // - + bool alternative : 1; // # + bool quote : 1; // ' + + bool widthAsArg : 1; // if width is * + bool precisionAsArg : 1; // if precision is * } flags; }; } @@ -263,12 +266,13 @@ namespace format { .precision = std::nullopt, .addSign = '\0', .padding = ' ', + .length = format_specifier::None, .flags = { .leftJustified = false, .alternative = false, + .quote = false, .widthAsArg = false, .precisionAsArg = false, - .length = format_specifier::None } }; @@ -313,6 +317,10 @@ namespace format { { spec.padding = '0'; } + else if (c == '\'') + { + spec.flags.quote = true; + } else { break; @@ -389,33 +397,33 @@ namespace format { { ++pos; checkEos(); - spec.flags.length = (c == 'h' ? L::Quarter : L::LongLong); + spec.length = (c == 'h' ? L::Quarter : L::LongLong); ++pos; } else { - spec.flags.length = (c == 'h' ? L::Half : L::Long); + spec.length = (c == 'h' ? L::Half : L::Long); ++pos; } } else if (c == 'L') { - spec.flags.length = L::LongDouble; + spec.length = L::LongDouble; ++pos; } else if (c == 'j') { - spec.flags.length = L::Maximum; + spec.length = L::Maximum; ++pos; } else if (c == 'z') { - spec.flags.length = L::SizeT; + spec.length = L::SizeT; ++pos; } else if (c == 't') { - spec.flags.length = L::PtrDiff; + spec.length = L::PtrDiff; ++pos; } } diff --git a/include/cxxformat/formatters.hpp b/include/cxxformat/formatters.hpp index b248b17..b9d6fcc 100644 --- a/include/cxxformat/formatters.hpp +++ b/include/cxxformat/formatters.hpp @@ -36,6 +36,58 @@ namespace format { } } + constexpr void formatQuoteGrouped(const format_output auto& out, std::string_view subject, std::size_t leadingZeros, unsigned int grouping) + { + const auto quote = '\''; + const auto subjectDigits = subject.size(); + const auto totalDigits = subjectDigits + leadingZeros; + const auto firstZeros = totalDigits % grouping; + const auto firstSubject = subjectDigits % grouping; + + if (firstZeros > 0 && leadingZeros >= firstZeros) + { + out('0', firstZeros); + out(quote); + leadingZeros -= firstZeros; + } + + for (; leadingZeros > grouping; leadingZeros -= grouping) + { + out('0', grouping); + out(quote); + } + + out('0', leadingZeros); + + auto subjectPos = 0; + if (firstSubject > 0 && subjectDigits > firstSubject) + { + out(subject.substr(0, firstSubject)); + subjectPos += firstSubject; + out(quote); + } + + for (; subjectPos + grouping < subjectDigits; subjectPos += grouping) + { + out(subject.substr(subjectPos, grouping)); + out(quote); + } + + out(subject.substr(subjectPos)); + } + + constexpr std::size_t calculateNeededGroupedZeroPadding(std::size_t targetWidth, std::size_t digitCount, unsigned int grouping) noexcept + { + // tw = targetWidth; d = digitCount; g = grouping; x = result + // tw = (d + x) / g - 1 + d + x + // tw = (d + d * g + x + x * g + - g) / g + // tw * g = (d + d * g + x + x * g - g) + // tw * g - (d + d * g - g) = x * (1 + g) + // ((tw - (- 1)) * g - (d * (1 + g))) / (1 + g) = x + // ((tw + 1) * g - (d * (1 + g))) / (1 + g) = x + return ((targetWidth + 1) * grouping - (digitCount * (1 + grouping))) / (1 + grouping); + } + constexpr void simpleConversionSpecifierCheck(std::string_view supported, char c, std::string_view typeDesc) { using namespace std::string_literals; @@ -72,6 +124,15 @@ namespace format { } } + constexpr void noQuote(bool quote, std::string_view typeDesc) + { + using namespace std::string_literals; + if (quote) + { + throw std::invalid_argument{"The quote (‘'’) flag is not supported for "s + std::string{typeDesc}}; + } + } + constexpr void noPrecision(optional_int precision, std::string_view typeDesc) { using namespace std::string_literals; @@ -80,6 +141,16 @@ namespace format { throw std::invalid_argument{"Specifying a precision is not supported for "s + std::string{typeDesc}}; } } + + template + constexpr auto ceilDiv(T nom_, U den_) + { + using Result = std::common_type_t; + Result nom = static_cast(nom_); + Result den = static_cast(den_); + + return (nom + den - 1) / den; + } } using invalid_argument = std::invalid_argument; @@ -97,6 +168,7 @@ namespace format { } const int base = std::unsigned_integral ? (conv == 'o' ? 8 : "xX"_contains(conv) ? 16 : 10) : 10; + const unsigned int quoteGrouping = spec.flags.quote ? (base == 10 ? 3 : 2) : 0; std::string_view converted; std::array::digits + 2) / 3> result; @@ -162,19 +234,31 @@ namespace format { precisionPadding = *precision - converted.size(); } } - else if (minWidth && (spec.flags.alternative && base != 10 && spec.padding == '0')) + else if (minWidth && spec.padding == '0') { - const auto totalLength = converted.size() + prefix.size(); + const auto numSize = converted.size(); + const auto restSize = prefix.size(); + const auto totalLength = numSize + restSize + (quoteGrouping != 0 ? numSize / quoteGrouping : 0); if (*minWidth > totalLength) { - precisionPadding = *minWidth - totalLength; - minWidth.clear(); + if (quoteGrouping != 0) + { + precisionPadding = calculateNeededGroupedZeroPadding(*minWidth - restSize, numSize, quoteGrouping); + spec.padding = ' '; + } + else + { + precisionPadding = *minWidth - totalLength; + minWidth.clear(); + } } } if (minWidth) { - const auto totalLength = converted.size() + precisionPadding + prefix.size(); + const auto numSize = converted.size(); + const auto quoteSize = quoteGrouping != 0 ? ((numSize + precisionPadding - 1) / quoteGrouping) : 0; + const auto totalLength = numSize + precisionPadding + quoteSize + prefix.size(); if (*minWidth > totalLength) { normalPadding = *minWidth - totalLength; @@ -188,9 +272,16 @@ namespace format { } out(prefix); - out('0', precisionPadding); - out(converted); + if (quoteGrouping != 0) + { + formatQuoteGrouped(out, converted, precisionPadding, quoteGrouping); + } + else + { + out('0', precisionPadding); + out(converted); + } if(spec.flags.leftJustified) { @@ -226,6 +317,11 @@ namespace format { noPrecision(spec.precision, "integral types with %c"); } + if ("co"_contains(conv)) + { + noQuote(spec.flags.quote, "integral types with %c and %o"); + } + if (spec.addSign != '\0' && !(std::signed_integral && "idv"_contains(conv))) { throw std::invalid_argument{"‘+’ and ‘ ’ (sign related) flags are only allowed for signed integral types and conversions %i, %d and %v"}; @@ -262,6 +358,7 @@ namespace format { simpleConversionSpecifierCheck("usv", conv, "bool"); noAlternative(spec.flags.alternative, "bools"); noSignFlags(spec.addSign, "bools"); + noQuote(spec.flags.quote, "bools"); if (conv == 'u') { @@ -312,6 +409,8 @@ namespace format { assert(!"Unsupported conversion; should have been caught by formatter::conversionSupported"); } + const unsigned int quoteGrouping = spec.flags.quote ? (format == std::chars_format::hex ? 2 : 3) : 0; + optional_int intPrecision; if (precision) @@ -365,8 +464,15 @@ namespace format { mantissa = converted.substr(0, exponentPos); exponent = converted.substr(exponentPos); } + std::string_view integralPart = mantissa; + std::string_view fractionalPart; + if (const auto dotPos = mantissa.find('.'); dotPos != std::string_view::npos) + { + integralPart = mantissa.substr(0, dotPos); + fractionalPart = mantissa.substr(dotPos); + } - const bool hasDot = mantissa.find('.') != std::string_view::npos; + const bool hasDot = fractionalPart.starts_with('.'); const bool addDot = spec.flags.alternative && !hasDot; std::size_t extraZeros{0}; @@ -401,13 +507,32 @@ namespace format { } } + + std::size_t zeroPadding = 0; std::size_t paddingLength{0}; if (minWidth) { - const auto totalLength = (signPrefix != '\0' ? 1 : 0) + hexPrefix.size() + converted.size() + (addDot ? 1 : 0) + extraZeros; + const auto prefixSize = (signPrefix != '\0' ? 1 : 0) + hexPrefix.size(); + const auto dotSize = (addDot ? 1 : 0); + const auto integralDigitCount = integralPart.size(); + const auto convertedSize = converted.size(); + const auto totalLength = prefixSize + convertedSize + dotSize + extraZeros + (quoteGrouping != 0 ? integralDigitCount / quoteGrouping : 0); if (*minWidth > totalLength) { - paddingLength = *minWidth - totalLength; + if (quoteGrouping != 0 && spec.padding == '0') + { + zeroPadding = calculateNeededGroupedZeroPadding(*minWidth - prefixSize - dotSize - (convertedSize - integralDigitCount), integralDigitCount, quoteGrouping); + spec.padding = ' '; + paddingLength = *minWidth - (totalLength + zeroPadding + zeroPadding / quoteGrouping); + } + else + { + paddingLength = *minWidth - totalLength; + if(!spec.flags.leftJustified && spec.padding == '0') + { + zeroPadding = paddingLength; + } + } } } @@ -422,12 +547,16 @@ namespace format { } out(hexPrefix); - if(!spec.flags.leftJustified && spec.padding == '0') + if (quoteGrouping != 0) { - out(spec.padding, paddingLength); + formatQuoteGrouped(out, integralPart, zeroPadding, quoteGrouping); + out(fractionalPart); + } + else + { + out('0', zeroPadding); + out(mantissa); } - - out(mantissa); if (addDot) { out('.'); @@ -470,6 +599,7 @@ namespace format { noAlternative(spec.flags.alternative, "stringy types"); noSignFlags(spec.addSign, "stringy types"); noZeroPadding(spec.padding, "stringy types"); + noQuote(spec.flags.quote, "stringy types"); } }; @@ -500,6 +630,7 @@ namespace format { noSignFlags(spec.addSign, "pointer types"); noPrecision(spec.precision, "pointer types"); noZeroPadding(spec.padding, "pointer types"); + noQuote(spec.flags.quote, "pointer types"); delegated_formatter::conversionSupported(delegateSpec); } }; @@ -546,6 +677,7 @@ namespace format { noAlternative(spec.flags.alternative, "char pointer"); noSignFlags(spec.addSign, "char pointer"); noZeroPadding(spec.padding, "char pointer"); + noQuote(spec.flags.quote, "char pointer"); } }; @@ -584,6 +716,7 @@ namespace format { noSignFlags(spec.addSign, fallbackName); noPrecision(spec.precision, fallbackName); noZeroPadding(spec.padding, fallbackName); + noQuote(spec.flags.quote, fallbackName); } }; } diff --git a/main.cpp b/main.cpp index ff4a4a0..91b5aa1 100644 --- a/main.cpp +++ b/main.cpp @@ -30,7 +30,25 @@ int main(int argc, char* argv[]) format_to("Hallo %%du %1$s %s%c %s %u = %4$#x = %4$#X = %4$#o %f!\n", std::cout, "Knirp", 's', argv[0], 42u, 1.337); assert(format("Hallo %%du %1$s %s%c %s %u = %4$#x = %4$#X = %4$#o %f!\n", "Knirp", 's', argv[0], 42u, 1.337) == "Hallo %%du %1$s %s%c %s %u = %4$#x = %4$#X = %4$#o %f!\n"_format("Knirp", 's', argv[0], 42u, 1.337)); - "-%5s-%5s-%u-%u-\n"_format_to(stdout, true, false, true, false); + "-%6s--%6s--%6u--%6u-\n"_format_to(stdout, true, false, true, false); + "-%'030f-\n"_format_to(stdout, 1'000'000.337); + "-%'030a-\n"_format_to(stdout, 1'000'000.337); + "-%'030g-\n"_format_to(stdout, 1'000'000.337); + "-%'030e-\n"_format_to(stdout, 1'000'000.337); + "-%'30f-\n"_format_to(stdout, 1'000'000.337); + "-%'20x-\n"_format_to(stdout, 1'000'000u); + "-%'20u-\n"_format_to(stdout, 1'000'000u); + "- %'19x-\n"_format_to(stdout, 1'000'000u); + "- %'19u-\n"_format_to(stdout, 1'000'000u); + "- %'018x-\n"_format_to(stdout, 1'000'000u); + "- %'018u-\n"_format_to(stdout, 1'000'000u); + "- %'18x-\n"_format_to(stdout, 1'000'000u); + "- %'18u-\n"_format_to(stdout, 1'000'000u); + "- %'019x-\n"_format_to(stdout, 1'000'000u); + "- %'019u-\n"_format_to(stdout, 1'000'000u); + "-%'020x-\n"_format_to(stdout, 1'000'000u); + "-%'020u-\n"_format_to(stdout, 1'000'000u); + "-%'20.10u-\n"_format_to(stdout, 1'000'000u); "-%20g-\n"_format_to(stdout, 0x1.8p0); "-%20.0a-\n"_format_to(stdout, 0.0); "-%#+20.0a-\n"_format_to(stdout, 1.0); -- cgit v1.2.3-54-g00ecf