slices: add Chunk

Chunk returns an iterator over consecutive sub-slices of up to n elements of s.

Fixes #53987.

Change-Id: I508274eca388db39550eb9e4d8abd5ce68d29d8d
Reviewed-on: https://go-review.googlesource.com/c/go/+/562935
Reviewed-by: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
Auto-Submit: Ian Lance Taylor <iant@google.com>
diff --git a/api/next/53987.txt b/api/next/53987.txt
new file mode 100644
index 0000000..1861d0b
--- /dev/null
+++ b/api/next/53987.txt
@@ -0,0 +1 @@
+pkg slices, func Chunk[$0 interface{ ~[]$1 }, $1 interface{}]($0, int) iter.Seq[$0] #53987
diff --git a/doc/next/6-stdlib/3-iter.md b/doc/next/6-stdlib/3-iter.md
index bc74f45..6b52b7c 100644
--- a/doc/next/6-stdlib/3-iter.md
+++ b/doc/next/6-stdlib/3-iter.md
@@ -19,3 +19,5 @@
   comparison function.
 - [SortedStableFunc](/pkg/slices#SortedStableFunc) is like `SortFunc`
   but uses a stable sort algorithm.
+- [Chunk](/pkg/slices#Chunk) returns an iterator over consecutive
+  sub-slices of up to n elements of a slice.
diff --git a/doc/next/6-stdlib/99-minor/slices/53987.md b/doc/next/6-stdlib/99-minor/slices/53987.md
new file mode 100644
index 0000000..02d77cd
--- /dev/null
+++ b/doc/next/6-stdlib/99-minor/slices/53987.md
@@ -0,0 +1 @@
+<!-- see ../../3-iter.md -->
diff --git a/src/slices/example_test.go b/src/slices/example_test.go
index e1bda36..cb601ad 100644
--- a/src/slices/example_test.go
+++ b/src/slices/example_test.go
@@ -384,3 +384,30 @@
 	// Output:
 	// [0 1 2 3 0 1 2 3]
 }
+
+func ExampleChunk() {
+	type Person struct {
+		Name string
+		Age  int
+	}
+
+	type People []Person
+
+	people := People{
+		{"Gopher", 13},
+		{"Alice", 20},
+		{"Bob", 5},
+		{"Vera", 24},
+		{"Zac", 15},
+	}
+
+	// Chunk people into []Person 2 elements at a time.
+	for c := range slices.Chunk(people, 2) {
+		fmt.Println(c)
+	}
+
+	// Output:
+	// [{Gopher 13} {Alice 20}]
+	// [{Bob 5} {Vera 24}]
+	// [{Zac 15}]
+}
diff --git a/src/slices/iter.go b/src/slices/iter.go
index 985bd27..a0f642e 100644
--- a/src/slices/iter.go
+++ b/src/slices/iter.go
@@ -84,3 +84,27 @@
 	SortStableFunc(s, cmp)
 	return s
 }
+
+// Chunk returns an iterator over consecutive sub-slices of up to n elements of s.
+// All but the last sub-slice will have size n.
+// All sub-slices are clipped to have no capacity beyond the length.
+// If s is empty, the sequence is empty: there is no empty slice in the sequence.
+// Chunk panics if n is less than 1.
+func Chunk[Slice ~[]E, E any](s Slice, n int) iter.Seq[Slice] {
+	if n < 1 {
+		panic("cannot be less than 1")
+	}
+
+	return func(yield func(Slice) bool) {
+		for i := 0; i < len(s); i += n {
+			// Clamp the last chunk to the slice bound as necessary.
+			end := min(n, len(s[i:]))
+
+			// Set the capacity of each chunk so that appending to a chunk does
+			// not modify the original slice.
+			if !yield(s[i : i+end : i+end]) {
+				return
+			}
+		}
+	}
+}
diff --git a/src/slices/iter_test.go b/src/slices/iter_test.go
index 67520f6..07d73e9 100644
--- a/src/slices/iter_test.go
+++ b/src/slices/iter_test.go
@@ -182,3 +182,113 @@
 		t.Errorf("SortedStableFunc wasn't stable on %d reverse ints", n)
 	}
 }
+
+func TestChunk(t *testing.T) {
+	cases := []struct {
+		name   string
+		s      []int
+		n      int
+		chunks [][]int
+	}{
+		{
+			name:   "nil",
+			s:      nil,
+			n:      1,
+			chunks: nil,
+		},
+		{
+			name:   "empty",
+			s:      []int{},
+			n:      1,
+			chunks: nil,
+		},
+		{
+			name:   "short",
+			s:      []int{1, 2},
+			n:      3,
+			chunks: [][]int{{1, 2}},
+		},
+		{
+			name:   "one",
+			s:      []int{1, 2},
+			n:      2,
+			chunks: [][]int{{1, 2}},
+		},
+		{
+			name:   "even",
+			s:      []int{1, 2, 3, 4},
+			n:      2,
+			chunks: [][]int{{1, 2}, {3, 4}},
+		},
+		{
+			name:   "odd",
+			s:      []int{1, 2, 3, 4, 5},
+			n:      2,
+			chunks: [][]int{{1, 2}, {3, 4}, {5}},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			var chunks [][]int
+			for c := range Chunk(tc.s, tc.n) {
+				chunks = append(chunks, c)
+			}
+
+			if !chunkEqual(chunks, tc.chunks) {
+				t.Errorf("Chunk(%v, %d) = %v, want %v", tc.s, tc.n, chunks, tc.chunks)
+			}
+
+			if len(chunks) == 0 {
+				return
+			}
+
+			// Verify that appending to the end of the first chunk does not
+			// clobber the beginning of the next chunk.
+			s := Clone(tc.s)
+			chunks[0] = append(chunks[0], -1)
+			if !Equal(s, tc.s) {
+				t.Errorf("slice was clobbered: %v, want %v", s, tc.s)
+			}
+		})
+	}
+}
+
+func TestChunkPanics(t *testing.T) {
+	for _, test := range []struct {
+		name string
+		x    []struct{}
+		n    int
+	}{
+		{
+			name: "cannot be less than 1",
+			x:    make([]struct{}, 0),
+			n:    0,
+		},
+	} {
+		if !panics(func() { _ = Chunk(test.x, test.n) }) {
+			t.Errorf("Chunk %s: got no panic, want panic", test.name)
+		}
+	}
+}
+
+func TestChunkRange(t *testing.T) {
+	// Verify Chunk iteration can be stopped.
+	var got [][]int
+	for c := range Chunk([]int{1, 2, 3, 4, -100}, 2) {
+		if len(got) == 2 {
+			// Found enough values, break early.
+			break
+		}
+
+		got = append(got, c)
+	}
+
+	if want := [][]int{{1, 2}, {3, 4}}; !chunkEqual(got, want) {
+		t.Errorf("Chunk iteration did not stop, got %v, want %v", got, want)
+	}
+}
+
+func chunkEqual[Slice ~[]E, E comparable](s1, s2 []Slice) bool {
+	return EqualFunc(s1, s2, Equal[Slice])
+}