use std::io::Write;
use std::sync::Arc;
use arrow_array::builder::{
BinaryBuilder, Float64Builder, ListBuilder, NullBufferBuilder, StringBuilder,
};
use arrow_array::{
ArrayRef, Float32Array, Float64Array, Int16Array, Int32Array, Int64Array, Int8Array,
RecordBatch, UInt16Array, UInt32Array, UInt64Array, UInt8Array,
};
use arrow_ipc::writer::FileWriter;
use arrow_schema::{DataType, Field, Schema};
use crate::error::{Mf4Error, Result};
use crate::export::{array_index_suffixes, element_columns};
use crate::model::SignalValues;
use crate::time_ops::SignalSeries;
pub fn to_record_batch(series: &[SignalSeries]) -> Result<RecordBatch> {
let timestamps = shared_timestamps(series)?;
let mut fields = Vec::with_capacity(series.len() + 1);
let mut columns: Vec<ArrayRef> = Vec::with_capacity(series.len() + 1);
fields.push(Field::new("time", DataType::Float64, false));
columns.push(Arc::new(Float64Array::from(timestamps.to_vec())));
for s in series {
let cols = columns_for(s)?;
for (col_name, column) in cols {
fields.push(Field::new(
col_name,
column.data_type().clone(),
s.validity().is_some(),
));
columns.push(column);
}
}
let schema = Arc::new(Schema::new(fields));
RecordBatch::try_new(schema, columns)
.map_err(|e| Mf4Error::write_error(format!("could not assemble the Arrow table: {e}")))
}
pub fn write_arrow_ipc<W: Write>(series: &[SignalSeries], out: &mut W) -> Result<()> {
let batch = to_record_batch(series)?;
let mut writer = FileWriter::try_new(out, &batch.schema())
.map_err(|e| Mf4Error::write_error(format!("could not open the Arrow IPC writer: {e}")))?;
writer
.write(&batch)
.map_err(|e| Mf4Error::write_error(format!("could not write the Arrow IPC table: {e}")))?;
writer
.finish()
.map_err(|e| Mf4Error::write_error(format!("could not finish the Arrow IPC file: {e}")))?;
Ok(())
}
fn shared_timestamps(series: &[SignalSeries]) -> Result<&[f64]> {
let Some(first) = series.first() else {
return Ok(&[]);
};
for s in &series[1..] {
if s.timestamps() != first.timestamps() {
return Err(Mf4Error::write_error(format!(
"Arrow export needs one time axis for the whole table, but '{}' has {} \
samples and '{}' has {}; put them on a common raster with `Mf4File::resample` \
first",
first.name(),
first.len(),
s.name(),
s.len()
)));
}
}
Ok(first.timestamps())
}
fn columns_for(series: &SignalSeries) -> Result<Vec<(String, ArrayRef)>> {
let keep = |i: usize| series.validity().is_none_or(|v| v[i]);
macro_rules! numeric {
($array:ty, $values:expr) => {{
let values = $values;
match series.validity() {
None => Arc::new(<$array>::from_iter_values(values.iter().copied())) as ArrayRef,
Some(mask) => {
let mut nulls = NullBufferBuilder::new(values.len());
let mut out = Vec::with_capacity(values.len());
for (i, &x) in values.iter().enumerate() {
if mask[i] {
nulls.append_non_null();
out.push(x);
} else {
nulls.append_null();
out.push(Default::default());
}
}
Arc::new(<$array>::from_iter_values_with_nulls(out, nulls.finish())) as ArrayRef
}
}
}};
}
match series.values() {
SignalValues::U8(v) => Ok(vec![(series.name().to_string(), numeric!(UInt8Array, v))]),
SignalValues::U16(v) => Ok(vec![(series.name().to_string(), numeric!(UInt16Array, v))]),
SignalValues::U32(v) => Ok(vec![(series.name().to_string(), numeric!(UInt32Array, v))]),
SignalValues::U64(v) => Ok(vec![(series.name().to_string(), numeric!(UInt64Array, v))]),
SignalValues::I8(v) => Ok(vec![(series.name().to_string(), numeric!(Int8Array, v))]),
SignalValues::I16(v) => Ok(vec![(series.name().to_string(), numeric!(Int16Array, v))]),
SignalValues::I32(v) => Ok(vec![(series.name().to_string(), numeric!(Int32Array, v))]),
SignalValues::I64(v) => Ok(vec![(series.name().to_string(), numeric!(Int64Array, v))]),
SignalValues::F32(v) => Ok(vec![(series.name().to_string(), numeric!(Float32Array, v))]),
SignalValues::F64(v) => Ok(vec![(series.name().to_string(), numeric!(Float64Array, v))]),
SignalValues::Str(v) => {
let mut builder = StringBuilder::new();
for (i, s) in v.iter().enumerate() {
if keep(i) {
builder.append_value(s);
} else {
builder.append_null();
}
}
Ok(vec![(
series.name().to_string(),
Arc::new(builder.finish()) as ArrayRef,
)])
}
SignalValues::Bytes { .. } | SignalValues::VarBytes { .. } => {
let mut builder = BinaryBuilder::new();
for i in 0..series.len() {
match series.values().bytes_at(i) {
Some(bytes) if keep(i) => builder.append_value(bytes),
_ => builder.append_null(),
}
}
Ok(vec![(
series.name().to_string(),
Arc::new(builder.finish()) as ArrayRef,
)])
}
SignalValues::Complex { re, im } => {
let re_col = numeric!(Float64Array, re);
let im_col = numeric!(Float64Array, im);
Ok(vec![
(format!("{}.re", series.name()), re_col),
(format!("{}.im", series.name()), im_col),
])
}
SignalValues::CanopenDate(v) => {
let nanos: Vec<i64> = v.iter().map(|d| d.to_unix_nanos()).collect();
let col = numeric!(Int64Array, nanos);
Ok(vec![(series.name().to_string(), col)])
}
SignalValues::CanopenTime(v) => {
let nanos: Vec<i64> = v.iter().map(|t| t.to_unix_nanos()).collect();
let col = numeric!(Int64Array, nanos);
Ok(vec![(series.name().to_string(), col)])
}
SignalValues::Array {
values,
elements_per_sample,
} => {
let eps = *elements_per_sample;
let suffixes = array_index_suffixes(series.channel.array_shape.as_deref(), eps);
let mut cols = Vec::with_capacity(eps);
for (elem_vals, suffix) in element_columns(values, eps).into_iter().zip(suffixes) {
let name = format!("{}{suffix}", series.name());
let col = numeric!(Float64Array, elem_vals);
cols.push((name, col));
}
Ok(cols)
}
SignalValues::ArrayVarLen { values, starts } => {
let mut builder = ListBuilder::new(Float64Builder::with_capacity(values.len()));
for i in 0..series.len() {
let elements = starts
.get(i)
.zip(starts.get(i + 1))
.and_then(|(&a, &b)| values.get(a..b));
match elements {
Some(elements) if keep(i) => {
builder.values().append_slice(elements);
builder.append(true);
}
_ => builder.append(false),
}
}
Ok(vec![(
series.name().to_string(),
Arc::new(builder.finish()) as ArrayRef,
)])
}
}
}