diff --git a/crdt/gcounter.go b/crdt/gcounter.go new file mode 100644 index 0000000..5196b3b --- /dev/null +++ b/crdt/gcounter.go @@ -0,0 +1,60 @@ +package crdt + +import ( + "fmt" + "constraints" +) + +func max[N constraints.Ordered](a, b N) N { + if a >= b { + return a + } + return b +} + +func NewGCounter[K comparable]() GCounter[K] { + return GCounter[K]{slots: make(map[K]int)} +} + +type GCounter[K comparable] struct { + slots map[K]int `json:"slots"` +} + +func (g GCounter[K]) Incr(slot K) error { + var zero K + if slot == zero { + return fmt.Errorf("gcounter refuses incr on the zero-value of its key") + } + + g.slots[slot]++ + return nil +} + +func (g GCounter[K]) Add(slot K, delta int) error { + var zero K + if slot == zero { + return fmt.Errorf("gcounter refuses add on the zero-value of its key") + } + + if delta < 0 { + return fmt.Errorf("gcounters cannot go down, use a pncounter instead") + } + g.slots[slot] += delta + return nil +} + +func (g *GCounter[K]) Merge(dest *GCounter[K]) { + for slot, count := range g.slots { + v := max(count, dest.slots[slot]) + dest.slots[slot] = v + g.slots[slot] = v + } +} + +func (g *GCounter[K]) Total() int { + var n int + for _, count := range g.slots { + n += count + } + return n +} diff --git a/crdt/gcounter_test.go b/crdt/gcounter_test.go new file mode 100644 index 0000000..001e9e2 --- /dev/null +++ b/crdt/gcounter_test.go @@ -0,0 +1,65 @@ +package crdt + +import ( + "testing" +) + +func TestGCounter(t *testing.T) { + t.Run("incr", func(t *testing.T) { + g := NewGCounter[string]() + if n := g.Total(); n != 0 { + t.Fatalf("new gcounter has count of %d, should be 0", n) + } + + if err := g.Incr("jordan"); err != nil { + t.Fatalf("gcounter failed incr: %v", err) + } + + if n := g.Total(); n != 1 { + t.Fatalf("new gcounter has count of %d, should be 1", n) + } + + if err := g.Incr(""); err == nil { + t.Fatalf("incrementing the zero value succeeded, should have failed") + } + + if err := g.Incr("jordan"); err != nil { + t.Fatalf("gcounter failed incr: %v", err) + } + + if n := g.Total(); n != 2 { + t.Fatalf("new gcounter has count of %d, should be 2", n) + } + }) + + t.Run("add", func(t *testing.T) { + g := NewGCounter[string]() + if n := g.Total(); n != 0 { + t.Fatalf("new gcounter has count of %d, should be 0", n) + } + + if err := g.Add("jordan", 4); err != nil { + t.Fatalf("gcounter failed incr: %v", err) + } + + if n := g.Total(); n != 4 { + t.Fatalf("new gcounter has count of %d, should be 1", n) + } + + if err := g.Add("", 10); err == nil { + t.Fatalf("adding to zero key succeeded, should have failed") + } + + if err := g.Add("jordan", -3); err == nil { + t.Fatalf("adding negatively to the gcounter succeeded, should have failed") + } + + if err := g.Add("jordan", 3); err != nil { + t.Fatalf("gcounter failed add: %v", err) + } + + if n := g.Total(); n != 7 { + t.Fatalf("new gcounter has count of %d, should be 7", n) + } + }) +}