pub mod binary;
pub mod error;
pub mod header;
pub mod matrix;
pub mod mer;
pub mod query;
pub mod string_mers;
pub mod text;
pub use binary::BinaryReader;
pub use error::{Error, Result};
pub use header::FileHeader;
pub use matrix::RectangularBinaryMatrix;
pub use mer::MerDna;
pub use query::QueryMerFile;
pub use string_mers::{StringMers, string_canonicals, string_mers};
pub use text::TextReader;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
pub enum ReadMerFile {
Binary(BinaryReader<BufReader<File>>),
Text(TextReader<BufReader<File>>),
}
impl ReadMerFile {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let file = File::open(path.as_ref())?;
let mut reader = BufReader::new(file);
let header = FileHeader::read(&mut reader)?;
match header.format() {
header::FORMAT_BINARY_SORTED => {
let binary = BinaryReader::new(reader, &header)?;
Ok(ReadMerFile::Binary(binary))
}
header::FORMAT_TEXT_SORTED => {
let text = TextReader::new(reader);
Ok(ReadMerFile::Text(text))
}
other => Err(Error::UnsupportedFormat(other.to_string())),
}
}
}
impl Iterator for ReadMerFile {
type Item = Result<(MerDna, u64)>;
fn next(&mut self) -> Option<Self::Item> {
match self {
ReadMerFile::Binary(r) => r.next(),
ReadMerFile::Text(r) => r.next(),
}
}
}
#[cfg(test)]
mod integration_tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn create_binary_jf(k: usize, val_len: usize, records: &[(&str, u64)]) -> NamedTempFile {
let key_len_bits = k * 2;
let key_len_bytes = (key_len_bits + 7) / 8;
let mut sorted_records: Vec<(MerDna, u64)> = records
.iter()
.map(|(s, c)| (s.parse::<MerDna>().unwrap(), *c))
.collect();
sorted_records.sort_by(|a, b| a.0.cmp(&b.0));
let size = sorted_records.len().next_power_of_two().max(2);
let json = serde_json::json!({
"format": "binary/sorted",
"key_len": key_len_bits,
"val_len": val_len,
"counter_len": val_len,
"size": size,
"canonical": false
});
let json_str = serde_json::to_string(&json).unwrap();
let mut file = NamedTempFile::new().unwrap();
write!(file, "{:09}", json_str.len()).unwrap();
file.write_all(json_str.as_bytes()).unwrap();
for (mer, count) in &sorted_records {
let words = mer.words();
let mut bytes_written = 0;
for &word in words {
for byte_idx in 0..8 {
if bytes_written >= key_len_bytes {
break;
}
file.write_all(&[(word >> (byte_idx * 8)) as u8]).unwrap();
bytes_written += 1;
}
}
for i in 0..val_len {
file.write_all(&[(count >> (i * 8)) as u8]).unwrap();
}
}
file.flush().unwrap();
file
}
fn create_text_jf(k: usize, records: &[(&str, u64)]) -> NamedTempFile {
let json = serde_json::json!({
"format": "text/sorted",
"key_len": k * 2,
"counter_len": 8
});
let json_str = serde_json::to_string(&json).unwrap();
let mut file = NamedTempFile::new().unwrap();
write!(file, "{:09}", json_str.len()).unwrap();
file.write_all(json_str.as_bytes()).unwrap();
for (seq, count) in records {
writeln!(file, "{seq} {count}").unwrap();
}
file.flush().unwrap();
file
}
#[test]
fn test_read_binary_auto_detect() {
let file = create_binary_jf(4, 4, &[("ACGT", 42), ("AAAA", 10)]);
let reader = ReadMerFile::open(file.path()).unwrap();
let results: Vec<_> = reader.map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 2);
let strs: Vec<String> = results.iter().map(|(m, _)| m.to_string()).collect();
let counts: Vec<u64> = results.iter().map(|(_, c)| *c).collect();
assert!(strs.contains(&"ACGT".to_string()));
assert!(strs.contains(&"AAAA".to_string()));
let acgt_idx = strs.iter().position(|s| s == "ACGT").unwrap();
let aaaa_idx = strs.iter().position(|s| s == "AAAA").unwrap();
assert_eq!(counts[acgt_idx], 42);
assert_eq!(counts[aaaa_idx], 10);
}
#[test]
fn test_read_text_auto_detect() {
let file = create_text_jf(4, &[("ACGT", 42), ("AAAA", 10)]);
let reader = ReadMerFile::open(file.path()).unwrap();
let results: Vec<_> = reader.map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 2);
assert_eq!(results[0].0.to_string(), "ACGT");
assert_eq!(results[0].1, 42);
assert_eq!(results[1].0.to_string(), "AAAA");
assert_eq!(results[1].1, 10);
}
#[test]
fn test_binary_and_query_consistent() {
let records = [
("AAAA", 10u64),
("ACGT", 42),
("CCCC", 7),
("GGGG", 33),
("TTTT", 100),
];
let file = create_binary_jf(4, 4, &records);
let reader = ReadMerFile::open(file.path()).unwrap();
let seq_results: Vec<_> = reader.map(|r| r.unwrap()).collect();
let qf = QueryMerFile::open(file.path()).unwrap();
for (mer, count) in &seq_results {
let query_count = qf.get(mer);
assert_eq!(
query_count,
Some(*count),
"mismatch for {}: sequential={}, query={:?}",
mer,
count,
query_count
);
}
}
#[test]
fn test_roundtrip_various_k() {
for k in [1, 2, 4, 8, 16, 25, 31, 32, 33] {
let seq: String = "ACGT".chars().cycle().take(k).collect();
let file = create_binary_jf(k, 4, &[(&seq, 1)]);
let reader = ReadMerFile::open(file.path()).unwrap();
let results: Vec<_> = reader.map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 1, "k={k}");
assert_eq!(results[0].0.to_string(), seq, "k={k}");
assert_eq!(results[0].1, 1, "k={k}");
}
}
#[test]
fn test_empty_binary_file() {
let file = create_binary_jf(4, 4, &[]);
let reader = ReadMerFile::open(file.path()).unwrap();
let results: Vec<_> = reader.collect();
assert_eq!(results.len(), 0);
}
#[test]
fn test_empty_text_file() {
let file = create_text_jf(4, &[]);
let reader = ReadMerFile::open(file.path()).unwrap();
let results: Vec<_> = reader.collect();
assert_eq!(results.len(), 0);
}
#[test]
fn test_large_counts() {
let file = create_binary_jf(4, 8, &[("ACGT", u64::MAX)]);
let reader = ReadMerFile::open(file.path()).unwrap();
let results: Vec<_> = reader.map(|r| r.unwrap()).collect();
assert_eq!(results[0].1, u64::MAX);
}
#[test]
fn test_many_records() {
let bases = ['A', 'C', 'G', 'T'];
let mut records = Vec::new();
for &a in &bases {
for &b in &bases {
for &c in &bases {
records.push(format!("{a}{b}{c}"));
}
}
}
let records_with_counts: Vec<(&str, u64)> = records
.iter()
.enumerate()
.map(|(i, s)| (s.as_str(), (i + 1) as u64))
.collect();
let file = create_binary_jf(3, 4, &records_with_counts);
let reader = ReadMerFile::open(file.path()).unwrap();
let results: Vec<_> = reader.map(|r| r.unwrap()).collect();
assert_eq!(results.len(), 64);
let qf = QueryMerFile::open(file.path()).unwrap();
for (seq, expected_count) in &records_with_counts {
let mer: MerDna = seq.parse().unwrap();
let count = qf.get(&mer);
assert_eq!(count, Some(*expected_count), "query mismatch for {seq}");
}
}
}