rsomics_quantile_transform/
io.rs1use std::io::Read;
4use std::path::Path;
5
6use rsomics_common::{Result, RsomicsError};
7
8pub struct Matrix {
10 pub n_rows: usize,
11 pub n_cols: usize,
12 pub row_names: Vec<String>,
13 pub col_names: Vec<String>,
14 pub data: Vec<f64>,
15}
16
17impl Matrix {
18 #[must_use]
19 pub fn row(&self, i: usize) -> &[f64] {
20 &self.data[i * self.n_cols..(i + 1) * self.n_cols]
21 }
22
23 #[must_use]
25 pub fn column(&self, j: usize) -> Vec<f64> {
26 (0..self.n_rows)
27 .map(|i| self.data[i * self.n_cols + j])
28 .collect()
29 }
30
31 pub fn read(path: Option<&Path>) -> Result<Matrix> {
36 let mut buf = String::new();
37 match path {
38 Some(p) if p.as_os_str() != "-" => {
39 std::fs::File::open(p)
40 .map_err(|e| RsomicsError::InvalidInput(format!("{}: {e}", p.display())))?
41 .read_to_string(&mut buf)
42 .map_err(RsomicsError::Io)?;
43 }
44 _ => {
45 std::io::stdin()
46 .lock()
47 .read_to_string(&mut buf)
48 .map_err(RsomicsError::Io)?;
49 }
50 }
51 Matrix::parse(&buf)
52 }
53
54 pub fn parse(text: &str) -> Result<Matrix> {
59 let rows: Vec<&str> = text
60 .lines()
61 .filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
62 .collect();
63 if rows.is_empty() {
64 return Err(RsomicsError::InvalidInput("empty matrix".into()));
65 }
66
67 let has_header = rows[0].starts_with('\t');
68 let (col_names, body) = if has_header {
69 let names: Vec<String> = rows[0].split('\t').skip(1).map(str::to_string).collect();
70 (names, &rows[1..])
71 } else {
72 let ncol = rows[0].split('\t').count();
73 ((1..=ncol).map(|j| j.to_string()).collect(), &rows[..])
74 };
75
76 let n_cols = col_names.len();
77 if n_cols == 0 {
78 return Err(RsomicsError::InvalidInput("matrix has no columns".into()));
79 }
80
81 let mut row_names = Vec::with_capacity(body.len());
82 let mut data = Vec::with_capacity(body.len() * n_cols);
83 for (li, line) in body.iter().enumerate() {
84 let mut fields = line.split('\t');
85 if has_header {
86 row_names.push(fields.next().unwrap_or("").to_string());
87 } else {
88 row_names.push((li + 1).to_string());
89 }
90 let mut seen = 0;
91 for cell in fields {
92 data.push(parse_cell(cell)?);
93 seen += 1;
94 }
95 if seen != n_cols {
96 return Err(RsomicsError::InvalidInput(format!(
97 "row {} has {seen} value columns, expected {n_cols}",
98 li + 1
99 )));
100 }
101 }
102
103 let n_rows = row_names.len();
104 if n_rows == 0 {
105 return Err(RsomicsError::InvalidInput("matrix has no data rows".into()));
106 }
107
108 Ok(Matrix {
109 n_rows,
110 n_cols,
111 row_names,
112 col_names,
113 data,
114 })
115 }
116}
117
118fn parse_cell(cell: &str) -> Result<f64> {
119 let t = cell.trim();
120 match t {
121 "" | "NA" | "NaN" | "na" | "nan" => Ok(f64::NAN),
122 "Inf" | "inf" | "+Inf" => Ok(f64::INFINITY),
123 "-Inf" | "-inf" => Ok(f64::NEG_INFINITY),
124 _ => fast_float2::parse(t)
125 .map_err(|_| RsomicsError::InvalidInput(format!("non-numeric cell '{cell}'"))),
126 }
127}
128
129#[must_use]
131pub fn fmt_value(v: f64) -> String {
132 if v.is_nan() {
133 "NA".to_string()
134 } else if v.is_infinite() {
135 if v > 0.0 { "Inf".into() } else { "-Inf".into() }
136 } else if v != 0.0 && (v.abs() < 1e-4 || v.abs() >= 1e16) {
137 format!("{v:e}")
138 } else {
139 format!("{v}")
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn parse_with_header() {
149 let m = Matrix::parse("\tc1\tc2\tc3\nr1\t1\t2\t3\nr2\t4\t5\t6\n").unwrap();
150 assert_eq!(m.n_rows, 2);
151 assert_eq!(m.n_cols, 3);
152 assert_eq!(m.row_names, ["r1", "r2"]);
153 assert_eq!(m.col_names, ["c1", "c2", "c3"]);
154 assert_eq!(m.row(1), &[4.0, 5.0, 6.0]);
155 assert_eq!(m.column(0), [1.0, 4.0]);
156 }
157
158 #[test]
159 fn parse_headerless() {
160 let m = Matrix::parse("1\t2\n3\t4\n").unwrap();
161 assert_eq!(m.n_rows, 2);
162 assert_eq!(m.n_cols, 2);
163 assert_eq!(m.row(0), &[1.0, 2.0]);
164 assert_eq!(m.col_names, ["1", "2"]);
165 }
166
167 #[test]
168 fn ragged_row_errors() {
169 assert!(Matrix::parse("1\t2\t3\n4\t5\n").is_err());
170 }
171}