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