Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions docs/usage/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,19 @@ Evaluates arguments in-order, choosing the first non-empty result.

Syntax: `{select {0} 1}`

Assuming that `{0}` is a whitespace-separated value, split the values and select the item at index `1`
Assuming that `{0}` is a whitespace-separated value, split the values and select the item at index `1`,
respecting quotes.

Eg. `{select "ab cd ef" 1}` will result in `cd`

#### Pick

Syntax: `{pick {0} "delim" {index}}`

Given a string, and a delimiter, select the string at a given index.

Eg. `{pick "a,b,c" , 1}` will return `b`

#### Bucket

Syntax: `{bucket intVal "bucketSize"}`
Expand Down Expand Up @@ -255,10 +264,24 @@ Formats a string based on `fmt.Sprintf`: [Go Docs](https://pkg.go.dev/fmt)

#### Substring

Syntax: `{substr {0} pos length}`
Syntax: `{substr {0} pos [length]}`

Takes the substring of the first argument starting at `pos` for `length`

If `pos` is `< 0`, will wrap-around at the length of the string.

If `length` is omitted or `< 0`, will return up to the end of the string.

#### Index, LastIndex

Syntax: `{index {0} {of}}`, `{lastindex {0} {of}}`

Returns the numeric index of a substring within a string.

*lastindex* returns the index of the last occurrence of a substring.

Example: `{index abcdef c}` returns `2`

#### Upper, Lower

Syntax: `{upper val}`, `{lower val}`
Expand Down
23 changes: 13 additions & 10 deletions pkg/expressions/stdlib/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,19 @@ var StandardFunctions = map[string]KeyBuilderFunction{
"or": KeyBuilderFunction(kfOr),

// Strings
"len": KeyBuilderFunction(kfLen),
"like": KeyBuilderFunction(kfLike),
"prefix": KeyBuilderFunction(kfPrefix),
"suffix": KeyBuilderFunction(kfSuffix),
"format": KeyBuilderFunction(kfFormat),
"substr": KeyBuilderFunction(kfSubstr),
"select": KeyBuilderFunction(kfSelect),
"upper": KeyBuilderFunction(kfUpper),
"lower": KeyBuilderFunction(kfLower),
"replace": KeyBuilderFunction(kfReplace),
"len": KeyBuilderFunction(kfLen),
"like": KeyBuilderFunction(kfLike),
"prefix": KeyBuilderFunction(kfPrefix),
"suffix": KeyBuilderFunction(kfSuffix),
"format": KeyBuilderFunction(kfFormat),
"substr": KeyBuilderFunction(kfSubstr),
"select": KeyBuilderFunction(kfSelect),
"index": kfIndexOf,
"lastindex": kfLastIndexOf,
"pick": kfPick,
"upper": KeyBuilderFunction(kfUpper),
"lower": KeyBuilderFunction(kfLower),
"replace": KeyBuilderFunction(kfReplace),

// Separation (Join)
"tab": kfJoin('\t'),
Expand Down
104 changes: 95 additions & 9 deletions pkg/expressions/stdlib/funcsStrings.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"github.com/zix99/rare/pkg/humanize"
"github.com/zix99/rare/pkg/stringSplitter"

. "github.com/zix99/rare/pkg/expressions" //lint:ignore ST1001 Legacy
)
Expand Down Expand Up @@ -70,10 +71,26 @@ func kfLower(args []KeyBuilderStage) (KeyBuilderStage, error) {
}, nil
}

// {substr {0} left len}
// {substr {0} left [len]}
func kfSubstr(args []KeyBuilderStage) (KeyBuilderStage, error) {
if len(args) != 3 {
return stageErrArgCount(args, 3)
if !isArgCountBetween(args, 2, 3) {
return stageErrArgRange(args, "2-3")
}

leftArg, leftOk := evalTypedStage(args[1], typedParserInt)
if !leftOk {
return stageArgError(ErrNum, 1)
}

var lengthArg typedStage[int]
if len(args) >= 3 {
var lengthOk bool
lengthArg, lengthOk = evalTypedStage(args[2], typedParserInt)
if !lengthOk {
return stageArgError(ErrNum, 2)
}
} else {
lengthArg = typedLiteral(-1)
}

return KeyBuilderStage(func(context KeyBuilderContext) string {
Expand All @@ -83,15 +100,12 @@ func kfSubstr(args []KeyBuilderStage) (KeyBuilderStage, error) {
return ""
}

left, err1 := strconv.Atoi(args[1](context))
length, err2 := strconv.Atoi(args[2](context))
if err1 != nil || err2 != nil {
left, leftOk := leftArg(context)
length, lengthOk := lengthArg(context)
if !leftOk || !lengthOk {
return ErrorNum
}

if length < 0 {
length = 0
}
if left < 0 { // negative number wrap-around
left += lenS
if left < 0 {
Expand All @@ -101,6 +115,11 @@ func kfSubstr(args []KeyBuilderStage) (KeyBuilderStage, error) {
left = lenS
}

// length wrap-around
if length < 0 {
length = lenS - left
}

right := left + length

if right > lenS {
Expand All @@ -110,6 +129,73 @@ func kfSubstr(args []KeyBuilderStage) (KeyBuilderStage, error) {
}), nil
}

// {index {0} {search}}
func kfIndexOf(args []KeyBuilderStage) (KeyBuilderStage, error) {
if len(args) != 2 {
return stageErrArgCount(args, 2)
}

return func(context KeyBuilderContext) string {
s := args[0](context)
search := args[1](context)

idx := strings.Index(s, search)

return strconv.Itoa(idx)
}, nil
}

// {lastindex {0} {search}}
func kfLastIndexOf(args []KeyBuilderStage) (KeyBuilderStage, error) {
if len(args) != 2 {
return stageErrArgCount(args, 2)
}

return func(context KeyBuilderContext) string {
s := args[0](context)
search := args[1](context)

idx := strings.LastIndex(s, search)

return strconv.Itoa(idx)
}, nil
}

// {pick {0} "delim" idx}
func kfPick(args []KeyBuilderStage) (KeyBuilderStage, error) {
if len(args) != 3 {
return stageErrArgCount(args, 3)
}

delim, delimOk := EvalStaticStage(args[1])
if !delimOk {
return stageArgError(ErrConst, 1)
}

idx, idxOk := evalTypedStage(args[2], typedParserInt)
if !idxOk {
return stageArgError(ErrNum, 2)
}

return func(context KeyBuilderContext) string {
splitter := stringSplitter.Splitter{
S: args[0](context),
Delim: delim,
}

offset, offsetOk := idx(context)
if !offsetOk {
return ErrorNum
}

item := splitter.Next()
for range offset {
item = splitter.Next()
}
return item
}, nil
}

// {select {0} 1}
func kfSelect(args []KeyBuilderStage) (KeyBuilderStage, error) {
if len(args) != 2 {
Expand Down
34 changes: 31 additions & 3 deletions pkg/expressions/stdlib/funcsStrings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,36 @@ func TestReplace(t *testing.T) {
testExpressionErr(t, mockContext(), "{replace a} {replace a b} {replace a b c d}", "<ARGN> <ARGN> <ARGN>", ErrArgCount)
}

func TestIndexOf(t *testing.T) {
testExpression(t, mockContext(), "{index abcdeaba b} {index abc x}", "1 -1")
testExpressionErr(t, mockContext(), "{index a} {index a b c}", "<ARGN> <ARGN>", ErrArgCount)
}

func TestLastIndexOf(t *testing.T) {
testExpression(t, mockContext(), "{lastindex abcdeaba b} {lastindex abc x}", "6 -1")
testExpressionErr(t, mockContext(), "{lastindex a} {lastindex a b c}", "<ARGN> <ARGN>", ErrArgCount)
}

func TestPick(t *testing.T) {
testExpression(t,
mockContext("apple,banana,cherry", "one|two|three|four"),
"{pick {0} , 0} {pick {0} , 1} {pick {0} , 2} {pick {1} | 2} {pick {1} | 10}",
"apple banana cherry three ")
testExpressionErr(t, mockContext(), "{pick a}", "<ARGN>", ErrArgCount)
testExpressionErr(t, mockContext(","), "{pick {0} , a}", "<BAD-TYPE>", ErrNum)
testExpressionErr(t, mockContext("abc", ","), "{pick {0} {1} 0}", "<CONST>", ErrConst)

testExpression(t, mockContext("abc", ","), "{pick {0} , {0}}", "<BAD-TYPE>")
}

func TestSubstring(t *testing.T) {
testExpression(t,
mockContext("abcd"),
"{substr {0} 0 2} {substr {0} 0 10} {substr {0} 3 2} {substr {0} 3 1}",
"ab abcd d d")
testExpressionErr(t,
mockContext("abcd"),
"{substr 0}", "<ARGN>", ErrArgCount)
testExpression(t, mockContext("abcd"), "{substr {0} 1} {substr {0} 3} {substr {0} 4} {substr {0} 10} {substr {0} 1 -1}", "bcd d bcd")
testExpressionErr(t, mockContext("abcd"), "{substr 0}", "<ARGN>", ErrArgCount)
testExpressionErr(t, mockContext(), "{substr abc 0 a}", "<BAD-TYPE>", ErrNum)
}

func TestSubstringOutOfBounds(t *testing.T) {
Expand Down Expand Up @@ -143,3 +165,9 @@ func BenchmarkSelectItem(b *testing.B) {
func BenchmarkPercent(b *testing.B) {
benchmarkExpression(b, mockContext(), "{percent 50 1 0 100}", "50.0%")
}

// BenchmarkSubstring/{substr_{0}_6_2}-4 30473634 36.31 ns/op 0 B/op 0 allocs/op
// BenchmarkSubstring/{substr_{0}_6_2}-4 45346548 22.51 ns/op 0 B/op 0 allocs/op
func BenchmarkSubstring(b *testing.B) {
benchmarkExpression(b, mockContext("hello to you"), "{substr {0} 6 2}", "to")
}
Loading