Skip to content

Commit cd2fc0c

Browse files
authored
Merge pull request #45 from jrohel/feature/posix_positional_args
Add POSIX extension for positional arguments
2 parents 389f37d + 7fde1c3 commit cd2fc0c

File tree

3 files changed

+211
-76
lines changed

3 files changed

+211
-76
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ of the type of `s`, tinyformat might be for you. Design goals include:
88

99
* Type safety and extensibility for user defined types.
1010
* C99 `printf()` compatibility, to the extent possible using `std::ostream`
11+
* POSIX extension for positional arguments
1112
* Simplicity and minimalism. A single header file to include and distribute
1213
with your projects.
1314
* Augment rather than replace the standard stream formatting mechanism
@@ -31,6 +32,16 @@ int min = 44;
3132
tfm::printf("%s, %s %d, %.2d:%.2d\n", weekday, month, day, hour, min);
3233
```
3334
35+
POSIX extension for positional arguments is available.
36+
The ability to rearrange formatting arguments is an important feature
37+
for localization because the word order may vary in different languages.
38+
39+
Previous example for German usage. Arguments are reordered:
40+
41+
```C++
42+
tfm::printf("%1$s, %3$d. %2$s, %4$d:%5$.2d\n", weekday, month, day, hour, min);
43+
```
44+
3445
The strange types here emphasize the type safety of the interface, for example
3546
it is possible to print a `std::string` using the `"%s"` conversion, and a
3647
`size_t` using the `"%d"` conversion. A similar result could be achieved

tinyformat.h

Lines changed: 178 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
//
3434
// * Type safety and extensibility for user defined types.
3535
// * C99 printf() compatibility, to the extent possible using std::ostream
36+
// * POSIX extension for positional arguments
3637
// * Simplicity and minimalism. A single header file to include and distribute
3738
// with your projects.
3839
// * Augment rather than replace the standard stream formatting mechanism
@@ -42,7 +43,7 @@
4243
// Main interface example usage
4344
// ----------------------------
4445
//
45-
// To print a date to std::cout:
46+
// To print a date to std::cout for American usage:
4647
//
4748
// std::string weekday = "Wednesday";
4849
// const char* month = "July";
@@ -52,6 +53,14 @@
5253
//
5354
// tfm::printf("%s, %s %d, %.2d:%.2d\n", weekday, month, day, hour, min);
5455
//
56+
// POSIX extension for positional arguments is available.
57+
// The ability to rearrange formatting arguments is an important feature
58+
// for localization because the word order may vary in different languages.
59+
//
60+
// Previous example for German usage. Arguments are reordered:
61+
//
62+
// tfm::printf("%1$s, %3$d. %2$s, %4$d:%5$.2d\n", weekday, month, day, hour, min);
63+
//
5564
// The strange types here emphasize the type safety of the interface; it is
5665
// possible to print a std::string using the "%s" conversion, and a
5766
// size_t using the "%d" conversion. A similar result could be achieved
@@ -551,6 +560,49 @@ inline int parseIntAndAdvance(const char*& c)
551560
return i;
552561
}
553562

563+
// Parse width or precision `n` from format string pointer `c`, and advance it
564+
// to the next character. If an indirection is requested with `*`, the argument
565+
// is read from `formatters[argIndex]` and `argIndex` is incremented (or read
566+
// from `formatters[n]` in positional mode). Returns true if one or more
567+
// characters were read.
568+
inline bool parseWidthOrPrecision(int& n, const char*& c, bool positionalMode,
569+
const detail::FormatArg* formatters,
570+
int& argIndex, int numFormatters)
571+
{
572+
if(*c >= '0' && *c <= '9')
573+
{
574+
n = parseIntAndAdvance(c);
575+
}
576+
else if(*c == '*')
577+
{
578+
++c;
579+
n = 0;
580+
if(positionalMode)
581+
{
582+
int pos = parseIntAndAdvance(c) - 1;
583+
if(*c != '$')
584+
TINYFORMAT_ERROR("tinyformat: Non-positional argument used after a positional one");
585+
if(pos >= 0 && pos < numFormatters)
586+
n = formatters[pos].toInt();
587+
else
588+
TINYFORMAT_ERROR("tinyformat: Positional argument out of range");
589+
++c;
590+
}
591+
else
592+
{
593+
if(argIndex < numFormatters)
594+
n = formatters[argIndex++].toInt();
595+
else
596+
TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable width or precision");
597+
}
598+
}
599+
else
600+
{
601+
return false;
602+
}
603+
return true;
604+
}
605+
554606
// Print literal part of format string and return next format spec
555607
// position.
556608
//
@@ -584,14 +636,38 @@ inline const char* printFormatStringLiteral(std::ostream& out, const char* fmt)
584636
// Parse a format string and set the stream state accordingly.
585637
//
586638
// The format mini-language recognized here is meant to be the one from C99,
587-
// with the form "%[flags][width][.precision][length]type".
639+
// with the form "%[flags][width][.precision][length]type" with POSIX
640+
// positional arguments extension.
641+
//
642+
// POSIX positional arguments extension:
643+
// Conversions can be applied to the nth argument after the format in
644+
// the argument list, rather than to the next unused argument. In this case,
645+
// the conversion specifier character % (see below) is replaced by the sequence
646+
// "%n$", where n is a decimal integer in the range [1,{NL_ARGMAX}],
647+
// giving the position of the argument in the argument list. This feature
648+
// provides for the definition of format strings that select arguments
649+
// in an order appropriate to specific languages.
650+
//
651+
// The format can contain either numbered argument conversion specifications
652+
// (that is, "%n$" and "*m$"), or unnumbered argument conversion specifications
653+
// (that is, % and * ), but not both. The only exception to this is that %%
654+
// can be mixed with the "%n$" form. The results of mixing numbered and
655+
// unnumbered argument specifications in a format string are undefined.
656+
// When numbered argument specifications are used, specifying the Nth argument
657+
// requires that all the leading arguments, from the first to the (N-1)th,
658+
// are specified in the format string.
659+
//
660+
// In format strings containing the "%n$" form of conversion specification,
661+
// numbered arguments in the argument list can be referenced from the format
662+
// string as many times as required.
588663
//
589664
// Formatting options which can't be natively represented using the ostream
590665
// state are returned in spacePadPositive (for space padded positive numbers)
591666
// and ntrunc (for truncating conversions). argIndex is incremented if
592667
// necessary to pull out variable width and precision. The function returns a
593668
// pointer to the character after the end of the current format spec.
594-
inline const char* streamStateFromFormat(std::ostream& out, bool& spacePadPositive,
669+
inline const char* streamStateFromFormat(std::ostream& out, bool& positionalMode,
670+
bool& spacePadPositive,
595671
int& ntrunc, const char* fmtStart,
596672
const detail::FormatArg* formatters,
597673
int& argIndex, int numFormatters)
@@ -613,89 +689,116 @@ inline const char* streamStateFromFormat(std::ostream& out, bool& spacePadPositi
613689
bool widthSet = false;
614690
int widthExtra = 0;
615691
const char* c = fmtStart + 1;
616-
// 1) Parse flags
617-
for(;; ++c)
692+
693+
// 1) Parse an argument index (if followed by '$') or a width possibly
694+
// preceded with '0' flag.
695+
if(*c >= '0' && *c <= '9')
618696
{
619-
switch(*c)
697+
const char tmpc = *c;
698+
int value = parseIntAndAdvance(c);
699+
if(*c == '$')
620700
{
621-
case '#':
622-
out.setf(std::ios::showpoint | std::ios::showbase);
623-
continue;
624-
case '0':
625-
// overridden by left alignment ('-' flag)
626-
if(!(out.flags() & std::ios::left))
627-
{
628-
// Use internal padding so that numeric values are
629-
// formatted correctly, eg -00010 rather than 000-10
630-
out.fill('0');
631-
out.setf(std::ios::internal, std::ios::adjustfield);
632-
}
633-
continue;
634-
case '-':
635-
out.fill(' ');
636-
out.setf(std::ios::left, std::ios::adjustfield);
637-
continue;
638-
case ' ':
639-
// overridden by show positive sign, '+' flag.
640-
if(!(out.flags() & std::ios::showpos))
641-
spacePadPositive = true;
642-
continue;
643-
case '+':
644-
out.setf(std::ios::showpos);
645-
spacePadPositive = false;
646-
widthExtra = 1;
647-
continue;
648-
default:
649-
break;
701+
// value is an argument index
702+
if(value > 0 && value <= numFormatters)
703+
argIndex = value - 1;
704+
else
705+
TINYFORMAT_ERROR("tinyformat: Positional argument out of range");
706+
++c;
707+
positionalMode = true;
708+
}
709+
else if(positionalMode)
710+
{
711+
TINYFORMAT_ERROR("tinyformat: Non-positional argument used after a positional one");
712+
}
713+
else
714+
{
715+
if(tmpc == '0')
716+
{
717+
// Use internal padding so that numeric values are
718+
// formatted correctly, eg -00010 rather than 000-10
719+
out.fill('0');
720+
out.setf(std::ios::internal, std::ios::adjustfield);
721+
}
722+
if(value != 0)
723+
{
724+
// Nonzero value means that we parsed width.
725+
widthSet = true;
726+
out.width(value);
727+
}
650728
}
651-
break;
652729
}
653-
// 2) Parse width
654-
if(*c >= '0' && *c <= '9')
730+
else if(positionalMode)
655731
{
656-
widthSet = true;
657-
out.width(parseIntAndAdvance(c));
732+
TINYFORMAT_ERROR("tinyformat: Non-positional argument used after a positional one");
658733
}
659-
if(*c == '*')
734+
// 2) Parse flags and width if we did not do it in previous step.
735+
if(!widthSet)
660736
{
661-
widthSet = true;
737+
// Parse flags
738+
for(;; ++c)
739+
{
740+
switch(*c)
741+
{
742+
case '#':
743+
out.setf(std::ios::showpoint | std::ios::showbase);
744+
continue;
745+
case '0':
746+
// overridden by left alignment ('-' flag)
747+
if(!(out.flags() & std::ios::left))
748+
{
749+
// Use internal padding so that numeric values are
750+
// formatted correctly, eg -00010 rather than 000-10
751+
out.fill('0');
752+
out.setf(std::ios::internal, std::ios::adjustfield);
753+
}
754+
continue;
755+
case '-':
756+
out.fill(' ');
757+
out.setf(std::ios::left, std::ios::adjustfield);
758+
continue;
759+
case ' ':
760+
// overridden by show positive sign, '+' flag.
761+
if(!(out.flags() & std::ios::showpos))
762+
spacePadPositive = true;
763+
continue;
764+
case '+':
765+
out.setf(std::ios::showpos);
766+
spacePadPositive = false;
767+
widthExtra = 1;
768+
continue;
769+
default:
770+
break;
771+
}
772+
break;
773+
}
774+
// Parse width
662775
int width = 0;
663-
if(argIndex < numFormatters)
664-
width = formatters[argIndex++].toInt();
665-
else
666-
TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable width");
667-
if(width < 0)
776+
widthSet = parseWidthOrPrecision(width, c, positionalMode,
777+
formatters, argIndex, numFormatters);
778+
if(widthSet)
668779
{
669-
// negative widths correspond to '-' flag set
670-
out.fill(' ');
671-
out.setf(std::ios::left, std::ios::adjustfield);
672-
width = -width;
780+
if(width < 0)
781+
{
782+
// negative widths correspond to '-' flag set
783+
out.fill(' ');
784+
out.setf(std::ios::left, std::ios::adjustfield);
785+
width = -width;
786+
}
787+
out.width(width);
673788
}
674-
out.width(width);
675-
++c;
676789
}
677790
// 3) Parse precision
678791
if(*c == '.')
679792
{
680793
++c;
681794
int precision = 0;
682-
if(*c == '*')
683-
{
684-
++c;
685-
if(argIndex < numFormatters)
686-
precision = formatters[argIndex++].toInt();
687-
else
688-
TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable precision");
689-
}
690-
else
691-
{
692-
if(*c >= '0' && *c <= '9')
693-
precision = parseIntAndAdvance(c);
694-
else if(*c == '-') // negative precisions ignored, treated as zero.
695-
parseIntAndAdvance(++c);
696-
}
697-
out.precision(precision);
698-
precisionSet = true;
795+
parseWidthOrPrecision(precision, c, positionalMode,
796+
formatters, argIndex, numFormatters);
797+
// Presence of `.` indicates precision set, unless the inferred value
798+
// was negative in which case the default is used.
799+
precisionSet = precision >= 0;
800+
if(precisionSet)
801+
out.precision(precision);
699802
}
700803
// 4) Ignore any C99 length modifier
701804
while(*c == 'l' || *c == 'h' || *c == 'L' ||
@@ -792,13 +895,16 @@ inline void formatImpl(std::ostream& out, const char* fmt,
792895
std::ios::fmtflags origFlags = out.flags();
793896
char origFill = out.fill();
794897

795-
for (int argIndex = 0; argIndex < numFormatters; ++argIndex)
898+
bool positionalMode = false;
899+
for(int argIndex = 0; positionalMode || argIndex < numFormatters; ++argIndex)
796900
{
797901
// Parse the format string
798902
fmt = printFormatStringLiteral(out, fmt);
903+
if(positionalMode && *fmt == '\0')
904+
break;
799905
bool spacePadPositive = false;
800906
int ntrunc = -1;
801-
const char* fmtEnd = streamStateFromFormat(out, spacePadPositive, ntrunc, fmt,
907+
const char* fmtEnd = streamStateFromFormat(out, positionalMode, spacePadPositive, ntrunc, fmt,
802908
formatters, argIndex, numFormatters);
803909
if (argIndex >= numFormatters)
804910
{

0 commit comments

Comments
 (0)