1use crate::{Feature, SequenceRecord, Strand, Topology};
4
5#[must_use]
7pub fn reverse_complement(sequence: &str) -> Option<String> {
8 sequence
9 .bytes()
10 .rev()
11 .map(|base| match base {
12 b'A' => Some('T'),
13 b'C' => Some('G'),
14 b'G' => Some('C'),
15 b'T' => Some('A'),
16 _ => None,
17 })
18 .collect()
19}
20
21#[must_use]
25pub fn feature_sequence(record: &SequenceRecord, feature: &Feature) -> Option<String> {
26 let sequence = record.sequence().as_str();
27 let length = sequence.len();
28 let mut joined = String::new();
29 for part in feature.location().parts() {
30 let start = part.start().get();
31 let size = part.length().get();
32 let end = start + size;
33 if end <= length {
34 joined.push_str(&sequence[start..end]);
35 } else {
36 joined.push_str(&sequence[start..]);
37 joined.push_str(&sequence[..end - length]);
38 }
39 }
40 if !joined
41 .bytes()
42 .all(|b| matches!(b, b'A' | b'C' | b'G' | b'T'))
43 {
44 return None;
45 }
46 if feature.location().strand() == Strand::Reverse {
47 reverse_complement(&joined)
48 } else {
49 Some(joined)
50 }
51}
52
53#[must_use]
56pub fn canonical(sequence: &str) -> Option<String> {
57 let reverse = reverse_complement(sequence)?;
58 Some(if reverse.as_str() < sequence {
59 reverse
60 } else {
61 sequence.to_owned()
62 })
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct Occurrence {
68 pub start: usize,
70 pub length: usize,
71 pub strand: Strand,
73}
74
75#[must_use]
79pub fn find_exact(sequence: &str, topology: Topology, query: &str) -> Vec<Occurrence> {
80 Searcher::new(sequence, topology, query.len()).find(query)
81}
82
83pub struct Searcher {
85 text: String,
86 length: usize,
87 circular: bool,
88}
89
90impl Searcher {
91 #[must_use]
93 pub fn new(sequence: &str, topology: Topology, longest: usize) -> Self {
94 let circular = topology == Topology::Circular;
95 let length = sequence.len();
96 let wrap = if circular {
97 longest.saturating_sub(1).min(length)
98 } else {
99 0
100 };
101 Self {
102 text: format!("{sequence}{}", &sequence[..wrap]),
103 length,
104 circular,
105 }
106 }
107
108 #[must_use]
111 pub fn find(&self, query: &str) -> Vec<Occurrence> {
112 let (n, m) = (self.length, query.len());
113 let Some(reverse) = reverse_complement(query) else {
114 return Vec::new();
115 };
116 if m == 0 || m > n {
117 return Vec::new();
118 }
119 let text = if self.circular {
120 &self.text[..(n + m - 1).min(self.text.len())]
121 } else {
122 &self.text[..n]
123 };
124 let mut found = Vec::new();
125 let mut scan = |needle: &str, strand: Strand| {
126 let mut from = 0;
127 while let Some(offset) = text[from..].find(needle) {
128 let start = from + offset;
129 if start >= n {
130 break;
131 }
132 found.push(Occurrence {
133 start,
134 length: m,
135 strand,
136 });
137 from = start + 1;
138 }
139 };
140 scan(query, Strand::Forward);
141 if reverse != query {
142 scan(&reverse, Strand::Reverse);
143 }
144 found.sort_by_key(|o| (o.start, o.strand == Strand::Reverse));
145 found
146 }
147}
148
149#[must_use]
152pub fn semiglobal_edits(short: &[u8], long: &[u8], max: usize) -> Option<usize> {
153 let m = long.len();
154 let mut previous = vec![0usize; m + 1]; let mut current = vec![0usize; m + 1];
156 for (i, &a) in short.iter().enumerate() {
157 current[0] = i + 1;
158 let mut best = current[0];
159 for j in 1..=m {
160 let substitution = previous[j - 1] + usize::from(a != long[j - 1]);
161 current[j] = substitution.min(previous[j] + 1).min(current[j - 1] + 1);
162 best = best.min(current[j]);
163 }
164 if best > max {
165 return None;
166 }
167 std::mem::swap(&mut previous, &mut current);
168 }
169 previous.iter().copied().min().filter(|&edits| edits <= max)
170}
171
172#[must_use]
174pub fn translate_standard(sequence: &str) -> String {
175 let code = crate::translation::GeneticCode::standard();
176 sequence
177 .as_bytes()
178 .as_chunks::<3>()
179 .0
180 .iter()
181 .map(|c| code.translate_codon(*c))
182 .collect()
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn both_strands_origin_and_palindromes() {
191 let hits = find_exact("TACCGAACGGT", Topology::Linear, "ACCG");
193 assert_eq!(
194 hits,
195 vec![
196 Occurrence {
197 start: 1,
198 length: 4,
199 strand: Strand::Forward
200 },
201 Occurrence {
202 start: 7,
203 length: 4,
204 strand: Strand::Reverse
205 },
206 ]
207 );
208 assert_eq!(
209 find_exact("GAATTCGAATTC", Topology::Linear, "GAATTC").len(),
210 2
211 );
212 assert_eq!(
214 find_exact("CGTTTTTAC", Topology::Circular, "ACCG"),
215 vec![Occurrence {
216 start: 7,
217 length: 4,
218 strand: Strand::Forward
219 }]
220 );
221 assert_eq!(find_exact("CGTTTTTAC", Topology::Linear, "ACCG"), []);
222 assert_eq!(find_exact("ACG", Topology::Circular, "ACGT"), []);
223 }
224
225 #[test]
226 fn semiglobal_edits_align_the_short_one_fully() {
227 assert_eq!(
228 semiglobal_edits(b"ACGT", b"TTACGTTT", 0),
229 Some(0),
230 "exact containment"
231 );
232 assert_eq!(
233 semiglobal_edits(b"ACCT", b"TTACGTTT", 1),
234 Some(1),
235 "one substitution"
236 );
237 assert_eq!(
238 semiglobal_edits(b"ACGGT", b"TTACGTTT", 1),
239 Some(1),
240 "one insertion in the short one"
241 );
242 assert_eq!(
243 semiglobal_edits(b"AGT", b"TTACGTTT", 1),
244 Some(1),
245 "one deletion"
246 );
247 assert_eq!(semiglobal_edits(b"GGGG", b"TTACGTTT", 2), None, "bounded");
248 assert_eq!(translate_standard("ATGAAATAGC"), "MK*");
249 }
250
251 #[test]
252 fn canonical_is_strand_independent() {
253 assert_eq!(canonical("TTGCA"), canonical("TGCAA"));
254 assert_eq!(canonical("ACGN"), None);
255 assert_eq!(reverse_complement("AACG").as_deref(), Some("CGTT"));
256 }
257}