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
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";
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