modewise_dataset/
participants.rs1use std::collections::{BTreeMap, BTreeSet};
4use std::path::Path;
5
6use modewise_study::Participants;
7use serde::Deserialize;
8
9use crate::Pattern;
10
11#[derive(Debug, Clone, Deserialize)]
13#[serde(deny_unknown_fields)]
14pub struct ParticipantsSpec {
15 #[serde(default = "default_entity")]
17 pub entity: String,
18 pub tables: Vec<ParticipantTable>,
19}
20
21#[derive(Debug, Clone, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct ParticipantTable {
24 pub path: String,
26 #[serde(default = "default_id_column")]
27 pub id_column: String,
28 pub id_pattern: Option<String>,
30 #[serde(default)]
32 pub missing: Vec<String>,
33 pub columns: Option<Vec<String>>,
35 #[serde(default)]
37 pub prefix: String,
38}
39
40fn default_entity() -> String {
41 "subject".into()
42}
43
44fn default_id_column() -> String {
45 "participant_id".into()
46}
47
48pub fn read_participants(root: &Path, spec: &ParticipantsSpec) -> Result<Participants, String> {
49 let mut joined = Participants::default();
50 for table in &spec.tables {
51 let read = read_table(&root.join(&table.path), table, &spec.entity)
52 .map_err(|e| format!("{}: {e}", table.path))?;
53 joined = joined
54 .join(read)
55 .map_err(|e| format!("{}: {e}", table.path))?;
56 }
57 Ok(joined)
58}
59
60fn read_table(path: &Path, spec: &ParticipantTable, entity: &str) -> Result<Participants, String> {
61 let text = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
62 let pattern_text = spec
63 .id_pattern
64 .clone()
65 .unwrap_or_else(|| format!("{{{entity}}}"));
66 let pattern = Pattern::parse(&pattern_text).map_err(|e| e.to_string())?;
67 if pattern.entities() != [entity] {
68 return Err(format!(
69 "id_pattern {pattern_text:?} must contain exactly {{{entity}}}"
70 ));
71 }
72 let mut lines = text.lines().filter(|l| !l.trim().is_empty());
73 let header: Vec<&str> = lines
74 .next()
75 .ok_or("empty file")?
76 .split('\t')
77 .map(str::trim)
78 .collect();
79 let id = header
80 .iter()
81 .position(|h| *h == spec.id_column)
82 .ok_or(format!("no column {:?}", spec.id_column))?;
83 let keep: Vec<usize> = match &spec.columns {
84 Some(wanted) => wanted
85 .iter()
86 .map(|w| {
87 header
88 .iter()
89 .position(|h| h == w)
90 .ok_or(format!("no column {w:?}"))
91 })
92 .collect::<Result<_, _>>()?,
93 None => (0..header.len()).filter(|&i| i != id).collect(),
94 };
95 let missing: BTreeSet<&str> = spec
96 .missing
97 .iter()
98 .map(String::as_str)
99 .chain(["", "n/a", "NA"])
100 .collect();
101 let mut rows = BTreeMap::new();
102 for line in lines {
103 let cells: Vec<&str> = line.split('\t').map(str::trim).collect();
104 let Some(subject) = cells
105 .get(id)
106 .and_then(|v| pattern.capture(v))
107 .and_then(|m| m.get(entity).cloned())
108 else {
109 continue;
110 };
111 let values: BTreeMap<String, String> = keep
112 .iter()
113 .filter_map(|&i| {
114 cells
115 .get(i)
116 .filter(|v| !missing.contains(*v))
117 .map(|v| (format!("{}{}", spec.prefix, header[i]), v.to_string()))
118 })
119 .collect();
120 if rows.insert(subject.clone(), values).is_some() {
121 return Err(format!("subject {subject:?} has more than one row"));
122 }
123 }
124 if rows.is_empty() {
125 return Err(format!(
126 "no id in column {:?} matches {pattern_text:?}",
127 spec.id_column
128 ));
129 }
130 let columns = keep
131 .iter()
132 .map(|&i| format!("{}{}", spec.prefix, header[i]))
133 .collect();
134 Participants::new(columns, rows).map_err(|e| e.to_string())
135}