234 lines
7.1 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|