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