Skip to main content

ferritin_core/info/
amino_acids.rs

1//! Amino acid conversion utilities.
2//!
3//! Single-letter ↔ three-letter ↔ integer mappings for the 20 standard amino
4//! acids plus the catch-all `UNK` / `'X'` / index 20.
5
6use strum::EnumIter;
7
8/// One-letter code alphabet for the 20 standard amino acids plus UNK.
9pub const ALPHABET: [char; 21] = [
10    'A', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W',
11    'Y', 'X',
12];
13
14/// The 20 standard amino acids plus Unknown, with integer discriminants
15/// matching the conventional protein-ML encoding (Ala=0 … Unknown=20).
16#[repr(u8)]
17#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter)]
18pub enum AminoAcid {
19    Ala = 0,
20    Cys = 1,
21    Asp = 2,
22    Glu = 3,
23    Phe = 4,
24    Gly = 5,
25    His = 6,
26    Ile = 7,
27    Lys = 8,
28    Leu = 9,
29    Met = 10,
30    Asn = 11,
31    Pro = 12,
32    Gln = 13,
33    Arg = 14,
34    Ser = 15,
35    Thr = 16,
36    Val = 17,
37    Trp = 18,
38    Tyr = 19,
39    Unknown = 20,
40}
41
42impl AminoAcid {
43    /// Parse a three-letter code (e.g. `"ALA"`). Unknown/missing → `Unknown`.
44    #[rustfmt::skip]
45    pub fn from_three_letter(aa: &str) -> Self {
46        match aa {
47            "ALA" => Self::Ala, "CYS" => Self::Cys, "ASP" => Self::Asp,
48            "GLU" => Self::Glu, "PHE" => Self::Phe, "GLY" => Self::Gly,
49            "HIS" => Self::His, "ILE" => Self::Ile, "LYS" => Self::Lys,
50            "LEU" => Self::Leu, "MET" => Self::Met, "ASN" => Self::Asn,
51            "PRO" => Self::Pro, "GLN" => Self::Gln, "ARG" => Self::Arg,
52            "SER" => Self::Ser, "THR" => Self::Thr, "VAL" => Self::Val,
53            "TRP" => Self::Trp, "TYR" => Self::Tyr, _    => Self::Unknown,
54        }
55    }
56
57    /// Parse a one-letter code (e.g. `'A'`). Unknown/missing → `Unknown`.
58    #[rustfmt::skip]
59    pub fn from_one_letter(aa: char) -> Self {
60        match aa {
61            'A' => Self::Ala, 'C' => Self::Cys, 'D' => Self::Asp,
62            'E' => Self::Glu, 'F' => Self::Phe, 'G' => Self::Gly,
63            'H' => Self::His, 'I' => Self::Ile, 'K' => Self::Lys,
64            'L' => Self::Leu, 'M' => Self::Met, 'N' => Self::Asn,
65            'P' => Self::Pro, 'Q' => Self::Gln, 'R' => Self::Arg,
66            'S' => Self::Ser, 'T' => Self::Thr, 'V' => Self::Val,
67            'W' => Self::Trp, 'Y' => Self::Tyr, _   => Self::Unknown,
68        }
69    }
70
71    /// Parse from integer index (0–20). Out-of-range → `Unknown`.
72    pub fn from_index(idx: u32) -> Self {
73        match idx {
74            0 => Self::Ala,
75            1 => Self::Cys,
76            2 => Self::Asp,
77            3 => Self::Glu,
78            4 => Self::Phe,
79            5 => Self::Gly,
80            6 => Self::His,
81            7 => Self::Ile,
82            8 => Self::Lys,
83            9 => Self::Leu,
84            10 => Self::Met,
85            11 => Self::Asn,
86            12 => Self::Pro,
87            13 => Self::Gln,
88            14 => Self::Arg,
89            15 => Self::Ser,
90            16 => Self::Thr,
91            17 => Self::Val,
92            18 => Self::Trp,
93            19 => Self::Tyr,
94            _ => Self::Unknown,
95        }
96    }
97
98    /// One-letter code (e.g. `'A'` for Ala, `'X'` for Unknown).
99    #[rustfmt::skip]
100    pub fn one_letter(self) -> char {
101        match self {
102            Self::Ala => 'A', Self::Cys => 'C', Self::Asp => 'D',
103            Self::Glu => 'E', Self::Phe => 'F', Self::Gly => 'G',
104            Self::His => 'H', Self::Ile => 'I', Self::Lys => 'K',
105            Self::Leu => 'L', Self::Met => 'M', Self::Asn => 'N',
106            Self::Pro => 'P', Self::Gln => 'Q', Self::Arg => 'R',
107            Self::Ser => 'S', Self::Thr => 'T', Self::Val => 'V',
108            Self::Trp => 'W', Self::Tyr => 'Y', Self::Unknown => 'X',
109        }
110    }
111
112    /// Three-letter code (e.g. `"ALA"` for Ala, `"UNK"` for Unknown).
113    #[rustfmt::skip]
114    pub fn three_letter(self) -> &'static str {
115        match self {
116            Self::Ala => "ALA", Self::Cys => "CYS", Self::Asp => "ASP",
117            Self::Glu => "GLU", Self::Phe => "PHE", Self::Gly => "GLY",
118            Self::His => "HIS", Self::Ile => "ILE", Self::Lys => "LYS",
119            Self::Leu => "LEU", Self::Met => "MET", Self::Asn => "ASN",
120            Self::Pro => "PRO", Self::Gln => "GLN", Self::Arg => "ARG",
121            Self::Ser => "SER", Self::Thr => "THR", Self::Val => "VAL",
122            Self::Trp => "TRP", Self::Tyr => "TYR", Self::Unknown => "UNK",
123        }
124    }
125
126    /// Integer index (discriminant), 0–20.
127    pub fn index(self) -> usize {
128        self as usize
129    }
130}
131
132// ── Backward-compatible free functions ───────────────────────────────────────
133
134/// Convert a 3-letter amino acid code to a 1-letter code.
135pub fn aa3to1(aa: &str) -> char {
136    AminoAcid::from_three_letter(aa).one_letter()
137}
138
139/// Convert a 1-letter amino acid code to an integer index (0–20, UNK→20).
140pub fn aa1to_int(aa: char) -> u32 {
141    AminoAcid::from_one_letter(aa).index() as u32
142}
143
144/// Convert an integer index (0–20) to a 1-letter amino acid code.
145pub fn int_to_aa1(aa_int: u32) -> char {
146    AminoAcid::from_index(aa_int).one_letter()
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_aa3to1_roundtrip() {
155        assert_eq!(aa3to1("ALA"), 'A');
156        assert_eq!(aa3to1("TRP"), 'W');
157        assert_eq!(aa3to1("UNK"), 'X');
158        assert_eq!(aa3to1("???"), 'X');
159    }
160
161    #[test]
162    fn test_aa1to_int_roundtrip() {
163        for (i, &ch) in ALPHABET.iter().enumerate() {
164            assert_eq!(aa1to_int(ch) as usize, i, "ALPHABET[{i}] = {ch}");
165        }
166        assert_eq!(aa1to_int('?'), 20);
167    }
168
169    #[test]
170    fn test_int_to_aa1_roundtrip() {
171        for i in 0u32..=20 {
172            let ch = int_to_aa1(i);
173            assert_eq!(aa1to_int(ch), i, "round-trip failed at index {i}");
174        }
175        assert_eq!(int_to_aa1(99), 'X');
176    }
177
178    #[test]
179    fn test_amino_acid_enum_roundtrip() {
180        use strum::IntoEnumIterator;
181        for aa in AminoAcid::iter() {
182            let idx = aa.index() as u32;
183            assert_eq!(AminoAcid::from_index(idx), aa);
184            assert_eq!(AminoAcid::from_one_letter(aa.one_letter()), aa);
185            if aa != AminoAcid::Unknown {
186                assert_eq!(AminoAcid::from_three_letter(aa.three_letter()), aa);
187            }
188        }
189    }
190}