Module strfmt

Author: Frank Fischer
License: MIT
Version: 0.9.0

Introduction

This module implements some basic helpers for formatting values as strings in a configurable way. It is inspired by and similar to Python's format function

The most important functions and macros provided are:

  1. the format functions to render a single value as a string,
  2. the fmt macro to construct a string containing several formatted values
  3. the writefmt and printfmt family of macros to write a formatted string to a file and stdout, respectively
  4. the interp and $$ string interpolation macros to render expressions embedded in the string itself

These functions are described in the following sections.

This package is hosted on bitbucket. If you encounter any bugs or have some feature requests, please use the issue tracker.

Formatting a single value: format

The format function transforms a single value to a string according to a given format string, e.g.

42.23.format("06.1f")

The syntax of the format specification string is similar to Python's Format Specification Mini-Language.

The general form of a format specifier is

format_spec ::= [[fill]align][sign][#][0][width][,][.precision][type][a[array_sep]]
fill        ::= rune
align       ::= "<" | ">" | "^" | "="
sign        ::= "+" | "-" | " "
width       ::= integer
precision   ::= integer
type        ::= "b" | "c" | "d" | "e" | "E" | "f" | "F" | "g" | "G" | "n" | "o" | "s" | "x" | "X" | "%"
array_sep   ::= "" | (<level separating character> string )+

The meaning of the various fields is as follows.

fill
this character (or rune) is used to for the additional characters to be written until the formatted string is at least width characters long. The fill character defaults to SPACE.
align
OptionMeaning
<Left alignment, additional characters are added to the right (default for string).
>Right alignment, additional characters are added to the left.
^Centered , the same amount of characters is added to the left and the right.
=Padding. If a numeric value is printed with a sign, then additional characters are added after the sign. Otherwise it behaves like ">". This option is only available for numbers (default for numbers).
sign
The sign character is only used for numeric values.
OptionMeaning
+All numbers (including positive ones) are preceded by a sign.
-Only negative numbers are preceded by a sign.
SPACENegative numbers are preceded by a sign, positive numbers are preceded by a space.
#
If this character is present then the integer values in the formats b, o, x and X are preceded by 0b, 0o or 0x, respectively. In all other formats this character is ignored.
width
The minimal width of the resulting string. The result string is padded with extra characters (according the align field) until at least width characters have been written.
,
Add , as a thousands separator
precision
The meaning of the precision field depends on the formatting type.
TypeMeaning
sThe maximal number of characters written.
f, F, e and EThe number of digits after the decimal point.
g, GThe number of significant digits written (i.e. the number of overall digits).

Note that in all cases the decimal point is printed if and only if there is at least one digit following the point.

The precision field is ignored in all other cases.

type
The formatting type. The valid types depend on the type of the value to be printed. For strings the following types are valid.
TypeMeaning
sA string. This is the default format for strings.

The following types are valid for integers.

TypeMeaning
dA decimal integer number. This is the default for integers.
bA binary integer (base 2).
oAn octal integer (base 8).
xA hexadecimal integer (base 16), all letters are lower case.
XA hexadecimal integer (base 16), all letters are upper case.
nThe same as d.

The following types are valid for real numbers.

TypeMeaning
fFixed point format.
FThe same as f.
eScientific format, exactly one digit before the decimal point. The exponent is written with a lower case 'e'. The exponent always has a sign as at least two digits.
EThe same as e but with an upper case 'E'.
g

General format. The number is written either in fixed point format or in scientific format depending on the precision and the exponent in scientific format.

The exact rule is as follows. Suppose exp is the exponent in scientific format and p the desired precision. If -4 <= exp <= p-1 then the number is formatted in fixed point format f with precision p-1-exp. Otherwise the number if formatted in scientific format e with precision p-1. Trailing zeros are removed in all cases and the decimal point is removed as well if there are no remaining digits following it.

GThe same as g but works like E if scientific format is used.
%The number if multiplied by 100, formatted in fixed point format f and followed by a percent sign.
array_sep

If an array is formatted, the format specifications above apply to each element of the array. The elements are printed in succession separated by a separator string. If the array is nested then this applies recursively.

The array_sep field specifies the separator string for all levels of a nested array. The first character after the a is the level separator and works as separator between the string for successive levels. It is never used in the resulting string. All characters between two level separators are the separator between two elements of the respective array level. See Array formatting below.

Array formatting

A format string may contain a separator string for formatting arrays. Because arrays might be nested the separator field contains the separator strings to be used between two successive elements of each level. The strings for each level are separated (in the format string itself) by a special separating character. This character is the first character after the a in the format string. The following example should make this clear:

[[2, 3, 4], [5, 6, 7]].format("02da|; |, ")

This code returns the string "02, 03, 04; 05, 06, 07". The special character separating the strings of different levels is the first character after the a, i.e. the pipe character | in this example. Following the first pipe character is the separator string for the outer most level, "; ". This means that after printing the first element of the outermost array the string "; " is printed. After the second pipe character comes the separator string for the second level, in this example ", ". Between each two elements of the second level the separator string ", " is printed. Because the elements of the second level array are integers, the format string "02d" applies to all of them. Thus, each number is printed with a leading 0. After the 4 has been printed the complete first element of the outer array (namely in array [2, 3, 4]) has been printed, so the separator string of the outer level follows, in this case a semicolon and a space. Finally the second array [6, 7, 8] is printed with the separator ", " between each two elements.

A string containing formatted values: fmt

The fmt macro allows to interpolate a string with several formatted values. This macro takes a format string as its first argument and the values to be formatted in the remaining arguments. The result is a formatted string expression. Note that the format string must be a literal string.

A format string contains a replacement field within curly braces {...}. Anything that is not contained in braces is considered literal text. Literal braces can be escaped by doubling the brace character {{ and }}, respectively.

A format string has the following form:

replacement_spec ::= "{" [<argument>] ["." <field>] ["[" <index> "]"] [":" format_spec] "}"

The single fields have the following meaning.

argument
A number denoting the argument passed to fmt. The first argument (after the format string) has number 0. This number can be used to refer to a specific argument. The same argument can be referred by multiple replacement fields:
"{0} {1} {0}".fmt(1, 0)

gives the string "1 0 1".

If no argument number is given, the replacement fields refer to the arguments passed to fmt in order. Note that this is an always-or-never option: either all replacement fields use explicit argument numbers or none.

field
If the argument is a structured type (e.g. a tuple), this specifies which field of the argument should be formatted, e.g.
"{0.x} {0.y}".fmt((x: 1, y:"foo"))

gives "1 foo".

index
If the argument is a sequence type the index refers to the elements of the sequence to be printed:
"<{[1]}>".fmt([23, 42, 81])

gives "<42>".

format_spec
This is the format specification for the argument as described in Formatting a single value: format.

Nested format strings

Format strings must be literal strings. Although this might be a restriction (format strings cannot be constructed during runtime), nested format strings give back a certain flexibility.

A nested format string is a format string in which the format specifier part of a replacement field contains further replacement fields, e.g.

"{:{}{}{}x}".fmt(66, ".", "^", 6)

Results in the string "..42..".

fmt allows exactly one nested level. Note that the resulting code is slightly more inefficient than without nesting (but only for those arguments that actually use nested fields), because after construction of the outer format specification, the format string must be parsed again at runtime. Furthermore, the constructed format string requires an additional temporary string.

The following example demonstrates how fmt together with array separators can be used to format a nested in array in a Matlab-like style:

"A=[{:6ga|;\n   |, }]".fmt([[1.0,2.0,3.0], [4.0,5.0,6.0]])

results in

A=[     1,      2,      3;
        4,      5,      6]

How fmt works

The fmt macros transforms the format string and its arguments into a sequence of commands that build the resulting string. The format specifications are parsed and transformed into a Format structure at compile time so that no overhead remains at runtime. For instance, the following expression

"This {} the number {:_^3} example".fmt("is", 1)

is roughly transformed to

(let arg0 = "is";
 let arg1 = 1;
 var ret = newString(0);
 addformat(ret, "This ");
 addformat(ret, arg0, DefaultFmt);
 addformat(ret, " the number ");
 addformat(ret, arg1, Format(...));
 addformat(ret, " example ");
 ret)

(Note that this is a statement-list-expression). The functions addformat are defined within strfmt and add formatted output to the string ret.

String interpolation interp


Warning: This feature is highly experimental.


The interp macro interpolates a string with embedded expressions. If the string to be interpolated contains a $, then the following characters are interpreted as expressions.

let x = 2
let y = 1.0/3.0
echo interp"Equation: $x + ${y:.2f} == ${x.float + y}"

The macro interp supports the following interpolations expressions:

StringMeaning
$<ident>The value of the variable denoted by <ident> is substituted into the string according to the default format for the respective type.
${<expr>}The expression <expr> is evaluated and its result is substituted into the string according to the default format of its type.
${<expr>:<format>}The expression <expr> is evaluated and its result is substituted into the string according to the format string <format>. The format string has the same structure as for the format function.
$$A literal $

How interp works

The macro interp is quite simple. A string with embedded expressions is simply transformed to an equivalent expression using the fmt macro:

echo interp"Equation: $x + ${y:.2f} == ${x.float + y}"

is transformed to

echo fmt("Equation: {} + {:.2f} == {}", x, y, x.float + y)

Writing formatted output to a file: writefmt

The writefmt family of macros are convenience helpers to write formatted output to a file. A call

writefmt(f, fmtstr, arg1, arg2, ...)

is equivalent to

write(f, fmtstr.fmt(arg1, arg2, ...))

However, the former avoids the creation of temporary intermediate strings (the variable ret in the example above) but writes directly to the output file. The printfmt family of functions does the same but writes to stdout.

Adding new formatting functions

In order to add a new formatting function for a type T one has to define a new function

proc writeformat(o: var Writer; x: T; fmt: Format)

The following example defines a formatting function for a simple 2D-point data type. The format specification is used for formatting the two coordinate values.

type Point = tuple[x, y: float]

proc writeformat*(o: var Writer; p: Point; fmt: Format) =
  write(o, '(')
  writeformat(o, p.x, fmt)
  write(o, ',')
  write(o, ' ')
  writeformat(o, p.y, fmt)
  write(o, ')')

Types

FormatError = object of Exception
Error in the format string.
Writer = concept W
    ## Writer to output a character `c`.
    write(W, ' ')
FmtAlign = enum
  faDefault,                  ## default for given format type
  faLeft,                     ## left aligned
  faRight,                    ## right aligned
  faCenter,                   ## centered
  faPadding                   ## right aligned, fill characters after sign (numbers only)
Format alignment
FmtSign = enum
  fsMinus,                    ## only unary minus, no reserved sign space for positive numbers
  fsPlus,                     ## unary minus and unary plus
  fsSpace                     ## unary minus and reserved space for positive numbers
Format sign
FmtType = enum
  ftDefault,                  ## default format for given parameter type
  ftStr,                      ## string
  ftChar,                     ## character
  ftDec,                      ## decimal integer
  ftBin,                      ## binary integer
  ftOct,                      ## octal integer
  ftHex,                      ## hexadecimal integer
  ftFix,                      ## real number in fixed point notation
  ftSci,                      ## real number in scientific notation
  ftGen,                      ## real number in generic form (either fixed point or scientific)
  ftPercent                   ## real number multiplied by 100 and % added
Format type
Format = tuple[typ: FmtType,     ## format type
             precision: int,   ## floating point precision
             width: int,       ## minimal width
             fill: string,     ## the fill character, UTF8
             align: FmtAlign,  ## alignment
             sign: FmtSign,    ## sign notation
             baseprefix: bool, ## whether binary, octal, hex should be prefixed by 0b, 0x, 0o
             upcase: bool,     ## upper case letters in hex or exponential formats
             comma: bool, arysep: string]
Formatting information.

Consts

DefaultFmt: Format = (ftDefault, -1, -1, "", faDefault, fsMinus, false, false, false, "")
Default format corresponding to the empty format string, i.e.

x.format("") == x.format(DefaultFmt).

Procs

proc write(s: var string; c: char) {...}{.raises: [], tags: [].}
proc parse(fmt: string): Format {...}{.nosideeffect, raises: [FormatError, OverflowError],
                              tags: [].}
proc getalign(fmt: Format; defalign: FmtAlign; slen: int): tuple[left, right: int] {...}{.
    nosideeffect, raises: [], tags: [].}
Returns the number of left and right padding characters for a given format alignment and width of the object to be printed.
fmt
the format data
default
if fmt.align == faDefault, then this alignment is used
slen
the width of the object to be printed.

The returned values (left, right) will be as minimal as possible so that left + slen + right >= fmt.width.

proc writeformat(o: var Writer; s: string; fmt: Format)
Write string s according to format fmt using output object o and output function add.
proc writeformat(o: var Writer; c: char; fmt: Format)
Write character c according to format fmt using output object o and output function add.
proc writeformat(o: var Writer; c: Rune; fmt: Format)
Write rune c according to format fmt using output object o and output function add.
proc writeformat(o: var Writer; i: SomeInteger; fmt: Format)
Write integer i according to format fmt using output object o and output function add.
proc writeformat(o: var Writer; p: pointer; fmt: Format)

Write pointer i according to format fmt using output object o and output function add.

Pointers are casted to unsigned int and formatted as hexadecimal with prefix unless specified otherwise.

proc writeformat(o: var Writer; x: SomeFloat; fmt: Format)
Write real number x according to format fmt using output object o and output function add.
proc writeformat(o: var Writer; b: bool; fmt: Format)
Write boolean value b according to format fmt using output object o. A boolean may be formatted numerically or as string. In the former case true is written as 1 and false as 0, in the latter the strings "true" and "false" are shown, respectively. The default is string format.
proc writeformat(o: var Writer; ary: openArray[any]; fmt: Format)
Write array ary according to format fmt using output object o and output function add.
proc addformat[T](o: var Writer; x: T; fmt: Format = DefaultFmt) {...}{.inline.}
Write x formatted with fmt to o.
proc addformat[T](o: var Writer; x: T; fmt: string) {...}{.inline.}
The same as addformat(o, x, parse(fmt)).
proc addformat(s: var string; x: string) {...}{.inline, raises: [], tags: [].}
Write x to s. This is a fast specialized version for appending unformatted strings.
proc addformat(f: File; x: string) {...}{.inline, raises: [IOError], tags: [WriteIOEffect].}
Write x to f. This is a fast specialized version for writing unformatted strings to a file.
proc addformat[T](f: File; x: T; fmt: Format = DefaultFmt) {...}{.inline.}
Write x to file f using format fmt.
proc addformat[T](f: File; x: T; fmt: string) {...}{.inline.}
Write x to file f using format string fmt. This is the same as addformat(f, x, parse(fmt))
proc addformat(s: Stream; x: string) {...}{.inline, raises: [Exception],
                                  tags: [WriteIOEffect].}
Write x to s. This is a fast specialized version for writing unformatted strings to a stream.
proc addformat[T](s: Stream; x: T; fmt: Format = DefaultFmt) {...}{.inline.}
Write x to stream s using format fmt.
proc addformat[T](s: Stream; x: T; fmt: string) {...}{.inline.}
Write x to stream s using format string fmt. This is the same as addformat(s, x, parse(fmt))
proc format[T](x: T; fmt: Format): string
Return x formatted as a string according to format fmt.
proc format[T](x: T; fmt: string): string
Return x formatted as a string according to format string fmt.
proc format[T](x: T): string {...}{.inline.}
Return x formatted as a string according to the default format. The default format corresponds to an empty format string.

Macros

macro fmt(fmtstr: string{lit}; args: varargs[untyped]): untyped
Formats arguments args according to the format string fmtstr.
macro writefmt(f: File; fmtstr: string{lit}; args: varargs[untyped]): untyped
The same as write(f, fmtstr.fmt(args...)) but faster.
macro writelnfmt(f: File; fmtstr: string{lit}; args: varargs[untyped]): untyped
The same as writeln(f, fmtstr.fmt(args...)) but faster.
macro printfmt(fmtstr: string{lit}; args: varargs[untyped]): untyped
The same as writefmt(stdout, fmtstr, args...).
macro printlnfmt(fmtstr: string{lit}; args: varargs[untyped]): untyped
The same as writelnfmt(stdout, fmtstr, args...).
macro writefmt(s: Stream; fmtstr: string{lit}; args: varargs[untyped]): untyped
The same as write(s, fmtstr.fmt(args...)) but faster.
macro writelnfmt(s: Stream; fmtstr: string{lit}; args: varargs[untyped]): untyped
The same as writeln(s, fmtstr.fmt(args...)) but faster.
macro addfmt(s: var string; fmtstr: string{lit}; args: varargs[untyped]): untyped
The same as s.add(fmtstr.fmt(args...)) but faster.
macro interp(fmtstr: string{lit}): untyped
Interpolate fmtstr with expressions.
macro `$$`(fmtstr: string{lit}): untyped
Interpolate fmtstr with expressions.