-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make kvstore an internal package; reformat comments.
- Loading branch information
Showing
14 changed files
with
442 additions
and
358 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.