fluent_iterable/lib.rs
1//! Fluent extension trait for [`std::iter::Iterator`] — the Rust port of the
2//! [`@codibre/fluent-iterable`](https://www.npmjs.com/package/@codibre/fluent-iterable)
3//! vocabulary.
4//!
5//! JavaScript got a fluent iterator chain protocol that Node's standard
6//! library never shipped, so this library ships the *same vocabulary* for
7//! Rust on the one mechanism the language actually has: an extension trait
8//! with a blanket impl over every [`Iterator`]. No wrapper type, no DSL —
9//! chain onto any iterator you already have.
10//!
11//! ```
12//! use fluent_iterable::FluentIter;
13//!
14//! let rows = [("sa", 10), ("sp", 30), ("sa", 25)];
15//! let by_state: Vec<(&str, Vec<(&str, i32)>)> =
16//! rows.into_iter().group_by_stable(|(s, _)| *s).collect();
17//! assert_eq!(by_state[0].1, vec![("sa", 10), ("sa", 25)]);
18//!
19//! let report = rows
20//! .into_iter()
21//! .map(|(_, v)| v)
22//! .join_by(|v| format!("{v:>4}"), " | ");
23//! assert_eq!(report, " 10 | 30 | 25");
24//! ```
25//!
26//! # npm ⇄ Rust map
27//!
28//! Only the operations that `std` lacks live here. Everything else already
29//! has a native spelling — using it keeps code idiomatic:
30//!
31//! | npm fluent-iterable | Rust |
32//! |--------------------------------|---------------------------------------------|
33//! | `.map/.filter` | [`Iterator::map`] / [`Iterator::filter`] |
34//! | `.flatMap` / `.flatten` | [`Iterator::flat_map`] |
35//! | `.mapNotNull` / pick-maps | [`Iterator::filter_map`] |
36//! | `.first()` | [`Iterator::next`] / [`Iterator::find`] |
37//! | `.last()` | `.next_back()` (double-ended) or fold |
38//! | `.some(p)` / `.every(p)` | [`Iterator::any`] / [`Iterator::all`] |
39//! | `.take/.skip/.takeWhile/.skipWhile` | same names on [`Iterator`] |
40//! | `.count()` | [`Iterator::count`] |
41//! | `.sum()` / `.min()` / `.max()` | [`Iterator::sum`] / [`Iterator::min`] / [`Iterator::max`] |
42//! | `.reduce(r, i)` | [`Iterator::fold`] |
43//! | `.partition(n)` | [`slice::chunks`](std::slice::chunks) |
44//! | `.concat/.append/.prepend` | [`Iterator::chain`] |
45//! | `.withIndex()` | [`Iterator::enumerate`] |
46//! | `.toArray()` | [`Iterator::collect`] |
47//! | `.toSet()` | `.collect::<std::collections::HashSet<_>>()` |
48//! | `.group(keyFn)` | [`FluentIter::group_by_stable`] |
49//! | `.combine(other, ka, kb)` | [`FluentIter::combine`] |
50//! | `.combineJoin(baseKey)` | [`FluentIter::combine_join`] |
51//! | `.distinct(keyFn?)` | [`FluentIter::distinct_by`] / [`FluentIter::distinct`] |
52//! | `.isDistinct(keyFn?)` | [`FluentIter::is_distinct_by`] / [`FluentIter::is_distinct`] |
53//! | `.toMap(k, v)` | [`FluentIter::to_map_by`] |
54//! | `.toMapChain(k1, k2)` | [`FluentIter::to_map_chain`] (depth 2) |
55//! | `.toMapChainReduce(…)` | [`FluentIter::to_map_chain_reduce`] (depth 2)|
56//! | `.join(sep, mapFn?)` | [`FluentIter::join`] / [`FluentIter::join_by`] |
57//! | `.avg(mapFn?)` | [`FluentIter::avg`] / [`FluentIter::avg_by`] |
58//! | `.sort(cmp?)` | [`FluentIter::sorted`] / [`FluentIter::sort_with`] |
59//! | `.sortBy(key…)` | [`FluentIter::sort_by`] / [`FluentIter::sort_by_desc`] |
60//! | `.hasExactly(n)` | [`FluentIter::has_exactly`] |
61//! | `.hasLessThan(n)` | [`FluentIter::has_less_than`] |
62//! | `.hasLessOrExactly(n)` | [`FluentIter::has_less_or_exactly`] |
63//! | `.hasMoreThan(n)` | [`FluentIter::has_more_than`] |
64//! | `.hasMoreOrExactly(n)` | [`FluentIter::has_more_or_exactly`] |
65//! | `.reduceAndMap(r, i, m)` | [`FluentIter::reduce_and_map`] |
66//! | `.contains(item)` | [`FluentIter::contains`] |
67//! | `.whenEmpty(fb)` | [`FluentIter::when_empty`] |
68//! | `.repeat(n)` | [`FluentIter::repeat`] |
69//! | `.top(proj, cmp)` | `.max_by_key` / `.max_by` |
70//!
71//! # What is deliberately *not* ported (and why)
72//!
73//! | npm surface | reason |
74//! |----------------------------------------------|--------|
75//! | every `*Async` operation, `flatMerge` | stable Rust has no `AsyncIterator` trait; generic async iteration is unexpressible. Revisit when the language lands it. |
76//! | `emit` / `catch` / `finally` / `execute` / `waitAll` | JS emitter/lifecycle surface. `Result`, `Drop` and dedicated stream crates own this in Rust. |
77//! | `toObject` / `toObjectChain*` / `unwind` | dynamic-property idioms; struct field selection via `flat_map` is the Rust answer. |
78//! | `branch` | needs fan-out buffering (`tee`); possible v2. |
79//! | `distinct(key, choose, maxOccurrences)` extras | v1 keeps first-wins; a chooser/max-occurrences flavor can land later without breaking names. |
80//! | `toMapChain` dynamic key depth | JS nests to an arbitrary number of key functions; Rust fixes the concrete nested type at **depth 2**. Deeper nesting is a fold of depth-2 layers. |
81//! | `avg()` returning `NaN` on empty | Rust says it in the type: `Option<f64>`, `None` on empty. |
82//! | `sort()` default comparator (stringified JS ordering) | Rust uses `Ord` ([`FluentIter::sorted`]) — numeric/lexicographic by the type's own ordering, never string coercion. |
83//! | `combine` default-identity keys | keys are explicit closures in Rust. |
84//! | `whenEmpty` with a differently-typed fallback | the fallback must yield the same `Item` type; map it first. |
85//!
86//! # Laziness
87//!
88//! Adapters ([`FluentIter::distinct`], [`FluentIter::when_empty`],
89//! [`FluentIter::repeat`]) stream and short-circuit like `std` adapters.
90//! Terminals that need the whole sequence (`group_by_stable`, `combine`,
91//! `sort*`, `to_map*`) return owned `Vec`/`HashMap` directly — the JS side
92//! materializes at these points anyway, and materializing keeps the order
93//! semantics exact and obvious.
94
95use std::cmp::Ordering;
96use std::collections::{HashMap, HashSet};
97use std::fmt::Display;
98use std::hash::Hash;
99
100/// Streaming deduplication adapter behind [`FluentIter::distinct`].
101///
102/// Keeps the first occurrence of every item and drops later ones, holding
103/// one [`HashSet`] of what has been seen — it never buffers the sequence.
104#[derive(Debug)]
105pub struct Distinct<I: Iterator>
106where
107 I::Item: Clone,
108{
109 iter: I,
110 seen: HashSet<I::Item>,
111}
112
113impl<I> Iterator for Distinct<I>
114where
115 I: Iterator,
116 I::Item: Eq + Hash + Clone,
117{
118 type Item = I::Item;
119
120 fn next(&mut self) -> Option<I::Item> {
121 loop {
122 let item = self.iter.next()?;
123 // `seen` must retain the value to compare future items, while
124 // we hand this occurrence out — so the item clones itself into
125 // the set (same contract as itertools' `unique`).
126 if self.seen.insert(item.clone()) {
127 return Some(item);
128 }
129 }
130 }
131
132 fn size_hint(&self) -> (usize, Option<usize>) {
133 let (_, upper) = self.iter.size_hint();
134 (0, upper)
135 }
136}
137
138/// Fallback adapter behind [`FluentIter::when_empty`].
139#[derive(Debug)]
140pub struct WhenEmpty<I, F>
141where
142 I: Iterator,
143 F: IntoIterator<Item = I::Item>,
144{
145 phase: WhenEmptyPhase<I, F::IntoIter>,
146 fallback: Option<F>,
147}
148
149#[derive(Debug)]
150enum WhenEmptyPhase<S, T> {
151 /// Still pulling from the source; flag remembers whether it yielded yet.
152 Source(S, bool),
153 /// Source ran out without yielding: draining the fallback.
154 Fallback(T),
155 /// Done — `Iterator::next` must keep returning `None`.
156 Done,
157}
158
159impl<I, F> Iterator for WhenEmpty<I, F>
160where
161 I: Iterator,
162 F: IntoIterator<Item = I::Item>,
163{
164 type Item = I::Item;
165
166 fn next(&mut self) -> Option<I::Item> {
167 match &mut self.phase {
168 WhenEmptyPhase::Source(source, yielded) => match source.next() {
169 Some(item) => {
170 *yielded = true;
171 Some(item)
172 }
173 None => {
174 if *yielded {
175 self.phase = WhenEmptyPhase::Done;
176 return None;
177 }
178 match self.fallback.take() {
179 Some(fallback) => {
180 let mut iter = fallback.into_iter();
181 let item = iter.next();
182 self.phase = WhenEmptyPhase::Fallback(iter);
183 item
184 }
185 None => {
186 self.phase = WhenEmptyPhase::Done;
187 None
188 }
189 }
190 }
191 },
192 WhenEmptyPhase::Fallback(iter) => iter.next(),
193 WhenEmptyPhase::Done => None,
194 }
195 }
196}
197
198/// Finite repetition adapter behind [`FluentIter::repeat`].
199///
200/// The first pass streams the source while caching; passes 2..=n replay the
201/// cache. With `times == 0` the source is never consumed at all.
202#[derive(Debug)]
203pub struct Repeat<I: Iterator>
204where
205 I::Item: Clone,
206{
207 source: I,
208 cache: Vec<I::Item>,
209 times: usize,
210 /// 0 while streaming the first pass, then the replay round index.
211 round: usize,
212 pos: usize,
213}
214
215impl<I> Iterator for Repeat<I>
216where
217 I: Iterator,
218 I::Item: Clone,
219{
220 type Item = I::Item;
221
222 fn next(&mut self) -> Option<I::Item> {
223 if self.times == 0 {
224 return None;
225 }
226 loop {
227 if self.round == 0 {
228 if let Some(item) = self.source.next() {
229 self.cache.push(item.clone());
230 return Some(item);
231 }
232 self.round = 1;
233 self.pos = 0;
234 }
235 if self.round >= self.times {
236 return None;
237 }
238 if let Some(item) = self.cache.get(self.pos) {
239 self.pos += 1;
240 return Some(item.clone());
241 }
242 self.round += 1;
243 self.pos = 0;
244 }
245 }
246}
247
248/// Internal phase of a buffered adapter: nothing consumed until the first
249/// `next()` (⇔ the JS generators that run their full pass on first pull).
250enum Phase<I: Iterator, O> {
251 Source(I),
252 Emitting(std::vec::IntoIter<O>),
253 Done,
254}
255
256impl<I, O> Phase<I, O>
257where
258 I: Iterator,
259{
260 /// Drive one step: on the first call run `build` over the source (that
261 /// is the only place the sequence is materialized), then drain.
262 fn next_with(&mut self, mut build: impl FnMut(I) -> Vec<O>) -> Option<O> {
263 if matches!(self, Phase::Source(_)) {
264 // Only the Pending→Emitting transition consumes; later calls
265 // must NOT replace (that would discard the buffered iterator).
266 let src = match std::mem::replace(self, Phase::Done) {
267 Phase::Source(i) => i,
268 _ => unreachable!("guarded by matches! above"),
269 };
270 *self = Phase::Emitting(build(src).into_iter());
271 }
272 match self {
273 Phase::Emitting(it) => it.next(),
274 _ => None,
275 }
276 }
277
278 fn size_hint(&self) -> (usize, Option<usize>) {
279 match self {
280 Phase::Emitting(it) => it.size_hint(),
281 _ => (0, None),
282 }
283 }
284}
285
286/// Keyed streaming dedup produced by [`FluentIter::distinct_by`] — see there.
287///
288/// Purely lazy: buffers only the seen-key [`HashSet`], never the sequence.
289/// `take(k)` stops pulling the source as soon as `k` distinct keys are out.
290#[derive(Debug)]
291pub struct DistinctBy<I, K, F>
292where
293 I: Iterator,
294{
295 iter: I,
296 key: F,
297 seen: HashSet<K>,
298}
299
300impl<I, K, F> Iterator for DistinctBy<I, K, F>
301where
302 I: Iterator,
303 K: Eq + Hash,
304 F: FnMut(&I::Item) -> K,
305{
306 type Item = I::Item;
307
308 fn next(&mut self) -> Option<I::Item> {
309 // Disjoint-field borrows (edition 2021 capture): iter is advanced
310 // while the key/seen state is consulted.
311 self.iter
312 .by_ref()
313 .find(|item| self.seen.insert((self.key)(item)))
314 }
315
316 fn size_hint(&self) -> (usize, Option<usize>) {
317 (0, self.iter.size_hint().1)
318 }
319}
320
321/// Stable grouping produced by [`FluentIter::group_by_stable`] — see there.
322///
323/// Lazy wrapper, buffering like npm's generator: the full pass runs on the
324/// first `next()`, then groups are yielded one at a time.
325pub struct GroupByStable<I, K, F>
326where
327 I: Iterator,
328{
329 phase: Phase<I, (K, Vec<I::Item>)>,
330 key: F,
331}
332
333impl<I, K, F> Iterator for GroupByStable<I, K, F>
334where
335 I: Iterator,
336 K: Eq + Hash + Clone,
337 F: FnMut(&I::Item) -> K,
338{
339 type Item = (K, Vec<I::Item>);
340
341 fn next(&mut self) -> Option<Self::Item> {
342 self.phase.next_with(|src| {
343 let mut index: HashMap<K, usize> = HashMap::new();
344 let mut out: Vec<(K, Vec<I::Item>)> = Vec::new();
345 for item in src {
346 let k = (self.key)(&item);
347 match index.get(&k) {
348 Some(&i) => out[i].1.push(item),
349 None => {
350 index.insert(k.clone(), out.len());
351 out.push((k, vec![item]));
352 }
353 }
354 }
355 out
356 })
357 }
358
359 fn size_hint(&self) -> (usize, Option<usize>) {
360 self.phase.size_hint()
361 }
362}
363
364/// Inner equi-join produced by [`FluentIter::combine`] — see there.
365///
366/// Lazy: the right side is indexed on the first `next()` (that side is read
367/// in full, like the JS side indexes it too), then the left side streams —
368/// `take(k)` stops the left walk once `k` pairs are out.
369pub struct Combine<I, J, K, KL, KR>
370where
371 I: Iterator,
372 J: Iterator,
373{
374 left: I,
375 key_left: KL,
376 right: J,
377 key_right: KR,
378 indexed: bool,
379 idx: HashMap<K, Vec<J::Item>>,
380 /// Left item currently yielding pairs, its key (cached: the closure is
381 /// not re-invoked per pair), and the next bucket position.
382 pending: Option<(I::Item, K, usize)>,
383}
384
385impl<I, J, K, KL, KR> Iterator for Combine<I, J, K, KL, KR>
386where
387 I: Iterator,
388 J: Iterator,
389 K: Eq + Hash + Clone,
390 KL: FnMut(&I::Item) -> K,
391 KR: FnMut(&J::Item) -> K,
392 I::Item: Clone,
393 J::Item: Clone,
394{
395 type Item = (I::Item, J::Item);
396
397 fn next(&mut self) -> Option<Self::Item> {
398 if !self.indexed {
399 for r in self.right.by_ref() {
400 self.idx.entry((self.key_right)(&r)).or_default().push(r);
401 }
402 self.indexed = true;
403 }
404 loop {
405 if self.pending.is_none() {
406 // Advance the left walk until an element finds a bucket.
407 loop {
408 let l = self.left.next()?;
409 let k = (self.key_left)(&l);
410 if self.idx.get(&k).is_some_and(|b| !b.is_empty()) {
411 self.pending = Some((l, k, 0));
412 break;
413 }
414 }
415 }
416 let (left, key, pos) = self.pending.take().expect("pending was just set");
417 let bucket = &self.idx[&key];
418 if pos < bucket.len() {
419 let pair = (left.clone(), bucket[pos].clone());
420 self.pending = Some((left, key, pos + 1));
421 return Some(pair);
422 }
423 // bucket exhausted — the outer loop takes the next matching left
424 }
425 }
426}
427
428/// Folded join produced by [`FluentIter::combine_join`] — see there.
429///
430/// Lazy wrapper, buffering like npm's generator: every iterable is read and
431/// folded on the first `next()`, then rows are yielded one at a time.
432pub struct CombineJoin<I, T, K, F>
433where
434 I: Iterator,
435{
436 phase: Phase<I, Vec<T>>,
437 key: F,
438 _k: std::marker::PhantomData<fn(&T) -> K>,
439}
440
441impl<I, T, K, F> Iterator for CombineJoin<I, T, K, F>
442where
443 I: Iterator,
444 I::Item: IntoIterator<Item = T>,
445 K: Eq + Hash,
446 T: Clone,
447 F: FnMut(&T) -> K,
448{
449 type Item = Vec<T>;
450
451 fn next(&mut self) -> Option<Vec<T>> {
452 self.phase.next_with(|src| {
453 let mut acc: Vec<Vec<T>> = Vec::new();
454 let mut first = true;
455 for iterable in src {
456 let items: Vec<T> = iterable.into_iter().collect();
457 if first {
458 first = false;
459 acc = items.into_iter().map(|item| vec![item]).collect();
460 continue;
461 }
462 if acc.is_empty() {
463 continue;
464 }
465 let mut idx: HashMap<K, Vec<T>> = HashMap::new();
466 for item in items {
467 idx.entry((self.key)(&item)).or_default().push(item);
468 }
469 let mut next_acc = Vec::new();
470 for row in acc {
471 // Rows are non-empty by construction; their key never
472 // changes across folds (inner-join invariant).
473 if let Some(bucket) = idx.get(&(self.key)(&row[0])) {
474 for item in bucket {
475 let mut grown = row.clone();
476 grown.push(item.clone());
477 next_acc.push(grown);
478 }
479 }
480 }
481 acc = next_acc;
482 }
483 acc
484 })
485 }
486
487 fn size_hint(&self) -> (usize, Option<usize>) {
488 self.phase.size_hint()
489 }
490}
491
492/// Sorted sequence produced by [`FluentIter::sorted`] — see there.
493pub struct Sorted<I: Iterator> {
494 phase: Phase<I, I::Item>,
495}
496
497impl<I> Iterator for Sorted<I>
498where
499 I: Iterator,
500 I::Item: Ord,
501{
502 type Item = I::Item;
503
504 fn next(&mut self) -> Option<I::Item> {
505 self.phase.next_with(|src| {
506 let mut out: Vec<I::Item> = src.collect();
507 out.sort();
508 out
509 })
510 }
511
512 fn size_hint(&self) -> (usize, Option<usize>) {
513 self.phase.size_hint()
514 }
515}
516
517/// Sorted sequence produced by [`FluentIter::sort_with`] — see there.
518pub struct SortWith<I: Iterator, F> {
519 phase: Phase<I, I::Item>,
520 compare: F,
521}
522
523impl<I, F> Iterator for SortWith<I, F>
524where
525 I: Iterator,
526 F: FnMut(&I::Item, &I::Item) -> Ordering,
527{
528 type Item = I::Item;
529
530 fn next(&mut self) -> Option<I::Item> {
531 self.phase.next_with(|src| {
532 let mut out: Vec<I::Item> = src.collect();
533 out.sort_by(&mut self.compare);
534 out
535 })
536 }
537
538 fn size_hint(&self) -> (usize, Option<usize>) {
539 self.phase.size_hint()
540 }
541}
542
543/// Sorted sequence produced by [`FluentIter::sort_by`] / [`FluentIter::sort_by_desc`].
544pub struct SortBy<I: Iterator, K, F> {
545 phase: Phase<I, I::Item>,
546 key: F,
547 descending: bool,
548 _k: std::marker::PhantomData<fn(&I::Item) -> K>,
549}
550
551impl<I, K, F> Iterator for SortBy<I, K, F>
552where
553 I: Iterator,
554 K: Ord,
555 F: FnMut(&I::Item) -> K,
556{
557 type Item = I::Item;
558
559 fn next(&mut self) -> Option<I::Item> {
560 self.phase.next_with(|src| {
561 let mut out: Vec<I::Item> = src.collect();
562 if self.descending {
563 out.sort_by_key(|item| std::cmp::Reverse((self.key)(item)));
564 } else {
565 out.sort_by_key(&mut self.key);
566 }
567 out
568 })
569 }
570
571 fn size_hint(&self) -> (usize, Option<usize>) {
572 self.phase.size_hint()
573 }
574}
575
576/// The missing combinators of [`std::iter::Iterator`], in the
577/// [`@codibre/fluent-iterable`] vocabulary (see the [crate docs](crate) for
578/// the full npm ⇄ Rust map).
579///
580/// Blanket-implemented for every iterator — just `use fluent_iterable::FluentIter;`.
581///
582/// Every operator that npm returns as an iterable returns an [`Iterator`]
583/// here too — chain freely and resolve with [`collect`][Iterator::collect]
584/// when you want a collection. Operators that npm resolves (`avg`, `join`,
585/// `toMap`, `hasX`, `contains`, …) return the plain value here.
586pub trait FluentIter: Iterator + Sized {
587 /// ⇔ `.group(keyFn)`: **stable** grouping — first-seen key order is
588 /// preserved and each group keeps encounter order.
589 ///
590 /// Note [`Iterator::group_by`] only groups *consecutive* elements,
591 /// which is a different operation (that one is `slice::group_by`
592 /// territory); this is the npm `group`, which is a full pass.
593 ///
594 /// ```
595 /// use fluent_iterable::FluentIter;
596 ///
597 /// let grouped: Vec<_> = ["b", "a", "b", "c", "a"]
598 /// .into_iter()
599 /// .group_by_stable(|x| *x)
600 /// .collect();
601 /// let keys: Vec<_> = grouped.iter().map(|(k, _)| *k).collect();
602 /// assert_eq!(keys, vec!["b", "a", "c"]);
603 /// assert_eq!(grouped[1].1, vec!["a", "a"]);
604 /// ```
605 ///
606 /// Lazy: the full grouping pass runs on the first pull, then groups are
607 /// yielded one at a time — chain on without materializing the pair vec.
608 fn group_by_stable<K, F>(self, key: F) -> GroupByStable<Self, K, F>
609 where
610 K: Eq + Hash + Clone,
611 F: FnMut(&Self::Item) -> K,
612 {
613 GroupByStable {
614 phase: Phase::Source(self),
615 key,
616 }
617 }
618
619 /// ⇔ `.combine(other, keySelf, keyOther)`: inner equi-join yielding
620 /// **all** matching `(left, right)` pairs in left order (right side is
621 /// indexed for `O(n + m)`; duplicate keys explode into pairs exactly
622 /// like the JS side).
623 ///
624 /// JS shares object references between pairs; Rust must clone to hand
625 /// owned pairs out. Joining iterators *of references*
626 /// (`a.iter().combine(b.iter(), …)`) keeps the clone pointer-cheap.
627 ///
628 /// ```
629 /// use fluent_iterable::FluentIter;
630 ///
631 /// let left = [1, 2, 1];
632 /// let right = [(1, "x"), (1, "y"), (3, "z")];
633 /// let joined: Vec<(&i32, &(i32, &str))> =
634 /// left.iter().combine(right.iter(), |l| **l, |(k, _)| *k).collect();
635 /// let shape: Vec<(i32, &str)> =
636 /// joined.into_iter().map(|(l, (_, n))| (*l, *n)).collect();
637 /// assert_eq!(shape, vec![(1, "x"), (1, "y"), (1, "x"), (1, "y")]);
638 /// ```
639 ///
640 /// Lazy: the right side is indexed on the first pull, the left side
641 /// streams — `take(k)` stops the left walk at `k` pairs.
642 fn combine<J, K, KL, KR>(
643 self,
644 other: J,
645 key_left: KL,
646 key_right: KR,
647 ) -> Combine<Self, J::IntoIter, K, KL, KR>
648 where
649 J: IntoIterator,
650 K: Eq + Hash + Clone,
651 KL: FnMut(&Self::Item) -> K,
652 KR: FnMut(&J::Item) -> K,
653 Self::Item: Clone,
654 J::Item: Clone,
655 {
656 Combine {
657 left: self,
658 key_left,
659 right: other.into_iter(),
660 key_right,
661 indexed: false,
662 idx: HashMap::new(),
663 pending: None,
664 }
665 }
666
667 /// ⇔ `.combineJoin(baseKey)` on an iterator **of iterables**: reduces
668 /// the whole family into one table of rows by a shared key — inner
669 /// join, pairwise folded (`n` iterables produce rows of length `n`).
670 /// The key is taken from the row's first element, exactly like the JS
671 /// `([a]) => key(a)`. Once a fold step matches nothing the result is
672 /// empty and stays empty.
673 ///
674 /// ```
675 /// use fluent_iterable::FluentIter;
676 ///
677 /// let a = [(1, "a1"), (2, "a2")];
678 /// let b = [(1, "b1"), (2, "b2")];
679 /// let c = [(1, "c1")];
680 /// let rows: Vec<Vec<_>> = [a.as_slice(), b.as_slice(), c.as_slice()]
681 /// .into_iter()
682 /// .combine_join(|(k, _)| *k)
683 /// .collect();
684 /// assert_eq!(rows.len(), 1);
685 /// assert_eq!(
686 /// rows[0].iter().map(|(_, v)| *v).collect::<Vec<_>>(),
687 /// vec!["a1", "b1", "c1"]
688 /// );
689 /// ```
690 ///
691 /// Lazy: the whole family is folded on the first pull, rows are yielded
692 /// one at a time.
693 fn combine_join<T, K, F>(self, key: F) -> CombineJoin<Self, T, K, F>
694 where
695 Self::Item: IntoIterator<Item = T>,
696 K: Eq + Hash,
697 T: Clone,
698 F: FnMut(&T) -> K,
699 {
700 CombineJoin {
701 phase: Phase::Source(self),
702 key,
703 _k: std::marker::PhantomData,
704 }
705 }
706
707 /// ⇔ `.distinct()` on hashable items: keeps the **first** occurrence of
708 /// every item. Lazy — buffers only the seen-set, and short-circuits
709 /// downstream (`take(k)` stops pulling once `k` uniques are out).
710 ///
711 /// `Item` must be `Clone` (like `itertools::unique`): the set retains a
712 /// copy of each value to compare against while the occurrence itself is
713 /// yielded. Use [`FluentIter::distinct_by`] when cloning is not
714 /// available — it tracks borrowed keys instead.
715 ///
716 /// ```
717 /// use fluent_iterable::FluentIter;
718 ///
719 /// let out: Vec<i32> = [1, 2, 1, 3, 2].into_iter().distinct().collect();
720 /// assert_eq!(out, vec![1, 2, 3]);
721 /// ```
722 fn distinct(self) -> Distinct<Self>
723 where
724 Self::Item: Eq + Hash + Clone,
725 {
726 Distinct {
727 iter: self,
728 seen: HashSet::new(),
729 }
730 }
731
732 /// ⇔ `.distinct(keyFn)`: keep the **first** item seen for each key.
733 ///
734 /// Purely lazy — buffers only the seen-key set, never the sequence, so
735 /// `take(k)` stops pulling the source once `k` distinct keys are out.
736 ///
737 /// ```
738 /// use fluent_iterable::FluentIter;
739 ///
740 /// let out: Vec<&str> = ["bb", "a1", "b1", "a2", "c"]
741 /// .into_iter()
742 /// .distinct_by(|s| s.chars().next().unwrap())
743 /// .collect();
744 /// assert_eq!(out, vec!["bb", "a1", "c"]);
745 /// ```
746 fn distinct_by<K, F>(self, key: F) -> DistinctBy<Self, K, F>
747 where
748 K: Eq + Hash,
749 F: FnMut(&Self::Item) -> K,
750 {
751 DistinctBy {
752 iter: self,
753 key,
754 seen: HashSet::new(),
755 }
756 }
757
758 /// ⇔ `.isDistinct()`: are all items unique? Short-circuits on the first
759 /// duplicate (the source is not drained).
760 ///
761 /// ```
762 /// use fluent_iterable::FluentIter;
763 ///
764 /// assert!([1, 2, 3].into_iter().is_distinct());
765 /// assert!(![1, 2, 1].into_iter().is_distinct());
766 /// ```
767 // npm parity names (`isDistinct`/`isDistinctBy`): these consume the
768 // iterator instead of taking &mut self, because Iterator::all needs an
769 // owned chain through the extension trait.
770 #[allow(clippy::wrong_self_convention)]
771 fn is_distinct(mut self) -> bool
772 where
773 Self::Item: Eq + Hash,
774 {
775 let mut seen = HashSet::new();
776 self.all(|item| seen.insert(item))
777 }
778
779 /// ⇔ `.isDistinct(keyFn)`: is every key unique? Short-circuits on the
780 /// first repetition.
781 ///
782 /// ```
783 /// use fluent_iterable::FluentIter;
784 ///
785 /// assert!([("a", 1), ("b", 2)].into_iter().is_distinct_by(|(k, _)| *k));
786 /// assert!(![("a", 1), ("b", 2), ("a", 3)].into_iter().is_distinct_by(|(k, _)| *k));
787 /// ```
788 #[allow(clippy::wrong_self_convention)]
789 fn is_distinct_by<K>(mut self, mut key: impl FnMut(&Self::Item) -> K) -> bool
790 where
791 K: Eq + Hash,
792 {
793 let mut seen = HashSet::new();
794 self.all(|item| seen.insert(key(&item)))
795 }
796
797 /// ⇔ `.whenEmpty(fallback)`: yield from the source; if it produces
798 /// **nothing**, yield the fallback instead. A non-empty source never
799 /// touches the fallback iterable.
800 ///
801 /// Unlike JS the fallback must yield the same `Item` type — map it
802 /// first if it doesn't.
803 ///
804 /// ```
805 /// use fluent_iterable::FluentIter;
806 ///
807 /// let empty: [i32; 0] = [];
808 /// let out: Vec<i32> = empty.into_iter().when_empty([7, 8]).collect();
809 /// assert_eq!(out, vec![7, 8]);
810 ///
811 /// let out: Vec<i32> = [1].into_iter().when_empty([7, 8]).collect();
812 /// assert_eq!(out, vec![1]);
813 /// ```
814 fn when_empty<F>(self, fallback: F) -> WhenEmpty<Self, F>
815 where
816 F: IntoIterator<Item = Self::Item>,
817 {
818 WhenEmpty {
819 phase: WhenEmptyPhase::Source(self, false),
820 fallback: Some(fallback),
821 }
822 }
823
824 /// ⇔ `.repeat(n)`: yield the whole sequence `n` times (first pass
825 /// streams and caches, later passes replay). `n == 0` yields nothing
826 /// and never consumes the source. Unlike `std`'s nightly-only `cycle`,
827 /// this is finite.
828 ///
829 /// ```
830 /// use fluent_iterable::FluentIter;
831 ///
832 /// let out: Vec<i32> = [1, 2].into_iter().repeat(3).collect();
833 /// assert_eq!(out, vec![1, 2, 1, 2, 1, 2]);
834 ///
835 /// let out: Vec<i32> = [1, 2].into_iter().repeat(0).collect();
836 /// assert!(out.is_empty());
837 /// ```
838 fn repeat(self, times: usize) -> Repeat<Self>
839 where
840 Self::Item: Clone,
841 {
842 Repeat {
843 source: self,
844 cache: Vec::new(),
845 times,
846 round: 0,
847 pos: 0,
848 }
849 }
850
851 /// ⇔ `.toMap(keyFn, valueFn)`: later entries overwrite earlier ones for
852 /// the same key (JS `Map.set` semantics; `HashMap` iteration order is
853 /// unspecified, like the JS engine's).
854 ///
855 /// ```
856 /// use fluent_iterable::FluentIter;
857 ///
858 /// let items = vec![("k", 1), ("j", 2), ("k", 3)];
859 /// let map = items.into_iter().to_map_by(|(k, _)| *k, |(_, v)| *v);
860 /// assert_eq!(map.get("k"), Some(&3));
861 /// ```
862 fn to_map_by<K, V>(
863 self,
864 mut key: impl FnMut(&Self::Item) -> K,
865 mut value: impl FnMut(&Self::Item) -> V,
866 ) -> HashMap<K, V>
867 where
868 K: Eq + Hash,
869 {
870 self.map(|item| (key(&item), value(&item))).collect()
871 }
872
873 /// ⇔ `.toMapChain(k1, k2)`: two-level index — `key1` outer, `key2`
874 /// inner, leaves keep encounter order.
875 ///
876 /// JS chains to an arbitrary number of key functions; Rust fixes the
877 /// nested type at depth 2 — deeper nesting is a fold of these layers.
878 ///
879 /// ```
880 /// use fluent_iterable::FluentIter;
881 ///
882 /// let rows = [("sp", "sa", 1), ("sp", "rj", 2), ("mg", "sa", 3)];
883 /// let index = rows
884 /// .into_iter()
885 /// .to_map_chain(|(a, _, _)| *a, |(_, b, _)| *b);
886 /// assert_eq!(index["sp"]["sa"], vec![("sp", "sa", 1)]);
887 /// assert_eq!(index["sp"]["rj"], vec![("sp", "rj", 2)]);
888 /// ```
889 fn to_map_chain<K1, K2>(
890 self,
891 mut key1: impl FnMut(&Self::Item) -> K1,
892 mut key2: impl FnMut(&Self::Item) -> K2,
893 ) -> HashMap<K1, HashMap<K2, Vec<Self::Item>>>
894 where
895 K1: Eq + Hash,
896 K2: Eq + Hash,
897 {
898 let mut out: HashMap<K1, HashMap<K2, Vec<Self::Item>>> = HashMap::new();
899 for item in self {
900 out.entry(key1(&item))
901 .or_default()
902 .entry(key2(&item))
903 .or_default()
904 .push(item);
905 }
906 out
907 }
908
909 /// ⇔ `.toMapChainReduce(k1, k2, reducer)`: two-level index whose leaves
910 /// are folded instead of collected. JS folds without an explicit
911 /// initial value; Rust demands one (there is no `undefined`).
912 ///
913 /// ```
914 /// use fluent_iterable::FluentIter;
915 ///
916 /// let rows = [("sp", "sa", 1), ("sp", "sa", 2), ("sp", "rj", 5)];
917 /// let totals = rows.into_iter().to_map_chain_reduce(
918 /// |(a, _, _)| *a,
919 /// |(_, b, _)| *b,
920 /// 0,
921 /// |acc, (_, _, v)| acc + v,
922 /// );
923 /// assert_eq!(totals["sp"]["sa"], 3);
924 /// assert_eq!(totals["sp"]["rj"], 5);
925 /// ```
926 fn to_map_chain_reduce<K1, K2, A>(
927 self,
928 key1: impl FnMut(&Self::Item) -> K1,
929 key2: impl FnMut(&Self::Item) -> K2,
930 init: A,
931 mut reduce: impl FnMut(A, Self::Item) -> A,
932 ) -> HashMap<K1, HashMap<K2, A>>
933 where
934 K1: Eq + Hash,
935 K2: Eq + Hash,
936 A: Clone,
937 {
938 self.to_map_chain(key1, key2)
939 .into_iter()
940 .map(|(k1, inner)| {
941 (
942 k1,
943 inner
944 .into_iter()
945 .map(|(k2, items)| {
946 let acc = items.into_iter().fold(init.clone(), &mut reduce);
947 (k2, acc)
948 })
949 .collect(),
950 )
951 })
952 .collect()
953 }
954
955 /// ⇔ `.sort()` with the type's own ordering: collect and **stable**
956 /// sort. (JS would stringify non-comparable items; Rust uses `Ord`.)
957 ///
958 /// ```
959 /// use fluent_iterable::FluentIter;
960 ///
961 /// let out: Vec<i32> = [3, 1, 2].into_iter().sorted().collect();
962 /// assert_eq!(out, vec![1, 2, 3]);
963 /// ```
964 ///
965 /// Lazy wrapper over the sort, ⇔ npm's `IterableSorted`: the collect +
966 /// sort runs on the first pull, then items are yielded in order.
967 fn sorted(self) -> Sorted<Self>
968 where
969 Self::Item: Ord,
970 {
971 Sorted {
972 phase: Phase::Source(self),
973 }
974 }
975
976 /// ⇔ `.sort(compareFn)`: stable sort with a custom comparator.
977 ///
978 /// ```
979 /// use fluent_iterable::FluentIter;
980 ///
981 /// let out: Vec<i32> = [1, 2, 3].into_iter().sort_with(|a, b| b.cmp(a)).collect();
982 /// assert_eq!(out, vec![3, 2, 1]);
983 /// ```
984 ///
985 /// Lazy wrapper over the sort (buffered on first pull).
986 fn sort_with<F>(self, compare: F) -> SortWith<Self, F>
987 where
988 F: FnMut(&Self::Item, &Self::Item) -> Ordering,
989 {
990 SortWith {
991 phase: Phase::Source(self),
992 compare,
993 }
994 }
995
996 /// ⇔ `.sortBy(keyFn)`: stable sort ascending by key. Ties keep encounter
997 /// order. As with `slice::sort_by_key` the key function may be called
998 /// more than once per element — keep it cheap, or sort decorated values.
999 ///
1000 /// ```
1001 /// use fluent_iterable::FluentIter;
1002 ///
1003 /// let rows = [("b", 1), ("a", 2), ("b", 3)];
1004 /// let out: Vec<_> = rows.into_iter().sort_by(|(_, v)| *v).collect();
1005 /// assert_eq!(out, vec![("b", 1), ("a", 2), ("b", 3)]);
1006 /// ```
1007 ///
1008 /// Lazy wrapper over the sort (buffered on first pull).
1009 fn sort_by<K, F>(self, key: F) -> SortBy<Self, K, F>
1010 where
1011 K: Ord,
1012 F: FnMut(&Self::Item) -> K,
1013 {
1014 SortBy {
1015 phase: Phase::Source(self),
1016 key,
1017 descending: false,
1018 _k: std::marker::PhantomData,
1019 }
1020 }
1021
1022 /// ⇔ `.sortBy('-keyFn')` (the descending order descriptor): stable sort
1023 /// descending by key; ties keep encounter order.
1024 ///
1025 /// ```
1026 /// use fluent_iterable::FluentIter;
1027 ///
1028 /// let rows = [("b", 1), ("a", 2), ("b", 3)];
1029 /// let out: Vec<_> = rows.into_iter().sort_by_desc(|(_, v)| *v).collect();
1030 /// assert_eq!(out, vec![("b", 3), ("a", 2), ("b", 1)]);
1031 /// ```
1032 ///
1033 /// Lazy wrapper over the sort (buffered on first pull).
1034 fn sort_by_desc<K, F>(self, key: F) -> SortBy<Self, K, F>
1035 where
1036 K: Ord,
1037 F: FnMut(&Self::Item) -> K,
1038 {
1039 SortBy {
1040 phase: Phase::Source(self),
1041 key,
1042 descending: true,
1043 _k: std::marker::PhantomData,
1044 }
1045 }
1046
1047 /// ⇔ `.avg()`: arithmetic mean as `Option<f64>` — `None` on empty
1048 /// (JS returns `NaN`; Rust says it in the type).
1049 ///
1050 /// ```
1051 /// use fluent_iterable::FluentIter;
1052 ///
1053 /// assert_eq!([2, 4, 6].into_iter().avg(), Some(4.0));
1054 /// assert_eq!(std::iter::empty::<i32>().avg(), None);
1055 /// ```
1056 fn avg(self) -> Option<f64>
1057 where
1058 Self::Item: Into<f64>,
1059 {
1060 let (mut sum, mut count) = (0f64, 0usize);
1061 for item in self {
1062 sum += item.into();
1063 count += 1;
1064 }
1065 if count == 0 {
1066 None
1067 } else {
1068 Some(sum / count as f64)
1069 }
1070 }
1071
1072 /// ⇔ `.avg(mapFn)`: mean of a projection.
1073 ///
1074 /// ```
1075 /// use fluent_iterable::FluentIter;
1076 ///
1077 /// let rows = [("x", 10.0), ("y", 20.0)];
1078 /// assert_eq!(rows.into_iter().avg_by(|(_, v)| *v), Some(15.0));
1079 /// ```
1080 fn avg_by(self, mut value: impl FnMut(&Self::Item) -> f64) -> Option<f64> {
1081 let (mut sum, mut count) = (0f64, 0usize);
1082 for item in self {
1083 sum += value(&item);
1084 count += 1;
1085 }
1086 if count == 0 {
1087 None
1088 } else {
1089 Some(sum / count as f64)
1090 }
1091 }
1092
1093 /// ⇔ `.join(sep)`: display every item with the separator between.
1094 /// (JS skips nullish items; Rust has neither, every item is shown.)
1095 ///
1096 /// ```
1097 /// use fluent_iterable::FluentIter;
1098 ///
1099 /// assert_eq!([1, 2, 3].into_iter().join(", "), "1, 2, 3");
1100 /// ```
1101 fn join(self, separator: &str) -> String
1102 where
1103 Self::Item: Display,
1104 {
1105 self.map(|item| item.to_string())
1106 .collect::<Vec<String>>()
1107 .join(separator)
1108 }
1109
1110 /// ⇔ `.join(sep, mapFn)`: project each item before joining.
1111 ///
1112 /// ```
1113 /// use fluent_iterable::FluentIter;
1114 ///
1115 /// let out = [1, 2].into_iter().join_by(|v| format!("{v:>3}"), "-");
1116 /// assert_eq!(out, " 1- 2");
1117 /// ```
1118 fn join_by<S: Display>(
1119 self,
1120 mut value: impl FnMut(&Self::Item) -> S,
1121 separator: &str,
1122 ) -> String {
1123 self.map(|item| value(&item).to_string())
1124 .collect::<Vec<String>>()
1125 .join(separator)
1126 }
1127
1128 /// ⇔ `.hasExactly(n)`: exactly `n` items — and no more. Short-circuits
1129 /// after `n + 1` items.
1130 ///
1131 /// ```
1132 /// use fluent_iterable::FluentIter;
1133 ///
1134 /// assert!([1, 2].into_iter().has_exactly(2));
1135 /// assert!(![1, 2, 3].into_iter().has_exactly(2));
1136 /// ```
1137 fn has_exactly(self, n: usize) -> bool {
1138 self.take(n.saturating_add(1)).count() == n
1139 }
1140
1141 /// ⇔ `.hasLessThan(n)`: strictly fewer than `n` items. Short-circuits
1142 /// after `n + 1`.
1143 ///
1144 /// ```
1145 /// use fluent_iterable::FluentIter;
1146 ///
1147 /// assert!([1].into_iter().has_less_than(2));
1148 /// assert!(![1, 2].into_iter().has_less_than(2));
1149 /// ```
1150 fn has_less_than(self, n: usize) -> bool {
1151 self.take(n.saturating_add(1)).count() < n
1152 }
1153
1154 /// ⇔ `.hasLessOrExactly(n)`: at most `n` items. Short-circuits after
1155 /// `n + 1`.
1156 ///
1157 /// ```
1158 /// use fluent_iterable::FluentIter;
1159 ///
1160 /// assert!([1, 2].into_iter().has_less_or_exactly(2));
1161 /// assert!(![1, 2, 3].into_iter().has_less_or_exactly(2));
1162 /// ```
1163 fn has_less_or_exactly(self, n: usize) -> bool {
1164 self.take(n.saturating_add(1)).count() <= n
1165 }
1166
1167 /// ⇔ `.hasMoreThan(n)`: strictly more than `n` items. Short-circuits
1168 /// after `n + 1`.
1169 ///
1170 /// ```
1171 /// use fluent_iterable::FluentIter;
1172 ///
1173 /// assert!([1, 2, 3].into_iter().has_more_than(2));
1174 /// assert!(![1, 2].into_iter().has_more_than(2));
1175 /// ```
1176 fn has_more_than(self, n: usize) -> bool {
1177 self.take(n.saturating_add(1)).count() > n
1178 }
1179
1180 /// ⇔ `.hasMoreOrExactly(n)`: at least `n` items. Short-circuits after
1181 /// `n` — it cannot tell you *how many* more.
1182 ///
1183 /// ```
1184 /// use fluent_iterable::FluentIter;
1185 ///
1186 /// assert!([1, 2].into_iter().has_more_or_exactly(2));
1187 /// assert!(![1].into_iter().has_more_or_exactly(2));
1188 /// ```
1189 fn has_more_or_exactly(self, n: usize) -> bool {
1190 self.take(n).count() >= n
1191 }
1192
1193 /// ⇔ `.reduceAndMap(reducer, initial, resultFn)`: fold, then project the
1194 /// accumulator. Argument order follows [`Iterator::fold`] (initial
1195 /// first), not the JS signature.
1196 ///
1197 /// ```
1198 /// use fluent_iterable::FluentIter;
1199 ///
1200 /// let report = [1, 2, 3]
1201 /// .into_iter()
1202 /// .reduce_and_map(0, |acc, x| acc + x, |sum| format!("total {sum}"));
1203 /// assert_eq!(report, "total 6");
1204 /// ```
1205 fn reduce_and_map<A, R>(
1206 self,
1207 init: A,
1208 mut fold: impl FnMut(A, Self::Item) -> A,
1209 mut map: impl FnMut(A) -> R,
1210 ) -> R {
1211 map(self.fold(init, &mut fold))
1212 }
1213
1214 /// ⇔ `.contains(item)`: any item equal to `item` (JS compares with
1215 /// `===`; Rust uses [`PartialEq`]). Short-circuits on the first hit.
1216 ///
1217 /// ```
1218 /// use fluent_iterable::FluentIter;
1219 ///
1220 /// assert!(["a", "b"].into_iter().contains(&"b"));
1221 /// assert!(!["a", "b"].into_iter().contains(&"c"));
1222 /// ```
1223 fn contains(mut self, item: &Self::Item) -> bool
1224 where
1225 Self::Item: PartialEq,
1226 {
1227 self.any(|x| &x == item)
1228 }
1229}
1230
1231impl<I: Iterator> FluentIter for I {}
1232
1233#[cfg(test)]
1234mod tests {
1235 use super::*;
1236 use std::cell::Cell;
1237 use std::iter::FromIterator;
1238 use std::rc::Rc;
1239
1240 /// Counts how many items the wrapped iterator ever yielded — proof of
1241 /// short-circuiting for the lazy/terminal checks.
1242 struct Counting<I: Iterator> {
1243 inner: I,
1244 yielded: Rc<Cell<usize>>,
1245 }
1246
1247 impl<I: Iterator> Iterator for Counting<I> {
1248 type Item = I::Item;
1249 fn next(&mut self) -> Option<I::Item> {
1250 let item = self.inner.next();
1251 if item.is_some() {
1252 self.yielded.set(self.yielded.get() + 1);
1253 }
1254 item
1255 }
1256 }
1257
1258 fn counting<I: Iterator>(inner: I) -> (Counting<I>, Rc<Cell<usize>>) {
1259 let counter = Rc::new(Cell::new(0));
1260 (
1261 Counting {
1262 inner,
1263 yielded: Rc::clone(&counter),
1264 },
1265 counter,
1266 )
1267 }
1268
1269 // ---- ported from the proven mq crate ----
1270
1271 #[test]
1272 fn stable_group_order() {
1273 let items = ["b", "a", "b", "c", "a"];
1274 let grouped: Vec<_> = items.into_iter().group_by_stable(|x| *x).collect();
1275 let keys: Vec<_> = grouped.iter().map(|(k, _)| *k).collect();
1276 assert_eq!(keys, vec!["b", "a", "c"]);
1277 assert_eq!(grouped[1].1, vec!["a", "a"]);
1278 }
1279
1280 #[test]
1281 fn combine_all_pairs_left_order() {
1282 let left = [1, 2, 1];
1283 let right = [(1, "x"), (1, "y"), (3, "z")];
1284 let joined: Vec<_> = left
1285 .iter()
1286 .combine(right.iter(), |l| **l, |(k, _)| *k)
1287 .collect();
1288 assert_eq!(joined.len(), 4);
1289 let shapes: Vec<(i32, &str)> = joined
1290 .into_iter()
1291 .map(|(l, (_, name))| (*l, *name))
1292 .collect();
1293 assert_eq!(shapes, vec![(1, "x"), (1, "y"), (1, "x"), (1, "y")]);
1294 }
1295
1296 #[test]
1297 fn combine_owned_items_fan_out_clones() {
1298 let left = vec![String::from("a"), String::from("a")];
1299 let right = vec![String::from("a1"), String::from("a2")];
1300 let joined = left
1301 .into_iter()
1302 .combine(right, |l| l.clone(), |r| r[..1].to_owned());
1303 let shapes: Vec<(String, String)> = joined.into_iter().collect();
1304 assert_eq!(
1305 shapes,
1306 vec![
1307 ("a".to_owned(), "a1".to_owned()),
1308 ("a".to_owned(), "a2".to_owned()),
1309 ("a".to_owned(), "a1".to_owned()),
1310 ("a".to_owned(), "a2".to_owned()),
1311 ]
1312 );
1313 }
1314
1315 #[test]
1316 fn distinct_keeps_first_per_key() {
1317 let items = ["bb", "a1", "b1", "a2", "c"];
1318 let out: Vec<_> = items
1319 .into_iter()
1320 .distinct_by(|s| s.chars().next().unwrap())
1321 .collect();
1322 assert_eq!(out, vec!["bb", "a1", "c"]);
1323 }
1324
1325 #[test]
1326 fn to_map_last_write_wins() {
1327 let items = vec![("k", 1), ("j", 2), ("k", 3)];
1328 let map = items.into_iter().to_map_by(|(k, _)| *k, |(_, v)| *v);
1329 assert_eq!(map.get("k"), Some(&3));
1330 assert_eq!(map.get("j"), Some(&2));
1331 assert_eq!(map.len(), 2);
1332 }
1333
1334 // ---- lazy adapters ----
1335
1336 #[test]
1337 fn distinct_is_lazy_and_short_circuits() {
1338 let (source, counter) = counting([1, 1, 2, 3, 3, 3, 4].into_iter());
1339 let out: Vec<i32> = source.distinct().take(2).collect();
1340 assert_eq!(out, vec![1, 2]);
1341 // stopped right after discovering 2 (never walked the tail)
1342 assert!(counter.get() <= 4, "consumed {}", counter.get());
1343 }
1344
1345 #[test]
1346 fn is_distinct_checks_and_short_circuits() {
1347 assert!([1, 2, 3].into_iter().is_distinct());
1348 assert!(std::iter::empty::<i32>().is_distinct());
1349 let (source, counter) = counting([1, 2, 1, 2, 1].into_iter());
1350 assert!(!source.is_distinct());
1351 assert_eq!(counter.get(), 3);
1352 }
1353
1354 #[test]
1355 fn is_distinct_by_short_circuits() {
1356 assert!([("a", 1), ("b", 2)].into_iter().is_distinct_by(|(k, _)| *k));
1357 let (source, counter) = counting([("a", 1), ("b", 2), ("a", 3), ("b", 4)].into_iter());
1358 assert!(!source.is_distinct_by(|(k, _)| *k));
1359 assert_eq!(counter.get(), 3);
1360 }
1361
1362 #[test]
1363 fn when_empty_uses_fallback_only_for_empty_source() {
1364 let empty: [i32; 0] = [];
1365 let out: Vec<i32> = empty.into_iter().when_empty([7, 8]).collect();
1366 assert_eq!(out, vec![7, 8]);
1367
1368 let (fallback_source, fallback_counter) = counting([7, 8].into_iter());
1369 let out: Vec<i32> = [1, 2].into_iter().when_empty(fallback_source).collect();
1370 assert_eq!(out, vec![1, 2]);
1371 assert_eq!(fallback_counter.get(), 0, "fallback must stay untouched");
1372 }
1373
1374 #[test]
1375 fn when_empty_with_empty_fallback_is_empty() {
1376 let empty: [i32; 0] = [];
1377 let out: Vec<i32> = empty.into_iter().when_empty(empty).collect();
1378 assert!(out.is_empty());
1379 }
1380
1381 #[test]
1382 fn when_empty_falls_back_after_late_first_value_absent() {
1383 // source yields nothing but is polled several times before giving up
1384 let (source, counter) = counting(std::iter::empty::<i32>());
1385 let out: Vec<i32> = source.when_empty([9]).collect();
1386 assert_eq!(out, vec![9]);
1387 assert_eq!(counter.get(), 0);
1388 }
1389
1390 #[test]
1391 fn repeat_replays_whole_sequence() {
1392 let out: Vec<i32> = [1, 2].into_iter().repeat(3).collect();
1393 assert_eq!(out, vec![1, 2, 1, 2, 1, 2]);
1394 let out: Vec<i32> = [1, 2].into_iter().repeat(1).collect();
1395 assert_eq!(out, vec![1, 2]);
1396 }
1397
1398 #[test]
1399 fn repeat_zero_never_consumes_source() {
1400 let (source, counter) = counting([1, 2, 3].into_iter());
1401 let out: Vec<i32> = source.repeat(0).collect();
1402 assert!(out.is_empty());
1403 assert_eq!(counter.get(), 0);
1404 }
1405
1406 #[test]
1407 fn repeat_streams_first_pass_before_replaying() {
1408 let (source, counter) = counting([1, 2, 3].into_iter());
1409 let mut rep = source.repeat(2);
1410 assert_eq!(rep.next(), Some(1));
1411 // only one item pulled so far: the first pass is streaming
1412 assert_eq!(counter.get(), 1);
1413 assert_eq!(rep.next(), Some(2));
1414 assert_eq!(rep.next(), Some(3));
1415 assert_eq!(rep.next(), Some(1)); // replay started
1416 assert_eq!(rep.count(), 2);
1417 }
1418
1419 #[test]
1420 fn repeat_of_empty_source_is_empty() {
1421 let out: Vec<i32> = std::iter::empty::<i32>().repeat(4).collect();
1422 assert!(out.is_empty());
1423 }
1424
1425 #[test]
1426 fn distinct_by_is_lazy_and_short_circuits() {
1427 // take(k) must stop pulling the source at the k-th distinct key —
1428 // impossible for an eager implementation (the design bug this
1429 // replaced).
1430 let (source, counter) = counting([1, 1, 2, 2, 3, 3, 4, 4].into_iter());
1431 let out: Vec<i32> = source.distinct_by(|x| *x).take(2).collect();
1432 assert_eq!(out, vec![1, 2]);
1433 assert_eq!(counter.get(), 3); // pulled up to the second `2`, no further
1434 }
1435
1436 #[test]
1437 fn buffered_adapters_consume_nothing_until_polled() {
1438 // ⇔ JS generators: building the chain runs no code; the internal
1439 // full pass happens on the first next().
1440 let (source, counter) = counting([3, 1, 2].into_iter());
1441 let _grouped = source.group_by_stable(|x| *x);
1442 assert_eq!(counter.get(), 0);
1443
1444 let (source, counter) = counting([3, 1, 2].into_iter());
1445 let mut sorted = source.sorted();
1446 assert_eq!(counter.get(), 0);
1447 assert_eq!(sorted.next(), Some(1)); // first pull buffers + sorts all
1448 assert_eq!(counter.get(), 3);
1449
1450 let (source, counter) = counting([vec![1], vec![2]].into_iter());
1451 let _joined = source.combine_join(|x| *x);
1452 assert_eq!(counter.get(), 0);
1453 }
1454
1455 #[test]
1456 fn combine_indexes_right_on_first_pull_and_streams_left() {
1457 let (left, left_pulls) = counting([1, 2, 3, 1, 2, 3].into_iter());
1458 let right = [(1, "x")];
1459 let mut joined = left.combine(right, |l| *l, |(k, _)| *k);
1460 assert_eq!(left_pulls.get(), 0);
1461 assert_eq!(joined.next(), Some((1, (1, "x"))));
1462 // left stopped at the first matching element: no full left scan
1463 assert_eq!(left_pulls.get(), 1);
1464 assert_eq!(joined.next(), Some((1, (1, "x"))));
1465 assert_eq!(joined.next(), None);
1466 }
1467
1468 // ---- resolving terminals ----
1469
1470 #[test]
1471 fn sort_family_is_stable() {
1472 let rows = [("b", 1), ("a", 2), ("b", 3), ("a", 1)];
1473 let by_value: Vec<_> = rows.into_iter().sort_by(|(_, v)| *v).collect();
1474 // ties (value 1 then 2..): encounter order preserved
1475 assert_eq!(by_value, vec![("b", 1), ("a", 1), ("a", 2), ("b", 3)]);
1476 let desc: Vec<_> = rows.into_iter().sort_by_desc(|(_, v)| *v).collect();
1477 assert_eq!(desc, vec![("b", 3), ("a", 2), ("b", 1), ("a", 1)]);
1478 let asc: Vec<u8> = [3u8, 1, 2].into_iter().sorted().collect();
1479 assert_eq!(asc, vec![1, 2, 3]);
1480 let custom: Vec<i32> = [1, 2, 3].into_iter().sort_with(|a, b| b.cmp(a)).collect();
1481 assert_eq!(custom, vec![3, 2, 1]);
1482 }
1483
1484 #[test]
1485 fn avg_handles_ints_floats_and_empty() {
1486 assert_eq!([2, 4, 6].into_iter().avg(), Some(4.0));
1487 assert_eq!(std::iter::empty::<i32>().avg(), None);
1488 let rows = [("x", 10.0), ("y", 20.0)];
1489 assert_eq!(rows.into_iter().avg_by(|(_, v)| *v), Some(15.0));
1490 assert_eq!(std::iter::empty::<&str>().avg_by(|_| 1.0), None);
1491 }
1492
1493 #[test]
1494 fn join_displays_and_projects() {
1495 assert_eq!([1, 2, 3].into_iter().join(", "), "1, 2, 3");
1496 assert_eq!(std::iter::empty::<i32>().join(","), "");
1497 let out = [1, 2].into_iter().join_by(|v| format!("{v:>3}"), "-");
1498 assert_eq!(out, " 1- 2");
1499 }
1500
1501 #[test]
1502 fn has_cardinality_matrix() {
1503 // source length × n → each predicate, matching comparisonRecipe:
1504 // count(take(n+1)) `op` n
1505 let len = |n: usize| (0..n).collect::<Vec<_>>();
1506 assert!(len(2).into_iter().has_exactly(2));
1507 assert!(!len(3).into_iter().has_exactly(2));
1508 assert!(!len(1).into_iter().has_exactly(2));
1509 // count(take(1)) == 0 → empty sequence hasExactly(0), like the JS
1510 // comparisonRecipe
1511 assert!(len(0).into_iter().has_exactly(0));
1512 assert!(len(1).into_iter().has_less_than(2));
1513 assert!(!len(2).into_iter().has_less_than(2));
1514 assert!(len(0).into_iter().has_less_than(1));
1515 assert!(!len(0).into_iter().has_less_than(0));
1516 assert!(len(2).into_iter().has_less_or_exactly(2));
1517 assert!(!len(3).into_iter().has_less_or_exactly(2));
1518 assert!(len(3).into_iter().has_more_than(2));
1519 assert!(!len(2).into_iter().has_more_than(2));
1520 assert!(len(2).into_iter().has_more_or_exactly(2));
1521 assert!(!len(1).into_iter().has_more_or_exactly(2));
1522 assert!(len(5).into_iter().has_more_or_exactly(2));
1523 }
1524
1525 #[test]
1526 fn has_cardinality_short_circuits() {
1527 let (source, counter) = counting(0..1_000_000i64);
1528 assert!(source.has_more_than(2));
1529 assert_eq!(counter.get(), 3);
1530
1531 let (source, counter) = counting(0..1_000_000i64);
1532 assert!(!source.has_less_than(2));
1533 assert_eq!(counter.get(), 3);
1534
1535 let (source, counter) = counting(0..1_000_000i64);
1536 assert!(source.has_more_or_exactly(2));
1537 assert_eq!(counter.get(), 2);
1538 }
1539
1540 #[test]
1541 fn reduce_and_map_folds_then_projects() {
1542 let report =
1543 [1, 2, 3]
1544 .into_iter()
1545 .reduce_and_map(0, |acc, x| acc + x, |sum| format!("total {sum}"));
1546 assert_eq!(report, "total 6");
1547 let count = std::iter::empty::<i32>().reduce_and_map(0, |a, _| a + 1, |c| c);
1548 assert_eq!(count, 0);
1549 }
1550
1551 #[test]
1552 fn contains_compares_and_short_circuits() {
1553 assert!(["a", "b"].into_iter().contains(&"b"));
1554 assert!(!["a", "b"].into_iter().contains(&"c"));
1555 let (source, counter) =
1556 counting(vec![String::from("x"), String::from("hit"), String::from("y")].into_iter());
1557 assert!(source.contains(&String::from("hit")));
1558 assert_eq!(counter.get(), 2);
1559 }
1560
1561 #[test]
1562 fn to_map_chain_builds_nested_leaves() {
1563 let rows = [
1564 ("sp", "sa", 1),
1565 ("sp", "rj", 2),
1566 ("sp", "sa", 3),
1567 ("mg", "sa", 4),
1568 ];
1569 let index = rows
1570 .into_iter()
1571 .to_map_chain(|(a, _, _)| *a, |(_, b, _)| *b);
1572 assert_eq!(index.len(), 2);
1573 assert_eq!(
1574 index["sp"]["sa"]
1575 .iter()
1576 .map(|(_, _, v)| *v)
1577 .collect::<Vec<_>>(),
1578 vec![1, 3]
1579 );
1580 assert_eq!(index["sp"]["rj"].len(), 1);
1581 assert_eq!(index["mg"]["sa"].len(), 1);
1582 }
1583
1584 #[test]
1585 fn to_map_chain_reduce_folds_leaves() {
1586 let rows = [("sp", "sa", 1), ("sp", "sa", 2), ("sp", "rj", 5)];
1587 let totals = rows.into_iter().to_map_chain_reduce(
1588 |(a, _, _)| *a,
1589 |(_, b, _)| *b,
1590 0,
1591 |acc, (_, _, v)| acc + v,
1592 );
1593 assert_eq!(totals["sp"]["sa"], 3);
1594 assert_eq!(totals["sp"]["rj"], 5);
1595 assert!(!totals.contains_key("mg"));
1596 }
1597
1598 #[test]
1599 fn combine_join_folds_iterables_by_key() {
1600 let a = [(1, "a1"), (2, "a2")];
1601 let b = [(1, "b1"), (2, "b2")];
1602 let c = [(1, "c1")];
1603 let rows: Vec<Vec<_>> = [a.as_slice(), b.as_slice(), c.as_slice()]
1604 .into_iter()
1605 .combine_join(|(k, _)| *k)
1606 .collect();
1607 assert_eq!(rows.len(), 1);
1608 assert_eq!(
1609 rows[0].iter().map(|(_, v)| *v).collect::<Vec<_>>(),
1610 vec!["a1", "b1", "c1"]
1611 );
1612 }
1613
1614 #[test]
1615 fn combine_join_drops_keys_missing_from_any_iterable() {
1616 let a = [(1, "a1"), (2, "a2")];
1617 let b = [(1, "b1")];
1618 let c = [(2, "c2")];
1619 // key 1 dies at `c`, key 2 died at `b` → empty
1620 let rows: Vec<Vec<_>> = [a.as_slice(), b.as_slice(), c.as_slice()]
1621 .into_iter()
1622 .combine_join(|(k, _)| *k)
1623 .collect();
1624 assert!(rows.is_empty());
1625 }
1626
1627 #[test]
1628 fn combine_join_multiplies_duplicate_keys() {
1629 let a = [(1, "a1"), (1, "a2")];
1630 let b = [(1, "b1")];
1631 let rows: Vec<Vec<_>> = [a.as_slice(), b.as_slice()]
1632 .into_iter()
1633 .combine_join(|(k, _)| *k)
1634 .collect();
1635 assert_eq!(rows.len(), 2);
1636 assert_eq!(
1637 HashSet::from_iter(rows.iter().map(|r| r[0].1)),
1638 HashSet::from(["a1", "a2"])
1639 );
1640 }
1641
1642 #[test]
1643 fn adapters_resolve_std_iterator_methods() {
1644 // 0.0.3 regression: SortBy/CombineJoin carried K only in the impl
1645 // header, so std Iterator methods failed to resolve on them from a
1646 // bare call site. Keep K pinned in the struct type parameters.
1647 // (Note std `Iterator` has no `first` — that one is inherent to
1648 // slices — so the std spelling of "take the least" is `next()`.)
1649 let rows = [("b", 1), ("a", 2), ("b", 3)];
1650 assert_eq!(rows.iter().sort_by(|(_, v)| *v).count(), 3);
1651 assert_eq!(
1652 rows.iter().sort_by_desc(|(_, v)| *v).next().copied(),
1653 Some(("b", 3))
1654 );
1655 // ascending first == the minimum key (1, held by "b").
1656 assert_eq!(
1657 rows.iter().sort_by(|(_, v)| *v).next().copied(),
1658 Some(("b", 1))
1659 );
1660 let groups = vec![vec![("x", 1)], vec![("x", 2), ("y", 3)]];
1661 assert_eq!(groups.into_iter().combine_join(|(k, _)| *k).count(), 1);
1662 }
1663
1664 #[test]
1665 fn combine_join_of_nothing_is_empty() {
1666 let rows: Vec<Vec<_>> = std::iter::empty::<&[(i32, &str)]>()
1667 .combine_join(|(k, _)| *k)
1668 .collect();
1669 assert!(rows.is_empty());
1670 }
1671
1672 #[test]
1673 fn chaining_matches_readme_example() {
1674 let rows = [("sa", 10), ("sp", 30), ("sa", 25)];
1675 let by_state: Vec<_> = rows.into_iter().group_by_stable(|(s, _)| *s).collect();
1676 assert_eq!(by_state[0].1, vec![("sa", 10), ("sa", 25)]);
1677 let report = rows
1678 .into_iter()
1679 .map(|(_, v)| v)
1680 .join_by(|v| format!("{v:>4}"), " | ");
1681 assert_eq!(report, " 10 | 30 | 25");
1682 }
1683}