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
81pub 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}