1use itertools::Itertools;
31use serde::{Deserialize, Serialize};
32use std::fmt::Write;
33
34use crate::*;
35
36#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
38pub enum CigarOp {
39 Match,
41 Sub,
43 Del,
45 Ins,
47}
48
49#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
51pub enum CigarOpChars {
52 Match(u8),
54 Sub(u8, u8),
56 Del(u8),
58 Ins(u8),
60}
61
62#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
64pub struct CigarElem {
65 pub op: CigarOp,
66 pub cnt: I,
67}
68
69impl CigarElem {
70 pub fn new(op: CigarOp, cnt: I) -> Self {
71 Self { op, cnt }
72 }
73}
74
75#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
81pub struct Cigar {
82 pub ops: Vec<CigarElem>,
83}
84
85impl CigarOp {
86 pub fn to_char(&self) -> char {
88 match self {
89 CigarOp::Match => '=',
90 CigarOp::Sub => 'X',
91 CigarOp::Ins => 'I',
92 CigarOp::Del => 'D',
93 }
94 }
95
96 #[inline(always)]
98 pub fn delta(&self) -> Pos {
99 match self {
100 CigarOp::Match | CigarOp::Sub => Pos(1, 1),
101 CigarOp::Del => Pos(1, 0), CigarOp::Ins => Pos(0, 1), }
104 }
105
106 #[inline(always)]
110 pub fn edit_cost(&self) -> Cost {
111 match self {
112 CigarOp::Match => 0,
113 _ => 1,
114 }
115 }
116
117 #[inline(always)]
121 pub fn from_delta(delta: Pos) -> Self {
122 match delta {
123 Pos(0, 1) => CigarOp::Ins,
124 Pos(1, 0) => CigarOp::Del,
125 Pos(1, 1) => CigarOp::Match,
126 _ => panic!("Invalid delta: {:?}", delta),
127 }
128 }
129}
130
131impl std::ops::Mul<I> for Pos {
133 type Output = Pos;
134 fn mul(self, rhs: I) -> Pos {
135 Pos(self.0 * rhs, self.1 * rhs)
136 }
137}
138
139impl From<u8> for CigarOp {
140 fn from(op: u8) -> Self {
144 match op {
145 b'=' | b'M' => CigarOp::Match,
146 b'X' => CigarOp::Sub,
147 b'I' => CigarOp::Ins,
148 b'D' => CigarOp::Del,
149 _ => panic!("Invalid CigarOp"),
150 }
151 }
152}
153
154impl ToString for Cigar {
155 fn to_string(&self) -> String {
157 let mut s = String::new();
158 for elem in &self.ops {
159 write!(&mut s, "{}{}", elem.cnt, elem.op.to_char()).unwrap();
160 }
161 s
162 }
163}
164
165impl Cigar {
166 pub fn from_ops(ops: impl Iterator<Item = CigarOp>) -> Self {
168 Cigar {
169 ops: ops
170 .chunk_by(|&op| op)
171 .into_iter()
172 .map(|(op, group)| CigarElem::new(op, group.count() as _))
173 .collect(),
174 }
175 }
176
177 pub fn from_path(text: Seq, pattern: Seq, path: &Path) -> Cigar {
183 if path[0] != Pos(0, 0) {
184 panic!("Path must start at (0,0)!");
185 }
186 Self::resolve_matches(
187 path.iter()
188 .tuple_windows()
189 .map(|(&text_pos, &pattern_pos)| {
190 CigarElem::new(CigarOp::from_delta(pattern_pos - text_pos), 1)
191 }),
192 text,
193 pattern,
194 )
195 }
196
197 pub fn to_char_pairs<'s>(&'s self, text: &'s [u8], pattern: &'s [u8]) -> Vec<CigarOpChars> {
199 let mut pos = Pos(0, 0);
200 let mut out = vec![];
202 for el in &self.ops {
203 for _ in 0..el.cnt {
204 let c = match el.op {
205 CigarOp::Match => {
206 CigarOpChars::Match(text[pos.0 as usize])
213 }
214 CigarOp::Sub => {
215 assert_ne!(
217 text[pos.0 as usize] as char,
218 pattern[pos.1 as usize] as char,
219 "cigar {:?}\npattern {:?}\ntext {:?}\nmismatch for {pos:?}",
220 self.to_string(),
221 String::from_utf8_lossy(pattern),
222 String::from_utf8_lossy(text)
223 );
224 CigarOpChars::Sub(text[pos.0 as usize], pattern[pos.1 as usize])
225 }
226 CigarOp::Del => {
227 CigarOpChars::Del(text[pos.0 as usize])
229 }
230 CigarOp::Ins => {
231 CigarOpChars::Ins(pattern[pos.1 as usize])
233 }
234 };
235 out.push(c);
236 pos += el.op.delta();
237 }
238 }
239 out
240 }
241
242 pub fn to_path(&self) -> Path {
244 let mut pos = Pos(0, 0);
245 let mut path = vec![pos];
246 for el in &self.ops {
247 for _ in 0..el.cnt {
248 pos += el.op.delta();
249 path.push(pos);
250 }
251 }
252 path
253 }
254
255 pub fn to_path_with_costs(&self, cm: CostModel) -> Vec<(Pos, Cost)> {
257 let mut pos = Pos(0, 0);
258 let mut cost = 0;
259 let mut path = vec![(pos, cost)];
260
261 for el in &self.ops {
262 match el.op {
263 CigarOp::Match => {
264 for _ in 0..el.cnt {
265 pos += el.op.delta();
266 path.push((pos, cost));
267 }
268 }
269 CigarOp::Sub => {
270 for _ in 0..el.cnt {
271 pos += el.op.delta();
272 cost += cm.sub;
273 path.push((pos, cost));
274 }
275 }
276 CigarOp::Ins => {
277 for len in 1..=(el.cnt as Cost) {
278 pos += el.op.delta();
279 path.push((pos, cost + cm.ins(len)));
280 }
281 cost += cm.ins(el.cnt);
282 }
283 CigarOp::Del => {
284 for len in 1..=(el.cnt as Cost) {
285 pos += el.op.delta();
286 path.push((pos, cost + cm.del(len)));
287 }
288 cost += cm.del(el.cnt);
289 }
290 }
291 }
292 path
293 }
294
295 pub fn push(&mut self, op: CigarOp) {
297 if let Some(s) = self.ops.last_mut() {
298 if s.op == op {
299 s.cnt += 1;
300 return;
301 }
302 }
303 self.ops.push(CigarElem { op, cnt: 1 });
304 }
305
306 pub fn pop_op(&mut self) -> Option<CigarOp> {
308 while let Some(elem) = self.ops.last_mut() {
309 let op = elem.op;
310 assert!(elem.cnt > 0);
311 elem.cnt -= 1;
312 if elem.cnt == 0 {
313 self.ops.pop();
314 }
315 return Some(op);
316 }
317 None
318 }
319
320 pub fn push_elem(&mut self, e: CigarElem) {
322 if let Some(s) = self.ops.last_mut() {
323 if s.op == e.op {
324 s.cnt += e.cnt;
325 return;
326 }
327 }
328 self.ops.push(e);
329 }
330
331 pub fn push_matches(&mut self, cnt: I) {
333 if let Some(s) = self.ops.last_mut() {
334 if s.op == CigarOp::Match {
335 s.cnt += cnt;
336 return;
337 }
338 }
339 self.ops.push(CigarElem {
340 op: CigarOp::Match,
341 cnt: cnt as _,
342 });
343 }
344
345 pub fn verify(&self, cm: &CostModel, text: Seq, pattern: Seq) -> Result<Cost, &str> {
347 let mut pos = Pos(0, 0);
348 let mut cost: Cost = 0;
349
350 for &CigarElem { op, cnt } in &self.ops {
351 match op {
352 CigarOp::Match => {
353 for _ in 0..cnt {
354 if text.get(pos.0 as usize) != pattern.get(pos.1 as usize) {
355 return Err("Expected match but found substitution.");
356 }
357 pos += op.delta();
358 }
359 }
360 CigarOp::Sub => {
361 for _ in 0..cnt {
362 if text.get(pos.0 as usize) == pattern.get(pos.1 as usize) {
363 return Err("Expected substitution but found match.");
364 }
365 pos += op.delta();
366 cost += cm.sub;
367 }
368 }
369 CigarOp::Ins => {
370 cost += cm.open + cnt as Cost * cm.extend;
371 pos += op.delta() * cnt;
372 }
373 CigarOp::Del => {
374 cost += cm.open + cnt as Cost * cm.extend;
375 pos += op.delta() * cnt;
376 }
377 }
378 }
379 if pos != Pos(text.len() as I, pattern.len() as I) {
380 return Err("Wrong alignment length.");
381 }
382
383 Ok(cost)
384 }
385
386 pub fn resolve_matches(ops: impl Iterator<Item = CigarElem>, text: Seq, pattern: Seq) -> Self {
388 let mut pos = Pos(0, 0);
389 let mut c = Cigar { ops: vec![] };
390 for CigarElem { op, cnt } in ops {
391 match op {
392 CigarOp::Match => {
393 for _ in 0..cnt {
394 c.push(if text[pos.0 as usize] == pattern[pos.1 as usize] {
395 CigarOp::Match
396 } else {
397 CigarOp::Sub
398 });
399 pos += op.delta();
400 }
401 continue;
402 }
403 _ => {
404 pos += op.delta() * cnt;
405 }
406 };
407 c.push_elem(CigarElem { op, cnt });
408 }
409 c
410 }
411
412 pub fn parse_without_counts(s: &str, text: Seq, pattern: Seq) -> Self {
415 Self::resolve_matches(
416 s.as_bytes().iter().map(|&op| CigarElem {
417 op: op.into(),
418 cnt: 1,
419 }),
420 text,
421 pattern,
422 )
423 }
424
425 pub fn parse_without_resolving(s: &str) -> Self {
428 let mut c = Cigar { ops: vec![] };
429 for &op in s.as_bytes() {
430 c.push(op.into())
431 }
432 c
433 }
434
435 pub fn from_string(s: &str) -> Self {
437 let mut c = Cigar { ops: vec![] };
438 for slice in s.as_bytes().split_inclusive(|b| !b.is_ascii_digit()) {
439 let (&op, cnt_bytes) = slice.split_last().expect("Cigar string cannot be empty");
440 let cnt = if cnt_bytes.is_empty() {
441 1
442 } else {
443 unsafe { std::str::from_utf8_unchecked(cnt_bytes) }
444 .parse()
445 .expect("Invalid Cigar count")
446 };
447 c.push_elem(CigarElem { op: op.into(), cnt });
448 }
449 c
450 }
451
452 pub fn parse(s: &str, text: Seq, pattern: Seq) -> Self {
455 Self::resolve_matches(
456 s.as_bytes()
457 .split_inclusive(|pattern| pattern.is_ascii_alphabetic())
458 .map(|pattern_slice| {
459 let (&op, cnt) = pattern_slice.split_last().unwrap();
460 let cnt = if cnt.is_empty() {
461 1
462 } else {
463 unsafe { std::str::from_utf8_unchecked(cnt) }
464 .parse()
465 .unwrap()
466 };
467 CigarElem { op: op.into(), cnt }
468 }),
469 text,
470 pattern,
471 )
472 }
473
474 pub fn clear(&mut self) {
476 self.ops.clear();
477 }
478
479 pub fn reverse(&mut self) {
481 self.ops.reverse();
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 #[test]
490 fn test_delta() {
491 for op in [CigarOp::Match, CigarOp::Del, CigarOp::Ins] {
492 assert_eq!(CigarOp::from_delta(op.delta()), op);
493 }
494 assert_eq!(CigarOp::from_delta(Pos(1, 1)), CigarOp::Match);
495 assert_eq!(CigarOp::from_delta(Pos(1, 0)), CigarOp::Del); assert_eq!(CigarOp::from_delta(Pos(0, 1)), CigarOp::Ins); }
499
500 #[test]
501 fn test_valid_eq() {
502 let c = Cigar::from_path(b"ab", b"aa", &vec![Pos(0, 0), Pos(1, 1), Pos(2, 2)]);
503 assert_eq!(c.to_string(), "1=1X");
504 }
505
506 #[test]
507 fn test_invalid_end_length() {
508 let c = Cigar::from_path(b"ab", b"aa", &vec![Pos(0, 0), Pos(0, 1)]);
510 assert!(c.verify(&CostModel::unit(), b"ab", b"aa").is_err());
511 }
512
513 #[test]
514 fn to_string() {
515 let c = Cigar {
516 ops: vec![
517 CigarElem {
518 op: CigarOp::Ins,
519 cnt: 1,
520 },
521 CigarElem {
522 op: CigarOp::Match,
523 cnt: 2,
524 },
525 ],
526 };
527 assert_eq!(c.to_string(), "1I2=");
528 }
529
530 #[test]
531 fn from_path() {
532 let c = Cigar::from_path(
533 b"aaa",
534 b"aabc",
535 &vec![Pos(0, 0), Pos(1, 1), Pos(2, 2), Pos(3, 3), Pos(3, 4)],
536 );
537 assert_eq!(c.to_string(), "2=1X1I");
538 }
539
540 #[test]
541 fn from_string_with_count() {
542 let c = Cigar::from_string("24=");
543 assert_eq!(c.ops.len(), 1);
544 assert_eq!(c.ops[0], CigarElem::new(CigarOp::Match, 24));
545 }
546
547 #[test]
548 fn from_string_mixed_ops() {
549 let c = Cigar::from_string("2=3I1X");
550 assert_eq!(
551 c.ops,
552 vec![
553 CigarElem::new(CigarOp::Match, 2),
554 CigarElem::new(CigarOp::Ins, 3),
555 CigarElem::new(CigarOp::Sub, 1)
556 ]
557 );
558 }
559
560 #[test]
561 fn from_string_no_counts() {
562 let c = Cigar::from_string("=XIDDD");
563 assert_eq!(
564 c.ops,
565 vec![
566 CigarElem::new(CigarOp::Match, 1),
567 CigarElem::new(CigarOp::Sub, 1),
568 CigarElem::new(CigarOp::Ins, 1),
569 CigarElem::new(CigarOp::Del, 3),
570 ]
571 );
572 }
573
574 #[test]
575 #[rustfmt::skip]
576 fn push_to_path() {
577 let mut c = Cigar::default();
578 c.push(CigarOp::Match); c.push(CigarOp::Del); c.push(CigarOp::Ins); c.push(CigarOp::Sub); assert_eq!(
585 c.to_path(),
586 [
587 Pos(0, 0),
588 Pos(1, 1),
589 Pos(2, 1),
590 Pos(2, 2),
591 Pos(3, 3),
592 ]
593 );
594 }
595
596 #[test]
597 fn to_char_pairs_all_match() {
598 let c = Cigar::from_string("3=");
599 let pairs = c.to_char_pairs(b"aaa", b"aaa");
600 assert_eq!(
601 pairs,
602 vec![
603 CigarOpChars::Match(b'a'),
604 CigarOpChars::Match(b'a'),
605 CigarOpChars::Match(b'a'),
606 ]
607 );
608 }
609
610 #[test]
611 fn to_char_pairs_sub() {
612 let c = Cigar::from_string("1X");
613 let pairs = c.to_char_pairs(b"a", b"c");
614 assert_eq!(pairs, vec![CigarOpChars::Sub(b'a', b'c')]);
615 }
616
617 #[test]
618 fn to_char_pairs_ins() {
619 let c = Cigar::from_string("1=1I1=");
620 let pairs = c.to_char_pairs(b"ac", b"abc");
621 assert_eq!(
622 pairs,
623 vec![
624 CigarOpChars::Match(b'a'),
625 CigarOpChars::Ins(b'b'),
626 CigarOpChars::Match(b'c'),
627 ]
628 );
629 }
630
631 #[test]
632 fn to_char_pairs_del() {
633 let c = Cigar::from_string("1=1D1=");
634 let pairs = c.to_char_pairs(b"abc", b"ac");
635 assert_eq!(
636 pairs,
637 vec![
638 CigarOpChars::Match(b'a'),
639 CigarOpChars::Del(b'b'),
640 CigarOpChars::Match(b'c'),
641 ]
642 );
643 }
644
645 #[test]
646 fn to_char_pairs_mixed() {
647 let c = Cigar::from_string("2=1X1I");
648 let pairs = c.to_char_pairs(b"abZd", b"abYc");
649 assert_eq!(
650 pairs,
651 vec![
652 CigarOpChars::Match(b'a'),
653 CigarOpChars::Match(b'b'),
654 CigarOpChars::Sub(b'Z', b'Y'),
655 CigarOpChars::Ins(b'c'),
656 ]
657 );
658 }
659}