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}