Skip to main content

miniextendr_api/dataframe/
group.rs

1//! Group-level iteration over a [`DataFrame`] key column.
2//!
3//! Two rungs, cheapest first:
4//!
5//! 1. **Typed rows, grouped Rust-side** — after `Vec::<Row>::from_dataframe(&df)?`,
6//!    grouping is plain Rust. [`group_rows`] makes the idiom discoverable:
7//!
8//!    ```ignore
9//!    let rows: Vec<Obs> = Vec::from_dataframe(&df)?;
10//!    let by_site = group_rows(rows, |r| r.site.clone());
11//!    // by_site: BTreeMap<String, Vec<Obs>> — plain Rust data, rayon-safe.
12//!    ```
13//!
14//! 2. **Untyped, index-based** — [`DataFrame::group_by`] computes group indices
15//!    once (single pass, main thread) without extracting rows:
16//!
17//!    ```ignore
18//!    let grouped = df.group_by("site")?;
19//!    for (key, rows) in grouped.iter() { /* key: &GroupKey, rows: &[usize] */ }
20//!    let mut out = NamedDataFrameListBuilder::with_capacity(grouped.len());
21//!    for (key, sub) in grouped.frames() {
22//!        // `sub` is a rooted `BuiltDataFrame`; deref to the view for push.
23//!        out = out.push(key.label(), *sub);
24//!    }
25//!    ```
26//!
27//! # Key semantics (vs R `split()`)
28//!
29//! - **Group order**: factor keys follow level order (empty levels kept, like
30//!   `split()`); character keys sort in byte order (R sorts in locale collation
31//!   order — identical for ASCII); integer keys sort numerically; logical keys
32//!   order `FALSE`, `TRUE`.
33//! - **`NA` keys form one group, ordered last** — a deliberate deviation from
34//!   `split()`, which silently drops NA-keyed rows. A literal NA *level*
35//!   (`addNA(f)`) also surfaces as [`GroupKey::Na`].
36//! - **Double key columns are an error**: grouping on floating point is a
37//!   footgun — `cut()` or `factor()` the column first.
38//!
39//! # Composite keys (`group_by_multi`)
40//!
41//! [`DataFrame::group_by_multi`] groups on several columns at once, keying by a
42//! [`GroupKey::Tuple`] of the per-column scalar keys. Non-NA groups match
43//! `split(df, interaction(col1, col2, …, drop = TRUE))` (the first column varies
44//! fastest, R `interaction()`'s default) — exactly, for keys whose byte order
45//! coincides with the session's collation (always true in the C locale;
46//! single-case ASCII in practice — the character-key byte-order choice above
47//! applies per column). Extending the single-column
48//! NA convention, any tuple with an NA in *any* component forms its own trailing
49//! group (first-encounter order) instead of being dropped as `interaction()` +
50//! `split()` would.
51
52use std::collections::{BTreeMap, HashMap};
53
54use super::{DataFrame, DataFrameError, FromDataFrame};
55use crate::{SEXP, SEXPTYPE, SexpExt};
56
57// region: group_rows — typed-rows grouping helper (rung 1)
58
59/// Group already-extracted rows by a key function.
60///
61/// Plain Rust — no SEXP contact, so the result is `Send` (given `T: Send`) and
62/// safe to iterate with rayon. Keys order by `Ord`; give NA-able keys a home by
63/// keying on `Option<T>` (`None` sorts first) or a custom enum.
64pub fn group_rows<T, K, F>(rows: Vec<T>, key: F) -> BTreeMap<K, Vec<T>>
65where
66    K: Ord,
67    F: Fn(&T) -> K,
68{
69    let mut groups: BTreeMap<K, Vec<T>> = BTreeMap::new();
70    for row in rows {
71        groups.entry(key(&row)).or_default().push(row);
72    }
73    groups
74}
75// endregion
76
77// region: GroupKey
78
79/// The key of one group produced by [`DataFrame::group_by`] or
80/// [`DataFrame::group_by_multi`].
81#[derive(Debug, Clone, PartialEq, Eq, Hash)]
82pub enum GroupKey {
83    /// A character or factor-level key.
84    Str(String),
85    /// An integer key.
86    Int(i32),
87    /// A logical key.
88    Bool(bool),
89    /// The NA-keyed group (always ordered last).
90    Na,
91    /// A composite key from [`DataFrame::group_by_multi`] — one scalar element
92    /// per grouping column, in column order. Elements are always scalar
93    /// ([`Str`](Self::Str)/[`Int`](Self::Int)/[`Bool`](Self::Bool)/[`Na`](Self::Na));
94    /// tuples never nest (enforced by a `debug_assert!` in [`label`](Self::label)).
95    Tuple(Vec<GroupKey>),
96}
97
98impl GroupKey {
99    /// R-facing label for this key — suitable as a name in a result list
100    /// (matches how R prints the value: `TRUE`/`FALSE`, `NA`, digits). Composite
101    /// [`Tuple`](Self::Tuple) keys join their element labels with `"."`, matching
102    /// R `interaction()`'s default separator.
103    pub fn label(&self) -> String {
104        match self {
105            GroupKey::Str(s) => s.clone(),
106            GroupKey::Int(i) => i.to_string(),
107            GroupKey::Bool(true) => "TRUE".to_string(),
108            GroupKey::Bool(false) => "FALSE".to_string(),
109            GroupKey::Na => "NA".to_string(),
110            GroupKey::Tuple(keys) => {
111                debug_assert!(
112                    keys.iter().all(|k| !matches!(k, GroupKey::Tuple(_))),
113                    "tuple keys never nest — elements come from scalar columns"
114                );
115                keys.iter()
116                    .map(GroupKey::label)
117                    .collect::<Vec<_>>()
118                    .join(".")
119            }
120        }
121    }
122}
123
124impl std::fmt::Display for GroupKey {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        match self {
127            GroupKey::Str(s) => f.write_str(s),
128            GroupKey::Int(i) => write!(f, "{}", i),
129            GroupKey::Bool(true) => f.write_str("TRUE"),
130            GroupKey::Bool(false) => f.write_str("FALSE"),
131            GroupKey::Na => f.write_str("NA"),
132            GroupKey::Tuple(keys) => {
133                for (i, key) in keys.iter().enumerate() {
134                    if i > 0 {
135                        f.write_str(".")?;
136                    }
137                    write!(f, "{}", key)?;
138                }
139                Ok(())
140            }
141        }
142    }
143}
144// endregion
145
146// region: GroupedDataFrame
147
148/// A [`DataFrame`] partitioned by the values of one key column.
149///
150/// Produced by [`DataFrame::group_by`]. Holds the source frame plus one
151/// `(key, row-indices)` pair per group; nothing is copied until you ask for
152/// [`frames`](Self::frames) or [`extract`](Self::extract).
153///
154/// # GC rooting
155///
156/// The source frame is preserved on R's precious list
157/// (`R_PreserveObject`) for this struct's lifetime and released on drop —
158/// order-independent, unlike the PROTECT stack, so the struct can be held
159/// across arbitrary allocations (e.g. a locally built frame from
160/// [`DataFrame::builder`], which is unprotected once `build()` returns).
161/// Without this, the per-group allocations in [`frames`](Self::frames) /
162/// [`extract`](Self::extract) could collect the source mid-iteration.
163/// Main-thread-only (holds a SEXP; `!Send`).
164pub struct GroupedDataFrame {
165    source: DataFrame,
166    groups: Vec<(GroupKey, Vec<usize>)>,
167}
168
169impl Drop for GroupedDataFrame {
170    fn drop(&mut self) {
171        // SAFETY: main thread (construction invariant — SEXP is !Send);
172        // releases the preserve taken in `group_by`.
173        unsafe { crate::sys::R_ReleaseObject(self.source.as_sexp()) };
174    }
175}
176
177impl GroupedDataFrame {
178    /// Root `source` on R's precious list and pair it with precomputed groups.
179    ///
180    /// `R_PreserveObject` conses onto the precious list — an allocation that can
181    /// itself GC — so PROTECT `source` across the call. Released on `Drop`.
182    fn new(source: DataFrame, groups: Vec<(GroupKey, Vec<usize>)>) -> Self {
183        unsafe {
184            let _guard = crate::OwnedProtect::new(source.sexp);
185            crate::sys::R_PreserveObject(source.sexp);
186        }
187        GroupedDataFrame { source, groups }
188    }
189
190    /// Number of groups (empty factor levels included).
191    pub fn len(&self) -> usize {
192        self.groups.len()
193    }
194
195    /// Whether there are no groups.
196    pub fn is_empty(&self) -> bool {
197        self.groups.is_empty()
198    }
199
200    /// The frame this grouping was computed from.
201    pub fn source(&self) -> &DataFrame {
202        &self.source
203    }
204
205    /// Iterate `(key, row-indices)` pairs in group order. Indices are 0-based
206    /// rows of [`source`](Self::source).
207    pub fn iter(&self) -> impl Iterator<Item = (&GroupKey, &[usize])> {
208        self.groups.iter().map(|(k, idx)| (k, idx.as_slice()))
209    }
210
211    /// Iterate `(key, sub-frame)` pairs, materialising each group as its own
212    /// frame via [`DataFrame::select_rows`].
213    ///
214    /// Main thread only. Each yielded frame is an owned, GC-rooted
215    /// [`BuiltDataFrame`](crate::dataframe::BuiltDataFrame) (#1247) — safe to
216    /// hold across later iterations' allocations. Deref (`*sub`) to pass the
217    /// view where a [`DataFrame`] is expected, e.g. to
218    /// [`NamedDataFrameListBuilder::push`](crate::dataframe::NamedDataFrameListBuilder::push)
219    /// (which protects on push, so the handle may drop right after).
220    pub fn frames(&self) -> impl Iterator<Item = (&GroupKey, crate::dataframe::BuiltDataFrame)> {
221        self.groups
222            .iter()
223            .map(|(k, idx)| (k, self.source.select_rows(idx)))
224    }
225
226    /// Extract typed rows once, then partition them by group.
227    ///
228    /// One `Vec::<T>::from_dataframe` pass over the whole frame, then a
229    /// move-partition by the stored indices — no per-group R subsetting and no
230    /// `Clone` bound. The result is plain Rust data (rayon-safe afterwards).
231    pub fn extract<T>(&self) -> Result<Vec<(GroupKey, Vec<T>)>, DataFrameError>
232    where
233        Vec<T>: FromDataFrame,
234    {
235        let rows = Vec::<T>::from_dataframe(&self.source)?;
236        let mut slots: Vec<Option<T>> = rows.into_iter().map(Some).collect();
237        Ok(self
238            .groups
239            .iter()
240            .map(|(key, idx)| {
241                let group_rows: Vec<T> = idx
242                    .iter()
243                    .map(|&i| slots[i].take().expect("group indices are disjoint"))
244                    .collect();
245                (key.clone(), group_rows)
246            })
247            .collect())
248    }
249}
250// endregion
251
252// region: DataFrame::group_by
253
254impl DataFrame {
255    /// Partition this frame's rows by the values of the named column.
256    ///
257    /// Computes group indices in a single pass on the main thread. Supported
258    /// key columns: factor (fast path — levels are the keys, level order kept,
259    /// empty levels included), character, integer, and logical. Double columns
260    /// error — `cut()` or `factor()` the column in R first.
261    ///
262    /// NA keys form one group, ordered last (unlike R `split()`, which drops
263    /// NA-keyed rows). See the [module docs](self) for the full key semantics.
264    pub fn group_by(&self, col: &str) -> Result<GroupedDataFrame, DataFrameError> {
265        let column = self
266            .column_raw(col)
267            .ok_or_else(|| DataFrameError::NoSuchColumn(col.to_string()))?;
268        let groups = if column.is_factor() {
269            factor_groups(column)
270        } else {
271            match column.type_of() {
272                SEXPTYPE::STRSXP => character_groups(column),
273                SEXPTYPE::INTSXP => integer_groups(column),
274                SEXPTYPE::LGLSXP => logical_groups(column),
275                other => {
276                    return Err(DataFrameError::UnsupportedGroupColumn {
277                        column: col.to_string(),
278                        type_of: format!("{:?}", other),
279                    });
280                }
281            }
282        };
283        // Root the source for the GroupedDataFrame's lifetime (see its GC
284        // rooting docs).
285        Ok(GroupedDataFrame::new(*self, groups))
286    }
287
288    /// Partition this frame's rows by a composite key over several columns —
289    /// the multi-column analogue of [`group_by`](Self::group_by).
290    ///
291    /// Each supported column contributes one scalar key per row (factor level,
292    /// character, integer, or logical — same rules and errors as
293    /// [`group_by`](Self::group_by)); the per-row keys are zipped into a
294    /// [`GroupKey::Tuple`] in column order.
295    ///
296    /// # Order
297    ///
298    /// Non-NA groups match `split(df, interaction(col1, col2, …, drop = TRUE))`:
299    /// the **first** column varies fastest (R `interaction()`'s default,
300    /// `lex.order = FALSE`), each column ordered as [`group_by`](Self::group_by)
301    /// would order it alone (factor level order, byte-sorted characters, numeric
302    /// integers, `FALSE` then `TRUE`). For character keys the match is exact for
303    /// keys whose byte order coincides with the session's collation — always
304    /// true in the C locale; single-case ASCII in practice (e.g. `en_US.UTF-8`
305    /// collates `a A b B` where byte order gives `A B a b`). This is inherited
306    /// from [`group_by`](Self::group_by)'s byte-order choice for character keys
307    /// — see the group-order note in the [module docs](self).
308    ///
309    /// # NA
310    ///
311    /// `interaction()` maps any row with an NA in *any* component to NA and
312    /// `split()` drops it. This method instead keeps such rows: every distinct
313    /// NA-containing tuple forms its own group, all ordered **after** the non-NA
314    /// groups, in first-encounter row order. This extends the single-column
315    /// NA-last convention (see the [module docs](self)).
316    ///
317    /// # Slice
318    ///
319    /// An empty slice is an error. A single-column slice delegates to
320    /// [`group_by`](Self::group_by) and yields **scalar** keys (not 1-tuples), so
321    /// callers never have to special-case one-element tuples.
322    pub fn group_by_multi(&self, cols: &[&str]) -> Result<GroupedDataFrame, DataFrameError> {
323        if cols.is_empty() {
324            return Err(DataFrameError::EmptyGroupColumns);
325        }
326        // A single column is the scalar path — identical keys/order to group_by.
327        if let [col] = cols {
328            return self.group_by(col);
329        }
330
331        // One pass per column: per-row keys (build the tuples) plus the
332        // column's group order (assign each non-NA key an ordinal for sorting).
333        let mut per_row: Vec<Vec<GroupKey>> = Vec::with_capacity(cols.len());
334        let mut ordinals: Vec<HashMap<GroupKey, usize>> = Vec::with_capacity(cols.len());
335        for &col in cols {
336            let column = self
337                .column_raw(col)
338                .ok_or_else(|| DataFrameError::NoSuchColumn(col.to_string()))?;
339            per_row.push(column_keys(column, col)?);
340            ordinals.push(
341                column_level_order(column)
342                    .into_iter()
343                    .enumerate()
344                    .map(|(ord, key)| (key, ord))
345                    .collect(),
346            );
347        }
348
349        // Bucket rows into tuple keys, preserving first-encounter order.
350        let mut index: HashMap<GroupKey, usize> = HashMap::new();
351        let mut buckets: Vec<(GroupKey, Vec<usize>)> = Vec::new();
352        for (row, first) in per_row[0].iter().enumerate() {
353            let mut elems = Vec::with_capacity(cols.len());
354            elems.push(first.clone());
355            for col in &per_row[1..] {
356                elems.push(col[row].clone());
357            }
358            let key = GroupKey::Tuple(elems);
359            match index.get(&key) {
360                Some(&pos) => buckets[pos].1.push(row),
361                None => {
362                    index.insert(key.clone(), buckets.len());
363                    buckets.push((key, vec![row]));
364                }
365            }
366        }
367
368        // Non-NA tuples order by interaction() convention (first column varies
369        // fastest → last column is the most-significant sort key). NA-containing
370        // tuples trail, in first-encounter order (already the bucket order).
371        let is_na_tuple = |k: &GroupKey| matches!(k, GroupKey::Tuple(elems) if elems.iter().any(|e| matches!(e, GroupKey::Na)));
372        let (mut non_na, na_tuples): (Vec<_>, Vec<_>) =
373            buckets.into_iter().partition(|(k, _)| !is_na_tuple(k));
374        non_na.sort_by_key(|(k, _)| {
375            let GroupKey::Tuple(elems) = k else {
376                unreachable!("group_by_multi buckets are always tuples")
377            };
378            let mut ord: Vec<usize> = elems
379                .iter()
380                .enumerate()
381                .map(|(c, e)| ordinals[c][e])
382                .collect();
383            ord.reverse();
384            ord
385        });
386        non_na.extend(na_tuples);
387
388        Ok(GroupedDataFrame::new(*self, non_na))
389    }
390
391    /// Ingest a dplyr `grouped_df`'s existing grouping from its `groups`
392    /// attribute — **honoring the caller's grouping without recomputing it**.
393    ///
394    /// dplyr stores a `grouped_df`'s grouping in `attr(df, "groups")`: a
395    /// `data.frame` whose leading columns are the group-key columns (one row
396    /// per group, in dplyr's group order) and whose trailing `.rows`
397    /// list-column holds, per group, the 1-based row indices into `df`. This
398    /// method reads that metadata verbatim into a [`GroupedDataFrame`] — the
399    /// same type [`group_by`](Self::group_by) / [`group_by_multi`](Self::group_by_multi)
400    /// produce — so a `#[miniextendr]` function handed a dplyr-grouped frame can
401    /// respect the caller's grouping, including multi-column groupings.
402    ///
403    /// Unlike [`group_by`](Self::group_by), this does **no** recomputation: a
404    /// plain (non-grouped) `data.frame` is an error ([`NotGroupedDataFrame`]).
405    /// Callers who want the framework to compute grouping should use
406    /// [`group_by`](Self::group_by) / [`group_by_multi`](Self::group_by_multi).
407    ///
408    /// # Keys
409    ///
410    /// A single key column yields scalar [`GroupKey`]s; multiple key columns
411    /// yield [`GroupKey::Tuple`]s (labels `.`-joined), consistent with
412    /// [`group_by_multi`](Self::group_by_multi). Supported key-column types are
413    /// the same as [`group_by`](Self::group_by) (factor, character, integer,
414    /// logical); a double / list key column is an error.
415    ///
416    /// # Order & empty groups
417    ///
418    /// The `groups`-frame row order is preserved verbatim — dplyr's order is
419    /// authoritative, with no re-sorting and no NA reordering. `.drop = FALSE`
420    /// empty groups (zero-length `.rows`) are **kept** as groups with empty
421    /// index vectors, mirroring the empty-factor-level convention of
422    /// [`group_by`](Self::group_by).
423    ///
424    /// # Errors
425    ///
426    /// [`NotGroupedDataFrame`] (no `groups` attribute / not a `data.frame`),
427    /// [`MissingGroupRows`] (no `.rows` column), [`BadGroupRows`] (a `.rows`
428    /// element is not an integer/integerish vector), or [`GroupIndexOutOfRange`]
429    /// (a `.rows` index is `< 1` or `> nrow`). Every `.rows` index is converted
430    /// from R's 1-based to 0-based.
431    ///
432    /// [`NotGroupedDataFrame`]: DataFrameError::NotGroupedDataFrame
433    /// [`MissingGroupRows`]: DataFrameError::MissingGroupRows
434    /// [`BadGroupRows`]: DataFrameError::BadGroupRows
435    /// [`GroupIndexOutOfRange`]: DataFrameError::GroupIndexOutOfRange
436    ///
437    /// # GC
438    ///
439    /// The `groups` attribute frame (and its columns) is only read here, during
440    /// construction; it stays reachable via `self`'s attribute pairlist — which
441    /// R protects as a `.Call` argument frame for the duration of the call — so
442    /// it needs no separate root. Only the returned [`GroupedDataFrame`] roots
443    /// the **source** frame for its own lifetime (see its GC-rooting docs).
444    pub fn group_by_metadata(&self) -> Result<GroupedDataFrame, DataFrameError> {
445        // Read attr(self, "groups") — dplyr's grouping metadata frame.
446        let groups_sym = unsafe { crate::sys::Rf_install(c"groups".as_ptr()) };
447        let groups_attr = self.as_sexp().get_attr(groups_sym);
448        if groups_attr.is_nil() || !groups_attr.is_data_frame() {
449            return Err(DataFrameError::NotGroupedDataFrame);
450        }
451        let groups_frame = DataFrame::from_sexp(groups_attr)?;
452
453        // Locate the `.rows` list-column; the remaining columns (in order) are
454        // the group-key columns.
455        let rows_col = groups_frame
456            .column_raw(".rows")
457            .ok_or(DataFrameError::MissingGroupRows)?;
458        let key_names: Vec<String> = groups_frame
459            .names()
460            .into_iter()
461            .filter(|n| n != ".rows")
462            .collect();
463
464        // Per-key-column keys: one Vec<GroupKey> of length n_groups per key
465        // column (column_keys yields one scalar key per row of the groups frame).
466        let mut per_col_keys: Vec<Vec<GroupKey>> = Vec::with_capacity(key_names.len());
467        for name in &key_names {
468            let column = groups_frame
469                .column_raw(name)
470                .expect("name came from groups_frame.names()");
471            per_col_keys.push(column_keys(column, name)?);
472        }
473
474        let n_groups = rows_col.len();
475        let nrow = self.nrow();
476        let single_key = key_names.len() == 1;
477
478        let mut groups: Vec<(GroupKey, Vec<usize>)> = Vec::with_capacity(n_groups);
479        for g in 0..n_groups {
480            // Build this group's key: scalar for a single key column, otherwise
481            // a Tuple in column order (`.`-joined labels, like group_by_multi).
482            let key = if single_key {
483                per_col_keys[0][g].clone()
484            } else {
485                GroupKey::Tuple(per_col_keys.iter().map(|col| col[g].clone()).collect())
486            };
487            // Convert `.rows[[g]]` (1-based indices) to a validated 0-based Vec.
488            let elt = rows_col.vector_elt(g as isize);
489            let indices = group_rows_indices(elt, g, nrow)?;
490            groups.push((key, indices));
491        }
492
493        Ok(GroupedDataFrame::new(*self, groups))
494    }
495}
496
497/// Validate and convert one `.rows` list element — an integer / integerish
498/// vector of 1-based row indices — into a 0-based `Vec<usize>`. Rejects
499/// non-integer element types and any index outside `1..=nrow`.
500fn group_rows_indices(elt: SEXP, group: usize, nrow: usize) -> Result<Vec<usize>, DataFrameError> {
501    let nrow_i = nrow as i64;
502    let check = |value: i64| -> Result<usize, DataFrameError> {
503        if value < 1 || value > nrow_i {
504            Err(DataFrameError::GroupIndexOutOfRange { group, value, nrow })
505        } else {
506            Ok((value - 1) as usize)
507        }
508    };
509    match elt.type_of() {
510        SEXPTYPE::INTSXP => {
511            // SAFETY: element is INTSXP; as_slice handles empty vectors.
512            let values: &[i32] = unsafe { elt.as_slice() };
513            values.iter().map(|&v| check(v as i64)).collect()
514        }
515        SEXPTYPE::REALSXP => {
516            // SAFETY: element is REALSXP; as_slice handles empty vectors.
517            let values: &[f64] = unsafe { elt.as_slice() };
518            values
519                .iter()
520                .map(|&v| {
521                    if !v.is_finite() || v.fract() != 0.0 {
522                        return Err(DataFrameError::BadGroupRows {
523                            group,
524                            type_of: "non-integer double".to_string(),
525                        });
526                    }
527                    check(v as i64)
528                })
529                .collect()
530        }
531        other => Err(DataFrameError::BadGroupRows {
532            group,
533            type_of: format!("{:?}", other),
534        }),
535    }
536}
537
538/// Per-row group key for one supported key column, in row order. NA cells — and
539/// factor NA codes / `addNA()` levels — become [`GroupKey::Na`]. Dispatches on
540/// SEXPTYPE exactly as [`DataFrame::group_by`], surfacing the same
541/// unsupported-type error.
542fn column_keys(column: SEXP, col: &str) -> Result<Vec<GroupKey>, DataFrameError> {
543    if column.is_factor() {
544        // SAFETY: factor columns are INTSXP; as_slice handles empty vectors.
545        let codes: &[i32] = unsafe { column.as_slice() };
546        let levels = column.get_levels();
547        return Ok(codes
548            .iter()
549            .map(|&code| {
550                if code == i32::MIN {
551                    GroupKey::Na
552                } else {
553                    match levels.string_elt_str((code - 1) as isize) {
554                        Some(label) => GroupKey::Str(label.to_string()),
555                        None => GroupKey::Na, // addNA() level
556                    }
557                }
558            })
559            .collect());
560    }
561    match column.type_of() {
562        SEXPTYPE::STRSXP => {
563            let n = column.len() as isize;
564            Ok((0..n)
565                .map(|i| match column.string_elt_str(i) {
566                    Some(s) => GroupKey::Str(s.to_string()),
567                    None => GroupKey::Na,
568                })
569                .collect())
570        }
571        SEXPTYPE::INTSXP => {
572            // SAFETY: INTSXP column; as_slice handles empty vectors.
573            let values: &[i32] = unsafe { column.as_slice() };
574            Ok(values
575                .iter()
576                .map(|&v| {
577                    if v == i32::MIN {
578                        GroupKey::Na
579                    } else {
580                        GroupKey::Int(v)
581                    }
582                })
583                .collect())
584        }
585        SEXPTYPE::LGLSXP => {
586            let n = column.len() as isize;
587            Ok((0..n)
588                .map(|i| match column.logical_elt(i) {
589                    0 => GroupKey::Bool(false),
590                    v if v == i32::MIN => GroupKey::Na,
591                    _ => GroupKey::Bool(true),
592                })
593                .collect())
594        }
595        other => Err(DataFrameError::UnsupportedGroupColumn {
596            column: col.to_string(),
597            type_of: format!("{:?}", other),
598        }),
599    }
600}
601
602/// Distinct non-NA keys of one column in single-column group order (factor level
603/// order incl. empty levels; byte-sorted characters; numeric integers; `FALSE`
604/// then `TRUE`). NA is excluded — `interaction()` drops NA rows and NA-containing
605/// tuples are ordered separately. Reuses the single-column bucketers so the
606/// per-column order stays byte-identical to [`DataFrame::group_by`]. Only reached
607/// for supported columns ([`column_keys`] rejects the rest first).
608fn column_level_order(column: SEXP) -> Vec<GroupKey> {
609    let groups = if column.is_factor() {
610        factor_groups(column)
611    } else {
612        match column.type_of() {
613            SEXPTYPE::STRSXP => character_groups(column),
614            SEXPTYPE::INTSXP => integer_groups(column),
615            SEXPTYPE::LGLSXP => logical_groups(column),
616            _ => Vec::new(),
617        }
618    };
619    groups
620        .into_iter()
621        .map(|(k, _)| k)
622        .filter(|k| !matches!(k, GroupKey::Na))
623        .collect()
624}
625
626/// Factor fast path: levels are the keys (level order, empty levels kept).
627/// NA codes — and a literal NA level from `addNA()` — land in [`GroupKey::Na`].
628fn factor_groups(column: SEXP) -> Vec<(GroupKey, Vec<usize>)> {
629    // SAFETY: factor columns are INTSXP; as_slice handles empty vectors.
630    let codes: &[i32] = unsafe { column.as_slice() };
631    let levels = column.get_levels();
632    let n_levels: usize = if levels.is_nil() { 0 } else { levels.len() };
633
634    let mut buckets: Vec<Vec<usize>> = vec![Vec::new(); n_levels];
635    let mut na_bucket: Vec<usize> = Vec::new();
636    for (row, &code) in codes.iter().enumerate() {
637        if code == i32::MIN {
638            na_bucket.push(row);
639        } else {
640            buckets[(code - 1) as usize].push(row);
641        }
642    }
643
644    let mut groups: Vec<(GroupKey, Vec<usize>)> = Vec::with_capacity(n_levels + 1);
645    for (lvl, bucket) in buckets.into_iter().enumerate() {
646        let key = match levels.string_elt_str(lvl as isize) {
647            Some(label) => GroupKey::Str(label.to_string()),
648            None => GroupKey::Na, // addNA() level
649        };
650        groups.push((key, bucket));
651    }
652    if !na_bucket.is_empty() {
653        groups.push((GroupKey::Na, na_bucket));
654    }
655    groups
656}
657
658/// Character keys: byte-order sort (BTreeMap), NA last.
659fn character_groups(column: SEXP) -> Vec<(GroupKey, Vec<usize>)> {
660    let n = column.len() as isize;
661    let mut map: BTreeMap<String, Vec<usize>> = BTreeMap::new();
662    let mut na_bucket: Vec<usize> = Vec::new();
663    for i in 0..n {
664        match column.string_elt_str(i) {
665            Some(s) => map.entry(s.to_string()).or_default().push(i as usize),
666            None => na_bucket.push(i as usize),
667        }
668    }
669    let mut groups: Vec<(GroupKey, Vec<usize>)> = map
670        .into_iter()
671        .map(|(k, idx)| (GroupKey::Str(k), idx))
672        .collect();
673    if !na_bucket.is_empty() {
674        groups.push((GroupKey::Na, na_bucket));
675    }
676    groups
677}
678
679/// Integer keys: numeric sort (BTreeMap), NA (`i32::MIN`) last.
680fn integer_groups(column: SEXP) -> Vec<(GroupKey, Vec<usize>)> {
681    // SAFETY: INTSXP column; as_slice handles empty vectors.
682    let values: &[i32] = unsafe { column.as_slice() };
683    let mut map: BTreeMap<i32, Vec<usize>> = BTreeMap::new();
684    let mut na_bucket: Vec<usize> = Vec::new();
685    for (row, &v) in values.iter().enumerate() {
686        if v == i32::MIN {
687            na_bucket.push(row);
688        } else {
689            map.entry(v).or_default().push(row);
690        }
691    }
692    let mut groups: Vec<(GroupKey, Vec<usize>)> = map
693        .into_iter()
694        .map(|(k, idx)| (GroupKey::Int(k), idx))
695        .collect();
696    if !na_bucket.is_empty() {
697        groups.push((GroupKey::Na, na_bucket));
698    }
699    groups
700}
701
702/// Logical keys: `FALSE` then `TRUE` (R's sort order), NA last.
703/// Only keys present in the data appear.
704fn logical_groups(column: SEXP) -> Vec<(GroupKey, Vec<usize>)> {
705    let n = column.len() as isize;
706    let mut false_bucket: Vec<usize> = Vec::new();
707    let mut true_bucket: Vec<usize> = Vec::new();
708    let mut na_bucket: Vec<usize> = Vec::new();
709    for i in 0..n {
710        match column.logical_elt(i) {
711            0 => false_bucket.push(i as usize),
712            v if v == i32::MIN => na_bucket.push(i as usize),
713            _ => true_bucket.push(i as usize),
714        }
715    }
716    let mut groups: Vec<(GroupKey, Vec<usize>)> = Vec::with_capacity(3);
717    if !false_bucket.is_empty() {
718        groups.push((GroupKey::Bool(false), false_bucket));
719    }
720    if !true_bucket.is_empty() {
721        groups.push((GroupKey::Bool(true), true_bucket));
722    }
723    if !na_bucket.is_empty() {
724        groups.push((GroupKey::Na, na_bucket));
725    }
726    groups
727}
728// endregion
729
730#[cfg(test)]
731mod tests {
732    use super::*;
733
734    #[test]
735    fn group_rows_partitions_and_orders_by_key() {
736        let rows = vec![("b", 1), ("a", 2), ("b", 3), ("c", 4)];
737        let grouped = group_rows(rows, |r| r.0);
738        let keys: Vec<&str> = grouped.keys().copied().collect();
739        assert_eq!(keys, vec!["a", "b", "c"]);
740        assert_eq!(grouped["b"], vec![("b", 1), ("b", 3)]);
741    }
742
743    #[test]
744    fn group_rows_option_key_gives_na_a_home() {
745        let rows = vec![(Some(2), "x"), (None, "y"), (Some(1), "z")];
746        let grouped = group_rows(rows, |r| r.0);
747        let keys: Vec<Option<i32>> = grouped.keys().copied().collect();
748        assert_eq!(keys, vec![None, Some(1), Some(2)]);
749    }
750
751    #[test]
752    fn group_key_labels_match_r_printing() {
753        assert_eq!(GroupKey::Str("a".into()).label(), "a");
754        assert_eq!(GroupKey::Int(-3).label(), "-3");
755        assert_eq!(GroupKey::Bool(true).label(), "TRUE");
756        assert_eq!(GroupKey::Bool(false).label(), "FALSE");
757        assert_eq!(GroupKey::Na.label(), "NA");
758        assert_eq!(GroupKey::Na.to_string(), "NA");
759    }
760
761    #[test]
762    fn tuple_key_labels_join_with_dot() {
763        let key = GroupKey::Tuple(vec![
764            GroupKey::Str("a".into()),
765            GroupKey::Int(2),
766            GroupKey::Bool(true),
767            GroupKey::Na,
768        ]);
769        assert_eq!(key.label(), "a.2.TRUE.NA");
770        assert_eq!(key.to_string(), "a.2.TRUE.NA");
771    }
772}