1use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6 CellArray, CharArray, LogicalArray, ObjectInstance, ResolveContext, StringArray, StructValue,
7 Tensor, Type, Value,
8};
9use runmat_macros::runtime_builtin;
10
11use crate::builtins::math::reduction::{mean, median, min, std as std_reduction, sum, var};
12use crate::builtins::table::{
13 is_tabular_object, select_rows, selected_row_names, table_from_columns_like, table_height,
14 table_variable_names_from_object, table_variables, table_width,
15};
16use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
17
18const MISSING_TEXT: &str = "<missing>";
19
20const VALUE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
21 name: "B",
22 ty: BuiltinParamType::Any,
23 arity: BuiltinParamArity::Required,
24 default: None,
25 description: "Result value.",
26}];
27const LOGICAL_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
28 name: "TF",
29 ty: BuiltinParamType::LogicalArray,
30 arity: BuiltinParamArity::Required,
31 default: None,
32 description: "Logical missing-value mask.",
33}];
34const VALUE_AND_MASK_OUTPUTS: [BuiltinParamDescriptor; 2] = [
35 BuiltinParamDescriptor {
36 name: "B",
37 ty: BuiltinParamType::Any,
38 arity: BuiltinParamArity::Required,
39 default: None,
40 description: "Result value.",
41 },
42 BuiltinParamDescriptor {
43 name: "TF",
44 ty: BuiltinParamType::LogicalArray,
45 arity: BuiltinParamArity::Optional,
46 default: None,
47 description: "Logical mask of entries, rows, or columns that were filled or removed.",
48 },
49];
50const VALUE_INPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
51 name: "A",
52 ty: BuiltinParamType::Any,
53 arity: BuiltinParamArity::Required,
54 default: None,
55 description: "Input value.",
56}];
57const VARIADIC_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
58 name: "args",
59 ty: BuiltinParamType::Any,
60 arity: BuiltinParamArity::Variadic,
61 default: None,
62 description: "Size, method, dimension, or option arguments.",
63}];
64const VALUE_AND_ARGS_INPUTS: [BuiltinParamDescriptor; 2] = [
65 BuiltinParamDescriptor {
66 name: "A",
67 ty: BuiltinParamType::Any,
68 arity: BuiltinParamArity::Required,
69 default: None,
70 description: "Input value.",
71 },
72 BuiltinParamDescriptor {
73 name: "args",
74 ty: BuiltinParamType::Any,
75 arity: BuiltinParamArity::Variadic,
76 default: None,
77 description: "Method, dimension, or option arguments.",
78 },
79];
80
81const MISSING_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
82 BuiltinSignatureDescriptor {
83 label: "missing",
84 inputs: &[],
85 outputs: &VALUE_OUTPUT,
86 },
87 BuiltinSignatureDescriptor {
88 label: "missing(sz)",
89 inputs: &VARIADIC_INPUTS,
90 outputs: &VALUE_OUTPUT,
91 },
92];
93const ONE_VALUE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
94 label: "TF = ismissing(A)",
95 inputs: &VALUE_INPUT,
96 outputs: &LOGICAL_OUTPUT,
97}];
98const ANYMISSING_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
99 label: "TF = anymissing(A)",
100 inputs: &VALUE_INPUT,
101 outputs: &LOGICAL_OUTPUT,
102}];
103const FILLMISSING_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
104 label: "B = fillmissing(A, method, ...)",
105 inputs: &VALUE_AND_ARGS_INPUTS,
106 outputs: &VALUE_AND_MASK_OUTPUTS,
107}];
108const RMMISSING_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
109 label: "B = rmmissing(A, ...)",
110 inputs: &VALUE_AND_ARGS_INPUTS,
111 outputs: &VALUE_AND_MASK_OUTPUTS,
112}];
113const STANDARDIZE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
114 label: "B = standardizeMissing(A, indicators)",
115 inputs: &VALUE_AND_ARGS_INPUTS,
116 outputs: &VALUE_OUTPUT,
117}];
118const NANAWARE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
119 label: "B = nanmean(A, ...)",
120 inputs: &VALUE_AND_ARGS_INPUTS,
121 outputs: &VALUE_OUTPUT,
122}];
123
124const MISSING_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
125 code: "RM.MISSING.INVALID_ARGUMENT",
126 identifier: Some("RunMat:missing:InvalidArgument"),
127 when: "Arguments do not match a supported missing-value syntax.",
128 message: "missing-value builtin: invalid argument",
129};
130const MISSING_ERROR_UNSUPPORTED_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
131 code: "RM.MISSING.UNSUPPORTED_TYPE",
132 identifier: Some("RunMat:missing:UnsupportedType"),
133 when: "The input type has no missing-value representation in RunMat.",
134 message: "missing-value builtin: unsupported input type",
135};
136const MISSING_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
137 code: "RM.MISSING.INTERNAL",
138 identifier: Some("RunMat:missing:InternalError"),
139 when: "Internal shape or table materialization fails.",
140 message: "missing-value builtin: internal error",
141};
142const MISSING_ERRORS: [BuiltinErrorDescriptor; 3] = [
143 MISSING_ERROR_INVALID_ARGUMENT,
144 MISSING_ERROR_UNSUPPORTED_TYPE,
145 MISSING_ERROR_INTERNAL,
146];
147
148macro_rules! descriptor {
149 ($name:ident, $signatures:ident, $mode:expr) => {
150 pub const $name: BuiltinDescriptor = BuiltinDescriptor {
151 signatures: &$signatures,
152 output_mode: $mode,
153 completion_policy: BuiltinCompletionPolicy::Public,
154 errors: &MISSING_ERRORS,
155 };
156 };
157}
158
159descriptor!(
160 MISSING_DESCRIPTOR,
161 MISSING_SIGNATURES,
162 BuiltinOutputMode::Fixed
163);
164descriptor!(
165 ISMISSING_DESCRIPTOR,
166 ONE_VALUE_SIGNATURES,
167 BuiltinOutputMode::Fixed
168);
169descriptor!(
170 ANYMISSING_DESCRIPTOR,
171 ANYMISSING_SIGNATURES,
172 BuiltinOutputMode::Fixed
173);
174descriptor!(
175 FILLMISSING_DESCRIPTOR,
176 FILLMISSING_SIGNATURES,
177 BuiltinOutputMode::ByRequestedOutputCount
178);
179descriptor!(
180 RMMISSING_DESCRIPTOR,
181 RMMISSING_SIGNATURES,
182 BuiltinOutputMode::ByRequestedOutputCount
183);
184descriptor!(
185 STANDARDIZE_MISSING_DESCRIPTOR,
186 STANDARDIZE_SIGNATURES,
187 BuiltinOutputMode::Fixed
188);
189descriptor!(
190 NAN_AWARE_DESCRIPTOR,
191 NANAWARE_SIGNATURES,
192 BuiltinOutputMode::Fixed
193);
194
195fn logical_type(_args: &[Type], _ctx: &ResolveContext) -> Type {
196 Type::Logical { shape: None }
197}
198
199fn any_type(_args: &[Type], _ctx: &ResolveContext) -> Type {
200 Type::Unknown
201}
202
203fn missing_error(
204 error: &'static BuiltinErrorDescriptor,
205 detail: impl Into<String>,
206) -> RuntimeError {
207 let mut builder = build_runtime_error(format!("{}: {}", error.message, detail.into()))
208 .with_builtin("missing");
209 if let Some(identifier) = error.identifier {
210 builder = builder.with_identifier(identifier);
211 }
212 builder.build()
213}
214
215fn invalid_argument(detail: impl Into<String>) -> RuntimeError {
216 missing_error(&MISSING_ERROR_INVALID_ARGUMENT, detail)
217}
218
219fn unsupported_type(detail: impl Into<String>) -> RuntimeError {
220 missing_error(&MISSING_ERROR_UNSUPPORTED_TYPE, detail)
221}
222
223fn internal_error(detail: impl Into<String>) -> RuntimeError {
224 missing_error(&MISSING_ERROR_INTERNAL, detail)
225}
226
227#[runtime_builtin(
228 name = "missing",
229 category = "missing",
230 summary = "Create MATLAB missing string scalars or arrays.",
231 keywords = "missing,string,missing values",
232 accel = "cpu",
233 type_resolver(any_type),
234 descriptor(crate::builtins::missing::MISSING_DESCRIPTOR),
235 builtin_path = "crate::builtins::missing"
236)]
237async fn missing_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
238 let packed = Value::OutputList(args);
239 let gathered = gather_if_needed_async(&packed)
240 .await
241 .map_err(|err| invalid_argument(format!("missing: failed to gather arguments: {err}")))?;
242 let args = match gathered {
243 Value::OutputList(values) => values,
244 _ => Vec::new(),
245 };
246 let shape = parse_size_args(&args)?;
247 missing_string_array(shape)
248}
249
250#[runtime_builtin(
251 name = "ismissing",
252 category = "missing",
253 summary = "Return a logical mask identifying missing values.",
254 keywords = "ismissing,missing,NaN,NaT,string,table",
255 accel = "cpu",
256 type_resolver(logical_type),
257 descriptor(crate::builtins::missing::ISMISSING_DESCRIPTOR),
258 builtin_path = "crate::builtins::missing"
259)]
260async fn ismissing_builtin(value: Value) -> BuiltinResult<Value> {
261 let value = gather_if_needed_async(&value)
262 .await
263 .map_err(|err| invalid_argument(format!("ismissing: failed to gather input: {err}")))?;
264 ismissing_value(&value)
265}
266
267#[runtime_builtin(
268 name = "anymissing",
269 category = "missing",
270 summary = "Return true when an input contains at least one missing value.",
271 keywords = "anymissing,missing,NaN,string,table",
272 accel = "cpu",
273 type_resolver(logical_type),
274 descriptor(crate::builtins::missing::ANYMISSING_DESCRIPTOR),
275 builtin_path = "crate::builtins::missing"
276)]
277async fn anymissing_builtin(value: Value) -> BuiltinResult<Value> {
278 let value = gather_if_needed_async(&value)
279 .await
280 .map_err(|err| invalid_argument(format!("anymissing: failed to gather input: {err}")))?;
281 Ok(Value::Bool(any_missing(&value)?))
282}
283
284#[runtime_builtin(
285 name = "standardizeMissing",
286 category = "missing",
287 summary = "Replace user-specified missing indicators with canonical missing values.",
288 keywords = "standardizeMissing,missing,NaN,string,table",
289 accel = "cpu",
290 type_resolver(any_type),
291 descriptor(crate::builtins::missing::STANDARDIZE_MISSING_DESCRIPTOR),
292 builtin_path = "crate::builtins::missing"
293)]
294async fn standardize_missing_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
295 let value = gather_if_needed_async(&value).await.map_err(|err| {
296 invalid_argument(format!("standardizeMissing: failed to gather input: {err}"))
297 })?;
298 let indicators = rest
299 .first()
300 .ok_or_else(|| invalid_argument("standardizeMissing: missing indicators argument"))?;
301 let indicators = indicator_set(indicators)?;
302 standardize_missing_value(value, &indicators)
303}
304
305#[runtime_builtin(
306 name = "rmmissing",
307 category = "missing",
308 summary = "Remove missing elements, rows, or columns.",
309 keywords = "rmmissing,missing,NaN,string,table",
310 accel = "cpu",
311 type_resolver(any_type),
312 descriptor(crate::builtins::missing::RMMISSING_DESCRIPTOR),
313 builtin_path = "crate::builtins::missing"
314)]
315async fn rmmissing_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
316 let value = gather_if_needed_async(&value)
317 .await
318 .map_err(|err| invalid_argument(format!("rmmissing: failed to gather input: {err}")))?;
319 let options = RemoveOptions::parse(&rest)?;
320 let (result, removed) = remove_missing_value(value, options)?;
321 match crate::output_count::current_output_count() {
322 Some(0) => Ok(Value::OutputList(Vec::new())),
323 Some(1) => Ok(Value::OutputList(vec![result])),
324 Some(n) => Ok(crate::output_count::output_list_with_padding(
325 n,
326 vec![result, Value::LogicalArray(removed)],
327 )),
328 None => Ok(result),
329 }
330}
331
332#[runtime_builtin(
333 name = "fillmissing",
334 category = "missing",
335 summary = "Fill missing entries using constant, neighbor, or summary methods.",
336 keywords = "fillmissing,missing,NaN,string,table,previous,next,linear,constant",
337 accel = "cpu",
338 type_resolver(any_type),
339 descriptor(crate::builtins::missing::FILLMISSING_DESCRIPTOR),
340 builtin_path = "crate::builtins::missing"
341)]
342async fn fillmissing_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
343 let value = gather_if_needed_async(&value)
344 .await
345 .map_err(|err| invalid_argument(format!("fillmissing: failed to gather input: {err}")))?;
346 let options = FillOptions::parse(&rest)?;
347 let (result, mask) = fill_missing_value(value, &options)?;
348 match crate::output_count::current_output_count() {
349 Some(0) => Ok(Value::OutputList(Vec::new())),
350 Some(1) => Ok(Value::OutputList(vec![result])),
351 Some(n) => Ok(crate::output_count::output_list_with_padding(
352 n,
353 vec![result, Value::LogicalArray(mask)],
354 )),
355 None => Ok(result),
356 }
357}
358
359#[runtime_builtin(
360 name = "nanmean",
361 category = "missing",
362 summary = "Mean that ignores NaN values.",
363 keywords = "nanmean,mean,omitnan,missing",
364 accel = "reduction",
365 type_resolver(any_type),
366 descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
367 builtin_path = "crate::builtins::missing"
368)]
369async fn nanmean_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
370 mean::mean_builtin(value, rest_with_omitnan(rest)).await
371}
372
373#[runtime_builtin(
374 name = "nansum",
375 category = "missing",
376 summary = "Sum that ignores NaN values.",
377 keywords = "nansum,sum,omitnan,missing",
378 accel = "reduction",
379 type_resolver(any_type),
380 descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
381 builtin_path = "crate::builtins::missing"
382)]
383async fn nansum_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
384 sum::sum_builtin(value, rest_with_omitnan(rest)).await
385}
386
387#[runtime_builtin(
388 name = "nanmin",
389 category = "missing",
390 summary = "Minimum that ignores NaN values.",
391 keywords = "nanmin,min,omitnan,missing",
392 accel = "reduction",
393 type_resolver(any_type),
394 descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
395 builtin_path = "crate::builtins::missing"
396)]
397async fn nanmin_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
398 if let Some(first) = rest.first() {
399 if is_numeric_data_like(first) {
400 if rest.len() != 1 {
401 return Err(invalid_argument(
402 "nanmin: pairwise form accepts exactly two numeric inputs",
403 ));
404 }
405 return pairwise_nan_min(value, first.clone());
406 }
407 }
408 min::min_builtin(value, rest_with_omitnan(rest)).await
409}
410
411#[runtime_builtin(
412 name = "nanmedian",
413 category = "missing",
414 summary = "Median that ignores NaN values.",
415 keywords = "nanmedian,median,omitnan,missing",
416 accel = "reduction",
417 type_resolver(any_type),
418 descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
419 builtin_path = "crate::builtins::missing"
420)]
421async fn nanmedian_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
422 median::median_builtin(value, rest_with_omitnan(rest)).await
423}
424
425#[runtime_builtin(
426 name = "nanstd",
427 category = "missing",
428 summary = "Standard deviation that ignores NaN values.",
429 keywords = "nanstd,std,omitnan,missing",
430 accel = "reduction",
431 type_resolver(any_type),
432 descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
433 builtin_path = "crate::builtins::missing"
434)]
435async fn nanstd_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
436 std_reduction::std_builtin(value, rest_with_omitnan(rest)).await
437}
438
439#[runtime_builtin(
440 name = "nanvar",
441 category = "missing",
442 summary = "Variance that ignores NaN values.",
443 keywords = "nanvar,var,omitnan,missing",
444 accel = "reduction",
445 type_resolver(any_type),
446 descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
447 builtin_path = "crate::builtins::missing"
448)]
449async fn nanvar_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
450 var::var_builtin(value, rest_with_omitnan(rest)).await
451}
452
453#[runtime_builtin(
454 name = "movmad",
455 category = "missing",
456 summary = "Moving median absolute deviation over vectors and matrix dimensions.",
457 keywords = "movmad,moving,median,absolute,deviation,missing",
458 accel = "cpu",
459 type_resolver(any_type),
460 descriptor(crate::builtins::missing::NAN_AWARE_DESCRIPTOR),
461 builtin_path = "crate::builtins::missing"
462)]
463async fn movmad_builtin(value: Value, window: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
464 let value = gather_if_needed_async(&value)
465 .await
466 .map_err(|err| invalid_argument(format!("movmad: failed to gather input: {err}")))?;
467 let tensor = numeric_tensor(value, "movmad")?;
468 let window = scalar_usize(&window, "movmad window")?;
469 let options = MovingOptions::parse(&rest)?;
470 moving_mad(tensor, window, options)
471}
472
473fn rest_with_omitnan(mut rest: Vec<Value>) -> Vec<Value> {
474 let insert_at = rest
475 .iter()
476 .position(|arg| scalar_text(arg).is_some_and(|text| text.eq_ignore_ascii_case("like")))
477 .unwrap_or(rest.len());
478 rest.insert(insert_at, Value::from("omitnan"));
479 rest
480}
481
482fn parse_size_args(args: &[Value]) -> BuiltinResult<Vec<usize>> {
483 if args.is_empty() {
484 return Ok(vec![1, 1]);
485 }
486 if args.len() == 1 {
487 match &args[0] {
488 Value::Tensor(tensor) => return tensor_shape_as_size(&tensor.data),
489 Value::Int(_) | Value::Num(_) => {
490 let n = scalar_usize(&args[0], "missing size")?;
491 return Ok(vec![n, n]);
492 }
493 Value::String(s) if s.eq_ignore_ascii_case("like") => {
494 return Err(invalid_argument(
495 "missing: 'like' requires a prototype value",
496 ));
497 }
498 _ => {}
499 }
500 }
501 let mut out = Vec::with_capacity(args.len());
502 let mut idx = 0;
503 while idx < args.len() {
504 if scalar_text(&args[idx])
505 .map(|text| text.eq_ignore_ascii_case("like"))
506 .unwrap_or(false)
507 {
508 idx += 2;
509 continue;
510 }
511 out.push(scalar_usize(&args[idx], "missing size")?);
512 idx += 1;
513 }
514 if out.is_empty() {
515 Ok(vec![1, 1])
516 } else {
517 Ok(out)
518 }
519}
520
521fn tensor_shape_as_size(data: &[f64]) -> BuiltinResult<Vec<usize>> {
522 if data.is_empty() {
523 return Ok(vec![0, 0]);
524 }
525 data.iter()
526 .map(|value| {
527 if !value.is_finite() || *value < 0.0 || value.fract() != 0.0 {
528 return Err(invalid_argument(
529 "missing: sizes must be nonnegative finite integers",
530 ));
531 }
532 if *value > usize::MAX as f64 {
533 return Err(invalid_argument("missing: size exceeds platform limits"));
534 }
535 Ok(*value as usize)
536 })
537 .collect()
538}
539
540fn missing_string_array(shape: Vec<usize>) -> BuiltinResult<Value> {
541 let count = element_count(&shape)?;
542 let array =
543 StringArray::new(vec![MISSING_TEXT.to_string(); count], shape).map_err(internal_error)?;
544 Ok(Value::StringArray(array))
545}
546
547fn element_count(shape: &[usize]) -> BuiltinResult<usize> {
548 shape.iter().try_fold(1usize, |acc, dim| {
549 acc.checked_mul(*dim)
550 .ok_or_else(|| invalid_argument("array size is too large"))
551 })
552}
553
554fn ismissing_value(value: &Value) -> BuiltinResult<Value> {
555 match value {
556 Value::Num(n) => Ok(Value::Bool(n.is_nan())),
557 Value::Complex(re, im) => Ok(Value::Bool(re.is_nan() || im.is_nan())),
558 Value::Int(_) | Value::Bool(_) | Value::FunctionHandle(_) | Value::ClassRef(_) => {
559 Ok(Value::Bool(false))
560 }
561 Value::String(s) => Ok(Value::Bool(is_missing_text(s))),
562 Value::CharArray(array) => Ok(Value::LogicalArray(
563 LogicalArray::new(
564 char_rows(array)
565 .into_iter()
566 .map(|text| u8::from(text.trim().is_empty() || is_missing_text(&text)))
567 .collect(),
568 vec![array.rows, 1],
569 )
570 .map_err(internal_error)?,
571 )),
572 Value::StringArray(array) => logical_from_iter(
573 array.data.iter().map(|text| is_missing_text(text)),
574 array.shape.clone(),
575 ),
576 Value::Tensor(tensor) => logical_from_iter(
577 tensor.data.iter().map(|value| value.is_nan()),
578 tensor.shape.clone(),
579 ),
580 Value::ComplexTensor(tensor) => logical_from_iter(
581 tensor
582 .data
583 .iter()
584 .map(|(re, im)| re.is_nan() || im.is_nan()),
585 tensor.shape.clone(),
586 ),
587 Value::SparseTensor(tensor) => {
588 let mut data = vec![0u8; tensor.rows * tensor.cols];
589 for col in 0..tensor.cols {
590 for idx in tensor.col_ptrs[col]..tensor.col_ptrs[col + 1] {
591 if tensor.values[idx].is_nan() {
592 data[tensor.row_indices[idx] + col * tensor.rows] = 1;
593 }
594 }
595 }
596 Ok(Value::LogicalArray(
597 LogicalArray::new(data, vec![tensor.rows, tensor.cols]).map_err(internal_error)?,
598 ))
599 }
600 Value::LogicalArray(array) => Ok(Value::LogicalArray(LogicalArray::zeros(
601 array.shape.clone(),
602 ))),
603 Value::Cell(cell) => {
604 let mut data = Vec::with_capacity(cell.data.len());
605 for item in &cell.data {
606 data.push(u8::from(any_missing(item)?));
607 }
608 Ok(Value::LogicalArray(
609 LogicalArray::new(data, vec![cell.rows, cell.cols]).map_err(internal_error)?,
610 ))
611 }
612 Value::Struct(st) => {
613 let mut out = StructValue::new();
614 for (name, field) in &st.fields {
615 out.insert(name.clone(), ismissing_value(field)?);
616 }
617 Ok(Value::Struct(out))
618 }
619 Value::Object(object) if is_tabular_object(object) => ismissing_table(object),
620 Value::Object(object) if object.is_class("datetime") => {
621 let serials = crate::builtins::datetime::serials_from_datetime_value(value)?;
622 logical_from_iter(
623 serials.data.iter().map(|serial| serial.is_nan()),
624 serials.shape,
625 )
626 }
627 Value::Object(object) if object.is_class("duration") => {
628 let days = crate::builtins::duration::duration_tensor_from_duration_value(value)?;
629 logical_from_iter(days.data.iter().map(|day| day.is_nan()), days.shape)
630 }
631 Value::OutputList(values) => {
632 let mut data = Vec::with_capacity(values.len());
633 for item in values {
634 data.push(u8::from(any_missing(item)?));
635 }
636 Ok(Value::LogicalArray(
637 LogicalArray::new(data, vec![1, values.len()]).map_err(internal_error)?,
638 ))
639 }
640 _ => Ok(Value::Bool(false)),
641 }
642}
643
644fn ismissing_table(object: &ObjectInstance) -> BuiltinResult<Value> {
645 let height = table_height(object)?;
646 let width = table_width(object)?;
647 let names = table_variable_names_from_object(object)?;
648 let variables = table_variables(object)?;
649 let mut data = vec![0u8; height * width];
650 for (col, name) in names.iter().enumerate() {
651 let Some(value) = variables.fields.get(name) else {
652 continue;
653 };
654 let mask = logical_mask_for_rows(value, height)?;
655 for row in 0..height {
656 if mask.get(row).copied().unwrap_or(0) != 0 {
657 data[row + col * height] = 1;
658 }
659 }
660 }
661 Ok(Value::LogicalArray(
662 LogicalArray::new(data, vec![height, width]).map_err(internal_error)?,
663 ))
664}
665
666fn logical_from_iter<I>(iter: I, shape: Vec<usize>) -> BuiltinResult<Value>
667where
668 I: IntoIterator<Item = bool>,
669{
670 Ok(Value::LogicalArray(
671 LogicalArray::new(iter.into_iter().map(u8::from).collect(), shape)
672 .map_err(internal_error)?,
673 ))
674}
675
676fn any_missing(value: &Value) -> BuiltinResult<bool> {
677 match ismissing_value(value)? {
678 Value::Bool(flag) => Ok(flag),
679 Value::LogicalArray(array) => Ok(array.data.iter().any(|flag| *flag != 0)),
680 Value::Struct(st) => {
681 for field in st.fields.values() {
682 if any_missing(field)? {
683 return Ok(true);
684 }
685 }
686 Ok(false)
687 }
688 _ => Ok(false),
689 }
690}
691
692fn logical_mask_for_rows(value: &Value, expected_rows: usize) -> BuiltinResult<Vec<u8>> {
693 let mask_value = ismissing_value(value)?;
694 match mask_value {
695 Value::Bool(flag) => Ok(vec![u8::from(flag); expected_rows]),
696 Value::LogicalArray(mask) => logical_array_mask_for_rows(&mask, expected_rows),
697 _ => Err(unsupported_type("cannot build row missing mask for value")),
698 }
699}
700
701fn logical_array_mask_for_rows(
702 mask: &LogicalArray,
703 expected_rows: usize,
704) -> BuiltinResult<Vec<u8>> {
705 let rows = mask.shape.first().copied().unwrap_or(mask.data.len());
706 let cols = mask.shape.get(1).copied().unwrap_or(1);
707 if rows == expected_rows {
708 let mut out = vec![0u8; expected_rows];
709 for col in 0..cols {
710 for (row, slot) in out.iter_mut().enumerate().take(expected_rows) {
711 let idx = row + col * rows;
712 if mask.data.get(idx).copied().unwrap_or(0) != 0 {
713 *slot = 1;
714 }
715 }
716 }
717 Ok(out)
718 } else if mask.data.len() == expected_rows {
719 Ok(mask.data.clone())
720 } else {
721 Err(invalid_argument(
722 "missing mask shape does not match table height",
723 ))
724 }
725}
726
727#[derive(Clone, Copy)]
728enum RemoveDim {
729 Auto,
730 Rows,
731 Columns,
732}
733
734#[derive(Clone, Copy)]
735struct RemoveOptions {
736 dim: RemoveDim,
737}
738
739impl RemoveOptions {
740 fn parse(args: &[Value]) -> BuiltinResult<Self> {
741 let mut dim = RemoveDim::Auto;
742 let mut idx = 0;
743 while idx < args.len() {
744 if let Some(text) = scalar_text(&args[idx]) {
745 match text.to_ascii_lowercase().as_str() {
746 "dim" if idx + 1 < args.len() => {
747 dim = dim_from_value(&args[idx + 1])?;
748 idx += 2;
749 continue;
750 }
751 "dim" => return Err(invalid_argument("rmmissing: 'dim' requires a value")),
752 "rows" => {
753 dim = RemoveDim::Rows;
754 idx += 1;
755 continue;
756 }
757 "columns" | "cols" => {
758 dim = RemoveDim::Columns;
759 idx += 1;
760 continue;
761 }
762 other => {
763 return Err(invalid_argument(format!(
764 "rmmissing: unsupported option '{other}'"
765 )))
766 }
767 }
768 }
769 if matches!(args[idx], Value::Num(_) | Value::Int(_)) {
770 dim = dim_from_value(&args[idx])?;
771 idx += 1;
772 continue;
773 }
774 return Err(invalid_argument(format!(
775 "rmmissing: unsupported option argument {:?}",
776 args[idx]
777 )));
778 }
779 Ok(Self { dim })
780 }
781}
782
783fn dim_from_value(value: &Value) -> BuiltinResult<RemoveDim> {
784 match scalar_usize(value, "dimension")? {
785 1 => Ok(RemoveDim::Rows),
786 2 => Ok(RemoveDim::Columns),
787 _ => Err(invalid_argument("dimension must be 1 or 2 for rmmissing")),
788 }
789}
790
791fn remove_missing_value(
792 value: Value,
793 options: RemoveOptions,
794) -> BuiltinResult<(Value, LogicalArray)> {
795 match value {
796 Value::Object(object) if is_tabular_object(&object) => {
797 remove_missing_table(object, options)
798 }
799 Value::Tensor(tensor) => remove_missing_tensor(tensor, options),
800 Value::StringArray(array) => remove_missing_string_array(array, options),
801 Value::LogicalArray(array) => remove_missing_logical_array(array, options),
802 Value::Cell(cell) => remove_missing_cell(cell, options),
803 other => {
804 if any_missing(&other)? {
805 Ok((
806 empty_like(other)?,
807 LogicalArray::new(vec![1], vec![1, 1]).map_err(internal_error)?,
808 ))
809 } else {
810 Ok((
811 other,
812 LogicalArray::new(vec![0], vec![1, 1]).map_err(internal_error)?,
813 ))
814 }
815 }
816 }
817}
818
819fn remove_missing_table(
820 object: ObjectInstance,
821 options: RemoveOptions,
822) -> BuiltinResult<(Value, LogicalArray)> {
823 let height = table_height(&object)?;
824 let width = table_width(&object)?;
825 let names = table_variable_names_from_object(&object)?;
826 let variables = table_variables(&object)?;
827 let remove_columns = matches!(options.dim, RemoveDim::Columns);
828 if remove_columns {
829 let mut keep_names = Vec::new();
830 let mut removed = Vec::with_capacity(width);
831 let mut keep_values = Vec::new();
832 for name in &names {
833 let value = variables
834 .fields
835 .get(name)
836 .ok_or_else(|| internal_error(format!("table missing variable {name}")))?;
837 let has_missing = any_missing(value)?;
838 removed.push(u8::from(has_missing));
839 if !has_missing {
840 keep_names.push(name.clone());
841 keep_values.push(value.clone());
842 }
843 }
844 let row_names = selected_row_names(&object, &(0..height).collect::<Vec<_>>())?;
845 Ok((
846 table_from_columns_like(&object, keep_names, keep_values, row_names, None)?,
847 LogicalArray::new(removed, vec![1, width]).map_err(internal_error)?,
848 ))
849 } else {
850 let mut row_has_missing = vec![0u8; height];
851 for name in &names {
852 if let Some(value) = variables.fields.get(name) {
853 let mask = logical_mask_for_rows(value, height)?;
854 for row in 0..height {
855 if mask[row] != 0 {
856 row_has_missing[row] = 1;
857 }
858 }
859 }
860 }
861 let keep_rows: Vec<usize> = row_has_missing
862 .iter()
863 .enumerate()
864 .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
865 .collect();
866 let mut values = Vec::with_capacity(names.len());
867 for name in &names {
868 let value = variables
869 .fields
870 .get(name)
871 .ok_or_else(|| internal_error(format!("table missing variable {name}")))?;
872 values.push(select_rows(value, &keep_rows)?);
873 }
874 let row_names = selected_row_names(&object, &keep_rows)?;
875 Ok((
876 table_from_columns_like(&object, names, values, row_names, Some(&keep_rows))?,
877 LogicalArray::new(row_has_missing, vec![height, 1]).map_err(internal_error)?,
878 ))
879 }
880}
881
882fn remove_missing_tensor(
883 tensor: Tensor,
884 options: RemoveOptions,
885) -> BuiltinResult<(Value, LogicalArray)> {
886 if tensor.rows() == 1 || tensor.cols() == 1 {
887 let mut data = Vec::new();
888 let mut removed = Vec::with_capacity(tensor.data.len());
889 let source_rows = tensor.rows();
890 let source_is_row = source_rows == 1;
891 for value in tensor.data {
892 if value.is_nan() {
893 removed.push(1);
894 } else {
895 removed.push(0);
896 data.push(value);
897 }
898 }
899 let shape = if source_is_row {
900 vec![1, data.len()]
901 } else {
902 vec![data.len(), 1]
903 };
904 let removed_len = removed.len();
905 return Ok((
906 Value::Tensor(
907 Tensor::new_with_dtype(data, shape, tensor.dtype).map_err(internal_error)?,
908 ),
909 LogicalArray::new(removed, vec![1, removed_len]).map_err(internal_error)?,
910 ));
911 }
912 let remove_columns = matches!(options.dim, RemoveDim::Columns);
913 if remove_columns {
914 remove_missing_columns_tensor(tensor)
915 } else {
916 remove_missing_rows_tensor(tensor)
917 }
918}
919
920fn remove_missing_rows_tensor(tensor: Tensor) -> BuiltinResult<(Value, LogicalArray)> {
921 let rows = tensor.rows();
922 let cols = tensor.cols();
923 let mut removed = vec![0u8; rows];
924 for col in 0..cols {
925 for (row, slot) in removed.iter_mut().enumerate().take(rows) {
926 if tensor.get2(row, col).map_err(internal_error)?.is_nan() {
927 *slot = 1;
928 }
929 }
930 }
931 let keep: Vec<usize> = removed
932 .iter()
933 .enumerate()
934 .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
935 .collect();
936 let out = select_rows(&Value::Tensor(tensor), &keep)?;
937 Ok((
938 out,
939 LogicalArray::new(removed, vec![rows, 1]).map_err(internal_error)?,
940 ))
941}
942
943fn remove_missing_columns_tensor(tensor: Tensor) -> BuiltinResult<(Value, LogicalArray)> {
944 let rows = tensor.rows();
945 let cols = tensor.cols();
946 let mut removed = vec![0u8; cols];
947 for (col, slot) in removed.iter_mut().enumerate().take(cols) {
948 for row in 0..rows {
949 if tensor.get2(row, col).map_err(internal_error)?.is_nan() {
950 *slot = 1;
951 }
952 }
953 }
954 let keep_cols: Vec<usize> = removed
955 .iter()
956 .enumerate()
957 .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
958 .collect();
959 let mut data = Vec::with_capacity(rows * keep_cols.len());
960 for col in keep_cols {
961 for row in 0..rows {
962 data.push(tensor.get2(row, col).map_err(internal_error)?);
963 }
964 }
965 Ok((
966 Value::Tensor(
967 Tensor::new_with_dtype(
968 data,
969 vec![rows, removed.iter().filter(|f| **f == 0).count()],
970 tensor.dtype,
971 )
972 .map_err(internal_error)?,
973 ),
974 LogicalArray::new(removed, vec![1, cols]).map_err(internal_error)?,
975 ))
976}
977
978fn remove_missing_string_array(
979 array: StringArray,
980 options: RemoveOptions,
981) -> BuiltinResult<(Value, LogicalArray)> {
982 let rows = array.rows();
983 let cols = array.cols();
984 let shape = array.shape.clone();
985 remove_missing_column_major(
986 array.data,
987 rows,
988 cols,
989 shape,
990 options,
991 |text| is_missing_text(text),
992 |data, shape| {
993 StringArray::new(data, shape)
994 .map(Value::StringArray)
995 .map_err(internal_error)
996 },
997 )
998}
999
1000fn remove_missing_logical_array(
1001 array: LogicalArray,
1002 options: RemoveOptions,
1003) -> BuiltinResult<(Value, LogicalArray)> {
1004 let rows = array.shape.first().copied().unwrap_or(array.data.len());
1005 let cols = array.shape.get(1).copied().unwrap_or(1);
1006 remove_missing_column_major(
1007 array.data,
1008 rows,
1009 cols,
1010 array.shape.clone(),
1011 options,
1012 |_| false,
1013 |data, shape| {
1014 LogicalArray::new(data, shape)
1015 .map(Value::LogicalArray)
1016 .map_err(internal_error)
1017 },
1018 )
1019}
1020
1021fn remove_missing_cell(
1022 cell: CellArray,
1023 options: RemoveOptions,
1024) -> BuiltinResult<(Value, LogicalArray)> {
1025 let rows = cell.rows;
1026 let cols = cell.cols;
1027 let is_missing = |row: usize, col: usize| -> bool {
1028 cell.get(row, col)
1029 .ok()
1030 .and_then(|value| any_missing(&value).ok())
1031 .unwrap_or(false)
1032 };
1033
1034 if rows == 1 || cols == 1 {
1035 let mut out = Vec::new();
1036 let mut removed = Vec::with_capacity(cell.data.len());
1037 for value in cell.data {
1038 if any_missing(&value).unwrap_or(false) {
1039 removed.push(1);
1040 } else {
1041 removed.push(0);
1042 out.push(value);
1043 }
1044 }
1045 let out_shape = if rows == 1 {
1046 vec![1, out.len()]
1047 } else {
1048 vec![out.len(), 1]
1049 };
1050 let removed_len = removed.len();
1051 let out_rows = out_shape.first().copied().unwrap_or(0);
1052 let out_cols = out_shape.get(1).copied().unwrap_or(0);
1053 return Ok((
1054 CellArray::new(out, out_rows, out_cols)
1055 .map(Value::Cell)
1056 .map_err(internal_error)?,
1057 LogicalArray::new(removed, vec![1, removed_len]).map_err(internal_error)?,
1058 ));
1059 }
1060
1061 if matches!(options.dim, RemoveDim::Columns) {
1062 let mut removed = vec![0u8; cols];
1063 for col in 0..cols {
1064 for row in 0..rows {
1065 if is_missing(row, col) {
1066 removed[col] = 1;
1067 }
1068 }
1069 }
1070 let kept_cols: Vec<usize> = removed
1071 .iter()
1072 .enumerate()
1073 .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
1074 .collect();
1075 let mut out = Vec::with_capacity(rows * kept_cols.len());
1076 for row in 0..rows {
1077 for col in &kept_cols {
1078 out.push(cell.get(row, *col).map_err(internal_error)?);
1079 }
1080 }
1081 let kept = kept_cols.len();
1082 Ok((
1083 CellArray::new(out, rows, kept)
1084 .map(Value::Cell)
1085 .map_err(internal_error)?,
1086 LogicalArray::new(removed, vec![1, cols]).map_err(internal_error)?,
1087 ))
1088 } else {
1089 let mut removed = vec![0u8; rows];
1090 for row in 0..rows {
1091 for col in 0..cols {
1092 if is_missing(row, col) {
1093 removed[row] = 1;
1094 }
1095 }
1096 }
1097 let kept_rows: Vec<usize> = removed
1098 .iter()
1099 .enumerate()
1100 .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
1101 .collect();
1102 let mut out = Vec::with_capacity(kept_rows.len() * cols);
1103 for row in &kept_rows {
1104 for col in 0..cols {
1105 out.push(cell.get(*row, col).map_err(internal_error)?);
1106 }
1107 }
1108 Ok((
1109 CellArray::new(out, kept_rows.len(), cols)
1110 .map(Value::Cell)
1111 .map_err(internal_error)?,
1112 LogicalArray::new(removed, vec![rows, 1]).map_err(internal_error)?,
1113 ))
1114 }
1115}
1116
1117fn remove_missing_column_major<T: Clone>(
1118 data: Vec<T>,
1119 rows: usize,
1120 cols: usize,
1121 _shape: Vec<usize>,
1122 options: RemoveOptions,
1123 is_missing: impl Fn(&T) -> bool,
1124 build: impl Fn(Vec<T>, Vec<usize>) -> BuiltinResult<Value>,
1125) -> BuiltinResult<(Value, LogicalArray)> {
1126 if rows == 1 || cols == 1 {
1127 let mut out = Vec::new();
1128 let mut removed = Vec::with_capacity(data.len());
1129 for value in data {
1130 if is_missing(&value) {
1131 removed.push(1);
1132 } else {
1133 removed.push(0);
1134 out.push(value);
1135 }
1136 }
1137 let out_shape = if rows == 1 {
1138 vec![1, out.len()]
1139 } else {
1140 vec![out.len(), 1]
1141 };
1142 let removed_len = removed.len();
1143 return Ok((
1144 build(out, out_shape)?,
1145 LogicalArray::new(removed, vec![1, removed_len]).map_err(internal_error)?,
1146 ));
1147 }
1148 if matches!(options.dim, RemoveDim::Columns) {
1149 let mut removed = vec![0u8; cols];
1150 for col in 0..cols {
1151 for row in 0..rows {
1152 if is_missing(&data[row + col * rows]) {
1153 removed[col] = 1;
1154 }
1155 }
1156 }
1157 let kept_cols: Vec<usize> = removed
1158 .iter()
1159 .enumerate()
1160 .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
1161 .collect();
1162 let mut out = Vec::with_capacity(rows * kept_cols.len());
1163 for col in kept_cols {
1164 for row in 0..rows {
1165 out.push(data[row + col * rows].clone());
1166 }
1167 }
1168 let kept = removed.iter().filter(|flag| **flag == 0).count();
1169 Ok((
1170 build(out, vec![rows, kept])?,
1171 LogicalArray::new(removed, vec![1, cols]).map_err(internal_error)?,
1172 ))
1173 } else {
1174 let mut removed = vec![0u8; rows];
1175 for col in 0..cols {
1176 for row in 0..rows {
1177 if is_missing(&data[row + col * rows]) {
1178 removed[row] = 1;
1179 }
1180 }
1181 }
1182 let kept_rows: Vec<usize> = removed
1183 .iter()
1184 .enumerate()
1185 .filter_map(|(idx, flag)| (*flag == 0).then_some(idx))
1186 .collect();
1187 let mut out = Vec::with_capacity(kept_rows.len() * cols);
1188 for col in 0..cols {
1189 for row in &kept_rows {
1190 out.push(data[*row + col * rows].clone());
1191 }
1192 }
1193 Ok((
1194 build(out, vec![kept_rows.len(), cols])?,
1195 LogicalArray::new(removed, vec![rows, 1]).map_err(internal_error)?,
1196 ))
1197 }
1198}
1199
1200fn empty_like(value: Value) -> BuiltinResult<Value> {
1201 match value {
1202 Value::Tensor(tensor) => Tensor::new_with_dtype(Vec::new(), vec![0, 0], tensor.dtype)
1203 .map(Value::Tensor)
1204 .map_err(internal_error),
1205 Value::StringArray(_) | Value::String(_) => StringArray::new(Vec::new(), vec![0, 0])
1206 .map(Value::StringArray)
1207 .map_err(internal_error),
1208 Value::Cell(_) => CellArray::new(Vec::new(), 0, 0)
1209 .map(Value::Cell)
1210 .map_err(internal_error),
1211 _ => Ok(Value::OutputList(Vec::new())),
1212 }
1213}
1214
1215#[derive(Clone)]
1216enum FillMethod {
1217 Constant(Value),
1218 Previous,
1219 Next,
1220 Nearest,
1221 Linear,
1222 Mean,
1223 Median,
1224}
1225
1226#[derive(Clone)]
1227struct FillOptions {
1228 method: FillMethod,
1229 dim: Option<usize>,
1230}
1231
1232impl FillOptions {
1233 fn parse(args: &[Value]) -> BuiltinResult<Self> {
1234 if args.is_empty() {
1235 return Err(invalid_argument("fillmissing: method is required"));
1236 }
1237 let mut idx = 0;
1238 let method_text = scalar_text(&args[idx])
1239 .ok_or_else(|| invalid_argument("fillmissing: method must be a string"))?
1240 .to_ascii_lowercase();
1241 idx += 1;
1242 let method = match method_text.as_str() {
1243 "constant" => {
1244 let fill = args
1245 .get(idx)
1246 .ok_or_else(|| {
1247 invalid_argument("fillmissing: constant method needs a fill value")
1248 })?
1249 .clone();
1250 idx += 1;
1251 FillMethod::Constant(fill)
1252 }
1253 "previous" => FillMethod::Previous,
1254 "next" => FillMethod::Next,
1255 "nearest" => FillMethod::Nearest,
1256 "linear" => FillMethod::Linear,
1257 "mean" => FillMethod::Mean,
1258 "median" => FillMethod::Median,
1259 other => {
1260 return Err(invalid_argument(format!(
1261 "fillmissing: unsupported method '{other}'"
1262 )))
1263 }
1264 };
1265 let mut dim = None;
1266 while idx < args.len() {
1267 if let Some(text) = scalar_text(&args[idx]) {
1268 if text.eq_ignore_ascii_case("dim") && idx + 1 < args.len() {
1269 dim = Some(scalar_usize(&args[idx + 1], "fillmissing dimension")?);
1270 idx += 2;
1271 continue;
1272 }
1273 if text.eq_ignore_ascii_case("dim") {
1274 return Err(invalid_argument("fillmissing: 'dim' requires a value"));
1275 }
1276 return Err(invalid_argument(format!(
1277 "fillmissing: unsupported option '{text}'"
1278 )));
1279 }
1280 if matches!(args[idx], Value::Num(_) | Value::Int(_)) {
1281 dim = Some(scalar_usize(&args[idx], "fillmissing dimension")?);
1282 idx += 1;
1283 continue;
1284 }
1285 return Err(invalid_argument(format!(
1286 "fillmissing: unsupported option argument {:?}",
1287 args[idx]
1288 )));
1289 }
1290 if dim.is_some_and(|dim| dim != 1 && dim != 2) {
1291 return Err(invalid_argument("fillmissing: dimension must be 1 or 2"));
1292 }
1293 Ok(Self { method, dim })
1294 }
1295}
1296
1297fn fill_missing_value(value: Value, options: &FillOptions) -> BuiltinResult<(Value, LogicalArray)> {
1298 match value {
1299 Value::Tensor(tensor) => fill_missing_tensor(tensor, options),
1300 Value::StringArray(array) => fill_missing_string_array(array, options),
1301 Value::Object(object) if is_tabular_object(&object) => fill_missing_table(object, options),
1302 Value::Cell(cell) => fill_missing_cell(cell, options),
1303 other => {
1304 let missing = any_missing(&other)?;
1305 let mask =
1306 LogicalArray::new(vec![u8::from(missing)], vec![1, 1]).map_err(internal_error)?;
1307 if missing {
1308 match &options.method {
1309 FillMethod::Constant(fill) => Ok((fill.clone(), mask)),
1310 _ => Err(unsupported_type(
1311 "fillmissing: scalar fill requires the constant method",
1312 )),
1313 }
1314 } else {
1315 Ok((other, mask))
1316 }
1317 }
1318 }
1319}
1320
1321fn fill_missing_table(
1322 mut object: ObjectInstance,
1323 options: &FillOptions,
1324) -> BuiltinResult<(Value, LogicalArray)> {
1325 let height = table_height(&object)?;
1326 let width = table_width(&object)?;
1327 let names = table_variable_names_from_object(&object)?;
1328 let variables = table_variables(&object)?;
1329 let mut output_vars = StructValue::new();
1330 let mut mask_data = vec![0u8; height * width];
1331 for (col, name) in names.iter().enumerate() {
1332 let value = variables
1333 .fields
1334 .get(name)
1335 .ok_or_else(|| internal_error(format!("table missing variable {name}")))?
1336 .clone();
1337 let (filled, mask) = fill_missing_value(value, options)?;
1338 let row_mask = logical_array_mask_for_rows(&mask, height)?;
1339 for row in 0..height {
1340 if row_mask.get(row).copied().unwrap_or(0) != 0 {
1341 mask_data[row + col * height] = 1;
1342 }
1343 }
1344 output_vars.insert(name.clone(), filled);
1345 }
1346 object
1347 .properties
1348 .insert("__table_variables".to_string(), Value::Struct(output_vars));
1349 Ok((
1350 Value::Object(object),
1351 LogicalArray::new(mask_data, vec![height, width]).map_err(internal_error)?,
1352 ))
1353}
1354
1355fn fill_missing_tensor(
1356 tensor: Tensor,
1357 options: &FillOptions,
1358) -> BuiltinResult<(Value, LogicalArray)> {
1359 let rows = tensor.rows();
1360 let cols = tensor.cols();
1361 let dim = options
1362 .dim
1363 .unwrap_or_else(|| first_nonsingleton_dim(rows, cols));
1364 validate_matrix_dim(dim, "fillmissing")?;
1365 let mut data = tensor.data.clone();
1366 let mask: Vec<u8> = data.iter().map(|value| u8::from(value.is_nan())).collect();
1367 match &options.method {
1368 FillMethod::Constant(fill) => {
1369 let fill = numeric_scalar(fill, "fillmissing constant")?;
1370 for (idx, value) in data.iter_mut().enumerate() {
1371 if mask[idx] != 0 {
1372 *value = fill;
1373 }
1374 }
1375 }
1376 FillMethod::Mean => fill_summary_numeric(&mut data, rows, cols, dim, Summary::Mean),
1377 FillMethod::Median => fill_summary_numeric(&mut data, rows, cols, dim, Summary::Median),
1378 FillMethod::Previous => {
1379 fill_neighbor_numeric(&mut data, rows, cols, dim, Neighbor::Previous)
1380 }
1381 FillMethod::Next => fill_neighbor_numeric(&mut data, rows, cols, dim, Neighbor::Next),
1382 FillMethod::Nearest => fill_nearest_numeric(&mut data, rows, cols, dim),
1383 FillMethod::Linear => fill_linear_numeric(&mut data, rows, cols, dim),
1384 }
1385 Ok((
1386 Value::Tensor(
1387 Tensor::new_with_dtype(data, tensor.shape, tensor.dtype).map_err(internal_error)?,
1388 ),
1389 LogicalArray::new(mask, vec![rows, cols]).map_err(internal_error)?,
1390 ))
1391}
1392
1393fn fill_missing_string_array(
1394 array: StringArray,
1395 options: &FillOptions,
1396) -> BuiltinResult<(Value, LogicalArray)> {
1397 let rows = array.rows();
1398 let cols = array.cols();
1399 let dim = options
1400 .dim
1401 .unwrap_or_else(|| first_nonsingleton_dim(rows, cols));
1402 validate_matrix_dim(dim, "fillmissing")?;
1403 let mut data = array.data.clone();
1404 let mask: Vec<u8> = data
1405 .iter()
1406 .map(|text| u8::from(is_missing_text(text)))
1407 .collect();
1408 match &options.method {
1409 FillMethod::Constant(fill) => {
1410 let fill = scalar_text(fill)
1411 .ok_or_else(|| invalid_argument("fillmissing: string constant must be text"))?;
1412 for (idx, text) in data.iter_mut().enumerate() {
1413 if mask[idx] != 0 {
1414 *text = fill.clone();
1415 }
1416 }
1417 }
1418 FillMethod::Previous => fill_neighbor_text(&mut data, rows, cols, dim, Neighbor::Previous),
1419 FillMethod::Next => fill_neighbor_text(&mut data, rows, cols, dim, Neighbor::Next),
1420 FillMethod::Nearest => fill_nearest_text(&mut data, rows, cols, dim),
1421 _ => {
1422 return Err(unsupported_type(
1423 "fillmissing: string arrays support constant, previous, next, and nearest",
1424 ))
1425 }
1426 }
1427 Ok((
1428 Value::StringArray(StringArray::new(data, array.shape).map_err(internal_error)?),
1429 LogicalArray::new(mask, vec![rows, cols]).map_err(internal_error)?,
1430 ))
1431}
1432
1433fn fill_missing_cell(
1434 cell: CellArray,
1435 options: &FillOptions,
1436) -> BuiltinResult<(Value, LogicalArray)> {
1437 let mut data = cell.data.clone();
1438 let mut mask = Vec::with_capacity(data.len());
1439 for item in &data {
1440 mask.push(u8::from(any_missing(item)?));
1441 }
1442 match &options.method {
1443 FillMethod::Constant(fill) => {
1444 for (idx, item) in data.iter_mut().enumerate() {
1445 if mask[idx] != 0 {
1446 *item = fill.clone();
1447 }
1448 }
1449 }
1450 _ => {
1451 return Err(unsupported_type(
1452 "fillmissing: cell arrays currently support the constant method",
1453 ))
1454 }
1455 }
1456 Ok((
1457 Value::Cell(CellArray::new(data, cell.rows, cell.cols).map_err(internal_error)?),
1458 LogicalArray::new(mask, vec![cell.rows, cell.cols]).map_err(internal_error)?,
1459 ))
1460}
1461
1462enum Summary {
1463 Mean,
1464 Median,
1465}
1466
1467fn fill_summary_numeric(data: &mut [f64], rows: usize, cols: usize, dim: usize, summary: Summary) {
1468 if dim == 1 {
1469 for col in 0..cols {
1470 let vals = finite_slice(data, rows, col, true);
1471 let replacement = summary_value(vals, &summary);
1472 for row in 0..rows {
1473 let idx = row + col * rows;
1474 if data[idx].is_nan() {
1475 data[idx] = replacement;
1476 }
1477 }
1478 }
1479 } else {
1480 for row in 0..rows {
1481 let vals = finite_slice(data, rows, row, false);
1482 let replacement = summary_value(vals, &summary);
1483 for col in 0..cols {
1484 let idx = row + col * rows;
1485 if data[idx].is_nan() {
1486 data[idx] = replacement;
1487 }
1488 }
1489 }
1490 }
1491}
1492
1493fn finite_slice(data: &[f64], rows: usize, fixed: usize, along_rows: bool) -> Vec<f64> {
1494 let mut out = Vec::new();
1495 if along_rows {
1496 let cols = data.len() / rows;
1497 for row in 0..rows {
1498 let value = data[row + fixed * rows];
1499 if !value.is_nan() {
1500 out.push(value);
1501 }
1502 }
1503 debug_assert!(fixed < cols);
1504 } else {
1505 let cols = data.len() / rows;
1506 for col in 0..cols {
1507 let value = data[fixed + col * rows];
1508 if !value.is_nan() {
1509 out.push(value);
1510 }
1511 }
1512 }
1513 out
1514}
1515
1516fn summary_value(mut vals: Vec<f64>, summary: &Summary) -> f64 {
1517 if vals.is_empty() {
1518 return f64::NAN;
1519 }
1520 match summary {
1521 Summary::Mean => vals.iter().sum::<f64>() / vals.len() as f64,
1522 Summary::Median => {
1523 vals.sort_by(|a, b| a.total_cmp(b));
1524 let mid = vals.len() / 2;
1525 if vals.len().is_multiple_of(2) {
1526 (vals[mid - 1] + vals[mid]) / 2.0
1527 } else {
1528 vals[mid]
1529 }
1530 }
1531 }
1532}
1533
1534#[derive(Clone, Copy)]
1535enum Neighbor {
1536 Previous,
1537 Next,
1538}
1539
1540fn fill_neighbor_numeric(data: &mut [f64], rows: usize, cols: usize, dim: usize, dir: Neighbor) {
1541 if dim == 1 {
1542 for col in 0..cols {
1543 fill_line_numeric(data, rows, col, rows, 1, dir);
1544 }
1545 } else {
1546 for row in 0..rows {
1547 fill_line_numeric(data, rows, row, cols, rows, dir);
1548 }
1549 }
1550}
1551
1552fn fill_line_numeric(
1553 data: &mut [f64],
1554 _rows: usize,
1555 start: usize,
1556 len: usize,
1557 step: usize,
1558 dir: Neighbor,
1559) {
1560 match dir {
1561 Neighbor::Previous => {
1562 let mut last = None;
1563 for i in 0..len {
1564 let idx = start + i * step;
1565 if data[idx].is_nan() {
1566 if let Some(value) = last {
1567 data[idx] = value;
1568 }
1569 } else {
1570 last = Some(data[idx]);
1571 }
1572 }
1573 }
1574 Neighbor::Next => {
1575 let mut next = None;
1576 for i in (0..len).rev() {
1577 let idx = start + i * step;
1578 if data[idx].is_nan() {
1579 if let Some(value) = next {
1580 data[idx] = value;
1581 }
1582 } else {
1583 next = Some(data[idx]);
1584 }
1585 }
1586 }
1587 }
1588}
1589
1590fn fill_nearest_numeric(data: &mut [f64], rows: usize, cols: usize, dim: usize) {
1591 let original = data.to_vec();
1592 if dim == 1 {
1593 for col in 0..cols {
1594 fill_nearest_line_numeric(&original, data, col * rows, rows, 1);
1595 }
1596 } else {
1597 for row in 0..rows {
1598 fill_nearest_line_numeric(&original, data, row, cols, rows);
1599 }
1600 }
1601}
1602
1603fn fill_nearest_line_numeric(
1604 original: &[f64],
1605 data: &mut [f64],
1606 start: usize,
1607 len: usize,
1608 step: usize,
1609) {
1610 for i in 0..len {
1611 let idx = start + i * step;
1612 if !original[idx].is_nan() {
1613 continue;
1614 }
1615 let prev = (0..i).rev().find_map(|j| {
1616 let value = original[start + j * step];
1617 (!value.is_nan()).then_some((i - j, value))
1618 });
1619 let next = ((i + 1)..len).find_map(|j| {
1620 let value = original[start + j * step];
1621 (!value.is_nan()).then_some((j - i, value))
1622 });
1623 data[idx] = match (prev, next) {
1624 (Some((pd, _)), Some((nd, nv))) if nd < pd => nv,
1625 (Some((_, pv)), _) => pv,
1626 (_, Some((_, nv))) => nv,
1627 _ => f64::NAN,
1628 };
1629 }
1630}
1631
1632fn fill_linear_numeric(data: &mut [f64], rows: usize, cols: usize, dim: usize) {
1633 if dim == 1 {
1634 for col in 0..cols {
1635 fill_linear_line(data, col * rows, rows, 1);
1636 }
1637 } else {
1638 for row in 0..rows {
1639 fill_linear_line(data, row, cols, rows);
1640 }
1641 }
1642}
1643
1644fn fill_linear_line(data: &mut [f64], start: usize, len: usize, step: usize) {
1645 let mut i = 0;
1646 while i < len {
1647 let idx = start + i * step;
1648 if !data[idx].is_nan() {
1649 i += 1;
1650 continue;
1651 }
1652 let run_start = i;
1653 while i < len && data[start + i * step].is_nan() {
1654 i += 1;
1655 }
1656 let run_end = i;
1657 let prev = (run_start > 0).then(|| data[start + (run_start - 1) * step]);
1658 let next = (run_end < len).then(|| data[start + run_end * step]);
1659 match (prev, next) {
1660 (Some(a), Some(b)) if !a.is_nan() && !b.is_nan() => {
1661 let span = (run_end - run_start + 1) as f64;
1662 for (offset, pos) in (run_start..run_end).enumerate() {
1663 data[start + pos * step] = a + (b - a) * ((offset + 1) as f64 / span);
1664 }
1665 }
1666 (Some(a), _) if !a.is_nan() => {
1667 for pos in run_start..run_end {
1668 data[start + pos * step] = a;
1669 }
1670 }
1671 (_, Some(b)) if !b.is_nan() => {
1672 for pos in run_start..run_end {
1673 data[start + pos * step] = b;
1674 }
1675 }
1676 _ => {}
1677 }
1678 }
1679}
1680
1681fn fill_neighbor_text(data: &mut [String], rows: usize, cols: usize, dim: usize, dir: Neighbor) {
1682 if dim == 1 {
1683 for col in 0..cols {
1684 fill_line_text(data, col * rows, rows, 1, dir);
1685 }
1686 } else {
1687 for row in 0..rows {
1688 fill_line_text(data, row, cols, rows, dir);
1689 }
1690 }
1691}
1692
1693fn fill_line_text(data: &mut [String], start: usize, len: usize, step: usize, dir: Neighbor) {
1694 match dir {
1695 Neighbor::Previous => {
1696 let mut last: Option<String> = None;
1697 for i in 0..len {
1698 let idx = start + i * step;
1699 if is_missing_text(&data[idx]) {
1700 if let Some(value) = &last {
1701 data[idx] = value.clone();
1702 }
1703 } else {
1704 last = Some(data[idx].clone());
1705 }
1706 }
1707 }
1708 Neighbor::Next => {
1709 let mut next: Option<String> = None;
1710 for i in (0..len).rev() {
1711 let idx = start + i * step;
1712 if is_missing_text(&data[idx]) {
1713 if let Some(value) = &next {
1714 data[idx] = value.clone();
1715 }
1716 } else {
1717 next = Some(data[idx].clone());
1718 }
1719 }
1720 }
1721 }
1722}
1723
1724fn fill_nearest_text(data: &mut [String], rows: usize, cols: usize, dim: usize) {
1725 let original = data.to_vec();
1726 if dim == 1 {
1727 for col in 0..cols {
1728 fill_nearest_line_text(&original, data, col * rows, rows, 1);
1729 }
1730 } else {
1731 for row in 0..rows {
1732 fill_nearest_line_text(&original, data, row, cols, rows);
1733 }
1734 }
1735}
1736
1737fn fill_nearest_line_text(
1738 original: &[String],
1739 data: &mut [String],
1740 start: usize,
1741 len: usize,
1742 step: usize,
1743) {
1744 for i in 0..len {
1745 let idx = start + i * step;
1746 if !is_missing_text(&original[idx]) {
1747 continue;
1748 }
1749 let prev = (0..i).rev().find_map(|j| {
1750 let value = &original[start + j * step];
1751 (!is_missing_text(value)).then_some((i - j, value.clone()))
1752 });
1753 let next = ((i + 1)..len).find_map(|j| {
1754 let value = &original[start + j * step];
1755 (!is_missing_text(value)).then_some((j - i, value.clone()))
1756 });
1757 data[idx] = match (prev, next) {
1758 (Some((pd, _)), Some((nd, nv))) if nd < pd => nv,
1759 (Some((_, pv)), _) => pv,
1760 (_, Some((_, nv))) => nv,
1761 _ => MISSING_TEXT.to_string(),
1762 };
1763 }
1764}
1765
1766#[derive(Clone, Copy)]
1767struct MovingOptions {
1768 dim: Option<usize>,
1769 omit_nan: bool,
1770}
1771
1772impl MovingOptions {
1773 fn parse(args: &[Value]) -> BuiltinResult<Self> {
1774 let mut dim = None;
1775 let mut omit_nan = false;
1776 let mut idx = 0;
1777 while idx < args.len() {
1778 if let Some(text) = scalar_text(&args[idx]) {
1779 match text.to_ascii_lowercase().as_str() {
1780 "omitnan" | "omitmissing" => omit_nan = true,
1781 "includenan" | "includemissing" => omit_nan = false,
1782 "dim" if idx + 1 < args.len() => {
1783 dim = Some(scalar_usize(&args[idx + 1], "movmad dimension")?);
1784 idx += 1;
1785 }
1786 "dim" => return Err(invalid_argument("movmad: 'dim' requires a value")),
1787 other => {
1788 return Err(invalid_argument(format!(
1789 "movmad: unsupported option '{other}'"
1790 )))
1791 }
1792 }
1793 } else if matches!(args[idx], Value::Num(_) | Value::Int(_)) {
1794 dim = Some(scalar_usize(&args[idx], "movmad dimension")?);
1795 } else {
1796 return Err(invalid_argument(format!(
1797 "movmad: unsupported option argument {:?}",
1798 args[idx]
1799 )));
1800 }
1801 idx += 1;
1802 }
1803 if dim.is_some_and(|dim| dim != 1 && dim != 2) {
1804 return Err(invalid_argument("movmad: dimension must be 1 or 2"));
1805 }
1806 Ok(Self { dim, omit_nan })
1807 }
1808}
1809
1810fn moving_mad(tensor: Tensor, window: usize, options: MovingOptions) -> BuiltinResult<Value> {
1811 if window == 0 {
1812 return Err(invalid_argument("movmad: window length must be positive"));
1813 }
1814 let rows = tensor.rows();
1815 let cols = tensor.cols();
1816 let dim = options
1817 .dim
1818 .unwrap_or_else(|| first_nonsingleton_dim(rows, cols));
1819 validate_matrix_dim(dim, "movmad")?;
1820 let mut out = vec![f64::NAN; tensor.data.len()];
1821 if dim == 1 {
1822 for col in 0..cols {
1823 for row in 0..rows {
1824 out[row + col * rows] = moving_mad_at(
1825 &tensor.data,
1826 rows,
1827 col * rows,
1828 row,
1829 rows,
1830 1,
1831 window,
1832 options.omit_nan,
1833 );
1834 }
1835 }
1836 } else {
1837 for row in 0..rows {
1838 for col in 0..cols {
1839 out[row + col * rows] = moving_mad_at(
1840 &tensor.data,
1841 rows,
1842 row,
1843 col,
1844 cols,
1845 rows,
1846 window,
1847 options.omit_nan,
1848 );
1849 }
1850 }
1851 }
1852 Tensor::new_with_dtype(out, tensor.shape, tensor.dtype)
1853 .map(Value::Tensor)
1854 .map_err(internal_error)
1855}
1856
1857fn moving_mad_at(
1858 data: &[f64],
1859 _rows: usize,
1860 start: usize,
1861 pos: usize,
1862 len: usize,
1863 step: usize,
1864 window: usize,
1865 omit_nan: bool,
1866) -> f64 {
1867 let before = (window - 1) / 2;
1868 let after = window / 2;
1869 let lo = pos.saturating_sub(before);
1870 let hi = pos.saturating_add(after).saturating_add(1).min(len);
1871 let mut vals = Vec::new();
1872 for idx in lo..hi {
1873 let value = data[start + idx * step];
1874 if value.is_nan() && omit_nan {
1875 continue;
1876 }
1877 vals.push(value);
1878 }
1879 if vals.is_empty() || vals.iter().any(|value| value.is_nan()) {
1880 return f64::NAN;
1881 }
1882 let med = summary_value(vals.clone(), &Summary::Median);
1883 let mut devs: Vec<f64> = vals.into_iter().map(|value| (value - med).abs()).collect();
1884 summary_value(::std::mem::take(&mut devs), &Summary::Median)
1885}
1886
1887struct IndicatorSet {
1888 numeric: Vec<f64>,
1889 text: Vec<String>,
1890}
1891
1892fn indicator_set(value: &Value) -> BuiltinResult<IndicatorSet> {
1893 let mut set = IndicatorSet {
1894 numeric: Vec::new(),
1895 text: Vec::new(),
1896 };
1897 collect_indicators(value, &mut set)?;
1898 Ok(set)
1899}
1900
1901fn collect_indicators(value: &Value, set: &mut IndicatorSet) -> BuiltinResult<()> {
1902 match value {
1903 Value::Num(n) => set.numeric.push(*n),
1904 Value::Int(i) => set.numeric.push(i.to_f64()),
1905 Value::String(s) => set.text.push(s.clone()),
1906 Value::StringArray(array) => set.text.extend(array.data.iter().cloned()),
1907 Value::CharArray(array) => set.text.extend(char_rows(array)),
1908 Value::Tensor(tensor) => set.numeric.extend(tensor.data.iter().copied()),
1909 Value::Cell(cell) => {
1910 for item in &cell.data {
1911 collect_indicators(item, set)?;
1912 }
1913 }
1914 other => {
1915 return Err(unsupported_type(format!(
1916 "unsupported missing indicator {other:?}"
1917 )))
1918 }
1919 }
1920 Ok(())
1921}
1922
1923fn standardize_missing_value(value: Value, indicators: &IndicatorSet) -> BuiltinResult<Value> {
1924 match value {
1925 Value::Tensor(mut tensor) => {
1926 for value in &mut tensor.data {
1927 if indicators
1928 .numeric
1929 .iter()
1930 .any(|marker| numeric_indicator_matches(*value, *marker))
1931 {
1932 *value = f64::NAN;
1933 }
1934 }
1935 Ok(Value::Tensor(tensor))
1936 }
1937 Value::String(mut s) => {
1938 if indicators.text.iter().any(|marker| marker == &s) {
1939 s = MISSING_TEXT.to_string();
1940 }
1941 Ok(Value::String(s))
1942 }
1943 Value::StringArray(mut array) => {
1944 for text in &mut array.data {
1945 if indicators.text.iter().any(|marker| marker == text) {
1946 *text = MISSING_TEXT.to_string();
1947 }
1948 }
1949 Ok(Value::StringArray(array))
1950 }
1951 Value::CharArray(array) => {
1952 let rows = char_rows(&array);
1953 let data: Vec<String> = rows
1954 .into_iter()
1955 .map(|text| {
1956 if indicators.text.iter().any(|marker| marker == &text) {
1957 MISSING_TEXT.to_string()
1958 } else {
1959 text
1960 }
1961 })
1962 .collect();
1963 Ok(Value::StringArray(
1964 StringArray::new(data, vec![array.rows, 1]).map_err(internal_error)?,
1965 ))
1966 }
1967 Value::Object(mut object) if is_tabular_object(&object) => {
1968 let variables = table_variables(&object)?;
1969 let mut out = StructValue::new();
1970 for (name, field) in variables.fields {
1971 out.insert(name, standardize_missing_value(field, indicators)?);
1972 }
1973 object
1974 .properties
1975 .insert("__table_variables".to_string(), Value::Struct(out));
1976 Ok(Value::Object(object))
1977 }
1978 Value::Cell(cell) => {
1979 let mut out = Vec::with_capacity(cell.data.len());
1980 for item in cell.data {
1981 out.push(standardize_missing_value(item, indicators)?);
1982 }
1983 Ok(Value::Cell(
1984 CellArray::new(out, cell.rows, cell.cols).map_err(internal_error)?,
1985 ))
1986 }
1987 other => Ok(other),
1988 }
1989}
1990
1991fn numeric_indicator_matches(value: f64, marker: f64) -> bool {
1992 if marker.is_nan() {
1993 value.is_nan()
1994 } else {
1995 value == marker
1996 }
1997}
1998
1999fn numeric_tensor(value: Value, context: &str) -> BuiltinResult<Tensor> {
2000 match value {
2001 Value::Tensor(tensor) => Ok(tensor),
2002 Value::Num(n) => Tensor::new(vec![n], vec![1, 1]).map_err(internal_error),
2003 Value::Int(i) => Tensor::new(vec![i.to_f64()], vec![1, 1]).map_err(internal_error),
2004 Value::LogicalArray(array) => Tensor::new(
2005 array
2006 .data
2007 .iter()
2008 .map(|flag| f64::from(*flag != 0))
2009 .collect(),
2010 array.shape,
2011 )
2012 .map_err(internal_error),
2013 other => Err(unsupported_type(format!(
2014 "{context}: expected numeric input, got {other:?}"
2015 ))),
2016 }
2017}
2018
2019fn is_numeric_data_like(value: &Value) -> bool {
2020 matches!(
2021 value,
2022 Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::Tensor(_) | Value::LogicalArray(_)
2023 )
2024}
2025
2026fn pairwise_nan_min(left: Value, right: Value) -> BuiltinResult<Value> {
2027 let left_scalar = numeric_scalar(&left, "nanmin left").ok();
2028 let right_scalar = numeric_scalar(&right, "nanmin right").ok();
2029 if let (Some(a), Some(b)) = (left_scalar, right_scalar) {
2030 return Ok(Value::Num(nan_min_pair(a, b)));
2031 }
2032 let left = numeric_tensor(left, "nanmin left")?;
2033 let right = numeric_tensor(right, "nanmin right")?;
2034 let (data, shape, dtype) = broadcast_pairwise_numeric(&left, &right, nan_min_pair)?;
2035 Tensor::new_with_dtype(data, shape, dtype)
2036 .map(Value::Tensor)
2037 .map_err(internal_error)
2038}
2039
2040fn broadcast_pairwise_numeric(
2041 left: &Tensor,
2042 right: &Tensor,
2043 op: impl Fn(f64, f64) -> f64,
2044) -> BuiltinResult<(Vec<f64>, Vec<usize>, runmat_builtins::NumericDType)> {
2045 if left.data.len() == right.data.len() && left.shape == right.shape {
2046 let data = left
2047 .data
2048 .iter()
2049 .zip(right.data.iter())
2050 .map(|(a, b)| op(*a, *b))
2051 .collect();
2052 return Ok((data, left.shape.clone(), left.dtype));
2053 }
2054 if left.data.len() == 1 {
2055 let data = right.data.iter().map(|b| op(left.data[0], *b)).collect();
2056 return Ok((data, right.shape.clone(), right.dtype));
2057 }
2058 if right.data.len() == 1 {
2059 let data = left.data.iter().map(|a| op(*a, right.data[0])).collect();
2060 return Ok((data, left.shape.clone(), left.dtype));
2061 }
2062 Err(invalid_argument(
2063 "nanmin: pairwise inputs must have the same shape or one scalar input",
2064 ))
2065}
2066
2067fn nan_min_pair(a: f64, b: f64) -> f64 {
2068 match (a.is_nan(), b.is_nan()) {
2069 (true, true) => f64::NAN,
2070 (true, false) => b,
2071 (false, true) => a,
2072 (false, false) => a.min(b),
2073 }
2074}
2075
2076fn numeric_scalar(value: &Value, context: &str) -> BuiltinResult<f64> {
2077 match value {
2078 Value::Num(n) => Ok(*n),
2079 Value::Int(i) => Ok(i.to_f64()),
2080 Value::Bool(b) => Ok(f64::from(*b)),
2081 Value::Tensor(tensor) if tensor.data.len() == 1 => Ok(tensor.data[0]),
2082 other => Err(invalid_argument(format!(
2083 "{context}: expected numeric scalar, got {other:?}"
2084 ))),
2085 }
2086}
2087
2088fn first_nonsingleton_dim(rows: usize, cols: usize) -> usize {
2089 if rows > 1 {
2090 1
2091 } else if cols > 1 {
2092 2
2093 } else {
2094 1
2095 }
2096}
2097
2098fn validate_matrix_dim(dim: usize, context: &str) -> BuiltinResult<()> {
2099 if dim == 1 || dim == 2 {
2100 Ok(())
2101 } else {
2102 Err(invalid_argument(format!(
2103 "{context}: dimension must be 1 or 2"
2104 )))
2105 }
2106}
2107
2108fn scalar_usize(value: &Value, context: &str) -> BuiltinResult<usize> {
2109 let n = numeric_scalar(value, context)?;
2110 if !n.is_finite() || n < 0.0 || n.fract() != 0.0 {
2111 return Err(invalid_argument(format!(
2112 "{context}: expected nonnegative integer"
2113 )));
2114 }
2115 if n > usize::MAX as f64 {
2116 return Err(invalid_argument(format!("{context}: integer too large")));
2117 }
2118 Ok(n as usize)
2119}
2120
2121fn scalar_text(value: &Value) -> Option<String> {
2122 match value {
2123 Value::String(text) => Some(text.clone()),
2124 Value::StringArray(array) if array.data.len() == 1 => Some(array.data[0].clone()),
2125 Value::CharArray(array) if array.rows == 1 => Some(array.data.iter().collect()),
2126 _ => None,
2127 }
2128}
2129
2130fn char_rows(array: &CharArray) -> Vec<String> {
2131 let mut out = Vec::with_capacity(array.rows);
2132 for row in 0..array.rows {
2133 let start = row * array.cols;
2134 out.push(array.data[start..start + array.cols].iter().collect());
2135 }
2136 out
2137}
2138
2139fn is_missing_text(text: &str) -> bool {
2140 text.eq_ignore_ascii_case(MISSING_TEXT)
2141}
2142
2143#[cfg(test)]
2144mod tests {
2145 use super::*;
2146 use futures::executor::block_on;
2147
2148 fn tensor(data: Vec<f64>, shape: Vec<usize>) -> Value {
2149 Value::Tensor(Tensor::new(data, shape).unwrap())
2150 }
2151
2152 #[test]
2153 fn missing_constructs_scalar_and_arrays() {
2154 let scalar = block_on(missing_builtin(Vec::new())).unwrap();
2155 assert!(matches!(scalar, Value::StringArray(sa) if sa.data == vec![MISSING_TEXT]));
2156 let shaped = block_on(missing_builtin(vec![Value::Num(2.0), Value::Num(3.0)])).unwrap();
2157 assert!(
2158 matches!(shaped, Value::StringArray(sa) if sa.shape == vec![2, 3] && sa.data.len() == 6)
2159 );
2160 }
2161
2162 #[test]
2163 fn ismissing_detects_numeric_and_string_values() {
2164 let result = block_on(ismissing_builtin(tensor(
2165 vec![1.0, f64::NAN, 3.0],
2166 vec![1, 3],
2167 )))
2168 .unwrap();
2169 assert!(matches!(result, Value::LogicalArray(mask) if mask.data == vec![0, 1, 0]));
2170
2171 let strings = StringArray::new(vec!["a".into(), MISSING_TEXT.into()], vec![1, 2]).unwrap();
2172 let result = block_on(ismissing_builtin(Value::StringArray(strings))).unwrap();
2173 assert!(matches!(result, Value::LogicalArray(mask) if mask.data == vec![0, 1]));
2174 }
2175
2176 #[test]
2177 fn rmmissing_removes_rows_and_columns() {
2178 let value = tensor(vec![1.0, 2.0, f64::NAN, 4.0, 5.0, 6.0], vec![3, 2]);
2179 let result = block_on(rmmissing_builtin(value, Vec::new())).unwrap();
2180 assert!(
2181 matches!(result, Value::Tensor(t) if t.shape == vec![2, 2] && t.data == vec![1.0, 2.0, 4.0, 5.0])
2182 );
2183
2184 let value = tensor(vec![1.0, 2.0, f64::NAN, 4.0, 5.0, 6.0], vec![3, 2]);
2185 let result = block_on(rmmissing_builtin(value, vec![Value::Num(2.0)])).unwrap();
2186 assert!(
2187 matches!(result, Value::Tensor(t) if t.shape == vec![3, 1] && t.data == vec![4.0, 5.0, 6.0])
2188 );
2189 }
2190
2191 #[test]
2192 fn rmmissing_cell_arrays_use_cell_row_major_order() {
2193 let value = Value::Cell(
2194 CellArray::new(
2195 vec![
2196 Value::Num(1.0),
2197 Value::StringArray(
2198 StringArray::new(vec![MISSING_TEXT.into()], vec![1, 1]).unwrap(),
2199 ),
2200 Value::Num(2.0),
2201 Value::Num(3.0),
2202 ],
2203 2,
2204 2,
2205 )
2206 .unwrap(),
2207 );
2208 let result = block_on(rmmissing_builtin(value.clone(), Vec::new())).unwrap();
2209 match result {
2210 Value::Cell(cell) => {
2211 assert_eq!((cell.rows, cell.cols), (1, 2));
2212 assert_eq!(cell.get(0, 0).unwrap(), Value::Num(2.0));
2213 assert_eq!(cell.get(0, 1).unwrap(), Value::Num(3.0));
2214 }
2215 other => panic!("expected cell result, got {other:?}"),
2216 }
2217
2218 let result = block_on(rmmissing_builtin(value, vec![Value::Num(2.0)])).unwrap();
2219 match result {
2220 Value::Cell(cell) => {
2221 assert_eq!((cell.rows, cell.cols), (2, 1));
2222 assert_eq!(cell.get(0, 0).unwrap(), Value::Num(1.0));
2223 assert_eq!(cell.get(1, 0).unwrap(), Value::Num(2.0));
2224 }
2225 other => panic!("expected cell result, got {other:?}"),
2226 }
2227 }
2228
2229 #[test]
2230 fn fillmissing_supports_constant_previous_and_linear() {
2231 let value = tensor(vec![1.0, f64::NAN, 3.0], vec![3, 1]);
2232 let result = block_on(fillmissing_builtin(value, vec![Value::from("linear")])).unwrap();
2233 assert!(matches!(result, Value::Tensor(t) if t.data == vec![1.0, 2.0, 3.0]));
2234
2235 let value = tensor(vec![1.0, f64::NAN, 3.0], vec![1, 3]);
2236 let result = block_on(fillmissing_builtin(value, vec![Value::from("linear")])).unwrap();
2237 assert!(matches!(result, Value::Tensor(t) if t.data == vec![1.0, 2.0, 3.0]));
2238
2239 let value = tensor(vec![1.0, f64::NAN, f64::NAN], vec![3, 1]);
2240 let result = block_on(fillmissing_builtin(
2241 value,
2242 vec![Value::from("constant"), Value::Num(9.0)],
2243 ))
2244 .unwrap();
2245 assert!(matches!(result, Value::Tensor(t) if t.data == vec![1.0, 9.0, 9.0]));
2246 }
2247
2248 #[test]
2249 fn fillmissing_nearest_uses_original_neighbors() {
2250 let value = tensor(vec![1.0, f64::NAN, f64::NAN, 4.0], vec![1, 4]);
2251 let result = block_on(fillmissing_builtin(value, vec![Value::from("nearest")])).unwrap();
2252 assert!(matches!(result, Value::Tensor(t) if t.data == vec![1.0, 1.0, 4.0, 4.0]));
2253 }
2254
2255 #[test]
2256 fn fillmissing_rejects_unknown_options() {
2257 let value = tensor(vec![1.0, f64::NAN], vec![1, 2]);
2258 let result = block_on(fillmissing_builtin(
2259 value,
2260 vec![
2261 Value::from("constant"),
2262 Value::Num(0.0),
2263 Value::from("bogus"),
2264 ],
2265 ));
2266 assert!(result.is_err());
2267 }
2268
2269 #[test]
2270 fn standardize_missing_replaces_indicators() {
2271 let result = block_on(standardize_missing_builtin(
2272 tensor(vec![-99.0, 2.0], vec![1, 2]),
2273 vec![Value::Num(-99.0)],
2274 ))
2275 .unwrap();
2276 assert!(matches!(result, Value::Tensor(t) if t.data[0].is_nan() && t.data[1] == 2.0));
2277 }
2278
2279 #[test]
2280 fn nanmean_alias_uses_omitnan() {
2281 let result = block_on(nanmean_builtin(
2282 tensor(vec![1.0, f64::NAN, 3.0], vec![1, 3]),
2283 vec![Value::from("all")],
2284 ))
2285 .unwrap();
2286 assert!(matches!(result, Value::Num(n) if (n - 2.0).abs() < 1e-12));
2287 }
2288
2289 #[test]
2290 fn nanmin_supports_pairwise_form() {
2291 let result = block_on(nanmin_builtin(
2292 tensor(vec![f64::NAN, 4.0, 3.0], vec![1, 3]),
2293 vec![tensor(vec![2.0, f64::NAN, 5.0], vec![1, 3])],
2294 ))
2295 .unwrap();
2296 assert!(matches!(result, Value::Tensor(t) if t.data == vec![2.0, 4.0, 3.0]));
2297 }
2298
2299 #[test]
2300 fn movmad_computes_centered_median_absolute_deviation() {
2301 let result = block_on(movmad_builtin(
2302 tensor(vec![1.0, 2.0, 100.0, 4.0, 5.0], vec![5, 1]),
2303 Value::Num(3.0),
2304 Vec::new(),
2305 ))
2306 .unwrap();
2307 assert!(matches!(result, Value::Tensor(t) if t.data[2] == 2.0));
2308 }
2309}