Skip to main content

segul_core/parser/
mod.rs

1//! All the parsers for the supported formats.
2#[cfg(feature = "maf")]
3pub mod bed;
4pub mod delimited;
5pub mod fasta;
6#[cfg(feature = "maf")]
7pub mod maf;
8pub mod nexus;
9pub mod partition;
10pub mod phylip;
11#[cfg(feature = "read")]
12pub mod qscores;
13pub mod txt;
14
15#[cfg(target_os = "windows")]
16const CAR_RETURN: u8 = b'\r';
17
18#[cfg(feature = "maf")]
19const END_OF_LINE: u8 = b'\n';
20#[cfg(feature = "maf")]
21const EOF: usize = 0;
22
23/// Upper bound for pre-allocating interleaved sequences,
24/// guarding against invalid NCHAR values in the header.
25pub(crate) const MAX_RESERVE: usize = 1 << 30;
26
27/// Split a matrix line into its ID and sequence.
28/// Returns None unless the line has exactly two
29/// whitespace-separated tokens.
30/// Works on bytes because `split_whitespace` decodes every
31/// character of the sequence, which is slow for long sequences.
32pub(crate) fn split_id_seq(line: &str) -> Option<(&str, &str)> {
33    let line = line.trim();
34    let split = line.bytes().position(|b| b.is_ascii_whitespace())?;
35    let (id, seq) = (&line[..split], line[split..].trim_start());
36    if has_whitespace(seq.as_bytes()) {
37        return None;
38    }
39    Some((id, seq))
40}
41
42fn has_whitespace(bytes: &[u8]) -> bool {
43    // ASCII whitespace and control characters are all <= b' '.
44    // Branch-free per chunk so the compiler can vectorize the scan.
45    // Matches are then confirmed with the exact check.
46    bytes
47        .chunks(64)
48        .any(|c| c.iter().fold(false, |acc, &b| acc | (b <= b' ')))
49        && bytes.iter().any(|b| b.is_ascii_whitespace())
50}
51
52macro_rules! insert_matrix {
53    ($self: ident, $id: ident, $seq: ident ) => {
54        match $self.matrix.get($id) {
55            Some(original_seq) => panic!(
56                "Found duplicate IDs for file {}. \
57                First duplicate ID found: {}. \
58                Both sequences are the same: {}.",
59                $self.input.display(),
60                $id,
61                original_seq == $seq
62            ),
63            None => {
64                $self.matrix.insert($id.to_string(), $seq.to_string());
65            }
66        }
67    };
68}
69
70macro_rules! warn_duplicate_ids {
71    ($self: ident, $ids: ident) => {
72        if $ids.len() != $self.header.ntax {
73            log::warn!(
74                "{} Found problematic alignment: {}.\n\
75                Number of taxa in the matrix ({} taxa) \
76                does not match the number of taxa in the header ({} taxa).\n\
77                If this is `id` subcommand, try to use the same file as an input \
78                for other subcommands.\n\
79                If the cause is duplicate IDs, \
80                the other subcommands will show the first found duplicate.\n\n",
81                "WARNING!".red(),
82                $self.input.display(),
83                $ids.len(),
84                $self.header.ntax
85            );
86        }
87    };
88}
89
90pub(crate) use insert_matrix;
91pub(crate) use warn_duplicate_ids;
92
93#[cfg(test)]
94mod test {
95    use super::*;
96
97    #[test]
98    fn test_split_id_seq() {
99        assert_eq!(Some(("ABCD", "ACGT")), split_id_seq("ABCD ACGT"));
100        assert_eq!(Some(("ABCD", "ACGT")), split_id_seq(" ABCD \t  ACGT\r\n"));
101        assert_eq!(None, split_id_seq("AB CD ACGT"));
102        assert_eq!(None, split_id_seq("ABCD ACG T"));
103        assert_eq!(None, split_id_seq("ABCD"));
104        assert_eq!(None, split_id_seq(""));
105    }
106}