diff --git a/docs/src/manpage.md b/docs/src/manpage.md index 17c8fdf14..871becd15 100644 --- a/docs/src/manpage.md +++ b/docs/src/manpage.md @@ -2748,11 +2748,13 @@ This is simply a copy of what you should see on running `man mlr` at a command p Map example: fold({"a":1, "b":3, "c": 5}, func(acck,accv,ek,ev) {return {"sum": accv+ev**2}}, {"sum":10000}) returns 10035. 1mformat0m - (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded. + (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Also supports 1-based positional placeholders such as "{1}", which allow arguments to be reused and/or reordered. Plain "{}" placeholders consume arguments sequentially, independently of any positional placeholders. Too-few arguments are treated as the empty string, as are positional placeholders exceeding the number of arguments; too-many arguments are discarded. "{0}" is an error value, since positional placeholders are 1-based. Examples: - format("{}:{}:{}", 1,2) gives "1:2:". - format("{}:{}:{}", 1,2,3) gives "1:2:3". - format("{}:{}:{}", 1,2,3,4) gives "1:2:3". + format("{}:{}:{}", 1,2) gives "1:2:". + format("{}:{}:{}", 1,2,3) gives "1:2:3". + format("{}:{}:{}", 1,2,3,4) gives "1:2:3". + format("{1}:{2}:{1}", "a","b") gives "a:b:a". + format("{2}{}:{1}{}", 3,4) gives "43:34". 1mfsec2dhms0m (class=time #args=1) Formats floating-point seconds as in fsec2dhms(500000.25) = "5d18h53m20.250000s" diff --git a/docs/src/manpage.txt b/docs/src/manpage.txt index 307a8bb39..8bcb44aac 100644 --- a/docs/src/manpage.txt +++ b/docs/src/manpage.txt @@ -2727,11 +2727,13 @@ Map example: fold({"a":1, "b":3, "c": 5}, func(acck,accv,ek,ev) {return {"sum": accv+ev**2}}, {"sum":10000}) returns 10035. 1mformat0m - (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded. + (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Also supports 1-based positional placeholders such as "{1}", which allow arguments to be reused and/or reordered. Plain "{}" placeholders consume arguments sequentially, independently of any positional placeholders. Too-few arguments are treated as the empty string, as are positional placeholders exceeding the number of arguments; too-many arguments are discarded. "{0}" is an error value, since positional placeholders are 1-based. Examples: - format("{}:{}:{}", 1,2) gives "1:2:". - format("{}:{}:{}", 1,2,3) gives "1:2:3". - format("{}:{}:{}", 1,2,3,4) gives "1:2:3". + format("{}:{}:{}", 1,2) gives "1:2:". + format("{}:{}:{}", 1,2,3) gives "1:2:3". + format("{}:{}:{}", 1,2,3,4) gives "1:2:3". + format("{1}:{2}:{1}", "a","b") gives "a:b:a". + format("{2}{}:{1}{}", 3,4) gives "43:34". 1mfsec2dhms0m (class=time #args=1) Formats floating-point seconds as in fsec2dhms(500000.25) = "5d18h53m20.250000s" diff --git a/docs/src/reference-dsl-builtin-functions.md b/docs/src/reference-dsl-builtin-functions.md index e77395981..ad729f793 100644 --- a/docs/src/reference-dsl-builtin-functions.md +++ b/docs/src/reference-dsl-builtin-functions.md @@ -1256,11 +1256,13 @@ contains([1,2,3], 2) gives (error) ### format
-format  (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded.
+format  (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Also supports 1-based positional placeholders such as "{1}", which allow arguments to be reused and/or reordered. Plain "{}" placeholders consume arguments sequentially, independently of any positional placeholders. Too-few arguments are treated as the empty string, as are positional placeholders exceeding the number of arguments; too-many arguments are discarded. "{0}" is an error value, since positional placeholders are 1-based.
 Examples:
-format("{}:{}:{}", 1,2)     gives "1:2:".
-format("{}:{}:{}", 1,2,3)   gives "1:2:3".
-format("{}:{}:{}", 1,2,3,4) gives "1:2:3".
+format("{}:{}:{}", 1,2)        gives "1:2:".
+format("{}:{}:{}", 1,2,3)      gives "1:2:3".
+format("{}:{}:{}", 1,2,3,4)    gives "1:2:3".
+format("{1}:{2}:{1}", "a","b") gives "a:b:a".
+format("{2}{}:{1}{}", 3,4)     gives "43:34".
 
diff --git a/man/manpage.txt b/man/manpage.txt index 307a8bb39..8bcb44aac 100644 --- a/man/manpage.txt +++ b/man/manpage.txt @@ -2727,11 +2727,13 @@ Map example: fold({"a":1, "b":3, "c": 5}, func(acck,accv,ek,ev) {return {"sum": accv+ev**2}}, {"sum":10000}) returns 10035. 1mformat0m - (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded. + (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Also supports 1-based positional placeholders such as "{1}", which allow arguments to be reused and/or reordered. Plain "{}" placeholders consume arguments sequentially, independently of any positional placeholders. Too-few arguments are treated as the empty string, as are positional placeholders exceeding the number of arguments; too-many arguments are discarded. "{0}" is an error value, since positional placeholders are 1-based. Examples: - format("{}:{}:{}", 1,2) gives "1:2:". - format("{}:{}:{}", 1,2,3) gives "1:2:3". - format("{}:{}:{}", 1,2,3,4) gives "1:2:3". + format("{}:{}:{}", 1,2) gives "1:2:". + format("{}:{}:{}", 1,2,3) gives "1:2:3". + format("{}:{}:{}", 1,2,3,4) gives "1:2:3". + format("{1}:{2}:{1}", "a","b") gives "a:b:a". + format("{2}{}:{1}{}", 3,4) gives "43:34". 1mfsec2dhms0m (class=time #args=1) Formats floating-point seconds as in fsec2dhms(500000.25) = "5d18h53m20.250000s" diff --git a/man/mlr.1 b/man/mlr.1 index 56ad0cb92..108dff8ea 100644 --- a/man/mlr.1 +++ b/man/mlr.1 @@ -3733,11 +3733,13 @@ Map example: fold({"a":1, "b":3, "c": 5}, func(acck,accv,ek,ev) {return {"sum": .RS 0 .\} .nf - (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded. + (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Also supports 1-based positional placeholders such as "{1}", which allow arguments to be reused and/or reordered. Plain "{}" placeholders consume arguments sequentially, independently of any positional placeholders. Too-few arguments are treated as the empty string, as are positional placeholders exceeding the number of arguments; too-many arguments are discarded. "{0}" is an error value, since positional placeholders are 1-based. Examples: -format("{}:{}:{}", 1,2) gives "1:2:". -format("{}:{}:{}", 1,2,3) gives "1:2:3". -format("{}:{}:{}", 1,2,3,4) gives "1:2:3". +format("{}:{}:{}", 1,2) gives "1:2:". +format("{}:{}:{}", 1,2,3) gives "1:2:3". +format("{}:{}:{}", 1,2,3,4) gives "1:2:3". +format("{1}:{2}:{1}", "a","b") gives "a:b:a". +format("{2}{}:{1}{}", 3,4) gives "43:34". .fi .if n \{\ .RE diff --git a/pkg/bifs/strings.go b/pkg/bifs/strings.go index 4fbc7e1d0..db15097ff 100644 --- a/pkg/bifs/strings.go +++ b/pkg/bifs/strings.go @@ -381,6 +381,10 @@ func BIF_clean_whitespace(input1 *mlrval.Mlrval) *mlrval.Mlrval { return mlrval.FromInferredType(mv.String()) } +// Matches either a sequential placeholder "{}" or a positional placeholder +// like "{1}", "{2}", etc. +var _format_placeholder_regexp = regexp.MustCompile(`\{[0-9]*\}`) + func BIF_format(mlrvals []*mlrval.Mlrval) *mlrval.Mlrval { if len(mlrvals) == 0 { return mlrval.VOID @@ -390,38 +394,62 @@ func BIF_format(mlrvals []*mlrval.Mlrval) *mlrval.Mlrval { return mlrval.FromTypeErrorUnary("format", mlrvals[0]) } - pieces := lib.SplitString(formatString, "{}") + // Example: format("{}:{}", 8, 9) gives "8:9". + // + // Placeholders come in two forms: + // * "{}": consumes the next argument in sequence. The sequence counter is + // advanced only by "{}" placeholders, independently of any positional + // placeholders (same as Rust's format!). + // * "{N}" with N >= 1: refers to the Nth argument after the format string + // (1-based). Arguments may be repeated and/or reordered this way: + // format("{1}:{2}:{1}", 8, 9) gives "8:9:8". + // + // Q: What if too few arguments for format, or a positional index exceeds + // the number of arguments? + // A: Interpolate the empty string. + // Q: What if too many arguments for format? + // A: Leave them off. + // Q: What about "{0}"? + // A: It's an error value, since positional placeholders are 1-based. var buffer bytes.Buffer - // Example: format("{}:{}", 8, 9) - // - // * piece[0] "" - // * piece[1] ":" - // * piece[2] "" - // * mlrval[1] 8 - // * mlrval[2] 9 - // - // So: - // * Write piece[0] - // * Write mlrvals[1] - // * Write piece[1] - // * Write mlrvals[2] - // * Write piece[2] + n := len(mlrvals) // arguments are mlrvals[1] .. mlrvals[n-1] + position := 1 // implicit counter for "{}" placeholders + remaining := formatString - // Q: What if too few arguments for format? - // A: Leave them off - // Q: What if too many arguments for format? - // A: Leave them off - - n := len(mlrvals) - for i, piece := range pieces { - if i > 0 { - if i < n { - buffer.WriteString(mlrvals[i].String()) - } + for { + loc := _format_placeholder_regexp.FindStringIndex(remaining) + if loc == nil { + buffer.WriteString(remaining) + break } - buffer.WriteString(piece) + buffer.WriteString(remaining[:loc[0]]) + placeholder := remaining[loc[0]:loc[1]] + remaining = remaining[loc[1]:] + + var index int + if placeholder == "{}" { + index = position + position++ + } else { + numString := placeholder[1 : len(placeholder)-1] + num, err := strconv.Atoi(numString) + if err != nil || num < 1 { + return mlrval.FromError( + fmt.Errorf( + "format: positional placeholder %s is invalid: indices are 1-based", + placeholder, + ), + ) + } + index = num + } + + if index < n { + buffer.WriteString(mlrvals[index].String()) + } + // Else, too few arguments: interpolate the empty string. } return mlrval.FromString(buffer.String()) diff --git a/pkg/bifs/strings_test.go b/pkg/bifs/strings_test.go new file mode 100644 index 000000000..3492662f6 --- /dev/null +++ b/pkg/bifs/strings_test.go @@ -0,0 +1,76 @@ +package bifs + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/johnkerl/miller/v6/pkg/mlrval" +) + +func TestBIF_format(t *testing.T) { + // No arguments at all + output := BIF_format([]*mlrval.Mlrval{}) + assert.True(t, output.IsVoid()) + + // Non-string format string + output = BIF_format([]*mlrval.Mlrval{mlrval.FromInt(1)}) + assert.True(t, output.IsError()) + + // Sequential placeholders + cases := []struct { + formatString string + args []string + expected string + }{ + // Plain "{}" placeholders (pre-existing behavior) + {"", []string{}, ""}, + {"abc", []string{}, "abc"}, + {"{}", []string{}, ""}, + {"{}", []string{"1"}, "1"}, + {"{}", []string{"1", "2"}, "1"}, + {"{}:{}:{}", []string{"1", "2"}, "1:2:"}, + {"{}:{}:{}", []string{"1", "2", "3"}, "1:2:3"}, + {"{}:{}:{}", []string{"1", "2", "3", "4"}, "1:2:3"}, + {"<{}:{}>", []string{"abc"}, ""}, + {"<{}:{}>", []string{"abc", "def"}, ""}, + + // Positional "{N}" placeholders + {"{1}", []string{"a"}, "a"}, + {"{1}:{1}", []string{"a"}, "a:a"}, + {"{2}:{1}", []string{"a", "b"}, "b:a"}, + {"{1}/{2}/{1}_{3}.ext", []string{"a", "b", "c"}, "a/b/a_c.ext"}, + // Out-of-range positional index interpolates the empty string, + // consistent with too-few arguments for "{}" + {"{3}", []string{"a"}, ""}, + {"<{4}>", []string{"a", "b"}, "<>"}, + // Leading zeros are accepted + {"{01}", []string{"a"}, "a"}, + + // Mixing: "{}" consumes arguments sequentially, independently of + // positional placeholders + {"{2}{}:{1}{}", []string{"3", "4"}, "43:34"}, + {"{}{1}{}", []string{"a", "b"}, "aab"}, + + // Non-placeholder braces are left alone + {"{ }", []string{"a"}, "{ }"}, + {"{x}", []string{"a"}, "{x}"}, + {"{", []string{"a"}, "{"}, + {"}", []string{"a"}, "}"}, + } + + for _, c := range cases { + mlrvals := []*mlrval.Mlrval{mlrval.FromString(c.formatString)} + for _, arg := range c.args { + mlrvals = append(mlrvals, mlrval.FromString(arg)) + } + output := BIF_format(mlrvals) + stringval, ok := output.GetStringValue() + assert.True(t, ok, "format(%q, %v)", c.formatString, c.args) + assert.Equal(t, c.expected, stringval, "format(%q, %v)", c.formatString, c.args) + } + + // "{0}" is an error, since positional placeholders are 1-based + output = BIF_format([]*mlrval.Mlrval{mlrval.FromString("{0}"), mlrval.FromString("a")}) + assert.True(t, output.IsError()) +} diff --git a/pkg/dsl/cst/builtin_function_manager.go b/pkg/dsl/cst/builtin_function_manager.go index e58fb34ff..953717308 100644 --- a/pkg/dsl/cst/builtin_function_manager.go +++ b/pkg/dsl/cst/builtin_function_manager.go @@ -696,11 +696,17 @@ Arrays are new in Miller 6; the substr function is older.`, name: "format", class: FUNC_CLASS_STRING, help: `Using first argument as format string, interpolate remaining arguments in place of -each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded.`, +each "{}" in the format string. Also supports 1-based positional placeholders such as "{1}", which allow +arguments to be reused and/or reordered. Plain "{}" placeholders consume arguments sequentially, +independently of any positional placeholders. Too-few arguments are treated as the empty string, as are +positional placeholders exceeding the number of arguments; too-many arguments are discarded. "{0}" is an +error value, since positional placeholders are 1-based.`, examples: []string{ - `format("{}:{}:{}", 1,2) gives "1:2:".`, - `format("{}:{}:{}", 1,2,3) gives "1:2:3".`, - `format("{}:{}:{}", 1,2,3,4) gives "1:2:3".`, + `format("{}:{}:{}", 1,2) gives "1:2:".`, + `format("{}:{}:{}", 1,2,3) gives "1:2:3".`, + `format("{}:{}:{}", 1,2,3,4) gives "1:2:3".`, + `format("{1}:{2}:{1}", "a","b") gives "a:b:a".`, + `format("{2}{}:{1}{}", 3,4) gives "43:34".`, }, variadicFunc: bifs.BIF_format, }, diff --git a/test/cases/dsl-format/0011/cmd b/test/cases/dsl-format/0011/cmd new file mode 100644 index 000000000..6add080d4 --- /dev/null +++ b/test/cases/dsl-format/0011/cmd @@ -0,0 +1 @@ +mlr -n put -f ${CASEDIR}/mlr diff --git a/test/cases/dsl-format/0011/experr b/test/cases/dsl-format/0011/experr new file mode 100644 index 000000000..e69de29bb diff --git a/test/cases/dsl-format/0011/expout b/test/cases/dsl-format/0011/expout new file mode 100644 index 000000000..73c31a83b --- /dev/null +++ b/test/cases/dsl-format/0011/expout @@ -0,0 +1 @@ +a/b/a_c.ext diff --git a/test/cases/dsl-format/0011/mlr b/test/cases/dsl-format/0011/mlr new file mode 100644 index 000000000..5564dc098 --- /dev/null +++ b/test/cases/dsl-format/0011/mlr @@ -0,0 +1,3 @@ +end { + print format("{1}/{2}/{1}_{3}.ext", "a", "b", "c") +} diff --git a/test/cases/dsl-format/0012/cmd b/test/cases/dsl-format/0012/cmd new file mode 100644 index 000000000..6add080d4 --- /dev/null +++ b/test/cases/dsl-format/0012/cmd @@ -0,0 +1 @@ +mlr -n put -f ${CASEDIR}/mlr diff --git a/test/cases/dsl-format/0012/experr b/test/cases/dsl-format/0012/experr new file mode 100644 index 000000000..e69de29bb diff --git a/test/cases/dsl-format/0012/expout b/test/cases/dsl-format/0012/expout new file mode 100644 index 000000000..5473de45b --- /dev/null +++ b/test/cases/dsl-format/0012/expout @@ -0,0 +1 @@ +b:a diff --git a/test/cases/dsl-format/0012/mlr b/test/cases/dsl-format/0012/mlr new file mode 100644 index 000000000..f0560f735 --- /dev/null +++ b/test/cases/dsl-format/0012/mlr @@ -0,0 +1,3 @@ +end { + print format("{2}:{1}", "a", "b") +} diff --git a/test/cases/dsl-format/0013/cmd b/test/cases/dsl-format/0013/cmd new file mode 100644 index 000000000..6add080d4 --- /dev/null +++ b/test/cases/dsl-format/0013/cmd @@ -0,0 +1 @@ +mlr -n put -f ${CASEDIR}/mlr diff --git a/test/cases/dsl-format/0013/experr b/test/cases/dsl-format/0013/experr new file mode 100644 index 000000000..e69de29bb diff --git a/test/cases/dsl-format/0013/expout b/test/cases/dsl-format/0013/expout new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/cases/dsl-format/0013/expout @@ -0,0 +1 @@ + diff --git a/test/cases/dsl-format/0013/mlr b/test/cases/dsl-format/0013/mlr new file mode 100644 index 000000000..7bb7073d6 --- /dev/null +++ b/test/cases/dsl-format/0013/mlr @@ -0,0 +1,3 @@ +end { + print format("{3}", 1) +} diff --git a/test/cases/dsl-format/0014/cmd b/test/cases/dsl-format/0014/cmd new file mode 100644 index 000000000..6add080d4 --- /dev/null +++ b/test/cases/dsl-format/0014/cmd @@ -0,0 +1 @@ +mlr -n put -f ${CASEDIR}/mlr diff --git a/test/cases/dsl-format/0014/experr b/test/cases/dsl-format/0014/experr new file mode 100644 index 000000000..e69de29bb diff --git a/test/cases/dsl-format/0014/expout b/test/cases/dsl-format/0014/expout new file mode 100644 index 000000000..ffba14cee --- /dev/null +++ b/test/cases/dsl-format/0014/expout @@ -0,0 +1 @@ +(error) diff --git a/test/cases/dsl-format/0014/mlr b/test/cases/dsl-format/0014/mlr new file mode 100644 index 000000000..16b3b0ff4 --- /dev/null +++ b/test/cases/dsl-format/0014/mlr @@ -0,0 +1,3 @@ +end { + print format("{0}", 1) +} diff --git a/test/cases/dsl-format/0015/cmd b/test/cases/dsl-format/0015/cmd new file mode 100644 index 000000000..6add080d4 --- /dev/null +++ b/test/cases/dsl-format/0015/cmd @@ -0,0 +1 @@ +mlr -n put -f ${CASEDIR}/mlr diff --git a/test/cases/dsl-format/0015/experr b/test/cases/dsl-format/0015/experr new file mode 100644 index 000000000..e69de29bb diff --git a/test/cases/dsl-format/0015/expout b/test/cases/dsl-format/0015/expout new file mode 100644 index 000000000..dd3be91ed --- /dev/null +++ b/test/cases/dsl-format/0015/expout @@ -0,0 +1 @@ +43:34 diff --git a/test/cases/dsl-format/0015/mlr b/test/cases/dsl-format/0015/mlr new file mode 100644 index 000000000..8c288f8a6 --- /dev/null +++ b/test/cases/dsl-format/0015/mlr @@ -0,0 +1,3 @@ +end { + print format("{2}{}:{1}{}", 3, 4) +} diff --git a/test/cases/dsl-format/0016/cmd b/test/cases/dsl-format/0016/cmd new file mode 100644 index 000000000..6add080d4 --- /dev/null +++ b/test/cases/dsl-format/0016/cmd @@ -0,0 +1 @@ +mlr -n put -f ${CASEDIR}/mlr diff --git a/test/cases/dsl-format/0016/experr b/test/cases/dsl-format/0016/experr new file mode 100644 index 000000000..e69de29bb diff --git a/test/cases/dsl-format/0016/expout b/test/cases/dsl-format/0016/expout new file mode 100644 index 000000000..4bc88feba --- /dev/null +++ b/test/cases/dsl-format/0016/expout @@ -0,0 +1 @@ +{ }:{x} diff --git a/test/cases/dsl-format/0016/mlr b/test/cases/dsl-format/0016/mlr new file mode 100644 index 000000000..5b768760f --- /dev/null +++ b/test/cases/dsl-format/0016/mlr @@ -0,0 +1,3 @@ +end { + print format("{ }:{x}", "a") +} diff --git a/test/cases/help/0012/expout b/test/cases/help/0012/expout index 0a5403b5c..506c4d4de 100644 --- a/test/cases/help/0012/expout +++ b/test/cases/help/0012/expout @@ -27,11 +27,13 @@ Options: -n Coerce field values autodetected as int to float, and then apply the float format. -h|--help Show this message. -format (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded. +format (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Also supports 1-based positional placeholders such as "{1}", which allow arguments to be reused and/or reordered. Plain "{}" placeholders consume arguments sequentially, independently of any positional placeholders. Too-few arguments are treated as the empty string, as are positional placeholders exceeding the number of arguments; too-many arguments are discarded. "{0}" is an error value, since positional placeholders are 1-based. Examples: -format("{}:{}:{}", 1,2) gives "1:2:". -format("{}:{}:{}", 1,2,3) gives "1:2:3". -format("{}:{}:{}", 1,2,3,4) gives "1:2:3". +format("{}:{}:{}", 1,2) gives "1:2:". +format("{}:{}:{}", 1,2,3) gives "1:2:3". +format("{}:{}:{}", 1,2,3,4) gives "1:2:3". +format("{1}:{2}:{1}", "a","b") gives "a:b:a". +format("{2}{}:{1}{}", 3,4) gives "43:34". unformat (class=string #args=2) Using first argument as format string, unpacks second argument into an array of matches, with type-inference. On non-match, returns error -- use is_error() to check. Examples: unformat("{}:{}:{}", "1:2:3") gives [1, 2, 3]. diff --git a/test/cases/repl-help/0012/expout b/test/cases/repl-help/0012/expout index 580c754e0..ccb50a216 100644 --- a/test/cases/repl-help/0012/expout +++ b/test/cases/repl-help/0012/expout @@ -13,11 +13,13 @@ For-loop over out-of-stream variables: C-style for-loop: Example: 'for (var i = 0, var b = 1; i < 10; i += 1, b *= 2) { ... }' -format (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded. +format (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Also supports 1-based positional placeholders such as "{1}", which allow arguments to be reused and/or reordered. Plain "{}" placeholders consume arguments sequentially, independently of any positional placeholders. Too-few arguments are treated as the empty string, as are positional placeholders exceeding the number of arguments; too-many arguments are discarded. "{0}" is an error value, since positional placeholders are 1-based. Examples: -format("{}:{}:{}", 1,2) gives "1:2:". -format("{}:{}:{}", 1,2,3) gives "1:2:3". -format("{}:{}:{}", 1,2,3,4) gives "1:2:3". +format("{}:{}:{}", 1,2) gives "1:2:". +format("{}:{}:{}", 1,2,3) gives "1:2:3". +format("{}:{}:{}", 1,2,3,4) gives "1:2:3". +format("{1}:{2}:{1}", "a","b") gives "a:b:a". +format("{2}{}:{1}{}", 3,4) gives "43:34". unformat (class=string #args=2) Using first argument as format string, unpacks second argument into an array of matches, with type-inference. On non-match, returns error -- use is_error() to check. Examples: unformat("{}:{}:{}", "1:2:3") gives [1, 2, 3].