diff --git a/crstrings/utils.go b/crstrings/utils.go new file mode 100644 index 0000000..1e90d7d --- /dev/null +++ b/crstrings/utils.go @@ -0,0 +1,99 @@ +// Copyright 2024 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package crstrings + +import ( + "fmt" + "slices" + "strings" +) + +// JoinStringers concatenates the string representations of the given +// fmt.Stringer implementations. +func JoinStringers[T fmt.Stringer](delim string, args ...T) string { + switch len(args) { + case 0: + return "" + case 1: + return args[0].String() + } + elems := make([]string, len(args)) + for i := range args { + elems[i] = args[i].String() + } + return strings.Join(elems, delim) +} + +// MapAndJoin converts each argument to a string using the given function and +// joins the strings with the given delimiter. +func MapAndJoin[T any](fn func(T) string, delim string, args ...T) string { + switch len(args) { + case 0: + return "" + case 1: + return fn(args[0]) + } + elems := make([]string, len(args)) + for i := range args { + elems[i] = fn(args[i]) + } + return strings.Join(elems, delim) +} + +// If returns the given value if the flag is true, otherwise an empty string. +func If(flag bool, trueValue string) string { + return IfElse(flag, trueValue, "") +} + +// IfElse returns the value that matches the value of the flag. +func IfElse(flag bool, trueValue, falseValue string) string { + if flag { + return trueValue + } + return falseValue +} + +// WithSep prints the strings a and b with the given separator in-between, +// unless one of the strings is empty (in which case the other string is +// returned). +func WithSep(a string, separator string, b string) string { + if a == "" { + return b + } + if b == "" { + return a + } + return strings.Join([]string{a, b}, separator) +} + +// FilterEmpty removes empty strings from the given slice. +func FilterEmpty(elems []string) []string { + return slices.DeleteFunc(elems, func(s string) bool { + return s == "" + }) +} + +// Lines breaks up the given string into lines. +func Lines(s string) []string { + // Remove any trailing newline (to avoid getting an extraneous empty line at + // the end). + s = strings.TrimSuffix(s, "\n") + if s == "" { + // In this case, Split returns a slice with a single empty string (which is + // not what we want). + return nil + } + return strings.Split(s, "\n") +} diff --git a/crstrings/utils_test.go b/crstrings/utils_test.go new file mode 100644 index 0000000..2f86f7e --- /dev/null +++ b/crstrings/utils_test.go @@ -0,0 +1,83 @@ +// Copyright 2024 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package crstrings + +import ( + "fmt" + "strings" + "testing" +) + +type num int + +func (n num) String() string { + return fmt.Sprintf("%03d", int(n)) +} + +func TestJoinStringers(t *testing.T) { + nums := []num{0, 1, 2, 3} + expect(t, "", JoinStringers(", ", nums[:0]...)) + expect(t, "000", JoinStringers(", ", nums[0])) + expect(t, "000, 001", JoinStringers(", ", nums[0], nums[1])) + expect(t, "000, 001, 002, 003", JoinStringers(", ", nums...)) +} + +func TestMapAndJoin(t *testing.T) { + nums := []int{0, 1, 2, 3} + fn := func(n int) string { + return fmt.Sprintf("%d", n) + } + expect(t, "", MapAndJoin(fn, ", ", nums[:0]...)) + expect(t, "0", MapAndJoin(fn, ", ", nums[0])) + expect(t, "0, 1", MapAndJoin(fn, ", ", nums[0], nums[1])) + expect(t, "0, 1, 2, 3", MapAndJoin(fn, ", ", nums...)) +} + +func expect(t *testing.T, expected, actual string) { + t.Helper() + if actual != expected { + t.Errorf("expected %q got %q", expected, actual) + } +} + +func TestIf(t *testing.T) { + expect(t, "", If(false, "true")) + expect(t, "true", If(true, "true")) +} + +func TestIfElse(t *testing.T) { + expect(t, "false", IfElse(false, "true", "false")) + expect(t, "true", IfElse(true, "true", "false")) +} + +func TestWithSep(t *testing.T) { + expect(t, "a,b", WithSep("a", ",", "b")) + expect(t, "a", WithSep("a", ",", "")) + expect(t, "b", WithSep("", ",", "b")) +} + +func TestFilterEmpty(t *testing.T) { + s := []string{"a", "", "b", "", "c", ""} + expect(t, "a,b,c", strings.Join(FilterEmpty(s), ",")) +} + +func TestLines(t *testing.T) { + expect(t, `["a" "b" "c"]`, fmt.Sprintf("%q", Lines("a\nb\nc"))) + expect(t, `["a" "b" "c"]`, fmt.Sprintf("%q", Lines("a\nb\nc\n"))) + expect(t, `["a" "b" "c" ""]`, fmt.Sprintf("%q", Lines("a\nb\nc\n\n"))) + expect(t, `["" "a" "b" "c"]`, fmt.Sprintf("%q", Lines("\na\nb\nc\n"))) + expect(t, `[]`, fmt.Sprintf("%q", Lines(""))) + expect(t, `[]`, fmt.Sprintf("%q", Lines("\n"))) +}