Skip to main content

cuttlefish_rs/
dna.rs

1#[repr(u8)]
2#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3pub enum Base {
4    A = 0,
5    C = 1,
6    G = 2,
7    T = 3,
8    N = 4,
9    E = 5,
10}
11
12pub const INVALID_BASE_BITS: u8 = 4;
13
14pub const ASCII_BASE_BITS: [u8; 256] = {
15    let mut bits = [INVALID_BASE_BITS; 256];
16    bits[b'A' as usize] = 0;
17    bits[b'a' as usize] = 0;
18    bits[b'C' as usize] = 1;
19    bits[b'c' as usize] = 1;
20    bits[b'G' as usize] = 2;
21    bits[b'g' as usize] = 2;
22    bits[b'T' as usize] = 3;
23    bits[b't' as usize] = 3;
24    bits[b'U' as usize] = 3;
25    bits[b'u' as usize] = 3;
26    bits
27};
28
29impl Base {
30    #[inline]
31    pub const fn complement(self) -> Self {
32        match self {
33            Self::A => Self::T,
34            Self::C => Self::G,
35            Self::G => Self::C,
36            Self::T => Self::A,
37            Self::N => Self::N,
38            Self::E => Self::E,
39        }
40    }
41
42    #[inline]
43    pub const fn to_ascii(self) -> u8 {
44        match self {
45            Self::A => b'A',
46            Self::C => b'C',
47            Self::G => b'G',
48            Self::T => b'T',
49            Self::N => b'N',
50            Self::E => b'$',
51        }
52    }
53
54    #[inline]
55    pub const fn from_ascii(byte: u8) -> Self {
56        match byte {
57            b'A' | b'a' => Self::A,
58            b'C' | b'c' => Self::C,
59            b'G' | b'g' => Self::G,
60            b'T' | b't' | b'U' | b'u' => Self::T,
61            _ => Self::N,
62        }
63    }
64
65    #[inline]
66    pub const fn is_dna(self) -> bool {
67        matches!(self, Self::A | Self::C | Self::G | Self::T)
68    }
69
70    #[inline]
71    pub const fn bits(self) -> u8 {
72        self as u8
73    }
74}
75
76#[inline]
77pub const fn is_dna_ascii(byte: u8) -> bool {
78    ASCII_BASE_BITS[byte as usize] != INVALID_BASE_BITS
79}
80
81/// ASCII complement for every byte, built from [`Base::from_ascii`] so the table
82/// is exactly equivalent to computing it, including for non-DNA bytes.
83///
84/// Reverse-strand label assembly complements one base at a time in its inner
85/// loop; going through the enum compiles to range checks and a jump table, while
86/// this is a single indexed load.
87pub const ASCII_COMPLEMENT: [u8; 256] = {
88    let mut table = [0u8; 256];
89    let mut byte = 0usize;
90    while byte < 256 {
91        table[byte] = Base::from_ascii(byte as u8).complement().to_ascii();
92        byte += 1;
93    }
94    table
95};
96
97#[inline]
98pub const fn complement_ascii(byte: u8) -> u8 {
99    ASCII_COMPLEMENT[byte as usize]
100}
101
102#[inline]
103pub const fn ascii_base_bits(byte: u8) -> Option<u8> {
104    let bits = ASCII_BASE_BITS[byte as usize];
105    if bits == INVALID_BASE_BITS {
106        None
107    } else {
108        Some(bits)
109    }
110}
111
112#[inline]
113pub const fn valid_ascii_base_bits(byte: u8) -> u8 {
114    ((byte >> 2) ^ (byte >> 1)) & 0b11
115}
116
117#[inline]
118pub const fn ascii_complement_bits(byte: u8) -> Option<u8> {
119    match ascii_base_bits(byte) {
120        Some(bits) => Some(bits ^ 0b11),
121        None => None,
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn ascii_mapping_matches_cpp_encoding() {
131        assert_eq!(Base::from_ascii(b'A').bits(), 0);
132        assert_eq!(Base::from_ascii(b'C').bits(), 1);
133        assert_eq!(Base::from_ascii(b'G').bits(), 2);
134        assert_eq!(Base::from_ascii(b'T').bits(), 3);
135        assert_eq!(Base::from_ascii(b'N'), Base::N);
136    }
137
138    #[test]
139    fn complement_table_matches_enum_path() {
140        for byte in 0..=u8::MAX {
141            assert_eq!(
142                complement_ascii(byte),
143                Base::from_ascii(byte).complement().to_ascii(),
144                "complement table diverges at byte {byte}"
145            );
146        }
147    }
148
149    #[test]
150    fn complements_are_involutions() {
151        for b in [Base::A, Base::C, Base::G, Base::T, Base::N, Base::E] {
152            assert_eq!(b.complement().complement(), b);
153        }
154    }
155}