From 6f19b2d88deb9b94924c609f25ceec527e8d75b3 Mon Sep 17 00:00:00 2001 From: Jaroslav Rohel Date: Tue, 7 Nov 2017 13:00:33 +0100 Subject: [PATCH 1/3] Add POSIX extension for positional arguments The ability to rearrange formatting arguments is an important feature for localization because the word order may vary in different languages. --- tinyformat.h | 231 +++++++++++++++++++++++++++++++++----------- tinyformat_test.cpp | 18 +++- 2 files changed, 189 insertions(+), 60 deletions(-) diff --git a/tinyformat.h b/tinyformat.h index 85a22c1..afba6f7 100644 --- a/tinyformat.h +++ b/tinyformat.h @@ -33,6 +33,7 @@ // // * Type safety and extensibility for user defined types. // * C99 printf() compatibility, to the extent possible using std::ostream +// * POSIX extension for positional arguments // * Simplicity and minimalism. A single header file to include and distribute // with your projects. // * Augment rather than replace the standard stream formatting mechanism @@ -42,7 +43,7 @@ // Main interface example usage // ---------------------------- // -// To print a date to std::cout: +// To print a date to std::cout for American usage: // // std::string weekday = "Wednesday"; // const char* month = "July"; @@ -52,6 +53,14 @@ // // tfm::printf("%s, %s %d, %.2d:%.2d\n", weekday, month, day, hour, min); // +// POSIX extension for positional arguments is available. +// The ability to rearrange formatting arguments is an important feature +// for localization because the word order may vary in different languages. +// +// Previous example for German usage. Arguments are reordered: +// +// tfm::printf("%1$s, %3$d. %2$s, %4$d:%5$.2d\n", weekday, month, day, hour, min); +// // The strange types here emphasize the type safety of the interface; it is // possible to print a std::string using the "%s" conversion, and a // size_t using the "%d" conversion. A similar result could be achieved @@ -579,14 +588,38 @@ inline const char* printFormatStringLiteral(std::ostream& out, const char* fmt) // Parse a format string and set the stream state accordingly. // // The format mini-language recognized here is meant to be the one from C99, -// with the form "%[flags][width][.precision][length]type". +// with the form "%[flags][width][.precision][length]type" with POSIX +// positional arguments extension. +// +// POSIX positional arguments extension: +// Conversions can be applied to the nth argument after the format in +// the argument list, rather than to the next unused argument. In this case, +// the conversion specifier character % (see below) is replaced by the sequence +// "%n$", where n is a decimal integer in the range [1,{NL_ARGMAX}], +// giving the position of the argument in the argument list. This feature +// provides for the definition of format strings that select arguments +// in an order appropriate to specific languages. +// +// The format can contain either numbered argument conversion specifications +// (that is, "%n$" and "*m$"), or unnumbered argument conversion specifications +// (that is, % and * ), but not both. The only exception to this is that %% +// can be mixed with the "%n$" form. The results of mixing numbered and +// unnumbered argument specifications in a format string are undefined. +// When numbered argument specifications are used, specifying the Nth argument +// requires that all the leading arguments, from the first to the (N-1)th, +// are specified in the format string. +// +// In format strings containing the "%n$" form of conversion specification, +// numbered arguments in the argument list can be referenced from the format +// string as many times as required. // // Formatting options which can't be natively represented using the ostream // state are returned in spacePadPositive (for space padded positive numbers) // and ntrunc (for truncating conversions). argIndex is incremented if // necessary to pull out variable width and precision . The function returns a // pointer to the character after the end of the current format spec. -inline const char* streamStateFromFormat(std::ostream& out, bool& spacePadPositive, +inline const char* streamStateFromFormat(std::ostream& out, bool& positionalMode, + bool& spacePadPositive, int& ntrunc, const char* fmtStart, const detail::FormatArg* formatters, int& argIndex, int numFormatters) @@ -608,66 +641,129 @@ inline const char* streamStateFromFormat(std::ostream& out, bool& spacePadPositi bool widthSet = false; int widthExtra = 0; const char* c = fmtStart + 1; - // 1) Parse flags - for(;; ++c) + + // 1) Parse an argument index (if followed by '$') or a width possibly + // preceded with '0' flag. + if(*c >= '0' && *c <= '9') { - switch(*c) + const char tmpc = *c; + int value = parseIntAndAdvance(c); + if(*c == '$') + { // value is an argument index + if(value > 0 && value <= numFormatters) + { + argIndex = value - 1; + } + else + { + TINYFORMAT_ERROR("tinyformat: Positional argument out of range"); + } + ++c; + positionalMode = true; + } + else if(positionalMode) { - case '#': - out.setf(std::ios::showpoint | std::ios::showbase); - continue; - case '0': - // overridden by left alignment ('-' flag) - if(!(out.flags() & std::ios::left)) - { - // Use internal padding so that numeric values are - // formatted correctly, eg -00010 rather than 000-10 - out.fill('0'); - out.setf(std::ios::internal, std::ios::adjustfield); - } - continue; - case '-': - out.fill(' '); - out.setf(std::ios::left, std::ios::adjustfield); - continue; - case ' ': - // overridden by show positive sign, '+' flag. - if(!(out.flags() & std::ios::showpos)) - spacePadPositive = true; - continue; - case '+': - out.setf(std::ios::showpos); - spacePadPositive = false; - widthExtra = 1; - continue; - default: - break; + TINYFORMAT_ERROR("tinyformat: Non-positional argument used after a positional one"); + } + else + { + if(tmpc == '0') + { + // Use internal padding so that numeric values are + // formatted correctly, eg -00010 rather than 000-10 + out.fill('0'); + out.setf(std::ios::internal, std::ios::adjustfield); + } + if(value != 0) + { + // Nonzero value means that we parsed width. + widthSet = true; + out.width(value); + } } - break; } - // 2) Parse width - if(*c >= '0' && *c <= '9') + else if(positionalMode) { - widthSet = true; - out.width(parseIntAndAdvance(c)); + TINYFORMAT_ERROR("tinyformat: Non-positional argument used after a positional one"); } - if(*c == '*') + // 2) Parse flags and width if we did not do it in previous step. + if(!widthSet) { - widthSet = true; - int width = 0; - if(argIndex < numFormatters) - width = formatters[argIndex++].toInt(); - else - TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable width"); - if(width < 0) + // Parse flags + for(;; ++c) { - // negative widths correspond to '-' flag set - out.fill(' '); - out.setf(std::ios::left, std::ios::adjustfield); - width = -width; + switch(*c) + { + case '#': + out.setf(std::ios::showpoint | std::ios::showbase); + continue; + case '0': + // overridden by left alignment ('-' flag) + if(!(out.flags() & std::ios::left)) + { + // Use internal padding so that numeric values are + // formatted correctly, eg -00010 rather than 000-10 + out.fill('0'); + out.setf(std::ios::internal, std::ios::adjustfield); + } + continue; + case '-': + out.fill(' '); + out.setf(std::ios::left, std::ios::adjustfield); + continue; + case ' ': + // overridden by show positive sign, '+' flag. + if(!(out.flags() & std::ios::showpos)) + spacePadPositive = true; + continue; + case '+': + out.setf(std::ios::showpos); + spacePadPositive = false; + widthExtra = 1; + continue; + default: + break; + } + break; + } + // Parse width + if(*c >= '0' && *c <= '9') + { + widthSet = true; + out.width(parseIntAndAdvance(c)); + } + else if(*c == '*') + { + widthSet = true; + int width = 0; + if(positionalMode) + { + ++c; + int pos = parseIntAndAdvance(c) - 1; + if(*c != '$') + TINYFORMAT_ERROR("tinyformat: Non-positional argument used after a positional one"); + if(pos >= 0 && pos < numFormatters) + width = formatters[pos].toInt(); + else + TINYFORMAT_ERROR("tinyformat: Positional argument out of range"); + } + else + { + if(argIndex < numFormatters) + width = formatters[argIndex++].toInt(); + else + TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable width"); + } + if(width < 0) + { + // negative widths correspond to '-' flag set + out.fill(' '); + out.setf(std::ios::left, std::ios::adjustfield); + width = -width; + } + out.width(width); + ++c; } - out.width(width); - ++c; } // 3) Parse precision if(*c == '.') @@ -677,10 +773,24 @@ inline const char* streamStateFromFormat(std::ostream& out, bool& spacePadPositi if(*c == '*') { ++c; - if(argIndex < numFormatters) - precision = formatters[argIndex++].toInt(); + if(positionalMode) + { + int pos = parseIntAndAdvance(c) - 1; + if(*c != '$') + TINYFORMAT_ERROR("tinyformat: Non-positional argument used after a positional one"); + if(pos >= 0 && pos < numFormatters) + precision = formatters[pos].toInt(); + else + TINYFORMAT_ERROR("tinyformat: Positional argument out of range"); + ++c; + } else - TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable precision"); + { + if(argIndex < numFormatters) + precision = formatters[argIndex++].toInt(); + else + TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable precision"); + } } else { @@ -787,13 +897,16 @@ inline void formatImpl(std::ostream& out, const char* fmt, std::ios::fmtflags origFlags = out.flags(); char origFill = out.fill(); - for (int argIndex = 0; argIndex < numFormatters; ++argIndex) + bool positionalMode = false; + for(int argIndex = 0; positionalMode || argIndex < numFormatters; ++argIndex) { // Parse the format string fmt = printFormatStringLiteral(out, fmt); + if(positionalMode && *fmt == '\0') + break; bool spacePadPositive = false; int ntrunc = -1; - const char* fmtEnd = streamStateFromFormat(out, spacePadPositive, ntrunc, fmt, + const char* fmtEnd = streamStateFromFormat(out, positionalMode, spacePadPositive, ntrunc, fmt, formatters, argIndex, numFormatters); if (argIndex >= numFormatters) { diff --git a/tinyformat_test.cpp b/tinyformat_test.cpp index bcc7278..16c5556 100644 --- a/tinyformat_test.cpp +++ b/tinyformat_test.cpp @@ -165,11 +165,16 @@ int unitTests() CHECK_EQUAL(tfm::format("%.f", 10.1), "10"); CHECK_EQUAL(tfm::format("%.2s", "asdf"), "as"); // strings truncate to precision CHECK_EQUAL(tfm::format("%.2s", std::string("asdf")), "as"); -// // Test variable precision & width + // Test variable precision & width CHECK_EQUAL(tfm::format("%*.4f", 10, 1234.1234567890), " 1234.1235"); CHECK_EQUAL(tfm::format("%10.*f", 4, 1234.1234567890), " 1234.1235"); CHECK_EQUAL(tfm::format("%*.*f", 10, 4, 1234.1234567890), " 1234.1235"); CHECK_EQUAL(tfm::format("%*.*f", -10, 4, 1234.1234567890), "1234.1235 "); + // Test variable precision & width with positional arguments + CHECK_EQUAL(tfm::format("%1$*2$.4f", 1234.1234567890, 10), " 1234.1235"); + CHECK_EQUAL(tfm::format("%1$10.*2$f", 1234.1234567890, 4), " 1234.1235"); + CHECK_EQUAL(tfm::format("%1$*3$.*2$f", 1234.1234567890, 4, 10), " 1234.1235"); + CHECK_EQUAL(tfm::format("%1$*2$.*3$f", 1234.1234567890, -10, 4), "1234.1235 "); // Test flags CHECK_EQUAL(tfm::format("%#x", 0x271828), "0x271828"); @@ -212,6 +217,10 @@ int unitTests() 1.234, 42, 3.13, "str", 0XDEAD, (int)'X'), "1.2340000000:0042:+3.13:str:0XDEAD:X:%:%asdf"); + CHECK_EQUAL(tfm::format("%2$0.10f:%3$0*4$d:%1$+g:%6$s:%5$#X:%7$c:%%:%%asdf", + 3.13, 1.234, 42, 4, 0XDEAD, "str", (int)'X'), + "1.2340000000:0042:+3.13:str:0XDEAD:X:%:%asdf"); + // Test wrong number of args EXPECT_ERROR( tfm::format("%d", 5, 10) ) EXPECT_ERROR( tfm::format("%d %d", 1) ) @@ -224,6 +233,13 @@ int unitTests() EXPECT_ERROR( tfm::format("%*d", 1) ) EXPECT_ERROR( tfm::format("%.*d", 1) ) EXPECT_ERROR( tfm::format("%*.*d", 1, 2) ) + // Error required if positional argument refers to non-existent argument + EXPECT_ERROR( tfm::format("%2$d", 1) ) + EXPECT_ERROR( tfm::format("%0$d", 1) ) + EXPECT_ERROR( tfm::format("%1$.*3$d", 1, 2) ) + EXPECT_ERROR( tfm::format("%1$.*0$d", 1, 2) ) + EXPECT_ERROR( tfm::format("%3$*4$.*2$d", 1, 2, 3) ) + EXPECT_ERROR( tfm::format("%3$*0$.*2$d", 1, 2, 3) ) // Unhandled C99 format spec EXPECT_ERROR( tfm::format("%n", 10) ) From b2b6ce354e73ba6d2930b164f0e94713298653f9 Mon Sep 17 00:00:00 2001 From: Jaroslav Rohel Date: Tue, 7 Nov 2017 13:05:13 +0100 Subject: [PATCH 2/3] Add info about POSIX extension for positional arguments --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 966f4b5..74cccc3 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ of the type of `s`, tinyformat might be for you. Design goals include: * Type safety and extensibility for user defined types. * C99 `printf()` compatibility, to the extent possible using `std::ostream` +* POSIX extension for positional arguments * Simplicity and minimalism. A single header file to include and distribute with your projects. * Augment rather than replace the standard stream formatting mechanism @@ -31,6 +32,16 @@ int min = 44; tfm::printf("%s, %s %d, %.2d:%.2d\n", weekday, month, day, hour, min); ``` +POSIX extension for positional arguments is available. +The ability to rearrange formatting arguments is an important feature +for localization because the word order may vary in different languages. + +Previous example for German usage. Arguments are reordered: + +```C++ +tfm::printf("%1$s, %3$d. %2$s, %4$d:%5$.2d\n", weekday, month, day, hour, min); +``` + The strange types here emphasize the type safety of the interface, for example it is possible to print a `std::string` using the `"%s"` conversion, and a `size_t` using the `"%d"` conversion. A similar result could be achieved From 7fde1c32f59c2c884fb9715a7c7216c77e0fceb6 Mon Sep 17 00:00:00 2001 From: Chris Foster Date: Thu, 18 Apr 2019 17:26:56 +1000 Subject: [PATCH 3/3] Refactor to combine code for reading width and precision The positional mode code makes this complex enough that it seems worth combining these in one place. Also add an extra test to check that negative variable position is treated as though no precision was set. --- tinyformat.h | 119 +++++++++++++++++++++----------------------- tinyformat_test.cpp | 8 +-- 2 files changed, 61 insertions(+), 66 deletions(-) diff --git a/tinyformat.h b/tinyformat.h index afba6f7..06900ae 100644 --- a/tinyformat.h +++ b/tinyformat.h @@ -555,6 +555,49 @@ inline int parseIntAndAdvance(const char*& c) return i; } +// Parse width or precision `n` from format string pointer `c`, and advance it +// to the next character. If an indirection is requested with `*`, the argument +// is read from `formatters[argIndex]` and `argIndex` is incremented (or read +// from `formatters[n]` in positional mode). Returns true if one or more +// characters were read. +inline bool parseWidthOrPrecision(int& n, const char*& c, bool positionalMode, + const detail::FormatArg* formatters, + int& argIndex, int numFormatters) +{ + if(*c >= '0' && *c <= '9') + { + n = parseIntAndAdvance(c); + } + else if(*c == '*') + { + ++c; + n = 0; + if(positionalMode) + { + int pos = parseIntAndAdvance(c) - 1; + if(*c != '$') + TINYFORMAT_ERROR("tinyformat: Non-positional argument used after a positional one"); + if(pos >= 0 && pos < numFormatters) + n = formatters[pos].toInt(); + else + TINYFORMAT_ERROR("tinyformat: Positional argument out of range"); + ++c; + } + else + { + if(argIndex < numFormatters) + n = formatters[argIndex++].toInt(); + else + TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable width or precision"); + } + } + else + { + return false; + } + return true; +} + // Print literal part of format string and return next format spec // position. // @@ -649,15 +692,12 @@ inline const char* streamStateFromFormat(std::ostream& out, bool& positionalMode const char tmpc = *c; int value = parseIntAndAdvance(c); if(*c == '$') - { // value is an argument index + { + // value is an argument index if(value > 0 && value <= numFormatters) - { argIndex = value - 1; - } else - { TINYFORMAT_ERROR("tinyformat: Positional argument out of range"); - } ++c; positionalMode = true; } @@ -727,33 +767,11 @@ inline const char* streamStateFromFormat(std::ostream& out, bool& positionalMode break; } // Parse width - if(*c >= '0' && *c <= '9') - { - widthSet = true; - out.width(parseIntAndAdvance(c)); - } - else if(*c == '*') + int width = 0; + widthSet = parseWidthOrPrecision(width, c, positionalMode, + formatters, argIndex, numFormatters); + if(widthSet) { - widthSet = true; - int width = 0; - if(positionalMode) - { - ++c; - int pos = parseIntAndAdvance(c) - 1; - if(*c != '$') - TINYFORMAT_ERROR("tinyformat: Non-positional argument used after a positional one"); - if(pos >= 0 && pos < numFormatters) - width = formatters[pos].toInt(); - else - TINYFORMAT_ERROR("tinyformat: Positional argument out of range"); - } - else - { - if(argIndex < numFormatters) - width = formatters[argIndex++].toInt(); - else - TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable width"); - } if(width < 0) { // negative widths correspond to '-' flag set @@ -762,7 +780,6 @@ inline const char* streamStateFromFormat(std::ostream& out, bool& positionalMode width = -width; } out.width(width); - ++c; } } // 3) Parse precision @@ -770,37 +787,13 @@ inline const char* streamStateFromFormat(std::ostream& out, bool& positionalMode { ++c; int precision = 0; - if(*c == '*') - { - ++c; - if(positionalMode) - { - int pos = parseIntAndAdvance(c) - 1; - if(*c != '$') - TINYFORMAT_ERROR("tinyformat: Non-positional argument used after a positional one"); - if(pos >= 0 && pos < numFormatters) - precision = formatters[pos].toInt(); - else - TINYFORMAT_ERROR("tinyformat: Positional argument out of range"); - ++c; - } - else - { - if(argIndex < numFormatters) - precision = formatters[argIndex++].toInt(); - else - TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable precision"); - } - } - else - { - if(*c >= '0' && *c <= '9') - precision = parseIntAndAdvance(c); - else if(*c == '-') // negative precisions ignored, treated as zero. - parseIntAndAdvance(++c); - } - out.precision(precision); - precisionSet = true; + parseWidthOrPrecision(precision, c, positionalMode, + formatters, argIndex, numFormatters); + // Presence of `.` indicates precision set, unless the inferred value + // was negative in which case the default is used. + precisionSet = precision >= 0; + if(precisionSet) + out.precision(precision); } // 4) Ignore any C99 length modifier while(*c == 'l' || *c == 'h' || *c == 'L' || diff --git a/tinyformat_test.cpp b/tinyformat_test.cpp index 16c5556..7291f06 100644 --- a/tinyformat_test.cpp +++ b/tinyformat_test.cpp @@ -128,9 +128,9 @@ int unitTests() CHECK_EQUAL(tfm::format("%hc", (short)65), "A"); CHECK_EQUAL(tfm::format("%lc", (long)65), "A"); CHECK_EQUAL(tfm::format("%s", "asdf_123098"), "asdf_123098"); - // Note: All tests printing pointers are different on windows, since - // there's no standard numerical representation. - // Representation also differs between 32-bit and 64-bit windows. + + // Test printing of pointers. Note that there's no standard numerical + // representation so this is platform and OS dependent. # ifdef _MSC_VER # ifdef _WIN64 CHECK_EQUAL(tfm::format("%p", (void*)0x12345), "0000000000012345"); @@ -170,6 +170,7 @@ int unitTests() CHECK_EQUAL(tfm::format("%10.*f", 4, 1234.1234567890), " 1234.1235"); CHECK_EQUAL(tfm::format("%*.*f", 10, 4, 1234.1234567890), " 1234.1235"); CHECK_EQUAL(tfm::format("%*.*f", -10, 4, 1234.1234567890), "1234.1235 "); + CHECK_EQUAL(tfm::format("%.*f", -4, 1234.1234567890), "1234.123457"); // negative precision ignored // Test variable precision & width with positional arguments CHECK_EQUAL(tfm::format("%1$*2$.4f", 1234.1234567890, 10), " 1234.1235"); CHECK_EQUAL(tfm::format("%1$10.*2$f", 1234.1234567890, 4), " 1234.1235"); @@ -238,6 +239,7 @@ int unitTests() EXPECT_ERROR( tfm::format("%0$d", 1) ) EXPECT_ERROR( tfm::format("%1$.*3$d", 1, 2) ) EXPECT_ERROR( tfm::format("%1$.*0$d", 1, 2) ) + EXPECT_ERROR( tfm::format("%1$.*$d", 1, 2) ) EXPECT_ERROR( tfm::format("%3$*4$.*2$d", 1, 2, 3) ) EXPECT_ERROR( tfm::format("%3$*0$.*2$d", 1, 2, 3) )