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.
5556use std::collections::{BTreeMap, HashMap};
5758use super::{DataFrame, DataFrameError, FromDataFrame};
59use crate::into_r::IntoR;
60use crate::{IsNA, SEXP, SEXPTYPE, SexpExt};
6162// region: group_rows — typed-rows grouping helper (rung 1)
6364/// 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
71K: Ord,
72 F: Fn(&T) -> K,
73{
74let mut groups: BTreeMap<K, Vec<T>> = BTreeMap::new();
75for row in rows {
76 groups.entry(key(&row)).or_default().push(row);
77 }
78groups79}
80// endregion
8182// region: GroupKey
8384/// 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.
89Str(String),
90/// An integer key.
91Int(i32),
92/// A double key: a plain number, a `Date`, a `POSIXct`, or another classed
93 /// double. See [`RealKey`] for its equality and label.
94Real(RealKey),
95/// A logical key.
96Bool(bool),
97/// The NA-keyed group (always ordered last).
98Na,
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)).
103Tuple(Vec<GroupKey>),
104}
105106impl 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.
112pub fn label(&self) -> String {
113match 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) => {
121if 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);
125keys.iter()
126 .map(GroupKey::label)
127 .collect::<Vec<_>>()
128 .join(".")
129 }
130 }
131 }
132}
133134impl std::fmt::Displayfor GroupKey {
135fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136match 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) => {
144for (i, key) in keys.iter().enumerate() {
145if i > 0 {
146 f.write_str(".")?;
147 }
148f.write_fmt(format_args!("{0}", key))write!(f, "{}", key)?;
149 }
150Ok(())
151 }
152 }
153 }
154}
155156/// 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}
180181impl 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.
184pub fn new(value: f64, label: impl Into<String>) -> Self {
185RealKey {
186 value: normalize_real(value),
187 label: label.into(),
188 }
189 }
190191/// 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.
194pub fn value(&self) -> f64 {
195self.value
196 }
197198/// The text R prints for the key (see the [type docs](Self)).
199pub fn label(&self) -> &str {
200&self.label
201 }
202}
203204impl PartialEqfor RealKey {
205fn eq(&self, other: &Self) -> bool {
206self.value.to_bits() == other.value.to_bits()
207 }
208}
209210impl Eqfor RealKey {}
211212impl std::hash::Hashfor RealKey {
213fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
214self.value.to_bits().hash(state);
215 }
216}
217218/// 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;
222223/// 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 {
226if x == 0.0 {
2270.0
228} else if x.is_nan() {
229f64::from_bits(CANONICAL_NAN_BITS)
230 } else {
231x232 }
233}
234// endregion
235236// region: GroupDeclaration
237238/// 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.
247pub 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`.
255pub drop: bool,
256}
257258/// 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.
263frame: SEXP,
264/// Its last column, `.rows`: a list of integer vectors, one per group.
265rows: SEXP,
266}
267268impl 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`].
274fn key_columns(&self) -> Vec<(String, SEXP)> {
275let names = self.frame.get_names();
276let mut columns: Vec<(String, SEXP)> = Vec::new();
277for i in 0..self.frame.len() as isize {
278let name = names.string_elt_str(i).unwrap_or("");
279if name == ".rows" || columns.iter().any(|(seen, _)| seen == name) {
280continue;
281 }
282 columns.push((name.to_string(), self.frame.vector_elt(i)));
283 }
284columns285 }
286}
287// endregion
288289// region: GroupedDataFrame
290291/// 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}
311312impl Dropfor GroupedDataFrame {
313fn drop(&mut self) {
314// SAFETY: main thread (construction invariant — SEXP is !Send);
315 // releases the preserve taken in `group_by`.
316unsafe { crate::sys::R_ReleaseObject(self.source.as_sexp()) };
317 }
318}
319320impl 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`.
325fn new(source: DataFrame, groups: Vec<(GroupKey, Vec<usize>)>) -> Self {
326unsafe {
327let _guard = crate::OwnedProtect::new(source.sexp);
328crate::sys::R_PreserveObject(source.sexp);
329 }
330GroupedDataFrame { source, groups }
331 }
332333/// Number of groups (empty factor levels included).
334pub fn len(&self) -> usize {
335self.groups.len()
336 }
337338/// Whether there are no groups.
339pub fn is_empty(&self) -> bool {
340self.groups.is_empty()
341 }
342343/// The frame this grouping was computed from.
344pub fn source(&self) -> &DataFrame {
345&self.source
346 }
347348/// Iterate `(key, row-indices)` pairs in group order. Indices are 0-based
349 /// rows of [`source`](Self::source).
350pub fn iter(&self) -> impl Iterator<Item = (&GroupKey, &[usize])> {
351self.groups.iter().map(|(k, idx)| (k, idx.as_slice()))
352 }
353354/// 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.
362pub fn frames(&self) -> impl Iterator<Item = (&GroupKey, crate::dataframe::BuiltDataFrame)> {
363self.groups
364 .iter()
365 .map(|(k, idx)| (k, self.source.select_rows(idx)))
366 }
367368/// 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).
373pub fn extract<T>(&self) -> Result<Vec<(GroupKey, Vec<T>)>, DataFrameError>
374where
375Vec<T>: FromDataFrame,
376 {
377let rows = Vec::<T>::from_dataframe(&self.source)?;
378let mut slots: Vec<Option<T>> = rows.into_iter().map(Some).collect();
379Ok(self380 .groups
381 .iter()
382 .map(|(key, idx)| {
383let group_rows: Vec<T> = idx384 .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
393394// region: DataFrame::group_by
395396impl 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.
408pub fn group_by(&self, col: &str) -> Result<GroupedDataFrame, DataFrameError> {
409let column = self
410.column_raw(col)
411 .ok_or_else(|| DataFrameError::NoSuchColumn(col.to_string()))?;
412let groups = column_groups(column, col)?;
413// Root the source for the GroupedDataFrame's lifetime (see its GC
414 // rooting docs).
415Ok(GroupedDataFrame::new(*self, groups))
416 }
417418/// 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.
453pub fn group_by_multi(&self, cols: &[&str]) -> Result<GroupedDataFrame, DataFrameError> {
454if cols.is_empty() {
455return Err(DataFrameError::EmptyGroupColumns);
456 }
457// A single column is the scalar path — identical keys/order to group_by.
458if let [col] = cols {
459return self.group_by(col);
460 }
461462// 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.
466let mut per_row: Vec<Vec<GroupKey>> = Vec::with_capacity(cols.len());
467let mut ordinals: Vec<HashMap<GroupKey, usize>> = Vec::with_capacity(cols.len());
468for &col in cols {
469let column = self
470.column_raw(col)
471 .ok_or_else(|| DataFrameError::NoSuchColumn(col.to_string()))?;
472let 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 }
484485// Bucket rows into tuple keys, preserving first-encounter order.
486let mut index: HashMap<GroupKey, usize> = HashMap::new();
487let mut buckets: Vec<(GroupKey, Vec<usize>)> = Vec::new();
488for (row, first) in per_row[0].iter().enumerate() {
489let mut elems = Vec::with_capacity(cols.len());
490 elems.push(first.clone());
491for col in &per_row[1..] {
492 elems.push(col[row].clone());
493 }
494let key = GroupKey::Tuple(elems);
495match index.get(&key) {
496Some(&pos) => buckets[pos].1.push(row),
497None => {
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 }
503504// 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).
507let 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)));
508let (mut non_na, na_tuples): (Vec<_>, Vec<_>) =
509buckets.into_iter().partition(|(k, _)| !is_na_tuple(k));
510non_na.sort_by_key(|(k, _)| {
511let GroupKey::Tuple(elems) = kelse {
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 };
514let mut ord: Vec<usize> = elems515 .iter()
516 .enumerate()
517 .map(|(c, e)| ordinals[c][e])
518 .collect();
519ord.reverse();
520ord521 });
522non_na.extend(na_tuples);
523524Ok(GroupedDataFrame::new(*self, non_na))
525 }
526527/// 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 /// ```
577pub fn group_declaration(&self) -> Result<Option<GroupDeclaration>, DataFrameError> {
578let Some(groups) = self.groups_attr()? else {
579return 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.
586let drop = !self.sexp.inherits_class(c"grouped_df") || {
587let drop_attr = groups588 .frame
589 .get_attr(unsafe { crate::sys::Rf_install(c".drop".as_ptr()) });
590unsafe {
591crate::sys::R_compute_identical(
592drop_attr,
593crate::sys::Rf_ScalarLogical(0),
594crate::sys::IDENT_USE_CLOENV,
595 ) == crate::sexp_types::Rboolean::FALSE596 }
597 };
598Ok(Some(GroupDeclaration {
599 vars: groups600 .key_columns()
601 .into_iter()
602 .map(|(name, _)| name)
603 .collect(),
604drop,
605 }))
606 }
607608/// 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).
686pub fn group_by_metadata(&self) -> Result<GroupedDataFrame, DataFrameError> {
687let attr = self
688.groups_attr()?
689.ok_or(DataFrameError::NotGroupedDataFrame)?;
690let n_groups = attr.rows.len();
691let key_columns = attr.key_columns();
692693// 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).
695let mut per_col_keys: Vec<Vec<GroupKey>> = Vec::with_capacity(key_columns.len());
696for (name, column) in &key_columns {
697let keys = column_keys(*column, name)?;
698if keys.len() != n_groups {
699return 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 }
706707let nrow = self.nrow();
708let single_key = key_columns.len() == 1;
709710let mut groups: Vec<(GroupKey, Vec<usize>)> = Vec::with_capacity(n_groups);
711for 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).
714let 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.
720let elt = attr.rows.vector_elt(g as isize);
721let 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.
725check_rows_partition(&groups, nrow)?;
726727Ok(GroupedDataFrame::new(*self, groups))
728 }
729730/// 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.
744fn groups_attr(&self) -> Result<Option<GroupsAttr>, DataFrameError> {
745if !(self.sexp.inherits_class(c"grouped_df") || self.sexp.inherits_class(c"rowwise_df")) {
746return Ok(None);
747 }
748let groups_sym = unsafe { crate::sys::Rf_install(c"groups".as_ptr()) };
749let frame = self.sexp.get_attr(groups_sym);
750// dplyr reads the columns with VECTOR_ELT, so the frame must be a list.
751if frame.type_of() != SEXPTYPE::VECSXP || !frame.is_data_frame() || frame.len() == 0 {
752return Err(DataFrameError::GroupsNotDataFrame);
753 }
754let names = frame.get_names();
755let last_is_rows = names.type_of() == SEXPTYPE::STRSXP756 && names.len() == frame.len()
757 && names.string_elt_str(names.len() as isize - 1) == Some(".rows");
758if !last_is_rows {
759return Err(DataFrameError::MissingGroupRows);
760 }
761let rows = frame.vector_elt(frame.len() as isize - 1);
762if rows.type_of() != SEXPTYPE::VECSXP {
763return Err(DataFrameError::BadGroupRows {
764 group: None,
765 type_of: r_typeof(rows),
766 });
767 }
768for group in 0..rows.len() {
769let elt = rows.vector_elt(group as isize);
770if elt.type_of() != SEXPTYPE::INTSXP {
771return Err(DataFrameError::BadGroupRows {
772 group: Some(group),
773 type_of: r_typeof(elt),
774 });
775 }
776 }
777Ok(Some(GroupsAttr { frame, rows }))
778 }
779}
780781/// 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.
784unsafe { std::ffi::CStr::from_ptr(crate::sys::Rf_type2char(x.type_of())) }
785 .to_string_lossy()
786 .into_owned()
787}
788789/// 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.
795let values: &[i32] = unsafe { elt.as_slice() };
796values797 .iter()
798 .map(|&value| match usize::try_from(value) {
799Ok(row) if (1..=nrow).contains(&row) => Ok(row - 1),
800_ => Err(DataFrameError::GroupIndexOutOfRange {
801group,
802 value: i64::from(value),
803nrow,
804 }),
805 })
806 .collect()
807}
808809/// 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> {
816let mut owner: Vec<Option<usize>> = ::alloc::vec::from_elem(None, nrow)vec![None; nrow];
817for (group, (_, rows)) in groups.iter().enumerate() {
818for &row in rows {
819if let Some(first_group) = owner[row] {
820return Err(DataFrameError::GroupRowDuplicated {
821 row,
822 first_group,
823 second_group: group,
824 });
825 }
826 owner[row] = Some(group);
827 }
828 }
829match owner.iter().position(Option::is_none) {
830Some(row) => Err(DataFrameError::GroupRowUncovered { row, nrow }),
831None => Ok(()),
832 }
833}
834835/// 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> {
840if column.is_factor() {
841return Ok(factor_groups(column));
842 }
843let unsupported = |type_of: String| DataFrameError::UnsupportedGroupColumn {
844 column: col.to_string(),
845type_of,
846 };
847match 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.
854SEXPTYPE::REALSXPif column.inherits_class(c"integer64") => {
855Err(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}
861862/// 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> {
865let mut keys: Vec<Option<GroupKey>> = ::alloc::vec::from_elem(None, nrow)vec![None; nrow];
866for (key, rows) in groups {
867for &row in rows {
868 keys[row] = Some(key.clone());
869 }
870 }
871keys.into_iter()
872 .map(|key| key.expect("each row of a key column is in exactly one group"))
873 .collect()
874}
875876/// 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> {
880Ok(row_keys(&column_groups(column, col)?, column.len()))
881}
882883/// 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.
887let codes: &[i32] = unsafe { column.as_slice() };
888let levels = column.get_levels();
889let n_levels: usize = if levels.is_nil() { 0 } else { levels.len() };
890891let mut buckets: Vec<Vec<usize>> = ::alloc::vec::from_elem(Vec::new(), n_levels)vec![Vec::new(); n_levels];
892let mut na_bucket: Vec<usize> = Vec::new();
893for (row, &code) in codes.iter().enumerate() {
894if code.is_na() {
895 na_bucket.push(row);
896 } else {
897 buckets[(code - 1) as usize].push(row);
898 }
899 }
900901let mut groups: Vec<(GroupKey, Vec<usize>)> = Vec::with_capacity(n_levels + 1);
902for (lvl, bucket) in buckets.into_iter().enumerate() {
903let key = match levels.string_elt_str(lvl as isize) {
904Some(label) => GroupKey::Str(label.to_string()),
905None => GroupKey::Na, // addNA() level
906};
907 groups.push((key, bucket));
908 }
909if !na_bucket.is_empty() {
910groups.push((GroupKey::Na, na_bucket));
911 }
912groups913}
914915/// Character keys: byte-order sort (BTreeMap), NA last.
916fn character_groups(column: SEXP) -> Vec<(GroupKey, Vec<usize>)> {
917let n = column.len() as isize;
918let mut map: BTreeMap<String, Vec<usize>> = BTreeMap::new();
919let mut na_bucket: Vec<usize> = Vec::new();
920for i in 0..n {
921match column.string_elt_str(i) {
922Some(s) => map.entry(s.to_string()).or_default().push(i as usize),
923None => na_bucket.push(i as usize),
924 }
925 }
926let mut groups: Vec<(GroupKey, Vec<usize>)> = map927 .into_iter()
928 .map(|(k, idx)| (GroupKey::Str(k), idx))
929 .collect();
930if !na_bucket.is_empty() {
931groups.push((GroupKey::Na, na_bucket));
932 }
933groups934}
935936/// 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.
939let values: &[i32] = unsafe { column.as_slice() };
940let mut map: BTreeMap<i32, Vec<usize>> = BTreeMap::new();
941let mut na_bucket: Vec<usize> = Vec::new();
942for (row, &v) in values.iter().enumerate() {
943if v.is_na() {
944 na_bucket.push(row);
945 } else {
946 map.entry(v).or_default().push(row);
947 }
948 }
949let mut groups: Vec<(GroupKey, Vec<usize>)> = map950 .into_iter()
951 .map(|(k, idx)| (GroupKey::Int(k), idx))
952 .collect();
953if !na_bucket.is_empty() {
954groups.push((GroupKey::Na, na_bucket));
955 }
956groups957}
958959/// 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>)> {
962let n = column.len() as isize;
963let mut false_bucket: Vec<usize> = Vec::new();
964let mut true_bucket: Vec<usize> = Vec::new();
965let mut na_bucket: Vec<usize> = Vec::new();
966for i in 0..n {
967match column.logical_elt(i) {
9680 => 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 }
973let mut groups: Vec<(GroupKey, Vec<usize>)> = Vec::with_capacity(3);
974if !false_bucket.is_empty() {
975groups.push((GroupKey::Bool(false), false_bucket));
976 }
977if !true_bucket.is_empty() {
978groups.push((GroupKey::Bool(true), true_bucket));
979 }
980if !na_bucket.is_empty() {
981groups.push((GroupKey::Na, na_bucket));
982 }
983groups984}
985986/// 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.
992let values: &[f64] = unsafe { column.as_slice() };
993let mut buckets: HashMap<u64, Vec<usize>> = HashMap::new();
994let mut na_bucket: Vec<usize> = Vec::new();
995for (row, &v) in values.iter().enumerate() {
996if 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).
1007let mut distinct: Vec<(f64, Vec<usize>)> = buckets1008 .into_iter()
1009 .map(|(bits, rows)| (f64::from_bits(bits), rows))
1010 .collect();
1011distinct.sort_by(|a, b| a.0.total_cmp(&b.0));
10121013let key_values: Vec<f64> = distinct.iter().map(|(value, _)| *value).collect();
1014let labels = real_labels(column, &key_values, col)?;
1015let mut groups: Vec<(GroupKey, Vec<usize>)> = distinct1016 .into_iter()
1017 .zip(labels)
1018 .map(|((value, rows), label)| (GroupKey::Real(RealKey { value, label }), rows))
1019 .collect();
1020if !na_bucket.is_empty() {
1021groups.push((GroupKey::Na, na_bucket));
1022 }
1023Ok(groups)
1024}
10251026/// 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> {
1034let 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.
1039let labels = unsafe {
1040let keys = crate::OwnedProtect::new(values.into_sexp());
1041 keys.copy_most_attrib(column);
1042crate::convert::read_character(keys.get())
1043 }
1044 .map_err(|e| label_error(e.to_string()))?;
1045if labels.len() != values.len() {
1046return 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 }
1052Ok(labels1053 .into_iter()
1054 .map(|label| label.unwrap_or_else(|| "NA".to_string()))
1055 .collect())
1056}
1057// endregion
10581059#[cfg(test)]
1060mod tests {
1061use super::*;
10621063#[test]
1064fn group_rows_partitions_and_orders_by_key() {
1065let rows = vec![("b", 1), ("a", 2), ("b", 3), ("c", 4)];
1066let grouped = group_rows(rows, |r| r.0);
1067let keys: Vec<&str> = grouped.keys().copied().collect();
1068assert_eq!(keys, vec!["a", "b", "c"]);
1069assert_eq!(grouped["b"], vec![("b", 1), ("b", 3)]);
1070 }
10711072#[test]
1073fn group_rows_option_key_gives_na_a_home() {
1074let rows = vec![(Some(2), "x"), (None, "y"), (Some(1), "z")];
1075let grouped = group_rows(rows, |r| r.0);
1076let keys: Vec<Option<i32>> = grouped.keys().copied().collect();
1077assert_eq!(keys, vec![None, Some(1), Some(2)]);
1078 }
10791080#[test]
1081fn group_key_labels_match_r_printing() {
1082assert_eq!(GroupKey::Str("a".into()).label(), "a");
1083assert_eq!(GroupKey::Int(-3).label(), "-3");
1084assert_eq!(GroupKey::Bool(true).label(), "TRUE");
1085assert_eq!(GroupKey::Bool(false).label(), "FALSE");
1086assert_eq!(GroupKey::Na.label(), "NA");
1087assert_eq!(GroupKey::Na.to_string(), "NA");
1088 }
10891090#[test]
1091fn tuple_key_labels_join_with_dot() {
1092let 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 ]);
1099assert_eq!(key.label(), "a.2.2.5.TRUE.NA");
1100assert_eq!(key.to_string(), "a.2.2.5.TRUE.NA");
1101 }
11021103fn hash_of(key: &RealKey) -> u64 {
1104use std::hash::{BuildHasher, BuildHasherDefault};
1105 BuildHasherDefault::<std::collections::hash_map::DefaultHasher>::default().hash_one(key)
1106 }
11071108#[test]
1109fn real_key_equality_follows_vctrs() {
1110// -0 and 0 are one key (and hash alike).
1111let neg_zero = RealKey::new(-0.0, "0");
1112let zero = RealKey::new(0.0, "0");
1113assert_eq!(neg_zero, zero);
1114assert_eq!(hash_of(&neg_zero), hash_of(&zero));
1115assert!(neg_zero.value().is_sign_positive());
11161117// Every NaN is one key, stored as the canonical positive NaN —
1118 // including x86's negative `0/0` NaN.
1119let nan = RealKey::new(f64::NAN, "NaN");
1120let neg_nan = RealKey::new(f64::from_bits(0xFFF8_0000_0000_0000), "NaN");
1121assert_eq!(nan, neg_nan);
1122assert_eq!(hash_of(&nan), hash_of(&neg_nan));
1123assert_eq!(neg_nan.value().to_bits(), CANONICAL_NAN_BITS);
1124assert!(!nan.value().is_na());
11251126// The label plays no part in equality; distinct values differ.
1127assert_eq!(RealKey::new(1.5, "x"), RealKey::new(1.5, "y"));
1128assert_ne!(RealKey::new(1.5, "1.5"), RealKey::new(2.5, "2.5"));
1129 }
11301131#[test]
1132fn real_key_label_and_order() {
1133let key = GroupKey::Real(RealKey::new(19724.0, "2024-01-02"));
1134assert_eq!(key.label(), "2024-01-02");
1135assert_eq!(key.to_string(), "2024-01-02");
11361137// Normalised values sort -Inf < numbers < Inf < NaN under total_cmp.
1138let 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);
1143assert_eq!(values[0], f64::NEG_INFINITY);
1144assert_eq!(values[1].to_bits(), 0.0f64.to_bits());
1145assert_eq!(values[2], 1.0);
1146assert_eq!(values[3], f64::INFINITY);
1147assert!(values[4].is_nan());
1148 }
1149}