use crate::hgvs::edit::{InsertedSequence, NaEdit};
use crate::hgvs::variant::HgvsVariant;
const MASKED_NUCLEOTIDE: char = 'X';
const MASKED_NUCLEOTIDE_RNA: char = 'x';
const INDETERMINATE_GAP: char = '-';
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SymbolTable {
Dna,
Rna,
}
impl SymbolTable {
fn daggered(self) -> &'static [char] {
match self {
Self::Dna => &[MASKED_NUCLEOTIDE, INDETERMINATE_GAP],
Self::Rna => &[MASKED_NUCLEOTIDE, MASKED_NUCLEOTIDE_RNA, INDETERMINATE_GAP],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlignmentOnlySymbol {
pub symbol: char,
pub stated: String,
}
impl AlignmentOnlySymbol {
#[must_use]
pub fn meaning(&self) -> &'static str {
if self.symbol == INDETERMINATE_GAP {
"gap of indeterminate length"
} else {
"masked nucleotide"
}
}
#[must_use]
pub fn clause(&self) -> &'static str {
if self.symbol == MASKED_NUCLEOTIDE_RNA {
"background/standards.md:39 read through recommendations/general.md:50"
} else {
"background/standards.md:39"
}
}
#[must_use]
pub fn alphabet_clause(&self) -> &'static str {
if self.symbol == MASKED_NUCLEOTIDE_RNA {
"general.md:50"
} else {
"general.md:48"
}
}
#[must_use]
pub fn unknown_base(&self) -> char {
if self.symbol == MASKED_NUCLEOTIDE_RNA {
'n'
} else {
'N'
}
}
}
fn symbol_in(text: &str, table: SymbolTable) -> Option<char> {
let daggered = table.daggered();
text.chars().find(|c| daggered.contains(c))
}
fn symbol_in_inserted(
sequence: &InsertedSequence,
table: SymbolTable,
) -> Option<AlignmentOnlySymbol> {
match sequence {
InsertedSequence::Named(name) => symbol_in(name, table).map(|symbol| AlignmentOnlySymbol {
symbol,
stated: name.clone(),
}),
InsertedSequence::Empty
| InsertedSequence::Literal(_)
| InsertedSequence::Count(_)
| InsertedSequence::Range(_, _)
| InsertedSequence::Repeat { .. }
| InsertedSequence::SequenceRepeat { .. }
| InsertedSequence::Complex(_)
| InsertedSequence::Reference(_)
| InsertedSequence::PositionRange { .. }
| InsertedSequence::PositionRangeInv { .. }
| InsertedSequence::SpecialPositionRange { .. }
| InsertedSequence::UncertainRangeInv { .. }
| InsertedSequence::Uncertain => None,
}
}
fn symbol_in_edit(edit: &NaEdit, table: SymbolTable) -> Option<AlignmentOnlySymbol> {
match edit {
NaEdit::Insertion { sequence }
| NaEdit::BreakpointInsertion { sequence }
| NaEdit::Delins { sequence, .. }
| NaEdit::DupIns { sequence } => symbol_in_inserted(sequence, table),
NaEdit::Substitution { .. }
| NaEdit::SubstitutionNoRef { .. }
| NaEdit::Deletion { .. }
| NaEdit::NPaddedDeletion { .. }
| NaEdit::Duplication { .. }
| NaEdit::Inversion { .. }
| NaEdit::Repeat { .. }
| NaEdit::MultiRepeat { .. }
| NaEdit::Identity { .. }
| NaEdit::Conversion { .. }
| NaEdit::Unknown { .. }
| NaEdit::Methylation { .. }
| NaEdit::CopyNumber { .. }
| NaEdit::Splice { .. }
| NaEdit::NoProduct
| NaEdit::PositionOnly => None,
}
}
#[must_use]
pub fn alignment_only_symbol(variant: &HgvsVariant) -> Option<AlignmentOnlySymbol> {
match variant {
HgvsVariant::Genome(v) => v
.loc_edit
.edit
.inner()
.and_then(|edit| symbol_in_edit(edit, SymbolTable::Dna)),
HgvsVariant::Cds(v) => v
.loc_edit
.edit
.inner()
.and_then(|edit| symbol_in_edit(edit, SymbolTable::Dna)),
HgvsVariant::Tx(v) => v
.loc_edit
.edit
.inner()
.and_then(|edit| symbol_in_edit(edit, SymbolTable::Dna)),
HgvsVariant::Rna(v) => v
.loc_edit
.edit
.inner()
.and_then(|edit| symbol_in_edit(edit, SymbolTable::Rna)),
HgvsVariant::Mt(v) => v
.loc_edit
.edit
.inner()
.and_then(|edit| symbol_in_edit(edit, SymbolTable::Dna)),
HgvsVariant::Circular(v) => v
.loc_edit
.edit
.inner()
.and_then(|edit| symbol_in_edit(edit, SymbolTable::Dna)),
HgvsVariant::Allele(allele) => allele.variants.iter().find_map(alignment_only_symbol),
HgvsVariant::GenomeRing(ring) => ring.segments.iter().find_map(|segment| {
segment
.edit
.inner()
.and_then(|edit| symbol_in_edit(edit, SymbolTable::Dna))
}),
HgvsVariant::Supernumerary(inner) => alignment_only_symbol(inner),
HgvsVariant::Protein(_)
| HgvsVariant::RnaFusion(_)
| HgvsVariant::NullAllele
| HgvsVariant::UnknownAllele => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse_hgvs;
fn symbol_of(input: &str) -> Option<AlignmentOnlySymbol> {
alignment_only_symbol(&parse_hgvs(input).expect("input parses"))
}
#[test]
fn a_lone_masked_nucleotide_is_found() {
let found = symbol_of("NC_TEST.1:g.10delinsX").expect("X is stated");
assert_eq!(found.symbol, 'X');
assert_eq!(found.stated, "X");
assert_eq!(found.meaning(), "masked nucleotide");
}
#[test]
fn an_embedded_masked_nucleotide_is_found_at_every_offset() {
for (input, stated) in [
("NC_TEST.1:g.10delinsACGTX", "ACGTX"),
("NC_TEST.1:g.10delinsXACGT", "XACGT"),
("NC_TEST.1:g.10delinsACXGT", "ACXGT"),
("NC_TEST.1:g.10_11insACGTX", "ACGTX"),
] {
let found = symbol_of(input).unwrap_or_else(|| panic!("{input}: X is stated"));
assert_eq!(found.symbol, 'X', "{input}");
assert_eq!(found.stated, stated, "{input}");
}
}
#[test]
fn genuine_mobile_element_names_state_no_alignment_symbol() {
for input in [
"NC_TEST.1:g.10delinsAluYb8",
"NC_TEST.1:g.10delinsLINE1",
"NC_TEST.1:g.10delinsL1",
"NC_TEST.1:g.10delinsAlu",
"NC_TEST.1:g.10_11insAluYb8",
] {
assert_eq!(
symbol_of(input),
None,
"{input} is a legitimate named element"
);
}
}
#[test]
fn a_literal_run_states_no_alignment_symbol() {
assert_eq!(symbol_of("NC_TEST.1:g.10delinsACGT"), None);
assert_eq!(symbol_of("NC_TEST.1:g.10delinsN"), None);
}
#[test]
fn an_x_prefixed_accession_is_not_a_masked_nucleotide() {
assert_eq!(symbol_of("XM_005260378.1:c.10delinsACGT"), None);
}
#[test]
fn a_member_of_any_composite_spelling_is_reached() {
for input in [
"NC_TEST.1:g.[10delinsACGT;20delinsACGTX]",
"NC_TEST.1:g.[10delinsACGTX];[20del]",
"NC_TEST.1:g.(10delinsACGTX)",
] {
let found = symbol_of(input).unwrap_or_else(|| panic!("{input}: X is stated"));
assert_eq!(found.symbol, 'X', "{input}");
assert_eq!(found.stated, "ACGTX", "{input}");
}
}
#[test]
fn a_lowercase_rna_masked_nucleotide_is_found() {
let found = symbol_of("NM_TEST.1:r.10delinsacgux").expect("x is stated");
assert_eq!(found.symbol, 'x');
assert_eq!(found.stated, "acgux");
assert_eq!(found.meaning(), "masked nucleotide");
assert_eq!(found.alphabet_clause(), "general.md:50");
assert_eq!(found.unknown_base(), 'n');
assert!(
found.clause().contains("general.md:50"),
"{}",
found.clause()
);
}
#[test]
fn an_embedded_lowercase_masked_nucleotide_is_found_at_every_reachable_offset() {
for (input, stated) in [
("NM_TEST.1:r.10delinsacgux", "acgux"),
("NM_TEST.1:r.10delinsacxgu", "acxgu"),
("NM_TEST.1:r.10delinsax", "ax"),
("NM_TEST.1:r.10delinsaxa", "axa"),
("NM_TEST.1:r.10_11insacgux", "acgux"),
("NM_TEST.1:r.10delinsAcgux", "Acgux"),
] {
let found = symbol_of(input).unwrap_or_else(|| panic!("{input}: x is stated"));
assert_eq!(found.symbol, 'x', "{input}");
assert_eq!(found.stated, stated, "{input}");
}
}
#[test]
fn a_member_of_any_rna_composite_spelling_is_reached() {
for input in [
"NM_TEST.1:r.[10delinsacgu;20delinsacgux]",
"NM_TEST.1:r.[10delinsacgux];[20del]",
"NM_TEST.1:r.(10delinsacgux)",
] {
let found = symbol_of(input).unwrap_or_else(|| panic!("{input}: x is stated"));
assert_eq!(found.symbol, 'x', "{input}");
assert_eq!(found.stated, "acgux", "{input}");
}
}
#[test]
fn an_uppercase_masked_nucleotide_on_the_rna_axis_keeps_its_dna_citation() {
let found = symbol_of("NM_TEST.1:r.10delinsX").expect("X is stated");
assert_eq!(found.symbol, 'X');
assert_eq!(found.clause(), "background/standards.md:39");
assert_eq!(found.alphabet_clause(), "general.md:48");
assert_eq!(found.unknown_base(), 'N');
}
#[test]
fn a_named_element_without_the_symbol_is_untouched_on_the_rna_axis() {
for input in [
"NM_TEST.1:r.10delinsAluYb8",
"NM_TEST.1:r.10delinsLINE1",
"NM_TEST.1:r.10delinsalu",
"NM_TEST.1:r.10delinsAlu",
] {
assert_eq!(symbol_of(input), None, "{input} states no daggered symbol");
}
}
#[test]
fn a_pure_rna_iupac_run_states_no_alignment_symbol() {
assert_eq!(symbol_of("NM_TEST.1:r.10delinsacgu"), None);
assert_eq!(symbol_of("NM_TEST.1:r.10delinsn"), None);
}
#[test]
fn a_lowercase_x_on_a_dna_axis_is_still_accepted() {
for input in [
"NC_TEST.1:g.10delinsACGTx",
"NM_TEST.1:c.10delinsACGTx",
"NM_TEST.1:n.10delinsACGTx",
"NC_TEST.1:m.10delinsACGTx",
] {
assert_eq!(
symbol_of(input),
None,
"{input}: `general.md:48` is the CAPITALS bullet, so a lower-case letter is an \
ordinary name character on every DNA axis"
);
}
}
}