Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Golang: group and sum slice of structs

I come from the .NET world where I had LINQ so I could do in-memory queries like the one we usually see in SQL.

I have a slice of this structure I want to group by 8 fields, and then sum another integer field. Something like:

type Register struct {
    id1 int
    id2 int
    id3 int
    id4 int
    id5 int
    id6 int
    id7 int
    id8 int
    money int
}

I thought in:

  • Creating an Equal function, to compare structures (those eight
    fields). Iterate over the collection I'm analyzing. For each item
    check if it is already in the hash table. If it is there => I sum the field. If it is not => I add the new item to hash table.

Is there a better way or any beautiful, efficient and easy ready to use library?

like image 786
Marcos Avatar asked Oct 11 '16 21:10

Marcos


2 Answers

Basically your idXX fields are the keys, an n-tuple. And the money field is the data to be summed.

This can easily be done if you slightly refactor your types. Put only the keys into a struct, so it can be used as a key in a map. Struct values are comparable:

Struct values are comparable if all their fields are comparable. Two struct values are equal if their corresponding non-blank fields are equal.

So the new types:

type Key struct {
    id1 int
    id2 int
    id3 int
    id4 int
    id5 int
    id6 int
    id7 int
    id8 int
}

type Register struct {
    key   Key
    money int
}

And to group and calculate sum, you can use a map[Key]int, using Register.key as the map key to "group" all registers with the same keys (same ids):

regs := []*Register{
    {Key{id1: 345}, 1500},
    {Key{id1: 345, id2: 140}, 2700},
    {Key{id1: 345, id2: 140}, 1300},
    {Key{id1: 345}, 1000},
    {Key{id3: 999}, 1000},
    {Key{id3: 999}, 2000},
}

// calculate sum:
m := map[Key]int{}
for _, v := range regs {
    m[v.key] += v.money
}

fmt.Println(m)

Output:

map[{345 0 0 0 0 0 0 0}:2500 {345 140 0 0 0 0 0 0}:4000 {0 0 999 0 0 0 0 0}:3000]

For a nice output:

fmt.Println("Nice output:")
for k, v := range m {
    fmt.Printf("%+3v: %d\n", k, v)
}

Output:

Nice output:
{id1:345 id2:  0 id3:  0 id4:  0 id5:  0 id6:  0 id7:  0 id8:  0}: 2500
{id1:345 id2:140 id3:  0 id4:  0 id5:  0 id6:  0 id7:  0 id8:  0}: 4000
{id1:  0 id2:  0 id3:999 id4:  0 id5:  0 id6:  0 id7:  0 id8:  0}: 3000

This is as easy and as efficient as it can get. Try the examples on the Go Playground.

Notes:

In the map we didn't have to check if a Key is already in it. This is so because indexing a map yields the zero value of the value type of the map if the key is not in the map. So in this case if a Key is not yet in the map, m[key] will give you 0 (0 is the zero value for the int type), properly telling that the "previous" sum for that key is 0 so far.

Also note that Key may be an embedded field in Register instead of a "regular" field, it doesn't matter, and that way you could refer to the idXX fields as if they were part of the Register.

like image 96
icza Avatar answered Nov 13 '22 01:11

icza


You can try go-linq: https://github.com/ahmetalpbalkan/go-linq/

It is similar to c# linq, use the GroupBy and Aggregate in your case

https://godoc.org/github.com/ahmetalpbalkan/go-linq#example-Query-GroupBy https://godoc.org/github.com/ahmetalpbalkan/go-linq#example-Query-Aggregate

pseudo code:

From(regs).GroupBy(merge ids to a string as group key).Select(use Aggregate or SumInts to sum money)
like image 25
chendesheng Avatar answered Nov 13 '22 01:11

chendesheng