Skip to main content

runmat_runtime/builtins/missing/
mod.rs

1//! MATLAB-compatible missing-value construction, predicates, cleanup, and NaN-aware aliases.
2
3use 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}