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 = NamedListBuilder::with_capacity(grouped.len());
21//!    for (key, sub) in grouped.frames() {
22//!        // Transfer each rooted frame into the owned list.
23//!        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`; double keys (`Date` and `POSIXct` included) sort
33//!   numerically, with `NaN` after every number.
34//! - **`NA` keys form one group, ordered last** — a deliberate deviation from
35//!   `split()`, which silently drops NA-keyed rows. A literal NA *level*
36//!   (`addNA(f)`) also surfaces as [`GroupKey::Na`].
37//! - **Double keys group by value with dplyr's (vctrs') equality**: `-0` and
38//!   `0` are one key, every `NaN` is one key, and `NaN` and `NA` are separate
39//!   groups. Labels are R's `as.character()` of the key — see [`RealKey`]. A
40//!   bit64 `integer64` column (int64 bits stored in a double vector) is an
41//!   error.
42//!
43//! # Composite keys (`group_by_multi`)
44//!
45//! [`DataFrame::group_by_multi`] groups on several columns at once, keying by a
46//! [`GroupKey::Tuple`] of the per-column scalar keys. Non-NA groups match
47//! `split(df, interaction(col1, col2, …, drop = TRUE))` (the first column varies
48//! fastest, R `interaction()`'s default) — exactly, for keys whose byte order
49//! coincides with the session's collation (always true in the C locale;
50//! single-case ASCII in practice — the character-key byte-order choice above
51//! applies per column). Extending the single-column
52//! NA convention, any tuple with an NA in *any* component forms its own trailing
53//! group (first-encounter order) instead of being dropped as `interaction()` +
54//! `split()` would.
55
56use std::collections::{BTreeMap, HashMap};
57
58use super::{DataFrame, DataFrameError, FromDataFrame};
59use crate::into_r::IntoR;
60use crate::{IsNA, SEXP, SEXPTYPE, SexpExt};
61
62// region: group_rows — typed-rows grouping helper (rung 1)
63
64/// Group already-extracted rows by a key function.
65///
66/// Plain Rust — no SEXP contact, so the result is `Send` (given `T: Send`) and
67/// safe to iterate with rayon. Keys order by `Ord`; give NA-able keys a home by
68/// keying on `Option<T>` (`None` sorts first) or a custom enum.
69pub fn group_rows<T, K, F>(rows: Vec<T>, key: F) -> BTreeMap<K, Vec<T>>
70where
71    K: Ord,
72    F: Fn(&T) -> K,
73{
74    let mut groups: BTreeMap<K, Vec<T>> = BTreeMap::new();
75    for row in rows {
76        groups.entry(key(&row)).or_default().push(row);
77    }
78    groups
79}
80// endregion
81
82// region: GroupKey
83
84/// The key of one group produced by [`DataFrame::group_by`] or
85/// [`DataFrame::group_by_multi`].
86#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GroupKey {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            Self::Str(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Str",
                    &__self_0),
            Self::Int(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Int",
                    &__self_0),
            Self::Real(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Real",
                    &__self_0),
            Self::Bool(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Bool",
                    &__self_0),
            Self::Na => ::core::fmt::Formatter::write_str(f, "Na"),
            Self::Tuple(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Tuple",
                    &__self_0),
        }
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GroupKey {
    #[inline]
    fn clone(&self) -> Self {
        match self {
            Self::Str(__self_0) =>
                GroupKey::Str(::core::clone::Clone::clone(__self_0)),
            Self::Int(__self_0) =>
                GroupKey::Int(::core::clone::Clone::clone(__self_0)),
            Self::Real(__self_0) =>
                GroupKey::Real(::core::clone::Clone::clone(__self_0)),
            Self::Bool(__self_0) =>
                GroupKey::Bool(::core::clone::Clone::clone(__self_0)),
            Self::Na => GroupKey::Na,
            Self::Tuple(__self_0) =>
                GroupKey::Tuple(::core::clone::Clone::clone(__self_0)),
        }
    }
}Clone, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for GroupKey { }
#[automatically_derived]
impl ::core::cmp::PartialEq for GroupKey {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        ::core::intrinsics::discriminant_value(self) ==
                ::core::intrinsics::discriminant_value(other) &&
            match (self, other) {
                (Self::Str(__self_0), Self::Str(__arg1_0)) =>
                    __self_0 == __arg1_0,
                (Self::Int(__self_0), Self::Int(__arg1_0)) =>
                    __self_0 == __arg1_0,
                (Self::Real(__self_0), Self::Real(__arg1_0)) =>
                    __self_0 == __arg1_0,
                (Self::Bool(__self_0), Self::Bool(__arg1_0)) =>
                    __self_0 == __arg1_0,
                (Self::Tuple(__self_0), Self::Tuple(__arg1_0)) =>
                    __self_0 == __arg1_0,
                _ => true,
            }
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for GroupKey {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<String>;
        let _: ::core::cmp::AssertParamIsEq<i32>;
        let _: ::core::cmp::AssertParamIsEq<RealKey>;
        let _: ::core::cmp::AssertParamIsEq<bool>;
        let _: ::core::cmp::AssertParamIsEq<Vec<GroupKey>>;
    }
}Eq, #[automatically_derived]
impl ::core::hash::Hash for GroupKey {
    #[inline]
    fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
        ::core::hash::Hash::hash(&::core::intrinsics::discriminant_value(self),
            state);
        match self {
            Self::Str(__self_0) => ::core::hash::Hash::hash(__self_0, state),
            Self::Int(__self_0) => ::core::hash::Hash::hash(__self_0, state),
            Self::Real(__self_0) => ::core::hash::Hash::hash(__self_0, state),
            Self::Bool(__self_0) => ::core::hash::Hash::hash(__self_0, state),
            Self::Tuple(__self_0) =>
                ::core::hash::Hash::hash(__self_0, state),
            _ => {}
        }
    }
}Hash)]
87pub enum GroupKey {
88    /// A character or factor-level key.
89    Str(String),
90    /// An integer key.
91    Int(i32),
92    /// A double key: a plain number, a `Date`, a `POSIXct`, or another classed
93    /// double. See [`RealKey`] for its equality and label.
94    Real(RealKey),
95    /// A logical key.
96    Bool(bool),
97    /// The NA-keyed group (always ordered last).
98    Na,
99    /// A composite key from [`DataFrame::group_by_multi`] — one scalar element
100    /// per grouping column, in column order. Elements are always scalar
101    /// ([`Str`](Self::Str)/[`Int`](Self::Int)/[`Real`](Self::Real)/[`Bool`](Self::Bool)/[`Na`](Self::Na));
102    /// tuples never nest (enforced by a `debug_assert!` in [`label`](Self::label)).
103    Tuple(Vec<GroupKey>),
104}
105
106impl GroupKey {
107    /// R-facing label for this key — suitable as a name in a result list
108    /// (matches how R prints the value: `TRUE`/`FALSE`, `NA`, digits, and R's
109    /// `as.character()` text for a [`Real`](Self::Real) key). Composite
110    /// [`Tuple`](Self::Tuple) keys join their element labels with `"."`, matching
111    /// R `interaction()`'s default separator.
112    pub fn label(&self) -> String {
113        match self {
114            GroupKey::Str(s) => s.clone(),
115            GroupKey::Int(i) => i.to_string(),
116            GroupKey::Real(r) => r.label.clone(),
117            GroupKey::Bool(true) => "TRUE".to_string(),
118            GroupKey::Bool(false) => "FALSE".to_string(),
119            GroupKey::Na => "NA".to_string(),
120            GroupKey::Tuple(keys) => {
121                if true {
    if !keys.iter().all(|k|
                    !#[allow(non_exhaustive_omitted_patterns)] match k {
                            GroupKey::Tuple(_) => true,
                            _ => false,
                        }) {
        {
            ::core::panicking::panic_fmt(format_args!("tuple keys never nest — elements come from scalar columns"));
        }
    };
};debug_assert!(
122                    keys.iter().all(|k| !matches!(k, GroupKey::Tuple(_))),
123                    "tuple keys never nest — elements come from scalar columns"
124                );
125                keys.iter()
126                    .map(GroupKey::label)
127                    .collect::<Vec<_>>()
128                    .join(".")
129            }
130        }
131    }
132}
133
134impl std::fmt::Display for GroupKey {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        match self {
137            GroupKey::Str(s) => f.write_str(s),
138            GroupKey::Int(i) => f.write_fmt(format_args!("{0}", i))write!(f, "{}", i),
139            GroupKey::Real(r) => f.write_str(&r.label),
140            GroupKey::Bool(true) => f.write_str("TRUE"),
141            GroupKey::Bool(false) => f.write_str("FALSE"),
142            GroupKey::Na => f.write_str("NA"),
143            GroupKey::Tuple(keys) => {
144                for (i, key) in keys.iter().enumerate() {
145                    if i > 0 {
146                        f.write_str(".")?;
147                    }
148                    f.write_fmt(format_args!("{0}", key))write!(f, "{}", key)?;
149                }
150                Ok(())
151            }
152        }
153    }
154}
155
156/// The key of one double-valued group ([`GroupKey::Real`]): the value plus
157/// the text R prints for it.
158///
159/// # Equality
160///
161/// Keys compare and hash by value alone, with the rules vctrs uses and
162/// therefore dplyr's `group_by()`: `-0.0` equals `0.0`, and every `NaN` is the
163/// same key. `NA_real_` is never a `RealKey`: an `NA` cell keys as
164/// [`GroupKey::Na`], so `NA` and `NaN` rows form separate groups, as in dplyr.
165/// The label takes no part in equality.
166///
167/// # Label
168///
169/// The grouping verbs label a key with R's `as.character()` of its value,
170/// called with the key column's attributes (class, `tzone`, …): the same text
171/// `factor()` and `split()` use for level names. So a plain double prints
172/// with up to 15 significant digits (`"0.1"`, `"1e+05"`, `"NaN"`), a `Date`
173/// prints as `"2024-01-02"`, and a `POSIXct` prints in the column's time zone
174/// (`"2024-01-02 10:30:00"`, or just the date at midnight).
175#[derive(#[automatically_derived]
impl ::core::fmt::Debug for RealKey {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f, "RealKey",
            "value", &self.value, "label", &&self.label)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for RealKey {
    #[inline]
    fn clone(&self) -> Self {
        RealKey {
            value: ::core::clone::Clone::clone(&self.value),
            label: ::core::clone::Clone::clone(&self.label),
        }
    }
}Clone)]
176pub struct RealKey {
177    value: f64,
178    label: String,
179}
180
181impl RealKey {
182    /// A key for `value`, printed as `label`. `-0.0` is stored as `0.0` and
183    /// every `NaN` as one canonical `NaN`, so equal keys hold identical bits.
184    pub fn new(value: f64, label: impl Into<String>) -> Self {
185        RealKey {
186            value: normalize_real(value),
187            label: label.into(),
188        }
189    }
190
191    /// The key's value: never `-0.0`, and `NaN` is the canonical positive
192    /// quiet `NaN`. For a `Date` key this is days since 1970-01-01; for a
193    /// `POSIXct` key, seconds since the epoch.
194    pub fn value(&self) -> f64 {
195        self.value
196    }
197
198    /// The text R prints for the key (see the [type docs](Self)).
199    pub fn label(&self) -> &str {
200        &self.label
201    }
202}
203
204impl PartialEq for RealKey {
205    fn eq(&self, other: &Self) -> bool {
206        self.value.to_bits() == other.value.to_bits()
207    }
208}
209
210impl Eq for RealKey {}
211
212impl std::hash::Hash for RealKey {
213    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
214        self.value.to_bits().hash(state);
215    }
216}
217
218/// Bits of the one `NaN` every `NaN` key is stored as: positive and quiet, so
219/// [`f64::total_cmp`] orders it after `+Inf` on every platform (x86's `0/0` has
220/// the sign bit set), and its low word is not R's NA payload (1954).
221const CANONICAL_NAN_BITS: u64 = 0x7FF8_0000_0000_0000;
222
223/// Map a non-NA double to its key representative: `-0.0` → `0.0`, any `NaN`
224/// → the canonical `NaN`. Equal keys (by vctrs' rules) then share their bits.
225fn normalize_real(x: f64) -> f64 {
226    if x == 0.0 {
227        0.0
228    } else if x.is_nan() {
229        f64::from_bits(CANONICAL_NAN_BITS)
230    } else {
231        x
232    }
233}
234// endregion
235
236// region: GroupDeclaration
237
238/// How a dplyr-grouped frame declares its grouping, returned by
239/// [`DataFrame::group_declaration`]: the grouping variables and the `.drop`
240/// policy, without the cached group rows.
241#[derive(#[automatically_derived]
impl ::core::fmt::Debug for GroupDeclaration {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f,
            "GroupDeclaration", "vars", &self.vars, "drop", &&self.drop)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for GroupDeclaration {
    #[inline]
    fn clone(&self) -> Self {
        GroupDeclaration {
            vars: ::core::clone::Clone::clone(&self.vars),
            drop: ::core::clone::Clone::clone(&self.drop),
        }
    }
}Clone, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for GroupDeclaration { }
#[automatically_derived]
impl ::core::cmp::PartialEq for GroupDeclaration {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        self.drop == other.drop && self.vars == other.vars
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for GroupDeclaration {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<Vec<String>>;
        let _: ::core::cmp::AssertParamIsEq<bool>;
    }
}Eq)]
242pub struct GroupDeclaration {
243    /// The grouping variable names, in declaration order — dplyr's
244    /// `group_vars()`, `setdiff(names(groups), ".rows")`: the `groups`
245    /// frame's column names other than `.rows`, each once. Empty for a
246    /// `rowwise_df` without id columns.
247    pub vars: Vec<String>,
248    /// Whether grouping drops empty groups — dplyr's
249    /// `group_by_drop_default()`. For a `grouped_df`, `true` unless the
250    /// `groups` frame's `.drop` attribute is exactly `FALSE`
251    /// (`identical(attr(groups, ".drop"), FALSE)`: a length-1 logical `FALSE`
252    /// with no attributes). Always `true` for a frame that is a `rowwise_df`
253    /// but not a `grouped_df`, whose `group_by_drop_default()` is the default
254    /// method's `TRUE`.
255    pub drop: bool,
256}
257
258/// A grouped frame's `groups` attribute after the structural checks of
259/// [`DataFrame::groups_attr`].
260struct GroupsAttr {
261    /// The `groups` data frame: a list inheriting from `data.frame`, with at
262    /// least one column.
263    frame: SEXP,
264    /// Its last column, `.rows`: a list of integer vectors, one per group.
265    rows: SEXP,
266}
267
268impl GroupsAttr {
269    /// The grouping-variable columns, by position, with their names: every
270    /// column not named `.rows`, keeping only the first column of a repeated
271    /// name. Their names are dplyr's `group_vars()`,
272    /// `setdiff(names(groups), ".rows")`. An `NA` name reads as `""`, as in
273    /// [`DataFrame::names`].
274    fn key_columns(&self) -> Vec<(String, SEXP)> {
275        let names = self.frame.get_names();
276        let mut columns: Vec<(String, SEXP)> = Vec::new();
277        for i in 0..self.frame.len() as isize {
278            let name = names.string_elt_str(i).unwrap_or("");
279            if name == ".rows" || columns.iter().any(|(seen, _)| seen == name) {
280                continue;
281            }
282            columns.push((name.to_string(), self.frame.vector_elt(i)));
283        }
284        columns
285    }
286}
287// endregion
288
289// region: GroupedDataFrame
290
291/// A [`DataFrame`] partitioned by the values of one key column.
292///
293/// Produced by [`DataFrame::group_by`]. Holds the source frame plus one
294/// `(key, row-indices)` pair per group; nothing is copied until you ask for
295/// [`frames`](Self::frames) or [`extract`](Self::extract).
296///
297/// # GC rooting
298///
299/// The source frame is preserved on R's precious list
300/// (`R_PreserveObject`) for this struct's lifetime and released on drop —
301/// order-independent, unlike the PROTECT stack, so the struct can be held
302/// across arbitrary allocations (e.g. a locally built frame from
303/// [`DataFrame::builder`], which is unprotected once `build()` returns).
304/// Without this, the per-group allocations in [`frames`](Self::frames) /
305/// [`extract`](Self::extract) could collect the source mid-iteration.
306/// Main-thread-only (holds a SEXP; `!Send`).
307pub struct GroupedDataFrame {
308    source: DataFrame,
309    groups: Vec<(GroupKey, Vec<usize>)>,
310}
311
312impl Drop for GroupedDataFrame {
313    fn drop(&mut self) {
314        // SAFETY: main thread (construction invariant — SEXP is !Send);
315        // releases the preserve taken in `group_by`.
316        unsafe { crate::sys::R_ReleaseObject(self.source.as_sexp()) };
317    }
318}
319
320impl GroupedDataFrame {
321    /// Root `source` on R's precious list and pair it with precomputed groups.
322    ///
323    /// `R_PreserveObject` conses onto the precious list — an allocation that can
324    /// itself GC — so PROTECT `source` across the call. Released on `Drop`.
325    fn new(source: DataFrame, groups: Vec<(GroupKey, Vec<usize>)>) -> Self {
326        unsafe {
327            let _guard = crate::OwnedProtect::new(source.sexp);
328            crate::sys::R_PreserveObject(source.sexp);
329        }
330        GroupedDataFrame { source, groups }
331    }
332
333    /// Number of groups (empty factor levels included).
334    pub fn len(&self) -> usize {
335        self.groups.len()
336    }
337
338    /// Whether there are no groups.
339    pub fn is_empty(&self) -> bool {
340        self.groups.is_empty()
341    }
342
343    /// The frame this grouping was computed from.
344    pub fn source(&self) -> &DataFrame {
345        &self.source
346    }
347
348    /// Iterate `(key, row-indices)` pairs in group order. Indices are 0-based
349    /// rows of [`source`](Self::source).
350    pub fn iter(&self) -> impl Iterator<Item = (&GroupKey, &[usize])> {
351        self.groups.iter().map(|(k, idx)| (k, idx.as_slice()))
352    }
353
354    /// Iterate `(key, sub-frame)` pairs, materialising each group as its own
355    /// frame via [`DataFrame::select_rows`].
356    ///
357    /// Main thread only. Each yielded frame is an owned, GC-rooted
358    /// [`BuiltDataFrame`](crate::dataframe::BuiltDataFrame) (#1247) — safe to
359    /// hold across later iterations' allocations. Pass each handle directly to
360    /// [`NamedListBuilder::push`](crate::NamedListBuilder::push)
361    /// to collect the frames into an owned named list.
362    pub fn frames(&self) -> impl Iterator<Item = (&GroupKey, crate::dataframe::BuiltDataFrame)> {
363        self.groups
364            .iter()
365            .map(|(k, idx)| (k, self.source.select_rows(idx)))
366    }
367
368    /// Extract typed rows once, then partition them by group.
369    ///
370    /// One `Vec::<T>::from_dataframe` pass over the whole frame, then a
371    /// move-partition by the stored indices — no per-group R subsetting and no
372    /// `Clone` bound. The result is plain Rust data (rayon-safe afterwards).
373    pub fn extract<T>(&self) -> Result<Vec<(GroupKey, Vec<T>)>, DataFrameError>
374    where
375        Vec<T>: FromDataFrame,
376    {
377        let rows = Vec::<T>::from_dataframe(&self.source)?;
378        let mut slots: Vec<Option<T>> = rows.into_iter().map(Some).collect();
379        Ok(self
380            .groups
381            .iter()
382            .map(|(key, idx)| {
383                let group_rows: Vec<T> = idx
384                    .iter()
385                    .map(|&i| slots[i].take().expect("group indices are disjoint"))
386                    .collect();
387                (key.clone(), group_rows)
388            })
389            .collect())
390    }
391}
392// endregion
393
394// region: DataFrame::group_by
395
396impl DataFrame {
397    /// Partition this frame's rows by the values of the named column.
398    ///
399    /// Computes group indices in a single pass on the main thread. Supported
400    /// key columns: factor (fast path — levels are the keys, level order kept,
401    /// empty levels included), character, integer, logical, and double
402    /// (`Date` and `POSIXct` included; keyed as [`GroupKey::Real`], labelled by
403    /// R's `as.character()`). List columns and bit64 `integer64` columns are an
404    /// error.
405    ///
406    /// NA keys form one group, ordered last (unlike R `split()`, which drops
407    /// NA-keyed rows). See the [module docs](self) for the full key semantics.
408    pub fn group_by(&self, col: &str) -> Result<GroupedDataFrame, DataFrameError> {
409        let column = self
410            .column_raw(col)
411            .ok_or_else(|| DataFrameError::NoSuchColumn(col.to_string()))?;
412        let groups = column_groups(column, col)?;
413        // Root the source for the GroupedDataFrame's lifetime (see its GC
414        // rooting docs).
415        Ok(GroupedDataFrame::new(*self, groups))
416    }
417
418    /// Partition this frame's rows by a composite key over several columns —
419    /// the multi-column analogue of [`group_by`](Self::group_by).
420    ///
421    /// Each supported column contributes one scalar key per row (factor level,
422    /// character, integer, logical, or double — same rules and errors as
423    /// [`group_by`](Self::group_by)); the per-row keys are zipped into a
424    /// [`GroupKey::Tuple`] in column order.
425    ///
426    /// # Order
427    ///
428    /// Non-NA groups match `split(df, interaction(col1, col2, …, drop = TRUE))`:
429    /// the **first** column varies fastest (R `interaction()`'s default,
430    /// `lex.order = FALSE`), each column ordered as [`group_by`](Self::group_by)
431    /// would order it alone (factor level order, byte-sorted characters, numeric
432    /// integers and doubles — `NaN` after every number — and `FALSE` then
433    /// `TRUE`). For character keys the match is exact for
434    /// keys whose byte order coincides with the session's collation — always
435    /// true in the C locale; single-case ASCII in practice (e.g. `en_US.UTF-8`
436    /// collates `a A b B` where byte order gives `A B a b`). This is inherited
437    /// from [`group_by`](Self::group_by)'s byte-order choice for character keys
438    /// — see the group-order note in the [module docs](self).
439    ///
440    /// # NA
441    ///
442    /// `interaction()` maps any row with an NA in *any* component to NA and
443    /// `split()` drops it. This method instead keeps such rows: every distinct
444    /// NA-containing tuple forms its own group, all ordered **after** the non-NA
445    /// groups, in first-encounter row order. This extends the single-column
446    /// NA-last convention (see the [module docs](self)).
447    ///
448    /// # Slice
449    ///
450    /// An empty slice is an error. A single-column slice delegates to
451    /// [`group_by`](Self::group_by) and yields **scalar** keys (not 1-tuples), so
452    /// callers never have to special-case one-element tuples.
453    pub fn group_by_multi(&self, cols: &[&str]) -> Result<GroupedDataFrame, DataFrameError> {
454        if cols.is_empty() {
455            return Err(DataFrameError::EmptyGroupColumns);
456        }
457        // A single column is the scalar path — identical keys/order to group_by.
458        if let [col] = cols {
459            return self.group_by(col);
460        }
461
462        // One grouping pass per column gives both the per-row keys (to build
463        // the tuples) and the column's group order (an ordinal per non-NA key,
464        // for sorting). NA is excluded from the ordinals: NA-containing tuples
465        // are ordered separately below.
466        let mut per_row: Vec<Vec<GroupKey>> = Vec::with_capacity(cols.len());
467        let mut ordinals: Vec<HashMap<GroupKey, usize>> = Vec::with_capacity(cols.len());
468        for &col in cols {
469            let column = self
470                .column_raw(col)
471                .ok_or_else(|| DataFrameError::NoSuchColumn(col.to_string()))?;
472            let groups = column_groups(column, col)?;
473            per_row.push(row_keys(&groups, column.len()));
474            ordinals.push(
475                groups
476                    .into_iter()
477                    .map(|(key, _)| key)
478                    .filter(|key| !#[allow(non_exhaustive_omitted_patterns)] match key {
    GroupKey::Na => true,
    _ => false,
}matches!(key, GroupKey::Na))
479                    .enumerate()
480                    .map(|(ord, key)| (key, ord))
481                    .collect(),
482            );
483        }
484
485        // Bucket rows into tuple keys, preserving first-encounter order.
486        let mut index: HashMap<GroupKey, usize> = HashMap::new();
487        let mut buckets: Vec<(GroupKey, Vec<usize>)> = Vec::new();
488        for (row, first) in per_row[0].iter().enumerate() {
489            let mut elems = Vec::with_capacity(cols.len());
490            elems.push(first.clone());
491            for col in &per_row[1..] {
492                elems.push(col[row].clone());
493            }
494            let key = GroupKey::Tuple(elems);
495            match index.get(&key) {
496                Some(&pos) => buckets[pos].1.push(row),
497                None => {
498                    index.insert(key.clone(), buckets.len());
499                    buckets.push((key, ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [row]))vec![row]));
500                }
501            }
502        }
503
504        // Non-NA tuples order by interaction() convention (first column varies
505        // fastest → last column is the most-significant sort key). NA-containing
506        // tuples trail, in first-encounter order (already the bucket order).
507        let is_na_tuple = |k: &GroupKey| #[allow(non_exhaustive_omitted_patterns)] match k {
    GroupKey::Tuple(elems) if
        elems.iter().any(|e|
                #[allow(non_exhaustive_omitted_patterns)] match e {
                    GroupKey::Na => true,
                    _ => false,
                }) => true,
    _ => false,
}matches!(k, GroupKey::Tuple(elems) if elems.iter().any(|e| matches!(e, GroupKey::Na)));
508        let (mut non_na, na_tuples): (Vec<_>, Vec<_>) =
509            buckets.into_iter().partition(|(k, _)| !is_na_tuple(k));
510        non_na.sort_by_key(|(k, _)| {
511            let GroupKey::Tuple(elems) = k else {
512                {
    ::core::panicking::panic_fmt(format_args!("internal error: entered unreachable code: {0}",
            format_args!("group_by_multi buckets are always tuples")));
}unreachable!("group_by_multi buckets are always tuples")
513            };
514            let mut ord: Vec<usize> = elems
515                .iter()
516                .enumerate()
517                .map(|(c, e)| ordinals[c][e])
518                .collect();
519            ord.reverse();
520            ord
521        });
522        non_na.extend(na_tuples);
523
524        Ok(GroupedDataFrame::new(*self, non_na))
525    }
526
527    /// Read a dplyr-grouped frame's grouping **declaration** — its grouping
528    /// variables and `.drop` policy — without its cached group rows.
529    ///
530    /// Returns what dplyr's `group_vars()` and `group_by_drop_default()`
531    /// report: `Ok(None)` when the frame is not grouped, and an error when it
532    /// is grouped but its `groups` attribute is corrupt, where `group_vars()`
533    /// errors too (see below). Nothing is partitioned and no row index is
534    /// read, so the key-column types and whether the cached `.rows` still fit
535    /// the frame never matter. A function that groups the frame's **current**
536    /// rows itself (e.g. with [`group_by_multi`](Self::group_by_multi) over
537    /// [`GroupDeclaration::vars`]) reads this instead of
538    /// [`group_by_metadata`](Self::group_by_metadata), which trusts the cached
539    /// rows.
540    ///
541    /// # Grouped frames
542    ///
543    /// A frame is grouped when it inherits from `grouped_df` or `rowwise_df`,
544    /// whatever its attributes: `dplyr::is_grouped_df()` is
545    /// `inherits(x, "grouped_df")`. A plain `data.frame` with a stray `groups`
546    /// attribute is not grouped (`Ok(None)`).
547    ///
548    /// # Errors
549    ///
550    /// A grouped frame's `groups` attribute must pass the structural checks
551    /// of dplyr's `validate_grouped_df()`, which `group_vars()` runs on a
552    /// `grouped_df`: it is a data frame ([`GroupsNotDataFrame`]), its last
553    /// column is called `.rows` ([`MissingGroupRows`]), and that column is a
554    /// list of integer vectors ([`BadGroupRows`]; a list of doubles is
555    /// rejected, as in dplyr). The row indices themselves are not checked,
556    /// as `group_vars()` does not check them either, so stale `.rows` after a
557    /// subset still yield the declaration.
558    ///
559    /// A `rowwise_df` gets the same checks. dplyr is looser here:
560    /// `group_vars()` reads a `rowwise_df`'s `groups` attribute without
561    /// validating it. `validate_rowwise_df()` applies the same checks, plus
562    /// row-shape checks that this method skips (one `.rows` element per row,
563    /// each holding its own row number).
564    ///
565    /// [`GroupsNotDataFrame`]: DataFrameError::GroupsNotDataFrame
566    /// [`MissingGroupRows`]: DataFrameError::MissingGroupRows
567    /// [`BadGroupRows`]: DataFrameError::BadGroupRows
568    ///
569    /// # Example
570    ///
571    /// ```ignore
572    /// // df arrived as dplyr::group_by(data, site, .drop = FALSE)
573    /// let decl = df.group_declaration()?.expect("a grouped_df");
574    /// assert_eq!(decl.vars, ["site"]);
575    /// assert!(!decl.drop);
576    /// ```
577    pub fn group_declaration(&self) -> Result<Option<GroupDeclaration>, DataFrameError> {
578        let Some(groups) = self.groups_attr()? else {
579            return Ok(None);
580        };
581        // group_by_drop_default(): only its grouped_df method reads `.drop`,
582        // as `!identical(attr(groups, ".drop"), FALSE)` with identical()'s
583        // default flags. A frame that is only a rowwise_df gets the default
584        // method's TRUE. `Rf_ScalarLogical(0)` returns R's shared FALSE
585        // without allocating.
586        let drop = !self.sexp.inherits_class(c"grouped_df") || {
587            let drop_attr = groups
588                .frame
589                .get_attr(unsafe { crate::sys::Rf_install(c".drop".as_ptr()) });
590            unsafe {
591                crate::sys::R_compute_identical(
592                    drop_attr,
593                    crate::sys::Rf_ScalarLogical(0),
594                    crate::sys::IDENT_USE_CLOENV,
595                ) == crate::sexp_types::Rboolean::FALSE
596            }
597        };
598        Ok(Some(GroupDeclaration {
599            vars: groups
600                .key_columns()
601                .into_iter()
602                .map(|(name, _)| name)
603                .collect(),
604            drop,
605        }))
606    }
607
608    /// Ingest a dplyr `grouped_df`'s existing grouping from its `groups`
609    /// attribute — **honoring the caller's grouping without recomputing it**.
610    ///
611    /// dplyr stores a `grouped_df`'s grouping in `attr(df, "groups")`: a
612    /// `data.frame` whose leading columns are the group-key columns (one row
613    /// per group, in dplyr's group order) and whose trailing `.rows`
614    /// list-column holds, per group, the 1-based row indices into `df`. This
615    /// method reads that metadata verbatim into a [`GroupedDataFrame`] — the
616    /// same type [`group_by`](Self::group_by) / [`group_by_multi`](Self::group_by_multi)
617    /// produce — so a `#[miniextendr]` function handed a dplyr-grouped frame can
618    /// respect the caller's grouping, including multi-column groupings.
619    ///
620    /// Unlike [`group_by`](Self::group_by), this does **no** recomputation: a
621    /// frame that is not grouped (see [`group_declaration`](Self::group_declaration)
622    /// for the test) is an error ([`NotGroupedDataFrame`]). Callers who want the
623    /// framework to compute grouping should use [`group_by`](Self::group_by) /
624    /// [`group_by_multi`](Self::group_by_multi).
625    ///
626    /// # Keys
627    ///
628    /// A single key column yields scalar [`GroupKey`]s; multiple key columns
629    /// yield [`GroupKey::Tuple`]s (labels `.`-joined), consistent with
630    /// [`group_by_multi`](Self::group_by_multi). Supported key-column types are
631    /// the same as [`group_by`](Self::group_by) (factor, character, integer,
632    /// logical, double — `Date` and `POSIXct` included); a list / `integer64`
633    /// key column is an error.
634    ///
635    /// # Order & empty groups
636    ///
637    /// The `groups`-frame row order is preserved verbatim — dplyr's order is
638    /// authoritative, with no re-sorting and no NA reordering. `.drop = FALSE`
639    /// empty groups (zero-length `.rows`) are **kept** as groups with empty
640    /// index vectors, mirroring the empty-factor-level convention of
641    /// [`group_by`](Self::group_by).
642    ///
643    /// # Trusts the cached rows
644    ///
645    /// The `.rows` are dplyr's cache of the grouping, and this method uses them
646    /// as they are. It checks that they still fit the frame — every index in
647    /// `1..=nrow`, every row in exactly one group — and reports stale metadata
648    /// otherwise ([`GroupIndexOutOfRange`], [`GroupRowUncovered`],
649    /// [`GroupRowDuplicated`]). The usual cause is subsetting a grouped frame
650    /// without dplyr loaded: `[` then keeps the old `groups` attribute. It does
651    /// **not** compare the rows' key values with their group's key, so a pure
652    /// reorder done the same way (`df[c(3, 4, 1, 2), ]`) still fits and yields
653    /// the old row positions (#1687). To group the current rows instead, read
654    /// the declaration with [`group_declaration`](Self::group_declaration) and
655    /// group by its variables.
656    ///
657    /// # Errors
658    ///
659    /// [`NotGroupedDataFrame`] (not grouped); for a corrupt `groups`
660    /// attribute, the errors [`group_declaration`](Self::group_declaration)
661    /// reports ([`GroupsNotDataFrame`], [`MissingGroupRows`],
662    /// [`BadGroupRows`]: `.rows` must be a list of integer vectors, so a
663    /// list of doubles is rejected, as in dplyr); [`UnequalLengths`] (a key
664    /// column's length differs from that of `.rows`);
665    /// [`UnsupportedGroupColumn`] (a key column of an unsupported type); or
666    /// one of the stale-metadata errors above. Every `.rows` index is
667    /// converted from R's 1-based to 0-based.
668    ///
669    /// [`NotGroupedDataFrame`]: DataFrameError::NotGroupedDataFrame
670    /// [`GroupsNotDataFrame`]: DataFrameError::GroupsNotDataFrame
671    /// [`MissingGroupRows`]: DataFrameError::MissingGroupRows
672    /// [`BadGroupRows`]: DataFrameError::BadGroupRows
673    /// [`UnequalLengths`]: DataFrameError::UnequalLengths
674    /// [`UnsupportedGroupColumn`]: DataFrameError::UnsupportedGroupColumn
675    /// [`GroupIndexOutOfRange`]: DataFrameError::GroupIndexOutOfRange
676    /// [`GroupRowUncovered`]: DataFrameError::GroupRowUncovered
677    /// [`GroupRowDuplicated`]: DataFrameError::GroupRowDuplicated
678    ///
679    /// # GC
680    ///
681    /// The `groups` attribute frame (and its columns) is only read here, during
682    /// construction; it stays reachable via `self`'s attribute pairlist — which
683    /// R protects as a `.Call` argument frame for the duration of the call — so
684    /// it needs no separate root. Only the returned [`GroupedDataFrame`] roots
685    /// the **source** frame for its own lifetime (see its GC-rooting docs).
686    pub fn group_by_metadata(&self) -> Result<GroupedDataFrame, DataFrameError> {
687        let attr = self
688            .groups_attr()?
689            .ok_or(DataFrameError::NotGroupedDataFrame)?;
690        let n_groups = attr.rows.len();
691        let key_columns = attr.key_columns();
692
693        // Per-key-column keys: one Vec<GroupKey> of length n_groups per key
694        // column (column_keys yields one scalar key per row of the groups frame).
695        let mut per_col_keys: Vec<Vec<GroupKey>> = Vec::with_capacity(key_columns.len());
696        for (name, column) in &key_columns {
697            let keys = column_keys(*column, name)?;
698            if keys.len() != n_groups {
699                return Err(DataFrameError::UnequalLengths {
700                    expected: n_groups,
701                    columns: ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [(name.clone(), keys.len())]))vec![(name.clone(), keys.len())],
702                });
703            }
704            per_col_keys.push(keys);
705        }
706
707        let nrow = self.nrow();
708        let single_key = key_columns.len() == 1;
709
710        let mut groups: Vec<(GroupKey, Vec<usize>)> = Vec::with_capacity(n_groups);
711        for g in 0..n_groups {
712            // Build this group's key: scalar for a single key column, otherwise
713            // a Tuple in column order (`.`-joined labels, like group_by_multi).
714            let key = if single_key {
715                per_col_keys[0][g].clone()
716            } else {
717                GroupKey::Tuple(per_col_keys.iter().map(|col| col[g].clone()).collect())
718            };
719            // Convert `.rows[[g]]` (1-based indices) to a validated 0-based Vec.
720            let elt = attr.rows.vector_elt(g as isize);
721            let indices = group_rows_indices(elt, g, nrow)?;
722            groups.push((key, indices));
723        }
724        // Every index is in range; now the rows must partition the frame.
725        check_rows_partition(&groups, nrow)?;
726
727        Ok(GroupedDataFrame::new(*self, groups))
728    }
729
730    /// The `groups` attribute of a dplyr-grouped frame, checked the way
731    /// dplyr's `validate_grouped_df()` checks it (with its default
732    /// `check_bounds = FALSE`).
733    ///
734    /// `Ok(None)` when the frame inherits from neither `grouped_df` nor
735    /// `rowwise_df`. Otherwise the attribute must be a data frame with at
736    /// least one column ([`DataFrameError::GroupsNotDataFrame`]), whose last
737    /// column is called `.rows` ([`DataFrameError::MissingGroupRows`]) and is
738    /// a list of integer vectors ([`DataFrameError::BadGroupRows`]). These are
739    /// dplyr's tests: `inherits(groups, "data.frame")` with at least one
740    /// column, the last name, `typeof(.rows) == "list"`, and every element's
741    /// `typeof() == "integer"`. `.rows` and the key columns are read by
742    /// position, so a column named `.rows` elsewhere in the frame is not
743    /// mistaken for it.
744    fn groups_attr(&self) -> Result<Option<GroupsAttr>, DataFrameError> {
745        if !(self.sexp.inherits_class(c"grouped_df") || self.sexp.inherits_class(c"rowwise_df")) {
746            return Ok(None);
747        }
748        let groups_sym = unsafe { crate::sys::Rf_install(c"groups".as_ptr()) };
749        let frame = self.sexp.get_attr(groups_sym);
750        // dplyr reads the columns with VECTOR_ELT, so the frame must be a list.
751        if frame.type_of() != SEXPTYPE::VECSXP || !frame.is_data_frame() || frame.len() == 0 {
752            return Err(DataFrameError::GroupsNotDataFrame);
753        }
754        let names = frame.get_names();
755        let last_is_rows = names.type_of() == SEXPTYPE::STRSXP
756            && names.len() == frame.len()
757            && names.string_elt_str(names.len() as isize - 1) == Some(".rows");
758        if !last_is_rows {
759            return Err(DataFrameError::MissingGroupRows);
760        }
761        let rows = frame.vector_elt(frame.len() as isize - 1);
762        if rows.type_of() != SEXPTYPE::VECSXP {
763            return Err(DataFrameError::BadGroupRows {
764                group: None,
765                type_of: r_typeof(rows),
766            });
767        }
768        for group in 0..rows.len() {
769            let elt = rows.vector_elt(group as isize);
770            if elt.type_of() != SEXPTYPE::INTSXP {
771                return Err(DataFrameError::BadGroupRows {
772                    group: Some(group),
773                    type_of: r_typeof(elt),
774                });
775            }
776        }
777        Ok(Some(GroupsAttr { frame, rows }))
778    }
779}
780
781/// R's `typeof()` of `x`: `"double"`, `"NULL"`, `"list"`, ….
782fn r_typeof(x: SEXP) -> String {
783    // SAFETY: Rf_type2char returns a static C string for every SEXPTYPE.
784    unsafe { std::ffi::CStr::from_ptr(crate::sys::Rf_type2char(x.type_of())) }
785        .to_string_lossy()
786        .into_owned()
787}
788
789/// Convert one `.rows` list element — an integer vector of 1-based row
790/// indices ([`DataFrame::groups_attr`] checked its type) — into 0-based
791/// indices, rejecting any index outside `1..=nrow` (`NA` included).
792fn group_rows_indices(elt: SEXP, group: usize, nrow: usize) -> Result<Vec<usize>, DataFrameError> {
793    // SAFETY: element is INTSXP (checked by groups_attr); as_slice handles
794    // empty vectors.
795    let values: &[i32] = unsafe { elt.as_slice() };
796    values
797        .iter()
798        .map(|&value| match usize::try_from(value) {
799            Ok(row) if (1..=nrow).contains(&row) => Ok(row - 1),
800            _ => Err(DataFrameError::GroupIndexOutOfRange {
801                group,
802                value: i64::from(value),
803                nrow,
804            }),
805        })
806        .collect()
807}
808
809/// Check that in-range `.rows` cover each of the frame's `nrow` rows exactly
810/// once — the shape dplyr always maintains. The first row claimed twice, or
811/// else the first row no group claims, is reported.
812fn check_rows_partition(
813    groups: &[(GroupKey, Vec<usize>)],
814    nrow: usize,
815) -> Result<(), DataFrameError> {
816    let mut owner: Vec<Option<usize>> = ::alloc::vec::from_elem(None, nrow)vec![None; nrow];
817    for (group, (_, rows)) in groups.iter().enumerate() {
818        for &row in rows {
819            if let Some(first_group) = owner[row] {
820                return Err(DataFrameError::GroupRowDuplicated {
821                    row,
822                    first_group,
823                    second_group: group,
824                });
825            }
826            owner[row] = Some(group);
827        }
828    }
829    match owner.iter().position(Option::is_none) {
830        Some(row) => Err(DataFrameError::GroupRowUncovered { row, nrow }),
831        None => Ok(()),
832    }
833}
834
835/// One supported key column's groups, in single-column group order (NA last)
836/// — the per-type dispatch behind [`DataFrame::group_by`], and through
837/// [`column_keys`] / [`row_keys`] behind `group_by_multi` and
838/// `group_by_metadata`.
839fn column_groups(column: SEXP, col: &str) -> Result<Vec<(GroupKey, Vec<usize>)>, DataFrameError> {
840    if column.is_factor() {
841        return Ok(factor_groups(column));
842    }
843    let unsupported = |type_of: String| DataFrameError::UnsupportedGroupColumn {
844        column: col.to_string(),
845        type_of,
846    };
847    match column.type_of() {
848        SEXPTYPE::STRSXP => Ok(character_groups(column)),
849        SEXPTYPE::INTSXP => Ok(integer_groups(column)),
850        SEXPTYPE::LGLSXP => Ok(logical_groups(column)),
851        // bit64 stores int64 bits in a double vector: read as doubles, its
852        // values would order wrongly, and its NA (the bits of -0.0) would
853        // join the 0 group.
854        SEXPTYPE::REALSXP if column.inherits_class(c"integer64") => {
855            Err(unsupported("integer64".to_string()))
856        }
857        SEXPTYPE::REALSXP => real_groups(column, col),
858        other => Err(unsupported(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0:?}", other))
    })format!("{:?}", other))),
859    }
860}
861
862/// Invert groups into one key per row (`nrow` rows, row order). Each row of a
863/// key column lands in exactly one group, so every slot is filled.
864fn row_keys(groups: &[(GroupKey, Vec<usize>)], nrow: usize) -> Vec<GroupKey> {
865    let mut keys: Vec<Option<GroupKey>> = ::alloc::vec::from_elem(None, nrow)vec![None; nrow];
866    for (key, rows) in groups {
867        for &row in rows {
868            keys[row] = Some(key.clone());
869        }
870    }
871    keys.into_iter()
872        .map(|key| key.expect("each row of a key column is in exactly one group"))
873        .collect()
874}
875
876/// Per-row group key for one supported key column, in row order. NA cells — and
877/// factor NA codes / `addNA()` levels — become [`GroupKey::Na`]. Same type
878/// dispatch and errors as [`DataFrame::group_by`].
879fn column_keys(column: SEXP, col: &str) -> Result<Vec<GroupKey>, DataFrameError> {
880    Ok(row_keys(&column_groups(column, col)?, column.len()))
881}
882
883/// Factor fast path: levels are the keys (level order, empty levels kept).
884/// NA codes — and a literal NA level from `addNA()` — land in [`GroupKey::Na`].
885fn factor_groups(column: SEXP) -> Vec<(GroupKey, Vec<usize>)> {
886    // SAFETY: factor columns are INTSXP; as_slice handles empty vectors.
887    let codes: &[i32] = unsafe { column.as_slice() };
888    let levels = column.get_levels();
889    let n_levels: usize = if levels.is_nil() { 0 } else { levels.len() };
890
891    let mut buckets: Vec<Vec<usize>> = ::alloc::vec::from_elem(Vec::new(), n_levels)vec![Vec::new(); n_levels];
892    let mut na_bucket: Vec<usize> = Vec::new();
893    for (row, &code) in codes.iter().enumerate() {
894        if code.is_na() {
895            na_bucket.push(row);
896        } else {
897            buckets[(code - 1) as usize].push(row);
898        }
899    }
900
901    let mut groups: Vec<(GroupKey, Vec<usize>)> = Vec::with_capacity(n_levels + 1);
902    for (lvl, bucket) in buckets.into_iter().enumerate() {
903        let key = match levels.string_elt_str(lvl as isize) {
904            Some(label) => GroupKey::Str(label.to_string()),
905            None => GroupKey::Na, // addNA() level
906        };
907        groups.push((key, bucket));
908    }
909    if !na_bucket.is_empty() {
910        groups.push((GroupKey::Na, na_bucket));
911    }
912    groups
913}
914
915/// Character keys: byte-order sort (BTreeMap), NA last.
916fn character_groups(column: SEXP) -> Vec<(GroupKey, Vec<usize>)> {
917    let n = column.len() as isize;
918    let mut map: BTreeMap<String, Vec<usize>> = BTreeMap::new();
919    let mut na_bucket: Vec<usize> = Vec::new();
920    for i in 0..n {
921        match column.string_elt_str(i) {
922            Some(s) => map.entry(s.to_string()).or_default().push(i as usize),
923            None => na_bucket.push(i as usize),
924        }
925    }
926    let mut groups: Vec<(GroupKey, Vec<usize>)> = map
927        .into_iter()
928        .map(|(k, idx)| (GroupKey::Str(k), idx))
929        .collect();
930    if !na_bucket.is_empty() {
931        groups.push((GroupKey::Na, na_bucket));
932    }
933    groups
934}
935
936/// Integer keys: numeric sort (BTreeMap), NA (`i32::MIN`) last.
937fn integer_groups(column: SEXP) -> Vec<(GroupKey, Vec<usize>)> {
938    // SAFETY: INTSXP column; as_slice handles empty vectors.
939    let values: &[i32] = unsafe { column.as_slice() };
940    let mut map: BTreeMap<i32, Vec<usize>> = BTreeMap::new();
941    let mut na_bucket: Vec<usize> = Vec::new();
942    for (row, &v) in values.iter().enumerate() {
943        if v.is_na() {
944            na_bucket.push(row);
945        } else {
946            map.entry(v).or_default().push(row);
947        }
948    }
949    let mut groups: Vec<(GroupKey, Vec<usize>)> = map
950        .into_iter()
951        .map(|(k, idx)| (GroupKey::Int(k), idx))
952        .collect();
953    if !na_bucket.is_empty() {
954        groups.push((GroupKey::Na, na_bucket));
955    }
956    groups
957}
958
959/// Logical keys: `FALSE` then `TRUE` (R's sort order), NA last.
960/// Only keys present in the data appear.
961fn logical_groups(column: SEXP) -> Vec<(GroupKey, Vec<usize>)> {
962    let n = column.len() as isize;
963    let mut false_bucket: Vec<usize> = Vec::new();
964    let mut true_bucket: Vec<usize> = Vec::new();
965    let mut na_bucket: Vec<usize> = Vec::new();
966    for i in 0..n {
967        match column.logical_elt(i) {
968            0 => false_bucket.push(i as usize),
969            v if v.is_na() => na_bucket.push(i as usize),
970            _ => true_bucket.push(i as usize),
971        }
972    }
973    let mut groups: Vec<(GroupKey, Vec<usize>)> = Vec::with_capacity(3);
974    if !false_bucket.is_empty() {
975        groups.push((GroupKey::Bool(false), false_bucket));
976    }
977    if !true_bucket.is_empty() {
978        groups.push((GroupKey::Bool(true), true_bucket));
979    }
980    if !na_bucket.is_empty() {
981        groups.push((GroupKey::Na, na_bucket));
982    }
983    groups
984}
985
986/// Double keys (plain, `Date`, `POSIXct`, other classed doubles): numeric
987/// sort with `NaN` after every number, `NA_real_` last. Equality follows
988/// [`RealKey`] (`-0 == 0`, one `NaN` key); labels come from one
989/// `as.character()` call over the distinct keys ([`real_labels`]).
990fn real_groups(column: SEXP, col: &str) -> Result<Vec<(GroupKey, Vec<usize>)>, DataFrameError> {
991    // SAFETY: REALSXP column; as_slice handles empty vectors.
992    let values: &[f64] = unsafe { column.as_slice() };
993    let mut buckets: HashMap<u64, Vec<usize>> = HashMap::new();
994    let mut na_bucket: Vec<usize> = Vec::new();
995    for (row, &v) in values.iter().enumerate() {
996        if v.is_na() {
997            na_bucket.push(row);
998        } else {
999            buckets
1000                .entry(normalize_real(v).to_bits())
1001                .or_default()
1002                .push(row);
1003        }
1004    }
1005    // Normalised values hold no -0.0 and one positive NaN, so `total_cmp`
1006    // gives -Inf < … < Inf < NaN, dplyr's order (NA trails separately).
1007    let mut distinct: Vec<(f64, Vec<usize>)> = buckets
1008        .into_iter()
1009        .map(|(bits, rows)| (f64::from_bits(bits), rows))
1010        .collect();
1011    distinct.sort_by(|a, b| a.0.total_cmp(&b.0));
1012
1013    let key_values: Vec<f64> = distinct.iter().map(|(value, _)| *value).collect();
1014    let labels = real_labels(column, &key_values, col)?;
1015    let mut groups: Vec<(GroupKey, Vec<usize>)> = distinct
1016        .into_iter()
1017        .zip(labels)
1018        .map(|((value, rows), label)| (GroupKey::Real(RealKey { value, label }), rows))
1019        .collect();
1020    if !na_bucket.is_empty() {
1021        groups.push((GroupKey::Na, na_bucket));
1022    }
1023    Ok(groups)
1024}
1025
1026/// R's `as.character()` of `values` carrying `column`'s attributes (class,
1027/// `tzone`, `units`, …): the text R prints for each key, with S3 dispatch for
1028/// a classed column (`Date`, `POSIXct`, …) and C-level coercion otherwise.
1029///
1030/// The temporary key vector is protected while `as.character()` allocates; a
1031/// failing method surfaces as [`DataFrameError::Conversion`] (it is evaluated
1032/// with `RCall::eval`, so it never unwinds through Rust frames).
1033fn real_labels(column: SEXP, values: &[f64], col: &str) -> Result<Vec<String>, DataFrameError> {
1034    let label_error = |detail: String| {
1035        DataFrameError::Conversion(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("cannot label the keys of column {0:?}: {1}",
                col, detail))
    })format!("cannot label the keys of column {col:?}: {detail}"))
1036    };
1037    // SAFETY: the grouping verbs run on R's main thread. `keys` is a fresh
1038    // REALSXP rooted by `OwnedProtect` until the labels are copied out.
1039    let labels = unsafe {
1040        let keys = crate::OwnedProtect::new(values.into_sexp());
1041        keys.copy_most_attrib(column);
1042        crate::convert::read_character(keys.get())
1043    }
1044    .map_err(|e| label_error(e.to_string()))?;
1045    if labels.len() != values.len() {
1046        return Err(label_error(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("as.character() returned {0} values for {1} keys",
                labels.len(), values.len()))
    })format!(
1047            "as.character() returned {} values for {} keys",
1048            labels.len(),
1049            values.len()
1050        )));
1051    }
1052    Ok(labels
1053        .into_iter()
1054        .map(|label| label.unwrap_or_else(|| "NA".to_string()))
1055        .collect())
1056}
1057// endregion
1058
1059#[cfg(test)]
1060mod tests {
1061    use super::*;
1062
1063    #[test]
1064    fn group_rows_partitions_and_orders_by_key() {
1065        let rows = vec![("b", 1), ("a", 2), ("b", 3), ("c", 4)];
1066        let grouped = group_rows(rows, |r| r.0);
1067        let keys: Vec<&str> = grouped.keys().copied().collect();
1068        assert_eq!(keys, vec!["a", "b", "c"]);
1069        assert_eq!(grouped["b"], vec![("b", 1), ("b", 3)]);
1070    }
1071
1072    #[test]
1073    fn group_rows_option_key_gives_na_a_home() {
1074        let rows = vec![(Some(2), "x"), (None, "y"), (Some(1), "z")];
1075        let grouped = group_rows(rows, |r| r.0);
1076        let keys: Vec<Option<i32>> = grouped.keys().copied().collect();
1077        assert_eq!(keys, vec![None, Some(1), Some(2)]);
1078    }
1079
1080    #[test]
1081    fn group_key_labels_match_r_printing() {
1082        assert_eq!(GroupKey::Str("a".into()).label(), "a");
1083        assert_eq!(GroupKey::Int(-3).label(), "-3");
1084        assert_eq!(GroupKey::Bool(true).label(), "TRUE");
1085        assert_eq!(GroupKey::Bool(false).label(), "FALSE");
1086        assert_eq!(GroupKey::Na.label(), "NA");
1087        assert_eq!(GroupKey::Na.to_string(), "NA");
1088    }
1089
1090    #[test]
1091    fn tuple_key_labels_join_with_dot() {
1092        let key = GroupKey::Tuple(vec![
1093            GroupKey::Str("a".into()),
1094            GroupKey::Int(2),
1095            GroupKey::Real(RealKey::new(2.5, "2.5")),
1096            GroupKey::Bool(true),
1097            GroupKey::Na,
1098        ]);
1099        assert_eq!(key.label(), "a.2.2.5.TRUE.NA");
1100        assert_eq!(key.to_string(), "a.2.2.5.TRUE.NA");
1101    }
1102
1103    fn hash_of(key: &RealKey) -> u64 {
1104        use std::hash::{BuildHasher, BuildHasherDefault};
1105        BuildHasherDefault::<std::collections::hash_map::DefaultHasher>::default().hash_one(key)
1106    }
1107
1108    #[test]
1109    fn real_key_equality_follows_vctrs() {
1110        // -0 and 0 are one key (and hash alike).
1111        let neg_zero = RealKey::new(-0.0, "0");
1112        let zero = RealKey::new(0.0, "0");
1113        assert_eq!(neg_zero, zero);
1114        assert_eq!(hash_of(&neg_zero), hash_of(&zero));
1115        assert!(neg_zero.value().is_sign_positive());
1116
1117        // Every NaN is one key, stored as the canonical positive NaN —
1118        // including x86's negative `0/0` NaN.
1119        let nan = RealKey::new(f64::NAN, "NaN");
1120        let neg_nan = RealKey::new(f64::from_bits(0xFFF8_0000_0000_0000), "NaN");
1121        assert_eq!(nan, neg_nan);
1122        assert_eq!(hash_of(&nan), hash_of(&neg_nan));
1123        assert_eq!(neg_nan.value().to_bits(), CANONICAL_NAN_BITS);
1124        assert!(!nan.value().is_na());
1125
1126        // The label plays no part in equality; distinct values differ.
1127        assert_eq!(RealKey::new(1.5, "x"), RealKey::new(1.5, "y"));
1128        assert_ne!(RealKey::new(1.5, "1.5"), RealKey::new(2.5, "2.5"));
1129    }
1130
1131    #[test]
1132    fn real_key_label_and_order() {
1133        let key = GroupKey::Real(RealKey::new(19724.0, "2024-01-02"));
1134        assert_eq!(key.label(), "2024-01-02");
1135        assert_eq!(key.to_string(), "2024-01-02");
1136
1137        // Normalised values sort -Inf < numbers < Inf < NaN under total_cmp.
1138        let mut values: Vec<f64> = [f64::NAN, 1.0, f64::INFINITY, -0.0, f64::NEG_INFINITY]
1139            .into_iter()
1140            .map(normalize_real)
1141            .collect();
1142        values.sort_by(f64::total_cmp);
1143        assert_eq!(values[0], f64::NEG_INFINITY);
1144        assert_eq!(values[1].to_bits(), 0.0f64.to_bits());
1145        assert_eq!(values[2], 1.0);
1146        assert_eq!(values[3], f64::INFINITY);
1147        assert!(values[4].is_nan());
1148    }
1149}