modewise_dataset/
events_tsv.rs

1//! BIDS `events.tsv` reader. Onsets and durations are seconds from the first stored
2//! sample (BIDS specification, task events).
3
4use std::path::Path;
5
6use modewise_epochs::{Event, Events};
7
8pub fn read(path: &Path, label_column: &str) -> Result<Events, String> {
9    let text = std::fs::read_to_string(path)
10        .map_err(|e| format!("events file {}: {e}", path.display()))?;
11    let mut lines = text.lines().filter(|l| !l.trim().is_empty());
12    let header: Vec<&str> = lines
13        .next()
14        .ok_or_else(|| format!("{} is empty", path.display()))?
15        .split('\t')
16        .map(str::trim)
17        .collect();
18    let column = |name: &str| {
19        header.iter().position(|h| *h == name).ok_or_else(|| {
20            format!(
21                "{} has no {name:?} column (columns: {})",
22                path.display(),
23                header.join(", ")
24            )
25        })
26    };
27    let (onset, duration, label) = (column("onset")?, column("duration")?, column(label_column)?);
28
29    let mut events = Vec::new();
30    for (row, line) in lines.enumerate() {
31        let cells: Vec<&str> = line.split('\t').map(str::trim).collect();
32        let cell = |i: usize, name: &str| {
33            cells
34                .get(i)
35                .copied()
36                .ok_or_else(|| format!("{} row {}: missing {name}", path.display(), row + 1))
37        };
38        let number = |i: usize, name: &str| -> Result<f64, String> {
39            let value = cell(i, name)?;
40            if value == "n/a" {
41                return Err(format!(
42                    "{} row {}: {name} is n/a; such rows cannot be epoched",
43                    path.display(),
44                    row + 1
45                ));
46            }
47            value.parse().map_err(|_| {
48                format!(
49                    "{} row {}: {name} {value:?} is not a number",
50                    path.display(),
51                    row + 1
52                )
53            })
54        };
55        events.push(Event {
56            onset_s: number(onset, "onset")?,
57            duration_s: number(duration, "duration")?,
58            label: cell(label, label_column)?.to_string(),
59        });
60    }
61    Events::new(events).map_err(|e| format!("{}: {e}", path.display()))
62}