Skip to content

Commit 74c2ad5

Browse files
committed
implement Iter::chunk_by
1 parent d1f4a55 commit 74c2ad5

File tree

3 files changed

+151
-0
lines changed

3 files changed

+151
-0
lines changed

builtin/builtin.mbti

+1
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ impl Iter {
218218
all[T](Self[T], (T) -> Bool) -> Bool
219219
any[T](Self[T], (T) -> Bool) -> Bool
220220
append[T](Self[T], T) -> Self[T]
221+
chunk_by[T, K : Eq](Self[T], (T) -> K) -> Self[(K, Self[T])]
221222
collect[T](Self[T]) -> Array[T]
222223
concat[T](Self[T], Self[T]) -> Self[T]
223224
contains[A : Eq](Self[A], A) -> Bool

builtin/iter.mbt

+65
Original file line numberDiff line numberDiff line change
@@ -1036,3 +1036,68 @@ pub fn Iter::group_by[T, K : Eq + Hash](
10361036
}
10371037
result
10381038
}
1039+
1040+
///|
1041+
/// Groups elements of an iterator according to a discriminator function.
1042+
///
1043+
/// # Parameters
1044+
///
1045+
/// * `self` - The input iterator.
1046+
/// * `f` - The discriminator function that maps elements to keys.
1047+
///
1048+
/// # Returns
1049+
///
1050+
/// An iterator of tuples where each tuple contains a key and an iterator of elements that share that key.
1051+
///
1052+
/// # Example
1053+
///
1054+
/// ```moonbit
1055+
/// test "chunk_by" {
1056+
/// let iter = [1, 1, 2, 3, 2, 2, 1].iter()
1057+
/// let chunked = iter.chunk_by(fn(x) { x })
1058+
/// let result = chunked.map(fn(g) { (g.0, g.1.collect()) }).collect()
1059+
/// assert_eq!(result, [(1, [1, 1]), (2, [2]), (3, [3]), (2, [2, 2]), (1, [1])])
1060+
/// }
1061+
pub fn Iter::chunk_by[T, K : Eq](
1062+
self : Iter[T],
1063+
f : (T) -> K
1064+
) -> Iter[(K, Iter[T])] {
1065+
fn(yield_) {
1066+
let mut has_current = false
1067+
let mut current_key : K? = None
1068+
let mut buffer : Array[T] = []
1069+
fn emit_current_chunk() -> IterResult {
1070+
if buffer.is_empty() {
1071+
return IterContinue
1072+
}
1073+
let key = current_key.unwrap()
1074+
let elements = buffer
1075+
buffer = Array::new(capacity=16)
1076+
yield_((key, elements.iter()))
1077+
}
1078+
1079+
self.run(fn(x) {
1080+
let key = f(x)
1081+
if has_current && key == current_key.unwrap() {
1082+
buffer.push(x)
1083+
IterContinue
1084+
} else {
1085+
if has_current {
1086+
if emit_current_chunk() == IterEnd {
1087+
return IterEnd
1088+
}
1089+
}
1090+
has_current = true
1091+
current_key = Some(key)
1092+
buffer.push(x)
1093+
IterContinue
1094+
}
1095+
})
1096+
|> ignore
1097+
if has_current {
1098+
emit_current_chunk()
1099+
} else {
1100+
IterContinue
1101+
}
1102+
}
1103+
}

builtin/iter_test.mbt

+85
Original file line numberDiff line numberDiff line change
@@ -825,3 +825,88 @@ test "group_by with complex objects" {
825825
let groups = grouped.values().map(fn(a) { a.map(fn(p) { p.name }) }).collect()
826826
assert_eq!(groups, [["Alice", "Bob"], ["Charlie", "Eve"], ["Dave"]])
827827
}
828+
829+
///|
830+
test "chunk_by with consecutive identical elements" {
831+
let iter = [1, 1, 2, 2, 3, 3].iter()
832+
let chunked = iter.chunk_by(fn(x) { x })
833+
let result = chunked.map(fn(g) { (g.0, g.1.collect()) }).collect()
834+
assert_eq!(result, [(1, [1, 1]), (2, [2, 2]), (3, [3, 3])])
835+
}
836+
837+
///|
838+
test "chunk_by with non-consecutive identical elements" {
839+
let iter = [1, 2, 1, 3, 2, 1].iter()
840+
let chunked = iter.chunk_by(fn(x) { x })
841+
let result = chunked.map(fn(g) { (g.0, g.1.collect()) }).collect()
842+
assert_eq!(result, [
843+
(1, [1]),
844+
(2, [2]),
845+
(1, [1]),
846+
(3, [3]),
847+
(2, [2]),
848+
(1, [1]),
849+
])
850+
}
851+
852+
///|
853+
test "chunk_by with empty input" {
854+
let iter = Iter::empty()
855+
let chunked = iter.chunk_by(fn(x) { x })
856+
let result = chunked.map(fn(g) { (g.0, g.1.collect()) }).collect()
857+
assert_eq!(result, [])
858+
}
859+
860+
///|
861+
test "chunk_by with single element input" {
862+
let iter = [42].iter()
863+
let chunked = iter.chunk_by(fn(x) { x })
864+
let result = chunked.map(fn(g) { (g.0, g.1.collect()) }).collect()
865+
assert_eq!(result, [(42, [42])])
866+
}
867+
868+
///|
869+
test "chunk_by with custom key function" {
870+
let iter = [1, 2, 3, 4].iter()
871+
let chunked = iter.chunk_by(fn(x) { x % 2 })
872+
let result = chunked.map(fn(g) { (g.0, g.1.collect()) }).collect()
873+
assert_eq!(result, [(1, [1]), (0, [2]), (1, [3]), (0, [4])])
874+
}
875+
876+
///|
877+
test "chunk_by with strings" {
878+
let iter = ["apple", "avocado", "banana", "cherry", "blueberry"].iter()
879+
let chunked = iter.chunk_by(fn(s) { s[0] })
880+
let result = chunked.map(fn(g) { (g.0, g.1.collect()) }).collect()
881+
assert_eq!(result, [
882+
('a', ["apple", "avocado"]),
883+
('b', ["banana"]),
884+
('c', ["cherry"]),
885+
('b', ["blueberry"]),
886+
])
887+
}
888+
889+
///|
890+
test "chunk_by with complex objects" {
891+
struct Person {
892+
name : String
893+
age : Int
894+
} derive(Show)
895+
let people = [
896+
Person::{ name: "Alice", age: 25 },
897+
Person::{ name: "Bob", age: 25 },
898+
Person::{ name: "Charlie", age: 30 },
899+
Person::{ name: "Dave", age: 35 },
900+
Person::{ name: "Eve", age: 30 },
901+
].iter()
902+
let chunked = people.chunk_by(fn(p) { p.age })
903+
let groups = chunked
904+
.map(fn(g) { (g.0, g.1.map(fn(p) { p.name }).collect()) })
905+
.collect()
906+
assert_eq!(groups, [
907+
(25, ["Alice", "Bob"]),
908+
(30, ["Charlie"]),
909+
(35, ["Dave"]),
910+
(30, ["Eve"]),
911+
])
912+
}

0 commit comments

Comments
 (0)