use alloc::vec::Vec;
use core::fmt;
use crate::domain::{Channel, FileRevision, GraphMetadata, Journal, Marker};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Datafile {
pub metadata: GraphMetadata,
pub channels: Vec<Channel>,
pub markers: Vec<Marker>,
pub journal: Option<Journal>,
}
impl Datafile {
pub fn channel_by_name(&self, name: &str) -> Option<&Channel> {
self.channels.iter().find(|c| c.name == name)
}
pub const fn base_sample_rate(&self) -> f64 {
self.metadata.samples_per_second
}
pub const fn channel_count(&self) -> usize {
self.channels.len()
}
pub const fn marker_count(&self) -> usize {
self.markers.len()
}
pub fn channel(&self, name: &str) -> Option<&Channel> {
self.channel_by_name(name)
}
pub fn channels(&self) -> impl Iterator<Item = &Channel> {
self.channels.iter()
}
pub const fn revision(&self) -> FileRevision {
self.metadata.file_revision
}
pub const fn samples_per_second(&self) -> f64 {
self.metadata.samples_per_second
}
pub fn duration(&self) -> Option<f64> {
self.channels
.iter()
.filter(|ch| ch.samples_per_second > 0.0 && ch.point_count > 0)
.map(|ch| {
#[expect(
clippy::cast_precision_loss,
reason = "point_count is a sample index; precision loss is negligible for physiological recordings"
)]
let count = ch.point_count as f64;
count / ch.samples_per_second
})
.reduce(f64::max)
}
pub fn summary(&self) -> impl fmt::Display + '_ {
DatafileSummary(self)
}
#[cfg(feature = "write")]
pub fn set_channel_data(
&mut self,
index: usize,
data: crate::domain::ChannelData,
) -> Result<(), crate::error::BiopacError> {
let ch = self
.channels
.get_mut(index)
.ok_or_else(|| crate::error::BiopacError::InvalidChannel(alloc::format!("{index}")))?;
let new_len = data.len();
ch.data = data;
ch.point_count = new_len;
Ok(())
}
#[cfg(feature = "write")]
pub fn add_marker(&mut self, marker: crate::domain::Marker) {
self.markers.push(marker);
}
#[cfg(feature = "write")]
pub fn set_journal(&mut self, text: impl Into<alloc::string::String>) {
self.journal = Some(crate::domain::Journal::Plain(text.into()));
}
}
impl fmt::Display for Datafile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Datafile({} channels, {} markers, {})",
self.channels.len(),
self.markers.len(),
self.metadata.file_revision,
)
}
}
struct DatafileSummary<'a>(&'a Datafile);
impl fmt::Display for DatafileSummary<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let d = self.0;
writeln!(
f,
"AcqKnowledge file revision={} ({}) compressed={} rate={} Hz",
d.metadata.file_revision.0,
d.metadata.file_revision.display_version(),
d.metadata.compressed,
d.metadata.samples_per_second,
)?;
for (i, ch) in d.channels.iter().enumerate() {
writeln!(f, " [{i}] {ch}")?;
}
if d.markers.is_empty() {
writeln!(f, " (no markers)")?;
} else {
writeln!(f, " {} marker(s)", d.markers.len())?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{ByteOrder, ChannelData, FileRevision};
fn make_datafile() -> Datafile {
Datafile {
metadata: GraphMetadata {
file_revision: FileRevision::new(73),
samples_per_second: 1000.0,
channel_count: 2,
byte_order: ByteOrder::LittleEndian,
compressed: false,
title: None,
acquisition_datetime: None,
max_samples_per_second: None,
},
channels: alloc::vec![
Channel {
name: String::from("ECG"),
units: String::from("mV"),
samples_per_second: 1000.0,
frequency_divider: 1,
data: ChannelData::Raw(alloc::vec![0, 1, 2]),
point_count: 3,
},
Channel {
name: String::from("EEG"),
units: String::from("μV"),
samples_per_second: 500.0,
frequency_divider: 2,
data: ChannelData::Raw(alloc::vec![10, 20]),
point_count: 2,
},
],
markers: Vec::new(),
journal: None,
}
}
use alloc::string::String;
#[test]
fn channel_by_name_found() {
let df = make_datafile();
assert!(df.channel_by_name("ECG").is_some());
assert_eq!(
df.channel_by_name("ECG").map(|c| &c.units),
Some(&String::from("mV"))
);
}
#[test]
fn channel_by_name_missing() {
let df = make_datafile();
assert!(df.channel_by_name("nonexistent").is_none());
}
#[test]
fn display_format_is_not_empty() {
let df = make_datafile();
let s = alloc::format!("{df}");
assert!(s.contains("Datafile("));
assert!(s.contains("2 channels"));
}
}