use core::fmt;
use serde::Deserialize;
use serde::de::{self, Deserializer, MapAccess, Visitor};
use crate::mat::de::mcos_reader::TABLE_META_KEY;
use crate::mat::matrix::Matrix;
use crate::mat::opaque::{MatCategorical, MatDatetime, MatDuration};
pub(crate) const MATCOLUMN_SENTINEL: &str = "__hdf5_pure_mat_MatColumn__";
pub(crate) const MATTABLE_SENTINEL: &str = "__hdf5_pure_mat_MatTable__";
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum MatColumn {
Numeric(Matrix<f64>),
Logical(Vec<bool>),
Text(Vec<String>),
Datetime(MatDatetime),
Duration(MatDuration),
Categorical(MatCategorical),
Other,
}
impl MatColumn {
#[must_use]
pub fn kind(&self) -> &'static str {
match self {
MatColumn::Numeric(_) => "numeric",
MatColumn::Logical(_) => "logical",
MatColumn::Text(_) => "text",
MatColumn::Datetime(_) => "datetime",
MatColumn::Duration(_) => "duration",
MatColumn::Categorical(_) => "categorical",
MatColumn::Other => "other",
}
}
#[must_use]
pub fn as_f64(&self) -> Option<&[f64]> {
match self {
MatColumn::Numeric(m) => Some(m.data()),
_ => None,
}
}
#[must_use]
pub fn as_matrix(&self) -> Option<&Matrix<f64>> {
match self {
MatColumn::Numeric(m) => Some(m),
_ => None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<&[bool]> {
match self {
MatColumn::Logical(v) => Some(v),
_ => None,
}
}
#[must_use]
pub fn as_strings(&self) -> Option<&[String]> {
match self {
MatColumn::Text(v) => Some(v),
_ => None,
}
}
#[must_use]
pub fn as_datetime(&self) -> Option<&MatDatetime> {
match self {
MatColumn::Datetime(d) => Some(d),
_ => None,
}
}
#[must_use]
pub fn as_duration(&self) -> Option<&MatDuration> {
match self {
MatColumn::Duration(d) => Some(d),
_ => None,
}
}
#[must_use]
pub fn as_categorical(&self) -> Option<&MatCategorical> {
match self {
MatColumn::Categorical(c) => Some(c),
_ => None,
}
}
}
impl<'de> Deserialize<'de> for MatColumn {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_struct(MATCOLUMN_SENTINEL, &["kind", "value"], MatColumnVisitor)
}
}
struct MatColumnVisitor;
impl<'de> Visitor<'de> for MatColumnVisitor {
type Value = MatColumn;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a MATLAB table column")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<MatColumn, A::Error> {
let kind_key: Option<String> = map.next_key()?;
if kind_key.as_deref() != Some("kind") {
return Err(de::Error::custom("MatColumn: expected `kind` first"));
}
let kind: String = map.next_value()?;
let value_key: Option<String> = map.next_key()?;
if value_key.as_deref() != Some("value") {
return Err(de::Error::custom(
"MatColumn: expected `value` after `kind`",
));
}
let column = match kind.as_str() {
"numeric" => MatColumn::Numeric(map.next_value()?),
"logical" => MatColumn::Logical(map.next_value()?),
"text" => MatColumn::Text(map.next_value()?),
"datetime" => MatColumn::Datetime(map.next_value()?),
"duration" => MatColumn::Duration(map.next_value()?),
"categorical" => MatColumn::Categorical(map.next_value()?),
_ => {
let _: de::IgnoredAny = map.next_value()?;
MatColumn::Other
}
};
while map.next_key::<de::IgnoredAny>()?.is_some() {
let _: de::IgnoredAny = map.next_value()?;
}
Ok(column)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatTable {
columns: Vec<(String, MatColumn)>,
num_rows: usize,
row_names: Vec<String>,
}
impl MatTable {
#[must_use]
pub fn num_rows(&self) -> usize {
self.num_rows
}
#[must_use]
pub fn num_variables(&self) -> usize {
self.columns.len()
}
pub fn variable_names(&self) -> impl Iterator<Item = &str> {
self.columns.iter().map(|(n, _)| n.as_str())
}
#[must_use]
pub fn row_names(&self) -> &[String] {
&self.row_names
}
pub fn columns(&self) -> impl Iterator<Item = (&str, &MatColumn)> {
self.columns.iter().map(|(n, c)| (n.as_str(), c))
}
#[must_use]
pub fn column(&self, name: &str) -> Option<&MatColumn> {
self.columns.iter().find(|(n, _)| n == name).map(|(_, c)| c)
}
}
impl<'de> Deserialize<'de> for MatTable {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let parts = deserializer.deserialize_struct(MATTABLE_SENTINEL, &[], TabularVisitor)?;
Ok(MatTable {
columns: parts.columns,
num_rows: parts.num_rows,
row_names: parts.meta.row_names,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatTimetable {
columns: Vec<(String, MatColumn)>,
num_rows: usize,
row_times: Option<MatColumn>,
}
impl MatTimetable {
#[must_use]
pub fn num_rows(&self) -> usize {
self.num_rows
}
#[must_use]
pub fn num_variables(&self) -> usize {
self.columns.len()
}
pub fn variable_names(&self) -> impl Iterator<Item = &str> {
self.columns.iter().map(|(n, _)| n.as_str())
}
#[must_use]
pub fn row_times(&self) -> Option<&MatColumn> {
self.row_times.as_ref()
}
pub fn columns(&self) -> impl Iterator<Item = (&str, &MatColumn)> {
self.columns.iter().map(|(n, c)| (n.as_str(), c))
}
#[must_use]
pub fn column(&self, name: &str) -> Option<&MatColumn> {
self.columns.iter().find(|(n, _)| n == name).map(|(_, c)| c)
}
}
impl<'de> Deserialize<'de> for MatTimetable {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let parts = deserializer.deserialize_struct(MATTABLE_SENTINEL, &[], TabularVisitor)?;
Ok(MatTimetable {
columns: parts.columns,
num_rows: parts.num_rows,
row_times: parts.meta.row_times,
})
}
}
#[derive(Default, Deserialize)]
struct TabularMeta {
#[serde(default)]
row_names: Vec<String>,
#[serde(default)]
row_times: Option<MatColumn>,
#[serde(default)]
num_rows: Option<f64>,
}
struct TabularParts {
columns: Vec<(String, MatColumn)>,
num_rows: usize,
meta: TabularMeta,
}
struct TabularVisitor;
impl<'de> Visitor<'de> for TabularVisitor {
type Value = TabularParts;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a MATLAB table or timetable")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<TabularParts, A::Error> {
let mut columns: Vec<(String, MatColumn)> = Vec::new();
let mut meta = TabularMeta::default();
while let Some(key) = map.next_key::<String>()? {
if key == TABLE_META_KEY {
meta = map.next_value()?;
} else {
columns.push((key, map.next_value()?));
}
}
let num_rows = match meta.num_rows {
Some(n) if n >= 0.0 => rows_from_f64(n),
_ => columns.first().map_or(0, |(_, c)| column_len(c)),
};
Ok(TabularParts {
columns,
num_rows,
meta,
})
}
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "a table's row count is a small non-negative integer stored as f64; \
the caller guards n >= 0.0 and the value is well within usize"
)]
fn rows_from_f64(n: f64) -> usize {
n as usize
}
fn column_len(column: &MatColumn) -> usize {
match column {
MatColumn::Numeric(m) => m.rows().max(m.cols()),
MatColumn::Logical(v) => v.len(),
MatColumn::Text(v) => v.len(),
MatColumn::Datetime(d) => d.millis_utc.len(),
MatColumn::Duration(d) => d.millis.len(),
MatColumn::Categorical(c) => c.codes.len(),
MatColumn::Other => 0,
}
}