Merge pull request #3566 from Feoramund/fmt-refactor

Refactor `wprintf`
This commit is contained in:
gingerBill
2024-05-10 15:56:26 +01:00
committed by GitHub
4 changed files with 305 additions and 214 deletions
+18 -15
View File
@@ -1,5 +1,5 @@
/* /*
package fmt implemented formatted I/O with procedures similar to C's printf and Python's format. package fmt implements formatted I/O with procedures similar to C's printf and Python's format.
The format 'verbs' are derived from C's but simpler. The format 'verbs' are derived from C's but simpler.
Printing Printing
@@ -33,6 +33,8 @@ Floating-point, complex numbers, and quaternions:
%E scientific notation, e.g. -1.23456E+78 %E scientific notation, e.g. -1.23456E+78
%f decimal point but no exponent, e.g. 123.456 %f decimal point but no exponent, e.g. 123.456
%F synonym for %f %F synonym for %f
%g synonym for %f with default maximum precision
%G synonym for %g
%h hexadecimal (lower-case) representation with 0h prefix (0h01234abcd) %h hexadecimal (lower-case) representation with 0h prefix (0h01234abcd)
%H hexadecimal (upper-case) representation with 0H prefix (0h01234ABCD) %H hexadecimal (upper-case) representation with 0H prefix (0h01234ABCD)
%m number of bytes in the best unit of measurement, e.g. 123.45mib %m number of bytes in the best unit of measurement, e.g. 123.45mib
@@ -61,9 +63,9 @@ For compound values, the elements are printed using these rules recursively; lai
bit sets {key0 = elem0, key1 = elem1, ...} bit sets {key0 = elem0, key1 = elem1, ...}
pointer to above: &{}, &[], &map[] pointer to above: &{}, &[], &map[]
Width is specified by an optional decimal number immediately preceding the verb. Width is specified by an optional decimal number immediately after the '%'.
If not present, the width is whatever is necessary to represent the value. If not present, the width is whatever is necessary to represent the value.
Precision is specified after the (optional) width followed by a period followed by a decimal number. Precision is specified after the (optional) width by a period followed by a decimal number.
If no period is present, a default precision is used. If no period is present, a default precision is used.
A period with no following number specifies a precision of 0. A period with no following number specifies a precision of 0.
@@ -75,7 +77,7 @@ Examples:
%8.f width 8, precision 0 %8.f width 8, precision 0
Width and precision are measured in units of Unicode code points (runes). Width and precision are measured in units of Unicode code points (runes).
n.b. C's printf uses units of bytes n.b. C's printf uses units of bytes.
Other flags: Other flags:
@@ -92,7 +94,7 @@ Other flags:
0 pad with leading zeros rather than spaces 0 pad with leading zeros rather than spaces
Flags are ignored by verbs that don't expect them Flags are ignored by verbs that don't expect them.
For each printf-like procedure, there is a print function that takes no For each printf-like procedure, there is a print function that takes no
@@ -105,19 +107,20 @@ Explicit argument indices:
In printf-like procedures, the default behaviour is for each formatting verb to format successive In printf-like procedures, the default behaviour is for each formatting verb to format successive
arguments passed in the call. However, the notation [n] immediately before the verb indicates that arguments passed in the call. However, the notation [n] immediately before the verb indicates that
the nth zero-index argument is to be formatted instead. the nth zero-index argument is to be formatted instead.
The same notation before an '*' for a width or precision selecting the argument index holding the value. The same notation before an '*' for a width or precision specifier selects the argument index
Python-like syntax with argument indices differs for the selecting the argument index: {N:v} holding the value.
Python-like syntax with argument indices differs for selecting the argument index: {n:v}
Examples: Examples:
fmt.printf("%[1]d %[0]d\n", 13, 37); // C-like syntax fmt.printfln("%[1]d %[0]d", 13, 37) // C-like syntax
fmt.printf("{1:d} {0:d}\n", 13, 37); // Python-like syntax fmt.printfln("{1:d} {0:d}", 13, 37) // Python-like syntax
prints "37 13", whilst: prints "37 13", whilst:
fmt.printf("%[2]*.[1]*[0]f\n", 17.0, 2, 6); // C-like syntax fmt.printfln("%*[2].*[1][0]f", 17.0, 2, 6) // C-like syntax
fmt.printf("%{0:[2]*.[1]*f}\n", 17.0, 2, 6); // Python-like syntax fmt.printfln("{0:*[2].*[1]f}", 17.0, 2, 6) // Python-like syntax
equivalent to: is equivalent to:
fmt.printf("%6.2f\n", 17.0, 2, 6); // C-like syntax fmt.printfln("%6.2f", 17.0) // C-like syntax
fmt.printf("{:6.2f}\n", 17.0, 2, 6); // Python-like syntax fmt.printfln("{:6.2f}", 17.0) // Python-like syntax
prints "17.00" and prints "17.00".
Format errors: Format errors:
+156 -191
View File
@@ -25,8 +25,6 @@ Info :: struct {
prec: int, prec: int,
indent: int, indent: int,
reordered: bool,
good_arg_index: bool,
ignore_user_formatters: bool, ignore_user_formatters: bool,
in_bad: bool, in_bad: bool,
@@ -527,13 +525,107 @@ wprintln :: proc(w: io.Writer, args: ..any, sep := " ", flush := true) -> int {
// Returns: The number of bytes written // Returns: The number of bytes written
// //
wprintf :: proc(w: io.Writer, fmt: string, args: ..any, flush := true, newline := false) -> int { wprintf :: proc(w: io.Writer, fmt: string, args: ..any, flush := true, newline := false) -> int {
MAX_CHECKED_ARGS :: 64
assert(len(args) <= MAX_CHECKED_ARGS, "number of args > 64 is unsupported")
parse_options :: proc(fi: ^Info, fmt: string, index, end: int, unused_args: ^bit_set[0 ..< MAX_CHECKED_ARGS], args: ..any) -> int {
i := index
// Prefix
prefix_loop: for ; i < end; i += 1 {
switch fmt[i] {
case '+':
fi.plus = true
case '-':
fi.minus = true
fi.zero = false
case ' ':
fi.space = true
case '#':
fi.hash = true
case '0':
fi.zero = !fi.minus
case:
break prefix_loop
}
}
// Width
if i < end && fmt[i] == '*' {
i += 1
width_index, _, index_ok := _arg_number(fmt, &i, len(args))
if index_ok {
unused_args^ -= {width_index}
fi.width, _, fi.width_set = int_from_arg(args, width_index)
if !fi.width_set {
io.write_string(fi.writer, "%!(BAD WIDTH)", &fi.n)
}
if fi.width < 0 {
fi.width = -fi.width
fi.minus = true
fi.zero = false
}
}
} else {
fi.width, i, fi.width_set = _parse_int(fmt, i)
}
// Precision
if i < end && fmt[i] == '.' {
i += 1
if i < end && fmt[i] == '*' {
i += 1
precision_index, _, index_ok := _arg_number(fmt, &i, len(args))
if index_ok {
unused_args^ -= {precision_index}
fi.prec, _, fi.prec_set = int_from_arg(args, precision_index)
if fi.prec < 0 {
fi.prec = 0
fi.prec_set = false
}
if !fi.prec_set {
io.write_string(fi.writer, "%!(BAD PRECISION)", &fi.n)
}
}
} else {
prev_i := i
fi.prec, i, fi.prec_set = _parse_int(fmt, i)
if i == prev_i {
fi.prec = 0
fi.prec_set = true
}
}
}
return i
}
error_check_arg :: proc(fi: ^Info, arg_parsed: bool, unused_args: bit_set[0 ..< MAX_CHECKED_ARGS]) -> (int, bool) {
if !arg_parsed {
for index in unused_args {
return index, true
}
io.write_string(fi.writer, "%!(MISSING ARGUMENT)", &fi.n)
} else {
io.write_string(fi.writer, "%!(BAD ARGUMENT NUMBER)", &fi.n)
}
return 0, false
}
fi: Info fi: Info
arg_index: int = 0
end := len(fmt) end := len(fmt)
was_prev_index := false unused_args: bit_set[0 ..< MAX_CHECKED_ARGS]
for i in 0 ..< len(args) {
unused_args += {i}
}
loop: for i := 0; i < end; /**/ { loop: for i := 0; i < end; /**/ {
fi = Info{writer = w, good_arg_index = true, reordered = fi.reordered, n = fi.n} fi = Info{writer = w, n = fi.n}
prev_i := i prev_i := i
for i < end && !(fmt[i] == '%' || fmt[i] == '{' || fmt[i] == '}') { for i < end && !(fmt[i] == '%' || fmt[i] == '{' || fmt[i] == '}') {
@@ -567,191 +659,65 @@ wprintf :: proc(w: io.Writer, fmt: string, args: ..any, flush := true, newline :
} }
if char == '%' { if char == '%' {
prefix_loop: for ; i < end; i += 1 { if i < end && fmt[i] == '%' {
switch fmt[i] { io.write_byte(fi.writer, '%', &fi.n)
case '+':
fi.plus = true
case '-':
fi.minus = true
fi.zero = false
case ' ':
fi.space = true
case '#':
fi.hash = true
case '0':
fi.zero = !fi.minus
case:
break prefix_loop
}
}
arg_index, i, was_prev_index = _arg_number(&fi, arg_index, fmt, i, len(args))
// Width
if i < end && fmt[i] == '*' {
i += 1 i += 1
fi.width, arg_index, fi.width_set = int_from_arg(args, arg_index) continue loop
if !fi.width_set {
io.write_string(w, "%!(BAD WIDTH)", &fi.n)
}
if fi.width < 0 {
fi.width = -fi.width
fi.minus = true
fi.zero = false
}
was_prev_index = false
} else {
fi.width, i, fi.width_set = _parse_int(fmt, i)
if was_prev_index && fi.width_set { // %[6]2d
fi.good_arg_index = false
}
} }
// Precision i = parse_options(&fi, fmt, i, end, &unused_args, ..args)
if i < end && fmt[i] == '.' {
i += 1
if was_prev_index { // %[6].2d
fi.good_arg_index = false
}
if i < end && fmt[i] == '*' {
arg_index, i, was_prev_index = _arg_number(&fi, arg_index, fmt, i, len(args))
i += 1
fi.prec, arg_index, fi.prec_set = int_from_arg(args, arg_index)
if fi.prec < 0 {
fi.prec = 0
fi.prec_set = false
}
if !fi.prec_set {
io.write_string(fi.writer, "%!(BAD PRECISION)", &fi.n)
}
was_prev_index = false
} else {
fi.prec, i, fi.prec_set = _parse_int(fmt, i)
}
}
if !was_prev_index { arg_index, arg_parsed, index_ok := _arg_number(fmt, &i, len(args))
arg_index, i, was_prev_index = _arg_number(&fi, arg_index, fmt, i, len(args))
if !index_ok {
arg_index, index_ok = error_check_arg(&fi, arg_parsed, unused_args)
} }
if i >= end { if i >= end {
io.write_string(fi.writer, "%!(NO VERB)", &fi.n) io.write_string(fi.writer, "%!(NO VERB)", &fi.n)
break loop break loop
} else if fmt[i] == ' ' {
io.write_string(fi.writer, "%!(NO VERB)", &fi.n)
continue loop
} }
verb, w := utf8.decode_rune_in_string(fmt[i:]) verb, w := utf8.decode_rune_in_string(fmt[i:])
i += w i += w
switch { if index_ok {
case verb == '%': unused_args -= {arg_index}
io.write_byte(fi.writer, '%', &fi.n)
case !fi.good_arg_index:
io.write_string(fi.writer, "%!(BAD ARGUMENT NUMBER)", &fi.n)
case arg_index >= len(args):
io.write_string(fi.writer, "%!(MISSING ARGUMENT)", &fi.n)
case:
fmt_arg(&fi, args[arg_index], verb) fmt_arg(&fi, args[arg_index], verb)
arg_index += 1
} }
} else if char == '{' { } else if char == '{' {
arg_index: int
arg_parsed, index_ok: bool
if i < end && fmt[i] != '}' && fmt[i] != ':' { if i < end && fmt[i] != '}' && fmt[i] != ':' {
new_arg_index, new_i, ok := _parse_int(fmt, i) arg_index, i, arg_parsed = _parse_int(fmt, i)
if ok { if arg_parsed {
fi.reordered = true index_ok = 0 <= arg_index && arg_index < len(args)
was_prev_index = true
arg_index = new_arg_index
i = new_i
} else {
io.write_string(fi.writer, "%!(BAD ARGUMENT NUMBER ", &fi.n)
// Skip over the bad argument
start_index := i
for i < end && fmt[i] != '}' && fmt[i] != ':' {
i += 1
}
fmt_arg(&fi, fmt[start_index:i], 'v')
io.write_string(fi.writer, ")", &fi.n)
} }
} }
if !index_ok {
arg_index, index_ok = error_check_arg(&fi, arg_parsed, unused_args)
}
verb: rune = 'v' verb: rune = 'v'
if i < end && fmt[i] == ':' { if i < end && fmt[i] == ':' {
i += 1 i += 1
prefix_loop_percent: for ; i < end; i += 1 { i = parse_options(&fi, fmt, i, end, &unused_args, ..args)
switch fmt[i] {
case '+':
fi.plus = true
case '-':
fi.minus = true
fi.zero = false
case ' ':
fi.space = true
case '#':
fi.hash = true
case '0':
fi.zero = !fi.minus
case:
break prefix_loop_percent
}
}
arg_index, i, was_prev_index = _arg_number(&fi, arg_index, fmt, i, len(args))
// Width
if i < end && fmt[i] == '*' {
i += 1
fi.width, arg_index, fi.width_set = int_from_arg(args, arg_index)
if !fi.width_set {
io.write_string(fi.writer, "%!(BAD WIDTH)", &fi.n)
}
if fi.width < 0 {
fi.width = -fi.width
fi.minus = true
fi.zero = false
}
was_prev_index = false
} else {
fi.width, i, fi.width_set = _parse_int(fmt, i)
if was_prev_index && fi.width_set { // %[6]2d
fi.good_arg_index = false
}
}
// Precision
if i < end && fmt[i] == '.' {
i += 1
if was_prev_index { // %[6].2d
fi.good_arg_index = false
}
if i < end && fmt[i] == '*' {
arg_index, i, was_prev_index = _arg_number(&fi, arg_index, fmt, i, len(args))
i += 1
fi.prec, arg_index, fi.prec_set = int_from_arg(args, arg_index)
if fi.prec < 0 {
fi.prec = 0
fi.prec_set = false
}
if !fi.prec_set {
io.write_string(fi.writer, "%!(BAD PRECISION)", &fi.n)
}
was_prev_index = false
} else {
fi.prec, i, fi.prec_set = _parse_int(fmt, i)
}
}
if !was_prev_index {
arg_index, i, was_prev_index = _arg_number(&fi, arg_index, fmt, i, len(args))
}
if i >= end { if i >= end {
io.write_string(fi.writer, "%!(NO VERB)", &fi.n) io.write_string(fi.writer, "%!(NO VERB)", &fi.n)
break loop break loop
} else if fmt[i] == '}' {
i += 1
io.write_string(fi.writer, "%!(NO VERB)", &fi.n)
continue
} }
w: int = 1 w: int = 1
@@ -770,31 +736,35 @@ wprintf :: proc(w: io.Writer, fmt: string, args: ..any, flush := true, newline :
switch { switch {
case brace != '}': case brace != '}':
io.write_string(fi.writer, "%!(MISSING CLOSE BRACE)", &fi.n) io.write_string(fi.writer, "%!(MISSING CLOSE BRACE)", &fi.n)
case !fi.good_arg_index: case index_ok:
io.write_string(fi.writer, "%!(BAD ARGUMENT NUMBER)", &fi.n)
case arg_index >= len(args):
io.write_string(fi.writer, "%!(MISSING ARGUMENT)", &fi.n)
case:
fmt_arg(&fi, args[arg_index], verb) fmt_arg(&fi, args[arg_index], verb)
arg_index += 1 unused_args -= {arg_index}
} }
} }
} }
if !fi.reordered && arg_index < len(args) { if unused_args != {} {
io.write_string(fi.writer, "%!(EXTRA ", &fi.n) // Use default options when formatting extra arguments.
for arg, index in args[arg_index:] { extra_fi := Info { writer = fi.writer, n = fi.n }
if index > 0 {
io.write_string(fi.writer, ", ", &fi.n) io.write_string(extra_fi.writer, "%!(EXTRA ", &extra_fi.n)
first_printed := false
for index in unused_args {
if first_printed {
io.write_string(extra_fi.writer, ", ", &extra_fi.n)
} }
arg := args[index]
if arg == nil { if arg == nil {
io.write_string(fi.writer, "<nil>", &fi.n) io.write_string(extra_fi.writer, "<nil>", &extra_fi.n)
} else { } else {
fmt_arg(&fi, args[index], 'v') fmt_arg(&extra_fi, arg, 'v')
} }
first_printed = true
} }
io.write_string(fi.writer, ")", &fi.n) io.write_byte(extra_fi.writer, ')', &extra_fi.n)
fi.n = extra_fi.n
} }
if newline { if newline {
@@ -877,18 +847,16 @@ _parse_int :: proc(s: string, offset: int) -> (result: int, new_offset: int, ok:
// Parses an argument number from a format string and determines if it's valid // Parses an argument number from a format string and determines if it's valid
// //
// Inputs: // Inputs:
// - fi: A pointer to an Info structure
// - arg_index: The current argument index
// - format: The format string to parse // - format: The format string to parse
// - offset: The current position in the format string // - offset: A pointer to the current position in the format string
// - arg_count: The total number of arguments // - arg_count: The total number of arguments
// //
// Returns: // Returns:
// - index: The parsed argument index // - index: The parsed argument index
// - new_offset: The new position in the format string // - parsed: A boolean indicating if an argument number was parsed
// - ok: A boolean indicating if the parsed argument number is valid // - ok: A boolean indicating if the parsed argument number is within arg_count
// //
_arg_number :: proc(fi: ^Info, arg_index: int, format: string, offset, arg_count: int) -> (index, new_offset: int, ok: bool) { _arg_number :: proc(format: string, offset: ^int, arg_count: int) -> (index: int, parsed, ok: bool) {
parse_arg_number :: proc(format: string) -> (int, int, bool) { parse_arg_number :: proc(format: string) -> (int, int, bool) {
if len(format) < 3 { if len(format) < 3 {
return 0, 1, false return 0, 1, false
@@ -896,30 +864,28 @@ _arg_number :: proc(fi: ^Info, arg_index: int, format: string, offset, arg_count
for i in 1..<len(format) { for i in 1..<len(format) {
if format[i] == ']' { if format[i] == ']' {
width, new_index, ok := _parse_int(format, 1) value, new_index, ok := _parse_int(format, 1)
if !ok || new_index != i { if !ok || new_index != i {
return 0, i+1, false return 0, i+1, false
} }
return width-1, i+1, true return value, i+1, true
} }
} }
return 0, 1, false return 0, 1, false
} }
i := offset^
if len(format) <= offset || format[offset] != '[' { if len(format) <= i || format[i] != '[' {
return arg_index, offset, false return 0, false, false
} }
fi.reordered = true
width: int width: int
index, width, ok = parse_arg_number(format[offset:]) index, width, parsed = parse_arg_number(format[i:])
if ok && 0 <= index && index < arg_count { offset^ = i + width
return index, offset+width, true ok = parsed && 0 <= index && index < arg_count
} return
fi.good_arg_index = false
return arg_index, offset+width, false
} }
// Retrieves an integer from a list of any type at the specified index // Retrieves an integer from a list of any type at the specified index
// //
@@ -2570,7 +2536,6 @@ fmt_value :: proc(fi: ^Info, v: any, verb: rune) {
if _user_formatters != nil && !fi.ignore_user_formatters { if _user_formatters != nil && !fi.ignore_user_formatters {
formatter := _user_formatters[v.id] formatter := _user_formatters[v.id]
if formatter != nil { if formatter != nil {
fi.ignore_user_formatters = false
if ok := formatter(fi, v, verb); !ok { if ok := formatter(fi, v, verb); !ok {
fi.ignore_user_formatters = true fi.ignore_user_formatters = true
fmt_bad_verb(fi, verb) fmt_bad_verb(fi, verb)
+1 -2
View File
@@ -104,8 +104,7 @@ generic_ftoa :: proc(buf: []byte, val: f64, fmt: byte, precision, bit_size: int)
} else { } else {
switch fmt { switch fmt {
case 'e', 'E': case 'e', 'E':
prec += 1 decimal.round(d, prec + 1)
decimal.round(d, prec)
case 'f', 'F': case 'f', 'F':
decimal.round(d, d.decimal_point+prec) decimal.round(d, d.decimal_point+prec)
case 'g', 'G': case 'g', 'G':
+130 -6
View File
@@ -29,6 +29,8 @@ when ODIN_TEST {
main :: proc() { main :: proc() {
t := testing.T{} t := testing.T{}
test_fmt_memory(&t) test_fmt_memory(&t)
test_fmt_doc_examples(&t)
test_fmt_options(&t)
fmt.printf("%v/%v tests successful.\n", TEST_count - TEST_fail, TEST_count) fmt.printf("%v/%v tests successful.\n", TEST_count - TEST_fail, TEST_count)
if TEST_fail > 0 { if TEST_fail > 0 {
@@ -36,12 +38,13 @@ main :: proc() {
} }
} }
test_fmt_memory :: proc(t: ^testing.T) { check :: proc(t: ^testing.T, exp: string, format: string, args: ..any, loc := #caller_location) {
check :: proc(t: ^testing.T, exp: string, format: string, args: ..any, loc := #caller_location) { got := fmt.tprintf(format, ..args)
got := fmt.tprintf(format, ..args) expect(t, got == exp, fmt.tprintf("(%q, %v): %q != %q", format, args, got, exp), loc)
expect(t, got == exp, fmt.tprintf("(%q, %v): %q != %q", format, args, got, exp), loc) }
}
@(test)
test_fmt_memory :: proc(t: ^testing.T) {
check(t, "5b", "%m", 5) check(t, "5b", "%m", 5)
check(t, "5B", "%M", 5) check(t, "5B", "%M", 5)
check(t, "-5B", "%M", -5) check(t, "-5B", "%M", -5)
@@ -52,8 +55,129 @@ test_fmt_memory :: proc(t: ^testing.T) {
check(t, "3.50 gib", "%#m", u32(mem.Gigabyte * 3.5)) check(t, "3.50 gib", "%#m", u32(mem.Gigabyte * 3.5))
check(t, "01tib", "%5.0m", mem.Terabyte) check(t, "01tib", "%5.0m", mem.Terabyte)
check(t, "-1tib", "%5.0m", -mem.Terabyte) check(t, "-1tib", "%5.0m", -mem.Terabyte)
check(t, "2.50 pib", "%#5.m", uint(mem.Petabyte * 2.5)) check(t, "2 pib", "%#5.m", uint(mem.Petabyte * 2.5))
check(t, "1.00 EiB", "%#M", mem.Exabyte) check(t, "1.00 EiB", "%#M", mem.Exabyte)
check(t, "255 B", "%#M", u8(255)) check(t, "255 B", "%#M", u8(255))
check(t, "0b", "%m", u8(0)) check(t, "0b", "%m", u8(0))
} }
@(test)
test_fmt_doc_examples :: proc(t: ^testing.T) {
// C-like syntax
check(t, "37 13", "%[1]d %[0]d", 13, 37)
check(t, "017.00", "%*[2].*[1][0]f", 17.0, 2, 6)
check(t, "017.00", "%6.2f", 17.0)
// Python-like syntax
check(t, "37 13", "{1:d} {0:d}", 13, 37)
check(t, "017.00", "{0:*[2].*[1]f}", 17.0, 2, 6)
check(t, "017.00", "{:6.2f}", 17.0)
}
@(test)
test_fmt_options :: proc(t: ^testing.T) {
// Escaping
check(t, "% { } 0 { } } {", "%% {{ }} {} {{ }} }} {{", 0 )
// Prefixes
check(t, "+3.000", "%+f", 3.0 )
check(t, "0003", "%04i", 3 )
check(t, "3 ", "% -4i", 3 )
check(t, "+3", "%+i", 3 )
check(t, "0b11", "%#b", 3 )
check(t, "0xA", "%#X", 10 )
// Specific index formatting
check(t, "1 2 3", "%i %i %i", 1, 2, 3)
check(t, "1 2 3", "%[0]i %[1]i %[2]i", 1, 2, 3)
check(t, "3 2 1", "%[2]i %[1]i %[0]i", 1, 2, 3)
check(t, "3 1 2", "%[2]i %i %i", 1, 2, 3)
check(t, "1 2 3", "%i %[1]i %i", 1, 2, 3)
check(t, "1 3 2", "%i %[2]i %i", 1, 2, 3)
check(t, "1 1 1", "%[0]i %[0]i %[0]i", 1)
// Width
check(t, "3.140", "%f", 3.14)
check(t, "3.140", "%4f", 3.14)
check(t, "3.140", "%5f", 3.14)
check(t, "03.140", "%6f", 3.14)
// Precision
check(t, "3", "%.f", 3.14)
check(t, "3", "%.0f", 3.14)
check(t, "3.1", "%.1f", 3.14)
check(t, "3.140", "%.3f", 3.14)
check(t, "3.14000", "%.5f", 3.14)
check(t, "3.1415", "%g", 3.1415)
// Scientific notation
check(t, "3.000000e+00", "%e", 3.0)
check(t, "3e+02", "%.e", 300.0)
check(t, "3e+02", "%.0e", 300.0)
check(t, "3.0e+02", "%.1e", 300.0)
check(t, "3.00e+02", "%.2e", 300.0)
check(t, "3.000e+02", "%.3e", 300.0)
check(t, "3e+01", "%.e", 30.56)
check(t, "3e+01", "%.0e", 30.56)
check(t, "3.1e+01", "%.1e", 30.56)
check(t, "3.06e+01", "%.2e", 30.56)
check(t, "3.056e+01", "%.3e", 30.56)
// Width and precision
check(t, "3.140", "%5.3f", 3.14)
check(t, "3.140", "%*[1].3f", 3.14, 5)
check(t, "3.140", "%*[1].*[2]f", 3.14, 5, 3)
check(t, "3.140", "%*[1].*[2][0]f", 3.14, 5, 3)
check(t, "3.140", "%*[2].*[1]f", 3.14, 3, 5)
check(t, "3.140", "%5.*[1]f", 3.14, 3)
// Error checking
check(t, "%!(MISSING ARGUMENT)%!(NO VERB)", "%" )
check(t, "1%!(EXTRA 2, 3)", "%i", 1, 2, 3)
check(t, "2%!(EXTRA 1, 3)", "%[1]i", 1, 2, 3)
check(t, "%!(BAD ARGUMENT NUMBER)%!(EXTRA 0)", "%[1]i", 0)
check(t, "%!(MISSING ARGUMENT)", "%f")
check(t, "%!(BAD ARGUMENT NUMBER)%!(NO VERB)", "%[0]")
check(t, "%!(BAD ARGUMENT NUMBER)", "%[0]f")
check(t, "%!(BAD ARGUMENT NUMBER)%!(NO VERB) %!(MISSING ARGUMENT)", "%[0] %i")
check(t, "%!(NO VERB) 1%!(EXTRA 2)", "%[0] %i", 1, 2)
check(t, "1 2 %!(MISSING ARGUMENT)", "%i %i %i", 1, 2)
check(t, "1 2 %!(BAD ARGUMENT NUMBER)", "%i %i %[2]i", 1, 2)
check(t, "%!(BAD ARGUMENT NUMBER)%!(NO VERB)%!(EXTRA 0)", "%[1]", 0)
check(t, "3.1%!(EXTRA 3.14)", "%.1f", 3.14, 3.14)
// Python-like syntax
check(t, "1 2 3", "{} {} {}", 1, 2, 3)
check(t, "3 2 1", "{2} {1} {0}", 1, 2, 3)
check(t, "1 2 3", "{:i} {:i} {:i}", 1, 2, 3)
check(t, "1 2 3", "{0:i} {1:i} {2:i}", 1, 2, 3)
check(t, "3 2 1", "{2:i} {1:i} {0:i}", 1, 2, 3)
check(t, "3 1 2", "{2:i} {0:i} {1:i}", 1, 2, 3)
check(t, "1 2 3", "{:i} {1:i} {:i}", 1, 2, 3)
check(t, "1 3 2", "{:i} {2:i} {:i}", 1, 2, 3)
check(t, "1 1 1", "{0:i} {0:i} {0:i}", 1)
check(t, "1 1%!(EXTRA 2)", "{} {0}", 1, 2)
check(t, "2 1", "{1} {}", 1, 2)
check(t, "%!(BAD ARGUMENT NUMBER) 1%!(EXTRA 2)", "{2} {}", 1, 2)
check(t, "%!(BAD ARGUMENT NUMBER)", "{1}")
check(t, "%!(BAD ARGUMENT NUMBER)%!(NO VERB)", "{1:}")
check(t, "%!(BAD ARGUMENT NUMBER)%!(NO VERB)%!(EXTRA 0)", "{1:}", 0)
check(t, "%!(MISSING ARGUMENT)", "{}" )
check(t, "%!(MISSING ARGUMENT)%!(MISSING CLOSE BRACE)", "{" )
check(t, "%!(MISSING CLOSE BRACE)%!(EXTRA 1)", "{", 1)
check(t, "%!(MISSING CLOSE BRACE)%!(EXTRA 1)", "{0", 1 )
}