use crate::hgvs::interval::{GenomeInterval, UncertainBoundary};
use crate::hgvs::location::GenomePos;
use crate::hgvs::variant::{CoordinateAxis, HgvsVariant};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GenomicAxisOffset {
pub axis: CoordinateAxis,
pub stated: String,
}
impl GenomicAxisOffset {
pub fn clause(&self) -> &'static str {
match self.axis {
CoordinateAxis::Circular => "background/numbering.md:8",
CoordinateAxis::Mitochondrial => "background/numbering.md:11",
_ => "background/numbering.md:6",
}
}
pub fn reference_kind(&self) -> &'static str {
match self.axis {
CoordinateAxis::Circular => "circular",
CoordinateAxis::Mitochondrial => "mitochondrial",
_ => "genomic",
}
}
pub fn prefix(&self) -> &'static str {
self.axis.code()
}
pub fn refusal(&self) -> String {
format!(
"`{}.{}` states a `+`/`-` offset on a {} position. {} says nucleotide numbers based \
on a {} reference sequence \"do not include\" `+`, `-`, `*`, or other prefixes: a {} \
accession carries no exon table, so there is no boundary for the offset to be \
measured from and the position names no nucleotide. Give the position a plain \
number, or describe the variant on a transcript accession, where an offset is \
meaningful",
self.prefix(),
self.stated,
self.reference_kind(),
self.clause(),
self.reference_kind(),
self.reference_kind(),
)
}
}
pub fn genomic_axis_offset(variant: &HgvsVariant) -> Option<GenomicAxisOffset> {
match variant {
HgvsVariant::Genome(v) => offset_in_interval(&v.loc_edit.location, CoordinateAxis::Genomic),
HgvsVariant::Mt(v) => {
offset_in_interval(&v.loc_edit.location, CoordinateAxis::Mitochondrial)
}
HgvsVariant::Circular(v) => {
offset_in_interval(&v.loc_edit.location, CoordinateAxis::Circular)
}
HgvsVariant::GenomeRing(ring) => ring
.segments
.iter()
.find_map(|segment| offset_in_interval(&segment.location, CoordinateAxis::Genomic)),
HgvsVariant::Allele(allele) => allele.variants.iter().find_map(genomic_axis_offset),
HgvsVariant::Supernumerary(inner) => genomic_axis_offset(inner),
HgvsVariant::Cds(_)
| HgvsVariant::Tx(_)
| HgvsVariant::Rna(_)
| HgvsVariant::Protein(_)
| HgvsVariant::RnaFusion(_)
| HgvsVariant::NullAllele
| HgvsVariant::UnknownAllele => None,
}
}
fn offset_in_interval(
interval: &GenomeInterval,
axis: CoordinateAxis,
) -> Option<GenomicAxisOffset> {
offset_in_boundary(&interval.start, axis).or_else(|| offset_in_boundary(&interval.end, axis))
}
fn offset_in_boundary(
boundary: &UncertainBoundary<GenomePos>,
axis: CoordinateAxis,
) -> Option<GenomicAxisOffset> {
match boundary {
UncertainBoundary::Single(mu) => mu.inner().and_then(|pos| offset_at(pos, axis)),
UncertainBoundary::Range { start, end } => start
.inner()
.and_then(|pos| offset_at(pos, axis))
.or_else(|| end.inner().and_then(|pos| offset_at(pos, axis))),
}
}
fn offset_at(pos: &GenomePos, axis: CoordinateAxis) -> Option<GenomicAxisOffset> {
match pos.offset {
Some(0) | None => None,
Some(_) => Some(GenomicAxisOffset {
axis,
stated: pos.to_string(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse_hgvs;
fn found(input: &str) -> Option<GenomicAxisOffset> {
genomic_axis_offset(&parse_hgvs(input).expect("input parses"))
}
#[test]
fn a_plus_offset_on_the_genomic_axis_is_found() {
let hit = found("NC_TEST.1:g.266+2del").expect("`+2` is stated");
assert_eq!(hit.axis, CoordinateAxis::Genomic);
assert_eq!(hit.stated, "266+2");
assert_eq!(hit.clause(), "background/numbering.md:6");
}
#[test]
fn a_hyphen_range_on_the_genomic_axis_is_found_as_an_offset() {
let hit = found("NC_TEST.1:g.266-268del").expect("`-268` is stated");
assert_eq!(hit.stated, "266-268");
}
#[test]
fn each_genomic_family_axis_cites_its_own_clause() {
for (input, axis, clause) in [
(
"NC_000001.11:g.100+2del",
CoordinateAxis::Genomic,
"background/numbering.md:6",
),
(
"NC_012920.1:m.100+2del",
CoordinateAxis::Mitochondrial,
"background/numbering.md:11",
),
(
"NC_001416.1:o.100+2del",
CoordinateAxis::Circular,
"background/numbering.md:8",
),
] {
let hit = found(input).unwrap_or_else(|| panic!("{input} states an offset"));
assert_eq!(hit.axis, axis, "{input}");
assert_eq!(hit.clause(), clause, "{input}");
assert!(
hit.refusal().contains(clause),
"the refusal must cite the axis's own clause: {}",
hit.refusal()
);
}
}
#[test]
fn an_unknown_offset_sentinel_is_still_an_offset() {
assert_eq!(
found("NC_000001.11:g.100+?del").expect("`+?`").stated,
"100+?"
);
assert_eq!(
found("NC_000001.11:g.100-?del").expect("`-?`").stated,
"100-?"
);
}
#[test]
fn a_plain_genomic_position_is_not_matched() {
assert!(found("NC_TEST.1:g.266del").is_none());
assert!(found("NC_000001.11:g.100_200del").is_none());
assert!(found("NC_000023.10:g.100A>G").is_none());
}
#[test]
fn a_transcript_axis_offset_is_left_alone() {
assert!(found("NM_000088.3:c.100+5A>G").is_none());
assert!(found("NR_003051.3:n.100+5A>G").is_none());
}
#[test]
fn an_offending_member_is_found_inside_an_allele() {
let hit = found("NC_TEST.1:g.[265del;266+2del]").expect("the second member states one");
assert_eq!(hit.stated, "266+2");
}
}