use std::fmt;
use crate::backtranslate::codon::{Codon, CodonTable};
use crate::conformance::spec_corpus::{three_exon_layout, transcript_provider, CODING_ACCESSION};
use crate::hgvs::edit::{
AminoAcidSeq, ExtDirection, FrameshiftTer, ProteinEdit, ProteinInsSeq, RepeatCount,
};
use crate::hgvs::interval::UncertainBoundary;
use crate::hgvs::location::{AminoAcid, ProtPos};
use crate::hgvs::uncertainty::Mu;
use crate::hgvs::variant::{AllelePhase, ProteinVariant};
use crate::hgvs::HgvsVariant;
use crate::parse_hgvs;
use crate::project::protein::translate;
use crate::reference::transcript::Strand;
use crate::reference::{MockProvider, ReferenceProvider};
pub const PROTEIN_ACCESSION: &str = "NP_TEST.1";
const UTR_LEN: usize = 12;
const UTR_FILLER: &str = "ACGT";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProteinRefShape {
SingleExon,
ThreeExon(Strand),
}
impl ProteinRefShape {
#[must_use]
pub fn all() -> Vec<Self> {
vec![
Self::SingleExon,
Self::ThreeExon(Strand::Plus),
Self::ThreeExon(Strand::Minus),
]
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::SingleExon => "p1",
Self::ThreeExon(Strand::Minus) => "p3m",
Self::ThreeExon(_) => "p3p",
}
}
fn strand(self) -> Strand {
match self {
Self::SingleExon => Strand::Plus,
Self::ThreeExon(strand) => strand,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProteinFrameError {
Empty,
NotMetInitiated(AminoAcid),
NotBackTranslatable(AminoAcid),
InternalStop(usize),
RoundTripMismatch {
requested: String,
translated: String,
},
TranscriptNotServed(String),
}
impl fmt::Display for ProteinFrameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "a synthetic peptide must have at least one residue"),
Self::NotMetInitiated(aa) => write!(
f,
"residue 1 must be Met (translation initiation), got {}",
aa.to_three_letter()
),
Self::NotBackTranslatable(aa) => write!(
f,
"{} has no codon in the standard table",
aa.to_three_letter()
),
Self::InternalStop(at) => {
write!(f, "the peptide has an internal Ter at residue {at}")
}
Self::RoundTripMismatch {
requested,
translated,
} => write!(
f,
"the built CDS translates to {translated}, not the requested {requested}"
),
Self::TranscriptNotServed(accession) => write!(
f,
"the provider does not serve {accession} back after it was registered, so the \
transcript-to-protein link cannot be recorded"
),
}
}
}
impl std::error::Error for ProteinFrameError {}
#[derive(Clone)]
pub struct ProteinFrame {
shape: ProteinRefShape,
residues: Vec<AminoAcid>,
transcript: String,
cds: (usize, usize),
provider: MockProvider,
}
impl ProteinFrame {
pub fn from_peptide(peptide: &[AminoAcid]) -> Result<Self, ProteinFrameError> {
Self::build(ProteinRefShape::SingleExon, peptide)
}
#[must_use]
pub fn peptide_from_three_letter(spelled: &str) -> Option<Vec<AminoAcid>> {
if !spelled.is_ascii() || !spelled.len().is_multiple_of(3) {
return None;
}
(0..spelled.len() / 3)
.map(|index| AminoAcid::from_three_letter(&spelled[index * 3..index * 3 + 3]))
.collect()
}
#[must_use]
pub fn spell(residues: &[AminoAcid]) -> String {
residues.iter().map(AminoAcid::to_three_letter).collect()
}
pub fn build(shape: ProteinRefShape, peptide: &[AminoAcid]) -> Result<Self, ProteinFrameError> {
let cds_bases = back_translate_cds(peptide)?;
Self::assemble(shape, &cds_bases, peptide)
}
pub fn from_cds(shape: ProteinRefShape, cds: &str) -> Result<Self, ProteinFrameError> {
if cds.len() < 6 || !cds.len().is_multiple_of(3) {
return Err(ProteinFrameError::Empty);
}
let table = CodonTable::standard();
let codons: Vec<Codon> = (0..cds.len() / 3)
.map(|index| Codon::parse(&cds[index * 3..index * 3 + 3]))
.collect::<Option<_>>()
.ok_or(ProteinFrameError::Empty)?;
let (last, body) = codons.split_last().ok_or(ProteinFrameError::Empty)?;
if !table.is_stop(last) {
return Err(ProteinFrameError::Empty);
}
let mut peptide = Vec::with_capacity(body.len());
for (index, codon) in body.iter().enumerate() {
match table.amino_acid_for(codon) {
Some(AminoAcid::Ter) | None => {
return Err(ProteinFrameError::InternalStop(index + 1))
}
Some(aa) => peptide.push(aa),
}
}
match peptide.first() {
Some(AminoAcid::Met) => {}
Some(other) => return Err(ProteinFrameError::NotMetInitiated(*other)),
None => return Err(ProteinFrameError::Empty),
}
Self::assemble(shape, cds, &peptide)
}
fn assemble(
shape: ProteinRefShape,
cds_bases: &str,
peptide: &[AminoAcid],
) -> Result<Self, ProteinFrameError> {
let filler = |len: usize| UTR_FILLER.chars().cycle().take(len).collect::<String>();
let utr5 = filler(UTR_LEN);
let utr3 = filler(UTR_LEN);
let transcript = format!("{utr5}{cds_bases}{utr3}");
let cds = (UTR_LEN + 1, UTR_LEN + cds_bases.len());
let exons = match shape {
ProteinRefShape::SingleExon => vec![(1, transcript.len())],
ProteinRefShape::ThreeExon(_) => three_exon_layout(transcript.len()),
};
let mut provider = transcript_provider(
CODING_ACCESSION,
shape.strand(),
&transcript,
Some(cds),
&exons,
);
let served_cds = &transcript[cds.0 - 1..cds.1];
let residues = translate_cds(served_cds);
if residues != peptide {
return Err(ProteinFrameError::RoundTripMismatch {
requested: Self::spell(peptide),
translated: Self::spell(&residues),
});
}
let built = provider
.get_transcript(CODING_ACCESSION)
.map_err(|_| ProteinFrameError::TranscriptNotServed(CODING_ACCESSION.to_string()))?;
let linked = (*built)
.clone()
.with_protein_id(Some(PROTEIN_ACCESSION.to_string()));
provider.add_transcript(linked);
provider.add_protein(
PROTEIN_ACCESSION,
residues
.iter()
.map(AminoAcid::to_one_letter)
.collect::<String>(),
);
Ok(Self {
shape,
residues,
transcript,
cds,
provider,
})
}
#[must_use]
pub fn provider(&self) -> &MockProvider {
&self.provider
}
#[must_use]
pub fn shape(&self) -> ProteinRefShape {
self.shape
}
#[must_use]
pub fn residues(&self) -> &[AminoAcid] {
&self.residues
}
#[must_use]
pub fn residues_with_stop(&self) -> Vec<AminoAcid> {
let mut with_stop = self.residues.clone();
with_stop.push(AminoAcid::Ter);
with_stop
}
#[must_use]
pub fn spelled(&self) -> String {
Self::spell(&self.residues)
}
#[must_use]
pub fn transcript(&self) -> &str {
&self.transcript
}
#[must_use]
pub fn cds(&self) -> &str {
&self.transcript[self.cds.0 - 1..self.cds.1]
}
#[must_use]
pub fn cds_bounds(&self) -> (usize, usize) {
self.cds
}
#[must_use]
pub fn cds_position_of_residue(&self, residue: u64) -> Option<i64> {
let index = usize::try_from(residue).ok()?.checked_sub(1)?;
if index > self.residues.len() {
return None;
}
i64::try_from(index * 3 + 1).ok()
}
#[must_use]
pub fn exons(&self) -> Vec<(usize, usize)> {
match self.shape {
ProteinRefShape::SingleExon => vec![(1, self.transcript.len())],
ProteinRefShape::ThreeExon(_) => three_exon_layout(self.transcript.len()),
}
}
#[must_use]
pub fn junction_phase_of_residue(&self, residue: u64) -> Option<usize> {
let first = usize::try_from(self.cds_position_of_residue(residue)?).ok()?;
let start = self.cds.0 + first - 1;
let exons = self.exons();
let exon_of = |position: usize| {
exons
.iter()
.position(|&(lo, hi)| (lo..=hi).contains(&position))
};
let first_exon = exon_of(start)?;
(0..3usize)
.map(|offset| exon_of(start + offset))
.position(|exon| exon != Some(first_exon))
}
#[must_use]
pub fn protein_descriptor(suffix: &str) -> String {
format!("{PROTEIN_ACCESSION}:p.{suffix}")
}
#[must_use]
pub fn coding_descriptor(suffix: &str) -> String {
format!("{CODING_ACCESSION}:c.{suffix}")
}
}
fn translate_cds(cds: &str) -> Vec<AminoAcid> {
let mut residues = Vec::with_capacity(cds.len() / 3);
for index in 0..cds.len() / 3 {
match translate(&cds[index * 3..index * 3 + 3]) {
Some(AminoAcid::Ter) | None => break,
Some(aa) => residues.push(aa),
}
}
residues
}
fn back_translate_cds(peptide: &[AminoAcid]) -> Result<String, ProteinFrameError> {
if peptide.is_empty() {
return Err(ProteinFrameError::Empty);
}
if peptide[0] != AminoAcid::Met {
return Err(ProteinFrameError::NotMetInitiated(peptide[0]));
}
if let Some(at) = peptide.iter().position(|aa| *aa == AminoAcid::Ter) {
return Err(ProteinFrameError::InternalStop(at + 1));
}
let table = CodonTable::standard();
let mut cds = String::with_capacity((peptide.len() + 1) * 3);
for aa in peptide {
let codon = table
.codons_for(aa)
.iter()
.find(|codon| !table.is_stop(codon))
.ok_or(ProteinFrameError::NotBackTranslatable(*aa))?;
cds.push_str(&codon.to_string());
}
let stop = table
.stop_codons()
.first()
.ok_or(ProteinFrameError::NotBackTranslatable(AminoAcid::Ter))?;
cds.push_str(&stop.to_string());
Ok(cds)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProteinDenotation {
Sequence(String),
Indeterminate(Indeterminate),
NoProtein {
predicted: bool,
},
Unparseable,
NotProteinAxis,
NoSingleSequence(NoSingleSequence),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Indeterminate {
pub prefix: String,
pub length: Option<usize>,
pub reason: Indeterminacy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Indeterminacy {
FrameshiftTail,
CTerminalExtension,
NTerminalExtension,
ResiduesStatedByCount,
ReadsPastStop,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NoSingleSequence {
ReferenceUnavailable(String),
UnknownConsequence,
UncertainPosition,
SetValued,
ReferenceMismatch {
position: u64,
stated: String,
actual: String,
},
NonFlankingInsertion,
PositionOutOfRange {
position: u64,
length: usize,
},
ReversedRange,
StatedExtentMismatch,
OverlappingMembers,
RepeatCountOutOfRange {
count: u64,
limit: usize,
},
Unsupported(&'static str),
}
fn checked_count(count: u64, reference_len: usize) -> Result<usize, NoSingleSequence> {
let limit = reference_len;
match usize::try_from(count) {
Ok(count) if count <= limit => Ok(count),
_ => Err(NoSingleSequence::RepeatCountOutOfRange { count, limit }),
}
}
#[must_use]
pub fn protein_denotation_of(provider: &MockProvider, descriptor: &str) -> ProteinDenotation {
let Ok(parsed) = parse_hgvs(descriptor) else {
return ProteinDenotation::Unparseable;
};
let members = match &parsed {
HgvsVariant::Protein(variant) => vec![variant.clone()],
HgvsVariant::Allele(allele) => {
if allele.phase != AllelePhase::Cis {
return ProteinDenotation::NoSingleSequence(NoSingleSequence::SetValued);
}
let mut protein_members = Vec::with_capacity(allele.variants.len());
for member in &allele.variants {
match member {
HgvsVariant::Protein(variant) => protein_members.push(variant.clone()),
_ => return ProteinDenotation::NotProteinAxis,
}
}
if protein_members.is_empty() {
return ProteinDenotation::NotProteinAxis;
}
protein_members
}
_ => return ProteinDenotation::NotProteinAxis,
};
let accession = members[0].accession.full();
if members
.iter()
.any(|member| member.accession.full() != accession)
{
return ProteinDenotation::NotProteinAxis;
}
let reference = match reference_protein(provider, &accession) {
Ok(reference) => reference,
Err(why) => return ProteinDenotation::NoSingleSequence(why),
};
let mut spans = Vec::with_capacity(members.len());
for member in &members {
match member_span(&reference, member) {
Ok(MemberOutcome::Span(span)) => spans.push(span),
Ok(MemberOutcome::Terminal(denotation)) => {
return if members.len() == 1 {
denotation
} else {
ProteinDenotation::NoSingleSequence(NoSingleSequence::Unsupported(
"a whole-protein member combined with another member",
))
};
}
Err(reason) => return ProteinDenotation::NoSingleSequence(reason),
}
}
match apply_spans(&reference, &spans) {
Some(applied) => {
ProteinDenotation::Sequence(ProteinFrame::spell(&truncate_at_stop(&applied)))
}
None => ProteinDenotation::NoSingleSequence(NoSingleSequence::OverlappingMembers),
}
}
fn reference_protein(
provider: &MockProvider,
accession: &str,
) -> Result<Vec<AminoAcid>, NoSingleSequence> {
let unavailable = || NoSingleSequence::ReferenceUnavailable(accession.to_string());
let length = provider
.get_protein_length(accession)
.ok()
.and_then(|length| usize::try_from(length).ok())
.ok_or_else(unavailable)?;
if length == 0 {
return Err(unavailable());
}
let sequence = u64::try_from(length)
.ok()
.and_then(|length| provider.get_protein_sequence(accession, 0, length).ok())
.ok_or_else(unavailable)?;
let mut residues: Vec<AminoAcid> = sequence
.chars()
.map(AminoAcid::from_one_letter)
.collect::<Option<_>>()
.ok_or(NoSingleSequence::Unsupported(
"the reference protein names a residue this oracle cannot read",
))?;
residues.push(AminoAcid::Ter);
Ok(residues)
}
#[derive(Debug, Clone)]
struct Span {
start: usize,
removed: usize,
inserted: Vec<AminoAcid>,
}
enum MemberOutcome {
Span(Span),
Terminal(ProteinDenotation),
}
fn apply_spans(reference: &[AminoAcid], spans: &[Span]) -> Option<Vec<AminoAcid>> {
let mut ordered: Vec<&Span> = spans.iter().collect();
ordered.sort_by_key(|span| {
(
std::cmp::Reverse(span.start),
std::cmp::Reverse(span.removed),
)
});
let mut edited = reference.to_vec();
let mut claimed = reference.len();
let mut insertion_at: Option<usize> = None;
for span in ordered {
let end = span.start.checked_add(span.removed)?;
if end > reference.len() || end > claimed {
return None;
}
if span.removed == 0 && insertion_at == Some(span.start) {
return None;
}
edited.splice(span.start..end, span.inserted.iter().copied());
if span.removed == 0 {
insertion_at = Some(span.start);
}
claimed = span.start;
}
Some(edited)
}
fn member_span(
reference: &[AminoAcid],
member: &ProteinVariant,
) -> Result<MemberOutcome, NoSingleSequence> {
let edit = match &member.loc_edit.edit {
Mu::Certain(edit) | Mu::Uncertain(edit) => edit,
Mu::Unknown => return Err(NoSingleSequence::UnknownConsequence),
};
match edit {
ProteinEdit::NoProtein { predicted } => {
return Ok(MemberOutcome::Terminal(ProteinDenotation::NoProtein {
predicted: *predicted,
}))
}
ProteinEdit::Unknown { .. } => return Err(NoSingleSequence::UnknownConsequence),
ProteinEdit::Identity {
whole_protein: true,
..
} => {
return Ok(MemberOutcome::Terminal(ProteinDenotation::Sequence(
ProteinFrame::spell(&truncate_at_stop(reference)),
)))
}
_ => {}
}
let start = resolve(reference, endpoint(&member.loc_edit.location.start)?)?;
let end = resolve(reference, endpoint(&member.loc_edit.location.end)?)?;
let span = match edit {
ProteinEdit::Substitution {
reference: stated,
alternative,
} => {
if start != end {
return Err(NoSingleSequence::Unsupported(
"a substitution names exactly one position",
));
}
check_residue(reference, start, *stated)?;
Span {
start,
removed: 1,
inserted: vec![*alternative],
}
}
ProteinEdit::SubstitutionAlternatives { .. }
| ProteinEdit::FrameshiftAlternatives { .. } => return Err(NoSingleSequence::SetValued),
ProteinEdit::Deletion { sequence, count } => {
let removed = range_len(start, end)?;
check_stated_sequence(reference, start, removed, sequence.as_ref())?;
check_stated_count(removed, *count)?;
Span {
start,
removed,
inserted: Vec::new(),
}
}
ProteinEdit::Duplication => {
let removed = range_len(start, end)?;
let copy = &reference[start..start + removed];
let mut inserted = copy.to_vec();
inserted.extend_from_slice(copy);
Span {
start,
removed,
inserted,
}
}
ProteinEdit::Insertion { sequence } => {
if end != start + 1 {
return Err(NoSingleSequence::NonFlankingInsertion);
}
match insertion_payload(sequence, reference.len())? {
Payload::Residues(inserted) => Span {
start: end,
removed: 0,
inserted,
},
Payload::ByCount { count, terminates } => {
return Ok(MemberOutcome::Terminal(ProteinDenotation::Indeterminate(
Indeterminate {
prefix: ProteinFrame::spell(&reference[..end]),
length: Some(stated_insert_length(reference, end, count, terminates)),
reason: Indeterminacy::ResiduesStatedByCount,
},
)))
}
}
}
ProteinEdit::Delins { sequence } => {
let removed = range_len(start, end)?;
Span {
start,
removed,
inserted: sequence.0.clone(),
}
}
ProteinEdit::Identity { .. } => {
let removed = range_len(start, end)?;
Span {
start,
removed,
inserted: reference[start..start + removed].to_vec(),
}
}
ProteinEdit::Frameshift { new_aa, ter } => {
let mut prefix = reference[..start].to_vec();
prefix.extend(new_aa.iter().copied());
let length = match ter {
FrameshiftTer::At(count) => {
Some(start.saturating_add(usize::try_from(*count).unwrap_or(0)))
}
FrameshiftTer::Unknown | FrameshiftTer::Unspecified => None,
};
return Ok(MemberOutcome::Terminal(ProteinDenotation::Indeterminate(
Indeterminate {
prefix: ProteinFrame::spell(&prefix),
length,
reason: Indeterminacy::FrameshiftTail,
},
)));
}
ProteinEdit::Extension {
new_aa,
direction,
count,
} => {
return Ok(MemberOutcome::Terminal(extension(
reference, start, *new_aa, *direction, *count,
)?))
}
ProteinEdit::Repeat { sequence, count } => {
if end < start {
return Err(NoSingleSequence::ReversedRange);
}
repeat_span(reference, start, end, sequence, count)?
}
ProteinEdit::MultiRepeat { .. } => {
return Err(NoSingleSequence::Unsupported("a multi-unit protein repeat"))
}
ProteinEdit::NoProtein { .. } | ProteinEdit::Unknown { .. } => unreachable!(),
};
let terminator = reference.len() - 1;
let removes_terminator = span.start <= terminator && terminator < span.start + span.removed;
if removes_terminator && !span.inserted.contains(&AminoAcid::Ter) {
let mut prefix = reference[..span.start].to_vec();
prefix.extend(span.inserted.iter().copied());
return Ok(MemberOutcome::Terminal(ProteinDenotation::Indeterminate(
Indeterminate {
prefix: ProteinFrame::spell(&prefix),
length: None,
reason: Indeterminacy::ReadsPastStop,
},
)));
}
Ok(MemberOutcome::Span(span))
}
fn endpoint(boundary: &UncertainBoundary<ProtPos>) -> Result<ProtPos, NoSingleSequence> {
match boundary {
UncertainBoundary::Single(Mu::Certain(pos) | Mu::Uncertain(pos)) => Ok(*pos),
UncertainBoundary::Single(Mu::Unknown) | UncertainBoundary::Range { .. } => {
Err(NoSingleSequence::UncertainPosition)
}
}
}
fn resolve(reference: &[AminoAcid], pos: ProtPos) -> Result<usize, NoSingleSequence> {
let number = usize::try_from(pos.number).unwrap_or(usize::MAX);
if number == 0 || number > reference.len() {
return Err(NoSingleSequence::PositionOutOfRange {
position: pos.number,
length: reference.len(),
});
}
let index = number - 1;
check_residue(reference, index, pos.aa)?;
Ok(index)
}
fn check_residue(
reference: &[AminoAcid],
index: usize,
stated: AminoAcid,
) -> Result<(), NoSingleSequence> {
let actual = reference[index];
if stated == AminoAcid::Xaa || stated == actual {
return Ok(());
}
Err(NoSingleSequence::ReferenceMismatch {
position: index as u64 + 1,
stated: stated.to_three_letter().to_string(),
actual: actual.to_three_letter().to_string(),
})
}
fn range_len(start: usize, end: usize) -> Result<usize, NoSingleSequence> {
if end < start {
return Err(NoSingleSequence::ReversedRange);
}
Ok(end - start + 1)
}
fn check_stated_sequence(
reference: &[AminoAcid],
start: usize,
removed: usize,
stated: Option<&AminoAcidSeq>,
) -> Result<(), NoSingleSequence> {
let Some(stated) = stated else { return Ok(()) };
if stated.0.len() != removed || stated.0 != reference[start..start + removed] {
return Err(NoSingleSequence::StatedExtentMismatch);
}
Ok(())
}
fn check_stated_count(removed: usize, stated: Option<u64>) -> Result<(), NoSingleSequence> {
match stated {
Some(count) if usize::try_from(count).unwrap_or(usize::MAX) != removed => {
Err(NoSingleSequence::StatedExtentMismatch)
}
_ => Ok(()),
}
}
enum Payload {
Residues(Vec<AminoAcid>),
ByCount {
count: usize,
terminates: bool,
},
}
fn insertion_payload(
sequence: &ProteinInsSeq,
reference_len: usize,
) -> Result<Payload, NoSingleSequence> {
match sequence {
ProteinInsSeq::Literal(seq) => Ok(Payload::Residues(seq.0.clone())),
ProteinInsSeq::Repeat { aa, count } => {
let RepeatCount::Exact(count) = count else {
return Err(NoSingleSequence::SetValued);
};
let count = checked_count(*count, reference_len)?;
if *aa == AminoAcid::Xaa {
Ok(Payload::ByCount {
count,
terminates: false,
})
} else {
Ok(Payload::Residues(vec![*aa; count]))
}
}
ProteinInsSeq::Stop { position } => Ok(Payload::ByCount {
count: checked_count(*position, reference_len)?,
terminates: true,
}),
}
}
fn stated_insert_length(
reference: &[AminoAcid],
at: usize,
count: usize,
terminates: bool,
) -> usize {
if terminates {
at.saturating_add(count)
} else {
reference.len().saturating_add(count)
}
}
fn extension(
reference: &[AminoAcid],
start: usize,
new_aa: Option<AminoAcid>,
direction: ExtDirection,
count: Option<i64>,
) -> Result<ProteinDenotation, NoSingleSequence> {
match direction {
ExtDirection::CTerminal => {
let mut prefix = reference[..start].to_vec();
prefix.extend(new_aa.iter().copied());
let length = match count.filter(|count| *count > 0) {
Some(count) => {
Some(start + 1 + checked_count(count.unsigned_abs(), reference.len())?)
}
None => None,
};
Ok(ProteinDenotation::Indeterminate(Indeterminate {
prefix: ProteinFrame::spell(&prefix),
length,
reason: Indeterminacy::CTerminalExtension,
}))
}
ExtDirection::NTerminal => {
let length = match count {
Some(count) => {
Some(reference.len() + checked_count(count.unsigned_abs(), reference.len())?)
}
None => None,
};
Ok(ProteinDenotation::Indeterminate(Indeterminate {
prefix: String::new(),
length,
reason: Indeterminacy::NTerminalExtension,
}))
}
}
}
fn repeat_span(
reference: &[AminoAcid],
start: usize,
end: usize,
spelled_unit: &AminoAcidSeq,
count: &RepeatCount,
) -> Result<Span, NoSingleSequence> {
let RepeatCount::Exact(count) = count else {
return Err(NoSingleSequence::SetValued);
};
let count = checked_count(*count, reference.len())?;
let unit: Vec<AminoAcid> = if spelled_unit.0.is_empty() {
reference[start..=end].to_vec()
} else {
spelled_unit.0.clone()
};
if unit.is_empty() {
return Err(NoSingleSequence::Unsupported("an empty repeat unit"));
}
let unit = AminoAcidSeq(unit);
let mut copies = 0usize;
while reference
.get(start + copies * unit.0.len()..start + (copies + 1) * unit.0.len())
.is_some_and(|window| window == unit.0)
{
copies += 1;
}
if copies == 0 {
return Err(NoSingleSequence::StatedExtentMismatch);
}
Ok(Span {
start,
removed: copies * unit.0.len(),
inserted: unit
.0
.iter()
.copied()
.cycle()
.take(count * unit.0.len())
.collect(),
})
}
fn truncate_at_stop(residues: &[AminoAcid]) -> Vec<AminoAcid> {
match residues.iter().position(|aa| *aa == AminoAcid::Ter) {
Some(at) => residues[..=at].to_vec(),
None => residues.to_vec(),
}
}