1use super::selectors::parse_variable_selector;
2use super::*;
3use runmat_value::IntValue;
4
5mod grpstats;
6
7pub(in crate::builtins::table) use grpstats::grpstats_impl;
8
9pub fn sortrows_table(value: Value, rest: &[Value]) -> BuiltinResult<(Value, Tensor)> {
10 let object = into_table_object(value, "sortrows")?;
11 let names = table_variable_names_from_object(&object)?;
12 let sort_spec = SortSpec::parse(rest, &names)?;
13 let height = table_height(&object)?;
14 let variables = table_variables(&object)?;
15 let mut indices: Vec<usize> = (0..height).collect();
16 indices.sort_by(|&a, &b| {
17 for key in &sort_spec.keys {
18 let Some(value) = variables.fields.get(&key.name) else {
19 continue;
20 };
21 let ord = compare_table_cells(value, a, b).unwrap_or(Ordering::Equal);
22 let ord = if key.descending { ord.reverse() } else { ord };
23 if ord != Ordering::Equal {
24 return ord;
25 }
26 }
27 a.cmp(&b)
28 });
29 let mut sorted_columns = Vec::with_capacity(names.len());
30 for name in &names {
31 let value = variables
32 .fields
33 .get(name)
34 .ok_or_else(|| invalid_variable(format!("table: missing variable '{name}'")))?;
35 sorted_columns.push(select_rows(value, &indices)?);
36 }
37 let row_names = selected_row_names(&object, &indices)?;
38 let sorted = table_from_columns_with_properties(names, sorted_columns, row_names)?;
39 let indices_tensor = Tensor::new(
40 indices.iter().map(|idx| *idx as f64 + 1.0).collect(),
41 vec![indices.len(), 1],
42 )
43 .map_err(invalid_variable)?;
44 Ok((sorted, indices_tensor))
45}
46
47pub(in crate::builtins::table) struct SortSpec {
48 keys: Vec<SortKey>,
49}
50
51pub(in crate::builtins::table) struct SortKey {
52 name: String,
53 descending: bool,
54}
55
56impl SortSpec {
57 fn parse(rest: &[Value], names: &[String]) -> BuiltinResult<Self> {
58 let mut keys = if rest.is_empty() {
59 names
60 .iter()
61 .map(|name| SortKey {
62 name: name.clone(),
63 descending: false,
64 })
65 .collect::<Vec<_>>()
66 } else {
67 parse_variable_selector(rest.first(), names)?
68 .into_iter()
69 .map(|name| SortKey {
70 name,
71 descending: false,
72 })
73 .collect()
74 };
75 if let Some(direction) = rest.get(1) {
76 let directions = string_list(direction)?;
77 if directions.len() == 1 {
78 let descending = directions[0].eq_ignore_ascii_case("descend")
79 || directions[0].eq_ignore_ascii_case("desc");
80 for key in &mut keys {
81 key.descending = descending;
82 }
83 } else {
84 for (key, direction) in keys.iter_mut().zip(directions.iter()) {
85 key.descending = direction.eq_ignore_ascii_case("descend")
86 || direction.eq_ignore_ascii_case("desc");
87 }
88 }
89 }
90 Ok(Self { keys })
91 }
92}
93
94pub(in crate::builtins::table) fn compare_table_cells(
95 value: &Value,
96 a: usize,
97 b: usize,
98) -> BuiltinResult<Ordering> {
99 match value {
100 Value::Tensor(tensor) => {
101 if let Some(storage) = tensor.integer_storage() {
102 let left = storage
103 .value_at(a)
104 .ok_or_else(|| invalid_index("table: numeric row index out of bounds"))?;
105 let right = storage
106 .value_at(b)
107 .ok_or_else(|| invalid_index("table: numeric row index out of bounds"))?;
108 return Ok(compare_integer_values(&left, &right));
109 }
110 Ok(tensor
111 .get2(a, 0)
112 .map_err(invalid_index)?
113 .partial_cmp(&tensor.get2(b, 0).map_err(invalid_index)?)
114 .unwrap_or(Ordering::Greater))
115 }
116 Value::StringArray(array) => {
117 let av = array.data.get(a).cloned().unwrap_or_default();
118 let bv = array.data.get(b).cloned().unwrap_or_default();
119 Ok(av.cmp(&bv))
120 }
121 Value::LogicalArray(array) => {
122 let av = *array.data.get(a).unwrap_or(&0);
123 let bv = *array.data.get(b).unwrap_or(&0);
124 Ok(av.cmp(&bv))
125 }
126 Value::Object(obj) if obj.is_class("datetime") => {
127 let tensor = crate::builtins::datetime::serials_from_datetime_value(value)?;
128 Ok(double_value_at(&tensor, a)
129 .unwrap_or(f64::NAN)
130 .partial_cmp(&double_value_at(&tensor, b).unwrap_or(f64::NAN))
131 .unwrap_or(Ordering::Greater))
132 }
133 other => Ok(cell_key_string(other, a).cmp(&cell_key_string(other, b))),
134 }
135}
136
137#[derive(Clone, Debug)]
138pub(in crate::builtins::table) enum GroupAtom {
139 Number(f64),
140 Integer(IntValue),
141 Text(String),
142 Logical(bool),
143 Missing,
144}
145
146impl GroupAtom {
147 fn rank(&self) -> u8 {
148 match self {
149 Self::Logical(_) => 0,
150 Self::Number(_) => 1,
151 Self::Integer(_) => 2,
152 Self::Text(_) => 3,
153 Self::Missing => 4,
154 }
155 }
156}
157
158impl PartialEq for GroupAtom {
159 fn eq(&self, other: &Self) -> bool {
160 self.cmp(other) == Ordering::Equal
161 }
162}
163
164impl Eq for GroupAtom {}
165
166impl PartialOrd for GroupAtom {
167 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
168 Some(self.cmp(other))
169 }
170}
171
172impl Ord for GroupAtom {
173 fn cmp(&self, other: &Self) -> Ordering {
174 let rank = self.rank().cmp(&other.rank());
175 if rank != Ordering::Equal {
176 return rank;
177 }
178 match (self, other) {
179 (Self::Missing, Self::Missing) => Ordering::Equal,
180 (Self::Logical(a), Self::Logical(b)) => a.cmp(b),
181 (Self::Number(a), Self::Number(b)) => a.total_cmp(b),
182 (Self::Integer(a), Self::Integer(b)) => compare_integer_values(a, b),
183 (Self::Text(a), Self::Text(b)) => a.cmp(b),
184 _ => Ordering::Equal,
185 }
186 }
187}
188
189pub(in crate::builtins::table) fn group_atom_is_missing(atom: &GroupAtom) -> bool {
190 match atom {
191 GroupAtom::Missing => true,
192 GroupAtom::Number(value) => value.is_nan(),
193 GroupAtom::Integer(_) => false,
194 GroupAtom::Text(value) => value.is_empty(),
195 GroupAtom::Logical(_) => false,
196 }
197}
198
199fn compare_integer_values(left: &IntValue, right: &IntValue) -> Ordering {
200 let left = integer_sign_and_magnitude(left);
201 let right = integer_sign_and_magnitude(right);
202 match (left.0, right.0) {
203 (true, false) => Ordering::Less,
204 (false, true) => Ordering::Greater,
205 (false, false) => left.1.cmp(&right.1),
206 (true, true) => right.1.cmp(&left.1),
207 }
208}
209
210fn integer_sign_and_magnitude(value: &IntValue) -> (bool, u64) {
211 match value {
212 IntValue::I8(value) => (*value < 0, value.unsigned_abs() as u64),
213 IntValue::I16(value) => (*value < 0, value.unsigned_abs() as u64),
214 IntValue::I32(value) => (*value < 0, value.unsigned_abs() as u64),
215 IntValue::I64(value) => (*value < 0, value.unsigned_abs()),
216 IntValue::U8(value) => (false, *value as u64),
217 IntValue::U16(value) => (false, *value as u64),
218 IntValue::U32(value) => (false, *value as u64),
219 IntValue::U64(value) => (false, *value),
220 }
221}
222
223pub(in crate::builtins::table) fn cell_group_atom(value: &Value, row: usize) -> GroupAtom {
224 match value {
225 Value::Tensor(tensor) => {
226 if let Some(storage) = tensor.integer_storage() {
227 return storage
228 .value_at(row)
229 .map(GroupAtom::Integer)
230 .unwrap_or(GroupAtom::Missing);
231 }
232 tensor
233 .get2(row, 0)
234 .map(number_group_atom)
235 .unwrap_or(GroupAtom::Missing)
236 }
237 Value::StringArray(array) => array
238 .data
239 .get(row)
240 .cloned()
241 .map(text_group_atom)
242 .unwrap_or(GroupAtom::Missing),
243 Value::LogicalArray(array) => array
244 .data
245 .get(row)
246 .map(|value| GroupAtom::Logical(*value != 0))
247 .unwrap_or(GroupAtom::Missing),
248 Value::Object(obj) if obj.is_class("datetime") => {
249 crate::builtins::datetime::serials_from_datetime_value(value)
250 .ok()
251 .and_then(|tensor| double_value_at(&tensor, row))
252 .map(number_group_atom)
253 .unwrap_or(GroupAtom::Missing)
254 }
255 Value::Object(obj) if obj.is_class("duration") => {
256 crate::builtins::duration::duration_tensor_from_duration_value(value)
257 .ok()
258 .and_then(|tensor| double_value_at(&tensor, row))
259 .map(number_group_atom)
260 .unwrap_or(GroupAtom::Missing)
261 }
262 Value::Object(obj) if obj.is_class("calendarDuration") => {
263 crate::builtins::datetime::calendar_duration_tensors_from_value(value)
264 .ok()
265 .and_then(|(months, days)| {
266 let months = double_value_at(&months, row)?;
267 let days = double_value_at(&days, row)?;
268 if months.is_nan() || days.is_nan() {
269 None
270 } else {
271 Some(text_group_atom(format!("{months}:{days}")))
272 }
273 })
274 .unwrap_or(GroupAtom::Missing)
275 }
276 Value::Object(obj) if obj.is_class(CATEGORICAL_CLASS) => {
277 match categorical_label_at(obj, row) {
278 Some(label) if label != "<undefined>" => text_group_atom(label),
279 _ => GroupAtom::Missing,
280 }
281 }
282 other => text_group_atom(cell_key_string(other, row)),
283 }
284}
285
286fn number_group_atom(value: f64) -> GroupAtom {
287 if value.is_nan() {
288 GroupAtom::Missing
289 } else {
290 GroupAtom::Number(value)
291 }
292}
293
294fn text_group_atom(value: String) -> GroupAtom {
295 if value.is_empty() {
296 GroupAtom::Missing
297 } else {
298 GroupAtom::Text(value)
299 }
300}
301
302pub(in crate::builtins::table) fn pivot_impl(
303 table: Value,
304 rowvars: Value,
305 colvars: Value,
306 datavar: Value,
307 method: &str,
308) -> BuiltinResult<Value> {
309 let object = into_table_object(table, "pivot")?;
310 let names = table_variable_names_from_object(&object)?;
311 let row_names = parse_variable_selector_for_object(Some(&rowvars), &object, &names)?;
312 let col_names = parse_variable_selector_for_object(Some(&colvars), &object, &names)?;
313 let data_names = parse_variable_selector_for_object(Some(&datavar), &object, &names)?;
314 if row_names.is_empty() || col_names.is_empty() || data_names.is_empty() {
315 return Err(invalid_argument(
316 "pivot: rowvars, colvars, and datavar must select at least one variable",
317 ));
318 }
319 if data_names.len() != 1 {
320 return Err(invalid_argument(
321 "pivot: exactly one data variable is currently supported",
322 ));
323 }
324 let data_name = &data_names[0];
325 let variables = table_variables(&object)?;
326 let data_value = variables
327 .fields
328 .get(data_name)
329 .ok_or_else(|| invalid_variable(format!("pivot: missing data variable '{data_name}'")))?;
330 if !matches!(data_value, Value::Tensor(tensor) if tensor.cols() == 1) {
331 return Err(invalid_variable(
332 "pivot: data variable must be a numeric column vector",
333 ));
334 }
335
336 let height = table_height(&object)?;
337 let mut row_order = Vec::<Vec<GroupAtom>>::new();
338 let mut row_first_index = BTreeMap::<Vec<GroupAtom>, usize>::new();
339 let mut col_order = Vec::<Vec<GroupAtom>>::new();
340 let mut col_seen = BTreeMap::<Vec<GroupAtom>, ()>::new();
341 let mut buckets = BTreeMap::<(Vec<GroupAtom>, Vec<GroupAtom>), Vec<usize>>::new();
342 for row in 0..height {
343 let row_key = group_key_for_row(&variables, &row_names, row);
344 let col_key = group_key_for_row(&variables, &col_names, row);
345 if !row_first_index.contains_key(&row_key) {
346 row_first_index.insert(row_key.clone(), row);
347 row_order.push(row_key.clone());
348 }
349 if !col_seen.contains_key(&col_key) {
350 col_seen.insert(col_key.clone(), ());
351 col_order.push(col_key.clone());
352 }
353 buckets.entry((row_key, col_key)).or_default().push(row);
354 }
355
356 let mut out_names = row_names.clone();
357 let mut out_columns = Vec::with_capacity(row_names.len() + col_order.len());
358 for name in &row_names {
359 let value = variables
360 .fields
361 .get(name)
362 .ok_or_else(|| invalid_variable(format!("pivot: missing row variable '{name}'")))?;
363 let rows = row_order
364 .iter()
365 .filter_map(|key| row_first_index.get(key).copied())
366 .collect::<Vec<_>>();
367 out_columns.push(select_rows(value, &rows)?);
368 }
369 for col_key in &col_order {
370 let mut values = Vec::with_capacity(row_order.len());
371 for row_key in &row_order {
372 let summary_rows = buckets
373 .get(&(row_key.clone(), col_key.clone()))
374 .cloned()
375 .unwrap_or_default();
376 if summary_rows.is_empty() {
377 values.push(f64::NAN);
378 } else {
379 values.push(
380 summarize_groups(data_value, std::iter::once(&summary_rows), method)?
381 .into_iter()
382 .next()
383 .unwrap_or(f64::NAN),
384 );
385 }
386 }
387 out_names.push(format!(
388 "{}_{}",
389 make_valid_variable_name(&group_key_label(col_key), out_names.len() + 1),
390 data_name
391 ));
392 out_columns.push(Value::Tensor(
393 Tensor::new(values, vec![row_order.len(), 1]).map_err(invalid_variable)?,
394 ));
395 }
396 let out_names = make_unique_variable_names(out_names);
397 table_from_columns(out_names, out_columns)
398}
399
400pub(in crate::builtins::table) fn group_key_for_row(
401 variables: &StructValue,
402 names: &[String],
403 row: usize,
404) -> Vec<GroupAtom> {
405 names
406 .iter()
407 .map(|name| {
408 variables
409 .fields
410 .get(name)
411 .map(|value| cell_group_atom(value, row))
412 .unwrap_or(GroupAtom::Missing)
413 })
414 .collect()
415}
416
417pub(in crate::builtins::table) fn group_key_label(key: &[GroupAtom]) -> String {
418 if key.is_empty() {
419 return "missing".to_string();
420 }
421 key.iter()
422 .map(group_atom_label)
423 .collect::<Vec<_>>()
424 .join("_")
425}
426
427pub(in crate::builtins::table) fn group_atom_label(atom: &GroupAtom) -> String {
428 match atom {
429 GroupAtom::Number(value) => format_key_number(*value),
430 GroupAtom::Integer(value) => format_integer_key(value),
431 GroupAtom::Text(text) => text.clone(),
432 GroupAtom::Logical(flag) => flag.to_string(),
433 GroupAtom::Missing => "missing".to_string(),
434 }
435}
436
437fn format_integer_key(value: &IntValue) -> String {
438 match value {
439 IntValue::I8(value) => value.to_string(),
440 IntValue::I16(value) => value.to_string(),
441 IntValue::I32(value) => value.to_string(),
442 IntValue::I64(value) => value.to_string(),
443 IntValue::U8(value) => value.to_string(),
444 IntValue::U16(value) => value.to_string(),
445 IntValue::U32(value) => value.to_string(),
446 IntValue::U64(value) => value.to_string(),
447 }
448}
449
450pub(in crate::builtins::table) fn groupsummary_impl(
451 table: Value,
452 groupvars: Value,
453 method: Value,
454 rest: Vec<Value>,
455) -> BuiltinResult<Value> {
456 let object = into_table_object(table, "groupsummary")?;
457 let names = table_variable_names_from_object(&object)?;
458 let group_names = parse_variable_selector_for_object(Some(&groupvars), &object, &names)?;
459 let methods = string_list(&method)?;
460 if methods.is_empty() {
461 return Err(invalid_argument(
462 "groupsummary: method list must not be empty",
463 ));
464 }
465 let mut include_missing = true;
466 let mut rest_index = 0usize;
467 let data_selector = rest.first().filter(|value| {
468 scalar_text(value, "groupsummary argument")
469 .map(|name| {
470 !name.eq_ignore_ascii_case("IncludeMissingGroups")
471 && !name.eq_ignore_ascii_case("IncludeEmptyGroups")
472 })
473 .unwrap_or(true)
474 });
475 if data_selector.is_some() {
476 rest_index = 1;
477 }
478 while rest_index < rest.len() {
479 if rest_index + 1 >= rest.len() {
480 return Err(invalid_argument(
481 "groupsummary: name-value options must be provided in pairs",
482 ));
483 }
484 let name = scalar_text(&rest[rest_index], "groupsummary option name")?;
485 let value = &rest[rest_index + 1];
486 if name.eq_ignore_ascii_case("IncludeMissingGroups") {
487 include_missing = zero_one_bool_scalar(value, "IncludeMissingGroups")?;
488 } else if name.eq_ignore_ascii_case("IncludeEmptyGroups") {
489 if zero_one_bool_scalar(value, "IncludeEmptyGroups")? {
490 return Err(invalid_argument(
491 "groupsummary: IncludeEmptyGroups=true is not supported until categorical level expansion is implemented",
492 ));
493 }
494 } else {
495 return Err(invalid_argument(format!(
496 "groupsummary: unsupported option '{name}'"
497 )));
498 }
499 rest_index += 2;
500 }
501 let data_names = if let Some(value) = data_selector {
502 parse_variable_selector_for_object(Some(value), &object, &names)?
503 } else {
504 names
505 .iter()
506 .filter(|name| !group_names.contains(name))
507 .filter(|name| {
508 table_variables(&object)
509 .ok()
510 .and_then(|vars| vars.fields.get(*name).cloned())
511 .map(|value| matches!(value, Value::Tensor(_)))
512 .unwrap_or(false)
513 })
514 .cloned()
515 .collect()
516 };
517 let variables = table_variables(&object)?;
518 let height = table_height(&object)?;
519 let mut groups: BTreeMap<Vec<GroupAtom>, Vec<usize>> = BTreeMap::new();
520 for row in 0..height {
521 let key = group_names
522 .iter()
523 .map(|name| {
524 variables
525 .fields
526 .get(name)
527 .map(|value| cell_group_atom(value, row))
528 .unwrap_or(GroupAtom::Missing)
529 })
530 .collect::<Vec<_>>();
531 if include_missing || !key.iter().any(group_atom_is_missing) {
532 groups.entry(key).or_default().push(row);
533 }
534 }
535 let group_rows = groups
536 .values()
537 .filter_map(|rows| rows.first().copied())
538 .collect::<Vec<_>>();
539 let mut out_names = Vec::new();
540 let mut out_columns = Vec::new();
541 for name in &group_names {
542 let value = variables.fields.get(name).ok_or_else(|| {
543 invalid_variable(format!("groupsummary: missing group variable '{name}'"))
544 })?;
545 out_names.push(name.clone());
546 out_columns.push(select_rows(value, &group_rows)?);
547 }
548 out_names.push("GroupCount".to_string());
549 out_columns.push(Value::Tensor(
550 Tensor::new(
551 groups.values().map(|rows| rows.len() as f64).collect(),
552 vec![groups.len(), 1],
553 )
554 .map_err(invalid_variable)?,
555 ));
556 let grouped_rows = groups.values().collect::<Vec<_>>();
557 for method in &methods {
558 for name in &data_names {
559 let value = variables.fields.get(name).ok_or_else(|| {
560 invalid_variable(format!("groupsummary: missing data variable '{name}'"))
561 })?;
562 let summary = summarize_groups_value(value, &grouped_rows, method)?;
563 out_names.push(format!("{}_{}", method.to_ascii_lowercase(), name));
564 out_columns.push(summary);
565 }
566 }
567 table_from_columns(out_names, out_columns)
568}
569
570fn summarize_groups_value(
571 value: &Value,
572 groups: &[&Vec<usize>],
573 method: &str,
574) -> BuiltinResult<Value> {
575 let Value::Tensor(tensor) = value else {
576 return Err(invalid_variable(
577 "groupsummary: summary data variables must be numeric column vectors",
578 ));
579 };
580 if tensor.cols() != 1 {
581 return Err(invalid_variable(
582 "groupsummary: summary data variables must be numeric column vectors",
583 ));
584 }
585 let Some(storage) = tensor.integer_storage() else {
586 let values = summarize_groups(value, groups.iter().copied(), method)?;
587 return Tensor::new(values, vec![groups.len(), 1])
588 .map(Value::Tensor)
589 .map_err(invalid_variable);
590 };
591 let method = method.to_ascii_lowercase();
592 if method == "min" || method == "max" {
593 let mut extrema = Vec::with_capacity(groups.len());
594 for rows in groups {
595 let mut values = rows.iter().map(|row| {
596 storage
597 .value_at(*row)
598 .ok_or_else(|| invalid_index("groupsummary: integer row out of bounds"))
599 });
600 let mut selected = values.next().transpose()?.ok_or_else(|| {
601 invalid_argument("groupsummary: observed integer groups cannot be empty")
602 })?;
603 for value in values {
604 let value = value?;
605 let ordering = compare_integer_values(&value, &selected);
606 if (method == "min" && ordering == Ordering::Less)
607 || (method == "max" && ordering == Ordering::Greater)
608 {
609 selected = value;
610 }
611 }
612 extrema.push(selected);
613 }
614 let output = storage
615 .from_exact_values_like(extrema)
616 .map_err(invalid_variable)?;
617 return Tensor::new_integer(output, vec![groups.len(), 1])
618 .map(Value::Tensor)
619 .map_err(invalid_variable);
620 }
621 if method == "count" || method == "numel" {
622 return Tensor::new(
623 groups.iter().map(|rows| rows.len() as f64).collect(),
624 vec![groups.len(), 1],
625 )
626 .map(Value::Tensor)
627 .map_err(invalid_variable);
628 }
629 let mut output = Vec::with_capacity(groups.len());
630 for rows in groups {
631 let mut values = rows
632 .iter()
633 .map(|row| {
634 let value = storage
635 .value_at(*row)
636 .ok_or_else(|| invalid_index("groupsummary: integer row out of bounds"))?;
637 if !crate::builtins::math::trigonometry::cos::integer_is_exact_f64(&value) {
638 return Err(invalid_argument(
639 "groupsummary: integer data must be exactly representable as double for floating summary methods",
640 ));
641 }
642 Ok(value.to_f64())
643 })
644 .collect::<BuiltinResult<Vec<_>>>()?;
645 let value = match method.as_str() {
646 "mean" => values.iter().sum::<f64>() / values.len() as f64,
647 "sum" => values.iter().sum(),
648 "median" => {
649 values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
650 let mid = values.len() / 2;
651 if values.len().is_multiple_of(2) {
652 (values[mid - 1] + values[mid]) / 2.0
653 } else {
654 values[mid]
655 }
656 }
657 other => {
658 return Err(invalid_argument(format!(
659 "groupsummary: unsupported method '{other}'"
660 )))
661 }
662 };
663 output.push(value);
664 }
665 Tensor::new(output, vec![groups.len(), 1])
666 .map(Value::Tensor)
667 .map_err(invalid_variable)
668}
669
670pub(in crate::builtins::table) fn summarize_groups<'a>(
671 value: &Value,
672 groups: impl Iterator<Item = &'a Vec<usize>>,
673 method: &str,
674) -> BuiltinResult<Vec<f64>> {
675 let tensor = match value {
676 Value::Tensor(tensor) if tensor.cols() == 1 => tensor,
677 _ => {
678 return Err(invalid_variable(
679 "groupsummary: summary data variables must be numeric column vectors",
680 ))
681 }
682 };
683 groups
684 .map(|rows| {
685 let mut values = rows
686 .iter()
687 .map(|row| tensor.get2(*row, 0).map_err(invalid_index))
688 .collect::<BuiltinResult<Vec<_>>>()?;
689 values.retain(|value| !value.is_nan());
690 let result = match method.to_ascii_lowercase().as_str() {
691 "mean" => {
692 if values.is_empty() {
693 f64::NAN
694 } else {
695 values.iter().sum::<f64>() / values.len() as f64
696 }
697 }
698 "sum" => values.iter().sum(),
699 "min" => values.into_iter().fold(f64::INFINITY, f64::min),
700 "max" => values.into_iter().fold(f64::NEG_INFINITY, f64::max),
701 "median" => {
702 if values.is_empty() {
703 f64::NAN
704 } else {
705 values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
706 let mid = values.len() / 2;
707 if values.len() % 2 == 0 {
708 (values[mid - 1] + values[mid]) / 2.0
709 } else {
710 values[mid]
711 }
712 }
713 }
714 "count" | "numel" => values.len() as f64,
715 other => {
716 return Err(invalid_argument(format!(
717 "groupsummary: unsupported method '{other}'"
718 )))
719 }
720 };
721 Ok(result)
722 })
723 .collect()
724}
725
726pub(in crate::builtins::table) fn cell_key_string(value: &Value, row: usize) -> String {
727 match value {
728 Value::Tensor(tensor) => {
729 if let Some(storage) = tensor.integer_storage() {
730 return storage
731 .value_at(row)
732 .map(|value| format_integer_key(&value))
733 .unwrap_or_default();
734 }
735 tensor
736 .get2(row, 0)
737 .map(format_key_number)
738 .unwrap_or_default()
739 }
740 Value::StringArray(array) => array.data.get(row).cloned().unwrap_or_default(),
741 Value::LogicalArray(array) => array
742 .data
743 .get(row)
744 .map(|value| value.to_string())
745 .unwrap_or_default(),
746 Value::Object(obj) if obj.is_class("datetime") => {
747 crate::builtins::datetime::serials_from_datetime_value(value)
748 .ok()
749 .and_then(|tensor| double_value_at(&tensor, row))
750 .map(format_key_number)
751 .unwrap_or_default()
752 }
753 Value::Object(obj) if obj.is_class("duration") => {
754 crate::builtins::duration::duration_tensor_from_duration_value(value)
755 .ok()
756 .and_then(|tensor| double_value_at(&tensor, row))
757 .map(format_key_number)
758 .unwrap_or_default()
759 }
760 Value::Object(obj) if obj.is_class(CATEGORICAL_CLASS) => {
761 categorical_label_at(obj, row).unwrap_or_default()
762 }
763 Value::Cell(cell) => cell
764 .get(row, 0)
765 .map(|item| cell_to_text(&item))
766 .unwrap_or_default(),
767 other => format!("{other}"),
768 }
769}
770
771fn double_value_at(tensor: &Tensor, index: usize) -> Option<f64> {
772 tensor.as_f64_slice()?.get(index).copied()
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778 use runmat_value::IntegerStorage;
779
780 #[test]
781 fn typed_integer_group_atoms_and_table_ordering_remain_exact() {
782 let large = 9_007_199_254_740_992_u64;
783 let value = Value::Tensor(
784 Tensor::new_integer(IntegerStorage::U64(vec![large, large + 1]), vec![2, 1]).unwrap(),
785 );
786
787 let first = cell_group_atom(&value, 0);
788 let second = cell_group_atom(&value, 1);
789 assert_ne!(first, second);
790 assert_eq!(compare_table_cells(&value, 0, 1).unwrap(), Ordering::Less);
791 assert_eq!(group_atom_label(&second), (large + 1).to_string());
792 }
793
794 #[test]
795 fn cell_key_string_reads_typed_integer_storage_exactly() {
796 let large = 9_007_199_254_740_993_u64;
797 let tensor = Tensor::new_integer(IntegerStorage::U64(vec![large]), vec![1, 1]).unwrap();
798
799 assert_eq!(
800 cell_key_string(&Value::Tensor(tensor), 0),
801 "9007199254740993"
802 );
803 }
804}