XXX
XXX

Reputation: 633

check for equality on slices without order

I am trying to find a solution to check for equality in 2 slices. Unfortanely, the answers I have found require values in the slice to be in the same order. For example, http://play.golang.org/p/yV0q1_u3xR evaluates equality to false.
I want a solution that lets []string{"a","b","c"} == []string{"b","a","c"} evaluate to true.
MORE EXAMPLES
[]string{"a","a","c"} == []string{"c","a","c"} >>> false
[]string{"z","z","x"} == []string{"x","z","z"} >>> true

Upvotes: 37

Views: 63640

Answers (9)

sberry
sberry

Reputation: 131978

Here is an alternate solution, though perhaps a bit verbose:

func sameStringSlice(x, y []string) bool {
    if len(x) != len(y) {
        return false
    }
    // create a map of string -> int
    diff := make(map[string]int, len(x))
    for _, _x := range x {
        // 0 value for int is 0, so just increment a counter for the string
        diff[_x]++
    }
    for _, _y := range y {
        // If the string _y is not in diff bail out early
        if _, ok := diff[_y]; !ok {
            return false
        }
        diff[_y]--
        if diff[_y] == 0 {
            delete(diff, _y)
        }
    }
    return len(diff) == 0
}

Try it on the Go Playground

Upvotes: 22

Pablo Bustos Aedo
Pablo Bustos Aedo

Reputation: 1

Order in slices matters. Make them something different where it doesn't. Then, you can use reflect. This implementation also takes in consideration that elements can be repeated.

func slicesEqual(want, got []string) bool {
    w := map[string]int{}
    g := map[string]int{}

    for _, v := range want {
        if _, ok := w[v]; !ok {
            w[v] = 1
            continue
        }
        w[v]++
    }

    for _, v := range got {
        if _, ok := g[v]; !ok {
            g[v] = 1
            continue
        }
        g[v]++
    }

    return reflect.DeepEqual(w, g)
}

Upvotes: 0

Johan Wikström
Johan Wikström

Reputation: 4239

You can use cmp.Diff together with cmpopts.SortSlices:

less := func(a, b string) bool { return a < b }
equalIgnoreOrder := cmp.Diff(x, y, cmpopts.SortSlices(less)) == ""

Here is a full example that runs on the Go Playground:

package main

import (
    "fmt"

    "github.com/google/go-cmp/cmp"
    "github.com/google/go-cmp/cmp/cmpopts"
)

func main() {
    x := []string{"a", "b", "c"}
    y := []string{"a", "c", "b"}
    
    less := func(a, b string) bool { return a < b }
    equalIgnoreOrder := cmp.Diff(x, y, cmpopts.SortSlices(less)) == ""
    fmt.Println(equalIgnoreOrder) // prints "true"

}

Upvotes: 18

Rafał Krypa
Rafał Krypa

Reputation: 353

Like adrianlzt wrote in his answer, an implementation of assert.ElementsMatch from testify can be used to achieve that. But how about reusing actual testify module instead of copying that code when all you need is a bool result of the comparison? The implementation in testify is intended for tests code and usually takes testing.T argument.

It turns out that ElementsMatch can be quite easily used outside of testing code. All it takes is a dummy implementation of an interface with ErrorF method:

type dummyt struct{}

func (t dummyt) Errorf(string, ...interface{}) {}

func elementsMatch(listA, listB interface{}) bool {
    return assert.ElementsMatch(dummyt{}, listA, listB)
}

Or test it on The Go Playground, which I've adapted from the adrianlzt's example.

Upvotes: 16

MathObsessed
MathObsessed

Reputation: 529

Since I haven't got enough reputation to comment, I have to post yet another answer with a bit better code readability:

func AssertSameStringSlice(x, y []string) bool {
    if len(x) != len(y) {
        return false
    }

    itemAppearsTimes := make(map[string]int, len(x))
    for _, i := range x {
        itemAppearsTimes[i]++
    }

    for _, i := range y {
        if _, ok := itemAppearsTimes[i]; !ok {
            return false
        }

        itemAppearsTimes[i]--

        if itemAppearsTimes[i] == 0 {
            delete(itemAppearsTimes, i)
        }
    }

    if len(itemAppearsTimes) == 0 {
        return true
    }

    return false
}

The logic is the same as in this answer

Upvotes: 1

adrianlzt
adrianlzt

Reputation: 2031

Generalizing the code of testify ElementsMatch, solution to compare any kind of objects (in the example []map[string]string):

https://play.golang.org/p/xUS2ngrUWUl

Upvotes: 8

Dhruv Pal
Dhruv Pal

Reputation: 957

I know its been answered but still I would like to add my answer. By following code here stretchr/testify we can have something like

  func Elementsmatch(listA, listB []string) (string, bool) {
    aLen := len(listA)
    bLen := len(listB)

    if aLen != bLen {
        return fmt.Sprintf("Len of the lists don't match , len listA %v, len listB %v", aLen, bLen), false
    }

    visited := make([]bool, bLen)

    for i := 0; i < aLen; i++ {
        found := false
        element := listA[i]
        for j := 0; j < bLen; j++ {
            if visited[j] {
                continue
            }
            if element == listB[j] {
                visited[j] = true
                found = true
                break
            }
        }
        if !found {
            return fmt.Sprintf("element %s appears more times in %s than in %s", element, listA, listB), false
        }
    }
    return "", true
}

Now lets talk about performance of this solution compared to map based ones. Well it really depends on the size of the lists which you are comparing, If size of list is large (I would say greater than 20) then map approach is better else this would be sufficent.

Well on Go PlayGround it shows 0s always, but run this on local system and you can see the difference in time taken as size of list increases

So the solution I propose is, adding map based comparision from above solution

func Elementsmatch(listA, listB []string) (string, bool) {
    aLen := len(listA)
    bLen := len(listB)

    if aLen != bLen {
        return fmt.Sprintf("Len of the lists don't match , len listA %v, len listB %v", aLen, bLen), false
    }

    if aLen > 20 {
        return elementsMatchByMap(listA, listB)
    }else{
        return elementsMatchByLoop(listA, listB)
    }

}

func elementsMatchByLoop(listA, listB []string) (string, bool) {
    aLen := len(listA)
    bLen := len(listB)

    visited := make([]bool, bLen)

    for i := 0; i < aLen; i++ {
        found := false
        element := listA[i]
        for j := 0; j < bLen; j++ {
            if visited[j] {
                continue
            }
            if element == listB[j] {
                visited[j] = true
                found = true
                break
            }
        }
        if !found {
            return fmt.Sprintf("element %s appears more times in %s than in %s", element, listA, listB), false
        }
    }
    return "", true
}

func elementsMatchByMap(x, y []string) (string, bool) {
    // create a map of string -> int
    diff := make(map[string]int, len(x))
    for _, _x := range x {
        // 0 value for int is 0, so just increment a counter for the string
        diff[_x]++
    }
    for _, _y := range y {
        // If the string _y is not in diff bail out early
        if _, ok := diff[_y]; !ok {
            return fmt.Sprintf(" %v is not present in list b", _y), false
        }
        diff[_y] -= 1
        if diff[_y] == 0 {
            delete(diff, _y)
        }
    }
    if len(diff) == 0 {
        return "", true
    }
    return "", false
}

Upvotes: 0

Akavall
Akavall

Reputation: 86168

The other answers have better time complexity O(N) vs (O(N log(N)), that are in my answer, also my solution will take up more memory if elements in the slices are repeated frequently, but I wanted to add it because I think this is the most straight forward way to do it:

package main

import (
    "fmt"
    "sort"
    "reflect"
)

func array_sorted_equal(a, b []string) bool {
    if len(a) != len(b) {return false }

    a_copy := make([]string, len(a))
    b_copy := make([]string, len(b))

    copy(a_copy, a)
    copy(b_copy, b)

    sort.Strings(a_copy)
    sort.Strings(b_copy)

    return reflect.DeepEqual(a_copy, b_copy)
}

func main() {
    a := []string {"a", "a", "c"}
    b := []string {"c", "a", "c"}
    c := []string {"z","z","x"} 
    d := []string {"x","z","z"}


    fmt.Println( array_sorted_equal(a, b))
    fmt.Println( array_sorted_equal(c, d))

}

Result:

false
true

Upvotes: 12

Dimitar Dimitrov
Dimitar Dimitrov

Reputation: 16357

I would think the easiest way would be to map the elements in each array/slice to their number of occurrences, then compare the maps:

func main() {
    x := []string{"a","b","c"}
    y := []string{"c","b","a"}

    xMap := make(map[string]int)
    yMap := make(map[string]int)

    for _, xElem := range x {
        xMap[xElem]++
    }
    for _, yElem := range y {
        yMap[yElem]++
    }

    for xMapKey, xMapVal := range xMap {
        if yMap[xMapKey] != xMapVal {
            return false
        }
    }
    return true
}

You'll need to add some additional due dilligence, like short circuiting if your arrays/slices contain elements of different types or are of different length.

Upvotes: 8

Related Questions