3

Given an array of float64 (or any other thing), I would like to build a set similar to the set we find in python. What is the best approach to designing such a data structure?

So far I managed to build a set using a map but it seems awkward to repurpose map to build a set.

package main

import (
    "fmt"
    "sort"
)

func main() {

    var scores = []float64{
        12.0, 6.0, 19.0, 20.0, 2.0, 8.0, 20.0,
    }

    set := make(map[float64]bool)
    for _, t := range scores {
        set[t] = true
    }

    unk := make([]float64, 0, len(set))
    for t := range set {
        unk = append(unk, t)
    }

    sort.Float64s(unk)
    fmt.Println(unk)
}

Will produce

[2 6 8 12 19 20]

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Michael
  • 2,436
  • 1
  • 36
  • 57
  • 7
    For a "nice" set, use a map with `bool` value type (with `true` values) and exploit the zero value. For a set with smallest memory footprint, use a map with `struct{}` value type (and use the comma-ok idiom to tell if a value is in the set / map). – icza Oct 08 '18 at 09:15
  • 2
    For details and examples, see [How can I create an array that contains unique strings?](https://stackoverflow.com/questions/33207197/how-can-i-create-an-array-that-contains-unique-strings/33207265#33207265) – icza Oct 08 '18 at 09:58
  • As an alternative, you can use an open-source library. [`github.com/soroushj/menge`](https://github.com/soroushj/menge) covers all basic types, including floats. [`k8s.io/apimachinery/pkg/util/sets`](https://pkg.go.dev/k8s.io/apimachinery/pkg/util/sets) implements sets of integers and strings, but not floats. – Soroush Oct 26 '20 at 12:19

2 Answers2

5

Using a map[float64]bool (or map[float64]struct{}) works well unless you need to have NaNs in the set in which case it will fail.

Float sets need extra logic to handle the NaN case as NaNs can be used as map indices but do not work as expected.

You should define your own FloatSet which consists of two fields: One map[float64]struct{} for all "normal" floats (including the Infs) and one hasNaN bool field for NaNs. Insert, Lookup, Remove code need both an extra code path to handle NaNs.

The "happy" path is perfectly covered by icza's answer but floats always need special treatment for NaNs. (Unless your set will never contain NaNs, until it will contain a NaN and your code will fail with a hard to detect bug.)

Volker
  • 40,468
  • 7
  • 81
  • 87
-2

Go 1.18 version added a great generics support, that made it possible to implement a generic Set!

See this package for example https://github.com/amit7itz/goset
It is implemented using a map (as you did), but it's completely transparent to the user.

This is how it would look for your use case:

package main

import (
    "fmt"
    "github.com/amit7itz/goset"
    "sort"
)

func main() {
    scores := []float64{12.0, 6.0, 19.0, 20.0, 2.0, 8.0, 20.0}

    scores = goset.FromSlice(scores).Items()
    
    sort.Float64s(scores)
    fmt.Println(scores)
}

You could also create the set with the items directly, like this:

set := goset.NewSet[float64](12.0, 6.0, 19.0, 20.0, 2.0, 8.0, 20.0)
Amit Itzkovitch
  • 175
  • 1
  • 7
  • If something can be done with a native map, I believe one should not be using 3rd party dependencies. If there are some useful helper functions then it's better to copy few lines of code rather than introduce a dependency. – Kangur May 12 '22 at 07:30