Skip to content

Commit

Permalink
slice: add Chunks and Batches
Browse files Browse the repository at this point in the history
- Chunks: partition a slice into chunks of size n.
- Batches: partition a slice into n equal batches.
  • Loading branch information
creachadair committed Jan 6, 2024
1 parent 1d83d90 commit 44547d9
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 0 deletions.
26 changes: 26 additions & 0 deletions slice/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,29 @@ func ExampleMatchingKeys() {
// blue 4
// yellow 6
}

func ExampleChunks() {
vs := strings.Fields("my heart is a fish hiding in the water grass")

for _, c := range slice.Chunks(vs, 3) {
fmt.Println(c)
}
// Output:
// [my heart is]
// [a fish hiding]
// [in the water]
// [grass]
}

func ExampleBatches() {
vs := strings.Fields("the freckles in our eyes are mirror images that when we kiss are perfectly aligned")

for _, b := range slice.Batches(vs, 4) {
fmt.Println(b)
}
// Output:
// [the freckles in our]
// [eyes are mirror images]
// [that when we kiss]
// [are perfectly aligned]
}
47 changes: 47 additions & 0 deletions slice/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,50 @@ func Coalesce[T comparable](vs ...T) T {
}
return zero
}

// Chunks returns a slice of contiguous subslices of vs, each having length at
// most max and together covering the input. The slices returned share storage
// with the input.
//
// Chunks will panic if max ≤ 0.
func Chunks[T any, Slice ~[]T](vs Slice, max int) []Slice {
if max <= 0 {
panic("max must be positive")
} else if max >= len(vs) {
return []Slice{vs}
}
out := make([]Slice, 0, (len(vs)+max-1)/max)
i := 0
for i < len(vs) {
end := i + max
if end > len(vs) {
end = len(vs)
}
out = append(out, vs[i:end])
i = end
}
return out
}

// Batches returns a slice of n contiguous subslices of vs, each having nearly
// as possible to equal length and together covering the input. The slices
// returned share storage with the input.
//
// Batches will panic if n ≤ 0 or max > len(vs).
func Batches[T any, Slice ~[]T](vs Slice, n int) []Slice {
if n <= 0 || n > len(vs) {
panic("n out of range")
}
out := make([]Slice, 0, n)
i, size, rem := 0, len(vs)/n, len(vs)%n
for i < len(vs) {
end := i + size
if rem > 0 {
end++
rem--
}
out = append(out, vs[i:end])
i = end
}
return out
}
63 changes: 63 additions & 0 deletions slice/slice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,69 @@ func TestCoalesce(t *testing.T) {
}
}

func TestChunks(t *testing.T) {
tests := []struct {
input string
max int
want [][]string
}{
// An empty slice has only one covering.
{"", 1, [][]string{{}}},
{"", 5, [][]string{{}}},

{"x", 1, [][]string{{"x"}}},
{"x", 2, [][]string{{"x"}}},

{"a b c d e", 1, [][]string{{"a"}, {"b"}, {"c"}, {"d"}, {"e"}}},
{"a b c d e", 2, [][]string{{"a", "b"}, {"c", "d"}, {"e"}}},
{"a b c d e", 3, [][]string{{"a", "b", "c"}, {"d", "e"}}},
{"a b c d e", 4, [][]string{{"a", "b", "c", "d"}, {"e"}}},
{"a b c d e", 5, [][]string{{"a", "b", "c", "d", "e"}}},
{"a b c d e", 6, [][]string{{"a", "b", "c", "d", "e"}}},
}
for _, tc := range tests {
got := slice.Chunks(strings.Fields(tc.input), tc.max)
if diff := cmp.Diff(got, tc.want); diff != "" {
t.Errorf("Chunks(%q, %d): (-got, +want)\n%s", tc.input, tc.max, diff)
}
}

t.Logf("OK max=0: %v", mtest.MustPanic(t, func() { slice.Chunks([]string{"a"}, 0) }))
t.Logf("OK max<0: %v", mtest.MustPanic(t, func() { slice.Chunks([]string{"a"}, -1) }))
}

func TestBatches(t *testing.T) {
input := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}
tests := []struct {
n int
want [][]int
}{
{1, [][]int{input}},
{2, [][]int{{1, 2, 3, 4, 5, 6, 7}, {8, 9, 10, 11, 12, 13}}},
{3, [][]int{{1, 2, 3, 4, 5}, {6, 7, 8, 9}, {10, 11, 12, 13}}},
{4, [][]int{{1, 2, 3, 4}, {5, 6, 7}, {8, 9, 10}, {11, 12, 13}}},
{5, [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11}, {12, 13}}},
{6, [][]int{{1, 2, 3}, {4, 5}, {6, 7}, {8, 9}, {10, 11}, {12, 13}}},
{7, [][]int{{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}, {11, 12}, {13}}},
{8, [][]int{{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}, {11}, {12}, {13}}},
{9, [][]int{{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9}, {10}, {11}, {12}, {13}}},
{10, [][]int{{1, 2}, {3, 4}, {5, 6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}}},
{11, [][]int{{1, 2}, {3, 4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}}},
{12, [][]int{{1, 2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}}},
{13, [][]int{{1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}}},
}
for _, tc := range tests {
got := slice.Batches(input, tc.n)
if diff := cmp.Diff(got, tc.want); diff != "" {
t.Errorf("Batches(%v, %d): (-got, +want)\n%s", input, tc.n, diff)
}
}

t.Logf("OK n=0: %v", mtest.MustPanic(t, func() { slice.Batches(input, 0) }))
t.Logf("OK n<0: %v", mtest.MustPanic(t, func() { slice.Batches(input, -1) }))
t.Logf("OK n>len: %v", mtest.MustPanic(t, func() { slice.Batches(input, len(input)+1) }))
}

func (tc *testCase[T]) partition(t *testing.T) {
t.Helper()

Expand Down

0 comments on commit 44547d9

Please sign in to comment.