use std::borrow::Borrow;
use std::collections::BTreeMap;
use std::fmt;
use crate::hgvs::variant::HgvsVariant;
use crate::reference::ReferenceProvider;
use crate::spdi::{canonical_spdi, SpdiVariant};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SpdiKey {
spdi: SpdiVariant,
}
impl SpdiKey {
pub fn accession(&self) -> &str {
&self.spdi.sequence
}
pub fn as_spdi(&self) -> &SpdiVariant {
&self.spdi
}
pub fn into_spdi(self) -> SpdiVariant {
self.spdi
}
}
impl fmt::Display for SpdiKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.spdi)
}
}
impl PartialOrd for SpdiKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SpdiKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.spdi.cmp(&other.spdi)
}
}
pub fn spdi_key<P: ReferenceProvider + ?Sized>(
variant: &HgvsVariant,
provider: &P,
) -> Option<SpdiKey> {
canonical_spdi(variant, provider)
.ok()
.map(|spdi| SpdiKey { spdi })
}
#[derive(Debug, Clone)]
pub struct SpdiKeyGrouping<T> {
groups: BTreeMap<SpdiKey, Vec<T>>,
unkeyable: Vec<T>,
}
impl<T> SpdiKeyGrouping<T> {
pub fn groups(&self) -> impl Iterator<Item = (&SpdiKey, &[T])> {
self.groups
.iter()
.map(|(key, items)| (key, items.as_slice()))
}
pub fn unkeyable(&self) -> &[T] {
&self.unkeyable
}
pub fn group_count(&self) -> usize {
self.groups.len()
}
pub fn get(&self, key: &SpdiKey) -> Option<&[T]> {
self.groups.get(key).map(Vec::as_slice)
}
pub fn is_confluent(&self) -> bool {
self.groups.len() <= 1
}
pub fn into_parts(self) -> (BTreeMap<SpdiKey, Vec<T>>, Vec<T>) {
(self.groups, self.unkeyable)
}
}
pub fn group_by_spdi_key<T, I, P>(variants: I, provider: &P) -> SpdiKeyGrouping<T>
where
I: IntoIterator<Item = T>,
T: Borrow<HgvsVariant>,
P: ReferenceProvider + ?Sized,
{
let mut groups: BTreeMap<SpdiKey, Vec<T>> = BTreeMap::new();
let mut unkeyable = Vec::new();
for variant in variants {
match spdi_key(variant.borrow(), provider) {
Some(key) => groups.entry(key).or_default().push(variant),
None => unkeyable.push(variant),
}
}
SpdiKeyGrouping { groups, unkeyable }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hgvs::parser::parse_hgvs;
use crate::reference::MockProvider;
fn provider() -> MockProvider {
let mut provider = MockProvider::new();
provider.add_genomic_sequence("NC_KEY.1", "GGATTACAGGCATTAGCCTGAGGATTACAGGCATTAGCCT");
provider.add_genomic_sequence("NC_OTHER.1", "TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT");
provider
}
fn key(descriptor: &str) -> Option<SpdiKey> {
let variant = parse_hgvs(descriptor).expect("fixture must parse");
spdi_key(&variant, &provider())
}
#[test]
fn different_partitions_of_one_edit_share_a_key() {
let spanning = key("NC_KEY.1:g.3_7delinsGGCTA").expect("keys");
let decomposed = key("NC_KEY.1:g.[3A>G;4T>G;5T>C;6A>T;7C>A]").expect("keys");
assert_eq!(spanning, decomposed);
assert_eq!(spanning.to_string(), "NC_KEY.1:2:ATTAC:GGCTA");
assert_eq!(spanning.accession(), "NC_KEY.1");
}
#[test]
fn different_edits_do_not_share_a_key() {
assert_ne!(key("NC_KEY.1:g.3A>G"), key("NC_KEY.1:g.3A>C"));
assert_ne!(key("NC_KEY.1:g.3_5del"), key("NC_KEY.1:g.3_6del"));
assert_ne!(key("NC_KEY.1:g.3A>G"), key("NC_OTHER.1:g.3T>G"));
}
#[test]
fn the_key_ignores_spelling_and_member_order() {
assert_eq!(key("NC_KEY.1:g.[3A>G;7C>A]"), key("NC_KEY.1:g.[7C>A;3A>G]"));
assert_eq!(key("NC_KEY.1:g.13_14insT"), key("NC_KEY.1:g.14dup"));
assert_eq!(key("NC_KEY.1:g.3_5del"), key("NC_KEY.1:g.4_6del"));
}
#[test]
fn the_documented_classes_have_no_key() {
for (descriptor, why) in [
("NP_000079.2:p.Gly1150Asp", "protein axis — SPDI has none"),
("NC_KEY.1:g.[3A>G(;)7C>A]", "trans phase — two molecules"),
("NC_KEY.1:g.[3_5del;4T>G]", "overlapping members"),
(
"NC_KEY.1:g.[5_6insA;5_6insC]",
"coincident insertions with no stated order",
),
(
"[NC_KEY.1:g.3A>G;NC_OTHER.1:g.7T>G]",
"members on two accessions",
),
(
"NC_KEY.1:g.3T>G",
"stated base disagrees with the reference",
),
("NC_ABSENT.1:g.3A>G", "the provider cannot serve the bases"),
("NC_KEY.1:g.10_11ins(10)", "unspecified inserted length"),
] {
assert_eq!(
key(descriptor),
None,
"`{descriptor}` must have no key ({why}) — a key here is a silent collision"
);
}
}
#[test]
fn two_no_ops_at_different_positions_do_not_group() {
let here = key("NC_KEY.1:g.3A>A").expect("a no-op still keys");
let there = key("NC_KEY.1:g.5T>T").expect("a no-op still keys");
assert_ne!(here, there, "the residual is stated on `spdi_key`");
for no_op in [&here, &there] {
assert_eq!(no_op.as_spdi().deletion, "", "a no-op deletes nothing");
assert_eq!(no_op.as_spdi().insertion, "", "a no-op inserts nothing");
}
}
#[test]
fn a_stated_insertion_order_still_keys() {
assert!(key("NC_KEY.1:g.5_6insAC").is_some());
}
#[test]
fn a_same_reference_range_insert_keys_off_its_resolved_bases() {
let ranged = key("NC_KEY.1:g.5_6ins9_19").expect("a resolvable range insert keys");
let literal = key("NC_KEY.1:g.5_6insGGCATTAGCCT").expect("the literal spelling keys");
assert_eq!(
ranged, literal,
"a range insert must key off the bases it names, not its spelling"
);
}
#[test]
fn grouping_separates_buckets_from_refusals() {
let provider = provider();
let variants: Vec<HgvsVariant> = [
"NC_KEY.1:g.3_7delinsGGCTA",
"NC_KEY.1:g.[3A>G;4T>G;5T>C;6A>T;7C>A]",
"NC_KEY.1:g.13_14insT",
"NC_KEY.1:g.[3_5del;4T>G]",
]
.iter()
.map(|d| parse_hgvs(d).expect("fixture must parse"))
.collect();
let grouped = group_by_spdi_key(&variants, &provider);
assert_eq!(grouped.group_count(), 2);
assert_eq!(grouped.unkeyable().len(), 1);
assert!(!grouped.is_confluent());
let (largest, members) = grouped
.groups()
.max_by_key(|(_, members)| members.len())
.expect("two buckets");
assert_eq!(members.len(), 2, "the two partitions of one edit");
assert_eq!(grouped.get(largest).map(<[_]>::len), Some(2));
}
#[test]
fn into_parts_preserves_every_input() {
let provider = provider();
let variants: Vec<HgvsVariant> = ["NC_KEY.1:g.3A>G", "NC_ABSENT.1:g.3A>G"]
.iter()
.map(|d| parse_hgvs(d).expect("fixture must parse"))
.collect();
let (groups, unkeyable) = group_by_spdi_key(variants.clone(), &provider).into_parts();
let returned = groups.values().map(Vec::len).sum::<usize>() + unkeyable.len();
assert_eq!(returned, variants.len());
}
#[test]
fn a_soft_masked_reference_keys_the_same_as_an_uppercase_one() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
const UPPER: &str = "GGATTACAGGCATTAGCCTGAGGATTACAGGCATTAGCCT";
let hashed = |key: &SpdiKey| {
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
hasher.finish()
};
let keyed = |sequence: &str, descriptor: &str| {
let mut provider = MockProvider::new();
provider.add_genomic_sequence("NC_KEY.1", sequence.to_string());
let variant = parse_hgvs(descriptor).expect("fixture must parse");
spdi_key(&variant, &provider)
};
for descriptor in [
"NC_KEY.1:g.3_7delinsGGCTA",
"NC_KEY.1:g.3_5del",
"NC_KEY.1:g.3_5dup",
"NC_KEY.1:g.3_7inv",
"NC_KEY.1:g.3A>G",
"NC_KEY.1:g.5_6insAC",
] {
let upper = keyed(UPPER, descriptor).expect("the uppercase reference keys");
let lower = keyed(&UPPER.to_ascii_lowercase(), descriptor)
.expect("the soft-masked reference keys");
assert_eq!(
upper, lower,
"`{descriptor}` keys differently on a soft-masked reference: \
{upper} vs {lower}"
);
assert_eq!(
hashed(&upper),
hashed(&lower),
"`{descriptor}` hashes differently on a soft-masked reference, so a \
`HashMap` would split the bucket even if `Eq` agreed"
);
assert_eq!(
upper.cmp(&lower),
std::cmp::Ordering::Equal,
"`{descriptor}` orders unequally on a soft-masked reference, so a \
`BTreeMap` would split the bucket even if `Eq` agreed"
);
}
}
#[test]
fn ord_follows_the_documented_field_order_and_agrees_with_eq() {
use std::cmp::Ordering;
let of = |spdi: SpdiVariant| SpdiKey { spdi };
let base = SpdiVariant::new("NC_A.1", 10, "AC", "GG");
let ascending = [
of(SpdiVariant::new("NC_A.1", 10, "AC", "GG")),
of(SpdiVariant::new("NC_A.1", 10, "AC", "GT")),
of(SpdiVariant::new("NC_A.1", 10, "AT", "GG")),
of(SpdiVariant::new("NC_A.1", 11, "AC", "GG")),
of(SpdiVariant::new("NC_B.1", 10, "AC", "GG")),
];
for (index, lower) in ascending.iter().enumerate() {
for higher in &ascending[index + 1..] {
assert_eq!(
lower.cmp(higher),
Ordering::Less,
"{lower} must sort before {higher}"
);
}
}
assert_eq!(of(base.clone()).cmp(&of(base.clone())), Ordering::Equal);
for other in &ascending[1..] {
assert_ne!(of(base.clone()), *other);
assert_ne!(
of(base.clone()).cmp(other),
Ordering::Equal,
"unequal keys must not compare equal — a `BTreeMap` would merge them"
);
}
}
#[test]
fn keys_order_positionally_within_an_accession() {
let mut keys = [
key("NC_KEY.1:g.7C>A").expect("keys"),
key("NC_KEY.1:g.3A>G").expect("keys"),
];
keys.sort();
assert!(keys[0].as_spdi().position < keys[1].as_spdi().position);
assert_eq!(keys[0].clone().into_spdi().position, 2);
}
}