#[cfg(feature = "arrow")]
mod arrow;
#[cfg(feature = "asc")]
mod asc;
#[cfg(feature = "hdf5")]
mod hdf5;
#[cfg(feature = "mat")]
mod mat;
#[cfg(feature = "mat73")]
mod mat73;
#[cfg(feature = "mat4")]
mod mat_v4;
#[cfg(feature = "parquet")]
mod parquet;
#[cfg(feature = "arrow")]
pub use arrow::{to_record_batch, write_arrow_ipc};
#[cfg(feature = "asc")]
pub use asc::{write_asc, write_asc_frames};
#[cfg(feature = "hdf5")]
pub use hdf5::write_hdf5;
#[cfg(feature = "mat")]
pub use mat::write_mat;
#[cfg(feature = "mat73")]
pub use mat73::write_mat73;
#[cfg(feature = "mat4")]
pub use mat_v4::write_mat_v4;
#[cfg(feature = "parquet")]
pub use parquet::{write_parquet, write_parquet_with, ParquetCompression};
use std::fmt::Write as _;
use std::io::Write;
use crate::error::{Mf4Error, Result};
use crate::model::{Channel, Signal, SignalValues};
use crate::Mf4File;
pub(crate) fn element_columns(values: &[f64], elements_per_sample: usize) -> Vec<Vec<f64>> {
if elements_per_sample == 0 {
return Vec::new();
}
let mut columns =
vec![Vec::with_capacity(values.len() / elements_per_sample); elements_per_sample];
for chunk in values.chunks_exact(elements_per_sample) {
for (column, &value) in chunk.iter().enumerate() {
columns[column].push(value);
}
}
columns
}
pub(crate) fn array_index_suffixes(
shape: Option<&[u64]>,
elements_per_sample: usize,
) -> Vec<String> {
if elements_per_sample == 0 {
return Vec::new();
}
match shape {
Some(dims)
if dims.len() > 1 && dims.iter().product::<u64>() == elements_per_sample as u64 =>
{
let mut result = Vec::with_capacity(elements_per_sample);
let mut indices = vec![0u64; dims.len()];
for _ in 0..elements_per_sample {
let suffix = indices
.iter()
.map(|idx| format!("[{idx}]"))
.collect::<String>();
result.push(suffix);
for d in (0..dims.len()).rev() {
indices[d] += 1;
if indices[d] < dims[d] {
break;
}
indices[d] = 0;
}
}
result
}
Some(dims) if dims.len() == 1 && dims[0] == elements_per_sample as u64 => {
(0..elements_per_sample).map(|i| format!("[{i}]")).collect()
}
_ => (0..elements_per_sample).map(|i| format!("[{i}]")).collect(),
}
}
pub fn write_csv<W: Write>(file: &Mf4File, channels: &[&Channel], out: &mut W) -> Result<()> {
let Some(first) = channels.first() else {
return Ok(());
};
let time = time_column(file, first);
let mut headers = Vec::new();
headers.push(match &time {
Some((_, unit)) => format!("Time [{unit}]"),
None => "Index".to_string(),
});
let mut columns = Vec::new();
for channel in channels {
let signal = file.signal(channel)?;
let col_data = csv_columns_for_signal(&signal)?;
for (header, values) in col_data {
headers.push(header);
columns.push(values);
}
}
writeln!(
out,
"{}",
headers
.iter()
.map(|h| csv_field(h))
.collect::<Vec<_>>()
.join(",")
)?;
let row_count = columns
.iter()
.map(Vec::len)
.chain(time.as_ref().map(|(times, _)| times.len()))
.max()
.unwrap_or(0);
let mut row = String::with_capacity(16 * (columns.len() + 1));
for row_index in 0..row_count {
row.clear();
match &time {
Some((times, _)) => {
if let Some(t) = times.get(row_index) {
write!(row, "{t:.9}").expect("writing to a String cannot fail");
}
}
None => write!(row, "{row_index}").expect("writing to a String cannot fail"),
}
for column in &columns {
row.push(',');
if let Some(value) = column.get(row_index) {
write!(row, "{value:.9}").expect("writing to a String cannot fail");
}
}
row.push('\n');
out.write_all(row.as_bytes())?;
}
Ok(())
}
fn csv_columns_for_signal(signal: &Signal) -> Result<Vec<(String, Vec<f64>)>> {
let unit = signal.unit();
let name = signal.name();
let shape = signal.channel.array_shape.as_deref();
let values = signal.values()?;
match values {
SignalValues::Complex { re, im } => {
let col_re = (
column_header_with_name(&format!("{name}.re"), unit),
re,
);
let col_im = (
column_header_with_name(&format!("{name}.im"), unit),
im,
);
Ok(vec![col_re, col_im])
}
SignalValues::CanopenDate(v) => {
let nanos: Vec<f64> = v.iter().map(|d| d.to_unix_nanos() as f64).collect();
Ok(vec![(column_header_with_name(name, unit), nanos)])
}
SignalValues::CanopenTime(v) => {
let nanos: Vec<f64> = v.iter().map(|t| t.to_unix_nanos() as f64).collect();
Ok(vec![(column_header_with_name(name, unit), nanos)])
}
SignalValues::Array {
values,
elements_per_sample,
} => {
let eps = elements_per_sample;
let suffixes = array_index_suffixes(shape, eps);
let element_values = element_columns(&values, eps);
let mut cols = Vec::with_capacity(eps);
for (elem_vals, suffix) in element_values.into_iter().zip(suffixes) {
let col_name = format!("{name}{suffix}");
let header = column_header_with_name(&col_name, unit);
cols.push((header, elem_vals));
}
Ok(cols)
}
SignalValues::ArrayVarLen { .. } => Err(Mf4Error::unsupported(
"CSV export",
format!(
"channel '{name}' holds variable-length array samples, which have no fixed column shape and cannot be exported to a tabular format"
),
)),
_ => {
let vals = signal.values_f64()?;
Ok(vec![(column_header_with_name(name, unit), vals)])
}
}
}
fn time_column(file: &Mf4File, channel: &Channel) -> Option<(Vec<f64>, String)> {
let dg = &file.data_groups()[channel.data_group_index];
let cg = &dg.channel_groups[channel.channel_group_index];
let signal = file.signal(cg.master_channel()?).ok()?;
let unit = signal.unit().to_string();
let times = signal.values_f64().ok()?;
Some((times, unit))
}
fn column_header_with_name(name: &str, unit: &str) -> String {
if unit.is_empty() {
name.to_string()
} else {
format!("{name} [{unit}]")
}
}
#[allow(dead_code)]
fn column_header(channel: &Channel) -> String {
column_header_with_name(&channel.name, &channel.unit)
}
fn csv_field(field: &str) -> std::borrow::Cow<'_, str> {
if field.contains([',', '"', '\n', '\r']) {
std::borrow::Cow::Owned(format!("\"{}\"", field.replace('"', "\"\"")))
} else {
std::borrow::Cow::Borrowed(field)
}
}