use std::cmp::Ordering;
use std::collections::HashMap;
use runmat_builtins::{IntValue, LogicalArray, NumericDType, ResolveContext, Tensor, Type, Value};
use runmat_macros::runtime_builtin;
use crate::builtins::common::tensor;
use crate::builtins::stats::type_resolvers::mode_type;
use crate::{build_runtime_error, BuiltinResult, RuntimeError};
const NAME: &str = "mode";
fn mode_error(message: impl Into<String>) -> RuntimeError {
build_runtime_error(message).with_builtin(NAME).build()
}
fn mode_type_resolver(args: &[Type], ctx: &ResolveContext) -> Type {
mode_type(args, ctx)
}
#[runtime_builtin(
name = "mode",
category = "stats/summary",
summary = "Most frequent value along a dimension with MATLAB-compatible tie semantics.",
keywords = "mode,frequency,statistics,reduction,ties",
type_resolver(mode_type_resolver),
builtin_path = "crate::builtins::stats::summary::mode"
)]
async fn mode_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
let parsed = parse_arguments(&rest).await?;
let output_class = OutputClass::from_value(&value);
let eval = mode_evaluate(value, parsed, output_class)?;
if let Some(out_count) = crate::output_count::current_output_count() {
if out_count == 0 {
return Ok(Value::OutputList(Vec::new()));
}
if out_count == 1 {
return Ok(Value::OutputList(vec![eval.into_values_value()?]));
}
if out_count == 2 {
let (values, freq) = eval.into_pair()?;
return Ok(Value::OutputList(vec![values, freq]));
}
let (values, freq, cells) = eval.into_triple()?;
return Ok(crate::output_count::output_list_with_padding(
out_count,
vec![values, freq, cells],
));
}
eval.into_values_value()
}
#[derive(Clone, Debug)]
enum ModeAxes {
Default,
Dim(usize),
All,
}
#[derive(Clone, Debug)]
struct ParsedArguments {
axes: ModeAxes,
}
async fn parse_arguments(args: &[Value]) -> BuiltinResult<ParsedArguments> {
let mut axes = ModeAxes::Default;
let mut axes_set = false;
for arg in args {
if axes_set {
return Err(mode_error(format!(
"mode: unexpected extra argument {arg:?}"
)));
}
if let Some(selection) = parse_axes(arg).await? {
axes = selection;
axes_set = true;
continue;
}
return Err(mode_error(format!("mode: unrecognised argument {arg:?}")));
}
Ok(ParsedArguments { axes })
}
async fn parse_axes(value: &Value) -> BuiltinResult<Option<ModeAxes>> {
if let Some(text) = value_as_str(value) {
let trimmed = text.trim();
if trimmed.is_empty() {
return Err(mode_error("mode: dimension string must not be empty"));
}
let lowered = trimmed.to_ascii_lowercase();
return match lowered.as_str() {
"all" => Ok(Some(ModeAxes::All)),
other => Err(mode_error(format!("mode: unrecognised argument '{other}'"))),
};
}
let dim = match value {
Value::Num(_) | Value::Int(_) | Value::Bool(_) => {
tensor::dimension_from_value_async(value, NAME, false)
.await
.map_err(mode_error)?
}
Value::Tensor(t) if t.data.len() == 1 => {
tensor::dimension_from_value_async(value, NAME, false)
.await
.map_err(mode_error)?
}
Value::LogicalArray(la) if la.data.len() == 1 => {
tensor::dimension_from_value_async(value, NAME, false)
.await
.map_err(mode_error)?
}
_ => return Ok(None),
};
let Some(dim) = dim else {
return Ok(None);
};
if dim < 1 {
return Err(mode_error("mode: dimension must be >= 1"));
}
Ok(Some(ModeAxes::Dim(dim)))
}
fn value_as_str(value: &Value) -> Option<String> {
match value {
Value::String(s) => Some(s.clone()),
Value::StringArray(sa) if sa.data.len() == 1 => Some(sa.data[0].clone()),
Value::CharArray(ca) if ca.rows == 1 => Some(ca.data.iter().collect()),
_ => None,
}
}
#[derive(Debug)]
pub struct ModeEvaluation {
values: Tensor,
freq: Tensor,
ties: Vec<Vec<f64>>,
output_shape: Vec<usize>,
output_class: OutputClass,
}
impl ModeEvaluation {
fn empty(output_shape: Vec<usize>, output_class: OutputClass) -> BuiltinResult<Self> {
let len = tensor::element_count(&output_shape);
let values = Tensor::new(vec![f64::NAN; len], output_shape.clone())
.map_err(|e| mode_error(format!("mode: {e}")))?;
let freq = Tensor::new(vec![0.0; len], output_shape.clone())
.map_err(|e| mode_error(format!("mode: {e}")))?;
let ties = vec![Vec::new(); len];
Ok(Self {
values,
freq,
ties,
output_shape,
output_class,
})
}
fn into_values_value(self) -> BuiltinResult<Value> {
tensor_into_class_value(self.values, self.output_class)
}
fn into_pair(self) -> BuiltinResult<(Value, Value)> {
let ModeEvaluation {
values,
freq,
output_class,
..
} = self;
Ok((
tensor_into_class_value(values, output_class)?,
tensor::tensor_into_value(freq),
))
}
fn into_triple(self) -> BuiltinResult<(Value, Value, Value)> {
let ModeEvaluation {
values,
freq,
ties,
output_shape,
output_class,
} = self;
let cell = ties_to_cell(ties, &output_shape, output_class)?;
Ok((
tensor_into_class_value(values, output_class)?,
tensor::tensor_into_value(freq),
cell,
))
}
}
fn ties_to_cell(
ties: Vec<Vec<f64>>,
output_shape: &[usize],
output_class: OutputClass,
) -> BuiltinResult<Value> {
let cell_shape = if output_shape.is_empty() {
vec![1, 1]
} else {
output_shape.to_vec()
};
let mut cell_values: Vec<Value> = Vec::with_capacity(ties.len());
for entry in ties {
let rows = entry.len();
let tensor = Tensor::new(entry, vec![rows, 1])
.map_err(|e| mode_error(format!("mode: cell construction failed: {e}")))?;
cell_values.push(tensor_into_class_array_value(tensor, output_class)?);
}
crate::make_cell_with_shape(cell_values, cell_shape).map_err(mode_error)
}
fn mode_evaluate(
value: Value,
args: ParsedArguments,
output_class: OutputClass,
) -> BuiltinResult<ModeEvaluation> {
let tensor = materialize_tensor(value)?;
match args.axes {
ModeAxes::Default => {
let dim = default_dimension_from_shape(&tensor.shape);
reduce_along_dim(tensor, dim, output_class)
}
ModeAxes::Dim(dim) => reduce_along_dim(tensor, dim, output_class),
ModeAxes::All => reduce_all(tensor, output_class),
}
}
fn materialize_tensor(value: Value) -> BuiltinResult<Tensor> {
match value {
Value::GpuTensor(_) => Err(mode_error(
"mode: GPU tensors must be gathered to the host before mode can be computed",
)),
Value::ComplexTensor(_) | Value::Complex(_, _) => Err(mode_error(
"mode: complex inputs are not supported; gather real data first",
)),
other => tensor::value_into_tensor_for(NAME, other).map_err(mode_error),
}
}
fn default_dimension_from_shape(shape: &[usize]) -> usize {
if shape.is_empty() {
return 1;
}
shape
.iter()
.position(|&extent| extent != 1)
.map(|idx| idx + 1)
.unwrap_or(1)
}
fn reduce_all(tensor: Tensor, output_class: OutputClass) -> BuiltinResult<ModeEvaluation> {
let output_shape = vec![1usize, 1];
if tensor.data.is_empty() {
return ModeEvaluation::empty(output_shape, output_class);
}
let scalar = scalar_mode(&tensor.data);
finalize_single_slice(scalar, output_shape, output_class)
}
fn finalize_single_slice(
scalar: ScalarMode,
output_shape: Vec<usize>,
output_class: OutputClass,
) -> BuiltinResult<ModeEvaluation> {
let values = Tensor::new(vec![scalar.value], output_shape.clone())
.map_err(|e| mode_error(format!("mode: {e}")))?;
let freq = Tensor::new(vec![scalar.frequency], output_shape.clone())
.map_err(|e| mode_error(format!("mode: {e}")))?;
let ties = vec![scalar.ties];
Ok(ModeEvaluation {
values,
freq,
ties,
output_shape,
output_class,
})
}
fn reduce_along_dim(
tensor: Tensor,
dim: usize,
output_class: OutputClass,
) -> BuiltinResult<ModeEvaluation> {
if dim == 0 {
return Err(mode_error("mode: dimension must be >= 1"));
}
if tensor.shape.is_empty() {
let scalar_value = tensor.data.first().copied().unwrap_or(f64::NAN);
let output_shape = vec![1usize, 1];
if scalar_value.is_nan() {
return ModeEvaluation::empty(output_shape, output_class);
}
let scalar = ScalarMode {
value: scalar_value,
frequency: 1.0,
ties: vec![scalar_value],
};
return finalize_single_slice(scalar, output_shape, output_class);
}
if dim > tensor.shape.len() {
let output_shape = tensor.shape.clone();
let len = tensor::element_count(&output_shape);
let mut values = Vec::with_capacity(len);
let mut freq = Vec::with_capacity(len);
let mut ties = Vec::with_capacity(len);
for &v in &tensor.data {
if v.is_nan() {
values.push(f64::NAN);
freq.push(0.0);
ties.push(Vec::new());
} else {
values.push(v);
freq.push(1.0);
ties.push(vec![v]);
}
}
let values_tensor = Tensor::new(values, output_shape.clone())
.map_err(|e| mode_error(format!("mode: {e}")))?;
let freq_tensor = Tensor::new(freq, output_shape.clone())
.map_err(|e| mode_error(format!("mode: {e}")))?;
return Ok(ModeEvaluation {
values: values_tensor,
freq: freq_tensor,
ties,
output_shape,
output_class,
});
}
let dim_index = dim - 1;
let reduce_len = tensor.shape[dim_index];
let mut output_shape = tensor.shape.clone();
output_shape[dim_index] = 1;
if reduce_len == 0 || tensor.data.is_empty() {
return ModeEvaluation::empty(output_shape, output_class);
}
let stride_before = dim_product(&tensor.shape[..dim_index])?;
let stride_after = dim_product(&tensor.shape[dim_index + 1..])?;
let output_len = stride_before
.checked_mul(stride_after)
.ok_or_else(|| mode_error("mode: output size overflow"))?;
let mut values = vec![0.0f64; output_len];
let mut freq = vec![0.0f64; output_len];
let mut ties: Vec<Vec<f64>> = vec![Vec::new(); output_len];
let mut slice = Vec::with_capacity(reduce_len);
for after in 0..stride_after {
for before in 0..stride_before {
slice.clear();
for k in 0..reduce_len {
let idx = before + k * stride_before + after * stride_before * reduce_len;
slice.push(tensor.data[idx]);
}
let scalar = scalar_mode(&slice);
let out_idx = before + after * stride_before;
values[out_idx] = scalar.value;
freq[out_idx] = scalar.frequency;
ties[out_idx] = scalar.ties;
}
}
let values_tensor =
Tensor::new(values, output_shape.clone()).map_err(|e| mode_error(format!("mode: {e}")))?;
let freq_tensor =
Tensor::new(freq, output_shape.clone()).map_err(|e| mode_error(format!("mode: {e}")))?;
Ok(ModeEvaluation {
values: values_tensor,
freq: freq_tensor,
ties,
output_shape,
output_class,
})
}
#[derive(Debug, Clone, Copy)]
enum OutputClass {
Double,
Single,
UInt8,
UInt16,
Logical,
Int(IntKind),
}
#[derive(Debug, Clone, Copy)]
enum IntKind {
I8,
I16,
I32,
I64,
U8,
U16,
U32,
U64,
}
impl OutputClass {
fn from_value(value: &Value) -> Self {
match value {
Value::Tensor(tensor) => match tensor.dtype {
NumericDType::F64 => OutputClass::Double,
NumericDType::F32 => OutputClass::Single,
NumericDType::U8 => OutputClass::UInt8,
NumericDType::U16 => OutputClass::UInt16,
},
Value::LogicalArray(_) | Value::Bool(_) => OutputClass::Logical,
Value::Int(value) => OutputClass::Int(IntKind::from_int_value(value)),
_ => OutputClass::Double,
}
}
}
impl IntKind {
fn from_int_value(value: &IntValue) -> Self {
match value {
IntValue::I8(_) => IntKind::I8,
IntValue::I16(_) => IntKind::I16,
IntValue::I32(_) => IntKind::I32,
IntValue::I64(_) => IntKind::I64,
IntValue::U8(_) => IntKind::U8,
IntValue::U16(_) => IntKind::U16,
IntValue::U32(_) => IntKind::U32,
IntValue::U64(_) => IntKind::U64,
}
}
fn to_value(self, value: f64) -> Value {
match self {
IntKind::I8 => Value::Int(IntValue::I8(value.round() as i8)),
IntKind::I16 => Value::Int(IntValue::I16(value.round() as i16)),
IntKind::I32 => Value::Int(IntValue::I32(value.round() as i32)),
IntKind::I64 => Value::Int(IntValue::I64(value.round() as i64)),
IntKind::U8 => Value::Int(IntValue::U8(value.round() as u8)),
IntKind::U16 => Value::Int(IntValue::U16(value.round() as u16)),
IntKind::U32 => Value::Int(IntValue::U32(value.round() as u32)),
IntKind::U64 => Value::Int(IntValue::U64(value.round() as u64)),
}
}
}
fn tensor_into_class_value(mut tensor: Tensor, class: OutputClass) -> BuiltinResult<Value> {
let contains_nan = tensor.data.iter().any(|value| value.is_nan());
match class {
OutputClass::Double => Ok(tensor::tensor_into_value(tensor)),
OutputClass::Single => {
for value in &mut tensor.data {
*value = (*value as f32) as f64;
}
tensor.dtype = NumericDType::F32;
Ok(Value::Tensor(tensor))
}
OutputClass::UInt8 => {
if contains_nan {
return Ok(tensor::tensor_into_value(tensor));
}
for value in &mut tensor.data {
*value = value.round().clamp(0.0, u8::MAX as f64);
}
tensor.dtype = NumericDType::U8;
if tensor.data.len() == 1 {
Ok(Value::Int(IntValue::U8(tensor.data[0] as u8)))
} else {
Ok(Value::Tensor(tensor))
}
}
OutputClass::UInt16 => {
if contains_nan {
return Ok(tensor::tensor_into_value(tensor));
}
for value in &mut tensor.data {
*value = value.round().clamp(0.0, u16::MAX as f64);
}
tensor.dtype = NumericDType::U16;
if tensor.data.len() == 1 {
Ok(Value::Int(IntValue::U16(tensor.data[0] as u16)))
} else {
Ok(Value::Tensor(tensor))
}
}
OutputClass::Logical => {
if contains_nan {
return Ok(tensor::tensor_into_value(tensor));
}
let data: Vec<u8> = tensor
.data
.iter()
.map(|value| if *value != 0.0 { 1 } else { 0 })
.collect();
if data.len() == 1 {
Ok(Value::Bool(data[0] != 0))
} else {
LogicalArray::new(data, tensor.shape)
.map(Value::LogicalArray)
.map_err(mode_error)
}
}
OutputClass::Int(kind) => {
if contains_nan {
return Ok(tensor::tensor_into_value(tensor));
}
if tensor.data.len() == 1 {
Ok(kind.to_value(tensor.data[0]))
} else {
Ok(tensor::tensor_into_value(tensor))
}
}
}
}
fn tensor_into_class_array_value(mut tensor: Tensor, class: OutputClass) -> BuiltinResult<Value> {
let contains_nan = tensor.data.iter().any(|value| value.is_nan());
match class {
OutputClass::Double => Ok(Value::Tensor(tensor)),
OutputClass::Single => {
for value in &mut tensor.data {
*value = (*value as f32) as f64;
}
tensor.dtype = NumericDType::F32;
Ok(Value::Tensor(tensor))
}
OutputClass::UInt8 => {
if contains_nan {
return Ok(Value::Tensor(tensor));
}
for value in &mut tensor.data {
*value = value.round().clamp(0.0, u8::MAX as f64);
}
tensor.dtype = NumericDType::U8;
Ok(Value::Tensor(tensor))
}
OutputClass::UInt16 => {
if contains_nan {
return Ok(Value::Tensor(tensor));
}
for value in &mut tensor.data {
*value = value.round().clamp(0.0, u16::MAX as f64);
}
tensor.dtype = NumericDType::U16;
Ok(Value::Tensor(tensor))
}
OutputClass::Logical => {
if contains_nan {
return Ok(Value::Tensor(tensor));
}
let data: Vec<u8> = tensor
.data
.iter()
.map(|value| if *value != 0.0 { 1 } else { 0 })
.collect();
LogicalArray::new(data, tensor.shape)
.map(Value::LogicalArray)
.map_err(mode_error)
}
OutputClass::Int(kind) => {
if contains_nan || tensor.data.len() != 1 {
return Ok(Value::Tensor(tensor));
}
Ok(kind.to_value(tensor.data[0]))
}
}
}
fn dim_product(dims: &[usize]) -> BuiltinResult<usize> {
dims.iter()
.copied()
.try_fold(1usize, |acc, dim| acc.checked_mul(dim))
.ok_or_else(|| mode_error("mode: output size overflow"))
}
#[derive(Debug, Clone)]
struct ScalarMode {
value: f64,
frequency: f64,
ties: Vec<f64>,
}
fn scalar_mode(values: &[f64]) -> ScalarMode {
let mut counts: HashMap<u64, (f64, usize)> = HashMap::new();
for &v in values {
if v.is_nan() {
continue;
}
let key = canonical_bits(v);
counts
.entry(key)
.and_modify(|(_, c)| *c += 1)
.or_insert((v, 1));
}
if counts.is_empty() {
return ScalarMode {
value: f64::NAN,
frequency: 0.0,
ties: Vec::new(),
};
}
let max_count = counts.values().map(|(_, c)| *c).max().unwrap_or(0);
let mut tied: Vec<f64> = counts
.values()
.filter_map(|(v, c)| if *c == max_count { Some(*v) } else { None })
.collect();
tied.sort_by(|a, b| compare_f64(*a, *b));
let smallest = tied[0];
ScalarMode {
value: smallest,
frequency: max_count as f64,
ties: tied,
}
}
fn canonical_bits(value: f64) -> u64 {
if value == 0.0 {
0u64
} else {
value.to_bits()
}
}
fn compare_f64(a: f64, b: f64) -> Ordering {
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use futures::executor::block_on;
use runmat_builtins::{IntValue, LogicalArray, NumericDType, Tensor, Value};
fn mode_call(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
block_on(super::mode_builtin(value, rest))
}
fn mode_outputs(value: Value, rest: Vec<Value>, out_count: usize) -> BuiltinResult<Vec<Value>> {
let _guard = crate::output_count::push_output_count(Some(out_count));
let result = mode_call(value, rest)?;
match result {
Value::OutputList(list) => Ok(list),
other => Ok(vec![other]),
}
}
#[test]
fn mode_type_resolver_reduces_first_dim() {
let ty = Type::Tensor {
shape: Some(vec![Some(3), Some(4)]),
};
let out = mode_type_resolver(&[ty], &ResolveContext::new(Vec::new()));
assert_eq!(
out,
Type::Tensor {
shape: Some(vec![Some(1), Some(4)])
}
);
}
#[test]
fn mode_scalar_returns_self() {
let result = mode_call(Value::Num(7.0), Vec::new()).expect("mode");
assert_eq!(result, Value::Num(7.0));
}
#[test]
fn mode_vector_simple_majority() {
let tensor = Tensor::new(vec![1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0], vec![7, 1]).unwrap();
let result = mode_call(Value::Tensor(tensor), Vec::new()).expect("mode");
assert_eq!(result, Value::Num(3.0));
}
#[test]
fn mode_ties_return_smallest_with_sorted_set() {
let tensor = Tensor::new(vec![1.0, 1.0, 2.0, 2.0], vec![1, 4]).unwrap();
let outputs = mode_outputs(Value::Tensor(tensor), Vec::new(), 3).expect("mode");
assert_eq!(outputs.len(), 3);
assert_eq!(outputs[0], Value::Num(1.0));
assert_eq!(outputs[1], Value::Num(2.0));
match &outputs[2] {
Value::Cell(cell) => {
assert_eq!(cell.shape, vec![1, 1]);
assert_eq!(cell.data.len(), 1);
let entry = unsafe { &*cell.data[0].as_raw() };
match entry {
Value::Tensor(t) => {
assert_eq!(t.shape, vec![2, 1]);
assert_eq!(t.data, vec![1.0, 2.0]);
}
other => panic!("expected tensor inside cell, got {other:?}"),
}
}
other => panic!("expected cell array, got {other:?}"),
}
}
#[test]
fn mode_matrix_default_dimension_columnwise() {
let tensor = Tensor::new(
vec![1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 4.0, 5.0],
vec![3, 3],
)
.unwrap();
let result = mode_call(Value::Tensor(tensor), Vec::new()).expect("mode");
match result {
Value::Tensor(t) => {
assert_eq!(t.shape, vec![1, 3]);
assert_eq!(t.data, vec![2.0, 3.0, 4.0]);
}
other => panic!("expected tensor, got {other:?}"),
}
}
#[test]
fn mode_matrix_along_dim_two() {
let tensor = Tensor::new(
vec![1.0, 2.0, 1.0, 3.0, 2.0, 3.0, 1.0, 4.0, 5.0],
vec![3, 3],
)
.unwrap();
let result =
mode_call(Value::Tensor(tensor), vec![Value::Int(IntValue::I32(2))]).expect("mode");
match result {
Value::Tensor(t) => {
assert_eq!(t.shape, vec![3, 1]);
assert_eq!(t.data, vec![1.0, 2.0, 1.0]);
}
other => panic!("expected tensor, got {other:?}"),
}
}
#[test]
fn mode_all_reduces_across_all_elements() {
let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 2.0, 3.0, 2.0], vec![2, 3]).unwrap();
let outputs =
mode_outputs(Value::Tensor(tensor), vec![Value::from("all")], 3).expect("mode");
assert_eq!(outputs[0], Value::Num(2.0));
assert_eq!(outputs[1], Value::Num(3.0));
match &outputs[2] {
Value::Cell(cell) => {
assert_eq!(cell.shape, vec![1, 1]);
let entry = unsafe { &*cell.data[0].as_raw() };
match entry {
Value::Tensor(t) => {
assert_eq!(t.shape, vec![1, 1]);
assert_eq!(t.data, vec![2.0]);
}
other => panic!("expected tensor in cell, got {other:?}"),
}
}
other => panic!("expected cell array, got {other:?}"),
}
}
#[test]
fn mode_skips_nans_in_majority() {
let tensor = Tensor::new(vec![1.0, f64::NAN, 2.0, 2.0, f64::NAN], vec![5, 1]).unwrap();
let outputs = mode_outputs(Value::Tensor(tensor), Vec::new(), 2).expect("mode");
assert_eq!(outputs[0], Value::Num(2.0));
assert_eq!(outputs[1], Value::Num(2.0));
}
#[test]
fn mode_all_nan_input_returns_nan() {
let tensor = Tensor::new(vec![f64::NAN, f64::NAN, f64::NAN], vec![3, 1]).unwrap();
let outputs = mode_outputs(Value::Tensor(tensor), Vec::new(), 3).expect("mode");
match &outputs[0] {
Value::Num(n) => assert!(n.is_nan()),
other => panic!("expected scalar NaN, got {other:?}"),
}
assert_eq!(outputs[1], Value::Num(0.0));
match &outputs[2] {
Value::Cell(cell) => {
assert_eq!(cell.shape, vec![1, 1]);
let entry = unsafe { &*cell.data[0].as_raw() };
match entry {
Value::Tensor(t) => {
assert_eq!(t.shape, vec![0, 1]);
assert!(t.data.is_empty());
}
other => panic!("expected empty tensor in cell, got {other:?}"),
}
}
other => panic!("expected cell array, got {other:?}"),
}
}
#[test]
fn mode_logical_input_preserves_scalar_class() {
let logical = LogicalArray::new(vec![0, 1, 1, 0, 1], vec![5, 1]).unwrap();
let outputs = mode_outputs(Value::LogicalArray(logical), Vec::new(), 2).expect("mode");
assert_eq!(outputs[0], Value::Bool(true));
assert_eq!(outputs[1], Value::Num(3.0));
}
#[test]
fn mode_integer_input_works() {
let result = mode_call(Value::Int(IntValue::I32(5)), Vec::new()).expect("mode");
assert_eq!(result, Value::Int(IntValue::I32(5)));
}
#[test]
fn mode_uint16_tensor_preserves_value_class() {
let mut tensor = Tensor::new(vec![9.0, 10.0, 2.0, 10.0], vec![1, 4]).unwrap();
tensor.dtype = NumericDType::U16;
let outputs = mode_outputs(Value::Tensor(tensor), Vec::new(), 3).expect("mode");
assert_eq!(outputs[0], Value::Int(IntValue::U16(10)));
assert_eq!(outputs[1], Value::Num(2.0));
match &outputs[2] {
Value::Cell(cell) => {
let entry = unsafe { &*cell.data[0].as_raw() };
match entry {
Value::Tensor(t) => {
assert_eq!(t.dtype, NumericDType::U16);
assert_eq!(t.shape, vec![1, 1]);
assert_eq!(t.data, vec![10.0]);
}
other => panic!("expected uint16 tensor inside cell, got {other:?}"),
}
}
other => panic!("expected cell array, got {other:?}"),
}
}
#[test]
fn mode_logical_input_preserves_logical_class() {
let logical = LogicalArray::new(vec![0, 1, 1, 0, 1], vec![5, 1]).unwrap();
let outputs = mode_outputs(Value::LogicalArray(logical), Vec::new(), 3).expect("mode");
assert_eq!(outputs[0], Value::Bool(true));
assert_eq!(outputs[1], Value::Num(3.0));
match &outputs[2] {
Value::Cell(cell) => {
let entry = unsafe { &*cell.data[0].as_raw() };
match entry {
Value::LogicalArray(array) => {
assert_eq!(array.shape, vec![1, 1]);
assert_eq!(array.data, vec![1]);
}
other => panic!("expected logical array inside cell, got {other:?}"),
}
}
other => panic!("expected cell array, got {other:?}"),
}
}
#[test]
fn mode_empty_input_returns_nan_frequency_zero() {
let tensor = Tensor::new(Vec::new(), vec![0, 1]).unwrap();
let outputs = mode_outputs(Value::Tensor(tensor), Vec::new(), 2).expect("mode");
match &outputs[0] {
Value::Num(n) => assert!(n.is_nan()),
other => panic!("expected scalar NaN, got {other:?}"),
}
assert_eq!(outputs[1], Value::Num(0.0));
}
#[test]
fn mode_rejects_unknown_string_argument() {
let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
let err = mode_call(Value::Tensor(tensor), vec![Value::from("flat")]).unwrap_err();
assert!(err.message().contains("unrecognised argument"));
}
#[test]
fn mode_rejects_negative_dimension() {
let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
let err = mode_call(Value::Tensor(tensor), vec![Value::Int(IntValue::I32(0))]).unwrap_err();
assert!(err.message().contains("dimension"));
}
#[test]
fn mode_dim_beyond_ndims_preserves_input_with_unit_frequency() {
let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
let outputs = mode_outputs(Value::Tensor(tensor), vec![Value::Int(IntValue::I32(5))], 2)
.expect("mode");
match &outputs[0] {
Value::Tensor(t) => {
assert_eq!(t.shape, vec![3, 1]);
assert_eq!(t.data, vec![1.0, 2.0, 3.0]);
}
other => panic!("expected tensor, got {other:?}"),
}
match &outputs[1] {
Value::Tensor(t) => {
assert_eq!(t.shape, vec![3, 1]);
assert_eq!(t.data, vec![1.0, 1.0, 1.0]);
}
other => panic!("expected tensor of frequencies, got {other:?}"),
}
}
}