use super::ExonData;
use crate::error::FerroError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IvsPosition {
pub intron: u32,
pub offset: i64,
}
pub fn parse_ivs(input: &str) -> Result<IvsPosition, FerroError> {
let input = input.trim();
if !input.starts_with("IVS") {
return Err(FerroError::Parse {
pos: 0,
msg: format!("Expected IVS prefix: {}", input),
diagnostic: None,
});
}
let rest = &input[3..];
let sign_pos = rest.find('+').or_else(|| rest.find('-'));
match sign_pos {
Some(pos) => {
let intron_str = &rest[..pos];
let intron: u32 = intron_str.parse().map_err(|_| FerroError::Parse {
pos: 3,
msg: format!("Invalid intron number: {}", intron_str),
diagnostic: None,
})?;
let offset_str = &rest[pos..];
let offset: i64 = offset_str.parse().map_err(|_| FerroError::Parse {
pos: 3 + pos,
msg: format!("Invalid offset: {}", offset_str),
diagnostic: None,
})?;
if offset == 0 {
return Err(FerroError::InvalidCoordinates {
msg: "IVS offset cannot be 0".to_string(),
});
}
Ok(IvsPosition { intron, offset })
}
None => Err(FerroError::Parse {
pos: 0,
msg: format!("IVS notation requires +/- offset: {}", input),
diagnostic: None,
}),
}
}
pub fn convert_ivs_notation(
input: &str,
exon_data: &ExonData,
) -> Result<Option<String>, FerroError> {
let ivs_start = match input.find("IVS") {
Some(pos) => pos,
None => return Ok(None),
};
let after_ivs = &input[ivs_start..];
let mut end_pos = 3;
while end_pos < after_ivs.len()
&& after_ivs
.as_bytes()
.get(end_pos)
.is_some_and(|&b| b.is_ascii_digit())
{
end_pos += 1;
}
if end_pos >= after_ivs.len() {
return Ok(None);
}
let sign_char = after_ivs.as_bytes()[end_pos];
if sign_char != b'+' && sign_char != b'-' {
return Ok(None);
}
end_pos += 1;
while end_pos < after_ivs.len()
&& after_ivs
.as_bytes()
.get(end_pos)
.is_some_and(|&b| b.is_ascii_digit())
{
end_pos += 1;
}
let ivs_str = &after_ivs[..end_pos];
let ivs = parse_ivs(ivs_str)?;
let (base, offset) = exon_data.ivs_to_cds(&ivs)?;
let modern_pos = match offset {
Some(off) if off > 0 => format!("{}+{}", base, off),
Some(off) => format!("{}{}", base, off), None => format!("{}", base),
};
let prefix = &input[..ivs_start];
let suffix = &after_ivs[end_pos..];
Ok(Some(format!("{}{}{}", prefix, modern_pos, suffix)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ivs_donor() {
let ivs = parse_ivs("IVS4+1").unwrap();
assert_eq!(ivs.intron, 4);
assert_eq!(ivs.offset, 1);
}
#[test]
fn test_parse_ivs_acceptor() {
let ivs = parse_ivs("IVS4-2").unwrap();
assert_eq!(ivs.intron, 4);
assert_eq!(ivs.offset, -2);
}
#[test]
fn test_parse_ivs_large_offset() {
let ivs = parse_ivs("IVS12+100").unwrap();
assert_eq!(ivs.intron, 12);
assert_eq!(ivs.offset, 100);
}
#[test]
fn test_parse_ivs_negative_large() {
let ivs = parse_ivs("IVS1-500").unwrap();
assert_eq!(ivs.intron, 1);
assert_eq!(ivs.offset, -500);
}
#[test]
fn test_parse_ivs_invalid_no_offset() {
assert!(parse_ivs("IVS4").is_err());
}
#[test]
fn test_parse_ivs_invalid_zero_offset() {
assert!(parse_ivs("IVS4+0").is_err());
}
#[test]
fn test_parse_ivs_invalid_no_ivs() {
assert!(parse_ivs("c.100+5").is_err());
}
#[test]
fn test_convert_ivs_notation() {
let exon_data = ExonData::new(vec![(1, 100), (201, 300), (401, 500), (601, 700)]);
let result = convert_ivs_notation("NM_000088.3:c.IVS1+5G>A", &exon_data).unwrap();
assert_eq!(result, Some("NM_000088.3:c.100+5G>A".to_string()));
let result = convert_ivs_notation("NM_000088.3:c.IVS2-10A>G", &exon_data).unwrap();
assert_eq!(result, Some("NM_000088.3:c.401-10A>G".to_string()));
}
#[test]
fn test_convert_ivs_notation_interval() {
let exon_data = ExonData::new(vec![
(1, 100),
(201, 300),
(401, 500),
(601, 700),
(801, 900),
(1001, 1100),
(1201, 1300),
]);
let result = convert_ivs_notation("c.IVS7+5del", &exon_data).unwrap();
assert_eq!(result, Some("c.1300+5del".to_string()));
}
#[test]
fn test_convert_ivs_no_ivs() {
let exon_data = ExonData::new(vec![(1, 100)]);
let result = convert_ivs_notation("NM_000088.3:c.100+5G>A", &exon_data).unwrap();
assert_eq!(result, None);
}
}