1use std::cmp::Ordering;
4use std::collections::BTreeMap;
5
6use runmat_builtins::{
7 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
8 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
9 CellArray, LogicalArray, ObjectInstance, SparseTensor, StringArray, Tensor, Value,
10};
11use runmat_macros::runtime_builtin;
12
13use crate::builtins::table::{
14 categorical_label_at, is_tabular_object, select_rows, table_from_columns, table_height,
15 table_variable_names_from_object, table_variables, value_row_count,
16};
17use crate::{
18 build_runtime_error, call_feval_async_with_outputs, gather_if_needed_async, BuiltinResult,
19 RuntimeError,
20};
21
22const MAX_MATERIALIZED_ELEMENTS: usize = 50_000_000;
23
24const OUTPUT_ANY: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
25 name: "varargout",
26 ty: BuiltinParamType::Any,
27 arity: BuiltinParamArity::Variadic,
28 default: None,
29 description: "Builtin outputs.",
30}];
31
32const INPUT_VARIADIC: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
33 name: "args",
34 ty: BuiltinParamType::Any,
35 arity: BuiltinParamArity::Variadic,
36 default: None,
37 description: "MATLAB-compatible arguments.",
38}];
39
40const SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
41 label: "varargout = groupingBuiltin(args...)",
42 inputs: &INPUT_VARIADIC,
43 outputs: &OUTPUT_ANY,
44}];
45
46const ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
47 code: "RM.GROUPING.INVALID_INPUT",
48 identifier: Some("RunMat:grouping:InvalidInput"),
49 when: "Inputs are malformed, have incompatible lengths, or request unsupported grouped output.",
50 message: "grouping builtin: invalid input",
51};
52
53const ERROR_CALLBACK: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
54 code: "RM.GROUPING.CALLBACK",
55 identifier: Some("RunMat:grouping:CallbackFailed"),
56 when: "A grouped callback fails or returns incompatible outputs.",
57 message: "grouping builtin: callback failed",
58};
59
60const ERROR_TOO_LARGE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
61 code: "RM.GROUPING.TOO_LARGE",
62 identifier: Some("RunMat:grouping:TooLarge"),
63 when: "The requested dense or combinatorial output exceeds RunMat's materialization limit.",
64 message: "grouping builtin: output is too large",
65};
66
67const ERRORS: [BuiltinErrorDescriptor; 3] = [ERROR_INVALID_INPUT, ERROR_CALLBACK, ERROR_TOO_LARGE];
68
69pub const GROUPING_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
70 signatures: &SIGNATURES,
71 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
72 completion_policy: BuiltinCompletionPolicy::Public,
73 errors: &ERRORS,
74};
75
76#[derive(Clone, Debug)]
77enum Atom {
78 Missing,
79 Logical(bool),
80 Number(f64),
81 Text(String),
82}
83
84impl Atom {
85 fn rank(&self) -> u8 {
86 match self {
87 Self::Missing => 0,
88 Self::Logical(_) => 1,
89 Self::Number(_) => 2,
90 Self::Text(_) => 3,
91 }
92 }
93
94 fn label(&self) -> String {
95 match self {
96 Self::Missing => "<missing>".to_string(),
97 Self::Logical(flag) => {
98 if *flag {
99 "true".to_string()
100 } else {
101 "false".to_string()
102 }
103 }
104 Self::Number(value) => format_key_number(*value),
105 Self::Text(text) => text.clone(),
106 }
107 }
108}
109
110impl PartialEq for Atom {
111 fn eq(&self, other: &Self) -> bool {
112 self.cmp(other) == Ordering::Equal
113 }
114}
115
116impl Eq for Atom {}
117
118impl PartialOrd for Atom {
119 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
120 Some(self.cmp(other))
121 }
122}
123
124impl Ord for Atom {
125 fn cmp(&self, other: &Self) -> Ordering {
126 let rank = self.rank().cmp(&other.rank());
127 if rank != Ordering::Equal {
128 return rank;
129 }
130 match (self, other) {
131 (Self::Missing, Self::Missing) => Ordering::Equal,
132 (Self::Logical(a), Self::Logical(b)) => a.cmp(b),
133 (Self::Number(a), Self::Number(b)) => a.total_cmp(b),
134 (Self::Text(a), Self::Text(b)) => a.cmp(b),
135 _ => Ordering::Equal,
136 }
137 }
138}
139
140#[derive(Clone)]
141struct GroupColumn {
142 name: String,
143 value: Value,
144 rows: usize,
145}
146
147struct Grouping {
148 ids: Vec<f64>,
149 keys: Vec<Vec<Atom>>,
150 first_rows: Vec<usize>,
151 row_groups: Vec<Vec<usize>>,
152}
153
154#[derive(Clone, Copy, Debug)]
155struct GroupOptions {
156 include_missing: bool,
157}
158
159impl GroupOptions {
160 fn parse(args: &[Value], context: &str) -> BuiltinResult<Self> {
161 let mut include_missing = false;
162 let mut idx = 0usize;
163 while idx < args.len() {
164 if idx + 1 >= args.len() {
165 return Err(grouping_error(format!(
166 "{context}: name-value options must be provided in pairs"
167 )));
168 }
169 let name = scalar_text(&args[idx], context)?;
170 if name.eq_ignore_ascii_case("IncludeMissingGroups") {
171 include_missing = bool_scalar(&args[idx + 1], "IncludeMissingGroups")?;
172 } else if name.eq_ignore_ascii_case("IncludeEmptyGroups") {
173 let include_empty = bool_scalar(&args[idx + 1], "IncludeEmptyGroups")?;
174 if include_empty {
175 return Err(grouping_error(format!(
176 "{context}: IncludeEmptyGroups=true is not supported until categorical level expansion is implemented"
177 )));
178 }
179 } else {
180 return Err(grouping_error(format!(
181 "{context}: unsupported option '{name}'"
182 )));
183 }
184 idx += 2;
185 }
186 Ok(Self { include_missing })
187 }
188}
189
190fn grouping_error(message: impl Into<String>) -> RuntimeError {
191 let mut builder = build_runtime_error(message).with_builtin("array_grouping");
192 if let Some(identifier) = ERROR_INVALID_INPUT.identifier {
193 builder = builder.with_identifier(identifier);
194 }
195 builder.build()
196}
197
198fn callback_error(message: impl Into<String>, source: Option<RuntimeError>) -> RuntimeError {
199 let mut builder = build_runtime_error(message).with_builtin("array_grouping");
200 if let Some(identifier) = ERROR_CALLBACK.identifier {
201 builder = builder.with_identifier(identifier);
202 }
203 if let Some(source) = source {
204 builder = builder.with_source(source);
205 }
206 builder.build()
207}
208
209fn too_large_error(message: impl Into<String>) -> RuntimeError {
210 let mut builder = build_runtime_error(message).with_builtin("array_grouping");
211 if let Some(identifier) = ERROR_TOO_LARGE.identifier {
212 builder = builder.with_identifier(identifier);
213 }
214 builder.build()
215}
216
217#[runtime_builtin(
218 name = "findgroups",
219 category = "array/grouping",
220 summary = "Find groups and return group numbers.",
221 keywords = "findgroups,groups,grouping,table,categorical",
222 accel = "cpu",
223 descriptor(crate::builtins::array::grouping::GROUPING_DESCRIPTOR),
224 builtin_path = "crate::builtins::array::grouping"
225)]
226pub(crate) async fn findgroups_builtin(first: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
227 let mut args = Vec::with_capacity(rest.len() + 1);
228 args.push(gather_if_needed_async(&first).await?);
229 for value in rest {
230 args.push(gather_if_needed_async(&value).await?);
231 }
232 let (columns, table_mode) = findgroups_columns(args)?;
233 let grouping = build_grouping(&columns)?;
234 let outputs = findgroups_outputs(&columns, &grouping, table_mode)?;
235 multi_output(outputs)
236}
237
238#[runtime_builtin(
239 name = "grp2idx",
240 category = "array/grouping",
241 summary = "Create an index vector from a grouping variable.",
242 keywords = "grp2idx,groups,index,categorical,statistics",
243 accel = "cpu",
244 descriptor(crate::builtins::array::grouping::GROUPING_DESCRIPTOR),
245 builtin_path = "crate::builtins::array::grouping"
246)]
247pub(crate) async fn grp2idx_builtin(value: Value) -> BuiltinResult<Value> {
248 let value = gather_if_needed_async(&value).await?;
249 let columns = columns_from_group_value("G", value, true)?;
250 if columns.len() != 1 {
251 return Err(grouping_error(
252 "grp2idx: expected one grouping vector, not a matrix of grouping columns",
253 ));
254 }
255 let grouping = build_grouping(&columns)?;
256 let g = Tensor::new(grouping.ids.clone(), vec![grouping.ids.len(), 1])
257 .map(Value::Tensor)
258 .map_err(grouping_error)?;
259 let names = grouping
260 .keys
261 .iter()
262 .map(|key| key.first().map(Atom::label).unwrap_or_default())
263 .collect::<Vec<_>>();
264 let gn = Value::StringArray(
265 StringArray::new(names.clone(), vec![names.len(), 1]).map_err(grouping_error)?,
266 );
267 let gl = Value::StringArray(
268 StringArray::new(names, vec![grouping.keys.len(), 1]).map_err(grouping_error)?,
269 );
270 multi_output(vec![g, gn, gl])
271}
272
273#[runtime_builtin(
274 name = "groupcounts",
275 category = "array/grouping",
276 summary = "Count the number of elements in each group.",
277 keywords = "groupcounts,groups,count,table,categorical",
278 accel = "cpu",
279 descriptor(crate::builtins::array::grouping::GROUPING_DESCRIPTOR),
280 builtin_path = "crate::builtins::array::grouping"
281)]
282pub(crate) async fn groupcounts_builtin(first: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
283 let first = gather_if_needed_async(&first).await?;
284 let rest = gather_values(rest).await?;
285 if let Value::Object(object) = first.clone() {
286 if is_tabular_object(&object) {
287 let (selector_args, option_args) = split_option_tail(rest)?;
288 return groupcounts_table(object, selector_args, option_args);
289 }
290 }
291 let (data_args, option_args) = split_option_tail(rest)?;
292 let options = GroupOptions::parse(&option_args, "groupcounts")?;
293 let columns = if matches!(first, Value::Cell(_)) {
294 columns_from_group_value("A", first, true)?
295 } else {
296 let mut args = vec![first];
297 args.extend(data_args);
298 columns_from_group_args(args)?
299 };
300 let grouping = build_grouping_with_options(&columns, options)?;
301 let counts = grouping
302 .row_groups
303 .iter()
304 .map(|rows| rows.len() as f64)
305 .collect::<Vec<_>>();
306 let b =
307 Value::Tensor(Tensor::new(counts, vec![grouping.keys.len(), 1]).map_err(grouping_error)?);
308 let bg = group_label_outputs(&columns, &grouping)?;
309 let bp = Value::Tensor(
310 Tensor::new(
311 grouping
312 .row_groups
313 .iter()
314 .map(|rows| {
315 if grouping.ids.is_empty() {
316 0.0
317 } else {
318 rows.len() as f64 * 100.0 / grouping.ids.len() as f64
319 }
320 })
321 .collect(),
322 vec![grouping.keys.len(), 1],
323 )
324 .map_err(grouping_error)?,
325 );
326 let mut outputs = vec![b];
327 outputs.extend(bg);
328 outputs.push(bp);
329 multi_output(outputs)
330}
331
332#[runtime_builtin(
333 name = "splitapply",
334 category = "array/grouping",
335 summary = "Split data into groups and apply a function.",
336 keywords = "splitapply,groups,apply,function,table",
337 accel = "cpu",
338 descriptor(crate::builtins::array::grouping::GROUPING_DESCRIPTOR),
339 builtin_path = "crate::builtins::array::grouping"
340)]
341pub(crate) async fn splitapply_builtin(
342 func: Value,
343 first_data: Value,
344 rest: Vec<Value>,
345) -> BuiltinResult<Value> {
346 let func = gather_if_needed_async(&func).await?;
347 let first_data = gather_if_needed_async(&first_data).await?;
348 let rest = gather_values(rest).await?;
349 if rest.is_empty() {
350 return Err(grouping_error(
351 "splitapply: expected data arguments followed by group numbers",
352 ));
353 }
354 let (group_value, data_tail) = rest.split_last().expect("checked non-empty");
355 let mut data_values = Vec::with_capacity(data_tail.len() + 1);
356 data_values.push(first_data);
357 data_values.extend_from_slice(data_tail);
358 let group_columns = columns_from_group_value("G", group_value.clone(), true)?;
359 if group_columns.len() != 1 {
360 return Err(grouping_error("splitapply: G must be a grouping vector"));
361 }
362 let grouping = build_grouping(&group_columns)?;
363 let expected_rows = grouping.ids.len();
364 for value in &data_values {
365 let rows = value_row_count(value)?;
366 if rows != expected_rows {
367 return Err(grouping_error(format!(
368 "splitapply: data arguments must have {expected_rows} rows to match G, got {rows}"
369 )));
370 }
371 }
372 let requested_outputs = crate::output_count::current_output_count()
373 .unwrap_or(1)
374 .max(1);
375 let mut collectors = (0..requested_outputs)
376 .map(|_| Vec::<Value>::new())
377 .collect::<Vec<_>>();
378 for rows in &grouping.row_groups {
379 let mut callback_args = Vec::with_capacity(data_values.len());
380 for value in &data_values {
381 callback_args.push(select_rows(value, rows)?);
382 }
383 let result = call_feval_async_with_outputs(func.clone(), &callback_args, requested_outputs)
384 .await
385 .map_err(|err| callback_error("splitapply: callback failed", Some(err)))?;
386 let outputs = normalize_outputs(result, requested_outputs, "splitapply")?;
387 for (collector, output) in collectors.iter_mut().zip(outputs) {
388 collector.push(gather_if_needed_async(&output).await?);
389 }
390 }
391 let outputs = collectors
392 .into_iter()
393 .map(|values| collect_group_results(values, grouping.keys.len(), "splitapply"))
394 .collect::<BuiltinResult<Vec<_>>>()?;
395 multi_output(outputs)
396}
397
398#[runtime_builtin(
399 name = "accumarray",
400 category = "array/grouping",
401 summary = "Accumulate values into an array by subscript groups.",
402 keywords = "accumarray,accumulate,groups,sum,sparse",
403 accel = "cpu",
404 descriptor(crate::builtins::array::grouping::GROUPING_DESCRIPTOR),
405 builtin_path = "crate::builtins::array::grouping"
406)]
407pub(crate) async fn accumarray_builtin(
408 subs: Value,
409 data: Value,
410 rest: Vec<Value>,
411) -> BuiltinResult<Value> {
412 let subs = gather_if_needed_async(&subs).await?;
413 let data = gather_if_needed_async(&data).await?;
414 let rest = gather_values(rest).await?;
415 accumarray_impl(subs, data, rest).await
416}
417
418#[runtime_builtin(
419 name = "discretize",
420 category = "array/grouping",
421 summary = "Group numeric data into bins.",
422 keywords = "discretize,bins,edges,categorical,grouping",
423 accel = "cpu",
424 descriptor(crate::builtins::array::grouping::GROUPING_DESCRIPTOR),
425 builtin_path = "crate::builtins::array::grouping"
426)]
427pub(crate) async fn discretize_builtin(
428 x: Value,
429 edges_or_n: Value,
430 rest: Vec<Value>,
431) -> BuiltinResult<Value> {
432 let x = gather_if_needed_async(&x).await?;
433 let edges_or_n = gather_if_needed_async(&edges_or_n).await?;
434 let rest = gather_values(rest).await?;
435 discretize_impl(x, edges_or_n, rest)
436}
437
438#[runtime_builtin(
439 name = "combinations",
440 category = "array/grouping",
441 summary = "Generate all element combinations of arrays.",
442 keywords = "combinations,cartesian,table,combinatorics",
443 accel = "cpu",
444 descriptor(crate::builtins::array::grouping::GROUPING_DESCRIPTOR),
445 builtin_path = "crate::builtins::array::grouping"
446)]
447pub(crate) async fn combinations_builtin(first: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
448 let first = gather_if_needed_async(&first).await?;
449 let rest = gather_values(rest).await?;
450 combinations_impl(first, rest)
451}
452
453fn findgroups_columns(args: Vec<Value>) -> BuiltinResult<(Vec<GroupColumn>, bool)> {
454 if args.is_empty() {
455 return Err(grouping_error("findgroups: expected at least one input"));
456 }
457 if let Value::Object(object) = args[0].clone() {
458 if is_tabular_object(&object) {
459 let names = table_variable_names_from_object(&object)?;
460 let selected = if let Some(selector) = args.get(1) {
461 parse_name_selector(selector, &names, "findgroups")?
462 } else {
463 names
464 };
465 let variables = table_variables(&object)?;
466 let mut columns = Vec::with_capacity(selected.len());
467 for name in selected {
468 let value = variables.fields.get(&name).cloned().ok_or_else(|| {
469 grouping_error(format!("findgroups: missing table variable '{name}'"))
470 })?;
471 columns.push(GroupColumn {
472 rows: value_row_count(&value)?,
473 name,
474 value,
475 });
476 }
477 return Ok((columns, true));
478 }
479 }
480 Ok((columns_from_group_args(args)?, false))
481}
482
483fn columns_from_group_args(args: Vec<Value>) -> BuiltinResult<Vec<GroupColumn>> {
484 let mut columns = Vec::new();
485 for (idx, value) in args.into_iter().enumerate() {
486 columns.extend(columns_from_group_value(
487 &format!("Var{}", idx + 1),
488 value,
489 true,
490 )?);
491 }
492 Ok(columns)
493}
494
495fn columns_from_group_value(
496 base_name: &str,
497 value: Value,
498 split_matrix: bool,
499) -> BuiltinResult<Vec<GroupColumn>> {
500 match value {
501 Value::Tensor(tensor) => tensor_columns(base_name, tensor, split_matrix),
502 Value::LogicalArray(array) => logical_columns(base_name, array, split_matrix),
503 Value::StringArray(array) => string_columns(base_name, array, split_matrix),
504 Value::Cell(cell) if cell_is_group_vector_list(&cell) => {
505 let mut columns = Vec::with_capacity(cell.data.len());
506 for (idx, value) in cell.data.into_iter().enumerate() {
507 columns.extend(columns_from_group_value(
508 &format!("{base_name}{}", idx + 1),
509 value,
510 false,
511 )?);
512 }
513 Ok(columns)
514 }
515 Value::Cell(cell) => Ok(vec![GroupColumn {
516 rows: cell.rows.max(cell.cols).max(cell.data.len()),
517 name: base_name.to_string(),
518 value: Value::Cell(cell),
519 }]),
520 Value::Object(object) if object.is_class("categorical") => {
521 let rows = value_row_count(&Value::Object(object.clone()))?;
522 Ok(vec![GroupColumn {
523 rows,
524 name: base_name.to_string(),
525 value: Value::Object(object),
526 }])
527 }
528 other => {
529 let rows = value_row_count(&other).unwrap_or(1);
530 Ok(vec![GroupColumn {
531 rows,
532 name: base_name.to_string(),
533 value: other,
534 }])
535 }
536 }
537}
538
539fn tensor_columns(
540 base_name: &str,
541 tensor: Tensor,
542 split_matrix: bool,
543) -> BuiltinResult<Vec<GroupColumn>> {
544 if !split_matrix || tensor.cols() <= 1 || tensor.rows() == 1 {
545 let rows = tensor.data.len();
546 return Ok(vec![GroupColumn {
547 name: base_name.to_string(),
548 rows,
549 value: Value::Tensor(Tensor::new(tensor.data, vec![rows, 1]).map_err(grouping_error)?),
550 }]);
551 }
552 let mut out = Vec::with_capacity(tensor.cols());
553 for col in 0..tensor.cols() {
554 let mut data = Vec::with_capacity(tensor.rows());
555 for row in 0..tensor.rows() {
556 data.push(tensor.get2(row, col).map_err(grouping_error)?);
557 }
558 out.push(GroupColumn {
559 name: format!("{base_name}{col_plus}", col_plus = col + 1),
560 rows: tensor.rows(),
561 value: Value::Tensor(
562 Tensor::new(data, vec![tensor.rows(), 1]).map_err(grouping_error)?,
563 ),
564 });
565 }
566 Ok(out)
567}
568
569fn logical_columns(
570 base_name: &str,
571 array: LogicalArray,
572 split_matrix: bool,
573) -> BuiltinResult<Vec<GroupColumn>> {
574 let rows = array.shape.first().copied().unwrap_or(array.data.len());
575 let cols = array.shape.get(1).copied().unwrap_or(1);
576 if !split_matrix || cols <= 1 || rows == 1 {
577 let len = array.data.len();
578 return Ok(vec![GroupColumn {
579 name: base_name.to_string(),
580 rows: len,
581 value: Value::LogicalArray(
582 LogicalArray::new(array.data, vec![len, 1]).map_err(grouping_error)?,
583 ),
584 }]);
585 }
586 let mut out = Vec::with_capacity(cols);
587 for col in 0..cols {
588 let mut data = Vec::with_capacity(rows);
589 for row in 0..rows {
590 data.push(*array.data.get(row + col * rows).ok_or_else(|| {
591 grouping_error("grouping: logical array shape/data length mismatch")
592 })?);
593 }
594 out.push(GroupColumn {
595 name: format!("{base_name}{col_plus}", col_plus = col + 1),
596 rows,
597 value: Value::LogicalArray(
598 LogicalArray::new(data, vec![rows, 1]).map_err(grouping_error)?,
599 ),
600 });
601 }
602 Ok(out)
603}
604
605fn string_columns(
606 base_name: &str,
607 array: StringArray,
608 split_matrix: bool,
609) -> BuiltinResult<Vec<GroupColumn>> {
610 let rows = array.rows();
611 let cols = array.cols();
612 if !split_matrix || cols <= 1 || rows == 1 {
613 let len = array.data.len();
614 return Ok(vec![GroupColumn {
615 name: base_name.to_string(),
616 rows: len,
617 value: Value::StringArray(
618 StringArray::new(array.data, vec![len, 1]).map_err(grouping_error)?,
619 ),
620 }]);
621 }
622 let mut out = Vec::with_capacity(cols);
623 for col in 0..cols {
624 let mut data = Vec::with_capacity(rows);
625 for row in 0..rows {
626 data.push(array.data[row + col * rows].clone());
627 }
628 out.push(GroupColumn {
629 name: format!("{base_name}{col_plus}", col_plus = col + 1),
630 rows,
631 value: Value::StringArray(
632 StringArray::new(data, vec![rows, 1]).map_err(grouping_error)?,
633 ),
634 });
635 }
636 Ok(out)
637}
638
639fn cell_is_group_vector_list(cell: &CellArray) -> bool {
640 !cell.data.is_empty()
641 && cell.data.iter().all(|value| {
642 matches!(
643 value,
644 Value::Tensor(_)
645 | Value::LogicalArray(_)
646 | Value::StringArray(_)
647 | Value::Object(_)
648 | Value::Cell(_)
649 )
650 })
651}
652
653fn build_grouping(columns: &[GroupColumn]) -> BuiltinResult<Grouping> {
654 build_grouping_with_options(
655 columns,
656 GroupOptions {
657 include_missing: false,
658 },
659 )
660}
661
662fn build_grouping_with_options(
663 columns: &[GroupColumn],
664 options: GroupOptions,
665) -> BuiltinResult<Grouping> {
666 if columns.is_empty() {
667 return Err(grouping_error(
668 "grouping: expected at least one grouping variable",
669 ));
670 }
671 let rows = columns[0].rows;
672 for column in columns {
673 if column.rows != rows {
674 return Err(grouping_error(format!(
675 "grouping: grouping variables must have matching row counts ({} vs {})",
676 rows, column.rows
677 )));
678 }
679 }
680 let mut buckets = BTreeMap::<Vec<Atom>, Vec<usize>>::new();
681 let mut row_keys = Vec::with_capacity(rows);
682 for row in 0..rows {
683 let key = columns
684 .iter()
685 .map(|column| atom_at(&column.value, row))
686 .collect::<BuiltinResult<Vec<_>>>()?;
687 if !options.include_missing && key.iter().any(|atom| matches!(atom, Atom::Missing)) {
688 row_keys.push(None);
689 continue;
690 }
691 buckets.entry(key.clone()).or_default().push(row);
692 row_keys.push(Some(key));
693 }
694 let keys = buckets.keys().cloned().collect::<Vec<_>>();
695 let mut key_to_index = BTreeMap::<Vec<Atom>, usize>::new();
696 let mut first_rows = Vec::<usize>::with_capacity(keys.len());
697 let mut row_groups = Vec::<Vec<usize>>::with_capacity(keys.len());
698 for (idx, key) in keys.iter().enumerate() {
699 key_to_index.insert(key.clone(), idx);
700 let rows = buckets
701 .get(key)
702 .cloned()
703 .expect("key collected from buckets must exist");
704 first_rows.push(*rows.first().expect("nonmissing group has rows"));
705 row_groups.push(rows);
706 }
707 let ids = row_keys
708 .into_iter()
709 .map(|key| {
710 key.and_then(|key| key_to_index.get(&key).copied())
711 .map(|idx| idx as f64 + 1.0)
712 .unwrap_or(f64::NAN)
713 })
714 .collect();
715 Ok(Grouping {
716 ids,
717 keys,
718 first_rows,
719 row_groups,
720 })
721}
722
723fn atom_at(value: &Value, row: usize) -> BuiltinResult<Atom> {
724 match value {
725 Value::Tensor(tensor) => {
726 let value = *tensor
727 .data
728 .get(row)
729 .ok_or_else(|| grouping_error("grouping: numeric row out of bounds"))?;
730 if value.is_nan() {
731 Ok(Atom::Missing)
732 } else {
733 Ok(Atom::Number(value))
734 }
735 }
736 Value::LogicalArray(array) => Ok(array
737 .data
738 .get(row)
739 .map(|flag| Atom::Logical(*flag != 0))
740 .unwrap_or(Atom::Missing)),
741 Value::StringArray(array) => Ok(array
742 .data
743 .get(row)
744 .map(|text| {
745 if is_missing_text(text) {
746 Atom::Missing
747 } else {
748 Atom::Text(text.clone())
749 }
750 })
751 .unwrap_or(Atom::Missing)),
752 Value::Cell(cell) => cell
753 .data
754 .get(row)
755 .map(scalar_atom)
756 .unwrap_or(Ok(Atom::Missing)),
757 Value::Object(object) if object.is_class("categorical") => {
758 let label = categorical_label_at(object, row);
759 Ok(match label.as_deref() {
760 None | Some("<undefined>") | Some("") => Atom::Missing,
761 Some(text) => Atom::Text(text.to_string()),
762 })
763 }
764 Value::Object(object) if object.is_class("datetime") => {
765 let serials = crate::builtins::datetime::serials_from_datetime_value(value)?;
766 let value = serials.data.get(row).copied().unwrap_or(f64::NAN);
767 if value.is_nan() {
768 Ok(Atom::Missing)
769 } else {
770 Ok(Atom::Number(value))
771 }
772 }
773 Value::Object(object) if object.is_class("duration") => {
774 let tensor = crate::builtins::duration::duration_tensor_from_duration_value(value)?;
775 let value = tensor.data.get(row).copied().unwrap_or(f64::NAN);
776 if value.is_nan() {
777 Ok(Atom::Missing)
778 } else {
779 Ok(Atom::Number(value))
780 }
781 }
782 other if row == 0 => scalar_atom(other),
783 _ => Ok(Atom::Missing),
784 }
785}
786
787fn scalar_atom(value: &Value) -> BuiltinResult<Atom> {
788 match value {
789 Value::Num(value) => {
790 if value.is_nan() {
791 Ok(Atom::Missing)
792 } else {
793 Ok(Atom::Number(*value))
794 }
795 }
796 Value::Int(value) => Ok(Atom::Number(value.to_f64())),
797 Value::Bool(flag) => Ok(Atom::Logical(*flag)),
798 Value::String(text) => {
799 if is_missing_text(text) {
800 Ok(Atom::Missing)
801 } else {
802 Ok(Atom::Text(text.clone()))
803 }
804 }
805 Value::CharArray(chars) if chars.rows == 1 => Ok(Atom::Text(chars.data.iter().collect())),
806 other => Ok(Atom::Text(format!("{other}"))),
807 }
808}
809
810fn findgroups_outputs(
811 columns: &[GroupColumn],
812 grouping: &Grouping,
813 table_mode: bool,
814) -> BuiltinResult<Vec<Value>> {
815 let g = Value::Tensor(
816 Tensor::new(grouping.ids.clone(), vec![grouping.ids.len(), 1]).map_err(grouping_error)?,
817 );
818 let mut outputs = vec![g];
819 if table_mode {
820 let mut names = Vec::with_capacity(columns.len());
821 let mut values = Vec::with_capacity(columns.len());
822 for column in columns {
823 names.push(column.name.clone());
824 values.push(select_rows(&column.value, &grouping.first_rows)?);
825 }
826 outputs.push(table_from_columns(names, values)?);
827 } else {
828 outputs.extend(group_label_outputs(columns, grouping)?);
829 }
830 Ok(outputs)
831}
832
833fn group_label_outputs(columns: &[GroupColumn], grouping: &Grouping) -> BuiltinResult<Vec<Value>> {
834 columns
835 .iter()
836 .map(|column| select_rows(&column.value, &grouping.first_rows))
837 .collect()
838}
839
840fn split_option_tail(args: Vec<Value>) -> BuiltinResult<(Vec<Value>, Vec<Value>)> {
841 let mut option_start = args.len();
842 for (idx, value) in args.iter().enumerate() {
843 if is_option_name(value) {
844 option_start = idx;
845 break;
846 }
847 }
848 if option_start < args.len() && !(args.len() - option_start).is_multiple_of(2) {
849 return Err(grouping_error(
850 "groupcounts: name-value options must be provided in pairs",
851 ));
852 }
853 Ok((args[..option_start].to_vec(), args[option_start..].to_vec()))
854}
855
856fn groupcounts_table(
857 object: ObjectInstance,
858 selector_args: Vec<Value>,
859 option_args: Vec<Value>,
860) -> BuiltinResult<Value> {
861 let all_names = table_variable_names_from_object(&object)?;
862 if selector_args.len() > 1 {
863 return Err(grouping_error(
864 "groupcounts: table input accepts one grouping variable selector before options",
865 ));
866 }
867 let selector = selector_args.first().ok_or_else(|| {
868 grouping_error("groupcounts: table input requires a grouping variable selector")
869 })?;
870 let options = GroupOptions::parse(&option_args, "groupcounts")?;
871 let selected = parse_name_selector(selector, &all_names, "groupcounts")?;
872 let variables = table_variables(&object)?;
873 let height = table_height(&object)?;
874 let mut columns = Vec::with_capacity(selected.len());
875 for name in &selected {
876 let value = variables
877 .fields
878 .get(name)
879 .cloned()
880 .ok_or_else(|| grouping_error(format!("groupcounts: missing variable '{name}'")))?;
881 columns.push(GroupColumn {
882 name: name.clone(),
883 rows: value_row_count(&value)?,
884 value,
885 });
886 }
887 let grouping = build_grouping_with_options(&columns, options)?;
888 let mut out_names = selected.clone();
889 let mut out_columns = group_label_outputs(&columns, &grouping)?;
890 out_names.push("GroupCount".to_string());
891 out_columns.push(Value::Tensor(
892 Tensor::new(
893 grouping
894 .row_groups
895 .iter()
896 .map(|rows| rows.len() as f64)
897 .collect(),
898 vec![grouping.keys.len(), 1],
899 )
900 .map_err(grouping_error)?,
901 ));
902 out_names.push("Percent".to_string());
903 out_columns.push(Value::Tensor(
904 Tensor::new(
905 grouping
906 .row_groups
907 .iter()
908 .map(|rows| {
909 if height == 0 {
910 0.0
911 } else {
912 rows.len() as f64 * 100.0 / height as f64
913 }
914 })
915 .collect(),
916 vec![grouping.keys.len(), 1],
917 )
918 .map_err(grouping_error)?,
919 ));
920 table_from_columns(out_names, out_columns)
921}
922
923async fn accumarray_impl(subs: Value, data: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
924 let index_rows = accumarray_subscripts(subs)?;
925 let rows = index_rows.len();
926 let data_values = accumarray_data_values(data, rows)?;
927 let output_shape = if let Some(size_value) = rest.first() {
928 if is_empty_value(size_value) {
929 infer_accumarray_shape(&index_rows)
930 } else {
931 parse_size_vector(size_value, "accumarray")?
932 }
933 } else {
934 infer_accumarray_shape(&index_rows)
935 };
936 let fun = rest.get(1).filter(|value| !is_empty_value(value)).cloned();
937 let fill = rest.get(2).filter(|value| !is_empty_value(value)).cloned();
938 let issparse = rest
939 .get(3)
940 .map(|value| bool_scalar(value, "accumarray issparse"))
941 .transpose()?
942 .unwrap_or(false);
943 let output_len = checked_element_count(&output_shape, "accumarray")?;
944 if output_len > MAX_MATERIALIZED_ELEMENTS {
945 return Err(too_large_error("accumarray: output is too large"));
946 }
947 let mut buckets: BTreeMap<usize, Vec<f64>> = BTreeMap::new();
948 for (idx, subs) in index_rows.iter().enumerate() {
949 let lin = subscript_to_linear(subs, &output_shape)?;
950 buckets.entry(lin).or_default().push(data_values[idx]);
951 }
952 let fill_num = match fill.as_ref() {
953 None => 0.0,
954 Some(value) => numeric_scalar(value, "accumarray fill value")?,
955 };
956 let mut data_out = vec![fill_num; output_len];
957 if let Some(fun) = fun {
958 let mut cell_out = vec![Value::Num(fill_num); data_out.len()];
959 let mut all_numeric = true;
960 for (lin, values) in buckets {
961 let result = apply_accumarray_callback(fun.clone(), values).await?;
962 if let Some(num) = value_as_numeric_scalar(&result) {
963 data_out[lin] = num;
964 cell_out[lin] = Value::Num(num);
965 } else {
966 all_numeric = false;
967 cell_out[lin] = result;
968 }
969 }
970 if all_numeric {
971 return accumarray_numeric_output(data_out, output_shape, issparse);
972 }
973 if issparse {
974 return Err(grouping_error(
975 "accumarray: sparse output requires numeric scalar group results",
976 ));
977 }
978 let (rows, cols) = shape_to_rows_cols(&output_shape)?;
979 return CellArray::new(cell_out, rows, cols)
980 .map(Value::Cell)
981 .map_err(grouping_error);
982 }
983 for (lin, values) in buckets {
984 data_out[lin] = values.iter().sum();
985 }
986 accumarray_numeric_output(data_out, output_shape, issparse)
987}
988
989fn accumarray_subscripts(subs: Value) -> BuiltinResult<Vec<Vec<usize>>> {
990 match subs {
991 Value::Tensor(tensor) => {
992 if tensor.data.is_empty() {
993 return Ok(Vec::new());
994 }
995 if tensor.cols() <= 1 || tensor.rows() == 1 {
996 tensor
997 .data
998 .into_iter()
999 .map(|value| Ok(vec![positive_integer(value, "accumarray subscript")?]))
1000 .collect()
1001 } else {
1002 let mut out = Vec::with_capacity(tensor.rows());
1003 for row in 0..tensor.rows() {
1004 let mut subs = Vec::with_capacity(tensor.cols());
1005 for col in 0..tensor.cols() {
1006 subs.push(positive_integer(
1007 tensor.get2(row, col).map_err(grouping_error)?,
1008 "accumarray subscript",
1009 )?);
1010 }
1011 out.push(subs);
1012 }
1013 Ok(out)
1014 }
1015 }
1016 Value::Cell(cell) => {
1017 let mut columns = Vec::with_capacity(cell.data.len());
1018 for value in cell.data {
1019 let column = accumarray_subscripts(value)?;
1020 if column.iter().any(|row| row.len() != 1) {
1021 return Err(grouping_error(
1022 "accumarray: cell subscript entries must be vectors",
1023 ));
1024 }
1025 columns.push(column.into_iter().map(|row| row[0]).collect::<Vec<_>>());
1026 }
1027 let rows = columns.first().map(Vec::len).unwrap_or(0);
1028 for column in &columns {
1029 if column.len() != rows {
1030 return Err(grouping_error(
1031 "accumarray: cell subscript vectors must have equal length",
1032 ));
1033 }
1034 }
1035 let mut out = Vec::with_capacity(rows);
1036 for row in 0..rows {
1037 out.push(columns.iter().map(|column| column[row]).collect());
1038 }
1039 Ok(out)
1040 }
1041 other => Err(grouping_error(format!(
1042 "accumarray: unsupported subscript input {other:?}"
1043 ))),
1044 }
1045}
1046
1047fn accumarray_data_values(data: Value, rows: usize) -> BuiltinResult<Vec<f64>> {
1048 match data {
1049 Value::Num(value) => Ok(vec![value; rows]),
1050 Value::Int(value) => Ok(vec![value.to_f64(); rows]),
1051 Value::Bool(value) => Ok(vec![if value { 1.0 } else { 0.0 }; rows]),
1052 Value::Tensor(tensor) => {
1053 if tensor.data.len() == 1 {
1054 Ok(vec![tensor.data[0]; rows])
1055 } else if tensor.data.len() == rows {
1056 Ok(tensor.data)
1057 } else {
1058 Err(grouping_error(
1059 "accumarray: data must be scalar or match subscript row count",
1060 ))
1061 }
1062 }
1063 Value::LogicalArray(array) => {
1064 if array.data.len() == 1 {
1065 Ok(vec![if array.data[0] != 0 { 1.0 } else { 0.0 }; rows])
1066 } else if array.data.len() == rows {
1067 Ok(array
1068 .data
1069 .into_iter()
1070 .map(|flag| if flag != 0 { 1.0 } else { 0.0 })
1071 .collect())
1072 } else {
1073 Err(grouping_error(
1074 "accumarray: data must be scalar or match subscript row count",
1075 ))
1076 }
1077 }
1078 other => Err(grouping_error(format!(
1079 "accumarray: unsupported data input {other:?}"
1080 ))),
1081 }
1082}
1083
1084fn infer_accumarray_shape(index_rows: &[Vec<usize>]) -> Vec<usize> {
1085 let dims = index_rows.first().map(Vec::len).unwrap_or(1).max(1);
1086 let mut shape = vec![0usize; dims];
1087 for row in index_rows {
1088 for (dim, idx) in row.iter().enumerate() {
1089 shape[dim] = shape[dim].max(*idx);
1090 }
1091 }
1092 if dims == 1 {
1093 vec![shape[0], 1]
1094 } else {
1095 shape
1096 }
1097}
1098
1099fn subscript_to_linear(subs: &[usize], shape: &[usize]) -> BuiltinResult<usize> {
1100 if subs.len() > shape.len() {
1101 return Err(grouping_error("accumarray: too many subscript dimensions"));
1102 }
1103 let mut linear = 0usize;
1104 let mut stride = 1usize;
1105 for (dim, &size) in shape.iter().enumerate() {
1106 let sub = subs.get(dim).copied().unwrap_or(1);
1107 if sub == 0 || sub > size {
1108 return Err(grouping_error("accumarray: subscript exceeds output size"));
1109 }
1110 linear = linear
1111 .checked_add(
1112 (sub - 1)
1113 .checked_mul(stride)
1114 .ok_or_else(|| too_large_error("accumarray: output linear index overflow"))?,
1115 )
1116 .ok_or_else(|| too_large_error("accumarray: output linear index overflow"))?;
1117 stride = stride
1118 .checked_mul(size)
1119 .ok_or_else(|| too_large_error("accumarray: output size overflow"))?;
1120 }
1121 Ok(linear)
1122}
1123
1124async fn apply_accumarray_callback(func: Value, values: Vec<f64>) -> BuiltinResult<Value> {
1125 if let Some(name) = function_name(&func) {
1126 match name.to_ascii_lowercase().as_str() {
1127 "sum" => return Ok(Value::Num(values.iter().sum())),
1128 "mean" => {
1129 return Ok(Value::Num(if values.is_empty() {
1130 f64::NAN
1131 } else {
1132 values.iter().sum::<f64>() / values.len() as f64
1133 }))
1134 }
1135 "numel" | "length" => return Ok(Value::Num(values.len() as f64)),
1136 "min" => return Ok(Value::Num(values.into_iter().fold(f64::INFINITY, f64::min))),
1137 "max" => {
1138 return Ok(Value::Num(
1139 values.into_iter().fold(f64::NEG_INFINITY, f64::max),
1140 ))
1141 }
1142 _ => {}
1143 }
1144 }
1145 let arg =
1146 Value::Tensor(Tensor::new(values.clone(), vec![values.len(), 1]).map_err(grouping_error)?);
1147 call_feval_async_with_outputs(func, &[arg], 1)
1148 .await
1149 .map_err(|err| callback_error("accumarray: callback failed", Some(err)))
1150}
1151
1152fn accumarray_numeric_output(
1153 data: Vec<f64>,
1154 shape: Vec<usize>,
1155 issparse: bool,
1156) -> BuiltinResult<Value> {
1157 if issparse {
1158 let (rows, cols) = shape_to_rows_cols(&shape)?;
1159 if shape.len() > 2 {
1160 return Err(grouping_error(
1161 "accumarray: sparse output is only supported for 2-D results",
1162 ));
1163 }
1164 let mut col_ptrs = Vec::with_capacity(cols + 1);
1165 let mut row_indices = Vec::new();
1166 let mut values = Vec::new();
1167 col_ptrs.push(0);
1168 for col in 0..cols {
1169 for row in 0..rows {
1170 let value = data[row + col * rows];
1171 if value != 0.0 {
1172 row_indices.push(row);
1173 values.push(value);
1174 }
1175 }
1176 col_ptrs.push(values.len());
1177 }
1178 return SparseTensor::new(rows, cols, col_ptrs, row_indices, values)
1179 .map(Value::SparseTensor)
1180 .map_err(grouping_error);
1181 }
1182 Tensor::new(data, shape)
1183 .map(Value::Tensor)
1184 .map_err(grouping_error)
1185}
1186
1187fn discretize_impl(x: Value, edges_or_n: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
1188 let values = numeric_values(&x, "discretize X")?;
1189 let shape = value_shape(&x);
1190 let (edges, labels, included_right) = parse_discretize_args(&values, edges_or_n, rest)?;
1191 if edges.len() < 2 {
1192 return Err(grouping_error(
1193 "discretize: at least two bin edges are required",
1194 ));
1195 }
1196 let bins = values
1197 .iter()
1198 .map(|value| discretize_one(*value, &edges, included_right))
1199 .collect::<Vec<_>>();
1200 if let Some(labels) = labels {
1201 let data = bins
1202 .iter()
1203 .map(|bin| match bin {
1204 Some(idx) => labels.get(*idx - 1).cloned().unwrap_or_default(),
1205 None => String::new(),
1206 })
1207 .collect::<Vec<_>>();
1208 return StringArray::new(data, shape)
1209 .map(Value::StringArray)
1210 .map_err(grouping_error);
1211 }
1212 Tensor::new(
1213 bins.into_iter()
1214 .map(|bin| bin.map(|idx| idx as f64).unwrap_or(f64::NAN))
1215 .collect(),
1216 shape,
1217 )
1218 .map(Value::Tensor)
1219 .map_err(grouping_error)
1220}
1221
1222fn parse_discretize_args(
1223 values: &[f64],
1224 edges_or_n: Value,
1225 rest: Vec<Value>,
1226) -> BuiltinResult<(Vec<f64>, Option<Vec<String>>, bool)> {
1227 let mut labels = None;
1228 let mut included_right = false;
1229 let mut idx = 0usize;
1230 if let Some(first) = rest.first() {
1231 if !is_option_name(first) {
1232 labels = Some(string_list(first)?);
1233 idx = 1;
1234 }
1235 }
1236 while idx < rest.len() {
1237 if idx + 1 >= rest.len() {
1238 return Err(grouping_error(
1239 "discretize: name-value options must be provided in pairs",
1240 ));
1241 }
1242 let name = scalar_text(&rest[idx], "discretize option")?;
1243 if name.eq_ignore_ascii_case("IncludedEdge") {
1244 let edge = scalar_text(&rest[idx + 1], "IncludedEdge")?;
1245 included_right = match edge.to_ascii_lowercase().as_str() {
1246 "right" => true,
1247 "left" => false,
1248 other => {
1249 return Err(grouping_error(format!(
1250 "discretize: unsupported IncludedEdge '{other}'"
1251 )))
1252 }
1253 };
1254 } else {
1255 return Err(grouping_error(format!(
1256 "discretize: unsupported option '{name}'"
1257 )));
1258 }
1259 idx += 2;
1260 }
1261 let edges = match edges_or_n {
1262 Value::Num(n) if is_positive_integer_f64(n) => equal_width_edges(values, n as usize)?,
1263 Value::Int(n) if n.to_i64() > 0 => equal_width_edges(values, n.to_i64() as usize)?,
1264 other => numeric_values(&other, "discretize edges")?,
1265 };
1266 if let Some(labels) = &labels {
1267 if labels.len() != edges.len().saturating_sub(1) {
1268 return Err(grouping_error(
1269 "discretize: number of labels must match number of bins",
1270 ));
1271 }
1272 }
1273 Ok((edges, labels, included_right))
1274}
1275
1276fn discretize_one(value: f64, edges: &[f64], included_right: bool) -> Option<usize> {
1277 if value.is_nan() {
1278 return None;
1279 }
1280 for bin in 0..edges.len() - 1 {
1281 let lower = edges[bin];
1282 let upper = edges[bin + 1];
1283 let hit = if included_right {
1284 (value > lower || (bin == 0 && value == lower)) && value <= upper
1285 } else {
1286 value >= lower && (value < upper || (bin == edges.len() - 2 && value == upper))
1287 };
1288 if hit {
1289 return Some(bin + 1);
1290 }
1291 }
1292 None
1293}
1294
1295fn equal_width_edges(values: &[f64], bins: usize) -> BuiltinResult<Vec<f64>> {
1296 if bins == 0 {
1297 return Err(grouping_error(
1298 "discretize: number of bins must be positive",
1299 ));
1300 }
1301 let finite = values
1302 .iter()
1303 .copied()
1304 .filter(|value| value.is_finite())
1305 .collect::<Vec<_>>();
1306 if finite.is_empty() {
1307 return Err(grouping_error(
1308 "discretize: cannot infer equal-width bins from all-missing data",
1309 ));
1310 }
1311 let min = finite.iter().copied().fold(f64::INFINITY, f64::min);
1312 let max = finite.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1313 if min == max {
1314 let half = 0.5;
1315 return Ok((0..=bins)
1316 .map(|idx| min - half + idx as f64 / bins as f64)
1317 .collect());
1318 }
1319 let step = (max - min) / bins as f64;
1320 Ok((0..=bins).map(|idx| min + idx as f64 * step).collect())
1321}
1322
1323fn combinations_impl(first: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
1324 let mut values = vec![first];
1325 let mut options_start = rest.len();
1326 let mut idx = 0usize;
1327 while idx < rest.len() {
1328 if is_option_name(&rest[idx]) {
1329 options_start = idx;
1330 break;
1331 }
1332 values.push(rest[idx].clone());
1333 idx += 1;
1334 }
1335 let mut names = (0..values.len())
1336 .map(|idx| format!("Var{}", idx + 1))
1337 .collect::<Vec<_>>();
1338 if options_start < rest.len() {
1339 let mut opt = options_start;
1340 while opt < rest.len() {
1341 if opt + 1 >= rest.len() {
1342 return Err(grouping_error(
1343 "combinations: name-value options must be provided in pairs",
1344 ));
1345 }
1346 let name = scalar_text(&rest[opt], "combinations option")?;
1347 if name.eq_ignore_ascii_case("VariableNames") {
1348 names = string_list(&rest[opt + 1])?;
1349 if names.len() != values.len() {
1350 return Err(grouping_error(
1351 "combinations: VariableNames length must match input count",
1352 ));
1353 }
1354 } else {
1355 return Err(grouping_error(format!(
1356 "combinations: unsupported option '{name}'"
1357 )));
1358 }
1359 opt += 2;
1360 }
1361 }
1362 let columns = values
1363 .into_iter()
1364 .map(vector_elements)
1365 .collect::<BuiltinResult<Vec<_>>>()?;
1366 let row_count = columns.iter().try_fold(1usize, |acc, column| {
1367 acc.checked_mul(column.len())
1368 .ok_or_else(|| too_large_error("combinations: output row count overflow"))
1369 })?;
1370 if row_count > MAX_MATERIALIZED_ELEMENTS {
1371 return Err(too_large_error("combinations: output is too large"));
1372 }
1373 let mut out_columns = Vec::with_capacity(columns.len());
1374 for col_idx in 0..columns.len() {
1375 let repeat_inner = columns[col_idx + 1..]
1376 .iter()
1377 .map(Vec::len)
1378 .product::<usize>()
1379 .max(1);
1380 let repeat_outer = columns[..col_idx]
1381 .iter()
1382 .map(Vec::len)
1383 .product::<usize>()
1384 .max(1);
1385 let mut values = Vec::with_capacity(row_count);
1386 for _ in 0..repeat_outer {
1387 for item in &columns[col_idx] {
1388 for _ in 0..repeat_inner {
1389 values.push(item.clone());
1390 }
1391 }
1392 }
1393 out_columns.push(collect_column_values(values, row_count)?);
1394 }
1395 table_from_columns(names, out_columns)
1396}
1397
1398fn parse_name_selector(
1399 value: &Value,
1400 names: &[String],
1401 context: &str,
1402) -> BuiltinResult<Vec<String>> {
1403 match value {
1404 Value::String(text) => {
1405 if names.contains(text) {
1406 Ok(vec![text.clone()])
1407 } else {
1408 Err(grouping_error(format!(
1409 "{context}: unknown variable '{text}'"
1410 )))
1411 }
1412 }
1413 Value::CharArray(chars) if chars.rows == 1 => {
1414 let text: String = chars.data.iter().collect();
1415 parse_name_selector(&Value::String(text), names, context)
1416 }
1417 Value::StringArray(array) => array
1418 .data
1419 .iter()
1420 .map(|name| {
1421 if names.contains(name) {
1422 Ok(name.clone())
1423 } else {
1424 Err(grouping_error(format!(
1425 "{context}: unknown variable '{name}'"
1426 )))
1427 }
1428 })
1429 .collect(),
1430 Value::Cell(cell) => cell
1431 .data
1432 .iter()
1433 .map(|value| scalar_text(value, context))
1434 .map(|res| {
1435 let name = res?;
1436 if names.contains(&name) {
1437 Ok(name)
1438 } else {
1439 Err(grouping_error(format!(
1440 "{context}: unknown variable '{name}'"
1441 )))
1442 }
1443 })
1444 .collect(),
1445 Value::Tensor(tensor) => tensor
1446 .data
1447 .iter()
1448 .map(|value| {
1449 let idx = positive_integer(*value, context)?;
1450 names.get(idx - 1).cloned().ok_or_else(|| {
1451 grouping_error(format!("{context}: variable index out of range"))
1452 })
1453 })
1454 .collect(),
1455 other => Err(grouping_error(format!(
1456 "{context}: unsupported variable selector {other:?}"
1457 ))),
1458 }
1459}
1460
1461async fn gather_values(values: Vec<Value>) -> BuiltinResult<Vec<Value>> {
1462 let mut out = Vec::with_capacity(values.len());
1463 for value in values {
1464 out.push(gather_if_needed_async(&value).await?);
1465 }
1466 Ok(out)
1467}
1468
1469fn normalize_outputs(value: Value, requested: usize, context: &str) -> BuiltinResult<Vec<Value>> {
1470 match value {
1471 Value::OutputList(values) if values.len() == requested => Ok(values),
1472 Value::OutputList(values) => Err(callback_error(
1473 format!(
1474 "{context}: callback returned {} outputs but {} were requested",
1475 values.len(),
1476 requested
1477 ),
1478 None,
1479 )),
1480 value if requested == 1 => Ok(vec![value]),
1481 _ => Err(callback_error(
1482 format!("{context}: callback did not return the requested number of outputs"),
1483 None,
1484 )),
1485 }
1486}
1487
1488fn collect_group_results(values: Vec<Value>, rows: usize, context: &str) -> BuiltinResult<Value> {
1489 if values
1490 .iter()
1491 .all(|value| value_as_numeric_scalar(value).is_some())
1492 {
1493 return Tensor::new(
1494 values
1495 .iter()
1496 .map(|value| value_as_numeric_scalar(value).unwrap())
1497 .collect(),
1498 vec![rows, 1],
1499 )
1500 .map(Value::Tensor)
1501 .map_err(grouping_error);
1502 }
1503 CellArray::new(values, rows, 1)
1504 .map(Value::Cell)
1505 .map_err(|err| callback_error(format!("{context}: {err}"), None))
1506}
1507
1508fn collect_column_values(values: Vec<Value>, rows: usize) -> BuiltinResult<Value> {
1509 if values
1510 .iter()
1511 .all(|value| value_as_numeric_scalar(value).is_some())
1512 {
1513 return Tensor::new(
1514 values
1515 .iter()
1516 .map(|value| value_as_numeric_scalar(value).unwrap())
1517 .collect(),
1518 vec![rows, 1],
1519 )
1520 .map(Value::Tensor)
1521 .map_err(grouping_error);
1522 }
1523 if values.iter().all(|value| matches!(value, Value::String(_))) {
1524 return StringArray::new(
1525 values
1526 .into_iter()
1527 .map(|value| match value {
1528 Value::String(text) => text,
1529 _ => unreachable!("checked above"),
1530 })
1531 .collect(),
1532 vec![rows, 1],
1533 )
1534 .map(Value::StringArray)
1535 .map_err(grouping_error);
1536 }
1537 if values.iter().all(|value| matches!(value, Value::Bool(_))) {
1538 return LogicalArray::new(
1539 values
1540 .into_iter()
1541 .map(|value| match value {
1542 Value::Bool(flag) => u8::from(flag),
1543 _ => unreachable!("checked above"),
1544 })
1545 .collect(),
1546 vec![rows, 1],
1547 )
1548 .map(Value::LogicalArray)
1549 .map_err(grouping_error);
1550 }
1551 CellArray::new(values, rows, 1)
1552 .map(Value::Cell)
1553 .map_err(grouping_error)
1554}
1555
1556fn vector_elements(value: Value) -> BuiltinResult<Vec<Value>> {
1557 match value {
1558 Value::Tensor(tensor) => Ok(tensor.data.into_iter().map(Value::Num).collect()),
1559 Value::LogicalArray(array) => Ok(array
1560 .data
1561 .into_iter()
1562 .map(|flag| Value::Bool(flag != 0))
1563 .collect()),
1564 Value::StringArray(array) => Ok(array.data.into_iter().map(Value::String).collect()),
1565 Value::Cell(cell) => Ok(cell.data),
1566 Value::CharArray(chars) if chars.rows == 1 => Ok(chars
1567 .data
1568 .into_iter()
1569 .map(|ch| Value::String(ch.to_string()))
1570 .collect()),
1571 Value::String(text) => Ok(vec![Value::String(text)]),
1572 other => Ok(vec![other]),
1573 }
1574}
1575
1576fn multi_output(outputs: Vec<Value>) -> BuiltinResult<Value> {
1577 if let Some(out_count) = crate::output_count::current_output_count() {
1578 if out_count == 0 {
1579 return Ok(Value::OutputList(Vec::new()));
1580 }
1581 return Ok(crate::output_count::output_list_with_padding(
1582 out_count, outputs,
1583 ));
1584 }
1585 Ok(outputs
1586 .into_iter()
1587 .next()
1588 .unwrap_or(Value::OutputList(Vec::new())))
1589}
1590
1591fn numeric_values(value: &Value, context: &str) -> BuiltinResult<Vec<f64>> {
1592 match value {
1593 Value::Num(value) => Ok(vec![*value]),
1594 Value::Int(value) => Ok(vec![value.to_f64()]),
1595 Value::Bool(value) => Ok(vec![if *value { 1.0 } else { 0.0 }]),
1596 Value::Tensor(tensor) => Ok(tensor.data.clone()),
1597 Value::LogicalArray(array) => Ok(array
1598 .data
1599 .iter()
1600 .map(|flag| if *flag != 0 { 1.0 } else { 0.0 })
1601 .collect()),
1602 Value::SparseTensor(sparse) => sparse
1603 .to_dense()
1604 .map(|tensor| tensor.data)
1605 .map_err(grouping_error),
1606 other => Err(grouping_error(format!(
1607 "{context}: expected numeric input, got {other:?}"
1608 ))),
1609 }
1610}
1611
1612fn value_shape(value: &Value) -> Vec<usize> {
1613 match value {
1614 Value::Tensor(tensor) => tensor.shape.clone(),
1615 Value::LogicalArray(array) => array.shape.clone(),
1616 Value::StringArray(array) => array.shape.clone(),
1617 Value::SparseTensor(sparse) => sparse.shape(),
1618 Value::Num(_) | Value::Int(_) | Value::Bool(_) => vec![1, 1],
1619 _ => vec![1, 1],
1620 }
1621}
1622
1623fn parse_size_vector(value: &Value, context: &str) -> BuiltinResult<Vec<usize>> {
1624 let values = numeric_values(value, context)?;
1625 let mut dims = Vec::with_capacity(values.len());
1626 for value in values {
1627 dims.push(nonnegative_integer(value, context)?);
1628 }
1629 if dims.is_empty() {
1630 return Err(grouping_error(format!(
1631 "{context}: size vector must not be empty"
1632 )));
1633 }
1634 if dims.len() == 1 {
1635 dims.push(1);
1636 }
1637 Ok(dims)
1638}
1639
1640fn shape_to_rows_cols(shape: &[usize]) -> BuiltinResult<(usize, usize)> {
1641 let rows = shape.first().copied().unwrap_or(0);
1642 let cols = if shape.len() <= 1 {
1643 1
1644 } else if shape.len() == 2 {
1645 shape[1]
1646 } else {
1647 shape[1..].iter().product()
1648 };
1649 Ok((rows, cols))
1650}
1651
1652fn checked_element_count(shape: &[usize], context: &str) -> BuiltinResult<usize> {
1653 shape.iter().try_fold(1usize, |acc, dim| {
1654 acc.checked_mul(*dim)
1655 .ok_or_else(|| too_large_error(format!("{context}: output size overflow")))
1656 })
1657}
1658
1659fn scalar_text(value: &Value, context: &str) -> BuiltinResult<String> {
1660 match value {
1661 Value::String(text) => Ok(text.clone()),
1662 Value::CharArray(chars) if chars.rows == 1 => Ok(chars.data.iter().collect()),
1663 Value::StringArray(array) if array.data.len() == 1 => Ok(array.data[0].clone()),
1664 other => Err(grouping_error(format!(
1665 "{context}: expected text scalar, got {other:?}"
1666 ))),
1667 }
1668}
1669
1670fn string_list(value: &Value) -> BuiltinResult<Vec<String>> {
1671 match value {
1672 Value::String(text) => Ok(vec![text.clone()]),
1673 Value::CharArray(chars) if chars.rows == 1 => Ok(vec![chars.data.iter().collect()]),
1674 Value::StringArray(array) => Ok(array.data.clone()),
1675 Value::Cell(cell) => cell
1676 .data
1677 .iter()
1678 .map(|value| scalar_text(value, "string list"))
1679 .collect(),
1680 other => Err(grouping_error(format!("expected text list, got {other:?}"))),
1681 }
1682}
1683
1684fn bool_scalar(value: &Value, context: &str) -> BuiltinResult<bool> {
1685 match value {
1686 Value::Bool(flag) => Ok(*flag),
1687 Value::Num(value) => Ok(*value != 0.0),
1688 Value::Int(value) => Ok(value.to_i64() != 0),
1689 Value::LogicalArray(array) if array.data.len() == 1 => Ok(array.data[0] != 0),
1690 other => Err(grouping_error(format!(
1691 "{context}: expected logical scalar, got {other:?}"
1692 ))),
1693 }
1694}
1695
1696fn numeric_scalar(value: &Value, context: &str) -> BuiltinResult<f64> {
1697 value_as_numeric_scalar(value)
1698 .ok_or_else(|| grouping_error(format!("{context}: expected numeric scalar")))
1699}
1700
1701fn value_as_numeric_scalar(value: &Value) -> Option<f64> {
1702 match value {
1703 Value::Num(value) => Some(*value),
1704 Value::Int(value) => Some(value.to_f64()),
1705 Value::Bool(value) => Some(if *value { 1.0 } else { 0.0 }),
1706 Value::Tensor(tensor) if tensor.data.len() == 1 => Some(tensor.data[0]),
1707 Value::LogicalArray(array) if array.data.len() == 1 => {
1708 Some(if array.data[0] != 0 { 1.0 } else { 0.0 })
1709 }
1710 _ => None,
1711 }
1712}
1713
1714fn is_empty_value(value: &Value) -> bool {
1715 match value {
1716 Value::Tensor(tensor) => tensor.data.is_empty(),
1717 Value::StringArray(array) => array.data.is_empty(),
1718 Value::Cell(cell) => cell.data.is_empty(),
1719 Value::CharArray(chars) => chars.data.is_empty(),
1720 _ => false,
1721 }
1722}
1723
1724fn is_option_name(value: &Value) -> bool {
1725 scalar_text(value, "option")
1726 .map(|text| {
1727 matches!(
1728 text.to_ascii_lowercase().as_str(),
1729 "includededge" | "variablenames" | "includemissinggroups" | "includeemptygroups"
1730 )
1731 })
1732 .unwrap_or(false)
1733}
1734
1735fn function_name(value: &Value) -> Option<&str> {
1736 match value {
1737 Value::FunctionHandle(name)
1738 | Value::ExternalFunctionHandle(name)
1739 | Value::MethodFunctionHandle(name)
1740 | Value::BoundFunctionHandle { name, .. }
1741 | Value::String(name) => Some(name.as_str()),
1742 _ => None,
1743 }
1744}
1745
1746fn positive_integer(value: f64, context: &str) -> BuiltinResult<usize> {
1747 if is_positive_integer_f64(value) {
1748 Ok(value as usize)
1749 } else {
1750 Err(grouping_error(format!(
1751 "{context}: expected positive integer"
1752 )))
1753 }
1754}
1755
1756fn nonnegative_integer(value: f64, context: &str) -> BuiltinResult<usize> {
1757 if value.is_finite() && value >= 0.0 && value.fract() == 0.0 {
1758 Ok(value as usize)
1759 } else {
1760 Err(grouping_error(format!(
1761 "{context}: expected nonnegative integer"
1762 )))
1763 }
1764}
1765
1766fn is_positive_integer_f64(value: f64) -> bool {
1767 value.is_finite() && value > 0.0 && value.fract() == 0.0
1768}
1769
1770fn format_key_number(value: f64) -> String {
1771 if value.fract() == 0.0 && value.abs() < 1e15 {
1772 format!("{}", value as i64)
1773 } else {
1774 let mut text = format!("{value:.12}");
1775 while text.contains('.') && text.ends_with('0') {
1776 text.pop();
1777 }
1778 if text.ends_with('.') {
1779 text.pop();
1780 }
1781 text
1782 }
1783}
1784
1785fn is_missing_text(text: &str) -> bool {
1786 text.eq_ignore_ascii_case("<missing>")
1787}
1788
1789#[cfg(test)]
1790mod tests {
1791 use super::*;
1792 use futures::executor::block_on;
1793
1794 #[test]
1795 fn accumarray_sums_vector_and_matrix_subscripts() {
1796 let out = block_on(accumarray_builtin(
1797 Value::Tensor(Tensor::new(vec![1.0, 3.0, 4.0, 2.0, 4.0, 1.0], vec![6, 1]).unwrap()),
1798 Value::Tensor(Tensor::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], vec![6, 1]).unwrap()),
1799 Vec::new(),
1800 ))
1801 .unwrap();
1802 match out {
1803 Value::Tensor(tensor) => assert_eq!(tensor.data, vec![7.0, 4.0, 2.0, 8.0]),
1804 other => panic!("expected tensor, got {other:?}"),
1805 }
1806
1807 let out = block_on(accumarray_builtin(
1808 Value::Tensor(
1809 Tensor::new(
1810 vec![1.0, 2.0, 3.0, 1.0, 2.0, 4.0, 1.0, 2.0, 2.0, 1.0, 2.0, 1.0],
1811 vec![6, 2],
1812 )
1813 .unwrap(),
1814 ),
1815 Value::Tensor(Tensor::new(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], vec![6, 1]).unwrap()),
1816 Vec::new(),
1817 ))
1818 .unwrap();
1819 match out {
1820 Value::Tensor(tensor) => {
1821 assert_eq!(tensor.shape, vec![4, 2]);
1822 assert_eq!(tensor.data, vec![5.0, 0.0, 0.0, 6.0, 0.0, 7.0, 3.0, 0.0]);
1823 }
1824 other => panic!("expected tensor, got {other:?}"),
1825 }
1826 }
1827
1828 #[test]
1829 fn discretize_assigns_bins_and_labels() {
1830 let out = block_on(discretize_builtin(
1831 Value::Tensor(Tensor::new(vec![0.0, 0.2, 1.0, 2.5], vec![4, 1]).unwrap()),
1832 Value::Tensor(Tensor::new(vec![0.0, 1.0, 2.0], vec![1, 3]).unwrap()),
1833 Vec::new(),
1834 ))
1835 .unwrap();
1836 match out {
1837 Value::Tensor(tensor) => {
1838 assert_eq!(tensor.data[0], 1.0);
1839 assert_eq!(tensor.data[1], 1.0);
1840 assert_eq!(tensor.data[2], 2.0);
1841 assert!(tensor.data[3].is_nan());
1842 }
1843 other => panic!("expected tensor, got {other:?}"),
1844 }
1845 }
1846
1847 #[test]
1848 fn findgroups_groupcounts_and_grp2idx_share_order() {
1849 let groups = Value::StringArray(
1850 StringArray::new(
1851 vec!["b".into(), "a".into(), "b".into(), "<missing>".into()],
1852 vec![4, 1],
1853 )
1854 .unwrap(),
1855 );
1856 let out = block_on(findgroups_builtin(groups.clone(), Vec::new())).unwrap();
1857 match out {
1858 Value::Tensor(tensor) => {
1859 assert_eq!(tensor.data[0], 2.0);
1860 assert_eq!(tensor.data[1], 1.0);
1861 assert_eq!(tensor.data[2], 2.0);
1862 assert!(tensor.data[3].is_nan());
1863 }
1864 other => panic!("expected tensor, got {other:?}"),
1865 }
1866 let counted = block_on(groupcounts_builtin(groups.clone(), Vec::new())).unwrap();
1867 match counted {
1868 Value::Tensor(tensor) => assert_eq!(tensor.data, vec![1.0, 2.0]),
1869 other => panic!("expected tensor, got {other:?}"),
1870 }
1871 let indexed = block_on(grp2idx_builtin(groups)).unwrap();
1872 match indexed {
1873 Value::Tensor(tensor) => assert!(tensor.data[3].is_nan()),
1874 other => panic!("expected tensor, got {other:?}"),
1875 }
1876
1877 let empty_is_group = block_on(groupcounts_builtin(
1878 Value::StringArray(
1879 StringArray::new(vec![String::new(), "a".into(), String::new()], vec![3, 1])
1880 .unwrap(),
1881 ),
1882 Vec::new(),
1883 ))
1884 .unwrap();
1885 match empty_is_group {
1886 Value::Tensor(tensor) => assert_eq!(tensor.data, vec![2.0, 1.0]),
1887 other => panic!("expected tensor, got {other:?}"),
1888 }
1889 }
1890
1891 #[test]
1892 fn combinations_returns_table_columns() {
1893 let out = block_on(combinations_builtin(
1894 Value::Tensor(Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap()),
1895 vec![Value::StringArray(
1896 StringArray::new(vec!["x".into(), "y".into()], vec![1, 2]).unwrap(),
1897 )],
1898 ))
1899 .unwrap();
1900 let Value::Object(object) = out else {
1901 panic!("expected table");
1902 };
1903 assert!(is_tabular_object(&object));
1904 assert_eq!(table_height(&object).unwrap(), 4);
1905 }
1906
1907 #[test]
1908 fn accumarray_supports_callbacks_and_sparse_output() {
1909 let out = block_on(accumarray_builtin(
1910 Value::Tensor(Tensor::new(vec![1.0, 1.0, 2.0, 2.0], vec![4, 1]).unwrap()),
1911 Value::Tensor(Tensor::new(vec![1.0, 3.0, 5.0, 7.0], vec![4, 1]).unwrap()),
1912 vec![
1913 Value::Tensor(Tensor::new(Vec::new(), vec![0, 0]).unwrap()),
1914 Value::FunctionHandle("mean".into()),
1915 ],
1916 ))
1917 .unwrap();
1918 match out {
1919 Value::Tensor(tensor) => assert_eq!(tensor.data, vec![2.0, 6.0]),
1920 other => panic!("expected tensor, got {other:?}"),
1921 }
1922
1923 let sparse = block_on(accumarray_builtin(
1924 Value::Tensor(Tensor::new(vec![1.0, 3.0], vec![2, 1]).unwrap()),
1925 Value::Num(1.0),
1926 vec![
1927 Value::Tensor(Tensor::new(vec![4.0, 1.0], vec![1, 2]).unwrap()),
1928 Value::Tensor(Tensor::new(Vec::new(), vec![0, 0]).unwrap()),
1929 Value::Tensor(Tensor::new(Vec::new(), vec![0, 0]).unwrap()),
1930 Value::Bool(true),
1931 ],
1932 ))
1933 .unwrap();
1934 match sparse {
1935 Value::SparseTensor(st) => {
1936 assert_eq!(st.rows, 4);
1937 assert_eq!(st.cols, 1);
1938 assert_eq!(st.nnz(), 2);
1939 }
1940 other => panic!("expected sparse tensor, got {other:?}"),
1941 }
1942 }
1943
1944 #[test]
1945 fn splitapply_invokes_callback_by_group() {
1946 let out = block_on(splitapply_builtin(
1947 Value::FunctionHandle("sum".into()),
1948 Value::Tensor(Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![4, 1]).unwrap()),
1949 vec![Value::Tensor(
1950 Tensor::new(vec![2.0, 1.0, 2.0, 1.0], vec![4, 1]).unwrap(),
1951 )],
1952 ))
1953 .unwrap();
1954 match out {
1955 Value::Tensor(tensor) => assert_eq!(tensor.data, vec![6.0, 4.0]),
1956 other => panic!("expected tensor, got {other:?}"),
1957 }
1958
1959 let err = block_on(splitapply_builtin(
1960 Value::FunctionHandle("sum".into()),
1961 Value::Tensor(Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap()),
1962 vec![Value::Tensor(
1963 Tensor::new(vec![1.0, 1.0], vec![2, 1]).unwrap(),
1964 )],
1965 ))
1966 .unwrap_err();
1967 assert!(err.message().contains("must have 2 rows"));
1968 }
1969
1970 #[test]
1971 fn groupcounts_table_returns_count_and_percent_columns() {
1972 let table = table_from_columns(
1973 vec!["G".into(), "X".into()],
1974 vec![
1975 Value::StringArray(
1976 StringArray::new(vec!["b".into(), "a".into(), "b".into()], vec![3, 1]).unwrap(),
1977 ),
1978 Value::Tensor(Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap()),
1979 ],
1980 )
1981 .unwrap();
1982 let out = block_on(groupcounts_builtin(table, vec![Value::from("G")])).unwrap();
1983 let Value::Object(object) = out else {
1984 panic!("expected table");
1985 };
1986 let names = table_variable_names_from_object(&object).unwrap();
1987 assert_eq!(names, vec!["G", "GroupCount", "Percent"]);
1988 assert_eq!(table_height(&object).unwrap(), 2);
1989
1990 let table = table_from_columns(
1991 vec!["G".into()],
1992 vec![Value::StringArray(
1993 StringArray::new(vec!["a".into(), "<missing>".into()], vec![2, 1]).unwrap(),
1994 )],
1995 )
1996 .unwrap();
1997 let out = block_on(groupcounts_builtin(
1998 table,
1999 vec![
2000 Value::from("G"),
2001 Value::from("IncludeMissingGroups"),
2002 Value::Bool(true),
2003 ],
2004 ))
2005 .unwrap();
2006 let Value::Object(object) = out else {
2007 panic!("expected table");
2008 };
2009 assert_eq!(table_height(&object).unwrap(), 2);
2010 }
2011}