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"}, "