From 1e88ec55fcc83fb23c9982155c1ad8d591b012f4 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 6 May 2024 17:54:21 +0100 Subject: [PATCH] Add `FormatSlice` function Signed-off-by: Babak K. Shandiz --- internal/text/text.go | 63 ++++++++++++++++++++++++++ internal/text/text_test.go | 92 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/internal/text/text.go b/internal/text/text.go index 9ecfad93b..c14071c32 100644 --- a/internal/text/text.go +++ b/internal/text/text.go @@ -2,8 +2,10 @@ package text import ( "fmt" + "math" "net/url" "regexp" + "slices" "strings" "time" @@ -81,3 +83,64 @@ func RemoveDiacritics(value string) string { func PadRight(maxWidth int, s string) string { return text.PadRight(maxWidth, s) } + +// FormatSlice concatenates elements of the given string slice into a +// well-formatted, possibly multiline, string with specific line length limit. +// Elements can be optionally surrounded by custom strings (e.g., quotes or +// brackets). If the lineLength argument is non-positive, no line length limit +// will be applied. +func FormatSlice(values []string, lineLength uint, indent uint, prependWith string, appendWith string, sort bool) string { + if lineLength <= 0 { + lineLength = math.MaxInt + } + + sortedValues := values + if sort { + sortedValues = slices.Clone(values) + slices.Sort(sortedValues) + } + + pre := strings.Repeat(" ", int(indent)) + if len(sortedValues) == 0 { + return pre + } else if len(sortedValues) == 1 { + return pre + sortedValues[0] + } + + builder := strings.Builder{} + currentLineLength := 0 + sep := "," + ws := " " + + for i := 0; i < len(sortedValues); i++ { + v := prependWith + sortedValues[i] + appendWith + isLast := i == -1+len(sortedValues) + + if currentLineLength == 0 { + builder.WriteString(pre) + builder.WriteString(v) + currentLineLength += len(v) + if !isLast { + builder.WriteString(sep) + currentLineLength += len(sep) + } + } else { + if !isLast && currentLineLength+len(ws)+len(v)+len(sep) > int(lineLength) || + isLast && currentLineLength+len(ws)+len(v) > int(lineLength) { + currentLineLength = 0 + builder.WriteString("\n") + i-- + continue + } + + builder.WriteString(ws) + builder.WriteString(v) + currentLineLength += len(ws) + len(v) + if !isLast { + builder.WriteString(sep) + currentLineLength += len(sep) + } + } + } + return builder.String() +} diff --git a/internal/text/text_test.go b/internal/text/text_test.go index 98f16da5d..6d76d4872 100644 --- a/internal/text/text_test.go +++ b/internal/text/text_test.go @@ -54,3 +54,95 @@ func TestFuzzyAgoAbbr(t *testing.T) { assert.Equal(t, expected, fuzzy) } } + +func TestFormatSliceDoc(t *testing.T) { + tests := []struct { + name string + values []string + indent uint + lineLength uint + prependWith string + appendWith string + sort bool + wants string + }{ + { + name: "empty", + lineLength: 10, + values: []string{}, + wants: "", + }, + { + name: "empty with indent", + lineLength: 10, + indent: 2, + values: []string{}, + wants: " ", + }, + { + name: "single", + lineLength: 10, + values: []string{"foo"}, + wants: "foo", + }, + { + name: "single with indent", + lineLength: 10, + indent: 2, + values: []string{"foo"}, + wants: " foo", + }, + { + name: "long single with indent", + lineLength: 10, + indent: 2, + values: []string{"some-long-value"}, + wants: " some-long-value", + }, + { + name: "exact line length", + lineLength: 4, + values: []string{"a", "b"}, + wants: "a, b", + }, + { + name: "values longer than line length", + lineLength: 4, + values: []string{"long-value", "long-value"}, + wants: "long-value,\nlong-value", + }, + { + name: "zero line length (no wrapping expected)", + lineLength: 0, + values: []string{"foo", "bar"}, + wants: "foo, bar", + }, + { + name: "simple", + lineLength: 10, + values: []string{"foo", "bar", "baz", "foo", "bar", "baz"}, + wants: "foo, bar,\nbaz, foo,\nbar, baz", + }, + { + name: "simple, surrounded", + lineLength: 13, + prependWith: "<", + appendWith: ">", + values: []string{"foo", "bar", "baz", "foo", "bar", "baz"}, + wants: ", ,\n, ,\n, ", + }, + { + name: "sort", + lineLength: 99, + sort: true, + values: []string{"c", "b", "a"}, + wants: "a, b, c", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.wants, FormatSlice(tt.values, tt.lineLength, tt.indent, tt.prependWith, tt.appendWith, tt.sort)) + }) + } +}