use crate::error::VarEffectError;
use crate::fasta::FastaReader;
pub(crate) fn left_align_indel(
fasta: &FastaReader,
chrom: &str,
pos_1based: u64,
ref_allele: &str,
alt_allele: &str,
) -> Result<Option<(u64, String, String)>, VarEffectError> {
let orig_pos = pos_1based;
let orig_ref = ref_allele;
let orig_alt = alt_allele;
let mut pos = pos_1based;
let mut r: Vec<u8> = ref_allele.as_bytes().to_vec();
let mut a: Vec<u8> = alt_allele.as_bytes().to_vec();
loop {
if pos <= 1 || r.is_empty() || a.is_empty() {
break;
}
let trimmed = if r.last() == a.last() {
r.pop();
a.pop();
true
} else {
false
};
if r.is_empty() || a.is_empty() {
pos -= 1;
let prepend = fasta.fetch_base(chrom, pos - 1)?;
r.insert(0, prepend);
a.insert(0, prepend);
} else if !trimmed {
break;
}
}
let mut prefix_skip = 0usize;
while r.len() - prefix_skip > 1 && a.len() - prefix_skip > 1 && r[prefix_skip] == a[prefix_skip]
{
prefix_skip += 1;
pos += 1;
}
let new_ref = std::str::from_utf8(&r[prefix_skip..])
.map_err(|_| VarEffectError::InvalidAllele)?
.to_string();
let new_alt = std::str::from_utf8(&a[prefix_skip..])
.map_err(|_| VarEffectError::InvalidAllele)?
.to_string();
if pos == orig_pos && new_ref == orig_ref && new_alt == orig_alt {
Ok(None)
} else {
Ok(Some((pos, new_ref, new_alt)))
}
}
#[cfg(test)]
mod left_align_unit_tests {
use super::*;
use crate::fasta::write_genome_binary;
use tempfile::TempDir;
const CONTIG: &[u8] = b"AATGGGGTAA";
fn fasta_with(contigs: &[(&str, &[u8])]) -> (TempDir, FastaReader) {
let tmp = TempDir::new().expect("tempdir");
let bin = tmp.path().join("g.bin");
let idx = tmp.path().join("g.bin.idx");
write_genome_binary(contigs, "test", &bin, &idx).expect("write synthetic genome");
let fasta = FastaReader::open(&bin).expect("open synthetic genome");
(tmp, fasta)
}
#[test]
fn duplication_left_aligns_preserving_insertion() {
let (_tmp, fasta) = fasta_with(&[("1", CONTIG)]);
let result = left_align_indel(&fasta, "chr1", 6, "G", "GG")
.expect("left_align_indel should not error");
assert_eq!(result, Some((3, "T".to_string(), "TG".to_string())));
}
#[test]
fn tandem_deletion_left_aligns_preserving_deletion() {
let (_tmp, fasta) = fasta_with(&[("1", CONTIG)]);
let result = left_align_indel(&fasta, "chr1", 6, "GG", "G")
.expect("left_align_indel should not error");
assert_eq!(result, Some((3, "TG".to_string(), "T".to_string())));
}
#[test]
fn left_aligned_insertion_is_idempotent() {
let (_tmp, fasta) = fasta_with(&[("1", CONTIG)]);
let result = left_align_indel(&fasta, "chr1", 3, "T", "TG")
.expect("left_align_indel should not error");
assert_eq!(result, None);
}
#[test]
fn snv_passes_through() {
let (_tmp, fasta) = fasta_with(&[("1", CONTIG)]);
let result = left_align_indel(&fasta, "chr1", 8, "T", "A")
.expect("left_align_indel should not error");
assert_eq!(result, None);
}
}