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);
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/// The missing combinators of [`std::iter::Iterator`], in the
249/// `@codibre/fluent-iterable` vocabulary (see the [crate docs](crate) for
250/// the full npm ⇄ Rust map).
251///
252/// Blanket-implemented for every iterator — just `use fluent_iterable::FluentIter;`.
253pub trait FluentIter: Iterator + Sized {
254    /// ⇔ `.group(keyFn)`: **stable** grouping — first-seen key order is
255    /// preserved and each group keeps encounter order.
256    ///
257    /// Note [`Iterator::group_by`] only groups *consecutive* elements,
258    /// which is a different operation (that one is `slice::group_by`
259    /// territory); this is the npm `group`, which is a full pass.
260    ///
261    /// ```
262    /// use fluent_iterable::FluentIter;
263    ///
264    /// let grouped = ["b", "a", "b", "c", "a"].into_iter().group_by_stable(|x| *x);
265    /// let keys: Vec<_> = grouped.iter().map(|(k, _)| *k).collect();
266    /// assert_eq!(keys, vec!["b", "a", "c"]);
267    /// assert_eq!(grouped[1].1, vec!["a", "a"]);
268    /// ```
269    fn group_by_stable<K>(self, mut key: impl FnMut(&Self::Item) -> K) -> Vec<(K, Vec<Self::Item>)>
270    where
271        K: Eq + Hash + Clone,
272    {
273        let mut index: HashMap<K, usize> = HashMap::new();
274        let mut out: Vec<(K, Vec<Self::Item>)> = Vec::new();
275        for item in self {
276            let k = key(&item);
277            match index.get(&k) {
278                Some(&i) => out[i].1.push(item),
279                None => {
280                    index.insert(k.clone(), out.len());
281                    out.push((k, vec![item]));
282                }
283            }
284        }
285        out
286    }
287
288    /// ⇔ `.combine(other, keySelf, keyOther)`: inner equi-join yielding
289    /// **all** matching `(left, right)` pairs in left order (right side is
290    /// indexed for `O(n + m)`; duplicate keys explode into pairs exactly
291    /// like the JS side).
292    ///
293    /// JS shares object references between pairs; Rust must clone to hand
294    /// owned pairs out. Joining iterators *of references*
295    /// (`a.iter().combine(b.iter(), …)`) keeps the clone pointer-cheap.
296    ///
297    /// ```
298    /// use fluent_iterable::FluentIter;
299    ///
300    /// let left = [1, 2, 1];
301    /// let right = [(1, "x"), (1, "y"), (3, "z")];
302    /// let joined: Vec<(&i32, &(i32, &str))> =
303    ///     left.iter().combine(right.iter(), |l| **l, |(k, _)| *k);
304    /// let shape: Vec<(i32, &str)> =
305    ///     joined.into_iter().map(|(l, (_, n))| (*l, *n)).collect();
306    /// assert_eq!(shape, vec![(1, "x"), (1, "y"), (1, "x"), (1, "y")]);
307    /// ```
308    fn combine<J, K>(
309        self,
310        other: J,
311        mut key_left: impl FnMut(&Self::Item) -> K,
312        mut key_right: impl FnMut(&J::Item) -> K,
313    ) -> Vec<(Self::Item, J::Item)>
314    where
315        J: IntoIterator,
316        K: Eq + Hash,
317        Self::Item: Clone,
318        J::Item: Clone,
319    {
320        let mut idx: HashMap<K, Vec<J::Item>> = HashMap::new();
321        for r in other {
322            idx.entry(key_right(&r)).or_default().push(r);
323        }
324        let mut out = Vec::new();
325        for l in self {
326            if let Some(rs) = idx.get(&key_left(&l)) {
327                let (last, rest) = rs.split_last().expect("join bucket is non-empty");
328                for r in rest {
329                    out.push((l.clone(), r.clone()));
330                }
331                out.push((l, last.clone()));
332            }
333        }
334        out
335    }
336
337    /// ⇔ `.combineJoin(baseKey)` on an iterator **of iterables**: reduces
338    /// the whole family into one table of rows by a shared key — inner
339    /// join, pairwise folded (`n` iterables produce rows of length `n`).
340    /// The key is taken from the row's first element, exactly like the JS
341    /// `([a]) => key(a)`. Once a fold step matches nothing the result is
342    /// empty and stays empty.
343    ///
344    /// ```
345    /// use fluent_iterable::FluentIter;
346    ///
347    /// let a = [(1, "a1"), (2, "a2")];
348    /// let b = [(1, "b1"), (2, "b2")];
349    /// let c = [(1, "c1")];
350    /// let rows = [a.as_slice(), b.as_slice(), c.as_slice()]
351    ///     .into_iter()
352    ///     .combine_join(|(k, _)| *k);
353    /// assert_eq!(rows.len(), 1);
354    /// assert_eq!(
355    ///     rows[0].iter().map(|(_, v)| *v).collect::<Vec<_>>(),
356    ///     vec!["a1", "b1", "c1"]
357    /// );
358    /// ```
359    fn combine_join<K, T>(self, mut key: impl FnMut(&T) -> K) -> Vec<Vec<T>>
360    where
361        Self::Item: IntoIterator<Item = T>,
362        K: Eq + Hash,
363        T: Clone,
364    {
365        let mut acc: Vec<Vec<T>> = Vec::new();
366        let mut first = true;
367        for iterable in self {
368            let items: Vec<T> = iterable.into_iter().collect();
369            if first {
370                first = false;
371                acc = items.into_iter().map(|item| vec![item]).collect();
372                continue;
373            }
374            if acc.is_empty() {
375                continue;
376            }
377            let mut idx: HashMap<K, Vec<T>> = HashMap::new();
378            for item in items {
379                idx.entry(key(&item)).or_default().push(item);
380            }
381            let mut next_acc = Vec::new();
382            for row in acc {
383                // Rows are non-empty by construction; their key never
384                // changes across folds (inner-join invariant).
385                if let Some(bucket) = idx.get(&key(&row[0])) {
386                    for item in bucket {
387                        let mut grown = row.clone();
388                        grown.push(item.clone());
389                        next_acc.push(grown);
390                    }
391                }
392            }
393            acc = next_acc;
394        }
395        acc
396    }
397
398    /// ⇔ `.distinct()` on hashable items: keeps the **first** occurrence of
399    /// every item. Lazy — buffers only the seen-set, and short-circuits
400    /// downstream (`take(k)` stops pulling once `k` uniques are out).
401    ///
402    /// `Item` must be `Clone` (like `itertools::unique`): the set retains a
403    /// copy of each value to compare against while the occurrence itself is
404    /// yielded. Use [`FluentIter::distinct_by`] when cloning is not
405    /// available — it tracks borrowed keys instead.
406    ///
407    /// ```
408    /// use fluent_iterable::FluentIter;
409    ///
410    /// let out: Vec<i32> = [1, 2, 1, 3, 2].into_iter().distinct().collect();
411    /// assert_eq!(out, vec![1, 2, 3]);
412    /// ```
413    fn distinct(self) -> Distinct<Self>
414    where
415        Self::Item: Eq + Hash + Clone,
416    {
417        Distinct {
418            iter: self,
419            seen: HashSet::new(),
420        }
421    }
422
423    /// ⇔ `.distinct(keyFn)`: keep the **first** item seen for each key.
424    ///
425    /// ```
426    /// use fluent_iterable::FluentIter;
427    ///
428    /// let out: Vec<&str> = ["bb", "a1", "b1", "a2", "c"]
429    ///     .into_iter()
430    ///     .distinct_by(|s| s.chars().next().unwrap());
431    /// assert_eq!(out, vec!["bb", "a1", "c"]);
432    /// ```
433    fn distinct_by<K>(self, mut key: impl FnMut(&Self::Item) -> K) -> Vec<Self::Item>
434    where
435        K: Eq + Hash,
436    {
437        let mut seen: HashSet<K> = HashSet::new();
438        self.filter(|item| seen.insert(key(item))).collect()
439    }
440
441    /// ⇔ `.isDistinct()`: are all items unique? Short-circuits on the first
442    /// duplicate (the source is not drained).
443    ///
444    /// ```
445    /// use fluent_iterable::FluentIter;
446    ///
447    /// assert!([1, 2, 3].into_iter().is_distinct());
448    /// assert!(![1, 2, 1].into_iter().is_distinct());
449    /// ```
450    // npm parity names (`isDistinct`/`isDistinctBy`): these consume the
451    // iterator instead of taking &mut self, because Iterator::all needs an
452    // owned chain through the extension trait.
453    #[allow(clippy::wrong_self_convention)]
454    fn is_distinct(mut self) -> bool
455    where
456        Self::Item: Eq + Hash,
457    {
458        let mut seen = HashSet::new();
459        self.all(|item| seen.insert(item))
460    }
461
462    /// ⇔ `.isDistinct(keyFn)`: is every key unique? Short-circuits on the
463    /// first repetition.
464    ///
465    /// ```
466    /// use fluent_iterable::FluentIter;
467    ///
468    /// assert!([("a", 1), ("b", 2)].into_iter().is_distinct_by(|(k, _)| *k));
469    /// assert!(![("a", 1), ("b", 2), ("a", 3)].into_iter().is_distinct_by(|(k, _)| *k));
470    /// ```
471    #[allow(clippy::wrong_self_convention)]
472    fn is_distinct_by<K>(mut self, mut key: impl FnMut(&Self::Item) -> K) -> bool
473    where
474        K: Eq + Hash,
475    {
476        let mut seen = HashSet::new();
477        self.all(|item| seen.insert(key(&item)))
478    }
479
480    /// ⇔ `.whenEmpty(fallback)`: yield from the source; if it produces
481    /// **nothing**, yield the fallback instead. A non-empty source never
482    /// touches the fallback iterable.
483    ///
484    /// Unlike JS the fallback must yield the same `Item` type — map it
485    /// first if it doesn't.
486    ///
487    /// ```
488    /// use fluent_iterable::FluentIter;
489    ///
490    /// let empty: [i32; 0] = [];
491    /// let out: Vec<i32> = empty.into_iter().when_empty([7, 8]).collect();
492    /// assert_eq!(out, vec![7, 8]);
493    ///
494    /// let out: Vec<i32> = [1].into_iter().when_empty([7, 8]).collect();
495    /// assert_eq!(out, vec![1]);
496    /// ```
497    fn when_empty<F>(self, fallback: F) -> WhenEmpty<Self, F>
498    where
499        F: IntoIterator<Item = Self::Item>,
500    {
501        WhenEmpty {
502            phase: WhenEmptyPhase::Source(self, false),
503            fallback: Some(fallback),
504        }
505    }
506
507    /// ⇔ `.repeat(n)`: yield the whole sequence `n` times (first pass
508    /// streams and caches, later passes replay). `n == 0` yields nothing
509    /// and never consumes the source. Unlike `std`'s nightly-only `cycle`,
510    /// this is finite.
511    ///
512    /// ```
513    /// use fluent_iterable::FluentIter;
514    ///
515    /// let out: Vec<i32> = [1, 2].into_iter().repeat(3).collect();
516    /// assert_eq!(out, vec![1, 2, 1, 2, 1, 2]);
517    ///
518    /// let out: Vec<i32> = [1, 2].into_iter().repeat(0).collect();
519    /// assert!(out.is_empty());
520    /// ```
521    fn repeat(self, times: usize) -> Repeat<Self>
522    where
523        Self::Item: Clone,
524    {
525        Repeat {
526            source: self,
527            cache: Vec::new(),
528            times,
529            round: 0,
530            pos: 0,
531        }
532    }
533
534    /// ⇔ `.toMap(keyFn, valueFn)`: later entries overwrite earlier ones for
535    /// the same key (JS `Map.set` semantics; `HashMap` iteration order is
536    /// unspecified, like the JS engine's).
537    ///
538    /// ```
539    /// use fluent_iterable::FluentIter;
540    ///
541    /// let items = vec![("k", 1), ("j", 2), ("k", 3)];
542    /// let map = items.into_iter().to_map_by(|(k, _)| *k, |(_, v)| *v);
543    /// assert_eq!(map.get("k"), Some(&3));
544    /// ```
545    fn to_map_by<K, V>(
546        self,
547        mut key: impl FnMut(&Self::Item) -> K,
548        mut value: impl FnMut(&Self::Item) -> V,
549    ) -> HashMap<K, V>
550    where
551        K: Eq + Hash,
552    {
553        self.map(|item| (key(&item), value(&item))).collect()
554    }
555
556    /// ⇔ `.toMapChain(k1, k2)`: two-level index — `key1` outer, `key2`
557    /// inner, leaves keep encounter order.
558    ///
559    /// JS chains to an arbitrary number of key functions; Rust fixes the
560    /// nested type at depth 2 — deeper nesting is a fold of these layers.
561    ///
562    /// ```
563    /// use fluent_iterable::FluentIter;
564    ///
565    /// let rows = [("sp", "sa", 1), ("sp", "rj", 2), ("mg", "sa", 3)];
566    /// let index = rows
567    ///     .into_iter()
568    ///     .to_map_chain(|(a, _, _)| *a, |(_, b, _)| *b);
569    /// assert_eq!(index["sp"]["sa"], vec![("sp", "sa", 1)]);
570    /// assert_eq!(index["sp"]["rj"], vec![("sp", "rj", 2)]);
571    /// ```
572    fn to_map_chain<K1, K2>(
573        self,
574        mut key1: impl FnMut(&Self::Item) -> K1,
575        mut key2: impl FnMut(&Self::Item) -> K2,
576    ) -> HashMap<K1, HashMap<K2, Vec<Self::Item>>>
577    where
578        K1: Eq + Hash,
579        K2: Eq + Hash,
580    {
581        let mut out: HashMap<K1, HashMap<K2, Vec<Self::Item>>> = HashMap::new();
582        for item in self {
583            out.entry(key1(&item))
584                .or_default()
585                .entry(key2(&item))
586                .or_default()
587                .push(item);
588        }
589        out
590    }
591
592    /// ⇔ `.toMapChainReduce(k1, k2, reducer)`: two-level index whose leaves
593    /// are folded instead of collected. JS folds without an explicit
594    /// initial value; Rust demands one (there is no `undefined`).
595    ///
596    /// ```
597    /// use fluent_iterable::FluentIter;
598    ///
599    /// let rows = [("sp", "sa", 1), ("sp", "sa", 2), ("sp", "rj", 5)];
600    /// let totals = rows.into_iter().to_map_chain_reduce(
601    ///     |(a, _, _)| *a,
602    ///     |(_, b, _)| *b,
603    ///     0,
604    ///     |acc, (_, _, v)| acc + v,
605    /// );
606    /// assert_eq!(totals["sp"]["sa"], 3);
607    /// assert_eq!(totals["sp"]["rj"], 5);
608    /// ```
609    fn to_map_chain_reduce<K1, K2, A>(
610        self,
611        key1: impl FnMut(&Self::Item) -> K1,
612        key2: impl FnMut(&Self::Item) -> K2,
613        init: A,
614        mut reduce: impl FnMut(A, Self::Item) -> A,
615    ) -> HashMap<K1, HashMap<K2, A>>
616    where
617        K1: Eq + Hash,
618        K2: Eq + Hash,
619        A: Clone,
620    {
621        self.to_map_chain(key1, key2)
622            .into_iter()
623            .map(|(k1, inner)| {
624                (
625                    k1,
626                    inner
627                        .into_iter()
628                        .map(|(k2, items)| {
629                            let acc = items.into_iter().fold(init.clone(), &mut reduce);
630                            (k2, acc)
631                        })
632                        .collect(),
633                )
634            })
635            .collect()
636    }
637
638    /// ⇔ `.sort()` with the type's own ordering: collect and **stable**
639    /// sort. (JS would stringify non-comparable items; Rust uses `Ord`.)
640    ///
641    /// ```
642    /// use fluent_iterable::FluentIter;
643    ///
644    /// assert_eq!([3, 1, 2].into_iter().sorted(), vec![1, 2, 3]);
645    /// ```
646    fn sorted(self) -> Vec<Self::Item>
647    where
648        Self::Item: Ord,
649    {
650        let mut out: Vec<Self::Item> = self.collect();
651        out.sort();
652        out
653    }
654
655    /// ⇔ `.sort(compareFn)`: collect and stable sort with a custom
656    /// comparator.
657    ///
658    /// ```
659    /// use fluent_iterable::FluentIter;
660    /// use std::cmp::Ordering;
661    ///
662    /// let out = [1, 2, 3].into_iter().sort_with(|a, b| b.cmp(a));
663    /// assert_eq!(out, vec![3, 2, 1]);
664    /// ```
665    fn sort_with(
666        self,
667        mut compare: impl FnMut(&Self::Item, &Self::Item) -> Ordering,
668    ) -> Vec<Self::Item> {
669        let mut out: Vec<Self::Item> = self.collect();
670        out.sort_by(&mut compare);
671        out
672    }
673
674    /// ⇔ `.sortBy(keyFn)`: stable sort ascending by key. Ties keep encounter
675    /// order. As with `slice::sort_by_key` the key function may be called
676    /// more than once per element — keep it cheap, or sort decorated values.
677    ///
678    /// ```
679    /// use fluent_iterable::FluentIter;
680    ///
681    /// let rows = [("b", 1), ("a", 2), ("b", 3)];
682    /// let out = rows.into_iter().sort_by(|(_, v)| *v);
683    /// assert_eq!(out, vec![("b", 1), ("a", 2), ("b", 3)]);
684    /// ```
685    fn sort_by<K>(self, mut key: impl FnMut(&Self::Item) -> K) -> Vec<Self::Item>
686    where
687        K: Ord,
688    {
689        let mut out: Vec<Self::Item> = self.collect();
690        out.sort_by_key(&mut key);
691        out
692    }
693
694    /// ⇔ `.sortBy('-keyFn')` (the descending order descriptor): stable sort
695    /// descending by key; ties keep encounter order.
696    ///
697    /// ```
698    /// use fluent_iterable::FluentIter;
699    ///
700    /// let rows = [("b", 1), ("a", 2), ("b", 3)];
701    /// let out = rows.into_iter().sort_by_desc(|(_, v)| *v);
702    /// assert_eq!(out, vec![("b", 3), ("a", 2), ("b", 1)]);
703    /// ```
704    fn sort_by_desc<K>(self, mut key: impl FnMut(&Self::Item) -> K) -> Vec<Self::Item>
705    where
706        K: Ord,
707    {
708        let mut out: Vec<Self::Item> = self.collect();
709        out.sort_by_key(|item| std::cmp::Reverse(key(item)));
710        out
711    }
712
713    /// ⇔ `.avg()`: arithmetic mean as `Option<f64>` — `None` on empty
714    /// (JS returns `NaN`; Rust says it in the type).
715    ///
716    /// ```
717    /// use fluent_iterable::FluentIter;
718    ///
719    /// assert_eq!([2, 4, 6].into_iter().avg(), Some(4.0));
720    /// assert_eq!(std::iter::empty::<i32>().avg(), None);
721    /// ```
722    fn avg(self) -> Option<f64>
723    where
724        Self::Item: Into<f64>,
725    {
726        let (mut sum, mut count) = (0f64, 0usize);
727        for item in self {
728            sum += item.into();
729            count += 1;
730        }
731        if count == 0 {
732            None
733        } else {
734            Some(sum / count as f64)
735        }
736    }
737
738    /// ⇔ `.avg(mapFn)`: mean of a projection.
739    ///
740    /// ```
741    /// use fluent_iterable::FluentIter;
742    ///
743    /// let rows = [("x", 10.0), ("y", 20.0)];
744    /// assert_eq!(rows.into_iter().avg_by(|(_, v)| *v), Some(15.0));
745    /// ```
746    fn avg_by(self, mut value: impl FnMut(&Self::Item) -> f64) -> Option<f64> {
747        let (mut sum, mut count) = (0f64, 0usize);
748        for item in self {
749            sum += value(&item);
750            count += 1;
751        }
752        if count == 0 {
753            None
754        } else {
755            Some(sum / count as f64)
756        }
757    }
758
759    /// ⇔ `.join(sep)`: display every item with the separator between.
760    /// (JS skips nullish items; Rust has neither, every item is shown.)
761    ///
762    /// ```
763    /// use fluent_iterable::FluentIter;
764    ///
765    /// assert_eq!([1, 2, 3].into_iter().join(", "), "1, 2, 3");
766    /// ```
767    fn join(self, separator: &str) -> String
768    where
769        Self::Item: Display,
770    {
771        self.map(|item| item.to_string())
772            .collect::<Vec<String>>()
773            .join(separator)
774    }
775
776    /// ⇔ `.join(sep, mapFn)`: project each item before joining.
777    ///
778    /// ```
779    /// use fluent_iterable::FluentIter;
780    ///
781    /// let out = [1, 2].into_iter().join_by(|v| format!("{v:>3}"), "-");
782    /// assert_eq!(out, "  1-  2");
783    /// ```
784    fn join_by<S: Display>(
785        self,
786        mut value: impl FnMut(&Self::Item) -> S,
787        separator: &str,
788    ) -> String {
789        self.map(|item| value(&item).to_string())
790            .collect::<Vec<String>>()
791            .join(separator)
792    }
793
794    /// ⇔ `.hasExactly(n)`: exactly `n` items — and no more. Short-circuits
795    /// after `n + 1` items.
796    ///
797    /// ```
798    /// use fluent_iterable::FluentIter;
799    ///
800    /// assert!([1, 2].into_iter().has_exactly(2));
801    /// assert!(![1, 2, 3].into_iter().has_exactly(2));
802    /// ```
803    fn has_exactly(self, n: usize) -> bool {
804        self.take(n.saturating_add(1)).count() == n
805    }
806
807    /// ⇔ `.hasLessThan(n)`: strictly fewer than `n` items. Short-circuits
808    /// after `n + 1`.
809    ///
810    /// ```
811    /// use fluent_iterable::FluentIter;
812    ///
813    /// assert!([1].into_iter().has_less_than(2));
814    /// assert!(![1, 2].into_iter().has_less_than(2));
815    /// ```
816    fn has_less_than(self, n: usize) -> bool {
817        self.take(n.saturating_add(1)).count() < n
818    }
819
820    /// ⇔ `.hasLessOrExactly(n)`: at most `n` items. Short-circuits after
821    /// `n + 1`.
822    ///
823    /// ```
824    /// use fluent_iterable::FluentIter;
825    ///
826    /// assert!([1, 2].into_iter().has_less_or_exactly(2));
827    /// assert!(![1, 2, 3].into_iter().has_less_or_exactly(2));
828    /// ```
829    fn has_less_or_exactly(self, n: usize) -> bool {
830        self.take(n.saturating_add(1)).count() <= n
831    }
832
833    /// ⇔ `.hasMoreThan(n)`: strictly more than `n` items. Short-circuits
834    /// after `n + 1`.
835    ///
836    /// ```
837    /// use fluent_iterable::FluentIter;
838    ///
839    /// assert!([1, 2, 3].into_iter().has_more_than(2));
840    /// assert!(![1, 2].into_iter().has_more_than(2));
841    /// ```
842    fn has_more_than(self, n: usize) -> bool {
843        self.take(n.saturating_add(1)).count() > n
844    }
845
846    /// ⇔ `.hasMoreOrExactly(n)`: at least `n` items. Short-circuits after
847    /// `n` — it cannot tell you *how many* more.
848    ///
849    /// ```
850    /// use fluent_iterable::FluentIter;
851    ///
852    /// assert!([1, 2].into_iter().has_more_or_exactly(2));
853    /// assert!(![1].into_iter().has_more_or_exactly(2));
854    /// ```
855    fn has_more_or_exactly(self, n: usize) -> bool {
856        self.take(n).count() >= n
857    }
858
859    /// ⇔ `.reduceAndMap(reducer, initial, resultFn)`: fold, then project the
860    /// accumulator. Argument order follows [`Iterator::fold`] (initial
861    /// first), not the JS signature.
862    ///
863    /// ```
864    /// use fluent_iterable::FluentIter;
865    ///
866    /// let report = [1, 2, 3]
867    ///     .into_iter()
868    ///     .reduce_and_map(0, |acc, x| acc + x, |sum| format!("total {sum}"));
869    /// assert_eq!(report, "total 6");
870    /// ```
871    fn reduce_and_map<A, R>(
872        self,
873        init: A,
874        mut fold: impl FnMut(A, Self::Item) -> A,
875        mut map: impl FnMut(A) -> R,
876    ) -> R {
877        map(self.fold(init, &mut fold))
878    }
879
880    /// ⇔ `.contains(item)`: any item equal to `item` (JS compares with
881    /// `===`; Rust uses [`PartialEq`]). Short-circuits on the first hit.
882    ///
883    /// ```
884    /// use fluent_iterable::FluentIter;
885    ///
886    /// assert!(["a", "b"].into_iter().contains(&"b"));
887    /// assert!(!["a", "b"].into_iter().contains(&"c"));
888    /// ```
889    fn contains(mut self, item: &Self::Item) -> bool
890    where
891        Self::Item: PartialEq,
892    {
893        self.any(|x| &x == item)
894    }
895}
896
897impl<I: Iterator> FluentIter for I {}
898
899#[cfg(test)]
900mod tests {
901    use super::*;
902    use std::cell::Cell;
903    use std::iter::FromIterator;
904    use std::rc::Rc;
905
906    /// Counts how many items the wrapped iterator ever yielded — proof of
907    /// short-circuiting for the lazy/terminal checks.
908    struct Counting<I: Iterator> {
909        inner: I,
910        yielded: Rc<Cell<usize>>,
911    }
912
913    impl<I: Iterator> Iterator for Counting<I> {
914        type Item = I::Item;
915        fn next(&mut self) -> Option<I::Item> {
916            let item = self.inner.next();
917            if item.is_some() {
918                self.yielded.set(self.yielded.get() + 1);
919            }
920            item
921        }
922    }
923
924    fn counting<I: Iterator>(inner: I) -> (Counting<I>, Rc<Cell<usize>>) {
925        let counter = Rc::new(Cell::new(0));
926        (
927            Counting {
928                inner,
929                yielded: Rc::clone(&counter),
930            },
931            counter,
932        )
933    }
934
935    // ---- ported from the proven mq crate ----
936
937    #[test]
938    fn stable_group_order() {
939        let items = ["b", "a", "b", "c", "a"];
940        let grouped = items.into_iter().group_by_stable(|x| *x);
941        let keys: Vec<_> = grouped.iter().map(|(k, _)| *k).collect();
942        assert_eq!(keys, vec!["b", "a", "c"]);
943        assert_eq!(grouped[1].1, vec!["a", "a"]);
944    }
945
946    #[test]
947    fn combine_all_pairs_left_order() {
948        let left = [1, 2, 1];
949        let right = [(1, "x"), (1, "y"), (3, "z")];
950        let joined = left.iter().combine(right.iter(), |l| **l, |(k, _)| *k);
951        assert_eq!(joined.len(), 4);
952        let shapes: Vec<(i32, &str)> = joined
953            .into_iter()
954            .map(|(l, (_, name))| (*l, *name))
955            .collect();
956        assert_eq!(shapes, vec![(1, "x"), (1, "y"), (1, "x"), (1, "y")]);
957    }
958
959    #[test]
960    fn combine_owned_items_fan_out_clones() {
961        let left = vec![String::from("a"), String::from("a")];
962        let right = vec![String::from("a1"), String::from("a2")];
963        let joined = left
964            .into_iter()
965            .combine(right, |l| l.clone(), |r| r[..1].to_owned());
966        let shapes: Vec<(String, String)> = joined.into_iter().collect();
967        assert_eq!(
968            shapes,
969            vec![
970                ("a".to_owned(), "a1".to_owned()),
971                ("a".to_owned(), "a2".to_owned()),
972                ("a".to_owned(), "a1".to_owned()),
973                ("a".to_owned(), "a2".to_owned()),
974            ]
975        );
976    }
977
978    #[test]
979    fn distinct_keeps_first_per_key() {
980        let items = ["bb", "a1", "b1", "a2", "c"];
981        let out = items.into_iter().distinct_by(|s| s.chars().next().unwrap());
982        assert_eq!(out, vec!["bb", "a1", "c"]);
983    }
984
985    #[test]
986    fn to_map_last_write_wins() {
987        let items = vec![("k", 1), ("j", 2), ("k", 3)];
988        let map = items.into_iter().to_map_by(|(k, _)| *k, |(_, v)| *v);
989        assert_eq!(map.get("k"), Some(&3));
990        assert_eq!(map.get("j"), Some(&2));
991        assert_eq!(map.len(), 2);
992    }
993
994    // ---- lazy adapters ----
995
996    #[test]
997    fn distinct_is_lazy_and_short_circuits() {
998        let (source, counter) = counting([1, 1, 2, 3, 3, 3, 4].into_iter());
999        let out: Vec<i32> = source.distinct().take(2).collect();
1000        assert_eq!(out, vec![1, 2]);
1001        // stopped right after discovering 2 (never walked the tail)
1002        assert!(counter.get() <= 4, "consumed {}", counter.get());
1003    }
1004
1005    #[test]
1006    fn is_distinct_checks_and_short_circuits() {
1007        assert!([1, 2, 3].into_iter().is_distinct());
1008        assert!(std::iter::empty::<i32>().is_distinct());
1009        let (source, counter) = counting([1, 2, 1, 2, 1].into_iter());
1010        assert!(!source.is_distinct());
1011        assert_eq!(counter.get(), 3);
1012    }
1013
1014    #[test]
1015    fn is_distinct_by_short_circuits() {
1016        assert!([("a", 1), ("b", 2)].into_iter().is_distinct_by(|(k, _)| *k));
1017        let (source, counter) = counting([("a", 1), ("b", 2), ("a", 3), ("b", 4)].into_iter());
1018        assert!(!source.is_distinct_by(|(k, _)| *k));
1019        assert_eq!(counter.get(), 3);
1020    }
1021
1022    #[test]
1023    fn when_empty_uses_fallback_only_for_empty_source() {
1024        let empty: [i32; 0] = [];
1025        let out: Vec<i32> = empty.into_iter().when_empty([7, 8]).collect();
1026        assert_eq!(out, vec![7, 8]);
1027
1028        let (fallback_source, fallback_counter) = counting([7, 8].into_iter());
1029        let out: Vec<i32> = [1, 2].into_iter().when_empty(fallback_source).collect();
1030        assert_eq!(out, vec![1, 2]);
1031        assert_eq!(fallback_counter.get(), 0, "fallback must stay untouched");
1032    }
1033
1034    #[test]
1035    fn when_empty_with_empty_fallback_is_empty() {
1036        let empty: [i32; 0] = [];
1037        let out: Vec<i32> = empty.into_iter().when_empty(empty).collect();
1038        assert!(out.is_empty());
1039    }
1040
1041    #[test]
1042    fn when_empty_falls_back_after_late_first_value_absent() {
1043        // source yields nothing but is polled several times before giving up
1044        let (source, counter) = counting(std::iter::empty::<i32>());
1045        let out: Vec<i32> = source.when_empty([9]).collect();
1046        assert_eq!(out, vec![9]);
1047        assert_eq!(counter.get(), 0);
1048    }
1049
1050    #[test]
1051    fn repeat_replays_whole_sequence() {
1052        let out: Vec<i32> = [1, 2].into_iter().repeat(3).collect();
1053        assert_eq!(out, vec![1, 2, 1, 2, 1, 2]);
1054        let out: Vec<i32> = [1, 2].into_iter().repeat(1).collect();
1055        assert_eq!(out, vec![1, 2]);
1056    }
1057
1058    #[test]
1059    fn repeat_zero_never_consumes_source() {
1060        let (source, counter) = counting([1, 2, 3].into_iter());
1061        let out: Vec<i32> = source.repeat(0).collect();
1062        assert!(out.is_empty());
1063        assert_eq!(counter.get(), 0);
1064    }
1065
1066    #[test]
1067    fn repeat_streams_first_pass_before_replaying() {
1068        let (source, counter) = counting([1, 2, 3].into_iter());
1069        let mut rep = source.repeat(2);
1070        assert_eq!(rep.next(), Some(1));
1071        // only one item pulled so far: the first pass is streaming
1072        assert_eq!(counter.get(), 1);
1073        assert_eq!(rep.next(), Some(2));
1074        assert_eq!(rep.next(), Some(3));
1075        assert_eq!(rep.next(), Some(1)); // replay started
1076        assert_eq!(rep.count(), 2);
1077    }
1078
1079    #[test]
1080    fn repeat_of_empty_source_is_empty() {
1081        let out: Vec<i32> = std::iter::empty::<i32>().repeat(4).collect();
1082        assert!(out.is_empty());
1083    }
1084
1085    // ---- eager terminals ----
1086
1087    #[test]
1088    fn sort_family_is_stable() {
1089        let rows = [("b", 1), ("a", 2), ("b", 3), ("a", 1)];
1090        let by_value = rows.into_iter().sort_by(|(_, v)| *v);
1091        // ties (value 1 then 2..): encounter order preserved
1092        assert_eq!(by_value, vec![("b", 1), ("a", 1), ("a", 2), ("b", 3)]);
1093        let desc = rows.into_iter().sort_by_desc(|(_, v)| *v);
1094        assert_eq!(desc, vec![("b", 3), ("a", 2), ("b", 1), ("a", 1)]);
1095        assert_eq!([3u8, 1, 2].into_iter().sorted(), vec![1, 2, 3]);
1096        let custom = [1, 2, 3].into_iter().sort_with(|a, b| b.cmp(a));
1097        assert_eq!(custom, vec![3, 2, 1]);
1098    }
1099
1100    #[test]
1101    fn avg_handles_ints_floats_and_empty() {
1102        assert_eq!([2, 4, 6].into_iter().avg(), Some(4.0));
1103        assert_eq!(std::iter::empty::<i32>().avg(), None);
1104        let rows = [("x", 10.0), ("y", 20.0)];
1105        assert_eq!(rows.into_iter().avg_by(|(_, v)| *v), Some(15.0));
1106        assert_eq!(std::iter::empty::<&str>().avg_by(|_| 1.0), None);
1107    }
1108
1109    #[test]
1110    fn join_displays_and_projects() {
1111        assert_eq!([1, 2, 3].into_iter().join(", "), "1, 2, 3");
1112        assert_eq!(std::iter::empty::<i32>().join(","), "");
1113        let out = [1, 2].into_iter().join_by(|v| format!("{v:>3}"), "-");
1114        assert_eq!(out, "  1-  2");
1115    }
1116
1117    #[test]
1118    fn has_cardinality_matrix() {
1119        // source length × n → each predicate, matching comparisonRecipe:
1120        // count(take(n+1)) `op` n
1121        let len = |n: usize| (0..n).collect::<Vec<_>>();
1122        assert!(len(2).into_iter().has_exactly(2));
1123        assert!(!len(3).into_iter().has_exactly(2));
1124        assert!(!len(1).into_iter().has_exactly(2));
1125        // count(take(1)) == 0 → empty sequence hasExactly(0), like the JS
1126        // comparisonRecipe
1127        assert!(len(0).into_iter().has_exactly(0));
1128        assert!(len(1).into_iter().has_less_than(2));
1129        assert!(!len(2).into_iter().has_less_than(2));
1130        assert!(len(0).into_iter().has_less_than(1));
1131        assert!(!len(0).into_iter().has_less_than(0));
1132        assert!(len(2).into_iter().has_less_or_exactly(2));
1133        assert!(!len(3).into_iter().has_less_or_exactly(2));
1134        assert!(len(3).into_iter().has_more_than(2));
1135        assert!(!len(2).into_iter().has_more_than(2));
1136        assert!(len(2).into_iter().has_more_or_exactly(2));
1137        assert!(!len(1).into_iter().has_more_or_exactly(2));
1138        assert!(len(5).into_iter().has_more_or_exactly(2));
1139    }
1140
1141    #[test]
1142    fn has_cardinality_short_circuits() {
1143        let (source, counter) = counting(0..1_000_000i64);
1144        assert!(source.has_more_than(2));
1145        assert_eq!(counter.get(), 3);
1146
1147        let (source, counter) = counting(0..1_000_000i64);
1148        assert!(!source.has_less_than(2));
1149        assert_eq!(counter.get(), 3);
1150
1151        let (source, counter) = counting(0..1_000_000i64);
1152        assert!(source.has_more_or_exactly(2));
1153        assert_eq!(counter.get(), 2);
1154    }
1155
1156    #[test]
1157    fn reduce_and_map_folds_then_projects() {
1158        let report =
1159            [1, 2, 3]
1160                .into_iter()
1161                .reduce_and_map(0, |acc, x| acc + x, |sum| format!("total {sum}"));
1162        assert_eq!(report, "total 6");
1163        let count = std::iter::empty::<i32>().reduce_and_map(0, |a, _| a + 1, |c| c);
1164        assert_eq!(count, 0);
1165    }
1166
1167    #[test]
1168    fn contains_compares_and_short_circuits() {
1169        assert!(["a", "b"].into_iter().contains(&"b"));
1170        assert!(!["a", "b"].into_iter().contains(&"c"));
1171        let (source, counter) =
1172            counting(vec![String::from("x"), String::from("hit"), String::from("y")].into_iter());
1173        assert!(source.contains(&String::from("hit")));
1174        assert_eq!(counter.get(), 2);
1175    }
1176
1177    #[test]
1178    fn to_map_chain_builds_nested_leaves() {
1179        let rows = [
1180            ("sp", "sa", 1),
1181            ("sp", "rj", 2),
1182            ("sp", "sa", 3),
1183            ("mg", "sa", 4),
1184        ];
1185        let index = rows
1186            .into_iter()
1187            .to_map_chain(|(a, _, _)| *a, |(_, b, _)| *b);
1188        assert_eq!(index.len(), 2);
1189        assert_eq!(
1190            index["sp"]["sa"]
1191                .iter()
1192                .map(|(_, _, v)| *v)
1193                .collect::<Vec<_>>(),
1194            vec![1, 3]
1195        );
1196        assert_eq!(index["sp"]["rj"].len(), 1);
1197        assert_eq!(index["mg"]["sa"].len(), 1);
1198    }
1199
1200    #[test]
1201    fn to_map_chain_reduce_folds_leaves() {
1202        let rows = [("sp", "sa", 1), ("sp", "sa", 2), ("sp", "rj", 5)];
1203        let totals = rows.into_iter().to_map_chain_reduce(
1204            |(a, _, _)| *a,
1205            |(_, b, _)| *b,
1206            0,
1207            |acc, (_, _, v)| acc + v,
1208        );
1209        assert_eq!(totals["sp"]["sa"], 3);
1210        assert_eq!(totals["sp"]["rj"], 5);
1211        assert!(!totals.contains_key("mg"));
1212    }
1213
1214    #[test]
1215    fn combine_join_folds_iterables_by_key() {
1216        let a = [(1, "a1"), (2, "a2")];
1217        let b = [(1, "b1"), (2, "b2")];
1218        let c = [(1, "c1")];
1219        let rows = [a.as_slice(), b.as_slice(), c.as_slice()]
1220            .into_iter()
1221            .combine_join(|(k, _)| *k);
1222        assert_eq!(rows.len(), 1);
1223        assert_eq!(
1224            rows[0].iter().map(|(_, v)| *v).collect::<Vec<_>>(),
1225            vec!["a1", "b1", "c1"]
1226        );
1227    }
1228
1229    #[test]
1230    fn combine_join_drops_keys_missing_from_any_iterable() {
1231        let a = [(1, "a1"), (2, "a2")];
1232        let b = [(1, "b1")];
1233        let c = [(2, "c2")];
1234        // key 1 dies at `c`, key 2 died at `b` → empty
1235        let rows = [a.as_slice(), b.as_slice(), c.as_slice()]
1236            .into_iter()
1237            .combine_join(|(k, _)| *k);
1238        assert!(rows.is_empty());
1239    }
1240
1241    #[test]
1242    fn combine_join_multiplies_duplicate_keys() {
1243        let a = [(1, "a1"), (1, "a2")];
1244        let b = [(1, "b1")];
1245        let rows = [a.as_slice(), b.as_slice()]
1246            .into_iter()
1247            .combine_join(|(k, _)| *k);
1248        assert_eq!(rows.len(), 2);
1249        assert_eq!(
1250            HashSet::from_iter(rows.iter().map(|r| r[0].1)),
1251            HashSet::from(["a1", "a2"])
1252        );
1253    }
1254
1255    #[test]
1256    fn combine_join_of_nothing_is_empty() {
1257        let rows = std::iter::empty::<&[(i32, &str)]>().combine_join(|(k, _)| *k);
1258        assert!(rows.is_empty());
1259    }
1260
1261    #[test]
1262    fn chaining_matches_readme_example() {
1263        let rows = [("sa", 10), ("sp", 30), ("sa", 25)];
1264        let by_state = rows.into_iter().group_by_stable(|(s, _)| *s);
1265        assert_eq!(by_state[0].1, vec![("sa", 10), ("sa", 25)]);
1266        let report = rows
1267            .into_iter()
1268            .map(|(_, v)| v)
1269            .join_by(|v| format!("{v:>4}"), " | ");
1270        assert_eq!(report, "  10 |   30 |   25");
1271    }
1272}