use super::edit::{NaEdit, ProteinEdit};
use super::interval::{CdsInterval, GenomeInterval, ProtInterval, RnaInterval, TxInterval};
use super::uncertainty::Mu;
use serde::{Deserialize, Serialize};
use smol_str::{SmolStr, SmolStrBuilder};
use std::fmt;
use std::sync::Arc;
mod interned {
use std::sync::{Arc, OnceLock};
macro_rules! interned_prefix {
($name:ident, $value:literal) => {
pub fn $name() -> Arc<str> {
static INSTANCE: OnceLock<Arc<str>> = OnceLock::new();
INSTANCE.get_or_init(|| Arc::from($value)).clone()
}
};
}
interned_prefix!(nc, "NC");
interned_prefix!(ng, "NG");
interned_prefix!(nt, "NT");
interned_prefix!(nw, "NW");
interned_prefix!(nm, "NM");
interned_prefix!(nr, "NR");
interned_prefix!(np, "NP");
interned_prefix!(xm, "XM");
interned_prefix!(xr, "XR");
interned_prefix!(xp, "XP");
interned_prefix!(enst, "ENST");
interned_prefix!(ensg, "ENSG");
interned_prefix!(ensp, "ENSP");
interned_prefix!(ense, "ENSE");
interned_prefix!(ensr, "ENSR");
interned_prefix!(lrg, "LRG");
interned_prefix!(empty, "");
#[inline]
pub fn get_prefix(s: &str) -> Option<Arc<str>> {
match s {
"NC" => Some(nc()),
"NG" => Some(ng()),
"NT" => Some(nt()),
"NW" => Some(nw()),
"NM" => Some(nm()),
"NR" => Some(nr()),
"NP" => Some(np()),
"XM" => Some(xm()),
"XR" => Some(xr()),
"XP" => Some(xp()),
"ENST" => Some(enst()),
"ENSG" => Some(ensg()),
"ENSP" => Some(ensp()),
"ENSE" => Some(ense()),
"ENSR" => Some(ensr()),
"LRG" => Some(lrg()),
"" => Some(empty()),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Accession {
pub prefix: Arc<str>,
pub number: SmolStr,
pub version: Option<u32>,
#[serde(default)]
pub ensembl_style: bool,
#[serde(default)]
pub assembly: Option<Arc<str>>,
#[serde(default)]
pub chromosome: Option<Arc<str>>,
#[serde(default)]
pub genomic_context: Option<Box<Accession>>,
}
impl Accession {
#[inline]
fn intern_prefix(prefix: impl AsRef<str> + Into<Arc<str>>) -> Arc<str> {
interned::get_prefix(prefix.as_ref()).unwrap_or_else(|| prefix.into())
}
pub fn new(
prefix: impl AsRef<str> + Into<Arc<str>>,
number: impl Into<SmolStr>,
version: Option<u32>,
) -> Self {
let prefix = Self::intern_prefix(prefix);
let ensembl_style = Self::is_ensembl_prefix(&prefix);
let version = if Self::is_lrg_prefix(&prefix) {
None
} else {
version
};
Self {
prefix,
number: number.into(),
version,
ensembl_style,
assembly: None,
chromosome: None,
genomic_context: None,
}
}
pub fn with_style(
prefix: impl AsRef<str> + Into<Arc<str>>,
number: impl Into<SmolStr>,
version: Option<u32>,
ensembl_style: bool,
) -> Self {
let prefix = Self::intern_prefix(prefix);
let version = if Self::is_lrg_prefix(&prefix) {
None
} else {
version
};
Self {
prefix,
number: number.into(),
version,
ensembl_style,
assembly: None,
chromosome: None,
genomic_context: None,
}
}
pub fn from_assembly(assembly: impl Into<Arc<str>>, chromosome: impl Into<Arc<str>>) -> Self {
Self {
prefix: interned::empty(),
number: SmolStr::default(),
version: None,
ensembl_style: false,
assembly: Some(assembly.into()),
chromosome: Some(chromosome.into()),
genomic_context: None,
}
}
pub fn with_genomic_context(mut self, context: Accession) -> Self {
self.genomic_context = Some(Box::new(context));
self
}
pub fn without_genomic_context(mut self) -> Self {
self.genomic_context = None;
self
}
pub fn is_assembly_ref(&self) -> bool {
self.assembly.is_some() && self.chromosome.is_some()
}
pub fn is_ensembl_prefix(prefix: &str) -> bool {
let Some(body) = prefix.strip_prefix("ENS") else {
return false;
};
if body.is_empty() {
return false;
}
if body.len() > 6 {
return false;
}
if !body.chars().all(|c| c.is_ascii_uppercase()) {
return false;
}
matches!(
body.as_bytes()[body.len() - 1],
b'G' | b'T' | b'P' | b'E' | b'R'
)
}
pub fn is_ensembl(&self) -> bool {
self.ensembl_style || Self::is_ensembl_prefix(&self.prefix)
}
fn ensembl_feature_variant_type(prefix: &str) -> Option<&'static str> {
if !Self::is_ensembl_prefix(prefix) {
return None;
}
let body = &prefix["ENS".len()..];
if body.ends_with("GT") {
return None;
}
match body.as_bytes()[body.len() - 1] {
b'T' => Some("c"),
b'G' => Some("g"),
b'P' => Some("p"),
_ => None, }
}
pub fn is_ensembl_transcript_prefix(prefix: &str) -> bool {
Self::ensembl_feature_variant_type(prefix) == Some("c")
}
pub fn is_lrg_prefix(prefix: &str) -> bool {
prefix == "LRG"
}
pub fn is_lrg(&self) -> bool {
Self::is_lrg_prefix(&self.prefix)
}
pub fn validate_ensembl(&self) -> bool {
if !self.is_ensembl() {
return true; }
let digit_count = self.number.len();
(11..=15).contains(&digit_count) && self.number.chars().all(|c| c.is_ascii_digit())
}
pub fn inferred_variant_type(&self) -> Option<&'static str> {
if let Some(variant_type) = Self::ensembl_feature_variant_type(&self.prefix) {
return Some(variant_type);
}
match &*self.prefix {
"NC" | "NG" | "NT" | "NW" => Some("g"),
"NM" => Some("c"),
"NR" => Some("n"),
"NP" => Some("p"),
p if Self::is_lrg_prefix(p) => Some(Self::lrg_inferred_variant_type(&self.number)),
p if p.len() == 1 && p.chars().next().is_some_and(|c| c.is_ascii_uppercase()) => {
Some("p")
}
_ => None,
}
}
fn lrg_inferred_variant_type(number: &str) -> &'static str {
let bytes = number.as_bytes();
let disc_pos = bytes.iter().position(|&b| b == b't' || b == b'p');
match disc_pos {
None => "g",
Some(pos) => {
let before_ok = pos > 0 && bytes[..pos].iter().all(|b| b.is_ascii_digit());
let after = &bytes[pos + 1..];
let after_ok = !after.is_empty() && after.iter().all(|b| b.is_ascii_digit());
if before_ok && after_ok {
if bytes[pos] == b't' {
"c"
} else {
"p"
}
} else {
"g"
}
}
}
}
pub fn is_uniprot(&self) -> bool {
self.prefix.len() == 1
&& self
.prefix
.chars()
.next()
.is_some_and(|c| c.is_ascii_uppercase())
&& self.number.len() == 5
&& self.number.chars().all(|c| c.is_ascii_alphanumeric())
}
pub fn is_noncoding_rna(&self) -> bool {
matches!(&*self.prefix, "NR" | "XR")
}
pub fn is_transcript_reference(&self) -> bool {
match &*self.prefix {
"NM" | "NR" | "XM" | "XR" => true,
p if Self::is_ensembl_transcript_prefix(p) => true,
p if Self::is_lrg_prefix(p) => Self::lrg_inferred_variant_type(&self.number) == "c",
_ => false,
}
}
pub(crate) fn admits_gene_selector(&self) -> bool {
!self.is_transcript_reference() && self.genomic_context.is_none()
}
pub fn is_mitochondrial(&self) -> bool {
let prefix: &str = self.prefix.as_ref();
let number: &str = self.number.as_ref();
matches!((prefix, number), ("NC", "012920") | ("NC", "001807"))
}
pub fn base(&self) -> String {
collect_written(|out| self.write_base(out))
}
pub fn full(&self) -> String {
collect_written(|out| self.write_full(out))
}
pub(crate) fn full_smol(&self) -> SmolStr {
collect_written_smol(|out| self.write_full(out))
}
fn write_base(&self, out: &mut impl fmt::Write) -> fmt::Result {
if let (Some(assembly), Some(chromosome)) = (&self.assembly, &self.chromosome) {
return write!(out, "{assembly}({chromosome})");
}
if let Some(ctx) = &self.genomic_context {
ctx.write_full(out)?;
out.write_char('(')?;
self.write_bare(out)?;
return out.write_char(')');
}
self.write_bare(out)
}
fn write_full(&self, out: &mut impl fmt::Write) -> fmt::Result {
if self.is_assembly_ref() {
return self.write_base(out);
}
if let Some(ctx) = &self.genomic_context {
ctx.write_full(out)?;
out.write_char('(')?;
self.write_versioned(out)?;
return out.write_char(')');
}
self.write_versioned(out)
}
fn write_bare(&self, out: &mut impl fmt::Write) -> fmt::Result {
out.write_str(&self.prefix)?;
if !self.ensembl_style {
out.write_char('_')?;
}
out.write_str(&self.number)
}
fn write_versioned(&self, out: &mut impl fmt::Write) -> fmt::Result {
self.write_bare(out)?;
match self.version {
Some(v) => write!(out, ".{v}"),
None => Ok(()),
}
}
pub fn transcript_accession(&self) -> String {
collect_written(|out| self.write_transcript_accession(out))
}
pub(crate) fn transcript_accession_smol(&self) -> SmolStr {
collect_written_smol(|out| self.write_transcript_accession(out))
}
fn write_transcript_accession(&self, out: &mut impl fmt::Write) -> fmt::Result {
if self.is_assembly_ref() {
return self.write_base(out);
}
self.write_versioned(out)
}
}
fn collect_written(produce: impl FnOnce(&mut String) -> fmt::Result) -> String {
const TYPICAL_ACCESSION_LEN: usize = 40;
let mut out = String::with_capacity(TYPICAL_ACCESSION_LEN);
produce(&mut out).expect("writing into a String is infallible");
out
}
fn collect_written_smol(produce: impl FnOnce(&mut SmolStrBuilder) -> fmt::Result) -> SmolStr {
let mut out = SmolStrBuilder::new();
produce(&mut out).expect("writing into a SmolStrBuilder is infallible");
out.finish()
}
impl fmt::Display for Accession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.write_full(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct LocEdit<L, E> {
pub location: L,
pub edit: Mu<E>,
}
impl<L, E> LocEdit<L, E> {
pub fn new(location: L, edit: E) -> Self {
Self {
location,
edit: Mu::Certain(edit),
}
}
pub fn new_predicted(location: L, edit: E) -> Self {
Self {
location,
edit: Mu::Uncertain(edit),
}
}
pub fn with_uncertainty(location: L, edit: Mu<E>) -> Self {
Self { location, edit }
}
}
impl<L: fmt::Display, E: fmt::Display> fmt::Display for LocEdit<L, E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.location, self.edit)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AllelePhase {
Cis,
Trans,
Unknown,
Mosaic,
Chimeric,
AndOr,
Products,
}
impl fmt::Display for AllelePhase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AllelePhase::Cis => write!(f, "cis"),
AllelePhase::Trans => write!(f, "trans"),
AllelePhase::Unknown => write!(f, "unknown"),
AllelePhase::Mosaic => write!(f, "mosaic"),
AllelePhase::Chimeric => write!(f, "chimeric"),
AllelePhase::AndOr => write!(f, "and/or"),
AllelePhase::Products => write!(f, "products"),
}
}
}
impl AllelePhase {
pub fn is_mosaic(&self) -> bool {
matches!(self, AllelePhase::Mosaic)
}
pub fn is_chimeric(&self) -> bool {
matches!(self, AllelePhase::Chimeric)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AlleleVariant {
pub variants: Vec<HgvsVariant>,
pub phase: AllelePhase,
#[serde(default)]
pub uncertain: bool,
}
pub(crate) fn non_flanking_genomic_insertion_anchor(variant: &HgvsVariant) -> Option<(u64, u64)> {
use crate::hgvs::edit::NaEdit;
use crate::hgvs::uncertainty::Mu;
let HgvsVariant::Genome(gv) = variant else {
return None;
};
if !matches!(gv.loc_edit.edit.inner(), Some(NaEdit::Insertion { .. })) {
return None;
}
let (Some(Mu::Certain(start)), Some(Mu::Certain(end))) = (
gv.loc_edit.location.start.as_single(),
gv.loc_edit.location.end.as_single(),
) else {
return None;
};
if start.special.is_some() || end.special.is_some() {
return None;
}
(end.base.checked_sub(start.base) != Some(1)).then_some((start.base, end.base))
}
impl AlleleVariant {
pub fn new(variants: Vec<HgvsVariant>, phase: AllelePhase) -> Self {
Self {
variants,
phase,
uncertain: false,
}
}
pub fn checked(variants: Vec<HgvsVariant>, phase: AllelePhase) -> Option<Self> {
if variants.is_empty() {
return None;
}
Some(Self::new(variants, phase))
}
pub fn new_uncertain(variants: Vec<HgvsVariant>, phase: AllelePhase) -> Self {
Self {
variants,
phase,
uncertain: true,
}
}
pub fn cis(variants: Vec<HgvsVariant>) -> Self {
Self::new(variants, AllelePhase::Cis)
}
pub fn trans(variants: Vec<HgvsVariant>) -> Self {
Self::new(variants, AllelePhase::Trans)
}
pub fn mosaic(variants: Vec<HgvsVariant>) -> Self {
Self::new(variants, AllelePhase::Mosaic)
}
pub fn chimeric(variants: Vec<HgvsVariant>) -> Self {
Self::new(variants, AllelePhase::Chimeric)
}
pub fn unknown_phase(variants: Vec<HgvsVariant>) -> Self {
Self::new(variants, AllelePhase::Unknown)
}
pub fn detect_self_cancelling_pair(variants: &[HgvsVariant]) -> Option<(usize, usize)> {
#[allow(clippy::needless_range_loop)]
for i in 0..variants.len() {
let Some((kind_i, axis_i, range_i, acc_i)) = self_cancelling_descriptor(&variants[i])
else {
continue;
};
for j in (i + 1)..variants.len() {
let Some((kind_j, axis_j, range_j, acc_j)) =
self_cancelling_descriptor(&variants[j])
else {
continue;
};
if acc_i.full() != acc_j.full() {
continue;
}
if axis_i != axis_j {
continue;
}
let (del_idx, dup_idx) = match (kind_i, kind_j) {
(SelfCancellingEditKind::Del, SelfCancellingEditKind::Dup) => (i, j),
(SelfCancellingEditKind::Dup, SelfCancellingEditKind::Del) => (j, i),
_ => continue,
};
if ranges_overlap(range_i, range_j) {
return Some((del_idx, dup_idx));
}
}
}
None
}
pub fn try_new_validated(
variants: Vec<HgvsVariant>,
phase: AllelePhase,
source_span: Option<crate::error::SourceSpan>,
) -> Result<Self, crate::error::FerroError> {
if let Some((dl, du)) = Self::detect_self_cancelling_pair(&variants) {
return Err(build_self_cancelling_error(dl, du, source_span, None));
}
Ok(Self::new(variants, phase))
}
}
pub(crate) fn build_self_cancelling_error(
del_idx: usize,
dup_idx: usize,
source_span: Option<crate::error::SourceSpan>,
source: Option<&str>,
) -> crate::error::FerroError {
let pos = source_span.as_ref().map(|s| s.start).unwrap_or(0);
let mut diagnostic = crate::error::Diagnostic::new()
.with_code(crate::error::ErrorCode::SelfCancellingAllele)
.with_hint(
"HGVS does not allow describing both a deletion and a duplication \
of overlapping reference positions in the same allele \
(recommendations/general.md line 58); drop one edit or rewrite \
as a single delins",
);
if let Some(span) = source_span {
diagnostic = diagnostic.with_span(span);
}
if let Some(src) = source {
diagnostic = diagnostic.with_source(src);
}
crate::error::FerroError::Parse {
pos,
msg: format!(
"Self-cancelling allele: variants at index {} (del) and {} (dup) describe \
overlapping reference positions",
del_idx, dup_idx
),
diagnostic: Some(Box::new(diagnostic)),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SelfCancellingEditKind {
Del,
Dup,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SelfCancellingAxis {
Cds,
Genome,
Tx,
Rna,
Mt,
Circular,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct SelfCancellingPoint {
region: bool,
base: i64,
offset: i64,
}
impl SelfCancellingPoint {
const fn new(region: bool, base: i64, offset: i64) -> Self {
Self {
region,
base,
offset,
}
}
}
type SelfCancellingRange = (SelfCancellingPoint, SelfCancellingPoint);
fn self_cancelling_descriptor(
v: &HgvsVariant,
) -> Option<(
SelfCancellingEditKind,
SelfCancellingAxis,
SelfCancellingRange,
&Accession,
)> {
use crate::hgvs::edit::NaEdit;
let (edit, axis, range, accession) = match v {
HgvsVariant::Cds(inner) => (
inner.loc_edit.edit.inner()?,
SelfCancellingAxis::Cds,
cds_simple_range(&inner.loc_edit.location)?,
&inner.accession,
),
HgvsVariant::Genome(inner) => (
inner.loc_edit.edit.inner()?,
SelfCancellingAxis::Genome,
genome_simple_range(&inner.loc_edit.location)?,
&inner.accession,
),
HgvsVariant::Tx(inner) => (
inner.loc_edit.edit.inner()?,
SelfCancellingAxis::Tx,
tx_simple_range(&inner.loc_edit.location)?,
&inner.accession,
),
HgvsVariant::Rna(inner) => (
inner.loc_edit.edit.inner()?,
SelfCancellingAxis::Rna,
rna_simple_range(&inner.loc_edit.location)?,
&inner.accession,
),
HgvsVariant::Mt(inner) => (
inner.loc_edit.edit.inner()?,
SelfCancellingAxis::Mt,
genome_simple_range(&inner.loc_edit.location)?,
&inner.accession,
),
HgvsVariant::Circular(inner) => (
inner.loc_edit.edit.inner()?,
SelfCancellingAxis::Circular,
genome_simple_range(&inner.loc_edit.location)?,
&inner.accession,
),
_ => return None,
};
let kind = match edit {
NaEdit::Deletion { .. } => SelfCancellingEditKind::Del,
NaEdit::Duplication { .. } => SelfCancellingEditKind::Dup,
_ => return None,
};
Some((kind, axis, range, accession))
}
fn certain_offset(raw: Option<i64>) -> Option<i64> {
use crate::hgvs::parser::position::{OFFSET_UNKNOWN_NEGATIVE, OFFSET_UNKNOWN_POSITIVE};
match raw {
None => Some(0),
Some(OFFSET_UNKNOWN_POSITIVE) | Some(OFFSET_UNKNOWN_NEGATIVE) => None,
Some(v) => Some(v),
}
}
fn cds_simple_range(interval: &crate::hgvs::interval::CdsInterval) -> Option<SelfCancellingRange> {
let s = interval.start.inner()?;
let e = interval.end.inner()?;
if s.is_unknown() || e.is_unknown() {
return None;
}
let s_off = certain_offset(s.offset)?;
let e_off = certain_offset(e.offset)?;
Some((
SelfCancellingPoint::new(s.utr3, s.base, s_off),
SelfCancellingPoint::new(e.utr3, e.base, e_off),
))
}
fn genome_simple_range(
interval: &crate::hgvs::interval::GenomeInterval,
) -> Option<SelfCancellingRange> {
let s = interval.start.inner()?;
let e = interval.end.inner()?;
let s_off = certain_offset(s.offset)?;
let e_off = certain_offset(e.offset)?;
Some((
SelfCancellingPoint::new(false, s.base as i64, s_off),
SelfCancellingPoint::new(false, e.base as i64, e_off),
))
}
fn tx_simple_range(interval: &crate::hgvs::interval::TxInterval) -> Option<SelfCancellingRange> {
let s = interval.start.inner()?;
let e = interval.end.inner()?;
let s_off = certain_offset(s.offset)?;
let e_off = certain_offset(e.offset)?;
Some((
SelfCancellingPoint::new(s.downstream, s.base, s_off),
SelfCancellingPoint::new(e.downstream, e.base, e_off),
))
}
fn rna_simple_range(interval: &crate::hgvs::interval::RnaInterval) -> Option<SelfCancellingRange> {
let s = interval.start.inner()?;
let e = interval.end.inner()?;
let s_off = certain_offset(s.offset)?;
let e_off = certain_offset(e.offset)?;
Some((
SelfCancellingPoint::new(s.utr3, s.base, s_off),
SelfCancellingPoint::new(e.utr3, e.base, e_off),
))
}
#[cfg(test)]
pub(crate) fn cis_member_order_prefix(
v: &HgvsVariant,
) -> (SmolStr, SelfCancellingPoint, SelfCancellingPoint, u8) {
let accession = v.accession().map(Accession::full_smol).unwrap_or_default();
let (start, end, rank) = cis_member_position_key(v);
(accession, start, end, rank)
}
fn cis_member_position_key(v: &HgvsVariant) -> (SelfCancellingPoint, SelfCancellingPoint, u8) {
let sentinel = SelfCancellingPoint::new(true, i64::MAX, i64::MAX);
let (start, end) = cis_member_range(v).unwrap_or((sentinel, sentinel));
(start, end, junction_rank(v))
}
#[cfg(test)]
pub(crate) fn cis_member_order_cmp(a: &HgvsVariant, b: &HgvsVariant) -> std::cmp::Ordering {
cis_member_order_prefix(a)
.cmp(&cis_member_order_prefix(b))
.then_with(|| a.to_string().cmp(&b.to_string()))
}
pub(crate) fn cis_member_order_cmp_within_accession(
a: &HgvsVariant,
b: &HgvsVariant,
) -> std::cmp::Ordering {
cis_member_position_key(a)
.cmp(&cis_member_position_key(b))
.then_with(|| a.to_string().cmp(&b.to_string()))
}
fn junction_rank(v: &HgvsVariant) -> u8 {
use crate::hgvs::edit::{NaEdit, ProteinEdit};
if let HgvsVariant::Protein(p) = v {
return u8::from(!matches!(
p.loc_edit.edit.inner(),
Some(ProteinEdit::Insertion { .. })
));
}
let edit = match v {
HgvsVariant::Genome(x) => x.loc_edit.edit.inner(),
HgvsVariant::Circular(x) => x.loc_edit.edit.inner(),
HgvsVariant::Mt(x) => x.loc_edit.edit.inner(),
HgvsVariant::Cds(x) => x.loc_edit.edit.inner(),
HgvsVariant::Tx(x) => x.loc_edit.edit.inner(),
HgvsVariant::Rna(x) => x.loc_edit.edit.inner(),
_ => None,
};
u8::from(!matches!(edit, Some(NaEdit::Insertion { .. })))
}
fn cis_member_range(v: &HgvsVariant) -> Option<SelfCancellingRange> {
match v {
HgvsVariant::Genome(x) => genome_simple_range(&x.loc_edit.location),
HgvsVariant::Cds(x) => cds_simple_range(&x.loc_edit.location),
HgvsVariant::Tx(x) => tx_simple_range(&x.loc_edit.location),
HgvsVariant::Rna(x) => rna_simple_range(&x.loc_edit.location),
HgvsVariant::Mt(x) => genome_simple_range(&x.loc_edit.location),
HgvsVariant::Circular(x) => genome_simple_range(&x.loc_edit.location),
HgvsVariant::Protein(x) => {
let point = |p: &crate::hgvs::location::ProtPos| {
SelfCancellingPoint::new(false, p.number as i64, 0)
};
let start = x.loc_edit.location.start.inner().map(point)?;
let end = x.loc_edit.location.end.inner().map(point).unwrap_or(start);
Some((start, end))
}
_ => None,
}
}
fn ranges_overlap(a: SelfCancellingRange, b: SelfCancellingRange) -> bool {
let lo = a.0.max(b.0);
let hi = a.1.min(b.1);
lo <= hi
}
pub(crate) fn set_repeat_count(
v: &mut HgvsVariant,
new_count: crate::hgvs::edit::RepeatCount,
) -> bool {
fn na(edit: &mut Mu<NaEdit>, c: crate::hgvs::edit::RepeatCount) -> bool {
match edit {
Mu::Certain(NaEdit::Repeat {
count,
additional_counts,
trailing,
..
})
| Mu::Uncertain(NaEdit::Repeat {
count,
additional_counts,
trailing,
..
}) if additional_counts.is_empty() && trailing.is_none() => {
*count = c;
true
}
_ => false,
}
}
match v {
HgvsVariant::Genome(x) => na(&mut x.loc_edit.edit, new_count),
HgvsVariant::Cds(x) => na(&mut x.loc_edit.edit, new_count),
HgvsVariant::Tx(x) => na(&mut x.loc_edit.edit, new_count),
HgvsVariant::Rna(x) => na(&mut x.loc_edit.edit, new_count),
HgvsVariant::Mt(x) => na(&mut x.loc_edit.edit, new_count),
HgvsVariant::Circular(x) => na(&mut x.loc_edit.edit, new_count),
HgvsVariant::Protein(x) => match &mut x.loc_edit.edit {
Mu::Certain(ProteinEdit::Repeat { count, .. })
| Mu::Uncertain(ProteinEdit::Repeat { count, .. }) => {
*count = new_count;
true
}
_ => false,
},
_ => false,
}
}
fn repeat_count_of(v: &HgvsVariant) -> Option<&crate::hgvs::edit::RepeatCount> {
fn na(edit: &Mu<NaEdit>) -> Option<&crate::hgvs::edit::RepeatCount> {
match edit.inner() {
Some(NaEdit::Repeat {
count,
additional_counts,
trailing,
..
}) if additional_counts.is_empty() && trailing.is_none() => Some(count),
_ => None,
}
}
match v {
HgvsVariant::Genome(x) => na(&x.loc_edit.edit),
HgvsVariant::Cds(x) => na(&x.loc_edit.edit),
HgvsVariant::Tx(x) => na(&x.loc_edit.edit),
HgvsVariant::Rna(x) => na(&x.loc_edit.edit),
HgvsVariant::Mt(x) => na(&x.loc_edit.edit),
HgvsVariant::Circular(x) => na(&x.loc_edit.edit),
HgvsVariant::Protein(x) => match x.loc_edit.edit.inner() {
Some(ProteinEdit::Repeat { count, .. }) => Some(count),
_ => None,
},
_ => None,
}
}
fn is_shared_repeat_trans(variants: &[HgvsVariant]) -> bool {
if variants.len() < 2 || !HgvsVariant::all_share_accession_and_type(variants) {
return false;
}
if !variants
.iter()
.all(|v| matches!(v, HgvsVariant::Rna(_) | HgvsVariant::Protein(_)))
{
return false;
}
let key = |v: &HgvsVariant| -> Option<String> {
let mut probe = v.clone();
set_repeat_count(&mut probe, crate::hgvs::edit::RepeatCount::Exact(0))
.then(|| format!("{}", probe))
};
match key(&variants[0]) {
Some(first) => variants
.iter()
.all(|v| key(v).as_deref() == Some(first.as_str())),
None => false,
}
}
fn use_compact_form(variants: &[HgvsVariant]) -> bool {
!variants.is_empty()
&& HgvsVariant::all_share_accession_and_type(variants)
&& !variants.iter().any(HgvsVariant::is_loc_edit_unknown)
}
fn trans_compact_anchor(variants: &[HgvsVariant]) -> Option<&HgvsVariant> {
let mut anchor: Option<&HgvsVariant> = None;
for member in variants {
let leaves: &[HgvsVariant] = match member {
HgvsVariant::NullAllele | HgvsVariant::UnknownAllele => &[],
HgvsVariant::Allele(a) => &a.variants,
other => std::slice::from_ref(other),
};
for leaf in leaves {
if leaf.accession().is_none()
|| matches!(
leaf.variant_type(),
"allele" | "null" | "unknown" | "r::r" | "g::g"
)
|| leaf.is_loc_edit_unknown()
{
return None;
}
match anchor {
None => anchor = Some(leaf),
Some(a) => {
if leaf.accession() != a.accession() || leaf.variant_type() != a.variant_type()
{
return None;
}
let selector_visible = a
.accession()
.is_some_and(|acc| gene_selector_visible(acc, a.variant_type()));
if selector_visible && leaf.gene_symbol() != a.gene_symbol() {
return None;
}
}
}
}
}
anchor
}
fn write_trans_member_bracketed(f: &mut fmt::Formatter<'_>, member: &HgvsVariant) -> fmt::Result {
if let HgvsVariant::Allele(a) = member {
if a.phase == AllelePhase::Unknown {
for (i, sub) in a.variants.iter().enumerate() {
if i == 0 {
write!(f, "[")?;
sub.fmt_loc_edit(f)?;
write!(f, "]")?;
} else {
write!(f, "(;)")?;
sub.fmt_loc_edit(f)?;
}
}
return Ok(());
}
}
write!(f, "[")?;
write_trans_member(f, member)?;
write!(f, "]")
}
fn write_trans_member(f: &mut fmt::Formatter<'_>, member: &HgvsVariant) -> fmt::Result {
match member {
HgvsVariant::NullAllele => write!(f, "0"),
HgvsVariant::UnknownAllele => write!(f, "?"),
HgvsVariant::Allele(a) => {
for (i, sub) in a.variants.iter().enumerate() {
if i > 0 {
write!(f, ";")?;
}
sub.fmt_loc_edit(f)?;
}
Ok(())
}
other => other.fmt_loc_edit(f),
}
}
pub(crate) fn first_leaf_with_accession(v: &HgvsVariant) -> Option<&HgvsVariant> {
match v {
HgvsVariant::Allele(a) => a.variants.iter().find_map(first_leaf_with_accession),
HgvsVariant::NullAllele | HgvsVariant::UnknownAllele | HgvsVariant::RnaFusion(_) => None,
other => {
if other.accession().is_some() {
Some(other)
} else {
None
}
}
}
}
fn andor_alleles_share_accession(variants: &[HgvsVariant]) -> bool {
let mut anchor: Option<&HgvsVariant> = None;
for member in variants {
let HgvsVariant::Allele(_) = member else {
return false;
};
let leaf = match first_leaf_with_accession(member) {
Some(l) => l,
None => return false,
};
match anchor {
None => anchor = Some(leaf),
Some(a) => {
if leaf.accession() != a.accession() || leaf.variant_type() != a.variant_type() {
return false;
}
}
}
}
anchor.is_some()
}
fn write_andor_allele_inner(f: &mut fmt::Formatter<'_>, allele: &AlleleVariant) -> fmt::Result {
let sep = if allele.phase == AllelePhase::Unknown {
"(;)"
} else {
";"
};
for (i, member) in allele.variants.iter().enumerate() {
if i > 0 {
write!(f, "{}", sep)?;
}
member.fmt_loc_edit(f)?;
}
Ok(())
}
fn write_compact_prefix(f: &mut fmt::Formatter<'_>, first: &HgvsVariant) -> fmt::Result {
let accession = first
.accession()
.expect("compact form requires an accession; guarded by all_share_accession_and_type");
write!(f, "{}", accession)?;
if let Some(gene) = first.gene_symbol() {
if gene_selector_visible(accession, first.variant_type()) {
write!(f, "({})", gene)?;
}
}
write!(f, ":{}.", first.variant_type())
}
fn gene_selector_visible(accession: &Accession, variant_type: &str) -> bool {
accession.admits_gene_selector() && variant_type != "p"
}
fn write_accession_with_optional_gene(
f: &mut fmt::Formatter<'_>,
accession: &Accession,
gene_symbol: Option<&str>,
) -> fmt::Result {
write!(f, "{}", accession)?;
if let Some(gene) = gene_symbol {
if accession.admits_gene_selector() {
write!(f, "({})", gene)?;
}
}
Ok(())
}
fn fmt_genome_loc_edit(
f: &mut fmt::Formatter<'_>,
loc_edit: &LocEdit<GenomeInterval, NaEdit>,
) -> fmt::Result {
if let Some(edit) = loc_edit.edit.inner() {
if edit.is_whole_entity() || edit.is_positionless() {
return write!(f, "{}", loc_edit.edit);
}
}
if loc_edit.edit.is_uncertain() {
if let Some(edit) = loc_edit.edit.inner() {
return write!(f, "({}{})", loc_edit.location, edit);
}
}
write!(f, "{}", loc_edit)
}
fn is_position_identity_lhs(variant: &HgvsVariant) -> bool {
let edit_is_pos_identity = |edit: &Mu<NaEdit>| {
matches!(
edit.inner(),
Some(NaEdit::Identity {
whole_entity: false,
..
})
)
};
match variant {
HgvsVariant::Genome(v) => edit_is_pos_identity(&v.loc_edit.edit),
HgvsVariant::Cds(v) => edit_is_pos_identity(&v.loc_edit.edit),
HgvsVariant::Tx(v) => edit_is_pos_identity(&v.loc_edit.edit),
HgvsVariant::Rna(v) => edit_is_pos_identity(&v.loc_edit.edit),
HgvsVariant::Mt(v) => edit_is_pos_identity(&v.loc_edit.edit),
HgvsVariant::Circular(v) => edit_is_pos_identity(&v.loc_edit.edit),
HgvsVariant::Protein(v) => matches!(
v.loc_edit.edit.inner(),
Some(crate::hgvs::edit::ProteinEdit::Identity {
whole_protein: false,
..
})
),
_ => false,
}
}
fn is_whole_entity_identity_rhs(variant: &HgvsVariant) -> bool {
let edit_is_whole = |edit: &Mu<NaEdit>| {
matches!(
edit.inner(),
Some(NaEdit::Identity {
whole_entity: true,
..
})
)
};
match variant {
HgvsVariant::Genome(v) => edit_is_whole(&v.loc_edit.edit),
HgvsVariant::Cds(v) => edit_is_whole(&v.loc_edit.edit),
HgvsVariant::Tx(v) => edit_is_whole(&v.loc_edit.edit),
HgvsVariant::Rna(v) => edit_is_whole(&v.loc_edit.edit),
HgvsVariant::Mt(v) => edit_is_whole(&v.loc_edit.edit),
HgvsVariant::Circular(v) => edit_is_whole(&v.loc_edit.edit),
_ => false,
}
}
fn is_whole_entity_identity_lhs(variant: &HgvsVariant) -> bool {
is_whole_entity_identity_rhs(variant)
}
fn intervals_match_for_compact_mosaic(a: &HgvsVariant, b: &HgvsVariant) -> bool {
match (a, b) {
(HgvsVariant::Genome(x), HgvsVariant::Genome(y)) => {
x.loc_edit.location == y.loc_edit.location
}
(HgvsVariant::Cds(x), HgvsVariant::Cds(y)) => x.loc_edit.location == y.loc_edit.location,
(HgvsVariant::Tx(x), HgvsVariant::Tx(y)) => x.loc_edit.location == y.loc_edit.location,
(HgvsVariant::Rna(x), HgvsVariant::Rna(y)) => x.loc_edit.location == y.loc_edit.location,
(HgvsVariant::Mt(x), HgvsVariant::Mt(y)) => x.loc_edit.location == y.loc_edit.location,
(HgvsVariant::Circular(x), HgvsVariant::Circular(y)) => {
x.loc_edit.location == y.loc_edit.location
}
(HgvsVariant::Protein(x), HgvsVariant::Protein(y)) => {
x.loc_edit.location == y.loc_edit.location
}
_ => false,
}
}
enum CompactMosaicRhs {
BareEdit,
LocEdit,
Identity,
}
fn compact_mosaic_rhs_kind(lhs: &HgvsVariant, rhs: &HgvsVariant) -> Option<CompactMosaicRhs> {
if is_position_identity_lhs(lhs) && intervals_match_for_compact_mosaic(lhs, rhs) {
Some(CompactMosaicRhs::BareEdit)
} else if is_whole_entity_identity_lhs(lhs) && !is_whole_entity_identity_rhs(rhs) {
Some(CompactMosaicRhs::LocEdit)
} else if !is_position_identity_lhs(lhs) && is_whole_entity_identity_rhs(rhs) {
Some(CompactMosaicRhs::Identity)
} else {
None
}
}
fn write_mosaic_or_chimeric(
f: &mut fmt::Formatter<'_>,
variants: &[HgvsVariant],
sep: &str,
uncertain: bool,
) -> fmt::Result {
if variants.len() == 2 && use_compact_form(variants) {
let lhs = &variants[0];
let rhs = &variants[1];
if let Some(kind) = compact_mosaic_rhs_kind(lhs, rhs) {
if uncertain {
write_compact_prefix(f, lhs)?;
write!(f, "(")?;
lhs.fmt_loc_edit(f)?;
write!(f, "{}", sep)?;
match kind {
CompactMosaicRhs::BareEdit => write_bare_edit(f, rhs)?,
CompactMosaicRhs::LocEdit => rhs.fmt_loc_edit(f)?,
CompactMosaicRhs::Identity => write!(f, "=")?,
}
return write!(f, ")");
}
write!(f, "{}{}", lhs, sep)?;
return match kind {
CompactMosaicRhs::BareEdit => write_bare_edit(f, rhs),
CompactMosaicRhs::LocEdit => rhs.fmt_loc_edit(f),
CompactMosaicRhs::Identity => write!(f, "="),
};
}
}
for (i, v) in variants.iter().enumerate() {
if i > 0 {
write!(f, "{}", sep)?;
}
write!(f, "{}", v)?;
}
Ok(())
}
fn write_bare_edit(f: &mut fmt::Formatter<'_>, variant: &HgvsVariant) -> fmt::Result {
match variant {
HgvsVariant::Genome(v) => write!(f, "{}", v.loc_edit.edit),
HgvsVariant::Cds(v) => write!(f, "{}", v.loc_edit.edit),
HgvsVariant::Tx(v) => write!(f, "{}", v.loc_edit.edit),
HgvsVariant::Rna(v) => match v.loc_edit.edit.inner() {
Some(edit) => write!(f, "{}", edit.to_rna_string()),
None => write!(f, "{}", v.loc_edit.edit),
},
HgvsVariant::Mt(v) => write!(f, "{}", v.loc_edit.edit),
HgvsVariant::Circular(v) => write!(f, "{}", v.loc_edit.edit),
HgvsVariant::Protein(v) => write!(f, "{}", v.loc_edit.edit),
_ => write!(f, "{}", variant),
}
}
impl AlleleVariant {
fn fmt_with_style(
&self,
f: &mut fmt::Formatter<'_>,
style: Option<crate::hgvs::location::ProteinRenderStyle>,
) -> fmt::Result {
if self.variants.len() == 1 {
return fmt_member_full(f, &self.variants[0], style);
}
match self.phase {
AllelePhase::Cis => {
let (lp, rp) = if self.uncertain { ("(", ")") } else { ("", "") };
if use_compact_form(&self.variants) {
write_compact_prefix(f, &self.variants[0])?;
write!(f, "[{}", lp)?;
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, ";")?;
}
fmt_member_loc_edit(f, v, style)?;
}
write!(f, "{}]", rp)
} else {
write!(f, "[{}", lp)?;
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, ";")?;
}
fmt_member_full(f, v, style)?;
}
write!(f, "{}]", rp)
}
}
AllelePhase::Products => {
let (lp, rp) = if self.uncertain { ("(", ")") } else { ("", "") };
if use_compact_form(&self.variants) {
write_compact_prefix(f, &self.variants[0])?;
write!(f, "[{}", lp)?;
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, ",")?;
}
fmt_member_loc_edit(f, v, style)?;
}
write!(f, "{}]", rp)
} else {
write!(f, "[{}", lp)?;
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, ",")?;
}
fmt_member_full(f, v, style)?;
}
write!(f, "{}]", rp)
}
}
AllelePhase::Trans => {
let has_nested_group = self
.variants
.iter()
.any(|v| matches!(v, HgvsVariant::Allele(_)));
if has_nested_group {
if let Some(anchor) = trans_compact_anchor(&self.variants) {
write_compact_prefix(f, anchor)?;
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, ";")?;
}
write_trans_member_bracketed(f, v)?;
}
Ok(())
} else {
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, ";")?;
}
write_trans_member_bracketed(f, v)?;
}
Ok(())
}
} else if is_shared_repeat_trans(&self.variants) {
write_compact_prefix(f, &self.variants[0])?;
fmt_member_loc_edit(f, &self.variants[0], style)?;
for v in &self.variants[1..] {
write!(f, ";")?;
debug_assert!(
repeat_count_of(v).is_some(),
"is_shared_repeat_trans guarantees a single-count repeat per member"
);
if let Some(count) = repeat_count_of(v) {
write!(f, "{}", count)?;
}
}
Ok(())
} else if use_compact_form(&self.variants) {
write_compact_prefix(f, &self.variants[0])?;
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, ";")?;
}
write!(f, "[")?;
fmt_member_loc_edit(f, v, style)?;
write!(f, "]")?;
}
Ok(())
} else {
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, ";")?;
}
write!(f, "[{}]", v)?;
}
Ok(())
}
}
AllelePhase::Unknown => {
if self.variants.len() > 1 && use_compact_form(&self.variants) {
write_compact_prefix(f, &self.variants[0])?;
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, "(;)")?;
}
fmt_member_loc_edit(f, v, style)?;
}
Ok(())
} else {
write!(f, "[")?;
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, "(;)")?;
}
fmt_member_full(f, v, style)?;
}
write!(f, "]")
}
}
AllelePhase::Mosaic => write_mosaic_or_chimeric(f, &self.variants, "/", self.uncertain),
AllelePhase::Chimeric => {
write_mosaic_or_chimeric(f, &self.variants, "//", self.uncertain)
}
AllelePhase::AndOr => {
if self.uncertain && use_compact_form(&self.variants) {
write_compact_prefix(f, &self.variants[0])?;
write!(f, "(")?;
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, "^")?;
}
fmt_member_loc_edit(f, v, style)?;
}
write!(f, ")")
} else if self.uncertain {
write!(f, "(")?;
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, "^")?;
}
fmt_member_full(f, v, style)?;
}
write!(f, ")")
} else if andor_alleles_share_accession(&self.variants) {
let anchor = first_leaf_with_accession(&self.variants[0])
.expect("andor_alleles_share_accession guarantees a leaf");
write_compact_prefix(f, anchor)?;
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, "^")?;
}
write!(f, "[")?;
if let HgvsVariant::Allele(a) = v {
write_andor_allele_inner(f, a)?;
} else {
unreachable!(
"andor_alleles_share_accession guarantees all operands are Alleles"
)
}
write!(f, "]")?;
}
Ok(())
} else {
for (i, v) in self.variants.iter().enumerate() {
if i > 0 {
write!(f, "^")?;
}
fmt_member_full(f, v, style)?;
}
Ok(())
}
}
}
}
}
impl fmt::Display for AlleleVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.fmt_with_style(f, None)
}
}
fn fmt_member_loc_edit(
f: &mut fmt::Formatter<'_>,
v: &HgvsVariant,
style: Option<crate::hgvs::location::ProteinRenderStyle>,
) -> fmt::Result {
match (v, style) {
(HgvsVariant::Protein(p), Some(s)) => p.fmt_loc_edit_styled(f, s),
_ => v.fmt_loc_edit(f),
}
}
fn fmt_member_full(
f: &mut fmt::Formatter<'_>,
v: &HgvsVariant,
style: Option<crate::hgvs::location::ProteinRenderStyle>,
) -> fmt::Result {
match (v, style) {
(HgvsVariant::Protein(p), Some(s)) => p.fmt_styled(f, s),
(HgvsVariant::Allele(a), _) => a.fmt_with_style(f, style),
_ => write!(f, "{}", v),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CoordinateAxis {
Genomic,
Coding,
NonCoding,
Rna,
Protein,
Mitochondrial,
Circular,
}
impl CoordinateAxis {
pub fn code(self) -> &'static str {
match self {
CoordinateAxis::Genomic => "g",
CoordinateAxis::Coding => "c",
CoordinateAxis::NonCoding => "n",
CoordinateAxis::Rna => "r",
CoordinateAxis::Protein => "p",
CoordinateAxis::Mitochondrial => "m",
CoordinateAxis::Circular => "o",
}
}
pub fn is_dna(self) -> bool {
matches!(
self,
CoordinateAxis::Genomic
| CoordinateAxis::Coding
| CoordinateAxis::NonCoding
| CoordinateAxis::Mitochondrial
| CoordinateAxis::Circular
)
}
pub fn is_rna(self) -> bool {
matches!(self, CoordinateAxis::Rna)
}
pub fn is_protein(self) -> bool {
matches!(self, CoordinateAxis::Protein)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum HgvsVariant {
Genome(GenomeVariant),
Cds(CdsVariant),
Tx(TxVariant),
Rna(RnaVariant),
Protein(ProteinVariant),
Mt(MtVariant),
Circular(CircularVariant),
RnaFusion(RnaFusionVariant),
GenomeRing(GenomeRing),
Supernumerary(Box<HgvsVariant>),
Allele(AlleleVariant),
NullAllele,
UnknownAllele,
}
impl HgvsVariant {
pub fn accession(&self) -> Option<&Accession> {
match self {
HgvsVariant::Genome(v) => Some(&v.accession),
HgvsVariant::Cds(v) => Some(&v.accession),
HgvsVariant::Tx(v) => Some(&v.accession),
HgvsVariant::Rna(v) => Some(&v.accession),
HgvsVariant::Protein(v) => Some(&v.accession),
HgvsVariant::Mt(v) => Some(&v.accession),
HgvsVariant::Circular(v) => Some(&v.accession),
HgvsVariant::RnaFusion(v) => Some(&v.five_prime.accession),
HgvsVariant::GenomeRing(g) => Some(&g.accession),
HgvsVariant::Supernumerary(inner) => inner.accession(),
HgvsVariant::Allele(a) => a.variants.first().and_then(|v| v.accession()),
HgvsVariant::NullAllele | HgvsVariant::UnknownAllele => None,
}
}
pub fn coordinate_axis(&self) -> Option<CoordinateAxis> {
match self {
HgvsVariant::Genome(_) => Some(CoordinateAxis::Genomic),
HgvsVariant::Cds(_) => Some(CoordinateAxis::Coding),
HgvsVariant::Tx(_) => Some(CoordinateAxis::NonCoding),
HgvsVariant::Rna(_) => Some(CoordinateAxis::Rna),
HgvsVariant::Protein(_) => Some(CoordinateAxis::Protein),
HgvsVariant::Mt(_) => Some(CoordinateAxis::Mitochondrial),
HgvsVariant::Circular(_) => Some(CoordinateAxis::Circular),
HgvsVariant::Supernumerary(inner) => inner.coordinate_axis(),
HgvsVariant::Allele(a) => {
let mut axis: Option<CoordinateAxis> = None;
for member in &a.variants {
match member.coordinate_axis() {
None => {
if matches!(
member,
HgvsVariant::NullAllele | HgvsVariant::UnknownAllele
) {
continue;
}
return None;
}
Some(member_axis) => match axis {
None => axis = Some(member_axis),
Some(prev) if prev == member_axis => {}
Some(_) => return None,
},
}
}
axis
}
HgvsVariant::GenomeRing(_) => Some(CoordinateAxis::Genomic),
HgvsVariant::RnaFusion(_) | HgvsVariant::NullAllele | HgvsVariant::UnknownAllele => {
None
}
}
}
pub fn gene_symbol(&self) -> Option<&str> {
match self {
HgvsVariant::Genome(v) => v.gene_symbol.as_deref(),
HgvsVariant::Cds(v) => v.gene_symbol.as_deref(),
HgvsVariant::Tx(v) => v.gene_symbol.as_deref(),
HgvsVariant::Rna(v) => v.gene_symbol.as_deref(),
HgvsVariant::Protein(v) => v.gene_symbol.as_deref(),
HgvsVariant::Mt(v) => v.gene_symbol.as_deref(),
HgvsVariant::Circular(v) => v.gene_symbol.as_deref(),
HgvsVariant::GenomeRing(g) => g.gene_symbol.as_deref(),
HgvsVariant::Supernumerary(inner) => inner.gene_symbol(),
HgvsVariant::RnaFusion(_)
| HgvsVariant::Allele(_)
| HgvsVariant::NullAllele
| HgvsVariant::UnknownAllele => None,
}
}
pub fn variant_type(&self) -> &'static str {
match self {
HgvsVariant::Genome(_) => "g",
HgvsVariant::Cds(_) => "c",
HgvsVariant::Tx(_) => "n",
HgvsVariant::Rna(_) => "r",
HgvsVariant::Protein(_) => "p",
HgvsVariant::Mt(_) => "m",
HgvsVariant::Circular(_) => "o",
HgvsVariant::RnaFusion(_) => "r::r",
HgvsVariant::GenomeRing(_) => "g::g",
HgvsVariant::Supernumerary(inner) => inner.variant_type(),
HgvsVariant::Allele(_) => "allele",
HgvsVariant::NullAllele => "null",
HgvsVariant::UnknownAllele => "unknown",
}
}
pub fn is_allele(&self) -> bool {
matches!(self, HgvsVariant::Allele(_))
}
pub fn is_null_allele(&self) -> bool {
matches!(self, HgvsVariant::NullAllele)
}
pub fn is_unknown_allele(&self) -> bool {
matches!(self, HgvsVariant::UnknownAllele)
}
pub fn to_styled_string(&self, style: crate::hgvs::location::ProteinRenderStyle) -> String {
struct Styled<'a>(&'a HgvsVariant, crate::hgvs::location::ProteinRenderStyle);
impl fmt::Display for Styled<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
HgvsVariant::Protein(v) => v.fmt_styled(f, self.1),
HgvsVariant::Allele(a) => a.fmt_with_style(f, Some(self.1)),
other => write!(f, "{}", other),
}
}
}
format!("{}", Styled(self, style))
}
pub fn spec_equivalent_renderings(&self) -> Vec<String> {
use crate::hgvs::location::{ProteinRenderStyle, TerStyle};
let starred = self.to_styled_string(ProteinRenderStyle {
stop: TerStyle::Star,
..Default::default()
});
if starred == self.to_string() {
Vec::new()
} else {
vec![starred]
}
}
fn fmt_loc_edit(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HgvsVariant::Genome(v) => v.fmt_loc_edit(f),
HgvsVariant::Cds(v) => v.fmt_loc_edit(f),
HgvsVariant::Tx(v) => v.fmt_loc_edit(f),
HgvsVariant::Rna(v) => v.fmt_loc_edit(f),
HgvsVariant::Protein(v) => v.fmt_loc_edit(f),
HgvsVariant::Mt(v) => v.fmt_loc_edit(f),
HgvsVariant::Circular(v) => v.fmt_loc_edit(f),
HgvsVariant::RnaFusion(v) => write!(f, "{}", v),
HgvsVariant::GenomeRing(g) => write!(f, "{}", g),
HgvsVariant::Supernumerary(_) => write!(f, "{}", self),
HgvsVariant::Allele(a) => write!(f, "{}", a),
HgvsVariant::NullAllele => write!(f, "0"),
HgvsVariant::UnknownAllele => write!(f, "?"),
}
}
pub(crate) fn is_loc_edit_unknown(&self) -> bool {
match self {
HgvsVariant::Genome(v) => is_na_edit_unknown(&v.loc_edit.edit),
HgvsVariant::Cds(v) => is_na_edit_unknown(&v.loc_edit.edit),
HgvsVariant::Tx(v) => is_na_edit_unknown(&v.loc_edit.edit),
HgvsVariant::Rna(v) => is_na_edit_unknown(&v.loc_edit.edit),
HgvsVariant::Mt(v) => is_na_edit_unknown(&v.loc_edit.edit),
HgvsVariant::Circular(v) => is_na_edit_unknown(&v.loc_edit.edit),
HgvsVariant::Protein(v) => match &v.loc_edit.edit {
Mu::Unknown => true,
Mu::Certain(e) | Mu::Uncertain(e) => e.is_whole_protein_unknown(),
},
HgvsVariant::RnaFusion(_)
| HgvsVariant::GenomeRing(_)
| HgvsVariant::Supernumerary(_)
| HgvsVariant::Allele(_)
| HgvsVariant::NullAllele
| HgvsVariant::UnknownAllele => false,
}
}
pub(crate) fn all_share_accession_and_type(variants: &[HgvsVariant]) -> bool {
let Some(first) = variants.first() else {
return true;
};
let first_acc = first.accession();
let first_type = first.variant_type();
let first_gene = first.gene_symbol();
if first_acc.is_none()
|| matches!(first_type, "allele" | "null" | "unknown" | "r::r" | "g::g")
{
return false;
}
let selector_visible = first_acc.is_some_and(|acc| gene_selector_visible(acc, first_type));
variants[1..].iter().all(|v| {
v.variant_type() == first_type
&& v.accession() == first_acc
&& (!selector_visible || v.gene_symbol() == first_gene)
})
}
}
fn is_position_only_identity(edit: &Mu<NaEdit>) -> bool {
matches!(
edit,
Mu::Certain(NaEdit::Identity {
sequence: None,
whole_entity: false,
})
)
}
pub(crate) fn make_supernumerary(inner: HgvsVariant) -> Option<HgvsVariant> {
let ok = matches!(&inner, HgvsVariant::GenomeRing(_))
|| matches!(&inner, HgvsVariant::Genome(g) if is_position_only_identity(&g.loc_edit.edit));
ok.then(|| HgvsVariant::Supernumerary(Box::new(inner)))
}
fn is_na_edit_unknown(edit: &Mu<NaEdit>) -> bool {
match edit {
Mu::Unknown => true,
Mu::Certain(e) | Mu::Uncertain(e) => e.is_whole_entity_unknown(),
}
}
impl fmt::Display for HgvsVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HgvsVariant::Genome(v) => write!(f, "{}", v),
HgvsVariant::Cds(v) => write!(f, "{}", v),
HgvsVariant::Tx(v) => write!(f, "{}", v),
HgvsVariant::Rna(v) => write!(f, "{}", v),
HgvsVariant::Protein(v) => write!(f, "{}", v),
HgvsVariant::Mt(v) => write!(f, "{}", v),
HgvsVariant::Circular(v) => write!(f, "{}", v),
HgvsVariant::RnaFusion(v) => write!(f, "{}", v),
HgvsVariant::GenomeRing(g) => write!(f, "{}", g),
HgvsVariant::Supernumerary(inner) => {
match inner.as_ref() {
HgvsVariant::GenomeRing(g) => g.fmt_bracketed(f)?,
HgvsVariant::Genome(v) if is_position_only_identity(&v.loc_edit.edit) => {
write_accession_with_optional_gene(
f,
&v.accession,
v.gene_symbol.as_deref(),
)?;
write!(f, ":g.{}", v.loc_edit.location)?;
}
other => write!(f, "{}", other)?,
}
write!(f, "sup")
}
HgvsVariant::Allele(a) => write!(f, "{}", a),
HgvsVariant::NullAllele => write!(f, "0"),
HgvsVariant::UnknownAllele => write!(f, "?"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GenomeVariant {
pub accession: Accession,
pub gene_symbol: Option<String>,
pub loc_edit: LocEdit<GenomeInterval, NaEdit>,
}
impl GenomeVariant {
fn fmt_loc_edit(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt_genome_loc_edit(f, &self.loc_edit)
}
}
impl fmt::Display for GenomeVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_accession_with_optional_gene(f, &self.accession, self.gene_symbol.as_deref())?;
write!(f, ":g.")?;
self.fmt_loc_edit(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GenomeRing {
pub accession: Accession,
pub gene_symbol: Option<String>,
pub segments: Vec<LocEdit<GenomeInterval, NaEdit>>,
}
impl GenomeRing {
pub fn new(
accession: Accession,
gene_symbol: Option<String>,
segments: Vec<LocEdit<GenomeInterval, NaEdit>>,
) -> Option<Self> {
if segments.len() < 2 {
return None;
}
Some(Self {
accession,
gene_symbol,
segments,
})
}
fn fmt_segments(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, segment) in self.segments.iter().enumerate() {
if i > 0 {
write!(f, "::")?;
}
fmt_genome_loc_edit(f, segment)?;
}
Ok(())
}
fn fmt_bracketed(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_accession_with_optional_gene(f, &self.accession, self.gene_symbol.as_deref())?;
write!(f, ":g.[")?;
self.fmt_segments(f)?;
write!(f, "]")
}
}
impl fmt::Display for GenomeRing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_accession_with_optional_gene(f, &self.accession, self.gene_symbol.as_deref())?;
write!(f, ":g.")?;
self.fmt_segments(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CdsVariant {
pub accession: Accession,
pub gene_symbol: Option<String>,
pub loc_edit: LocEdit<CdsInterval, NaEdit>,
}
impl CdsVariant {
fn fmt_loc_edit(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(edit) = self.loc_edit.edit.inner() {
if edit.is_whole_entity_identity() || edit.is_whole_entity_unknown() {
return write!(f, "{}", self.loc_edit.edit);
}
}
if self.loc_edit.edit.is_uncertain() {
if let Some(edit) = self.loc_edit.edit.inner() {
return write!(f, "({}{})", self.loc_edit.location, edit);
}
}
write!(f, "{}", self.loc_edit)
}
}
impl fmt::Display for CdsVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_accession_with_optional_gene(f, &self.accession, self.gene_symbol.as_deref())?;
write!(f, ":c.")?;
self.fmt_loc_edit(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TxVariant {
pub accession: Accession,
pub gene_symbol: Option<String>,
pub loc_edit: LocEdit<TxInterval, NaEdit>,
}
impl TxVariant {
fn fmt_loc_edit(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(edit) = self.loc_edit.edit.inner() {
if edit.is_whole_entity_identity()
|| edit.is_whole_entity_unknown()
|| edit.is_no_product()
{
return write!(f, "{}", self.loc_edit.edit);
}
}
if self.loc_edit.edit.is_uncertain() {
if let Some(edit) = self.loc_edit.edit.inner() {
return write!(f, "({}{})", self.loc_edit.location, edit);
}
}
write!(f, "{}", self.loc_edit)
}
}
impl fmt::Display for TxVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_accession_with_optional_gene(f, &self.accession, self.gene_symbol.as_deref())?;
write!(f, ":n.")?;
self.fmt_loc_edit(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RnaVariant {
pub accession: Accession,
pub gene_symbol: Option<String>,
pub loc_edit: LocEdit<RnaInterval, NaEdit>,
}
impl RnaVariant {
fn fmt_loc_edit(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.loc_edit.edit {
Mu::Certain(edit) => {
if edit.is_whole_entity() {
write!(f, "{}", edit.to_rna_string())
} else {
write!(f, "{}{}", self.loc_edit.location, edit.to_rna_string())
}
}
Mu::Uncertain(edit) => {
if edit.is_whole_entity() {
write!(f, "({})", edit.to_rna_string())
} else {
write!(f, "({}{})", self.loc_edit.location, edit.to_rna_string())
}
}
Mu::Unknown => write!(f, "?"),
}
}
}
impl fmt::Display for RnaVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_accession_with_optional_gene(f, &self.accession, self.gene_symbol.as_deref())?;
write!(f, ":r.")?;
self.fmt_loc_edit(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ProteinVariant {
pub accession: Accession,
pub gene_symbol: Option<String>,
pub loc_edit: LocEdit<ProtInterval, ProteinEdit>,
}
impl ProteinVariant {
pub fn to_styled_string(&self, style: crate::hgvs::location::ProteinRenderStyle) -> String {
struct StyledProtein<'a>(
&'a ProteinVariant,
crate::hgvs::location::ProteinRenderStyle,
);
impl fmt::Display for StyledProtein<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt_styled(f, self.1)
}
}
format!("{}", StyledProtein(self, style))
}
fn fmt_styled(
&self,
f: &mut fmt::Formatter<'_>,
style: crate::hgvs::location::ProteinRenderStyle,
) -> fmt::Result {
write!(f, "{}:p.", self.accession)?;
self.fmt_loc_edit_styled(f, style)
}
fn fmt_loc_edit_styled(
&self,
f: &mut fmt::Formatter<'_>,
style: crate::hgvs::location::ProteinRenderStyle,
) -> fmt::Result {
use crate::hgvs::edit::StyledProteinEdit;
use crate::hgvs::interval::Interval;
use crate::hgvs::location::StyledProtPos;
let styled_location = |loc: &crate::hgvs::interval::ProtInterval| Interval {
start: loc.start.map_ref(|p| StyledProtPos(*p, style)),
end: loc.end.map_ref(|p| StyledProtPos(*p, style)),
};
let styled_edit = self.loc_edit.edit.map_ref(|e| StyledProteinEdit(e, style));
if let Some(edit) = self.loc_edit.edit.inner() {
if edit.is_whole_protein_identity()
|| edit.is_no_protein()
|| edit.is_whole_protein_unknown()
{
return write!(f, "{}", styled_edit);
}
}
if self.loc_edit.edit.is_uncertain() {
if let Some(edit) = self.loc_edit.edit.inner() {
return write!(
f,
"({}{})",
styled_location(&self.loc_edit.location),
StyledProteinEdit(edit, style)
);
}
}
write!(
f,
"{}{}",
styled_location(&self.loc_edit.location),
styled_edit
)
}
fn fmt_loc_edit(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.fmt_loc_edit_styled(f, crate::hgvs::location::ProteinRenderStyle::default())
}
}
impl fmt::Display for ProteinVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.fmt_styled(f, crate::hgvs::location::ProteinRenderStyle::default())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MtVariant {
pub accession: Accession,
pub gene_symbol: Option<String>,
pub loc_edit: LocEdit<GenomeInterval, NaEdit>,
}
impl MtVariant {
fn fmt_loc_edit(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(edit) = self.loc_edit.edit.inner() {
if edit.is_whole_entity_identity() || edit.is_whole_entity_unknown() {
return write!(f, "{}", self.loc_edit.edit);
}
}
if self.loc_edit.edit.is_uncertain() {
if let Some(edit) = self.loc_edit.edit.inner() {
return write!(f, "({}{})", self.loc_edit.location, edit);
}
}
write!(f, "{}", self.loc_edit)
}
}
impl fmt::Display for MtVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_accession_with_optional_gene(f, &self.accession, self.gene_symbol.as_deref())?;
write!(f, ":m.")?;
self.fmt_loc_edit(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CircularVariant {
pub accession: Accession,
pub gene_symbol: Option<String>,
pub loc_edit: LocEdit<GenomeInterval, NaEdit>,
}
impl CircularVariant {
fn fmt_loc_edit(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(edit) = self.loc_edit.edit.inner() {
if edit.is_whole_entity_identity() || edit.is_whole_entity_unknown() {
return write!(f, "{}", self.loc_edit.edit);
}
}
if self.loc_edit.edit.is_uncertain() {
if let Some(edit) = self.loc_edit.edit.inner() {
return write!(f, "({}{})", self.loc_edit.location, edit);
}
}
write!(f, "{}", self.loc_edit)
}
}
impl fmt::Display for CircularVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_accession_with_optional_gene(f, &self.accession, self.gene_symbol.as_deref())?;
write!(f, ":o.")?;
self.fmt_loc_edit(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RnaFusionBreakpoint {
pub accession: Accession,
pub gene_symbol: Option<String>,
pub interval: RnaInterval,
}
impl fmt::Display for RnaFusionBreakpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_accession_with_optional_gene(f, &self.accession, self.gene_symbol.as_deref())?;
write!(f, ":r.{}", self.interval)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RnaFusionVariant {
pub five_prime: RnaFusionBreakpoint,
pub three_prime: RnaFusionBreakpoint,
}
impl RnaFusionVariant {
pub fn new(five_prime: RnaFusionBreakpoint, three_prime: RnaFusionBreakpoint) -> Self {
Self {
five_prime,
three_prime,
}
}
}
impl fmt::Display for RnaFusionVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}::{}", self.five_prime, self.three_prime)
}
}
pub fn is_frameshift(variant: &HgvsVariant) -> bool {
match crate::python_helpers::get_indel_length(variant) {
Some(len) if len != 0 => len % 3 != 0,
_ => false,
}
}
pub fn is_frameshift_with_provider<P: crate::reference::provider::ReferenceProvider + ?Sized>(
variant: &HgvsVariant,
provider: &P,
) -> bool {
match crate::python_helpers::get_indel_length_with_provider(variant, provider) {
Some(len) if len != 0 => len % 3 != 0,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hgvs::edit::Base;
use crate::hgvs::location::GenomePos;
fn tx_downstream_range(accession: &str, start: i64, end: i64, edit: &str) -> HgvsVariant {
use crate::hgvs::interval::TxInterval;
use crate::hgvs::location::TxPos;
let mut variant = crate::hgvs::parser::variant::parse_variant(&format!(
"{accession}:n.{start}_{end}{edit}"
))
.expect("the template parses");
let HgvsVariant::Tx(tx) = &mut variant else {
unreachable!("an `n.` description parses as Tx")
};
tx.loc_edit.location = TxInterval::new(TxPos::downstream(start), TxPos::downstream(end));
variant
}
#[test]
fn test_accession_display() {
let acc = Accession::new("NM", "000088", Some(3));
assert_eq!(format!("{}", acc), "NM_000088.3");
let acc_no_version = Accession::new("NM", "000088", None);
assert_eq!(format!("{}", acc_no_version), "NM_000088");
let bare = format!("{acc}");
assert_eq!(
format!("{acc:>20}"),
bare,
"width and alignment are ignored"
);
assert_eq!(format!("{acc:*^20}"), bare, "fill is ignored");
assert_eq!(format!("{acc:.4}"), bare, "precision does not truncate");
assert_eq!(
format!("{acc:<8.2}"),
bare,
"nor do they combine to any effect"
);
assert_eq!(bare, acc.full());
}
#[test]
fn test_genome_variant_display() {
let variant = GenomeVariant {
accession: Accession::new("NC", "000001", Some(11)),
gene_symbol: None,
loc_edit: LocEdit::new(
GenomeInterval::point(GenomePos::new(12345)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
};
assert_eq!(format!("{}", variant), "NC_000001.11:g.12345A>G");
}
#[test]
fn test_cds_variant_display() {
use crate::hgvs::location::CdsPos;
let variant = CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(459)),
NaEdit::Deletion {
sequence: None,
length: None,
},
),
};
assert_eq!(format!("{}", variant), "NM_000088.3:c.459del");
}
#[test]
fn test_allele_cis_display() {
use crate::hgvs::location::CdsPos;
let var1 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
let var2 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(200)),
NaEdit::Substitution {
reference: Base::C,
alternative: Base::T,
},
),
});
let allele = AlleleVariant::cis(vec![var1, var2]);
let allele_variant = HgvsVariant::Allele(allele);
assert_eq!(
format!("{}", allele_variant),
"NM_000088.3:c.[100A>G;200C>T]"
);
}
#[test]
fn test_allele_trans_display() {
use crate::hgvs::location::CdsPos;
let var1 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
let var2 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(200)),
NaEdit::Substitution {
reference: Base::C,
alternative: Base::T,
},
),
});
let allele = AlleleVariant::trans(vec![var1, var2]);
let allele_variant = HgvsVariant::Allele(allele);
assert_eq!(
format!("{}", allele_variant),
"NM_000088.3:c.[100A>G];[200C>T]"
);
}
#[test]
fn test_allele_cis_three_variants() {
use crate::hgvs::location::CdsPos;
let make_var = |pos, alt| {
HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(pos)),
NaEdit::Substitution {
reference: Base::A,
alternative: alt,
},
),
})
};
let allele = AlleleVariant::cis(vec![
make_var(100, Base::G),
make_var(200, Base::C),
make_var(300, Base::T),
]);
assert_eq!(
format!("{}", HgvsVariant::Allele(allele)),
"NM_000088.3:c.[100A>G;200A>C;300A>T]"
);
}
#[test]
fn test_allele_trans_three_variants() {
use crate::hgvs::location::CdsPos;
let make_var = |pos, alt| {
HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(pos)),
NaEdit::Substitution {
reference: Base::A,
alternative: alt,
},
),
})
};
let allele = AlleleVariant::trans(vec![
make_var(100, Base::G),
make_var(200, Base::C),
make_var(300, Base::T),
]);
assert_eq!(
format!("{}", HgvsVariant::Allele(allele)),
"NM_000088.3:c.[100A>G];[200A>C];[300A>T]"
);
}
#[test]
fn test_allele_unknown_phase_compact_form() {
use crate::hgvs::location::CdsPos;
let make_var = |pos, alt| {
HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(pos)),
NaEdit::Substitution {
reference: Base::A,
alternative: alt,
},
),
})
};
let pair =
AlleleVariant::unknown_phase(vec![make_var(100, Base::G), make_var(200, Base::C)]);
assert_eq!(
format!("{}", HgvsVariant::Allele(pair)),
"NM_000088.3:c.100A>G(;)200A>C"
);
let triple = AlleleVariant::unknown_phase(vec![
make_var(100, Base::G),
make_var(200, Base::C),
make_var(300, Base::T),
]);
assert_eq!(
format!("{}", HgvsVariant::Allele(triple)),
"NM_000088.3:c.100A>G(;)200A>C(;)300A>T"
);
}
#[test]
fn test_allele_mixed_coordinate_types_uses_expanded_form() {
use crate::hgvs::location::{CdsPos, TxPos};
let coding = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
let noncoding = HgvsVariant::Tx(TxVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
TxInterval::point(TxPos::new(200)),
NaEdit::Substitution {
reference: Base::C,
alternative: Base::T,
},
),
});
let cis = AlleleVariant::cis(vec![coding.clone(), noncoding.clone()]);
assert_eq!(
format!("{}", HgvsVariant::Allele(cis)),
"[NM_000088.3:c.100A>G;NM_000088.3:n.200C>T]"
);
let trans = AlleleVariant::trans(vec![coding, noncoding]);
assert_eq!(
format!("{}", HgvsVariant::Allele(trans)),
"[NM_000088.3:c.100A>G];[NM_000088.3:n.200C>T]"
);
}
#[test]
fn test_allele_singleton_emits_bare_form() {
use crate::hgvs::location::CdsPos;
let make_var = || {
HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
})
};
let bare = "NM_000088.3:c.100A>G";
for allele in [
AlleleVariant::cis(vec![make_var()]),
AlleleVariant::trans(vec![make_var()]),
AlleleVariant::unknown_phase(vec![make_var()]),
AlleleVariant::mosaic(vec![make_var()]),
AlleleVariant::chimeric(vec![make_var()]),
] {
assert_eq!(format!("{}", HgvsVariant::Allele(allele)), bare);
}
}
#[test]
fn test_allele_mixed_accession_uses_expanded_form() {
use crate::hgvs::location::CdsPos;
let var1 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
let var2 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000099", Some(1)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(200)),
NaEdit::Substitution {
reference: Base::C,
alternative: Base::T,
},
),
});
let cis = AlleleVariant::cis(vec![var1.clone(), var2.clone()]);
assert_eq!(
format!("{}", HgvsVariant::Allele(cis)),
"[NM_000088.3:c.100A>G;NM_000099.1:c.200C>T]"
);
let trans = AlleleVariant::trans(vec![var1, var2]);
assert_eq!(
format!("{}", HgvsVariant::Allele(trans)),
"[NM_000088.3:c.100A>G];[NM_000099.1:c.200C>T]"
);
}
#[test]
fn test_allele_with_unknown_sub_variant_uses_expanded_form() {
use crate::hgvs::location::CdsPos;
let unknown = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(1)),
NaEdit::whole_entity_unknown(),
),
});
let concrete = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
let trans = AlleleVariant::trans(vec![concrete, unknown]);
assert_eq!(
format!("{}", HgvsVariant::Allele(trans)),
"[NM_000088.3:c.100A>G];[NM_000088.3:c.?]"
);
}
#[test]
fn test_allele_compact_form_transcript_ref_compacts_despite_gene_mismatch() {
use crate::hgvs::location::CdsPos;
let acc = Accession::new("NM", "000088", Some(3));
let v_a = HgvsVariant::Cds(CdsVariant {
accession: acc.clone(),
gene_symbol: Some("COL1A1".to_string()),
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
let v_b = HgvsVariant::Cds(CdsVariant {
accession: acc.clone(),
gene_symbol: Some("COL1A2".to_string()),
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(200)),
NaEdit::Substitution {
reference: Base::C,
alternative: Base::T,
},
),
});
let cis = AlleleVariant::cis(vec![v_a.clone(), v_b.clone()]);
assert_eq!(
format!("{}", HgvsVariant::Allele(cis)),
"NM_000088.3:c.[100A>G;200C>T]",
"mismatched gene symbols on a transcript reference must not block \
compaction (the selector is not displayed either way, #1058)"
);
let trans = AlleleVariant::trans(vec![v_a, v_b]);
assert_eq!(
format!("{}", HgvsVariant::Allele(trans)),
"NM_000088.3:c.[100A>G];[200C>T]",
"mismatched gene symbols on a transcript reference must not block \
trans compaction either (#1058)"
);
}
#[test]
fn test_allele_compact_form_transcript_ref_compacts_gene_some_vs_none() {
use crate::hgvs::location::CdsPos;
let acc = Accession::new("NM", "000088", Some(3));
let v_with = HgvsVariant::Cds(CdsVariant {
accession: acc.clone(),
gene_symbol: Some("COL1A1".to_string()),
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
let v_bare = HgvsVariant::Cds(CdsVariant {
accession: acc.clone(),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(200)),
NaEdit::Substitution {
reference: Base::C,
alternative: Base::T,
},
),
});
let cis = AlleleVariant::cis(vec![v_with, v_bare]);
assert_eq!(
format!("{}", HgvsVariant::Allele(cis)),
"NM_000088.3:c.[100A>G;200C>T]",
"Some/None gene_symbol mismatch on a transcript reference must not \
block compaction (#1058)"
);
}
#[test]
fn test_allele_compact_form_nontranscript_ref_requires_matching_gene_symbol() {
use crate::hgvs::location::CdsPos;
let acc = Accession::new("NG", "007400", Some(1));
let v_a = HgvsVariant::Cds(CdsVariant {
accession: acc.clone(),
gene_symbol: Some("COL1A1".to_string()),
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
let v_b = HgvsVariant::Cds(CdsVariant {
accession: acc.clone(),
gene_symbol: Some("COL1A2".to_string()),
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(200)),
NaEdit::Substitution {
reference: Base::C,
alternative: Base::T,
},
),
});
let cis = AlleleVariant::cis(vec![v_a, v_b]);
assert_eq!(
format!("{}", HgvsVariant::Allele(cis)),
"[NG_007400.1(COL1A1):c.100A>G;NG_007400.1(COL1A2):c.200C>T]",
"mismatched gene symbols on a non-transcript reference must still \
use the expanded form so both selectors survive"
);
}
#[test]
fn test_allele_display_idempotent_mismatched_gene_transcript_ref() {
use crate::hgvs::location::CdsPos;
use crate::parse_hgvs;
let acc = Accession::new("NM", "000088", Some(3));
let v_a = HgvsVariant::Cds(CdsVariant {
accession: acc.clone(),
gene_symbol: Some("COL1A1".to_string()),
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
let v_b = HgvsVariant::Cds(CdsVariant {
accession: acc.clone(),
gene_symbol: Some("COL1A2".to_string()),
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(200)),
NaEdit::Substitution {
reference: Base::C,
alternative: Base::T,
},
),
});
for allele in [
AlleleVariant::cis(vec![v_a.clone(), v_b.clone()]),
AlleleVariant::trans(vec![v_a.clone(), v_b.clone()]),
] {
let first = format!("{}", HgvsVariant::Allele(allele));
let reparsed = parse_hgvs(&first).expect("first Display must reparse");
let second = format!("{}", reparsed);
assert_eq!(
first, second,
"Display -> parse -> Display must be stable for a mismatched-gene \
allele on a transcript reference (#1058)"
);
}
}
#[test]
fn test_allele_compact_form_keeps_compact_when_all_match() {
use crate::hgvs::location::CdsPos;
let acc = Accession::new("NM", "000088", Some(3));
let mk = |pos: i64, alt: Base| {
HgvsVariant::Cds(CdsVariant {
accession: acc.clone(),
gene_symbol: Some("COL1A1".to_string()),
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(pos)),
NaEdit::Substitution {
reference: Base::A,
alternative: alt,
},
),
})
};
let cis = AlleleVariant::cis(vec![mk(100, Base::G), mk(200, Base::T)]);
assert_eq!(
format!("{}", HgvsVariant::Allele(cis)),
"NM_000088.3:c.[100A>G;200A>T]",
"matching gene symbols keep compact form; the selector itself is \
dropped on a transcript reference (#1051)"
);
let mk_none = |pos: i64, alt: Base| {
HgvsVariant::Cds(CdsVariant {
accession: acc.clone(),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(pos)),
NaEdit::Substitution {
reference: Base::A,
alternative: alt,
},
),
})
};
let cis_none = AlleleVariant::cis(vec![mk_none(100, Base::G), mk_none(200, Base::T)]);
assert_eq!(
format!("{}", HgvsVariant::Allele(cis_none)),
"NM_000088.3:c.[100A>G;200A>T]",
"all-None gene_symbol must use compact form unchanged"
);
}
#[test]
fn test_allele_null_variant_singleton_delegates() {
let null = HgvsVariant::NullAllele;
let cis = AlleleVariant::cis(vec![null]);
assert_eq!(format!("{}", HgvsVariant::Allele(cis)), "0");
}
#[test]
fn test_allele_accession() {
use crate::hgvs::location::CdsPos;
let var1 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Deletion {
sequence: None,
length: None,
},
),
});
let allele = AlleleVariant::cis(vec![var1]);
let allele_variant = HgvsVariant::Allele(allele);
assert_eq!(
&*allele_variant
.accession()
.expect("Expected accession")
.prefix,
"NM"
);
assert!(allele_variant.is_allele());
}
#[test]
fn test_allele_mosaic_display() {
use crate::hgvs::location::CdsPos;
let var1 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
let var2 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(200)),
NaEdit::Substitution {
reference: Base::C,
alternative: Base::T,
},
),
});
let allele = AlleleVariant::mosaic(vec![var1, var2]);
let allele_variant = HgvsVariant::Allele(allele);
assert_eq!(
format!("{}", allele_variant),
"NM_000088.3:c.100A>G/NM_000088.3:c.200C>T"
);
}
#[test]
fn test_allele_chimeric_display() {
use crate::hgvs::location::CdsPos;
let var1 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
let var2 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(200)),
NaEdit::Substitution {
reference: Base::C,
alternative: Base::T,
},
),
});
let allele = AlleleVariant::chimeric(vec![var1, var2]);
let allele_variant = HgvsVariant::Allele(allele);
assert_eq!(
format!("{}", allele_variant),
"NM_000088.3:c.100A>G//NM_000088.3:c.200C>T"
);
}
#[test]
fn test_allele_phase_variants() {
assert!(!AllelePhase::Cis.is_mosaic());
assert!(!AllelePhase::Trans.is_mosaic());
assert!(!AllelePhase::Unknown.is_mosaic());
assert!(AllelePhase::Mosaic.is_mosaic());
assert!(!AllelePhase::Chimeric.is_mosaic());
assert!(!AllelePhase::AndOr.is_mosaic());
assert!(!AllelePhase::Products.is_mosaic());
assert!(!AllelePhase::Cis.is_chimeric());
assert!(!AllelePhase::Trans.is_chimeric());
assert!(!AllelePhase::Unknown.is_chimeric());
assert!(!AllelePhase::Mosaic.is_chimeric());
assert!(AllelePhase::Chimeric.is_chimeric());
assert!(!AllelePhase::AndOr.is_chimeric());
assert!(!AllelePhase::Products.is_chimeric());
}
#[test]
fn test_accession_new() {
let acc = Accession::new("NM", "000088", Some(3));
assert_eq!(&*acc.prefix, "NM");
assert_eq!(&*acc.number, "000088");
assert_eq!(acc.version, Some(3));
assert!(!acc.ensembl_style);
}
#[test]
fn test_accession_with_style() {
let acc = Accession::with_style("ENST", "00000012345", Some(1), true);
assert!(acc.ensembl_style);
assert_eq!(format!("{}", acc), "ENST00000012345.1");
}
#[test]
fn test_accession_from_assembly() {
let acc = Accession::from_assembly("GRCh37", "chr1");
assert!(acc.is_assembly_ref());
assert_eq!(format!("{}", acc), "GRCh37(chr1)");
}
#[test]
fn test_accession_is_ensembl_prefix() {
assert!(Accession::is_ensembl_prefix("ENST"));
assert!(Accession::is_ensembl_prefix("ENSG"));
assert!(Accession::is_ensembl_prefix("ENSP"));
assert!(Accession::is_ensembl_prefix("ENSE"));
assert!(Accession::is_ensembl_prefix("ENSR"));
assert!(Accession::is_ensembl_prefix("ENSMUST"));
assert!(Accession::is_ensembl_prefix("ENSMUSG"));
assert!(Accession::is_ensembl_prefix("ENSMUSP"));
assert!(Accession::is_ensembl_prefix("ENSMUSE"));
assert!(Accession::is_ensembl_prefix("ENSMUSR"));
assert!(Accession::is_ensembl_prefix("ENSRNOT"));
assert!(Accession::is_ensembl_prefix("ENSBTAG"));
assert!(Accession::is_ensembl_prefix("ENSDARP"));
assert!(Accession::is_ensembl_prefix("ENSCAFT"));
assert!(Accession::is_ensembl_prefix("ENSSSCG"));
assert!(!Accession::is_ensembl_prefix("NM"));
assert!(!Accession::is_ensembl_prefix("NC"));
assert!(!Accession::is_ensembl_prefix("ENS")); assert!(!Accession::is_ensembl_prefix("ENSA")); assert!(!Accession::is_ensembl_prefix("ENSmust")); assert!(!Accession::is_ensembl_prefix("ENSMUS1T")); assert!(!Accession::is_ensembl_prefix("")); assert!(!Accession::is_ensembl_prefix("ANYTHING"));
assert!(Accession::is_ensembl_prefix("ENSAAAAAT")); assert!(!Accession::is_ensembl_prefix("ENSAAAAAAT")); assert!(!Accession::is_ensembl_prefix("ENSAAAAAAAAAT")); }
#[test]
fn test_accession_is_ensembl() {
let ensembl = Accession::new("ENST", "00000012345", Some(1));
assert!(ensembl.is_ensembl());
let refseq = Accession::new("NM", "000088", Some(3));
assert!(!refseq.is_ensembl());
}
#[test]
fn test_accession_validate_ensembl() {
let valid = Accession::new("ENST", "00000012345", Some(1));
assert!(valid.validate_ensembl());
let refseq = Accession::new("NM", "000088", Some(3));
assert!(refseq.validate_ensembl());
}
#[test]
fn test_accession_inferred_variant_type() {
assert_eq!(
Accession::new("NC", "000001", Some(11)).inferred_variant_type(),
Some("g")
);
assert_eq!(
Accession::new("NG", "012345", Some(1)).inferred_variant_type(),
Some("g")
);
assert_eq!(
Accession::new("NM", "000088", Some(3)).inferred_variant_type(),
Some("c")
);
assert_eq!(
Accession::new("NR", "123456", Some(1)).inferred_variant_type(),
Some("n")
);
assert_eq!(
Accession::new("NP", "000079", Some(2)).inferred_variant_type(),
Some("p")
);
assert_eq!(
Accession::new("ENST", "00000012345", Some(1)).inferred_variant_type(),
Some("c")
);
assert_eq!(
Accession::new("ENSG", "00000012345", Some(1)).inferred_variant_type(),
Some("g")
);
assert_eq!(
Accession::new("ENSP", "00000012345", Some(1)).inferred_variant_type(),
Some("p")
);
assert_eq!(
Accession::new("ENSMUST", "00000012345", Some(1)).inferred_variant_type(),
Some("c")
);
assert_eq!(
Accession::new("ENSRNOT", "00000012345", Some(1)).inferred_variant_type(),
Some("c")
);
assert_eq!(
Accession::new("ENSMUSG", "00000012345", Some(1)).inferred_variant_type(),
Some("g")
);
assert_eq!(
Accession::new("ENSMUSP", "00000012345", Some(1)).inferred_variant_type(),
Some("p")
);
assert_eq!(
Accession::new("ENSE", "00000012345", Some(1)).inferred_variant_type(),
None
);
assert_eq!(
Accession::new("ENSMUSE", "00000012345", Some(1)).inferred_variant_type(),
None
);
assert_eq!(
Accession::new("ENSGT", "00000012345", Some(1)).inferred_variant_type(),
None
);
assert_eq!(
Accession::new("LRG", "1", None).inferred_variant_type(),
Some("g")
);
assert_eq!(
Accession::new("LRG", "1t1", None).inferred_variant_type(),
Some("c")
);
assert_eq!(
Accession::new("LRG", "292t1", None).inferred_variant_type(),
Some("c")
);
assert_eq!(
Accession::new("LRG", "1p1", None).inferred_variant_type(),
Some("p")
);
assert_eq!(
Accession::new("LRG", "673p1", None).inferred_variant_type(),
Some("p")
);
assert_eq!(
Accession::new("P", "12345", None).inferred_variant_type(),
Some("p")
);
assert_eq!(
Accession::new("XX", "12345", None).inferred_variant_type(),
None
);
}
#[test]
fn test_accession_is_transcript_reference() {
assert!(Accession::new("NM", "000088", Some(3)).is_transcript_reference());
assert!(Accession::new("NR", "046018", Some(2)).is_transcript_reference());
assert!(Accession::new("XM", "011535467", Some(1)).is_transcript_reference());
assert!(Accession::new("XR", "001737893", Some(1)).is_transcript_reference());
assert!(Accession::new("ENST", "00000012345", Some(1)).is_transcript_reference());
assert!(Accession::new("ENSMUST", "00000012345", Some(1)).is_transcript_reference());
assert!(Accession::new("ENSRNOT", "00000012345", Some(1)).is_transcript_reference());
assert!(Accession::new("ENSBTAT", "00000012345", Some(1)).is_transcript_reference());
assert!(Accession::new("LRG", "199t1", None).is_transcript_reference());
assert!(!Accession::new("NC", "000013", Some(11)).is_transcript_reference());
assert!(!Accession::new("NG", "012345", Some(1)).is_transcript_reference());
assert!(!Accession::new("NP", "000079", Some(2)).is_transcript_reference());
assert!(!Accession::new("ENSG", "00000012345", Some(1)).is_transcript_reference());
assert!(!Accession::new("ENSP", "00000012345", Some(1)).is_transcript_reference());
assert!(!Accession::new("ENSMUSG", "00000012345", Some(1)).is_transcript_reference());
assert!(!Accession::new("ENSMUSP", "00000012345", Some(1)).is_transcript_reference());
assert!(!Accession::new("ENSE", "00000012345", Some(1)).is_transcript_reference());
assert!(!Accession::new("ENSGT", "00000012345", Some(1)).is_transcript_reference());
assert!(!Accession::is_ensembl_transcript_prefix("ENSGT"));
assert!(!Accession::new("LRG", "199", None).is_transcript_reference());
assert!(!Accession::new("XX", "12345", None).is_transcript_reference());
}
#[test]
fn test_accession_is_uniprot() {
let uniprot = Accession::new("P", "12345", None);
assert!(uniprot.is_uniprot());
let not_uniprot = Accession::new("NM", "000088", Some(3));
assert!(!not_uniprot.is_uniprot());
let wrong_length = Accession::new("P", "123", None);
assert!(!wrong_length.is_uniprot());
}
#[test]
fn test_accession_is_mitochondrial() {
assert!(Accession::new("NC", "012920", Some(1)).is_mitochondrial());
assert!(Accession::new("NC", "001807", Some(1)).is_mitochondrial());
assert!(Accession::new("NC", "012920", None).is_mitochondrial());
assert!(!Accession::new("NC", "000001", Some(11)).is_mitochondrial());
assert!(!Accession::new("NM", "012920", Some(1)).is_mitochondrial());
}
#[test]
fn test_accession_is_lrg_prefix() {
assert!(Accession::is_lrg_prefix("LRG"));
assert!(!Accession::is_lrg_prefix("lrg"));
assert!(!Accession::is_lrg_prefix("LRG_"));
assert!(!Accession::is_lrg_prefix("NM"));
assert!(!Accession::is_lrg_prefix(""));
}
#[test]
fn test_accession_is_lrg() {
assert!(Accession::new("LRG", "1", None).is_lrg());
assert!(Accession::new("LRG", "1t1", None).is_lrg());
assert!(Accession::new("LRG", "1p1", None).is_lrg());
assert!(!Accession::new("NM", "000088", Some(3)).is_lrg());
assert!(!Accession::new("NC", "000001", Some(11)).is_lrg());
}
#[test]
fn test_accession_base() {
let acc = Accession::new("NM", "000088", Some(3));
assert_eq!(acc.base(), "NM_000088");
let ensembl = Accession::with_style("ENST", "00000012345", Some(1), true);
assert_eq!(ensembl.base(), "ENST00000012345");
let assembly = Accession::from_assembly("GRCh38", "chrX");
assert_eq!(assembly.base(), "GRCh38(chrX)");
}
#[test]
fn test_accession_full() {
let acc = Accession::new("NM", "000088", Some(3));
assert_eq!(acc.full(), "NM_000088.3");
let no_version = Accession::new("NM", "000088", None);
assert_eq!(no_version.full(), "NM_000088");
let ensembl = Accession::with_style("ENST", "00000012345", Some(1), true);
assert_eq!(ensembl.full(), "ENST00000012345.1");
}
#[test]
fn test_loc_edit_new() {
use crate::hgvs::location::GenomePos;
let loc_edit: LocEdit<GenomeInterval, NaEdit> = LocEdit::new(
GenomeInterval::point(GenomePos::new(100)),
NaEdit::Deletion {
sequence: None,
length: None,
},
);
assert!(matches!(loc_edit.edit, Mu::Certain(_)));
}
#[test]
fn test_loc_edit_with_uncertainty() {
use crate::hgvs::location::GenomePos;
let loc_edit: LocEdit<GenomeInterval, NaEdit> = LocEdit::with_uncertainty(
GenomeInterval::point(GenomePos::new(100)),
Mu::Uncertain(NaEdit::Deletion {
sequence: None,
length: None,
}),
);
assert!(matches!(loc_edit.edit, Mu::Uncertain(_)));
}
#[test]
fn test_allele_phase_display() {
assert_eq!(format!("{}", AllelePhase::Cis), "cis");
assert_eq!(format!("{}", AllelePhase::Trans), "trans");
assert_eq!(format!("{}", AllelePhase::Unknown), "unknown");
assert_eq!(format!("{}", AllelePhase::Mosaic), "mosaic");
assert_eq!(format!("{}", AllelePhase::Chimeric), "chimeric");
assert_eq!(format!("{}", AllelePhase::AndOr), "and/or");
assert_eq!(format!("{}", AllelePhase::Products), "products");
}
#[test]
fn test_allele_variant_unknown_phase() {
use crate::hgvs::location::CdsPos;
let var1 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
let var2 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(200)),
NaEdit::Substitution {
reference: Base::C,
alternative: Base::T,
},
),
});
let allele = AlleleVariant::unknown_phase(vec![var1, var2]);
assert_eq!(allele.phase, AllelePhase::Unknown);
}
#[test]
fn test_hgvs_variant_accession() {
let variant = HgvsVariant::Genome(GenomeVariant {
accession: Accession::new("NC", "000001", Some(11)),
gene_symbol: None,
loc_edit: LocEdit::new(
GenomeInterval::point(GenomePos::new(12345)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
let acc = variant.accession().expect("Expected accession");
assert_eq!(&*acc.prefix, "NC");
assert_eq!(&*acc.number, "000001");
}
#[test]
fn test_hgvs_variant_is_allele() {
use crate::hgvs::location::CdsPos;
let var1 = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
assert!(!var1.is_allele());
let allele = AlleleVariant::cis(vec![var1.clone()]);
let allele_variant = HgvsVariant::Allele(allele);
assert!(allele_variant.is_allele());
}
#[test]
fn test_hgvs_variant_protein_type() {
use crate::hgvs::location::{AminoAcid, ProtPos};
let protein = HgvsVariant::Protein(ProteinVariant {
accession: Accession::new("NP", "000079", Some(2)),
gene_symbol: None,
loc_edit: LocEdit::new(
ProtInterval::point(ProtPos::new(AminoAcid::Val, 600)),
ProteinEdit::Substitution {
reference: AminoAcid::Val,
alternative: AminoAcid::Glu,
},
),
});
assert_eq!(protein.variant_type(), "p");
assert!(matches!(protein, HgvsVariant::Protein(_)));
let genomic = HgvsVariant::Genome(GenomeVariant {
accession: Accession::new("NC", "000001", Some(11)),
gene_symbol: None,
loc_edit: LocEdit::new(
GenomeInterval::point(GenomePos::new(12345)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
assert_eq!(genomic.variant_type(), "g");
assert!(!matches!(genomic, HgvsVariant::Protein(_)));
}
#[test]
fn test_hgvs_variant_variant_type() {
let genomic = HgvsVariant::Genome(GenomeVariant {
accession: Accession::new("NC", "000001", Some(11)),
gene_symbol: None,
loc_edit: LocEdit::new(
GenomeInterval::point(GenomePos::new(12345)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
assert_eq!(genomic.variant_type(), "g");
use crate::hgvs::location::CdsPos;
let cds = HgvsVariant::Cds(CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: None,
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
});
assert_eq!(cds.variant_type(), "c");
}
#[test]
fn test_cds_variant_with_gene_symbol() {
use crate::hgvs::location::CdsPos;
let variant = CdsVariant {
accession: Accession::new("NM", "000088", Some(3)),
gene_symbol: Some("COL1A1".to_string()),
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(100)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
};
assert_eq!(variant.gene_symbol, Some("COL1A1".to_string()));
let display = format!("{}", variant);
assert_eq!(display, "NM_000088.3:c.100A>G");
}
fn cds_sub_with_gene(accession: Accession, gene_symbol: &str, position: i64) -> CdsVariant {
use crate::hgvs::location::CdsPos;
CdsVariant {
accession,
gene_symbol: Some(gene_symbol.to_string()),
loc_edit: LocEdit::new(
CdsInterval::point(CdsPos::new(position)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
}
}
#[test]
fn test_gene_selector_suppressed_on_compound_reference_with_custom_transcript() {
let accession = Accession::with_style("Template-gene", "", Some(1), true)
.with_genomic_context(Accession::with_style("Template", "", None, true));
let variant = cds_sub_with_gene(accession, "GENE1", 5);
assert_eq!(variant.gene_symbol, Some("GENE1".to_string()));
assert_eq!(format!("{}", variant), "Template(Template-gene.1):c.5A>G");
}
#[test]
fn test_gene_selector_suppressed_on_compound_reference_with_nontranscript_inner() {
let accession = Accession::new("NG", "012232", Some(1))
.with_genomic_context(Accession::new("NC", "000013", Some(11)));
let variant = cds_sub_with_gene(accession, "BRCA1", 100);
assert_eq!(format!("{}", variant), "NC_000013.11(NG_012232.1):c.100A>G");
}
#[test]
fn test_protein_allele_with_gene_mismatch_compacts() {
use crate::hgvs::edit::ProteinEdit;
use crate::hgvs::interval::ProtInterval;
use crate::hgvs::location::{AminoAcid, ProtPos};
let member = |gene: &str, pos: u64| {
HgvsVariant::Protein(ProteinVariant {
accession: Accession::new("NP", "003997", Some(2)),
gene_symbol: Some(gene.to_string()),
loc_edit: LocEdit::new_predicted(
ProtInterval::point(ProtPos::new(AminoAcid::Phe, pos)),
ProteinEdit::Identity {
predicted: true,
whole_protein: false,
},
),
})
};
let allele = HgvsVariant::Allele(AlleleVariant::cis(vec![
member("GENE1", 41),
member("GENE2", 47),
]));
assert_eq!(
allele.to_string(),
"NP_003997.2:p.[(Phe41(=));(Phe47(=))]",
"an invisible selector mismatch must not block compaction",
);
}
#[test]
fn test_gene_selector_preserved_on_bare_genomic_reference() {
let variant = cds_sub_with_gene(Accession::new("NC", "000023", Some(10)), "DMD", 5);
assert_eq!(format!("{}", variant), "NC_000023.10(DMD):c.5A>G");
}
#[test]
fn test_allele_compact_form_compound_custom_ref_compacts_despite_gene_mismatch() {
let accession = Accession::with_style("Template-gene", "", Some(1), true)
.with_genomic_context(Accession::with_style("Template", "", None, true));
let cis = AlleleVariant::cis(vec![
HgvsVariant::Cds(cds_sub_with_gene(accession.clone(), "GENE1", 100)),
HgvsVariant::Cds(cds_sub_with_gene(accession, "GENE2", 200)),
]);
assert_eq!(
format!("{}", HgvsVariant::Allele(cis)),
"Template(Template-gene.1):c.[100A>G;200A>G]"
);
}
#[test]
fn test_genome_variant_with_gene_symbol() {
let variant = GenomeVariant {
accession: Accession::new("NC", "000001", Some(11)),
gene_symbol: Some("BRCA1".to_string()),
loc_edit: LocEdit::new(
GenomeInterval::point(GenomePos::new(12345)),
NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
},
),
};
assert_eq!(variant.gene_symbol, Some("BRCA1".to_string()));
let display = format!("{}", variant);
assert_eq!(display, "NC_000001.11(BRCA1):g.12345A>G");
}
#[test]
fn test_transcript_accession_bare() {
let acc = Accession::new("NM", "004119", Some(3));
assert_eq!(acc.transcript_accession(), "NM_004119.3");
assert_eq!(acc.full(), "NM_004119.3");
}
#[test]
fn test_transcript_accession_with_genomic_context() {
let inner = Accession::new("NM", "004119", Some(3));
let outer = Accession::new("NC", "000013", Some(11));
let compound = inner.with_genomic_context(outer);
assert_eq!(compound.full(), "NC_000013.11(NM_004119.3)");
assert_eq!(compound.transcript_accession(), "NM_004119.3");
}
#[test]
fn test_transcript_accession_assembly_ref() {
let acc = Accession::from_assembly("GRCh38", "chr1");
assert_eq!(acc.transcript_accession(), "GRCh38(chr1)");
}
#[test]
fn test_transcript_accession_ensembl_with_context() {
let inner = Accession::with_style("ENST", "00000241453", Some(7), true);
let outer = Accession::new("NC", "000013", Some(11));
let compound = inner.with_genomic_context(outer);
assert_eq!(compound.full(), "NC_000013.11(ENST00000241453.7)");
assert_eq!(compound.transcript_accession(), "ENST00000241453.7");
}
use crate::hgvs::parser::variant::parse_variant;
#[test]
fn test_coordinate_axis_leaf_kinds() {
let cases = [
("NM_000088.3:c.459A>G", CoordinateAxis::Coding),
("NC_000001.11:g.12345A>G", CoordinateAxis::Genomic),
("NR_003286.2:n.100A>G", CoordinateAxis::NonCoding),
("NM_000088.3:r.100a>g", CoordinateAxis::Rna),
("NP_000079.2:p.Arg8Gln", CoordinateAxis::Protein),
("NC_012920.1:m.100A>G", CoordinateAxis::Mitochondrial),
];
for (input, expected) in cases {
let v = parse_variant(input).unwrap();
assert_eq!(
v.coordinate_axis(),
Some(expected),
"{input} should resolve to axis {expected:?}"
);
}
}
#[test]
fn test_coordinate_axis_allele_shares_member_axis() {
let coding = parse_variant("NM_000000.1:c.[6G>C;16_18del]").unwrap();
assert_eq!(coding.coordinate_axis(), Some(CoordinateAxis::Coding));
let genomic = parse_variant("NC_000000.1:g.[6G>C;16_18del]").unwrap();
assert_eq!(genomic.coordinate_axis(), Some(CoordinateAxis::Genomic));
let protein = parse_variant("NP_000000.1:p.[(Arg8Gln);(Ser10Gly)]").unwrap();
assert_eq!(protein.coordinate_axis(), Some(CoordinateAxis::Protein));
}
#[test]
fn test_coordinate_axis_mixed_allele_is_none() {
let cds = parse_variant("NM_004006.2:c.100_150del").unwrap();
let tx = parse_variant("NM_004006.2:n.100_150dup").unwrap();
let mixed = HgvsVariant::Allele(AlleleVariant::new(vec![cds, tx], AllelePhase::Cis));
assert_eq!(mixed.coordinate_axis(), None);
}
#[test]
fn test_coordinate_axis_markers_contribute_no_axis() {
assert_eq!(HgvsVariant::NullAllele.coordinate_axis(), None);
assert_eq!(HgvsVariant::UnknownAllele.coordinate_axis(), None);
let concrete = parse_variant("NM_000088.3:c.459A>G").unwrap();
let with_marker = HgvsVariant::Allele(AlleleVariant::new(
vec![concrete, HgvsVariant::UnknownAllele],
AllelePhase::Trans,
));
assert_eq!(with_marker.coordinate_axis(), Some(CoordinateAxis::Coding));
}
#[test]
fn test_coordinate_axis_nested_allele() {
let a = parse_variant("NM_000088.3:c.459A>G").unwrap();
let b = parse_variant("NM_000088.3:c.500A>G").unwrap();
let inner = HgvsVariant::Allele(AlleleVariant::new(vec![a, b], AllelePhase::Cis));
let outer = HgvsVariant::Allele(AlleleVariant::new(vec![inner], AllelePhase::Cis));
assert_eq!(outer.coordinate_axis(), Some(CoordinateAxis::Coding));
}
#[test]
fn test_coordinate_axis_genome_ring_is_genomic() {
let ring = parse_variant("NC_000022.11:g.pter_1000del::2000_qterdel").unwrap();
assert!(
matches!(ring, HgvsVariant::GenomeRing(_)),
"expected a GenomeRing, got {ring:?}"
);
assert_eq!(ring.coordinate_axis(), Some(CoordinateAxis::Genomic));
}
#[test]
fn test_coordinate_axis_enum_groupings() {
assert_eq!(CoordinateAxis::Coding.code(), "c");
assert_eq!(CoordinateAxis::Genomic.code(), "g");
assert_eq!(CoordinateAxis::NonCoding.code(), "n");
assert_eq!(CoordinateAxis::Rna.code(), "r");
assert_eq!(CoordinateAxis::Protein.code(), "p");
assert_eq!(CoordinateAxis::Mitochondrial.code(), "m");
assert_eq!(CoordinateAxis::Circular.code(), "o");
for dna in [
CoordinateAxis::Genomic,
CoordinateAxis::Coding,
CoordinateAxis::NonCoding,
CoordinateAxis::Mitochondrial,
CoordinateAxis::Circular,
] {
assert!(dna.is_dna(), "{dna:?} should be DNA");
assert!(!dna.is_rna());
assert!(!dna.is_protein());
}
assert!(CoordinateAxis::Rna.is_rna());
assert!(!CoordinateAxis::Rna.is_dna());
assert!(CoordinateAxis::Protein.is_protein());
assert!(!CoordinateAxis::Protein.is_dna());
}
#[test]
fn test_self_cancelling_detect_spec_example() {
let del = parse_variant("NM_004006.2:c.762_768del").unwrap();
let dup = parse_variant("NM_004006.2:c.767_774dup").unwrap();
let pair = AlleleVariant::detect_self_cancelling_pair(&[del, dup]);
assert!(
pair.is_some(),
"spec-example overlapping del+dup must be detected"
);
let (dl, du) = pair.unwrap();
assert_eq!(dl, 0, "first index is the del");
assert_eq!(du, 1, "second index is the dup");
}
#[test]
fn test_self_cancelling_no_overlap() {
let del = parse_variant("NM_004006.2:c.100_110del").unwrap();
let dup = parse_variant("NM_004006.2:c.500_510dup").unwrap();
assert!(AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_none());
}
#[test]
fn test_self_cancelling_two_dels() {
let a = parse_variant("NM_004006.2:c.100_110del").unwrap();
let b = parse_variant("NM_004006.2:c.105_115del").unwrap();
assert!(AlleleVariant::detect_self_cancelling_pair(&[a, b]).is_none());
}
#[test]
fn test_self_cancelling_different_accessions() {
let del = parse_variant("NM_004006.2:c.100_110del").unwrap();
let dup = parse_variant("NM_000088.3:c.105_115dup").unwrap();
assert!(AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_none());
}
#[test]
fn test_self_cancelling_dup_first_then_del() {
let dup = parse_variant("NM_004006.2:c.767_774dup").unwrap();
let del = parse_variant("NM_004006.2:c.762_768del").unwrap();
let pair = AlleleVariant::detect_self_cancelling_pair(&[dup, del]);
assert!(pair.is_some());
let (dl, du) = pair.unwrap();
assert_eq!(dl, 1, "del is at index 1");
assert_eq!(du, 0, "dup is at index 0");
}
#[test]
fn test_self_cancelling_genomic_overlap() {
let del = parse_variant("NC_000017.11:g.43044295_43044300del").unwrap();
let dup = parse_variant("NC_000017.11:g.43044298_43044305dup").unwrap();
assert!(AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_some());
}
#[test]
fn test_self_cancelling_cds_3utr_star_overlap() {
let del = parse_variant("NM_004006.2:c.*100_*150del").unwrap();
let dup = parse_variant("NM_004006.2:c.*145_*160dup").unwrap();
let pair = AlleleVariant::detect_self_cancelling_pair(&[del, dup]);
assert!(
pair.is_some(),
"overlapping 3'UTR del+dup must be detected (#172)"
);
}
#[test]
fn test_self_cancelling_cds_3utr_star_no_overlap() {
let del = parse_variant("NM_004006.2:c.*100_*110del").unwrap();
let dup = parse_variant("NM_004006.2:c.*200_*210dup").unwrap();
assert!(AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_none());
}
#[test]
fn test_self_cancelling_cds_intronic_offset_overlap() {
let del = parse_variant("NM_004006.2:c.100+5_100+15del").unwrap();
let dup = parse_variant("NM_004006.2:c.100+12_100+20dup").unwrap();
assert!(
AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_some(),
"overlapping intronic del+dup must be detected (#172)"
);
}
#[test]
fn test_self_cancelling_cds_intronic_offset_no_overlap() {
let del = parse_variant("NM_004006.2:c.100+5_100+10del").unwrap();
let dup = parse_variant("NM_004006.2:c.100+15_100+20dup").unwrap();
assert!(AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_none());
}
#[test]
fn test_self_cancelling_tx_intronic_overlap() {
let del = parse_variant("NR_001234.1:n.100+5_100+15del").unwrap();
let dup = parse_variant("NR_001234.1:n.100+12_100+20dup").unwrap();
assert!(
AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_some(),
"overlapping tx intronic del+dup must be detected (#172)"
);
}
#[test]
fn test_self_cancelling_cds_cross_region_no_overlap() {
let del = parse_variant("NM_004006.2:c.-50_-40del").unwrap();
let dup = parse_variant("NM_004006.2:c.*40_*50dup").unwrap();
assert!(AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_none());
}
#[test]
fn test_parse_self_cancelling_cds_3utr_rejected_via_parser() {
let result = parse_variant("NM_004006.2:c.[*100_*150del;*145_*160dup]");
let err = result.expect_err("3'UTR overlapping del+dup must reject");
assert_eq!(
err.code(),
Some(crate::error::ErrorCode::SelfCancellingAllele)
);
}
#[test]
fn test_parse_self_cancelling_cds_intronic_rejected_via_parser() {
let result = parse_variant("NM_004006.2:c.[100+5_100+15del;100+12_100+20dup]");
let err = result.expect_err("intronic overlapping del+dup must reject");
assert_eq!(
err.code(),
Some(crate::error::ErrorCode::SelfCancellingAllele)
);
}
#[test]
fn test_self_cancelling_cds_5utr_overlap() {
let del = parse_variant("NM_004006.2:c.-50_-30del").unwrap();
let dup = parse_variant("NM_004006.2:c.-35_-20dup").unwrap();
assert!(
AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_some(),
"overlapping 5'UTR del+dup must be detected (#172)"
);
}
#[test]
fn test_self_cancelling_cds_negative_intronic_offset_overlap() {
let del = parse_variant("NM_004006.2:c.100-15_100-5del").unwrap();
let dup = parse_variant("NM_004006.2:c.100-10_100-2dup").unwrap();
assert!(
AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_some(),
"overlapping negative-intronic-offset del+dup must be detected"
);
}
#[test]
fn test_self_cancelling_cds_cross_exon_intronic_range_ok() {
let del = parse_variant("NM_004006.2:c.99+1_99+10del").unwrap();
let dup = parse_variant("NM_004006.2:c.100-10_100-1dup").unwrap();
assert!(
AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_none(),
"non-overlapping cross-exon intronic ranges must NOT collide"
);
}
#[test]
fn test_self_cancelling_rna_intronic_overlap() {
let del = parse_variant("NM_004006.2:r.100+5_100+15del").unwrap();
let dup = parse_variant("NM_004006.2:r.100+12_100+20dup").unwrap();
assert!(
AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_some(),
"overlapping r. intronic del+dup must be detected (#172)"
);
}
#[test]
fn test_self_cancelling_tx_downstream_star_overlap() {
let del = tx_downstream_range("NR_001234.1", 100, 150, "del");
let dup = tx_downstream_range("NR_001234.1", 145, 160, "dup");
assert!(
AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_some(),
"overlapping n. downstream del+dup must be detected (#172)"
);
}
#[test]
fn test_self_cancelling_unknown_base_skipped() {
let del = parse_variant("NM_004006.2:c.?_200del").unwrap();
let dup = parse_variant("NM_004006.2:c.150_160dup").unwrap();
assert!(
AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_none(),
"pair containing c.? must be skipped (undecidable)"
);
}
#[test]
fn test_self_cancelling_unknown_offset_sentinel_skipped() {
let del = parse_variant("NM_004006.2:c.100+?_300del").unwrap();
let dup = parse_variant("NM_004006.2:c.150_160dup").unwrap();
assert!(
AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_none(),
"pair containing `+?` offset sentinel must be skipped"
);
}
#[test]
fn test_self_cancelling_mixed_axis_same_accession_no_false_positive() {
let del_cds = parse_variant("NM_004006.2:c.100_150del").unwrap();
let dup_tx = parse_variant("NM_004006.2:n.100_150dup").unwrap();
assert!(
AlleleVariant::detect_self_cancelling_pair(&[del_cds, dup_tx]).is_none(),
"c. del and n. dup with matching numeric ranges must NOT collide \
(different coordinate axes)"
);
let del_cds2 = parse_variant("NM_004006.2:c.100_150del").unwrap();
let dup_rna = parse_variant("NM_004006.2:r.100_150dup").unwrap();
assert!(
AlleleVariant::detect_self_cancelling_pair(&[del_cds2, dup_rna]).is_none(),
"c. del and r. dup with matching numeric ranges must NOT collide \
(different coordinate axes)"
);
let del_tx = parse_variant("NM_004006.2:n.100_150del").unwrap();
let dup_rna2 = parse_variant("NM_004006.2:r.100_150dup").unwrap();
assert!(
AlleleVariant::detect_self_cancelling_pair(&[del_tx, dup_rna2]).is_none(),
"n. del and r. dup with matching numeric ranges must NOT collide \
(different coordinate axes)"
);
}
#[test]
fn test_self_cancelling_genomic_vs_mito_no_false_positive() {
let del = parse_variant("NC_000017.11:g.43044295_43044300del").unwrap();
let dup = parse_variant("NC_000017.11:m.43044295_43044300dup").unwrap();
assert!(
AlleleVariant::detect_self_cancelling_pair(&[del, dup]).is_none(),
"g. and m. variants with matching numeric ranges must NOT collide"
);
}
#[test]
fn test_protein_variant_to_styled_string() {
use crate::hgvs::location::{AaCode, ProteinRenderStyle, TerStyle};
use crate::parse_hgvs;
let three_star = ProteinRenderStyle {
stop: TerStyle::Star,
aa_code: AaCode::Three,
};
let one = ProteinRenderStyle {
stop: TerStyle::Ter,
aa_code: AaCode::One,
};
let cases = [
(
"NP_000079.2:p.(Ser4Thr)",
"NP_000079.2:p.(Ser4Thr)",
"NP_000079.2:p.(S4T)",
),
(
"NP_000079.2:p.(Ile50Ter)",
"NP_000079.2:p.(Ile50*)",
"NP_000079.2:p.(I50*)",
),
(
"NP_000079.2:p.(Ser4LeufsTer76)",
"NP_000079.2:p.(Ser4Leufs*76)",
"NP_000079.2:p.(S4Lfs*76)",
),
(
"NP_000079.2:p.Met1ext-5",
"NP_000079.2:p.Met1ext-5",
"NP_000079.2:p.M1ext-5",
),
];
for (input, want_star, want_one) in cases {
let v = parse_hgvs(input).expect("parse");
assert_eq!(v.to_styled_string(three_star), want_star, "star: {input}");
assert_eq!(v.to_styled_string(one), want_one, "one: {input}");
assert_eq!(
v.to_styled_string(ProteinRenderStyle::default()),
v.to_string(),
"default: {input}"
);
}
}
#[test]
fn test_hgvs_variant_to_styled_string_non_protein_passthrough() {
use crate::hgvs::location::{AaCode, ProteinRenderStyle, TerStyle};
use crate::parse_hgvs;
let one = ProteinRenderStyle {
stop: TerStyle::Star,
aa_code: AaCode::One,
};
let v = parse_hgvs("NM_000088.3:c.459A>G").expect("parse");
assert_eq!(v.to_styled_string(one), v.to_string());
}
#[test]
fn spec_equivalent_renderings_offers_the_star_stop_spelling() {
use crate::parse_hgvs;
for (input, want) in [
("NP_000079.2:p.Trp24Ter", Some("NP_000079.2:p.Trp24*")),
(
"NP_000079.2:p.Gln151ThrfsTer9",
Some("NP_000079.2:p.Gln151Thrfs*9"),
),
(
"NP_000079.2:p.Ter110GlnextTer17",
Some("NP_000079.2:p.*110Glnext*17"),
),
(
"NP_000079.2:p.[Lys79Ter;Lys79Asn]",
Some("NP_000079.2:p.[Lys79*;Lys79Asn]"),
),
("NP_000079.2:p.(Ser4Thr)", None),
("NM_000088.3:c.459A>G", None),
("NC_000023.11:g.32867861_32867862del", None),
] {
let v = parse_hgvs(input).expect("parse");
let got = v.spec_equivalent_renderings();
match want {
Some(starred) => assert_eq!(got, vec![starred.to_string()], "for {input}"),
None => assert!(
got.is_empty(),
"expected no alternate rendering for {input}, got {got:?}"
),
}
}
}
#[test]
fn spec_equivalent_renderings_excludes_the_canonical_form() {
use crate::parse_hgvs;
for input in [
"NP_000079.2:p.Trp24Ter",
"NP_000079.2:p.(Ser4Thr)",
"NM_000088.3:c.459A>G",
] {
let v = parse_hgvs(input).expect("parse");
let canonical = v.to_string();
assert!(
!v.spec_equivalent_renderings().contains(&canonical),
"{input}: alternates must exclude the canonical rendering"
);
}
}
#[test]
fn test_styled_default_equals_display_corpus() {
use crate::hgvs::location::ProteinRenderStyle;
use crate::parse_hgvs;
let corpus = [
"NP_000079.2:p.(Ser4Thr)",
"NP_000079.2:p.Ile50Ter",
"NP_000079.2:p.Ser4LeufsTer76",
"NP_000079.2:p.Gly719(Ala^Ser)fsTer23",
"NP_000079.2:p.Ter110GlnextTer17",
"NP_000079.2:p.Met1ext-5",
"NP_000079.2:p.Lys23_Leu24insTer12",
"NP_000079.2:p.Gln367_Gly372del",
"NP_000079.2:p.=",
"NP_000079.2:p.0",
"NP_000079.2:p.(=)",
"NP_000079.2:p.?",
];
for input in corpus {
let v = parse_hgvs(input).expect("parse");
assert_eq!(
v.to_styled_string(ProteinRenderStyle::default()),
v.to_string(),
"default style must equal Display for {input}"
);
}
}
#[test]
fn the_insertion_anchor_check_survives_a_position_at_the_top_of_the_range() {
use crate::hgvs::parser::parse_hgvs;
let v =
parse_hgvs("NC_TEST.1:g.18446744073709551615_18446744073709551614insA").expect("parse");
assert_eq!(
non_flanking_genomic_insertion_anchor(&v),
Some((u64::MAX, u64::MAX - 1)),
);
}
}