use runmat_builtins::{
BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
CellArray, CharArray, StringArray, StructValue, Value,
};
use runmat_macros::runtime_builtin;
use crate::{build_runtime_error, BuiltinResult, RuntimeError};
const BUILTIN_NAME: &str = "cell2struct";
const OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
name: "S",
ty: BuiltinParamType::Any,
arity: BuiltinParamArity::Required,
default: None,
description: "Struct or struct array result.",
}];
const INPUTS: [BuiltinParamDescriptor; 3] = [
BuiltinParamDescriptor {
name: "C",
ty: BuiltinParamType::Any,
arity: BuiltinParamArity::Required,
default: None,
description: "Cell array containing field values.",
},
BuiltinParamDescriptor {
name: "fields",
ty: BuiltinParamType::Any,
arity: BuiltinParamArity::Required,
default: None,
description: "Field names as char, string, or cellstr.",
},
BuiltinParamDescriptor {
name: "dim",
ty: BuiltinParamType::IntegerScalar,
arity: BuiltinParamArity::Required,
default: None,
description: "Dimension whose entries correspond to field names.",
},
];
const SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
label: "S = cell2struct(C, fields, dim)",
inputs: &INPUTS,
outputs: &OUTPUT,
}];
const ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
code: "RM.CELL2STRUCT.INVALID_INPUT",
identifier: Some("RunMat:cell2struct:InvalidInput"),
when: "Arguments are not a cell array, field-name list, and valid dimension.",
message: "cell2struct: invalid input",
};
const ERROR_SHAPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
code: "RM.CELL2STRUCT.SHAPE",
identifier: Some("RunMat:cell2struct:ShapeMismatch"),
when: "The number of field names does not match the selected cell dimension.",
message: "cell2struct: field count does not match selected dimension",
};
const ERRORS: [BuiltinErrorDescriptor; 2] = [ERROR_INVALID_INPUT, ERROR_SHAPE];
pub const CELL2STRUCT_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
signatures: &SIGNATURES,
output_mode: BuiltinOutputMode::Fixed,
completion_policy: BuiltinCompletionPolicy::Public,
errors: &ERRORS,
};
#[runtime_builtin(
name = "cell2struct",
category = "cells/core",
summary = "Convert a cell array into a scalar struct or struct array.",
keywords = "cell2struct,cell,struct,conversion",
accel = "gather",
descriptor(crate::builtins::cells::core::cell2struct::CELL2STRUCT_DESCRIPTOR),
builtin_path = "crate::builtins::cells::core::cell2struct"
)]
fn cell2struct_builtin(cells: Value, fields: Value, dim: Value) -> BuiltinResult<Value> {
let Value::Cell(cells) = cells else {
return Err(error(
&ERROR_INVALID_INPUT,
"cell2struct: C must be a cell array",
));
};
let fields = field_names(&fields)?;
let dim = parse_dim(&dim)?;
build_structs(cells, fields, dim)
}
fn build_structs(cells: CellArray, fields: Vec<String>, dim: usize) -> BuiltinResult<Value> {
if fields.is_empty() {
return Err(error(
&ERROR_INVALID_INPUT,
"cell2struct: fields must not be empty",
));
}
let rank = cells.shape.len().max(dim);
let mut shape = cells.shape.clone();
shape.resize(rank, 1);
let field_dim = dim - 1;
if shape[field_dim] != fields.len() {
return Err(error(
&ERROR_SHAPE,
format!(
"cell2struct: selected dimension has extent {}, but {} field names were supplied",
shape[field_dim],
fields.len()
),
));
}
let mut out_shape = shape.clone();
out_shape[field_dim] = 1;
if out_shape.iter().product::<usize>() == 1 {
let mut st = StructValue::new();
for (field_idx, field) in fields.iter().enumerate() {
let mut coords = vec![0usize; rank];
coords[field_dim] = field_idx;
st.insert(
field.clone(),
cells.data[linear_col_major(&coords, &shape)].clone(),
);
}
return Ok(Value::Struct(st));
}
let count = out_shape.iter().product::<usize>();
let mut structs = Vec::with_capacity(count);
for out_linear in 0..count {
let out_coords = coords_col_major(out_linear, &out_shape);
let mut st = StructValue::new();
for (field_idx, field) in fields.iter().enumerate() {
let mut coords = out_coords.clone();
coords[field_dim] = field_idx;
st.insert(
field.clone(),
cells.data[linear_col_major(&coords, &shape)].clone(),
);
}
structs.push(Value::Struct(st));
}
if structs.len() == 1 {
return Ok(structs.pop().expect("one struct"));
}
CellArray::new_with_shape(structs, out_shape)
.map(Value::Cell)
.map_err(|err| error(&ERROR_INVALID_INPUT, format!("cell2struct: {err}")))
}
fn field_names(value: &Value) -> BuiltinResult<Vec<String>> {
match value {
Value::String(text) => Ok(vec![text.clone()]),
Value::StringArray(array) => Ok(array.data.clone()),
Value::CharArray(chars) if chars.rows == 1 => Ok(vec![chars.data.iter().collect()]),
Value::CharArray(chars) => {
let mut names = Vec::with_capacity(chars.rows);
for row in 0..chars.rows {
let start = row * chars.cols;
let text = chars.data[start..start + chars.cols]
.iter()
.collect::<String>()
.trim_end()
.to_string();
names.push(text);
}
Ok(names)
}
Value::Cell(cell) => cell.data.iter().map(field_name_scalar).collect(),
_ => Err(error(
&ERROR_INVALID_INPUT,
"cell2struct: fields must be text or cellstr",
)),
}
.and_then(|names| {
if names.iter().any(|name| name.is_empty()) {
Err(error(
&ERROR_INVALID_INPUT,
"cell2struct: field names must not be empty",
))
} else {
Ok(names)
}
})
}
fn field_name_scalar(value: &Value) -> BuiltinResult<String> {
match value {
Value::String(text) => Ok(text.clone()),
Value::StringArray(StringArray { data, .. }) if data.len() == 1 => Ok(data[0].clone()),
Value::CharArray(CharArray { rows: 1, data, .. }) => Ok(data.iter().collect()),
_ => Err(error(
&ERROR_INVALID_INPUT,
"cell2struct: cell field names must be text scalars",
)),
}
}
fn parse_dim(value: &Value) -> BuiltinResult<usize> {
let raw = match value {
Value::Num(value) if value.is_finite() => *value,
Value::Int(value) => value.to_f64(),
_ => {
return Err(error(
&ERROR_INVALID_INPUT,
"cell2struct: dim must be a positive integer",
))
}
};
if raw < 1.0 || raw.fract() != 0.0 {
return Err(error(
&ERROR_INVALID_INPUT,
"cell2struct: dim must be a positive integer",
));
}
Ok(raw as usize)
}
fn coords_col_major(mut linear: usize, shape: &[usize]) -> Vec<usize> {
let mut coords = Vec::with_capacity(shape.len());
for &extent in shape {
coords.push(if extent == 0 { 0 } else { linear % extent });
if extent != 0 {
linear /= extent;
}
}
coords
}
fn linear_col_major(coords: &[usize], shape: &[usize]) -> usize {
let mut linear = 0usize;
let mut stride = 1usize;
for (&coord, &extent) in coords.iter().zip(shape.iter()) {
linear += coord * stride;
stride *= extent;
}
linear
}
fn error(desc: &'static BuiltinErrorDescriptor, message: impl Into<String>) -> RuntimeError {
let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
if let Some(identifier) = desc.identifier {
builder = builder.with_identifier(identifier);
}
builder.build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scalar_struct_from_row_cell() {
let cells = CellArray::new(vec![Value::Num(1.0), Value::from("Ada")], 1, 2).unwrap();
let fields = CellArray::new(vec![Value::from("id"), Value::from("name")], 1, 2).unwrap();
let out =
cell2struct_builtin(Value::Cell(cells), Value::Cell(fields), Value::Num(2.0)).unwrap();
let Value::Struct(st) = out else {
panic!("expected scalar struct");
};
assert_eq!(st.fields.get("id"), Some(&Value::Num(1.0)));
assert_eq!(st.fields.get("name"), Some(&Value::from("Ada")));
}
#[test]
fn struct_array_from_field_dimension() {
let cells = CellArray::new(
vec![
Value::Num(1.0),
Value::from("Ada"),
Value::Num(2.0),
Value::from("Grace"),
],
2,
2,
)
.unwrap();
let fields = CellArray::new(vec![Value::from("id"), Value::from("name")], 2, 1).unwrap();
let out =
cell2struct_builtin(Value::Cell(cells), Value::Cell(fields), Value::Num(1.0)).unwrap();
let Value::Cell(out) = out else {
panic!("expected struct array cell");
};
assert_eq!(out.shape, vec![1, 2]);
assert!(
matches!(&out.data[0], Value::Struct(st) if st.fields.get("id") == Some(&Value::Num(1.0)))
);
assert!(
matches!(&out.data[1], Value::Struct(st) if st.fields.get("name") == Some(&Value::from("Grace")))
);
}
}