Initial commit
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
//! 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user