Skip to main content

ciri_toolkit/
misd.rs

1//! Port of CIRI3 `Misd.java`.
2//!
3//! This module parses CIGAR strings using an efficient manual state machine
4//! instead of regex to determine the alignment type (SM, MS, SMS) and mapped length.
5//! The structure is intentionally close to the Java logic because Scan1 and Scan2
6//! both use the returned shape to decide candidate ordering and rescue behavior.
7
8/// Parses a CIGAR string and returns CIRI-style classification metadata.
9///
10/// # Arguments
11/// * `old_cigar` - The CIGAR string from a SAM/BAM record.
12/// * `seq_length` - The total length of the read sequence.
13///
14/// # Returns
15/// An array of 4 integers:
16/// 0: Type indicator (-1 for SM, 1 for MS, 10 for SMS, 0 for pure M).
17/// 1: Soft-clip length at the start (for SM/SMS) or match length (for MS).
18/// 2: Relative position index.
19/// 3: Total mapped length (sum of M and D operations).
20pub fn misd(old_cigar: &str, seq_length: i32) -> [i32; 4] {
21    let mut cigar_ite = [0, 0, 0, 0];
22
23    // Java parity: classNumStr.length==1 and classCIGAR="*" falls into "not M" => -1.
24    if old_cigar == "*" {
25        cigar_ite[3] = -1;
26        return cigar_ite;
27    }
28
29    // Standardize: Replace H (Hard clip) with S (Soft clip) conceptually for length.
30    let mut counts = Vec::with_capacity(4);
31    let mut ops = Vec::with_capacity(4);
32
33    let mut current_val = 0;
34    for c in old_cigar.chars() {
35        if c.is_digit(10) {
36            current_val = current_val * 10 + c.to_digit(10).unwrap() as i32;
37        } else {
38            // Only accept valid CIGAR operations
39            if matches!(c, 'M' | 'I' | 'D' | 'N' | 'S' | 'H' | 'P' | '=' | 'X') {
40                let op = if c == 'H' { 'S' } else { c };
41                counts.push(current_val);
42                ops.push(op);
43                current_val = 0;
44            } else {
45                // Invalid character in CIGAR string (like '*')
46                cigar_ite[3] = -2;
47                return cigar_ite;
48            }
49        }
50    }
51
52    // Safety check for empty CIGAR strings
53    if ops.is_empty() {
54        cigar_ite[3] = -2;
55        return cigar_ite;
56    }
57
58    if ops.len() == 1 {
59        if ops[0] == 'M' {
60            cigar_ite[3] = seq_length;
61        } else {
62            cigar_ite[3] = -1;
63        }
64    } else if ops.len() == 2 {
65        if ops[0] == 'M' && ops[1] == 'S' {
66            cigar_ite[0] = 1;
67            cigar_ite[1] = counts[0];
68            cigar_ite[2] = counts[0] - 1;
69            cigar_ite[3] = counts[0];
70        } else if ops[0] == 'S' && ops[1] == 'M' {
71            cigar_ite[0] = -1;
72            cigar_ite[1] = counts[0];
73            cigar_ite[2] = 0;
74            cigar_ite[3] = counts[1];
75        } else {
76            cigar_ite[3] = -2;
77        }
78    } else if ops.len() == 3 {
79        if ops[0] == 'S' && ops[1] == 'M' && ops[2] == 'S' {
80            cigar_ite[0] = 10;
81            cigar_ite[1] = counts[0];
82            cigar_ite[2] = counts[2];
83            cigar_ite[3] = counts[1];
84        } else if ops[0] == 'M' && ops[1] == 'D' && ops[2] == 'M' {
85            cigar_ite[3] = seq_length + counts[1];
86        } else if ops[0] == 'M' && ops[1] == 'I' && ops[2] == 'M' {
87            cigar_ite[3] = seq_length - counts[1];
88        } else {
89            cigar_ite[3] = -2;
90        }
91    } else if ops[0] == 'M' && *ops.last().unwrap() == 'S' {
92        let (mut m_sum, mut d_sum) = (0, 0);
93        for i in 0..ops.len() {
94            if ops[i] == 'M' {
95                m_sum += counts[i];
96            } else if ops[i] == 'D' {
97                d_sum += counts[i];
98            }
99        }
100        cigar_ite[0] = 1;
101        cigar_ite[1] = seq_length - counts.last().unwrap();
102        cigar_ite[2] = m_sum + d_sum - 1;
103        cigar_ite[3] = m_sum + d_sum;
104    } else if ops[0] == 'S' && *ops.last().unwrap() == 'M' {
105        let (mut m_sum, mut d_sum) = (0, 0);
106        for i in 1..ops.len() {
107            if ops[i] == 'M' {
108                m_sum += counts[i];
109            } else if ops[i] == 'D' {
110                d_sum += counts[i];
111            }
112        }
113        cigar_ite[0] = -1;
114        cigar_ite[1] = counts[0];
115        cigar_ite[2] = 0;
116        cigar_ite[3] = m_sum + d_sum;
117    } else if ops[0] == 'M' && *ops.last().unwrap() == 'M' {
118        let (mut m_sum, mut d_sum) = (0, 0);
119        for i in 0..ops.len() {
120            if ops[i] == 'M' {
121                m_sum += counts[i];
122            } else if ops[i] == 'D' {
123                d_sum += counts[i];
124            }
125        }
126        cigar_ite[3] = m_sum + d_sum;
127    } else if ops[0] == 'S' && *ops.last().unwrap() == 'S' {
128        let (mut m_sum, mut d_sum) = (0, 0);
129        for i in 1..ops.len() - 1 {
130            if ops[i] == 'M' {
131                m_sum += counts[i];
132            } else if ops[i] == 'D' {
133                d_sum += counts[i];
134            }
135        }
136        cigar_ite[0] = 10;
137        cigar_ite[1] = counts[0];
138        cigar_ite[2] = *counts.last().unwrap();
139        cigar_ite[3] = m_sum + d_sum;
140    } else {
141        cigar_ite[3] = -2;
142    }
143
144    cigar_ite
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_misd_match() {
153        let res = misd("100M", 100);
154        assert_eq!(res, [0, 0, 0, 100]);
155    }
156
157    #[test]
158    fn test_misd_invalid() {
159        let res = misd("*", 100);
160        assert_eq!(res[3], -1);
161    }
162
163    #[test]
164    fn test_misd_soft_clip_start() {
165        let res = misd("20S80M", 100);
166        assert_eq!(res, [-1, 20, 0, 80]);
167    }
168
169    #[test]
170    fn test_misd_soft_clip_end() {
171        let res = misd("80M20S", 100);
172        assert_eq!(res, [1, 80, 79, 80]);
173    }
174
175    #[test]
176    fn test_misd_insertion() {
177        let res = misd("40M2I58M", 100);
178        assert_eq!(res, [0, 0, 0, 98]);
179    }
180
181    #[test]
182    fn test_misd_deletion() {
183        let res = misd("40M2D58M", 98);
184        assert_eq!(res, [0, 0, 0, 100]);
185    }
186
187    #[test]
188    fn test_misd_complex() {
189        let res = misd("10S30M2I40M20S", 102);
190        assert_eq!(res, [10, 10, 20, 70]);
191    }
192}