1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
use std::mem::size_of;

use fancy_regex::Regex;

const FUZZY_CHARACTER: &str = ".";
const ADAPTER_PATTERN_REGEX: &str = r"(?<!\[)\b[atgcryswkmbdhvn]+\b(?!\])";

pub fn generate_sequences_with_pcr_errors(string: &str, errors_num: &usize) -> Vec<String> {
    if *errors_num == 0 {
        return vec![string.to_string().to_ascii_uppercase()];
    }

    if string.is_empty() {
        return Vec::new();
    }

    if *errors_num >= string.len() {
        return vec![FUZZY_CHARACTER.repeat(string.len())];
    }

    let num_chars = string.chars().count();
    assert!(num_chars <= usize::BITS as usize * 8, "too many characters");

    let max_permutation_mask = usize::MAX
        .checked_shr(size_of::<usize>() as u32 * 8 - num_chars as u32)
        .unwrap();

    let mut cases = Vec::new();

    let upper: Vec<char> = string.chars().map(|c| c.to_ascii_uppercase()).collect();

    for permutation_mask in 0..=max_permutation_mask {
        if permutation_mask.count_ones() as usize != num_chars - errors_num {
            continue;
        }
        let mut s = String::new();
        for (idx, _) in upper.iter().enumerate().take(num_chars) {
            if (permutation_mask & (1 << idx)) == 0 {
                s.push_str(FUZZY_CHARACTER)
            } else {
                s.push(upper[idx])
            }
        }
        cases.push(s);
    }

    cases
}

pub fn get_with_errors(pattern: &str, max_error: &usize) -> String {
    let regex_pattern = Regex::new(ADAPTER_PATTERN_REGEX).unwrap();

    let mut result = String::new();
    let mut last_end = 0;

    for mat in regex_pattern.find_iter(pattern) {
        let mat = mat.unwrap();
        result.push_str(&pattern[last_end..mat.start()]);

        let fuzzy_patterns = generate_sequences_with_pcr_errors(mat.as_str(), max_error);
        result.push_str(&format!("({})", fuzzy_patterns.join("|")));

        last_end = mat.end();
    }

    result.push_str(&pattern[last_end..]);
    result
}

#[cfg(test)]
mod tests {
    use rstest::rstest;

    use crate::pattern;

    #[rstest]
    #[case(vec!["."], "a", 1)]
    #[case(vec!["A"], "a", 0)]
    #[case(vec![], "", 1)]
    #[case(vec!["AAA.", "AA.A", "A.AA", ".AAA"], "AAAA", 1)]
    #[case(vec!["..."], "AAA", 3)]
    #[case(vec!["..."], "AAA", 4)]
    fn test_generate_sequences_with_pcr_errors(
        #[case] expected: Vec<&str>,
        #[case] text: &str,
        #[case] errors_num: usize,
    ) {
        assert_eq!(
            expected,
            pattern::generate_sequences_with_pcr_errors(text, &errors_num)
        );
    }

    #[rstest]
    #[case("^(AA.|A.A|.AA)(?P<UMI>[ATGCN]{3})", "^aaa(?P<UMI>[ATGCN]{3})", 1)]
    #[case("^(...)(?P<UMI>[ATGCN]{3})", "^aaa(?P<UMI>[ATGCN]{3})", 3)]
    #[case("^(...)(?P<UMI>[ATGCN]{3})", "^aaa(?P<UMI>[ATGCN]{3})", 4)]
    #[case("^((...))(?P<UMI>[ATGCN]{3})", "^(aaa)(?P<UMI>[ATGCN]{3})", 4)]
    #[case(
        "^(AA.|A.A|.AA)(?P<UMI>[ATGCN]{3})CCC",
        "^aaa(?P<UMI>[ATGCN]{3})CCC",
        1
    )]
    #[case("^(?P<UMI>[ATGCN]{3})", "^(?P<UMI>[ATGCN]{3})", 1)]
    fn test_create_fuzzy(#[case] expected: &str, #[case] pattern: &str, #[case] max_error: usize) {
        assert_eq!(expected, pattern::get_with_errors(pattern, &max_error))
    }
}