Files
rustytorch/crates/specialized/rtx-neuro-io/src/bids/entities.rs
T
2026-03-04 00:08:42 +00:00

234 lines
7.1 KiB
Rust

//! BIDS Filename Entity Parsing
//!
//! Parses BIDS-compliant filenames to extract entities like subject, session, task, etc.
use std::path::Path;
/// Parsed BIDS filename entities
#[derive(Debug, Clone, Default)]
pub struct FileEntities {
/// Subject label (required)
pub subject: String,
/// Session label (optional)
pub session: Option<String>,
/// Task label (optional)
pub task: Option<String>,
/// Acquisition label (optional)
pub acquisition: Option<String>,
/// Run number (optional)
pub run: Option<u32>,
/// Processing label (optional)
pub processing: Option<String>,
/// Split number for large files (optional)
pub split: Option<u32>,
/// Data type suffix (meg, eeg, ieeg)
pub datatype: Option<String>,
/// File extension
pub extension: Option<String>,
}
impl FileEntities {
/// Check if this is an MEG file
pub fn is_meg(&self) -> bool {
self.datatype.as_deref() == Some("meg")
}
/// Check if this is an EEG file
pub fn is_eeg(&self) -> bool {
self.datatype.as_deref() == Some("eeg")
}
/// Check if this is an iEEG file
pub fn is_ieeg(&self) -> bool {
self.datatype.as_deref() == Some("ieeg")
}
}
/// Parse a BIDS-compliant filename into entities
///
/// # Arguments
///
/// * `filename` - The filename (with or without path) to parse
///
/// # Returns
///
/// `Some(FileEntities)` if the filename matches BIDS conventions, `None` otherwise.
///
/// # Example
///
/// ```
/// use rtx_neuro_io::bids::parse_bids_filename;
///
/// let entities = parse_bids_filename("sub-01_ses-pre_task-rest_meg.fif").unwrap();
/// assert_eq!(entities.subject, "01");
/// assert_eq!(entities.session, Some("pre".to_string()));
/// assert_eq!(entities.task, Some("rest".to_string()));
/// assert_eq!(entities.datatype, Some("meg".to_string()));
/// ```
pub fn parse_bids_filename(filename: &str) -> Option<FileEntities> {
// Extract just the filename if a path was provided
let filename = Path::new(filename)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(filename);
// Split extension
let (name, extension) = if let Some(pos) = filename.rfind('.') {
let ext = &filename[pos + 1..];
// Strip extension for parsing, including .ds directories
(&filename[..pos], Some(ext.to_string()))
} else {
(filename, None)
};
// Split by underscores
let parts: Vec<&str> = name.split('_').collect();
if parts.is_empty() {
return None;
}
let mut entities = FileEntities {
extension,
..Default::default()
};
// Parse each part
for part in &parts {
if let Some(value) = part.strip_prefix("sub-") {
entities.subject = value.to_string();
} else if let Some(value) = part.strip_prefix("ses-") {
entities.session = Some(value.to_string());
} else if let Some(value) = part.strip_prefix("task-") {
entities.task = Some(value.to_string());
} else if let Some(value) = part.strip_prefix("acq-") {
entities.acquisition = Some(value.to_string());
} else if let Some(value) = part.strip_prefix("run-") {
entities.run = value.parse().ok();
} else if let Some(value) = part.strip_prefix("proc-") {
entities.processing = Some(value.to_string());
} else if let Some(value) = part.strip_prefix("split-") {
entities.split = value.parse().ok();
}
}
// The last part (before extension) should be the datatype
if let Some(last) = parts.last() {
if !last.contains('-') {
match *last {
"meg" | "eeg" | "ieeg" => {
entities.datatype = Some(last.to_string());
}
_ => {}
}
}
}
// Subject is required
if entities.subject.is_empty() {
return None;
}
Some(entities)
}
/// Build a BIDS-compliant filename from entities
pub fn build_bids_filename(entities: &FileEntities) -> String {
let mut parts = vec![format!("sub-{}", entities.subject)];
if let Some(ref ses) = entities.session {
parts.push(format!("ses-{}", ses));
}
if let Some(ref task) = entities.task {
parts.push(format!("task-{}", task));
}
if let Some(ref acq) = entities.acquisition {
parts.push(format!("acq-{}", acq));
}
if let Some(run) = entities.run {
parts.push(format!("run-{:02}", run));
}
if let Some(ref proc) = entities.processing {
parts.push(format!("proc-{}", proc));
}
if let Some(split) = entities.split {
parts.push(format!("split-{:02}", split));
}
if let Some(ref dt) = entities.datatype {
parts.push(dt.clone());
}
let name = parts.join("_");
if let Some(ref ext) = entities.extension {
format!("{}.{}", name, ext)
} else {
name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_filename() {
let entities = parse_bids_filename("sub-01_task-rest_meg.fif").unwrap();
assert_eq!(entities.subject, "01");
assert_eq!(entities.task, Some("rest".to_string()));
assert_eq!(entities.datatype, Some("meg".to_string()));
assert_eq!(entities.extension, Some("fif".to_string()));
}
#[test]
fn test_parse_full_filename() {
let entities =
parse_bids_filename("sub-control01_ses-001_task-rest_acq-neuromag_run-01_meg.fif")
.unwrap();
assert_eq!(entities.subject, "control01");
assert_eq!(entities.session, Some("001".to_string()));
assert_eq!(entities.task, Some("rest".to_string()));
assert_eq!(entities.acquisition, Some("neuromag".to_string()));
assert_eq!(entities.run, Some(1));
assert_eq!(entities.datatype, Some("meg".to_string()));
}
#[test]
fn test_parse_eeg_filename() {
let entities = parse_bids_filename("sub-02_task-memory_eeg.edf").unwrap();
assert_eq!(entities.subject, "02");
assert_eq!(entities.task, Some("memory".to_string()));
assert!(entities.is_eeg());
assert!(!entities.is_meg());
}
#[test]
fn test_parse_ctf_directory() {
let entities = parse_bids_filename("sub-01_task-rest_meg.ds").unwrap();
assert_eq!(entities.subject, "01");
assert_eq!(entities.datatype, Some("meg".to_string()));
assert_eq!(entities.extension, Some("ds".to_string()));
}
#[test]
fn test_parse_invalid_filename() {
assert!(parse_bids_filename("random_file.txt").is_none());
assert!(parse_bids_filename("no_subject_here.fif").is_none());
}
#[test]
fn test_build_filename() {
let entities = FileEntities {
subject: "01".to_string(),
session: Some("pre".to_string()),
task: Some("rest".to_string()),
datatype: Some("meg".to_string()),
extension: Some("fif".to_string()),
..Default::default()
};
assert_eq!(
build_bids_filename(&entities),
"sub-01_ses-pre_task-rest_meg.fif"
);
}
}