Skip to content

Commit

Permalink
slice: rework Batches and Chunks to return iterators (#21)
Browse files Browse the repository at this point in the history
Instead of a slice of results, return a single-valued iterator over the
selected components. A caller who wants the actual slices can use the
slices.Collect helper:

    old := slice.Batches(vs, n)
    new := slices.Collect(slice.Batches(vs, n))

Existing use in the target of a range do not need to change, except for
updating the target variables:

    for _, c := range slice.Chunks(vs, n) {

becomes

    for c := range slice.Chunks(vs, n) {
  • Loading branch information
creachadair authored Jan 29, 2025
1 parent 95316af commit 5d31fc8
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 40 deletions.
4 changes: 2 additions & 2 deletions slice/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func ExampleRotate() {
func ExampleChunks() {
vs := strings.Fields("my heart is a fish hiding in the water grass")

for _, c := range slice.Chunks(vs, 3) {
for c := range slice.Chunks(vs, 3) {
fmt.Println(c)
}
// Output:
Expand All @@ -54,7 +54,7 @@ func ExampleChunks() {
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) {
for b := range slice.Batches(vs, 4) {
fmt.Println(b)
}
// Output:
Expand Down
65 changes: 34 additions & 31 deletions slice/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,55 +196,58 @@ func gcd(a, b int) int {
return a
}

// Chunks returns a slice of contiguous subslices ("chunks") of vs, each having
// length at most n and together covering the input. All slices except the
// last will have length exactly n; the last may have fewer. The slices
// returned share storage with the input.
// Chunks iterates a sequence of contiguous subslices ("chunks") of vs, each
// having length at most n and together covering the input. All slices except
// the last will have length exactly n; the last may have fewer. The slices
// yielded share storage with the input.
//
// Chunks will panic if n < 0. If n == 0, Chunks returns a single chunk
// containing the entire input.
func Chunks[T any, Slice ~[]T](vs Slice, n int) []Slice {
// Chunks will panic if n < 0. If n == 0, Chunks yields no chunks.
func Chunks[T any, Slice ~[]T](vs Slice, n int) iter.Seq[Slice] {
if n < 0 {
panic("max must be positive")
} else if n == 0 || n >= len(vs) {
return []Slice{vs}
} else if n == 0 {
return func(func(Slice) bool) {}
}
out := make([]Slice, 0, (len(vs)+n-1)/n)
i := 0
for i < len(vs) {
end := min(i+n, len(vs))
out = append(out, vs[i:end:end])
i = end
return func(yield func(Slice) bool) {
i := 0
for i < len(vs) {
end := min(i+n, len(vs))
if !yield(vs[i:end:end]) {
return
}
i = end
}
}
return out
}

// Batches returns a slice of up to n contiguous subslices ("batches") of vs,
// each having nearly as possible to equal length and together covering the
// Batches iterates a sequence of up to n contiguous subslices ("batches") of
// vs, each having nearly as possible to equal length and together covering the
// input. The slices returned share storage with the input. If n > len(vs), the
// number of batches is capped at len(vs); otherwise exactly n are constructed.
//
// Batches will panic if n < 0. If n == 0 Batches returns nil.
func Batches[T any, Slice ~[]T](vs Slice, n int) []Slice {
// Batches will panic if n < 0. If n == 0 Batches yields no batches.
func Batches[T any, Slice ~[]T](vs Slice, n int) iter.Seq[Slice] {
if n < 0 {
panic("n out of range")
} else if n == 0 {
return nil
return func(func(Slice) bool) {}
} else if n > len(vs) {
n = len(vs)
}
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--
return func(yield func(Slice) bool) {
i, size, rem := 0, len(vs)/n, len(vs)%n
for i < len(vs) {
end := i + size
if rem > 0 {
end++
rem--
}
if !yield(vs[i:end:end]) {
return
}
i = end
}
out = append(out, vs[i:end:end])
i = end
}
return out
}

// Stripe returns a "stripe" of the ith elements of each slice in vs. Any
Expand Down
14 changes: 7 additions & 7 deletions slice/slice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,15 +278,15 @@ func TestChunks(t *testing.T) {
want [][]string
}{
// An empty slice has only one covering.
{"", 0, [][]string{{}}},
{"", 1, [][]string{{}}},
{"", 5, [][]string{{}}},
{"", 0, nil},
{"", 1, nil},
{"", 5, nil},

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

{"a b c d e", 0, [][]string{{"a", "b", "c", "d", "e"}}},
{"a b c d e", 0, nil},
{"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"}}},
Expand All @@ -295,7 +295,7 @@ func TestChunks(t *testing.T) {
{"a b c d e", 6, [][]string{{"a", "b", "c", "d", "e"}}}, // n > len(input)
}
for _, tc := range tests {
got := slice.Chunks(strings.Fields(tc.input), tc.n)
got := slices.Collect(slice.Chunks(strings.Fields(tc.input), tc.n))
for i := range len(got) - 1 {
if len(got[i]) != tc.n {
t.Errorf("Chunk %d has length %d, want %d", i+1, len(got[i]), tc.n)
Expand Down Expand Up @@ -332,7 +332,7 @@ func TestBatches(t *testing.T) {
{100, [][]int{{1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}}},
}
for _, tc := range tests {
got := slice.Batches(input, tc.n)
got := slices.Collect(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)
}
Expand Down

0 comments on commit 5d31fc8

Please sign in to comment.