use crate::error::{IoError, Result};
use crate::matlab::MatType;
#[allow(unused_imports)]
use scirs2_core::ndarray::{ArrayD, IxDyn};
use std::collections::HashMap;
use std::path::Path;
#[cfg(feature = "hdf5")]
use crate::hdf5::{AttributeValue, CompressionOptions, DatasetOptions, FileMode, HDF5File};
#[derive(Debug, Clone)]
pub struct V73Features {
pub enable_partial_io: bool,
pub support_objects: bool,
pub support_function_handles: bool,
pub support_tables: bool,
pub support_tall_arrays: bool,
pub support_categorical: bool,
pub support_datetime: bool,
pub support_string_arrays: bool,
}
impl Default for V73Features {
fn default() -> Self {
Self {
enable_partial_io: true,
support_objects: true,
support_function_handles: true,
support_tables: true,
support_tall_arrays: false, support_categorical: true,
support_datetime: true,
support_string_arrays: true,
}
}
}
#[derive(Debug, Clone)]
pub enum ExtendedMatType {
Standard(Box<MatType>),
Table(MatlabTable),
Categorical(CategoricalArray),
DateTime(DateTimeArray),
StringArray(Vec<String>),
FunctionHandle(FunctionHandle),
Object(MatlabObject),
ComplexDouble(ArrayD<scirs2_core::numeric::Complex<f64>>),
ComplexSingle(ArrayD<scirs2_core::numeric::Complex<f32>>),
}
#[derive(Debug, Clone)]
pub struct MatlabTable {
pub variable_names: Vec<String>,
pub row_names: Option<Vec<String>>,
pub data: HashMap<String, MatType>,
pub properties: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct CategoricalArray {
pub categories: Vec<String>,
pub data: ArrayD<u32>,
pub ordered: bool,
}
#[derive(Debug, Clone)]
pub struct DateTimeArray {
pub data: ArrayD<f64>,
pub timezone: Option<String>,
pub format: String,
}
#[derive(Debug, Clone)]
pub struct FunctionHandle {
pub function: String,
pub function_type: String,
pub workspace: Option<HashMap<String, MatType>>,
}
#[derive(Debug, Clone)]
pub struct MatlabObject {
pub class_name: String,
pub properties: HashMap<String, MatType>,
pub superclass_data: Option<Box<MatlabObject>>,
}
pub struct V73MatFile {
#[allow(dead_code)]
features: V73Features,
#[cfg(feature = "hdf5")]
compression: Option<CompressionOptions>,
}
impl V73MatFile {
pub fn new(features: V73Features) -> Self {
Self {
features,
#[cfg(feature = "hdf5")]
compression: None,
}
}
#[cfg(feature = "hdf5")]
pub fn with_compression(mut self, compression: CompressionOptions) -> Self {
self.compression = Some(compression);
self
}
#[cfg(feature = "hdf5")]
pub fn write_extended<P: AsRef<Path>>(
&self,
path: P,
vars: &HashMap<String, ExtendedMatType>,
) -> Result<()> {
let mut hdf5_file = HDF5File::create(path)?;
hdf5_file.set_attribute(
"/",
"MATLAB_version",
AttributeValue::String("7.3".to_string()),
)?;
for (name, ext_type) in vars {
self.write_extended_type(&mut hdf5_file, name, ext_type)?;
}
hdf5_file.close()?;
Ok(())
}
#[cfg(feature = "hdf5")]
pub fn read_extended<P: AsRef<Path>>(
&self,
path: P,
) -> Result<HashMap<String, ExtendedMatType>> {
let hdf5_file = HDF5File::open(path, FileMode::ReadOnly)?;
let mut vars = HashMap::new();
let items = hdf5_file.list_all_items();
for item in items {
if let Ok(ext_type) = self.read_extended_type(&hdf5_file, &item) {
vars.insert(item.trim_start_matches('/').to_string(), ext_type);
}
}
Ok(vars)
}
#[cfg(feature = "hdf5")]
fn write_extended_type(
&self,
file: &mut HDF5File,
name: &str,
ext_type: &ExtendedMatType,
) -> Result<()> {
match ext_type {
ExtendedMatType::Standard(mat_type) => self.write_standard_type(file, name, &mat_type),
ExtendedMatType::Table(table) => self.write_table(file, name, table),
ExtendedMatType::Categorical(cat_array) => {
self.write_categorical(file, name, cat_array)
}
ExtendedMatType::DateTime(dt_array) => self.write_datetime(file, name, dt_array),
ExtendedMatType::StringArray(strings) => self.write_string_array(file, name, strings),
ExtendedMatType::FunctionHandle(func_handle) => {
self.write_function_handle(file, name, func_handle)
}
ExtendedMatType::Object(object) => self.write_object(file, name, object),
ExtendedMatType::ComplexDouble(array) => self.write_complex_double(file, name, array),
ExtendedMatType::ComplexSingle(array) => self.write_complex_single(file, name, array),
}
}
#[cfg(feature = "hdf5")]
fn write_table(&self, file: &mut HDF5File, name: &str, table: &MatlabTable) -> Result<()> {
file.create_group(name)?;
file.set_attribute(
name,
"MATLAB_class",
AttributeValue::String("table".to_string()),
)?;
let var_names_data: Vec<u16> = table
.variable_names
.iter()
.flat_map(|s| s.encode_utf16())
.collect();
let var_names_array = scirs2_core::ndarray::Array1::from_vec(var_names_data).into_dyn();
file.create_dataset_from_array(
&format!("{}/varnames", name),
&var_names_array,
Some(DatasetOptions::default()),
)?;
for (var_name, var_data) in &table.data {
let var_path = format!("{}/{}", name, var_name);
self.write_standard_type(file, &var_path, var_data)?;
}
if let Some(ref row_names) = table.row_names {
let row_names_data: Vec<u16> =
row_names.iter().flat_map(|s| s.encode_utf16()).collect();
let row_names_array = scirs2_core::ndarray::Array1::from_vec(row_names_data).into_dyn();
file.create_dataset_from_array(
&format!("{}/rownames", name),
&row_names_array,
Some(DatasetOptions::default()),
)?;
}
for (prop_name, prop_value) in &table.properties {
file.set_attribute(
name,
&format!("property_{}", prop_name),
AttributeValue::String(prop_value.clone()),
)?;
}
Ok(())
}
#[cfg(feature = "hdf5")]
fn write_categorical(
&self,
file: &mut HDF5File,
name: &str,
cat_array: &CategoricalArray,
) -> Result<()> {
file.create_group(name)?;
file.set_attribute(
name,
"MATLAB_class",
AttributeValue::String("categorical".to_string()),
)?;
let cats_data: Vec<u16> = cat_array
.categories
.iter()
.flat_map(|s| s.encode_utf16())
.collect();
let cats_array = scirs2_core::ndarray::Array1::from_vec(cats_data).into_dyn();
file.create_dataset_from_array(
&format!("{}/categories", name),
&cats_array,
Some(DatasetOptions::default()),
)?;
file.create_dataset_from_array(
&format!("{}/data", name),
&cat_array.data,
Some(DatasetOptions::default()),
)?;
file.set_attribute(name, "ordered", AttributeValue::Boolean(cat_array.ordered))?;
Ok(())
}
#[cfg(feature = "hdf5")]
fn write_datetime(
&self,
file: &mut HDF5File,
name: &str,
dt_array: &DateTimeArray,
) -> Result<()> {
file.create_dataset_from_array(
name,
&dt_array.data,
Some(DatasetOptions {
compression: self.compression.clone().unwrap_or_default(),
..Default::default()
}),
)?;
file.set_attribute(
name,
"MATLAB_class",
AttributeValue::String("datetime".to_string()),
)?;
if let Some(ref tz) = dt_array.timezone {
file.set_attribute(name, "timezone", AttributeValue::String(tz.clone()))?;
}
file.set_attribute(
name,
"format",
AttributeValue::String(dt_array.format.clone()),
)?;
Ok(())
}
#[cfg(feature = "hdf5")]
fn write_string_array(
&self,
file: &mut HDF5File,
name: &str,
strings: &[String],
) -> Result<()> {
file.create_group(name)?;
file.set_attribute(
name,
"MATLAB_class",
AttributeValue::String("string".to_string()),
)?;
for (i, string) in strings.iter().enumerate() {
let string_data: Vec<u16> = string.encode_utf16().collect();
let string_array = scirs2_core::ndarray::Array1::from_vec(string_data).into_dyn();
file.create_dataset_from_array(
&format!("{}/string_{}", name, i),
&string_array,
Some(DatasetOptions::default()),
)?;
}
file.set_attribute(
name,
"size",
AttributeValue::Array(vec![strings.len() as i64]),
)?;
Ok(())
}
#[cfg(feature = "hdf5")]
fn write_function_handle(
&self,
file: &mut HDF5File,
name: &str,
func_handle: &FunctionHandle,
) -> Result<()> {
file.create_group(name)?;
file.set_attribute(
name,
"MATLAB_class",
AttributeValue::String("function_handle".to_string()),
)?;
let func_data: Vec<u16> = func_handle.function.encode_utf16().collect();
let func_array = scirs2_core::ndarray::Array1::from_vec(func_data).into_dyn();
file.create_dataset_from_array(
&format!("{}/function", name),
&func_array,
Some(DatasetOptions::default()),
)?;
file.set_attribute(
name,
"type",
AttributeValue::String(func_handle.function_type.clone()),
)?;
if let Some(ref workspace) = func_handle.workspace {
let ws_group = format!("{}/workspace", name);
file.create_group(&ws_group)?;
for (var_name, var_data) in workspace {
let var_path = format!("{}/{}", ws_group, var_name);
self.write_standard_type(file, &var_path, var_data)?;
}
}
Ok(())
}
#[cfg(feature = "hdf5")]
fn write_object(&self, file: &mut HDF5File, name: &str, object: &MatlabObject) -> Result<()> {
file.create_group(name)?;
file.set_attribute(
name,
"MATLAB_class",
AttributeValue::String(object.class_name.clone()),
)?;
file.set_attribute(name, "MATLAB_object", AttributeValue::Boolean(true))?;
let props_group = format!("{}/properties", name);
file.create_group(&props_group)?;
for (prop_name, prop_data) in &object.properties {
let prop_path = format!("{}/{}", props_group, prop_name);
self.write_standard_type(file, &prop_path, prop_data)?;
}
if let Some(ref superclass) = object.superclass_data {
let super_path = format!("{}/superclass", name);
self.write_object(file, &super_path, superclass)?;
}
Ok(())
}
#[cfg(feature = "hdf5")]
fn write_complex_double(
&self,
file: &mut HDF5File,
name: &str,
array: &ArrayD<scirs2_core::numeric::Complex<f64>>,
) -> Result<()> {
let real_part = array.mapv(|x| x.re);
let imag_part = array.mapv(|x| x.im);
file.create_group(name)?;
file.set_attribute(
name,
"MATLAB_class",
AttributeValue::String("double".to_string()),
)?;
file.set_attribute(name, "MATLAB_complex", AttributeValue::Boolean(true))?;
file.create_dataset_from_array(
&format!("{}/real", name),
&real_part,
Some(DatasetOptions {
compression: self.compression.clone().unwrap_or_default(),
..Default::default()
}),
)?;
file.create_dataset_from_array(
&format!("{}/imag", name),
&imag_part,
Some(DatasetOptions {
compression: self.compression.clone().unwrap_or_default(),
..Default::default()
}),
)?;
Ok(())
}
#[cfg(feature = "hdf5")]
fn write_complex_single(
&self,
file: &mut HDF5File,
name: &str,
array: &ArrayD<scirs2_core::numeric::Complex<f32>>,
) -> Result<()> {
let real_part = array.mapv(|x| x.re);
let imag_part = array.mapv(|x| x.im);
file.create_group(name)?;
file.set_attribute(
name,
"MATLAB_class",
AttributeValue::String("single".to_string()),
)?;
file.set_attribute(name, "MATLAB_complex", AttributeValue::Boolean(true))?;
file.create_dataset_from_array(
&format!("{}/real", name),
&real_part,
Some(DatasetOptions {
compression: self.compression.clone().unwrap_or_default(),
..Default::default()
}),
)?;
file.create_dataset_from_array(
&format!("{}/imag", name),
&imag_part,
Some(DatasetOptions {
compression: self.compression.clone().unwrap_or_default(),
..Default::default()
}),
)?;
Ok(())
}
#[cfg(feature = "hdf5")]
fn write_standard_type(
&self,
file: &mut HDF5File,
name: &str,
mat_type: &MatType,
) -> Result<()> {
Err(IoError::Other("Not implemented yet".to_string()))
}
#[cfg(feature = "hdf5")]
fn read_extended_type(&self, file: &HDF5File, name: &str) -> Result<ExtendedMatType> {
if let Ok(Some(class_attr)) = file.get_attribute(name, "MATLAB_class") {
match class_attr {
AttributeValue::String(class_name) => {
match class_name.as_str() {
"table" => self.read_table(file, name),
"categorical" => self.read_categorical(file, name),
"datetime" => self.read_datetime(file, name),
"string" => self.read_string_array(file, name),
"function_handle" => self.read_function_handle(file, name),
_ => {
if let Ok(Some(AttributeValue::Boolean(true))) =
file.get_attribute(name, "MATLAB_object")
{
self.read_object(file, name)
} else {
Err(IoError::Other(
"Standard type reading not implemented".to_string(),
))
}
}
}
}
_ => Err(IoError::Other("Invalid MATLAB_class attribute".to_string())),
}
} else {
Err(IoError::Other("Missing MATLAB_class attribute".to_string()))
}
}
#[cfg(feature = "hdf5")]
fn read_table(&self, file: &HDF5File, name: &str) -> Result<ExtendedMatType> {
Err(IoError::Other(
"Table reading not implemented yet".to_string(),
))
}
#[cfg(feature = "hdf5")]
fn read_categorical(&self, file: &HDF5File, name: &str) -> Result<ExtendedMatType> {
Err(IoError::Other(
"Categorical reading not implemented yet".to_string(),
))
}
#[cfg(feature = "hdf5")]
fn read_datetime(&self, file: &HDF5File, name: &str) -> Result<ExtendedMatType> {
Err(IoError::Other(
"DateTime reading not implemented yet".to_string(),
))
}
#[cfg(feature = "hdf5")]
fn read_string_array(&self, file: &HDF5File, name: &str) -> Result<ExtendedMatType> {
Err(IoError::Other(
"String array reading not implemented yet".to_string(),
))
}
#[cfg(feature = "hdf5")]
fn read_function_handle(&self, file: &HDF5File, name: &str) -> Result<ExtendedMatType> {
Err(IoError::Other(
"Function handle reading not implemented yet".to_string(),
))
}
#[cfg(feature = "hdf5")]
fn read_object(&self, file: &HDF5File, name: &str) -> Result<ExtendedMatType> {
Err(IoError::Other(
"Object reading not implemented yet".to_string(),
))
}
}
pub struct PartialIoSupport;
impl PartialIoSupport {
#[cfg(feature = "hdf5")]
pub fn read_array_slice<T, P: AsRef<Path>>(
path: P,
var_name: &str,
start: &[usize],
count: &[usize],
) -> Result<ArrayD<T>>
where
T: Default + Clone,
{
Err(IoError::Other(
"Partial I/O not implemented yet".to_string(),
))
}
#[cfg(feature = "hdf5")]
pub fn write_array_slice<T, P: AsRef<Path>>(
path: P,
var_name: &str,
data: &ArrayD<T>,
start: &[usize],
) -> Result<()>
where
T: Default + Clone,
{
Err(IoError::Other(
"Partial I/O not implemented yet".to_string(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_v73_features_default() {
let features = V73Features::default();
assert!(features.enable_partial_io);
assert!(features.support_objects);
assert!(features.support_tables);
}
#[test]
fn test_matlab_table_creation() {
let mut table = MatlabTable {
variable_names: vec!["x".to_string(), "y".to_string()],
row_names: Some(vec!["row1".to_string(), "row2".to_string()]),
data: HashMap::new(),
properties: HashMap::new(),
};
table.data.insert(
"x".to_string(),
MatType::Double(ArrayD::zeros(IxDyn(&[2, 1]))),
);
table.data.insert(
"y".to_string(),
MatType::Double(ArrayD::ones(IxDyn(&[2, 1]))),
);
assert_eq!(table.variable_names.len(), 2);
assert_eq!(table.data.len(), 2);
}
}