use std::collections::HashMap;
use std::collections::VecDeque;
use std::io::{self, BufRead, BufReader, Read, Write};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum CountType {
Start,
End,
Core,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum OutputFormat {
Wig,
BedGraph,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CountRecord {
pub chrom: String,
pub position: i32, pub count: u32,
}
struct BedRecord {
chrom: String,
start: i32, end: i32, score: i32, }
#[derive(Clone, Debug, PartialEq)]
enum ChromState {
AwaitingFirstRecord,
InChromosome { name: String },
}
fn parse_bed_line(line: &[u8]) -> Result<Option<BedRecord>, String> {
if line.is_empty() {
return Ok(None);
}
if line[0] == b'#' {
return Ok(None);
}
let line_str = std::str::from_utf8(line).map_err(|e| format!("Invalid UTF-8: {}", e))?;
let trimmed = line_str.trim();
if trimmed.is_empty() {
return Ok(None);
}
if trimmed.starts_with("track") || trimmed.starts_with("browser") {
return Ok(None);
}
let fields: Vec<&str> = trimmed.split_whitespace().collect();
if fields.len() < 3 {
return Err(format!(
"BED line has fewer than 3 fields: '{}'",
trimmed
));
}
let chrom = fields[0].to_string();
let start: i32 = fields[1]
.parse()
.map_err(|e| format!("Cannot parse start '{}': {}", fields[1], e))?;
let end: i32 = fields[2]
.parse()
.map_err(|e| format!("Cannot parse end '{}': {}", fields[2], e))?;
let score = if fields.len() >= 5 {
fields[4]
.parse::<i32>()
.unwrap_or(1)
.max(0) } else {
1
};
Ok(Some(BedRecord {
chrom,
start,
end,
score,
}))
}
pub struct UniwigStreamProcessor {
state: ChromState,
count_buffer: VecDeque<u32>, buffer_start_pos: i32, smooth_size: i32,
step_size: i32,
count_type: CountType,
chrom_sizes: HashMap<String, u32>,
output_records: Vec<CountRecord>,
max_gap: i64, }
impl UniwigStreamProcessor {
pub fn new(
smooth_size: i32,
step_size: i32,
count_type: CountType,
chrom_sizes: HashMap<String, u32>,
) -> Self {
Self {
state: ChromState::AwaitingFirstRecord,
count_buffer: VecDeque::new(),
buffer_start_pos: 0,
smooth_size,
step_size,
count_type,
chrom_sizes,
output_records: Vec::new(),
max_gap: 0,
}
}
pub fn set_max_gap(&mut self, max_gap: i64) {
self.max_gap = max_gap;
}
pub fn process_line_bytes(&mut self, line: &[u8]) -> Result<(), String> {
match parse_bed_line(line) {
Ok(Some(record)) => {
self.process_record(record);
Ok(())
}
Ok(None) => Ok(()), Err(e) => Err(e),
}
}
fn process_record(&mut self, record: BedRecord) {
let (window_start, window_end) = match self.count_type {
CountType::Start => {
let center = record.start + 1;
let ws = (center - self.smooth_size).max(1);
let we = center + self.smooth_size;
(ws, we)
}
CountType::End => {
let center = record.end;
let ws = (center - self.smooth_size).max(1);
let we = center + self.smooth_size;
(ws, we)
}
CountType::Core => {
let ws = record.start + 1;
let we = record.end - 1;
if we < ws {
return; }
(ws, we)
}
};
match &self.state {
ChromState::AwaitingFirstRecord => {
self.state = ChromState::InChromosome {
name: record.chrom.clone(),
};
if self.max_gap < 0 {
self.buffer_start_pos = 1;
self.emit_up_to(window_start);
} else {
self.buffer_start_pos = window_start;
}
}
ChromState::InChromosome { name } => {
if *name != record.chrom {
self.finalize_current_chromosome();
self.reset_counting_state();
self.state = ChromState::InChromosome {
name: record.chrom.clone(),
};
if self.max_gap < 0 {
self.buffer_start_pos = 1;
self.emit_up_to(window_start);
} else {
self.buffer_start_pos = window_start;
}
}
}
}
self.emit_up_to(window_start);
self.ensure_buffer_covers(window_start, window_end);
if record.score > 0 {
let score = record.score as u32;
for pos in window_start..=window_end {
let idx = (pos - self.buffer_start_pos) as usize;
if idx < self.count_buffer.len() {
self.count_buffer[idx] += score;
}
}
}
}
fn emit_up_to(&mut self, up_to_pos: i32) {
let chrom_name = match &self.state {
ChromState::InChromosome { name } => name.clone(),
ChromState::AwaitingFirstRecord => return,
};
while self.buffer_start_pos < up_to_pos && !self.count_buffer.is_empty() {
let pos = self.buffer_start_pos;
let count = self.count_buffer.pop_front().unwrap();
if self.step_size <= 1 || ((pos - 1) % self.step_size == 0) {
self.output_records.push(CountRecord {
chrom: chrom_name.clone(),
position: pos,
count,
});
}
self.buffer_start_pos += 1;
}
if self.count_buffer.is_empty() && self.buffer_start_pos < up_to_pos {
let gap_width = (up_to_pos - self.buffer_start_pos) as i64;
let should_fill = self.max_gap < 0 || gap_width <= self.max_gap;
if should_fill {
while self.buffer_start_pos < up_to_pos {
if self.step_size <= 1 || ((self.buffer_start_pos - 1) % self.step_size == 0) {
self.output_records.push(CountRecord {
chrom: chrom_name.clone(),
position: self.buffer_start_pos,
count: 0,
});
}
self.buffer_start_pos += 1;
}
} else {
self.buffer_start_pos = up_to_pos;
}
}
}
fn ensure_buffer_covers(&mut self, start: i32, end: i32) {
if self.count_buffer.is_empty() {
self.buffer_start_pos = start;
}
let buffer_end_pos = self.buffer_start_pos + self.count_buffer.len() as i32 - 1;
if end > buffer_end_pos {
let needed = (end - buffer_end_pos) as usize;
self.count_buffer.extend(std::iter::repeat(0).take(needed));
}
}
fn finalize_current_chromosome(&mut self) {
let chrom_name = match &self.state {
ChromState::InChromosome { name } => name.clone(),
ChromState::AwaitingFirstRecord => return,
};
while !self.count_buffer.is_empty() {
let pos = self.buffer_start_pos;
let count = self.count_buffer.pop_front().unwrap();
if self.step_size <= 1 || ((pos - 1) % self.step_size == 0) {
self.output_records.push(CountRecord {
chrom: chrom_name.clone(),
position: pos,
count,
});
}
self.buffer_start_pos += 1;
}
if self.max_gap < 0 {
if let Some(&chrom_size) = self.chrom_sizes.get(&chrom_name) {
let end_pos = chrom_size as i32 + 1; while self.buffer_start_pos < end_pos {
if self.step_size <= 1
|| ((self.buffer_start_pos - 1) % self.step_size == 0)
{
self.output_records.push(CountRecord {
chrom: chrom_name.clone(),
position: self.buffer_start_pos,
count: 0,
});
}
self.buffer_start_pos += 1;
}
}
}
}
fn reset_counting_state(&mut self) {
self.count_buffer.clear();
self.buffer_start_pos = 0;
}
pub fn drain_output(&mut self) -> Vec<CountRecord> {
std::mem::take(&mut self.output_records)
}
pub fn finish(mut self) -> Result<Vec<CountRecord>, String> {
self.finalize_current_chromosome();
Ok(self.output_records)
}
}
pub struct WigWriter {
current_chrom: Option<String>,
last_pos: Option<i32>,
}
impl WigWriter {
pub fn new() -> Self {
Self {
current_chrom: None,
last_pos: None,
}
}
pub fn write_records<W: Write>(
&mut self,
writer: &mut W,
records: &[CountRecord],
) -> io::Result<()> {
for record in records {
let need_header = match &self.current_chrom {
None => true,
Some(chrom) => {
if chrom != &record.chrom {
true
} else {
match self.last_pos {
Some(lp) => record.position != lp + 1,
None => true,
}
}
}
};
if need_header {
writeln!(
writer,
"fixedStep chrom={} start={} step=1",
record.chrom, record.position
)?;
self.current_chrom = Some(record.chrom.clone());
}
writeln!(writer, "{}", record.count)?;
self.last_pos = Some(record.position);
}
Ok(())
}
}
pub fn write_records_as_wig<W: Write>(
writer: &mut W,
records: &[CountRecord],
) -> io::Result<()> {
WigWriter::new().write_records(writer, records)
}
pub fn write_records_as_bedgraph<W: Write>(
writer: &mut W,
records: &[CountRecord],
) -> io::Result<()> {
for record in records {
writeln!(
writer,
"{}\t{}\t{}\t{}",
record.chrom,
record.position - 1,
record.position,
record.count
)?;
}
Ok(())
}
pub fn read_chrom_sizes<R: BufRead>(reader: R) -> Result<HashMap<String, u32>, String> {
let mut sizes = HashMap::new();
for line in reader.lines() {
let line = line.map_err(|e| format!("IO error reading chrom.sizes: {}", e))?;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let fields: Vec<&str> = trimmed.split('\t').collect();
if fields.len() < 2 {
return Err(format!(
"chrom.sizes line has fewer than 2 fields: '{}'",
trimmed
));
}
let name = fields[0].to_string();
let size: u32 = fields[1]
.parse()
.map_err(|e| format!("Cannot parse size '{}': {}", fields[1], e))?;
sizes.insert(name, size);
}
Ok(sizes)
}
fn write_output<W: Write>(
wig_writer: &mut WigWriter,
output: &mut W,
records: &[CountRecord],
format: OutputFormat,
) -> io::Result<()> {
match format {
OutputFormat::Wig => wig_writer.write_records(output, records),
OutputFormat::BedGraph => write_records_as_bedgraph(output, records),
}
}
fn process_lines<R: BufRead, W: Write>(
mut reader: R,
processor: &mut UniwigStreamProcessor,
wig_writer: &mut WigWriter,
output: &mut W,
output_format: OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let mut line_buf = String::with_capacity(256);
loop {
line_buf.clear();
let bytes_read = reader.read_line(&mut line_buf)?;
if bytes_read == 0 {
break;
}
let trimmed = line_buf.trim_end_matches(|c| c == '\n' || c == '\r');
processor.process_line_bytes(trimmed.as_bytes())?;
let records = processor.drain_output();
if !records.is_empty() {
write_output(wig_writer, output, &records, output_format)?;
}
}
Ok(())
}
pub fn uniwig_streaming<R: Read, W: Write>(
input: R,
output: &mut W,
chrom_sizes: HashMap<String, u32>,
smooth_size: i32,
step_size: i32,
count_type: CountType,
output_format: OutputFormat,
max_gap: i64,
) -> Result<(), Box<dyn std::error::Error>> {
let mut processor = UniwigStreamProcessor::new(
smooth_size, step_size, count_type, chrom_sizes,
);
processor.set_max_gap(max_gap);
let mut buf_output = io::BufWriter::with_capacity(65536, output);
let mut wig_writer = WigWriter::new();
let mut buf_input = BufReader::with_capacity(65536, input);
let is_gzipped = {
let peek = buf_input.fill_buf()?;
peek.len() >= 2 && peek[0] == 0x1f && peek[1] == 0x8b
};
if is_gzipped {
let gz = flate2::bufread::GzDecoder::new(buf_input);
let gz_reader = BufReader::with_capacity(65536, gz);
process_lines(gz_reader, &mut processor, &mut wig_writer,
&mut buf_output, output_format)?;
} else {
process_lines(buf_input, &mut processor, &mut wig_writer,
&mut buf_output, output_format)?;
}
let records = processor.finish()?;
if !records.is_empty() {
write_output(&mut wig_writer, &mut buf_output, &records, output_format)?;
}
buf_output.flush()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn run_processor(
bed_data: &[u8],
smooth_size: i32,
step_size: i32,
count_type: CountType,
chrom_sizes: HashMap<String, u32>,
) -> Vec<CountRecord> {
let mut processor =
UniwigStreamProcessor::new(smooth_size, step_size, count_type, chrom_sizes);
for line in bed_data.split(|&b| b == b'\n') {
if !line.is_empty() {
processor.process_line_bytes(line).expect("process_line_bytes failed");
}
}
let mut all_records = processor.drain_output();
let final_records = processor.finish().expect("finish failed");
all_records.extend(final_records);
all_records
}
#[test]
fn test_single_read_smooth0() {
let bed = b"chr1\t100\t200\n";
let records = run_processor(bed, 0, 1, CountType::Start, HashMap::new());
let non_zero: Vec<_> = records.iter().filter(|r| r.count > 0).collect();
assert_eq!(non_zero.len(), 1);
assert_eq!(non_zero[0].position, 101);
assert_eq!(non_zero[0].count, 1);
}
#[test]
fn test_single_read_smooth5() {
let bed = b"chr1\t10\t200\n";
let records = run_processor(bed, 5, 1, CountType::Start, HashMap::new());
let non_zero: Vec<_> = records.iter().filter(|r| r.count > 0).collect();
assert_eq!(non_zero.len(), 11); assert_eq!(non_zero[0].position, 6);
assert_eq!(non_zero[10].position, 16);
for r in &non_zero {
assert_eq!(r.count, 1);
}
}
#[test]
fn test_overlapping_reads() {
let bed = b"chr1\t10\t20\nchr1\t12\t25\n";
let records = run_processor(bed, 2, 1, CountType::Start, HashMap::new());
let r11 = records.iter().find(|r| r.position == 11).unwrap();
assert_eq!(r11.count, 2); let r13 = records.iter().find(|r| r.position == 13).unwrap();
assert_eq!(r13.count, 2);
let r9 = records.iter().find(|r| r.position == 9).unwrap();
assert_eq!(r9.count, 1); let r15 = records.iter().find(|r| r.position == 15).unwrap();
assert_eq!(r15.count, 1); }
#[test]
fn test_chromosome_transition() {
let bed = b"chr1\t10\t20\nchr2\t10\t20\n";
let records = run_processor(bed, 0, 1, CountType::Start, HashMap::new());
let chr1: Vec<_> = records.iter().filter(|r| r.chrom == "chr1").collect();
let chr2: Vec<_> = records.iter().filter(|r| r.chrom == "chr2").collect();
assert_eq!(chr1.len(), 1);
assert_eq!(chr2.len(), 1);
assert_eq!(chr1[0].position, 11);
assert_eq!(chr2[0].position, 11);
let chr1_idx = records
.iter()
.position(|r| r.chrom == "chr1")
.unwrap();
let chr2_idx = records
.iter()
.position(|r| r.chrom == "chr2")
.unwrap();
assert!(chr1_idx < chr2_idx);
}
#[test]
fn test_step_size() {
let bed = b"chr1\t0\t10\n";
let records = run_processor(bed, 3, 2, CountType::Start, HashMap::new());
for r in &records {
assert!((r.position - 1) % 2 == 0, "Position {} not on step boundary", r.position);
}
}
#[test]
fn test_clamp_to_position_1() {
let bed = b"chr1\t0\t10\n";
let records = run_processor(bed, 5, 1, CountType::Start, HashMap::new());
assert_eq!(records[0].position, 1);
assert!(records.iter().all(|r| r.position >= 1));
}
#[test]
fn test_core_count_type() {
let bed = b"chr1\t10\t15\n";
let records = run_processor(bed, 0, 1, CountType::Core, HashMap::new());
let non_zero: Vec<_> = records.iter().filter(|r| r.count > 0).collect();
assert_eq!(non_zero.len(), 4); assert_eq!(non_zero[0].position, 11);
assert_eq!(non_zero[3].position, 14);
}
#[test]
fn test_end_count_type() {
let bed = b"chr1\t10\t15\n";
let records = run_processor(bed, 2, 1, CountType::End, HashMap::new());
let non_zero: Vec<_> = records.iter().filter(|r| r.count > 0).collect();
assert_eq!(non_zero.len(), 5);
assert_eq!(non_zero[0].position, 13);
assert_eq!(non_zero[4].position, 17);
}
#[test]
fn test_score_support() {
let bed = b"chr1\t10\t20\tpeak1\t3\n";
let records = run_processor(bed, 0, 1, CountType::Start, HashMap::new());
let r = records.iter().find(|r| r.count > 0).unwrap();
assert_eq!(r.count, 3);
}
#[test]
fn test_processor_matches_pipeline_output() {
let bed = b"chr1\t10\t20\nchr1\t15\t25\n";
let records = run_processor(bed, 1, 1, CountType::Start, HashMap::new());
let mut output = Vec::new();
uniwig_streaming(
Cursor::new(bed), &mut output, HashMap::new(),
1, 1, CountType::Start, OutputFormat::Wig, 0i64,
).unwrap();
let output_str = String::from_utf8(output).unwrap();
let mut parsed_records: Vec<(i32, u32)> = Vec::new();
let mut current_pos: i32 = 0;
for line in output_str.lines() {
if line.starts_with("fixedStep") {
let start_str = line.split("start=").nth(1).unwrap()
.split_whitespace().next().unwrap();
current_pos = start_str.parse().unwrap();
} else {
let count: u32 = line.parse().unwrap();
parsed_records.push((current_pos, count));
current_pos += 1;
}
}
assert_eq!(records.len(), parsed_records.len(),
"Processor produced {} records but pipeline WIG has {}",
records.len(), parsed_records.len());
for (rec, (pos, count)) in records.iter().zip(parsed_records.iter()) {
assert_eq!(rec.position, *pos, "Position mismatch");
assert_eq!(rec.count, *count, "Count mismatch at position {}", pos);
}
}
#[test]
fn test_empty_input() {
let records = run_processor(b"", 1, 1, CountType::Start, HashMap::new());
assert!(records.is_empty());
}
#[test]
fn test_comments_and_headers() {
let bed = b"# comment line\ntrack name=test\nbrowser position chr1:1-100\nchr1\t10\t20\n";
let records = run_processor(bed, 0, 1, CountType::Start, HashMap::new());
let non_zero: Vec<_> = records.iter().filter(|r| r.count > 0).collect();
assert_eq!(non_zero.len(), 1);
assert_eq!(non_zero[0].position, 11);
}
#[test]
fn test_gzip_input() {
use flate2::write::GzEncoder;
use flate2::Compression;
let bed = b"chr1\t10\t20\nchr1\t15\t25\n";
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(bed).unwrap();
let compressed = encoder.finish().unwrap();
let mut plain_output = Vec::new();
uniwig_streaming(
Cursor::new(bed), &mut plain_output, HashMap::new(),
1, 1, CountType::Start, OutputFormat::Wig, 0i64,
).unwrap();
let mut gz_output = Vec::new();
uniwig_streaming(
Cursor::new(&compressed), &mut gz_output, HashMap::new(),
1, 1, CountType::Start, OutputFormat::Wig, 0i64,
).unwrap();
assert_eq!(plain_output, gz_output);
}
#[test]
fn test_reference_start_wig() {
let bed = b"chr1\t2\t6\nchr1\t4\t7\nchr1\t5\t9\nchr1\t7\t12\n";
let mut chrom_sizes = HashMap::new();
chrom_sizes.insert("chr1".to_string(), 20);
let records = run_processor(bed, 1, 1, CountType::Start, chrom_sizes);
let expected_counts: Vec<u32> = vec![1, 1, 2, 2, 2, 2, 1, 1];
assert_eq!(records.len(), 8, "Expected 8 records (positions 2-9)");
assert_eq!(records[0].position, 2);
for (i, expected) in expected_counts.iter().enumerate() {
assert_eq!(
records[i].count, *expected,
"Mismatch at position {}: expected {}, got {}",
records[i].position, expected, records[i].count
);
}
}
#[test]
fn test_reference_end_wig() {
let bed = b"chr1\t2\t6\nchr1\t4\t7\nchr1\t5\t9\nchr1\t7\t12\n";
let mut chrom_sizes = HashMap::new();
chrom_sizes.insert("chr1".to_string(), 20);
let records = run_processor(bed, 1, 1, CountType::End, chrom_sizes);
let expected_counts: Vec<u32> = vec![1, 2, 2, 2, 1, 1, 1, 1, 1];
assert_eq!(records[0].position, 5);
assert_eq!(records.len(), 9, "Expected 9 records (positions 5-13)");
for (i, expected) in expected_counts.iter().enumerate() {
assert_eq!(
records[i].count, *expected,
"End wig mismatch at position {}: expected {}, got {}",
records[i].position, expected, records[i].count
);
}
}
#[test]
fn test_reference_core_wig() {
let bed = b"chr1\t2\t6\nchr1\t4\t7\nchr1\t5\t9\nchr1\t7\t12\n";
let mut chrom_sizes = HashMap::new();
chrom_sizes.insert("chr1".to_string(), 20);
let records = run_processor(bed, 0, 1, CountType::Core, chrom_sizes);
let expected_counts: Vec<u32> = vec![1, 1, 2, 2, 1, 2, 1, 1, 1];
assert_eq!(records[0].position, 3);
assert_eq!(records.len(), 9, "Expected 9 records (positions 3-11)");
for (i, expected) in expected_counts.iter().enumerate() {
assert_eq!(
records[i].count, *expected,
"Core wig mismatch at position {}: expected {}, got {}",
records[i].position, expected, records[i].count
);
}
}
#[test]
fn test_wig_output_formatting() {
let records = vec![
CountRecord { chrom: "chr1".to_string(), position: 5, count: 3 },
CountRecord { chrom: "chr1".to_string(), position: 6, count: 2 },
CountRecord { chrom: "chr1".to_string(), position: 7, count: 1 },
CountRecord { chrom: "chr2".to_string(), position: 10, count: 5 },
];
let mut output = Vec::new();
write_records_as_wig(&mut output, &records).unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("fixedStep chrom=chr1 start=5 step=1"));
assert!(output_str.contains("fixedStep chrom=chr2 start=10 step=1"));
let lines: Vec<&str> = output_str.lines().collect();
assert_eq!(lines.len(), 6);
assert_eq!(lines[1], "3");
assert_eq!(lines[2], "2");
assert_eq!(lines[3], "1");
assert_eq!(lines[5], "5");
}
#[test]
fn test_multi_chrom_bed() {
let bed = b"chr11 \t10\t50\nchr11\t20\t76\nchr12\t769\t2395\nchr13\t771\t3000\nchr14\t800\t2900\nchr21\t1\t30\nchr21\t2\t19\nchr21\t16\t31\n";
let records = run_processor(bed, 0, 1, CountType::Start, HashMap::new());
let chroms: Vec<String> = records
.iter()
.map(|r| r.chrom.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
assert!(chroms.contains(&"chr11".to_string()));
assert!(chroms.contains(&"chr12".to_string()));
assert!(chroms.contains(&"chr13".to_string()));
assert!(chroms.contains(&"chr14".to_string()));
assert!(chroms.contains(&"chr21".to_string()));
assert_eq!(chroms.len(), 5);
}
#[test]
fn test_read_chrom_sizes() {
let data = b"chr1\t248956422\nchr2\t242193529\n";
let sizes = read_chrom_sizes(Cursor::new(data)).unwrap();
assert_eq!(sizes.get("chr1"), Some(&248956422));
assert_eq!(sizes.get("chr2"), Some(&242193529));
}
fn run_processor_with_gap(
bed_data: &[u8],
smooth_size: i32,
step_size: i32,
count_type: CountType,
chrom_sizes: HashMap<String, u32>,
max_gap: i64,
) -> Vec<CountRecord> {
let mut processor =
UniwigStreamProcessor::new(smooth_size, step_size, count_type, chrom_sizes);
processor.set_max_gap(max_gap);
for line in bed_data.split(|&b| b == b'\n') {
if !line.is_empty() {
processor.process_line_bytes(line).expect("process_line_bytes failed");
}
}
let mut all_records = processor.drain_output();
let final_records = processor.finish().expect("finish failed");
all_records.extend(final_records);
all_records
}
#[test]
fn test_max_gap_zero_skips_all_gaps() {
let bed = b"chr1\t10\t20\nchr1\t30\t40\n";
let records = run_processor_with_gap(bed, 0, 1, CountType::Start, HashMap::new(), 0);
let positions: Vec<i32> = records.iter().map(|r| r.position).collect();
assert!(positions.contains(&11), "Should have pos 11");
assert!(positions.contains(&31), "Should have pos 31");
for pos in 12..31 {
assert!(!positions.contains(&pos), "Should NOT have pos {} with max_gap=0", pos);
}
}
#[test]
fn test_max_gap_fills_small_gaps() {
let bed = b"chr1\t10\t20\nchr1\t17\t25\n";
let records_filled = run_processor_with_gap(bed, 0, 1, CountType::Start, HashMap::new(), 10);
let positions_filled: Vec<i32> = records_filled.iter().map(|r| r.position).collect();
for pos in 12..18 {
assert!(
positions_filled.contains(&pos),
"With max_gap=10, pos {} should be filled with zeros",
pos
);
}
let records_skipped = run_processor_with_gap(bed, 0, 1, CountType::Start, HashMap::new(), 3);
let positions_skipped: Vec<i32> = records_skipped.iter().map(|r| r.position).collect();
assert!(positions_skipped.contains(&11), "Should have pos 11");
assert!(positions_skipped.contains(&18), "Should have pos 18");
for pos in 12..18 {
assert!(
!positions_skipped.contains(&pos),
"With max_gap=3, pos {} should be skipped",
pos
);
}
}
#[test]
fn test_max_gap_negative_one_fully_dense() {
let bed = b"chr1\t5\t10\n";
let mut chrom_sizes = HashMap::new();
chrom_sizes.insert("chr1".to_string(), 20u32);
let records = run_processor_with_gap(bed, 0, 1, CountType::Start, chrom_sizes, -1);
let positions: Vec<i32> = records.iter().map(|r| r.position).collect();
assert!(positions.contains(&1), "Fully dense should start at position 1");
assert!(positions.contains(&20), "Fully dense should end at chrom_size=20");
for pos in 1i32..=20 {
assert!(positions.contains(&pos), "Fully dense missing position {}", pos);
}
assert_eq!(records.len(), 20, "Should have exactly 20 records");
}
#[test]
fn test_max_gap_large_value_fills_all_data_gaps() {
let bed = b"chr1\t10\t20\nchr1\t30\t40\n";
let mut chrom_sizes = HashMap::new();
chrom_sizes.insert("chr1".to_string(), 100u32);
let records = run_processor_with_gap(bed, 0, 1, CountType::Start, chrom_sizes, i64::MAX);
let positions: Vec<i32> = records.iter().map(|r| r.position).collect();
for pos in 1..11 {
assert!(!positions.contains(&pos), "Should NOT have leading zero at pos {}", pos);
}
for pos in 12..31 {
assert!(
positions.contains(&pos),
"With max_gap=i64::MAX, pos {} should be filled",
pos
);
}
for pos in 32..=100 {
assert!(!positions.contains(&pos), "Should NOT have trailing zero at pos {}", pos);
}
}
#[test]
fn test_max_gap_exact_boundary() {
let bed = b"chr1\t10\t20\nchr1\t16\t25\n";
let records_filled = run_processor_with_gap(bed, 0, 1, CountType::Start, HashMap::new(), 5);
let positions_filled: Vec<i32> = records_filled.iter().map(|r| r.position).collect();
for pos in 12..17 {
assert!(
positions_filled.contains(&pos),
"With max_gap=5, pos {} (gap=5) should be filled",
pos
);
}
let bed2 = b"chr1\t10\t20\nchr1\t17\t25\n";
let records_skipped = run_processor_with_gap(bed2, 0, 1, CountType::Start, HashMap::new(), 5);
let positions_skipped: Vec<i32> = records_skipped.iter().map(|r| r.position).collect();
assert!(positions_skipped.contains(&11), "Should have data pos 11");
assert!(positions_skipped.contains(&18), "Should have data pos 18");
for pos in 12..18 {
assert!(
!positions_skipped.contains(&pos),
"With max_gap=5, pos {} (gap=6) should be skipped",
pos
);
}
}
}