diff --git a/core/fmt/doc.odin b/core/fmt/doc.odin index 597342e76..be666dcc4 100644 --- a/core/fmt/doc.odin +++ b/core/fmt/doc.odin @@ -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. Printing @@ -33,6 +33,8 @@ Floating-point, complex numbers, and quaternions: %E scientific notation, e.g. -1.23456E+78 %f decimal point but no exponent, e.g. 123.456 %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 (upper-case) representation with 0H prefix (0h01234ABCD) %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, ...} 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. -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. A period with no following number specifies a precision of 0. @@ -75,7 +77,7 @@ Examples: %8.f width 8, precision 0 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: @@ -92,7 +94,7 @@ Other flags: 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 @@ -105,19 +107,20 @@ Explicit argument indices: 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 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. -Python-like syntax with argument indices differs for the selecting the argument index: {N:v} +The same notation before an '*' for a width or precision specifier selects the argument index +holding the value. +Python-like syntax with argument indices differs for selecting the argument index: {n:v} Examples: - fmt.printf("%[1]d %[0]d\n", 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) // C-like syntax + fmt.printfln("{1:d} {0:d}", 13, 37) // Python-like syntax prints "37 13", whilst: - fmt.printf("%[2]*.[1]*[0]f\n", 17.0, 2, 6); // C-like syntax - fmt.printf("%{0:[2]*.[1]*f}\n", 17.0, 2, 6); // Python-like syntax -equivalent to: - fmt.printf("%6.2f\n", 17.0, 2, 6); // C-like syntax - fmt.printf("{:6.2f}\n", 17.0, 2, 6); // Python-like syntax -prints "17.00" + fmt.printfln("%*[2].*[1][0]f", 17.0, 2, 6) // C-like syntax + fmt.printfln("{0:*[2].*[1]f}", 17.0, 2, 6) // Python-like syntax +is equivalent to: + fmt.printfln("%6.2f", 17.0) // C-like syntax + fmt.printfln("{:6.2f}", 17.0) // Python-like syntax +and prints "17.00". Format errors: diff --git a/core/fmt/fmt.odin b/core/fmt/fmt.odin index 867257491..772d3dd9c 100644 --- a/core/fmt/fmt.odin +++ b/core/fmt/fmt.odin @@ -25,8 +25,6 @@ Info :: struct { prec: int, indent: int, - reordered: bool, - good_arg_index: bool, ignore_user_formatters: 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 // 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 - arg_index: int = 0 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; /**/ { - fi = Info{writer = w, good_arg_index = true, reordered = fi.reordered, n = fi.n} + fi = Info{writer = w, n = fi.n} prev_i := 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 == '%' { - 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 - } - } - - arg_index, i, was_prev_index = _arg_number(&fi, arg_index, fmt, i, len(args)) - - // Width - if i < end && fmt[i] == '*' { + if i < end && fmt[i] == '%' { + io.write_byte(fi.writer, '%', &fi.n) i += 1 - fi.width, arg_index, fi.width_set = int_from_arg(args, arg_index) - 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 - } + continue loop } - // 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) - } - } + i = parse_options(&fi, fmt, i, end, &unused_args, ..args) - if !was_prev_index { - arg_index, i, was_prev_index = _arg_number(&fi, arg_index, fmt, i, len(args)) + arg_index, arg_parsed, index_ok := _arg_number(fmt, &i, len(args)) + + if !index_ok { + arg_index, index_ok = error_check_arg(&fi, arg_parsed, unused_args) } if i >= end { io.write_string(fi.writer, "%!(NO VERB)", &fi.n) 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:]) i += w - switch { - case verb == '%': - 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: + if index_ok { + unused_args -= {arg_index} fmt_arg(&fi, args[arg_index], verb) - arg_index += 1 } } else if char == '{' { + arg_index: int + arg_parsed, index_ok: bool + if i < end && fmt[i] != '}' && fmt[i] != ':' { - new_arg_index, new_i, ok := _parse_int(fmt, i) - if ok { - fi.reordered = true - 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) + arg_index, i, arg_parsed = _parse_int(fmt, i) + if arg_parsed { + index_ok = 0 <= arg_index && arg_index < len(args) } } + if !index_ok { + arg_index, index_ok = error_check_arg(&fi, arg_parsed, unused_args) + } + verb: rune = 'v' if i < end && fmt[i] == ':' { i += 1 - prefix_loop_percent: 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_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)) - } - + i = parse_options(&fi, fmt, i, end, &unused_args, ..args) if i >= end { io.write_string(fi.writer, "%!(NO VERB)", &fi.n) break loop + } else if fmt[i] == '}' { + i += 1 + io.write_string(fi.writer, "%!(NO VERB)", &fi.n) + continue } w: int = 1 @@ -770,31 +736,35 @@ wprintf :: proc(w: io.Writer, fmt: string, args: ..any, flush := true, newline : switch { case brace != '}': io.write_string(fi.writer, "%!(MISSING CLOSE BRACE)", &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: + case index_ok: fmt_arg(&fi, args[arg_index], verb) - arg_index += 1 + unused_args -= {arg_index} } } } - if !fi.reordered && arg_index < len(args) { - io.write_string(fi.writer, "%!(EXTRA ", &fi.n) - for arg, index in args[arg_index:] { - if index > 0 { - io.write_string(fi.writer, ", ", &fi.n) + if unused_args != {} { + // Use default options when formatting extra arguments. + extra_fi := Info { writer = fi.writer, n = 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 { - io.write_string(fi.writer, "", &fi.n) + io.write_string(extra_fi.writer, "", &extra_fi.n) } 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 { @@ -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 // // Inputs: -// - fi: A pointer to an Info structure -// - arg_index: The current argument index // - 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 // // Returns: // - index: The parsed argument index -// - new_offset: The new position in the format string -// - ok: A boolean indicating if the parsed argument number is valid +// - parsed: A boolean indicating if an argument number was parsed +// - 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) { if len(format) < 3 { 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.. 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) { - got := fmt.tprintf(format, ..args) - expect(t, got == exp, fmt.tprintf("(%q, %v): %q != %q", format, args, got, exp), loc) - } +check :: proc(t: ^testing.T, exp: string, format: string, args: ..any, loc := #caller_location) { + got := fmt.tprintf(format, ..args) + 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) @@ -52,8 +55,129 @@ test_fmt_memory :: proc(t: ^testing.T) { check(t, "3.50 gib", "%#m", u32(mem.Gigabyte * 3.5)) check(t, "01tib", "%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, "255 B", "%#M", u8(255)) 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 ) +}