Skip to content

Commit

Permalink
Make kvstore an internal package; reformat comments.
Browse files Browse the repository at this point in the history
  • Loading branch information
jpfourny committed Jan 6, 2024
1 parent a3324c1 commit f0a765b
Show file tree
Hide file tree
Showing 14 changed files with 442 additions and 358 deletions.
112 changes: 112 additions & 0 deletions internal/kvstore/kvstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package kvstore

import (
"github.com/jpfourny/papaya/pkg/cmp"
"github.com/jpfourny/papaya/pkg/optional"
"slices"
)

// Store represents a container for key-value pairs.
// Used internally for key-grouping and key-joining operations.
type Store[K, V any] interface {
Get(key K) optional.Optional[V]
Put(key K, value V)
ForEach(func(key K, value V) bool) bool
}

// NewMapped creates a new Store backed by a map.
// The key type K must be comparable.
func NewMapped[K comparable, V any]() Store[K, V] {
return make(mappedStore[K, V])
}

// NewSorted creates a new Store of sorted keys, ordered by the given cmp.Comparer.
func NewSorted[K any, V any](compare cmp.Comparer[K]) Store[K, V] {
return &sortedStore[K, V]{
compare: compare,
}
}

// Maker is a factory function for creating a Store.
type Maker[K, V any] func() Store[K, V]

// MappedMaker returns a Maker that calls NewMapped.
// The key type K must be comparable.
func MappedMaker[K comparable, V any]() Maker[K, V] {
return func() Store[K, V] {
return NewMapped[K, V]()
}
}

// SortedMaker returns a Maker that calls NewSorted with the given cmp.Comparer.
func SortedMaker[K any, V any](compare cmp.Comparer[K]) Maker[K, V] {
return func() Store[K, V] {
return NewSorted[K, V](compare)
}
}

// mappedStore provides an implementation of Store using the builtin map.
// The key type K must be comparable.
type mappedStore[K comparable, V any] map[K]V

func (s mappedStore[K, V]) Get(key K) optional.Optional[V] {
if v, ok := s[key]; ok {
return optional.Of(v)
}
return optional.Empty[V]()
}

func (s mappedStore[K, V]) Put(key K, value V) {
s[key] = value
}

func (s mappedStore[K, V]) ForEach(yield func(K, V) bool) bool {
for k, v := range s {
if !yield(k, v) {
return false
}
}
return true
}

// sortedStore provides an implementation of Store using sorted slices and binary-search.
// The keys are ordered using the given cmp.Comparer.
type sortedStore[K any, V any] struct {
compare cmp.Comparer[K]
keys []K
values []V
}

func (s *sortedStore[K, V]) Get(key K) optional.Optional[V] {
if i, ok := s.indexOf(key); ok {
return optional.Of(s.values[i])
}
return optional.Empty[V]()
}

func (s *sortedStore[K, V]) Put(key K, value V) {
i, ok := s.indexOf(key)
if ok {
s.values[i] = value
} else {
s.keys = append(s.keys, key)
s.values = append(s.values, value)
copy(s.keys[i+1:], s.keys[i:]) // Shift keys.
copy(s.values[i+1:], s.values[i:]) // Shift values.
s.keys[i] = key // Insert key.
s.values[i] = value // Insert value.
}
}

func (s *sortedStore[K, V]) indexOf(key K) (int, bool) {
return slices.BinarySearchFunc(s.keys, key, s.compare)
}

func (s *sortedStore[K, V]) ForEach(f func(K, V) bool) bool {
for i, k := range s.keys {
if !f(k, s.values[i]) {
return false
}
}
return true
}
203 changes: 203 additions & 0 deletions internal/kvstore/kvstore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package kvstore

import (
"github.com/jpfourny/papaya/internal/assert"
"github.com/jpfourny/papaya/pkg/cmp"
"github.com/jpfourny/papaya/pkg/optional"
"testing"
)

func TestMappedMaker(t *testing.T) {
m := MappedMaker[int, string]()
if m == nil {
t.Fatalf("got %#v, want non-nil", m)
}
ks := m()
if ks == nil {
t.Fatalf("got %#v, want non-nil", ks)
}
}

func TestOrderedMaker(t *testing.T) {
m := SortedMaker[int, string](cmp.Natural[int]())
if m == nil {
t.Fatalf("got %#v, want non-nil", m)
}
ks := m()
if ks == nil {
t.Fatalf("got %#v, want non-nil", ks)
}
}

func TestOrderedStore_Get(t *testing.T) {
t.Run("empty", func(t *testing.T) {
ks := NewSorted[int, string](cmp.Natural[int]())
got := ks.Get(0)
want := optional.Empty[string]()
if got != want {
t.Fatalf("got %#v, want %#v", got, want)
}
})

t.Run("non-empty", func(t *testing.T) {
ks := NewSorted[int, string](cmp.Natural[int]())
ks.Put(1, "one")
ks.Put(2, "two")
ks.Put(1, "uno")
ks.Put(3, "three")
ks.Put(2, "dos")
got := ks.Get(1)
want := optional.Of("uno")
if got != want {
t.Fatalf("got %#v, want %#v", got, want)
}
got = ks.Get(2)
want = optional.Of("dos")
if got != want {
t.Fatalf("got %#v, want %#v", got, want)
}
got = ks.Get(3)
want = optional.Of("three")
if got != want {
t.Fatalf("got %#v, want %#v", got, want)
}
got = ks.Get(4)
want = optional.Empty[string]()
if got != want {
t.Fatalf("got %#v, want %#v", got, want)
}
})
}

func TestOrderedStore_ForEach(t *testing.T) {
t.Run("empty", func(t *testing.T) {
ks := NewSorted[int, string](cmp.Natural[int]())
var got []int
ks.ForEach(func(key int, value string) bool {
got = append(got, key)
return true
})
if len(got) != 0 {
t.Fatalf("got %#v, want %#v", got, []int{})
}
})

t.Run("non-empty", func(t *testing.T) {
ks := NewSorted[int, string](cmp.Natural[int]())
ks.Put(1, "one")
ks.Put(2, "two")
ks.Put(1, "uno")
ks.Put(3, "three")
ks.Put(2, "dos")
var got []int
ks.ForEach(func(key int, value string) bool {
got = append(got, key)
return true
})
want := []int{1, 2, 3}
assert.ElementsMatch(t, got, want)
})

t.Run("stop", func(t *testing.T) {
ks := NewSorted[int, string](cmp.Natural[int]())
ks.Put(1, "one")
ks.Put(2, "two")
ks.Put(1, "uno")
ks.Put(3, "three")
ks.Put(2, "dos")
var got []int
ks.ForEach(func(key int, value string) bool {
got = append(got, key)
return false
})
want := []int{1}
assert.ElementsMatch(t, got, want)
})
}

func TestMappedStore_Get(t *testing.T) {
t.Run("empty", func(t *testing.T) {
ks := NewMapped[int, string]()
got := ks.Get(0)
want := optional.Empty[string]()
if got != want {
t.Fatalf("got %#v, want %#v", got, want)
}
})

t.Run("non-empty", func(t *testing.T) {
ks := NewMapped[int, string]()
ks.Put(1, "one")
ks.Put(2, "two")
ks.Put(1, "uno")
ks.Put(3, "three")
ks.Put(2, "dos")
got := ks.Get(1)
want := optional.Of("uno")
if got != want {
t.Fatalf("got %#v, want %#v", got, want)
}
got = ks.Get(2)
want = optional.Of("dos")
if got != want {
t.Fatalf("got %#v, want %#v", got, want)
}
got = ks.Get(3)
want = optional.Of("three")
if got != want {
t.Fatalf("got %#v, want %#v", got, want)
}
got = ks.Get(4)
want = optional.Empty[string]()
if got != want {
t.Fatalf("got %#v, want %#v", got, want)
}
})
}

func TestMappedStore_ForEach(t *testing.T) {
t.Run("empty", func(t *testing.T) {
ks := NewMapped[int, string]()
var got []int
ks.ForEach(func(key int, value string) bool {
got = append(got, key)
return true
})
if len(got) != 0 {
t.Fatalf("got %#v, want %#v", got, []int{})
}
})

t.Run("non-empty", func(t *testing.T) {
ks := NewMapped[int, string]()
ks.Put(1, "one")
ks.Put(2, "two")
ks.Put(1, "uno")
ks.Put(3, "three")
ks.Put(2, "dos")
var got []int
ks.ForEach(func(key int, value string) bool {
got = append(got, key)
return true
})
want := []int{1, 2, 3}
assert.ElementsMatchAnyOrder(t, got, want)
})

t.Run("stop", func(t *testing.T) {
ks := NewMapped[int, string]()
ks.Put(1, "one")
ks.Put(2, "two")
ks.Put(1, "uno")
ks.Put(3, "three")
ks.Put(2, "dos")
var got []int
ks.ForEach(func(key int, value string) bool {
got = append(got, key)
return false
})
if len(got) != 1 {
t.Fatalf("expected exactly 1 element, got %d", len(got))
}
})
}
31 changes: 16 additions & 15 deletions pkg/stream/aggregate.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,19 @@ type Finisher[A, F any] func(a A) (result F)
//
// Example usage:
//
// out := stream.Reduce(
// stream.Of(1, 2, 3),
// func(a, e int) int { // Reduce values by addition.
// return a + e
// },
// ) // Some(6)
// out = stream.Reduce(
// stream.Empty[int](),
// func(a, e int) int { // Reduce values by addition.
// return a + e
// },
// ) // None()
// out := stream.Reduce(
// stream.Of(1, 2, 3),
// func(a, e int) int { // Reduce values by addition.
// return a + e
// },
// ) // Some(6)
//
// out = stream.Reduce(
// stream.Empty[int](),
// func(a, e int) int { // Reduce values by addition.
// return a + e
// },
// ) // None()
func Reduce[E any](s Stream[E], reduce Reducer[E]) (result optional.Optional[E]) {
result = optional.Empty[E]()
s(func(e E) bool {
Expand All @@ -60,12 +61,12 @@ func Reduce[E any](s Stream[E], reduce Reducer[E]) (result optional.Optional[E])
//
// s := stream.Aggregate(
// stream.Of(1, 2, 3),
// 0, // Initial value
// 0, // Initial value
// func(a, e int) int {
// return a + e // Accumulate with addition
// return a + e // Accumulate with addition
// },
// func(a int) int {
// return a * 2 // Finish with multiplication by 2
// return a * 2 // Finish with multiplication by 2
// },
// ) // (1+2+3) * 2 = 12
func Aggregate[E, A, F any](s Stream[E], identity A, accumulate Accumulator[A, E], finish Finisher[A, F]) F {
Expand Down
4 changes: 2 additions & 2 deletions pkg/stream/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func CollectChannelCtx[E any](ctx context.Context, s Stream[E], ch chan<- E) {
//
// ch := stream.CollectChannelAsync(stream.Of(1, 2, 3), 3)
// for e := range ch {
// fmt.Println(e)
// fmt.Println(e)
// }
//
// Output:
Expand Down Expand Up @@ -133,7 +133,7 @@ func CollectChannelAsync[E any](s Stream[E], buf int) <-chan E {
// ctx := context.Background()
// ch := stream.CollectChannelAsyncCtx(ctx, stream.Of(1, 2, 3), 3)
// for e := range ch {
// fmt.Println(e)
// fmt.Println(e)
// }
//
// Output:
Expand Down
Loading

0 comments on commit f0a765b

Please sign in to comment.