2
\$\begingroup\$

This is my custom function to join strings from an array into one string. We can provide normal separator and the last one. JoinStringsIntoArray(", ", " and ", words...)

func JoinStringsIntoArray(sep string, lastSep string, words ...string) string {
lastElementIndex := len(words) - 1
var joinedString string

for index, word := range words[0:lastElementIndex] {
    if index == lastElementIndex-1 {
        sep = lastSep
    }
    joinedString += word + sep
}

joinedString += words[lastElementIndex]

    return joinedString
}

Test for this function

func TestStringsJoinsWithCommas(t *testing.T) {
    var words []string
    words = append(words, "one", "two", "three")

    expectedResult := "one, two and three"
    result := JoinStringsIntoArray(", ", " and ", words...)

    if expectedResult != result {
        t.Errorf("Strings joiner is broken, it should be '%v', we got '%v'", expectedResult, result)
    }
}

What do you think about this, can this solution be improved? The function works fine but I'm not so sure it's written well.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

The Go standard library strings package has a Join function:

func Join(elems []string, sep string) string

Your function is a simple extension of strings.Join:

func JoinLast(elems []string, sep, lastSep string) string

However, for no obvious reason, you chose a different function parameter signature:

func JoinLast(sep string, lastSep string, words ...string) string

We need a comment to explain why this isn't an arbitrary or idiosyncratic decision.


Your testing is inadequate. Check boundary conditions. Your function fails on an empty words list.

words = []string{}

You have not provided any testing benchmarks. For example,

BenchmarkJoin-4   5534918  207.9 ns/op   104 B/op  5 allocs/op
func JoinStringsIntoArray(sep string, lastSep string, words ...string) string {
    lastElementIndex := len(words) - 1
    var joinedString string

    for index, word := range words[0:lastElementIndex] {
        if index == lastElementIndex-1 {
            sep = lastSep
        }
        joinedString += word + sep
    }

    joinedString += words[lastElementIndex]

    return joinedString
}

func BenchmarkJoin(b *testing.B) {
    words := []string{"one", "two", "three", "four", "five"}
    b.ReportAllocs()
    b.ResetTimer()
    for N := 0; N < b.N; N++ {
        JoinStringsIntoArray(", ", " and ", words...)
    }
}

You can do better. For example, minimize allocation,

BenchmarkJoin-4  16612879   70.95 ns/op   32 B/op  1 allocs/op
func JoinLast(elems []string, sep, lastSep string) string {
    var join strings.Builder
    joinCap := 0
    for i := 0; i < len(elems); i++ {
        joinCap += len(elems[i])
    }
    if len(elems) > 1 {
        joinCap += len(lastSep)
        if len(elems) > 2 {
            joinCap += (len(elems) - 2) * len(sep)
        }
    }
    join.Grow(joinCap)

    last := len(elems) - 1 - 1
    for i, elem := range elems {
        if i >= last {
            if i == last {
                sep = lastSep
            } else {
                sep = ""
            }
        }
        join.WriteString(elem)
        join.WriteString(sep)
    }

    return join.String()
}

func BenchmarkJoin(b *testing.B) {
    words := []string{"one", "two", "three", "four", "five"}
    b.ReportAllocs()
    b.ResetTimer()
    for N := 0; N < b.N; N++ {
        JoinLast(words, ", ", " and ")
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.