1use std::io::{BufRead, BufReader, Read, Write};
2
3use crate::error::IoError;
4
5#[derive(Debug, Clone)]
20pub struct Hat2Matrix {
21 pub names: Vec<String>,
23 pub distances: Vec<Vec<f64>>,
27}
28
29impl Hat2Matrix {
30 pub fn nseq(&self) -> usize {
31 self.names.len()
32 }
33
34 pub fn get(&self, i: usize, j: usize) -> f64 {
36 if i < j {
37 self.distances[i][j - i - 1]
38 } else if i > j {
39 self.distances[j][i - j - 1]
40 } else {
41 0.0
42 }
43 }
44}
45
46const VALS_PER_LINE: usize = 12;
48
49pub fn read_hat2<R: Read>(reader: R) -> Result<Hat2Matrix, IoError> {
51 let reader = BufReader::new(reader);
52 let mut lines = reader.lines();
53
54 lines.next().ok_or(IoError::Hat2Format("empty file".into()))??;
56
57 let nseq_line = lines
59 .next()
60 .ok_or(IoError::Hat2Format("missing nseq line".into()))??;
61 let nseq: usize = nseq_line
62 .trim()
63 .parse()
64 .map_err(|_| IoError::Hat2Format(format!("invalid nseq: '{nseq_line}'")))?;
65
66 lines.next().ok_or(IoError::Hat2Format("missing max line".into()))??;
68
69 let mut names = Vec::with_capacity(nseq);
71 for _ in 0..nseq {
72 let line = lines
73 .next()
74 .ok_or(IoError::Hat2Format("truncated name section".into()))??;
75 let raw = if let Some(pos) = line.find(". ") {
80 &line[pos + 2..]
81 } else {
82 line.trim_start()
83 };
84 let name = raw.strip_prefix('=').unwrap_or(raw).to_string();
85 names.push(name);
86 }
87
88 let mut all_values: Vec<f64> = Vec::new();
91 for line_result in lines {
92 let line = line_result?;
93 if !line.trim().is_empty() {
95 for token in line.split_whitespace() {
96 let val: f64 = token
97 .parse()
98 .map_err(|_| IoError::Hat2Format(format!("invalid float: '{token}'")))?;
99 all_values.push(val);
100 }
101 }
102 }
103
104 let expected = nseq * (nseq - 1) / 2;
106 if all_values.len() != expected {
107 return Err(IoError::Hat2Format(format!(
108 "expected {} distance values, got {}",
109 expected,
110 all_values.len()
111 )));
112 }
113
114 let mut distances = Vec::with_capacity(nseq);
115 let mut offset = 0;
116 for i in 0..nseq {
117 let row_len = nseq - i - 1;
118 distances.push(all_values[offset..offset + row_len].to_vec());
119 offset += row_len;
120 }
121
122 Ok(Hat2Matrix { names, distances })
123}
124
125pub fn write_hat2<W: Write>(mat: &Hat2Matrix, writer: &mut W) -> Result<(), IoError> {
127 let nseq = mat.nseq();
128
129 let max_dist = mat
131 .distances
132 .iter()
133 .flat_map(|row| row.iter())
134 .cloned()
135 .fold(0.0_f64, f64::max);
136
137 writeln!(writer, " 1")?;
145 writeln!(writer, "{nseq:5}")?;
146 writeln!(writer, " {:>6.3}", max_dist * 2.5)?;
147
148 for (i, name) in mat.names.iter().enumerate() {
155 writeln!(writer, "{:>4}. ={name}", i + 1)?;
156 }
157
158 for i in 0..nseq.saturating_sub(1) {
160 let row = &mat.distances[i];
161 for (col, val) in row.iter().enumerate() {
162 write!(writer, "{val:6.3}")?;
163 if (col + 1) % VALS_PER_LINE == 0 || col == row.len() - 1 {
164 writeln!(writer)?;
165 }
166 }
167 }
168
169 Ok(())
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use std::io::Cursor;
176
177 fn sample_matrix() -> Hat2Matrix {
178 Hat2Matrix {
179 names: vec!["seq1".into(), "seq2".into(), "seq3".into()],
180 distances: vec![
181 vec![0.123, 0.456], vec![0.789], ],
184 }
185 }
186
187 #[test]
188 fn roundtrip_hat2() {
189 let mat = sample_matrix();
190
191 let mut buf = Vec::new();
192 write_hat2(&mat, &mut buf).unwrap();
193
194 let parsed = read_hat2(Cursor::new(&buf)).unwrap();
195 assert_eq!(parsed.nseq(), 3);
196 assert_eq!(parsed.names, mat.names);
197 assert!((parsed.get(0, 1) - 0.123).abs() < 0.001);
198 assert!((parsed.get(0, 2) - 0.456).abs() < 0.001);
199 assert!((parsed.get(1, 2) - 0.789).abs() < 0.001);
200 assert!((parsed.get(2, 0) - 0.456).abs() < 0.001);
202 }
203
204 #[test]
205 fn self_distance_is_zero() {
206 let mat = sample_matrix();
207 assert_eq!(mat.get(0, 0), 0.0);
208 assert_eq!(mat.get(1, 1), 0.0);
209 }
210}