mirror of
https://github.com/johnkerl/miller.git
synced 2026-07-17 16:38:54 +00:00
Plain {} placeholders consume arguments sequentially as before; {N}
refers to the Nth argument (1-based) and may be used to repeat and/or
reorder arguments, e.g. format("{1}/{2}/{1}_{3}.ext", $p1, $p2, $p3).
Mixing is allowed: the {} sequence counter is independent of positional
placeholders (as in Rust format!). Out-of-range indices interpolate the
empty string, consistent with existing too-few-arguments behavior; {0}
is an error value since indices are 1-based.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a8ee858e60
commit
1004ba5097
34 changed files with 213 additions and 59 deletions
|
|
@ -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())
|
||||
|
|
|
|||
76
pkg/bifs/strings_test.go
Normal file
76
pkg/bifs/strings_test.go
Normal file
|
|
@ -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"}, "<abc:>"},
|
||||
{"<{}:{}>", []string{"abc", "def"}, "<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())
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue