Skip to main content

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, F>
433where
434    I: Iterator,
435{
436    phase: Phase<I, Vec<T>>,
437    key: F,
438}
439
440impl<I, T, K, F> Iterator for CombineJoin<I, T, F>
441where
442    I: Iterator,
443    I::Item: IntoIterator<Item = T>,
444    K: Eq + Hash,
445    T: Clone,
446    F: FnMut(&T) -> K,
447{
448    type Item = Vec<T>;
449
450    fn next(&mut self) -> Option<Vec<T>> {
451        self.phase.next_with(|src| {
452            let mut acc: Vec<Vec<T>> = Vec::new();
453            let mut first = true;
454            for iterable in src {
455                let items: Vec<T> = iterable.into_iter().collect();
456                if first {
457                    first = false;
458                    acc = items.into_iter().map(|item| vec![item]).collect();
459                    continue;
460                }
461                if acc.is_empty() {
462                    continue;
463                }
464                let mut idx: HashMap<K, Vec<T>> = HashMap::new();
465                for item in items {
466                    idx.entry((self.key)(&item)).or_default().push(item);
467                }
468                let mut next_acc = Vec::new();
469                for row in acc {
470                    // Rows are non-empty by construction; their key never
471                    // changes across folds (inner-join invariant).
472                    if let Some(bucket) = idx.get(&(self.key)(&row[0])) {
473                        for item in bucket {
474                            let mut grown = row.clone();
475                            grown.push(item.clone());
476                            next_acc.push(grown);
477                        }
478                    }
479                }
480                acc = next_acc;
481            }
482            acc
483        })
484    }
485
486    fn size_hint(&self) -> (usize, Option<usize>) {
487        self.phase.size_hint()
488    }
489}
490
491/// Sorted sequence produced by [`FluentIter::sorted`] — see there.
492pub struct Sorted<I: Iterator> {
493    phase: Phase<I, I::Item>,
494}
495
496impl<I> Iterator for Sorted<I>
497where
498    I: Iterator,
499    I::Item: Ord,
500{
501    type Item = I::Item;
502
503    fn next(&mut self) -> Option<I::Item> {
504        self.phase.next_with(|src| {
505            let mut out: Vec<I::Item> = src.collect();
506            out.sort();
507            out
508        })
509    }
510
511    fn size_hint(&self) -> (usize, Option<usize>) {
512        self.phase.size_hint()
513    }
514}
515
516/// Sorted sequence produced by [`FluentIter::sort_with`] — see there.
517pub struct SortWith<I: Iterator, F> {
518    phase: Phase<I, I::Item>,
519    compare: F,
520}
521
522impl<I, F> Iterator for SortWith<I, F>
523where
524    I: Iterator,
525    F: FnMut(&I::Item, &I::Item) -> Ordering,
526{
527    type Item = I::Item;
528
529    fn next(&mut self) -> Option<I::Item> {
530        self.phase.next_with(|src| {
531            let mut out: Vec<I::Item> = src.collect();
532            out.sort_by(&mut self.compare);
533            out
534        })
535    }
536
537    fn size_hint(&self) -> (usize, Option<usize>) {
538        self.phase.size_hint()
539    }
540}
541
542/// Sorted sequence produced by [`FluentIter::sort_by`] / [`FluentIter::sort_by_desc`].
543pub struct SortBy<I: Iterator, F> {
544    phase: Phase<I, I::Item>,
545    key: F,
546    descending: bool,
547}
548
549impl<I, K, F> Iterator for SortBy<I, F>
550where
551    I: Iterator,
552    K: Ord,
553    F: FnMut(&I::Item) -> K,
554{
555    type Item = I::Item;
556
557    fn next(&mut self) -> Option<I::Item> {
558        self.phase.next_with(|src| {
559            let mut out: Vec<I::Item> = src.collect();
560            if self.descending {
561                out.sort_by_key(|item| std::cmp::Reverse((self.key)(item)));
562            } else {
563                out.sort_by_key(&mut self.key);
564            }
565            out
566        })
567    }
568
569    fn size_hint(&self) -> (usize, Option<usize>) {
570        self.phase.size_hint()
571    }
572}
573
574/// The missing combinators of [`std::iter::Iterator`], in the
575/// [`@codibre/fluent-iterable`] vocabulary (see the [crate docs](crate) for
576/// the full npm ⇄ Rust map).
577///
578/// Blanket-implemented for every iterator — just `use fluent_iterable::FluentIter;`.
579///
580/// Every operator that npm returns as an iterable returns an [`Iterator`]
581/// here too — chain freely and resolve with [`collect`][Iterator::collect]
582/// when you want a collection. Operators that npm resolves (`avg`, `join`,
583/// `toMap`, `hasX`, `contains`, …) return the plain value here.
584pub trait FluentIter: Iterator + Sized {
585    /// ⇔ `.group(keyFn)`: **stable** grouping — first-seen key order is
586    /// preserved and each group keeps encounter order.
587    ///
588    /// Note [`Iterator::group_by`] only groups *consecutive* elements,
589    /// which is a different operation (that one is `slice::group_by`
590    /// territory); this is the npm `group`, which is a full pass.
591    ///
592    /// ```
593    /// use fluent_iterable::FluentIter;
594    ///
595    /// let grouped: Vec<_> = ["b", "a", "b", "c", "a"]
596    ///     .into_iter()
597    ///     .group_by_stable(|x| *x)
598    ///     .collect();
599    /// let keys: Vec<_> = grouped.iter().map(|(k, _)| *k).collect();
600    /// assert_eq!(keys, vec!["b", "a", "c"]);
601    /// assert_eq!(grouped[1].1, vec!["a", "a"]);
602    /// ```
603    ///
604    /// Lazy: the full grouping pass runs on the first pull, then groups are
605    /// yielded one at a time — chain on without materializing the pair vec.
606    fn group_by_stable<K, F>(self, key: F) -> GroupByStable<Self, K, F>
607    where
608        K: Eq + Hash + Clone,
609        F: FnMut(&Self::Item) -> K,
610    {
611        GroupByStable {
612            phase: Phase::Source(self),
613            key,
614        }
615    }
616
617    /// ⇔ `.combine(other, keySelf, keyOther)`: inner equi-join yielding
618    /// **all** matching `(left, right)` pairs in left order (right side is
619    /// indexed for `O(n + m)`; duplicate keys explode into pairs exactly
620    /// like the JS side).
621    ///
622    /// JS shares object references between pairs; Rust must clone to hand
623    /// owned pairs out. Joining iterators *of references*
624    /// (`a.iter().combine(b.iter(), …)`) keeps the clone pointer-cheap.
625    ///
626    /// ```
627    /// use fluent_iterable::FluentIter;
628    ///
629    /// let left = [1, 2, 1];
630    /// let right = [(1, "x"), (1, "y"), (3, "z")];
631    /// let joined: Vec<(&i32, &(i32, &str))> =
632    ///     left.iter().combine(right.iter(), |l| **l, |(k, _)| *k).collect();
633    /// let shape: Vec<(i32, &str)> =
634    ///     joined.into_iter().map(|(l, (_, n))| (*l, *n)).collect();
635    /// assert_eq!(shape, vec![(1, "x"), (1, "y"), (1, "x"), (1, "y")]);
636    /// ```
637    ///
638    /// Lazy: the right side is indexed on the first pull, the left side
639    /// streams — `take(k)` stops the left walk at `k` pairs.
640    fn combine<J, K, KL, KR>(
641        self,
642        other: J,
643        key_left: KL,
644        key_right: KR,
645    ) -> Combine<Self, J::IntoIter, K, KL, KR>
646    where
647        J: IntoIterator,
648        K: Eq + Hash + Clone,
649        KL: FnMut(&Self::Item) -> K,
650        KR: FnMut(&J::Item) -> K,
651        Self::Item: Clone,
652        J::Item: Clone,
653    {
654        Combine {
655            left: self,
656            key_left,
657            right: other.into_iter(),
658            key_right,
659            indexed: false,
660            idx: HashMap::new(),
661            pending: None,
662        }
663    }
664
665    /// ⇔ `.combineJoin(baseKey)` on an iterator **of iterables**: reduces
666    /// the whole family into one table of rows by a shared key — inner
667    /// join, pairwise folded (`n` iterables produce rows of length `n`).
668    /// The key is taken from the row's first element, exactly like the JS
669    /// `([a]) => key(a)`. Once a fold step matches nothing the result is
670    /// empty and stays empty.
671    ///
672    /// ```
673    /// use fluent_iterable::FluentIter;
674    ///
675    /// let a = [(1, "a1"), (2, "a2")];
676    /// let b = [(1, "b1"), (2, "b2")];
677    /// let c = [(1, "c1")];
678    /// let rows: Vec<Vec<_>> = [a.as_slice(), b.as_slice(), c.as_slice()]
679    ///     .into_iter()
680    ///     .combine_join(|(k, _)| *k)
681    ///     .collect();
682    /// assert_eq!(rows.len(), 1);
683    /// assert_eq!(
684    ///     rows[0].iter().map(|(_, v)| *v).collect::<Vec<_>>(),
685    ///     vec!["a1", "b1", "c1"]
686    /// );
687    /// ```
688    ///
689    /// Lazy: the whole family is folded on the first pull, rows are yielded
690    /// one at a time.
691    fn combine_join<T, K, F>(self, key: F) -> CombineJoin<Self, T, F>
692    where
693        Self::Item: IntoIterator<Item = T>,
694        K: Eq + Hash,
695        T: Clone,
696        F: FnMut(&T) -> K,
697    {
698        CombineJoin {
699            phase: Phase::Source(self),
700            key,
701        }
702    }
703
704    /// ⇔ `.distinct()` on hashable items: keeps the **first** occurrence of
705    /// every item. Lazy — buffers only the seen-set, and short-circuits
706    /// downstream (`take(k)` stops pulling once `k` uniques are out).
707    ///
708    /// `Item` must be `Clone` (like `itertools::unique`): the set retains a
709    /// copy of each value to compare against while the occurrence itself is
710    /// yielded. Use [`FluentIter::distinct_by`] when cloning is not
711    /// available — it tracks borrowed keys instead.
712    ///
713    /// ```
714    /// use fluent_iterable::FluentIter;
715    ///
716    /// let out: Vec<i32> = [1, 2, 1, 3, 2].into_iter().distinct().collect();
717    /// assert_eq!(out, vec![1, 2, 3]);
718    /// ```
719    fn distinct(self) -> Distinct<Self>
720    where
721        Self::Item: Eq + Hash + Clone,
722    {
723        Distinct {
724            iter: self,
725            seen: HashSet::new(),
726        }
727    }
728
729    /// ⇔ `.distinct(keyFn)`: keep the **first** item seen for each key.
730    ///
731    /// Purely lazy — buffers only the seen-key set, never the sequence, so
732    /// `take(k)` stops pulling the source once `k` distinct keys are out.
733    ///
734    /// ```
735    /// use fluent_iterable::FluentIter;
736    ///
737    /// let out: Vec<&str> = ["bb", "a1", "b1", "a2", "c"]
738    ///     .into_iter()
739    ///     .distinct_by(|s| s.chars().next().unwrap())
740    ///     .collect();
741    /// assert_eq!(out, vec!["bb", "a1", "c"]);
742    /// ```
743    fn distinct_by<K, F>(self, key: F) -> DistinctBy<Self, K, F>
744    where
745        K: Eq + Hash,
746        F: FnMut(&Self::Item) -> K,
747    {
748        DistinctBy {
749            iter: self,
750            key,
751            seen: HashSet::new(),
752        }
753    }
754
755    /// ⇔ `.isDistinct()`: are all items unique? Short-circuits on the first
756    /// duplicate (the source is not drained).
757    ///
758    /// ```
759    /// use fluent_iterable::FluentIter;
760    ///
761    /// assert!([1, 2, 3].into_iter().is_distinct());
762    /// assert!(![1, 2, 1].into_iter().is_distinct());
763    /// ```
764    // npm parity names (`isDistinct`/`isDistinctBy`): these consume the
765    // iterator instead of taking &mut self, because Iterator::all needs an
766    // owned chain through the extension trait.
767    #[allow(clippy::wrong_self_convention)]
768    fn is_distinct(mut self) -> bool
769    where
770        Self::Item: Eq + Hash,
771    {
772        let mut seen = HashSet::new();
773        self.all(|item| seen.insert(item))
774    }
775
776    /// ⇔ `.isDistinct(keyFn)`: is every key unique? Short-circuits on the
777    /// first repetition.
778    ///
779    /// ```
780    /// use fluent_iterable::FluentIter;
781    ///
782    /// assert!([("a", 1), ("b", 2)].into_iter().is_distinct_by(|(k, _)| *k));
783    /// assert!(![("a", 1), ("b", 2), ("a", 3)].into_iter().is_distinct_by(|(k, _)| *k));
784    /// ```
785    #[allow(clippy::wrong_self_convention)]
786    fn is_distinct_by<K>(mut self, mut key: impl FnMut(&Self::Item) -> K) -> bool
787    where
788        K: Eq + Hash,
789    {
790        let mut seen = HashSet::new();
791        self.all(|item| seen.insert(key(&item)))
792    }
793
794    /// ⇔ `.whenEmpty(fallback)`: yield from the source; if it produces
795    /// **nothing**, yield the fallback instead. A non-empty source never
796    /// touches the fallback iterable.
797    ///
798    /// Unlike JS the fallback must yield the same `Item` type — map it
799    /// first if it doesn't.
800    ///
801    /// ```
802    /// use fluent_iterable::FluentIter;
803    ///
804    /// let empty: [i32; 0] = [];
805    /// let out: Vec<i32> = empty.into_iter().when_empty([7, 8]).collect();
806    /// assert_eq!(out, vec![7, 8]);
807    ///
808    /// let out: Vec<i32> = [1].into_iter().when_empty([7, 8]).collect();
809    /// assert_eq!(out, vec![1]);
810    /// ```
811    fn when_empty<F>(self, fallback: F) -> WhenEmpty<Self, F>
812    where
813        F: IntoIterator<Item = Self::Item>,
814    {
815        WhenEmpty {
816            phase: WhenEmptyPhase::Source(self, false),
817            fallback: Some(fallback),
818        }
819    }
820
821    /// ⇔ `.repeat(n)`: yield the whole sequence `n` times (first pass
822    /// streams and caches, later passes replay). `n == 0` yields nothing
823    /// and never consumes the source. Unlike `std`'s nightly-only `cycle`,
824    /// this is finite.
825    ///
826    /// ```
827    /// use fluent_iterable::FluentIter;
828    ///
829    /// let out: Vec<i32> = [1, 2].into_iter().repeat(3).collect();
830    /// assert_eq!(out, vec![1, 2, 1, 2, 1, 2]);
831    ///
832    /// let out: Vec<i32> = [1, 2].into_iter().repeat(0).collect();
833    /// assert!(out.is_empty());
834    /// ```
835    fn repeat(self, times: usize) -> Repeat<Self>
836    where
837        Self::Item: Clone,
838    {
839        Repeat {
840            source: self,
841            cache: Vec::new(),
842            times,
843            round: 0,
844            pos: 0,
845        }
846    }
847
848    /// ⇔ `.toMap(keyFn, valueFn)`: later entries overwrite earlier ones for
849    /// the same key (JS `Map.set` semantics; `HashMap` iteration order is
850    /// unspecified, like the JS engine's).
851    ///
852    /// ```
853    /// use fluent_iterable::FluentIter;
854    ///
855    /// let items = vec![("k", 1), ("j", 2), ("k", 3)];
856    /// let map = items.into_iter().to_map_by(|(k, _)| *k, |(_, v)| *v);
857    /// assert_eq!(map.get("k"), Some(&3));
858    /// ```
859    fn to_map_by<K, V>(
860        self,
861        mut key: impl FnMut(&Self::Item) -> K,
862        mut value: impl FnMut(&Self::Item) -> V,
863    ) -> HashMap<K, V>
864    where
865        K: Eq + Hash,
866    {
867        self.map(|item| (key(&item), value(&item))).collect()
868    }
869
870    /// ⇔ `.toMapChain(k1, k2)`: two-level index — `key1` outer, `key2`
871    /// inner, leaves keep encounter order.
872    ///
873    /// JS chains to an arbitrary number of key functions; Rust fixes the
874    /// nested type at depth 2 — deeper nesting is a fold of these layers.
875    ///
876    /// ```
877    /// use fluent_iterable::FluentIter;
878    ///
879    /// let rows = [("sp", "sa", 1), ("sp", "rj", 2), ("mg", "sa", 3)];
880    /// let index = rows
881    ///     .into_iter()
882    ///     .to_map_chain(|(a, _, _)| *a, |(_, b, _)| *b);
883    /// assert_eq!(index["sp"]["sa"], vec![("sp", "sa", 1)]);
884    /// assert_eq!(index["sp"]["rj"], vec![("sp", "rj", 2)]);
885    /// ```
886    fn to_map_chain<K1, K2>(
887        self,
888        mut key1: impl FnMut(&Self::Item) -> K1,
889        mut key2: impl FnMut(&Self::Item) -> K2,
890    ) -> HashMap<K1, HashMap<K2, Vec<Self::Item>>>
891    where
892        K1: Eq + Hash,
893        K2: Eq + Hash,
894    {
895        let mut out: HashMap<K1, HashMap<K2, Vec<Self::Item>>> = HashMap::new();
896        for item in self {
897            out.entry(key1(&item))
898                .or_default()
899                .entry(key2(&item))
900                .or_default()
901                .push(item);
902        }
903        out
904    }
905
906    /// ⇔ `.toMapChainReduce(k1, k2, reducer)`: two-level index whose leaves
907    /// are folded instead of collected. JS folds without an explicit
908    /// initial value; Rust demands one (there is no `undefined`).
909    ///
910    /// ```
911    /// use fluent_iterable::FluentIter;
912    ///
913    /// let rows = [("sp", "sa", 1), ("sp", "sa", 2), ("sp", "rj", 5)];
914    /// let totals = rows.into_iter().to_map_chain_reduce(
915    ///     |(a, _, _)| *a,
916    ///     |(_, b, _)| *b,
917    ///     0,
918    ///     |acc, (_, _, v)| acc + v,
919    /// );
920    /// assert_eq!(totals["sp"]["sa"], 3);
921    /// assert_eq!(totals["sp"]["rj"], 5);
922    /// ```
923    fn to_map_chain_reduce<K1, K2, A>(
924        self,
925        key1: impl FnMut(&Self::Item) -> K1,
926        key2: impl FnMut(&Self::Item) -> K2,
927        init: A,
928        mut reduce: impl FnMut(A, Self::Item) -> A,
929    ) -> HashMap<K1, HashMap<K2, A>>
930    where
931        K1: Eq + Hash,
932        K2: Eq + Hash,
933        A: Clone,
934    {
935        self.to_map_chain(key1, key2)
936            .into_iter()
937            .map(|(k1, inner)| {
938                (
939                    k1,
940                    inner
941                        .into_iter()
942                        .map(|(k2, items)| {
943                            let acc = items.into_iter().fold(init.clone(), &mut reduce);
944                            (k2, acc)
945                        })
946                        .collect(),
947                )
948            })
949            .collect()
950    }
951
952    /// ⇔ `.sort()` with the type's own ordering: collect and **stable**
953    /// sort. (JS would stringify non-comparable items; Rust uses `Ord`.)
954    ///
955    /// ```
956    /// use fluent_iterable::FluentIter;
957    ///
958    /// let out: Vec<i32> = [3, 1, 2].into_iter().sorted().collect();
959    /// assert_eq!(out, vec![1, 2, 3]);
960    /// ```
961    ///
962    /// Lazy wrapper over the sort, ⇔ npm's `IterableSorted`: the collect +
963    /// sort runs on the first pull, then items are yielded in order.
964    fn sorted(self) -> Sorted<Self>
965    where
966        Self::Item: Ord,
967    {
968        Sorted {
969            phase: Phase::Source(self),
970        }
971    }
972
973    /// ⇔ `.sort(compareFn)`: stable sort with a custom comparator.
974    ///
975    /// ```
976    /// use fluent_iterable::FluentIter;
977    ///
978    /// let out: Vec<i32> = [1, 2, 3].into_iter().sort_with(|a, b| b.cmp(a)).collect();
979    /// assert_eq!(out, vec![3, 2, 1]);
980    /// ```
981    ///
982    /// Lazy wrapper over the sort (buffered on first pull).
983    fn sort_with<F>(self, compare: F) -> SortWith<Self, F>
984    where
985        F: FnMut(&Self::Item, &Self::Item) -> Ordering,
986    {
987        SortWith {
988            phase: Phase::Source(self),
989            compare,
990        }
991    }
992
993    /// ⇔ `.sortBy(keyFn)`: stable sort ascending by key. Ties keep encounter
994    /// order. As with `slice::sort_by_key` the key function may be called
995    /// more than once per element — keep it cheap, or sort decorated values.
996    ///
997    /// ```
998    /// use fluent_iterable::FluentIter;
999    ///
1000    /// let rows = [("b", 1), ("a", 2), ("b", 3)];
1001    /// let out: Vec<_> = rows.into_iter().sort_by(|(_, v)| *v).collect();
1002    /// assert_eq!(out, vec![("b", 1), ("a", 2), ("b", 3)]);
1003    /// ```
1004    ///
1005    /// Lazy wrapper over the sort (buffered on first pull).
1006    fn sort_by<K, F>(self, key: F) -> SortBy<Self, F>
1007    where
1008        K: Ord,
1009        F: FnMut(&Self::Item) -> K,
1010    {
1011        SortBy {
1012            phase: Phase::Source(self),
1013            key,
1014            descending: false,
1015        }
1016    }
1017
1018    /// ⇔ `.sortBy('-keyFn')` (the descending order descriptor): stable sort
1019    /// descending by key; ties keep encounter order.
1020    ///
1021    /// ```
1022    /// use fluent_iterable::FluentIter;
1023    ///
1024    /// let rows = [("b", 1), ("a", 2), ("b", 3)];
1025    /// let out: Vec<_> = rows.into_iter().sort_by_desc(|(_, v)| *v).collect();
1026    /// assert_eq!(out, vec![("b", 3), ("a", 2), ("b", 1)]);
1027    /// ```
1028    ///
1029    /// Lazy wrapper over the sort (buffered on first pull).
1030    fn sort_by_desc<K, F>(self, key: F) -> SortBy<Self, F>
1031    where
1032        K: Ord,
1033        F: FnMut(&Self::Item) -> K,
1034    {
1035        SortBy {
1036            phase: Phase::Source(self),
1037            key,
1038            descending: true,
1039        }
1040    }
1041
1042    /// ⇔ `.avg()`: arithmetic mean as `Option<f64>` — `None` on empty
1043    /// (JS returns `NaN`; Rust says it in the type).
1044    ///
1045    /// ```
1046    /// use fluent_iterable::FluentIter;
1047    ///
1048    /// assert_eq!([2, 4, 6].into_iter().avg(), Some(4.0));
1049    /// assert_eq!(std::iter::empty::<i32>().avg(), None);
1050    /// ```
1051    fn avg(self) -> Option<f64>
1052    where
1053        Self::Item: Into<f64>,
1054    {
1055        let (mut sum, mut count) = (0f64, 0usize);
1056        for item in self {
1057            sum += item.into();
1058            count += 1;
1059        }
1060        if count == 0 {
1061            None
1062        } else {
1063            Some(sum / count as f64)
1064        }
1065    }
1066
1067    /// ⇔ `.avg(mapFn)`: mean of a projection.
1068    ///
1069    /// ```
1070    /// use fluent_iterable::FluentIter;
1071    ///
1072    /// let rows = [("x", 10.0), ("y", 20.0)];
1073    /// assert_eq!(rows.into_iter().avg_by(|(_, v)| *v), Some(15.0));
1074    /// ```
1075    fn avg_by(self, mut value: impl FnMut(&Self::Item) -> f64) -> Option<f64> {
1076        let (mut sum, mut count) = (0f64, 0usize);
1077        for item in self {
1078            sum += value(&item);
1079            count += 1;
1080        }
1081        if count == 0 {
1082            None
1083        } else {
1084            Some(sum / count as f64)
1085        }
1086    }
1087
1088    /// ⇔ `.join(sep)`: display every item with the separator between.
1089    /// (JS skips nullish items; Rust has neither, every item is shown.)
1090    ///
1091    /// ```
1092    /// use fluent_iterable::FluentIter;
1093    ///
1094    /// assert_eq!([1, 2, 3].into_iter().join(", "), "1, 2, 3");
1095    /// ```
1096    fn join(self, separator: &str) -> String
1097    where
1098        Self::Item: Display,
1099    {
1100        self.map(|item| item.to_string())
1101            .collect::<Vec<String>>()
1102            .join(separator)
1103    }
1104
1105    /// ⇔ `.join(sep, mapFn)`: project each item before joining.
1106    ///
1107    /// ```
1108    /// use fluent_iterable::FluentIter;
1109    ///
1110    /// let out = [1, 2].into_iter().join_by(|v| format!("{v:>3}"), "-");
1111    /// assert_eq!(out, "  1-  2");
1112    /// ```
1113    fn join_by<S: Display>(
1114        self,
1115        mut value: impl FnMut(&Self::Item) -> S,
1116        separator: &str,
1117    ) -> String {
1118        self.map(|item| value(&item).to_string())
1119            .collect::<Vec<String>>()
1120            .join(separator)
1121    }
1122
1123    /// ⇔ `.hasExactly(n)`: exactly `n` items — and no more. Short-circuits
1124    /// after `n + 1` items.
1125    ///
1126    /// ```
1127    /// use fluent_iterable::FluentIter;
1128    ///
1129    /// assert!([1, 2].into_iter().has_exactly(2));
1130    /// assert!(![1, 2, 3].into_iter().has_exactly(2));
1131    /// ```
1132    fn has_exactly(self, n: usize) -> bool {
1133        self.take(n.saturating_add(1)).count() == n
1134    }
1135
1136    /// ⇔ `.hasLessThan(n)`: strictly fewer than `n` items. Short-circuits
1137    /// after `n + 1`.
1138    ///
1139    /// ```
1140    /// use fluent_iterable::FluentIter;
1141    ///
1142    /// assert!([1].into_iter().has_less_than(2));
1143    /// assert!(![1, 2].into_iter().has_less_than(2));
1144    /// ```
1145    fn has_less_than(self, n: usize) -> bool {
1146        self.take(n.saturating_add(1)).count() < n
1147    }
1148
1149    /// ⇔ `.hasLessOrExactly(n)`: at most `n` items. Short-circuits after
1150    /// `n + 1`.
1151    ///
1152    /// ```
1153    /// use fluent_iterable::FluentIter;
1154    ///
1155    /// assert!([1, 2].into_iter().has_less_or_exactly(2));
1156    /// assert!(![1, 2, 3].into_iter().has_less_or_exactly(2));
1157    /// ```
1158    fn has_less_or_exactly(self, n: usize) -> bool {
1159        self.take(n.saturating_add(1)).count() <= n
1160    }
1161
1162    /// ⇔ `.hasMoreThan(n)`: strictly more than `n` items. Short-circuits
1163    /// after `n + 1`.
1164    ///
1165    /// ```
1166    /// use fluent_iterable::FluentIter;
1167    ///
1168    /// assert!([1, 2, 3].into_iter().has_more_than(2));
1169    /// assert!(![1, 2].into_iter().has_more_than(2));
1170    /// ```
1171    fn has_more_than(self, n: usize) -> bool {
1172        self.take(n.saturating_add(1)).count() > n
1173    }
1174
1175    /// ⇔ `.hasMoreOrExactly(n)`: at least `n` items. Short-circuits after
1176    /// `n` — it cannot tell you *how many* more.
1177    ///
1178    /// ```
1179    /// use fluent_iterable::FluentIter;
1180    ///
1181    /// assert!([1, 2].into_iter().has_more_or_exactly(2));
1182    /// assert!(![1].into_iter().has_more_or_exactly(2));
1183    /// ```
1184    fn has_more_or_exactly(self, n: usize) -> bool {
1185        self.take(n).count() >= n
1186    }
1187
1188    /// ⇔ `.reduceAndMap(reducer, initial, resultFn)`: fold, then project the
1189    /// accumulator. Argument order follows [`Iterator::fold`] (initial
1190    /// first), not the JS signature.
1191    ///
1192    /// ```
1193    /// use fluent_iterable::FluentIter;
1194    ///
1195    /// let report = [1, 2, 3]
1196    ///     .into_iter()
1197    ///     .reduce_and_map(0, |acc, x| acc + x, |sum| format!("total {sum}"));
1198    /// assert_eq!(report, "total 6");
1199    /// ```
1200    fn reduce_and_map<A, R>(
1201        self,
1202        init: A,
1203        mut fold: impl FnMut(A, Self::Item) -> A,
1204        mut map: impl FnMut(A) -> R,
1205    ) -> R {
1206        map(self.fold(init, &mut fold))
1207    }
1208
1209    /// ⇔ `.contains(item)`: any item equal to `item` (JS compares with
1210    /// `===`; Rust uses [`PartialEq`]). Short-circuits on the first hit.
1211    ///
1212    /// ```
1213    /// use fluent_iterable::FluentIter;
1214    ///
1215    /// assert!(["a", "b"].into_iter().contains(&"b"));
1216    /// assert!(!["a", "b"].into_iter().contains(&"c"));
1217    /// ```
1218    fn contains(mut self, item: &Self::Item) -> bool
1219    where
1220        Self::Item: PartialEq,
1221    {
1222        self.any(|x| &x == item)
1223    }
1224}
1225
1226impl<I: Iterator> FluentIter for I {}
1227
1228#[cfg(test)]
1229mod tests {
1230    use super::*;
1231    use std::cell::Cell;
1232    use std::iter::FromIterator;
1233    use std::rc::Rc;
1234
1235    /// Counts how many items the wrapped iterator ever yielded — proof of
1236    /// short-circuiting for the lazy/terminal checks.
1237    struct Counting<I: Iterator> {
1238        inner: I,
1239        yielded: Rc<Cell<usize>>,
1240    }
1241
1242    impl<I: Iterator> Iterator for Counting<I> {
1243        type Item = I::Item;
1244        fn next(&mut self) -> Option<I::Item> {
1245            let item = self.inner.next();
1246            if item.is_some() {
1247                self.yielded.set(self.yielded.get() + 1);
1248            }
1249            item
1250        }
1251    }
1252
1253    fn counting<I: Iterator>(inner: I) -> (Counting<I>, Rc<Cell<usize>>) {
1254        let counter = Rc::new(Cell::new(0));
1255        (
1256            Counting {
1257                inner,
1258                yielded: Rc::clone(&counter),
1259            },
1260            counter,
1261        )
1262    }
1263
1264    // ---- ported from the proven mq crate ----
1265
1266    #[test]
1267    fn stable_group_order() {
1268        let items = ["b", "a", "b", "c", "a"];
1269        let grouped: Vec<_> = items.into_iter().group_by_stable(|x| *x).collect();
1270        let keys: Vec<_> = grouped.iter().map(|(k, _)| *k).collect();
1271        assert_eq!(keys, vec!["b", "a", "c"]);
1272        assert_eq!(grouped[1].1, vec!["a", "a"]);
1273    }
1274
1275    #[test]
1276    fn combine_all_pairs_left_order() {
1277        let left = [1, 2, 1];
1278        let right = [(1, "x"), (1, "y"), (3, "z")];
1279        let joined: Vec<_> = left
1280            .iter()
1281            .combine(right.iter(), |l| **l, |(k, _)| *k)
1282            .collect();
1283        assert_eq!(joined.len(), 4);
1284        let shapes: Vec<(i32, &str)> = joined
1285            .into_iter()
1286            .map(|(l, (_, name))| (*l, *name))
1287            .collect();
1288        assert_eq!(shapes, vec![(1, "x"), (1, "y"), (1, "x"), (1, "y")]);
1289    }
1290
1291    #[test]
1292    fn combine_owned_items_fan_out_clones() {
1293        let left = vec![String::from("a"), String::from("a")];
1294        let right = vec![String::from("a1"), String::from("a2")];
1295        let joined = left
1296            .into_iter()
1297            .combine(right, |l| l.clone(), |r| r[..1].to_owned());
1298        let shapes: Vec<(String, String)> = joined.into_iter().collect();
1299        assert_eq!(
1300            shapes,
1301            vec![
1302                ("a".to_owned(), "a1".to_owned()),
1303                ("a".to_owned(), "a2".to_owned()),
1304                ("a".to_owned(), "a1".to_owned()),
1305                ("a".to_owned(), "a2".to_owned()),
1306            ]
1307        );
1308    }
1309
1310    #[test]
1311    fn distinct_keeps_first_per_key() {
1312        let items = ["bb", "a1", "b1", "a2", "c"];
1313        let out: Vec<_> = items
1314            .into_iter()
1315            .distinct_by(|s| s.chars().next().unwrap())
1316            .collect();
1317        assert_eq!(out, vec!["bb", "a1", "c"]);
1318    }
1319
1320    #[test]
1321    fn to_map_last_write_wins() {
1322        let items = vec![("k", 1), ("j", 2), ("k", 3)];
1323        let map = items.into_iter().to_map_by(|(k, _)| *k, |(_, v)| *v);
1324        assert_eq!(map.get("k"), Some(&3));
1325        assert_eq!(map.get("j"), Some(&2));
1326        assert_eq!(map.len(), 2);
1327    }
1328
1329    // ---- lazy adapters ----
1330
1331    #[test]
1332    fn distinct_is_lazy_and_short_circuits() {
1333        let (source, counter) = counting([1, 1, 2, 3, 3, 3, 4].into_iter());
1334        let out: Vec<i32> = source.distinct().take(2).collect();
1335        assert_eq!(out, vec![1, 2]);
1336        // stopped right after discovering 2 (never walked the tail)
1337        assert!(counter.get() <= 4, "consumed {}", counter.get());
1338    }
1339
1340    #[test]
1341    fn is_distinct_checks_and_short_circuits() {
1342        assert!([1, 2, 3].into_iter().is_distinct());
1343        assert!(std::iter::empty::<i32>().is_distinct());
1344        let (source, counter) = counting([1, 2, 1, 2, 1].into_iter());
1345        assert!(!source.is_distinct());
1346        assert_eq!(counter.get(), 3);
1347    }
1348
1349    #[test]
1350    fn is_distinct_by_short_circuits() {
1351        assert!([("a", 1), ("b", 2)].into_iter().is_distinct_by(|(k, _)| *k));
1352        let (source, counter) = counting([("a", 1), ("b", 2), ("a", 3), ("b", 4)].into_iter());
1353        assert!(!source.is_distinct_by(|(k, _)| *k));
1354        assert_eq!(counter.get(), 3);
1355    }
1356
1357    #[test]
1358    fn when_empty_uses_fallback_only_for_empty_source() {
1359        let empty: [i32; 0] = [];
1360        let out: Vec<i32> = empty.into_iter().when_empty([7, 8]).collect();
1361        assert_eq!(out, vec![7, 8]);
1362
1363        let (fallback_source, fallback_counter) = counting([7, 8].into_iter());
1364        let out: Vec<i32> = [1, 2].into_iter().when_empty(fallback_source).collect();
1365        assert_eq!(out, vec![1, 2]);
1366        assert_eq!(fallback_counter.get(), 0, "fallback must stay untouched");
1367    }
1368
1369    #[test]
1370    fn when_empty_with_empty_fallback_is_empty() {
1371        let empty: [i32; 0] = [];
1372        let out: Vec<i32> = empty.into_iter().when_empty(empty).collect();
1373        assert!(out.is_empty());
1374    }
1375
1376    #[test]
1377    fn when_empty_falls_back_after_late_first_value_absent() {
1378        // source yields nothing but is polled several times before giving up
1379        let (source, counter) = counting(std::iter::empty::<i32>());
1380        let out: Vec<i32> = source.when_empty([9]).collect();
1381        assert_eq!(out, vec![9]);
1382        assert_eq!(counter.get(), 0);
1383    }
1384
1385    #[test]
1386    fn repeat_replays_whole_sequence() {
1387        let out: Vec<i32> = [1, 2].into_iter().repeat(3).collect();
1388        assert_eq!(out, vec![1, 2, 1, 2, 1, 2]);
1389        let out: Vec<i32> = [1, 2].into_iter().repeat(1).collect();
1390        assert_eq!(out, vec![1, 2]);
1391    }
1392
1393    #[test]
1394    fn repeat_zero_never_consumes_source() {
1395        let (source, counter) = counting([1, 2, 3].into_iter());
1396        let out: Vec<i32> = source.repeat(0).collect();
1397        assert!(out.is_empty());
1398        assert_eq!(counter.get(), 0);
1399    }
1400
1401    #[test]
1402    fn repeat_streams_first_pass_before_replaying() {
1403        let (source, counter) = counting([1, 2, 3].into_iter());
1404        let mut rep = source.repeat(2);
1405        assert_eq!(rep.next(), Some(1));
1406        // only one item pulled so far: the first pass is streaming
1407        assert_eq!(counter.get(), 1);
1408        assert_eq!(rep.next(), Some(2));
1409        assert_eq!(rep.next(), Some(3));
1410        assert_eq!(rep.next(), Some(1)); // replay started
1411        assert_eq!(rep.count(), 2);
1412    }
1413
1414    #[test]
1415    fn repeat_of_empty_source_is_empty() {
1416        let out: Vec<i32> = std::iter::empty::<i32>().repeat(4).collect();
1417        assert!(out.is_empty());
1418    }
1419
1420    #[test]
1421    fn distinct_by_is_lazy_and_short_circuits() {
1422        // take(k) must stop pulling the source at the k-th distinct key —
1423        // impossible for an eager implementation (the design bug this
1424        // replaced).
1425        let (source, counter) = counting([1, 1, 2, 2, 3, 3, 4, 4].into_iter());
1426        let out: Vec<i32> = source.distinct_by(|x| *x).take(2).collect();
1427        assert_eq!(out, vec![1, 2]);
1428        assert_eq!(counter.get(), 3); // pulled up to the second `2`, no further
1429    }
1430
1431    #[test]
1432    fn buffered_adapters_consume_nothing_until_polled() {
1433        // ⇔ JS generators: building the chain runs no code; the internal
1434        // full pass happens on the first next().
1435        let (source, counter) = counting([3, 1, 2].into_iter());
1436        let _grouped = source.group_by_stable(|x| *x);
1437        assert_eq!(counter.get(), 0);
1438
1439        let (source, counter) = counting([3, 1, 2].into_iter());
1440        let mut sorted = source.sorted();
1441        assert_eq!(counter.get(), 0);
1442        assert_eq!(sorted.next(), Some(1)); // first pull buffers + sorts all
1443        assert_eq!(counter.get(), 3);
1444
1445        let (source, counter) = counting([vec![1], vec![2]].into_iter());
1446        let _joined = source.combine_join(|x| *x);
1447        assert_eq!(counter.get(), 0);
1448    }
1449
1450    #[test]
1451    fn combine_indexes_right_on_first_pull_and_streams_left() {
1452        let (left, left_pulls) = counting([1, 2, 3, 1, 2, 3].into_iter());
1453        let right = [(1, "x")];
1454        let mut joined = left.combine(right, |l| *l, |(k, _)| *k);
1455        assert_eq!(left_pulls.get(), 0);
1456        assert_eq!(joined.next(), Some((1, (1, "x"))));
1457        // left stopped at the first matching element: no full left scan
1458        assert_eq!(left_pulls.get(), 1);
1459        assert_eq!(joined.next(), Some((1, (1, "x"))));
1460        assert_eq!(joined.next(), None);
1461    }
1462
1463    // ---- resolving terminals ----
1464
1465    #[test]
1466    fn sort_family_is_stable() {
1467        let rows = [("b", 1), ("a", 2), ("b", 3), ("a", 1)];
1468        let by_value: Vec<_> = rows.into_iter().sort_by(|(_, v)| *v).collect();
1469        // ties (value 1 then 2..): encounter order preserved
1470        assert_eq!(by_value, vec![("b", 1), ("a", 1), ("a", 2), ("b", 3)]);
1471        let desc: Vec<_> = rows.into_iter().sort_by_desc(|(_, v)| *v).collect();
1472        assert_eq!(desc, vec![("b", 3), ("a", 2), ("b", 1), ("a", 1)]);
1473        let asc: Vec<u8> = [3u8, 1, 2].into_iter().sorted().collect();
1474        assert_eq!(asc, vec![1, 2, 3]);
1475        let custom: Vec<i32> = [1, 2, 3].into_iter().sort_with(|a, b| b.cmp(a)).collect();
1476        assert_eq!(custom, vec![3, 2, 1]);
1477    }
1478
1479    #[test]
1480    fn avg_handles_ints_floats_and_empty() {
1481        assert_eq!([2, 4, 6].into_iter().avg(), Some(4.0));
1482        assert_eq!(std::iter::empty::<i32>().avg(), None);
1483        let rows = [("x", 10.0), ("y", 20.0)];
1484        assert_eq!(rows.into_iter().avg_by(|(_, v)| *v), Some(15.0));
1485        assert_eq!(std::iter::empty::<&str>().avg_by(|_| 1.0), None);
1486    }
1487
1488    #[test]
1489    fn join_displays_and_projects() {
1490        assert_eq!([1, 2, 3].into_iter().join(", "), "1, 2, 3");
1491        assert_eq!(std::iter::empty::<i32>().join(","), "");
1492        let out = [1, 2].into_iter().join_by(|v| format!("{v:>3}"), "-");
1493        assert_eq!(out, "  1-  2");
1494    }
1495
1496    #[test]
1497    fn has_cardinality_matrix() {
1498        // source length × n → each predicate, matching comparisonRecipe:
1499        // count(take(n+1)) `op` n
1500        let len = |n: usize| (0..n).collect::<Vec<_>>();
1501        assert!(len(2).into_iter().has_exactly(2));
1502        assert!(!len(3).into_iter().has_exactly(2));
1503        assert!(!len(1).into_iter().has_exactly(2));
1504        // count(take(1)) == 0 → empty sequence hasExactly(0), like the JS
1505        // comparisonRecipe
1506        assert!(len(0).into_iter().has_exactly(0));
1507        assert!(len(1).into_iter().has_less_than(2));
1508        assert!(!len(2).into_iter().has_less_than(2));
1509        assert!(len(0).into_iter().has_less_than(1));
1510        assert!(!len(0).into_iter().has_less_than(0));
1511        assert!(len(2).into_iter().has_less_or_exactly(2));
1512        assert!(!len(3).into_iter().has_less_or_exactly(2));
1513        assert!(len(3).into_iter().has_more_than(2));
1514        assert!(!len(2).into_iter().has_more_than(2));
1515        assert!(len(2).into_iter().has_more_or_exactly(2));
1516        assert!(!len(1).into_iter().has_more_or_exactly(2));
1517        assert!(len(5).into_iter().has_more_or_exactly(2));
1518    }
1519
1520    #[test]
1521    fn has_cardinality_short_circuits() {
1522        let (source, counter) = counting(0..1_000_000i64);
1523        assert!(source.has_more_than(2));
1524        assert_eq!(counter.get(), 3);
1525
1526        let (source, counter) = counting(0..1_000_000i64);
1527        assert!(!source.has_less_than(2));
1528        assert_eq!(counter.get(), 3);
1529
1530        let (source, counter) = counting(0..1_000_000i64);
1531        assert!(source.has_more_or_exactly(2));
1532        assert_eq!(counter.get(), 2);
1533    }
1534
1535    #[test]
1536    fn reduce_and_map_folds_then_projects() {
1537        let report =
1538            [1, 2, 3]
1539                .into_iter()
1540                .reduce_and_map(0, |acc, x| acc + x, |sum| format!("total {sum}"));
1541        assert_eq!(report, "total 6");
1542        let count = std::iter::empty::<i32>().reduce_and_map(0, |a, _| a + 1, |c| c);
1543        assert_eq!(count, 0);
1544    }
1545
1546    #[test]
1547    fn contains_compares_and_short_circuits() {
1548        assert!(["a", "b"].into_iter().contains(&"b"));
1549        assert!(!["a", "b"].into_iter().contains(&"c"));
1550        let (source, counter) =
1551            counting(vec![String::from("x"), String::from("hit"), String::from("y")].into_iter());
1552        assert!(source.contains(&String::from("hit")));
1553        assert_eq!(counter.get(), 2);
1554    }
1555
1556    #[test]
1557    fn to_map_chain_builds_nested_leaves() {
1558        let rows = [
1559            ("sp", "sa", 1),
1560            ("sp", "rj", 2),
1561            ("sp", "sa", 3),
1562            ("mg", "sa", 4),
1563        ];
1564        let index = rows
1565            .into_iter()
1566            .to_map_chain(|(a, _, _)| *a, |(_, b, _)| *b);
1567        assert_eq!(index.len(), 2);
1568        assert_eq!(
1569            index["sp"]["sa"]
1570                .iter()
1571                .map(|(_, _, v)| *v)
1572                .collect::<Vec<_>>(),
1573            vec![1, 3]
1574        );
1575        assert_eq!(index["sp"]["rj"].len(), 1);
1576        assert_eq!(index["mg"]["sa"].len(), 1);
1577    }
1578
1579    #[test]
1580    fn to_map_chain_reduce_folds_leaves() {
1581        let rows = [("sp", "sa", 1), ("sp", "sa", 2), ("sp", "rj", 5)];
1582        let totals = rows.into_iter().to_map_chain_reduce(
1583            |(a, _, _)| *a,
1584            |(_, b, _)| *b,
1585            0,
1586            |acc, (_, _, v)| acc + v,
1587        );
1588        assert_eq!(totals["sp"]["sa"], 3);
1589        assert_eq!(totals["sp"]["rj"], 5);
1590        assert!(!totals.contains_key("mg"));
1591    }
1592
1593    #[test]
1594    fn combine_join_folds_iterables_by_key() {
1595        let a = [(1, "a1"), (2, "a2")];
1596        let b = [(1, "b1"), (2, "b2")];
1597        let c = [(1, "c1")];
1598        let rows: Vec<Vec<_>> = [a.as_slice(), b.as_slice(), c.as_slice()]
1599            .into_iter()
1600            .combine_join(|(k, _)| *k)
1601            .collect();
1602        assert_eq!(rows.len(), 1);
1603        assert_eq!(
1604            rows[0].iter().map(|(_, v)| *v).collect::<Vec<_>>(),
1605            vec!["a1", "b1", "c1"]
1606        );
1607    }
1608
1609    #[test]
1610    fn combine_join_drops_keys_missing_from_any_iterable() {
1611        let a = [(1, "a1"), (2, "a2")];
1612        let b = [(1, "b1")];
1613        let c = [(2, "c2")];
1614        // key 1 dies at `c`, key 2 died at `b` → empty
1615        let rows: Vec<Vec<_>> = [a.as_slice(), b.as_slice(), c.as_slice()]
1616            .into_iter()
1617            .combine_join(|(k, _)| *k)
1618            .collect();
1619        assert!(rows.is_empty());
1620    }
1621
1622    #[test]
1623    fn combine_join_multiplies_duplicate_keys() {
1624        let a = [(1, "a1"), (1, "a2")];
1625        let b = [(1, "b1")];
1626        let rows: Vec<Vec<_>> = [a.as_slice(), b.as_slice()]
1627            .into_iter()
1628            .combine_join(|(k, _)| *k)
1629            .collect();
1630        assert_eq!(rows.len(), 2);
1631        assert_eq!(
1632            HashSet::from_iter(rows.iter().map(|r| r[0].1)),
1633            HashSet::from(["a1", "a2"])
1634        );
1635    }
1636
1637    #[test]
1638    fn combine_join_of_nothing_is_empty() {
1639        let rows: Vec<Vec<_>> = std::iter::empty::<&[(i32, &str)]>()
1640            .combine_join(|(k, _)| *k)
1641            .collect();
1642        assert!(rows.is_empty());
1643    }
1644
1645    #[test]
1646    fn chaining_matches_readme_example() {
1647        let rows = [("sa", 10), ("sp", 30), ("sa", 25)];
1648        let by_state: Vec<_> = rows.into_iter().group_by_stable(|(s, _)| *s).collect();
1649        assert_eq!(by_state[0].1, vec![("sa", 10), ("sa", 25)]);
1650        let report = rows
1651            .into_iter()
1652            .map(|(_, v)| v)
1653            .join_by(|v| format!("{v:>4}"), " | ");
1654        assert_eq!(report, "  10 |   30 |   25");
1655    }
1656}