Skip to main content

imgt/
regions.rs

1use std::{fmt::Display, str::FromStr, sync::Arc};
2
3use bincode::{Decode, Encode};
4use mzcore::sequence::{Annotation, Peptidoform, Region, UnAmbiguous};
5use serde::{Deserialize, Serialize};
6
7use super::species::Species;
8use crate::Allele;
9
10/// A selection of germlines from a single species.
11#[derive(Clone, Debug, Decode, Deserialize, Encode, Serialize)]
12pub struct Germlines {
13    /// The species of this selection of germlines
14    pub species: Species,
15    /// All heavy chain options
16    pub h: Chain,
17    /// All kappa light chain options
18    pub k: Chain,
19    /// All lambda light chain options
20    pub l: Chain,
21    /// All iota chain options
22    pub i: Chain,
23}
24
25impl Germlines {
26    /// Create a new empty collection
27    pub fn new(species: Species) -> Self {
28        Self {
29            species,
30            h: Chain::default(),
31            k: Chain::default(),
32            l: Chain::default(),
33            i: Chain::default(),
34        }
35    }
36
37    /// Insert a germline into this collection
38    pub fn insert(&mut self, germline: Germline) {
39        match &germline.name.chain {
40            ChainType::Heavy => self.h.insert(germline),
41            ChainType::LightKappa => self.k.insert(germline),
42            ChainType::LightLambda => self.l.insert(germline),
43            ChainType::Iota => self.i.insert(germline),
44        }
45    }
46}
47
48impl<'a> IntoIterator for &'a Germlines {
49    type IntoIter = std::array::IntoIter<(ChainType, &'a Chain), 4>;
50    type Item = (ChainType, &'a Chain);
51
52    fn into_iter(self) -> Self::IntoIter {
53        [
54            (ChainType::Heavy, &self.h),
55            (ChainType::LightKappa, &self.k),
56            (ChainType::LightLambda, &self.l),
57            (ChainType::Iota, &self.i),
58        ]
59        .into_iter()
60    }
61}
62
63impl Germlines {
64    /// Iterate over the chains
65    pub fn iter(
66        &self,
67    ) -> impl DoubleEndedIterator<Item = (ChainType, &Chain)> + ExactSizeIterator + '_ {
68        [
69            (ChainType::Heavy, &self.h),
70            (ChainType::LightKappa, &self.k),
71            (ChainType::LightLambda, &self.l),
72            (ChainType::Iota, &self.i),
73        ]
74        .into_iter()
75    }
76}
77
78#[cfg(feature = "rayon")]
79use rayon::prelude::*;
80#[cfg(feature = "rayon")]
81impl<'a> IntoParallelIterator for &'a Germlines {
82    type Item = (ChainType, &'a Chain);
83    type Iter = rayon::array::IntoIter<(ChainType, &'a Chain), 4>;
84
85    fn into_par_iter(self) -> Self::Iter {
86        [
87            (ChainType::Heavy, &self.h),
88            (ChainType::LightKappa, &self.k),
89            (ChainType::LightLambda, &self.l),
90            (ChainType::Iota, &self.i),
91        ]
92        .into_par_iter()
93    }
94}
95
96/// The intermediate representation for a chain
97#[derive(Clone, Debug, Decode, Default, Deserialize, Encode, Serialize)]
98pub struct Chain {
99    /// All V/variable germlines
100    pub variable: Vec<Arc<Germline>>,
101    /// All J/joining germlines
102    pub joining: Vec<Arc<Germline>>,
103    /// All C/constant germlines
104    pub c: Vec<Arc<Germline>>,
105    /// All A constant germlines
106    pub a: Vec<Arc<Germline>>,
107    /// All D constant germlines
108    pub d: Vec<Arc<Germline>>,
109    /// All E constant germlines
110    pub e: Vec<Arc<Germline>>,
111    /// All G constant germlines
112    pub g: Vec<Arc<Germline>>,
113    /// All M constant germlines
114    pub m: Vec<Arc<Germline>>,
115    /// All N constant germlines
116    pub n: Vec<Arc<Germline>>,
117    /// All O constant germlines
118    pub o: Vec<Arc<Germline>>,
119    /// All R constant germlines
120    pub r: Vec<Arc<Germline>>,
121    /// All T constant germlines
122    pub t: Vec<Arc<Germline>>,
123    /// All W constant germlines
124    pub w: Vec<Arc<Germline>>,
125    /// All Y constant germlines
126    pub y: Vec<Arc<Germline>>,
127    /// All Z constant germlines
128    pub z: Vec<Arc<Germline>>,
129}
130
131impl Chain {
132    /// # Panics
133    /// It panics when it inserts an allele it has already placed (has to be filtered and ranked
134    /// before)
135    pub fn insert(&mut self, mut germline: Germline) {
136        let db = match &germline.name.kind {
137            GeneType::V => &mut self.variable,
138            GeneType::J => &mut self.joining,
139            GeneType::C(None) => &mut self.c,
140            GeneType::C(Some(Constant::A)) => &mut self.a,
141            GeneType::C(Some(Constant::D)) => &mut self.d,
142            GeneType::C(Some(Constant::E)) => &mut self.e,
143            GeneType::C(Some(Constant::G)) => &mut self.g,
144            GeneType::C(Some(Constant::M)) => &mut self.m,
145            GeneType::C(Some(Constant::N)) => &mut self.n,
146            GeneType::C(Some(Constant::O)) => &mut self.o,
147            GeneType::C(Some(Constant::R)) => &mut self.r,
148            GeneType::C(Some(Constant::T)) => &mut self.t,
149            GeneType::C(Some(Constant::W)) => &mut self.w,
150            GeneType::C(Some(Constant::Y)) => &mut self.y,
151            GeneType::C(Some(Constant::Z)) => &mut self.z,
152        };
153
154        match db.binary_search_by_key(&germline.name, |g| g.name.clone()) {
155            // If there are multiple copies of the same region keep the one with the most
156            // annotations + regions
157            Ok(index) => {
158                match db[index].alleles.binary_search_by_key(&germline.alleles[0].0, |a| a.0) {
159                    Ok(_allele_index) => {
160                        // if germline.alleles[0].1.sequence
161                        //     == db[index].alleles[allele_index].1.sequence
162                        // {
163                        //     db[index].alleles[allele_index].2 = format!(
164                        //         "{}|{}",
165                        //         db[index].alleles[allele_index].2, germline.alleles[0].2
166                        //     );
167                        // } else {
168                        //     let mut found = false;
169                        //     for existing in &mut db[index].duplicates {
170                        //         if existing.1.sequence == germline.alleles[0].1.sequence {
171                        //             existing.2 =
172                        //                 format!("{}|{}", existing.2, germline.alleles[0].2);
173                        //             found = true;
174                        //         }
175                        //     }
176                        //     if !found {
177                        //         db[index].duplicates.push(germline.alleles.pop().unwrap())
178                        //     }
179                        // }
180                        // if germline.alleles[0].1.conserved.len()
181                        //     + germline.alleles[0].1.regions.len()
182                        //     > db[index].alleles[allele_index].1.conserved.len()
183                        //         + db[index].alleles[allele_index].1.regions.len()
184                        // {
185                        //     db[index].alleles[allele_index] = germline.alleles.pop().unwrap()
186                        // }
187                        panic!(
188                            "Not allowed to have multiple sequences for one allele in a germline"
189                        )
190                    }
191                    Err(allele_index) => Arc::get_mut(&mut db[index])
192                        .map(|g| {
193                            g.alleles.insert(allele_index, germline.alleles.pop().unwrap());
194                        })
195                        .expect("Multiple copies of Arc while building IMGT structure"),
196                }
197            }
198            Err(index) => db.insert(index, Arc::new(germline)),
199        }
200    }
201
202    /// Create a row for use in the documentation of the numbers of different sequences available
203    pub fn doc_row(&self) -> String {
204        format!(
205            "|{}/{}|{}/{}|{}/{}|",
206            self.variable.len(),
207            self.variable.iter().map(|g| g.alleles.len()).sum::<usize>(),
208            self.joining.len(),
209            self.joining.iter().map(|g| g.alleles.len()).sum::<usize>(),
210            self.constant().count(),
211            self.constant().map(|g| g.alleles.len()).sum::<usize>(),
212        )
213    }
214
215    /// Get all constant chains germline regardless of the exact type of constant chain
216    pub fn constant(&self) -> impl Iterator<Item = &Arc<Germline>> {
217        self.c
218            .iter()
219            .chain(self.a.iter())
220            .chain(self.d.iter())
221            .chain(self.e.iter())
222            .chain(self.g.iter())
223            .chain(self.m.iter())
224            .chain(self.n.iter())
225            .chain(self.o.iter())
226            .chain(self.r.iter())
227            .chain(self.t.iter())
228            .chain(self.w.iter())
229            .chain(self.y.iter())
230            .chain(self.z.iter())
231    }
232}
233
234impl<'a> IntoIterator for &'a Chain {
235    type IntoIter = std::array::IntoIter<(GeneType, &'a [Arc<Germline>]), 15>;
236    type Item = (GeneType, &'a [Arc<Germline>]);
237
238    fn into_iter(self) -> Self::IntoIter {
239        [
240            (GeneType::V, self.variable.as_slice()),
241            (GeneType::J, self.joining.as_slice()),
242            (GeneType::C(None), self.c.as_slice()),
243            (GeneType::C(Some(Constant::A)), self.a.as_slice()),
244            (GeneType::C(Some(Constant::D)), self.d.as_slice()),
245            (GeneType::C(Some(Constant::E)), self.e.as_slice()),
246            (GeneType::C(Some(Constant::G)), self.g.as_slice()),
247            (GeneType::C(Some(Constant::M)), self.m.as_slice()),
248            (GeneType::C(Some(Constant::N)), self.n.as_slice()),
249            (GeneType::C(Some(Constant::O)), self.o.as_slice()),
250            (GeneType::C(Some(Constant::R)), self.r.as_slice()),
251            (GeneType::C(Some(Constant::T)), self.t.as_slice()),
252            (GeneType::C(Some(Constant::W)), self.w.as_slice()),
253            (GeneType::C(Some(Constant::Y)), self.y.as_slice()),
254            (GeneType::C(Some(Constant::Z)), self.z.as_slice()),
255        ]
256        .into_iter()
257    }
258}
259
260impl Chain {
261    /// Iterate over the genes
262    pub fn iter(
263        &self,
264    ) -> impl DoubleEndedIterator<Item = (GeneType, &[Arc<Germline>])> + ExactSizeIterator + '_
265    {
266        [
267            (GeneType::V, self.variable.as_slice()),
268            (GeneType::J, self.joining.as_slice()),
269            (GeneType::C(None), self.c.as_slice()),
270            (GeneType::C(Some(Constant::A)), self.a.as_slice()),
271            (GeneType::C(Some(Constant::D)), self.d.as_slice()),
272            (GeneType::C(Some(Constant::E)), self.e.as_slice()),
273            (GeneType::C(Some(Constant::G)), self.g.as_slice()),
274            (GeneType::C(Some(Constant::M)), self.m.as_slice()),
275            (GeneType::C(Some(Constant::N)), self.n.as_slice()),
276            (GeneType::C(Some(Constant::O)), self.o.as_slice()),
277            (GeneType::C(Some(Constant::R)), self.r.as_slice()),
278            (GeneType::C(Some(Constant::T)), self.t.as_slice()),
279            (GeneType::C(Some(Constant::W)), self.w.as_slice()),
280            (GeneType::C(Some(Constant::Y)), self.y.as_slice()),
281            (GeneType::C(Some(Constant::Z)), self.z.as_slice()),
282        ]
283        .into_iter()
284    }
285}
286
287#[cfg(feature = "rayon")]
288impl<'a> IntoParallelIterator for &'a Chain {
289    type Item = (GeneType, &'a [Arc<Germline>]);
290    type Iter = rayon::array::IntoIter<(GeneType, &'a [Arc<Germline>]), 15>;
291
292    fn into_par_iter(self) -> Self::Iter {
293        [
294            (GeneType::V, self.variable.as_slice()),
295            (GeneType::J, self.joining.as_slice()),
296            (GeneType::C(None), self.c.as_slice()),
297            (GeneType::C(Some(Constant::A)), self.a.as_slice()),
298            (GeneType::C(Some(Constant::D)), self.d.as_slice()),
299            (GeneType::C(Some(Constant::E)), self.e.as_slice()),
300            (GeneType::C(Some(Constant::G)), self.g.as_slice()),
301            (GeneType::C(Some(Constant::M)), self.m.as_slice()),
302            (GeneType::C(Some(Constant::N)), self.m.as_slice()),
303            (GeneType::C(Some(Constant::O)), self.o.as_slice()),
304            (GeneType::C(Some(Constant::R)), self.r.as_slice()),
305            (GeneType::C(Some(Constant::T)), self.t.as_slice()),
306            (GeneType::C(Some(Constant::W)), self.w.as_slice()),
307            (GeneType::C(Some(Constant::Y)), self.y.as_slice()),
308            (GeneType::C(Some(Constant::Z)), self.z.as_slice()),
309        ]
310        .into_par_iter()
311    }
312}
313
314/// Intermediate representation for germline
315#[derive(Clone, Debug, Decode, Deserialize, Encode, Eq, PartialEq, Serialize)]
316pub struct Germline {
317    /// The species for this germline
318    pub species: Species,
319    /// The name for the germline
320    pub name: Gene,
321    /// All alleles (allele number, sequence, IMGT accession)
322    pub alleles: Vec<(usize, AnnotatedSequence, String)>,
323}
324
325impl<'a> IntoIterator for &'a Germline {
326    type IntoIter = std::slice::Iter<'a, (usize, AnnotatedSequence, String)>;
327    type Item = &'a (usize, AnnotatedSequence, String);
328
329    fn into_iter(self) -> Self::IntoIter {
330        self.alleles.iter()
331    }
332}
333
334impl Germline {
335    /// Iterate over the sequences
336    pub fn iter(&self) -> <&Self as IntoIterator>::IntoIter {
337        self.into_iter()
338    }
339
340    /// Select a specific allele
341    pub fn select_allele(&self, allele: Option<usize>) -> Option<Allele<'_>> {
342        allele
343            .map_or_else(
344                || self.alleles.first(),
345                |allele| self.alleles.iter().find(|(i, ..)| *i == allele),
346            )
347            .map(|(i, sequence, acc)| Allele {
348                species: self.species,
349                gene: std::borrow::Cow::Borrowed(&self.name),
350                number: *i,
351                sequence: &sequence.sequence,
352                regions: &sequence.regions,
353                annotations: &sequence.annotations,
354                acc,
355            })
356    }
357}
358
359#[cfg(feature = "rayon")]
360impl<'a> IntoParallelIterator for &'a Germline {
361    type Item = &'a (usize, AnnotatedSequence, String);
362    type Iter = rayon::slice::Iter<'a, (usize, AnnotatedSequence, String)>;
363
364    fn into_par_iter(self) -> Self::Iter {
365        self.alleles.par_iter()
366    }
367}
368
369/// Intermediate representation for annotated sequence
370#[derive(Clone, Debug, Decode, Deserialize, Encode, Eq, PartialEq, Serialize)]
371pub struct AnnotatedSequence {
372    /// The sequence
373    pub sequence: Peptidoform<UnAmbiguous>,
374    /// The different regions in the sequence, defined by their name and length
375    pub regions: Vec<(Region, usize)>,
376    /// 0 based locations of single amino acid annotations, overlapping with the regions defined
377    /// above
378    pub annotations: Vec<(Annotation, usize)>,
379}
380
381impl AnnotatedSequence {
382    /// Create a new annotated sequence
383    pub fn new(
384        sequence: Peptidoform<UnAmbiguous>,
385        regions: Vec<(Region, usize)>,
386        mut conserved: Vec<(Annotation, usize)>,
387    ) -> Self {
388        conserved.sort_unstable_by_key(|c| c.1);
389        Self {
390            sequence,
391            regions,
392            annotations: conserved,
393        }
394    }
395}
396
397/// A germline gene name, broken up in its constituent parts.
398#[derive(
399    Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, Hash, Decode, Encode,
400)]
401pub struct Gene {
402    /// The chain of this gene (heavy/kappa etc)
403    pub chain: ChainType,
404    /// The kind of gene (V/J/C)
405    pub kind: GeneType,
406    /// If present the additional number _IGHV_ **(I)**
407    pub number: Option<usize>,
408    /// The family indicators _IGHV_ **-1D**
409    pub family: Vec<(Option<usize>, String)>,
410}
411
412impl Display for Gene {
413    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414        const fn to_roman(n: usize) -> &'static str {
415            [
416                "0", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X",
417            ][n]
418        }
419
420        write!(
421            f,
422            "IG{}{}{}{}",
423            self.chain,
424            self.kind,
425            self.number
426                .as_ref()
427                .map_or_else(String::new, |n| format!("({})", to_roman(*n))),
428            if self.number.is_some() && !self.family.is_empty() {
429                "-"
430            } else {
431                ""
432            }
433        )?;
434
435        let mut first = true;
436        let mut last_str = false;
437        for element in &self.family {
438            if !first && !last_str {
439                write!(f, "-")?;
440            }
441            write!(
442                f,
443                "{}{}",
444                element.0.map(|i| i.to_string()).unwrap_or_default(),
445                element.1
446            )?;
447            last_str = !element.1.is_empty();
448            first = false;
449        }
450        Ok(())
451    }
452}
453
454impl Gene {
455    /// Get an IMGT name with allele, eg IGHV3-23*03 or IGHV1/OR15-1*01
456    /// # Errors
457    /// If not recognised as a name, returns a description of the error.
458    pub fn from_imgt_name_with_allele(s: &str) -> Result<(Self, usize), String> {
459        let mut recent_error = "Empty name".to_string();
460        // Try all names to find the first one that is valid
461        for s in s.split(" or ") {
462            let s = s.trim_end_matches(" F");
463            let (gene, tail) = match Self::from_imgt_name_internal(s) {
464                Ok(v) => v,
465                Err(err) => {
466                    recent_error = err;
467                    continue;
468                }
469            };
470            if tail.is_empty() {
471                return Ok((gene, 1));
472            }
473            match tail.strip_prefix('*').map_or_else(
474                || Err(format!("Invalid allele spec: `{tail}`")),
475                |tail| tail.parse().map_err(|_| format!("Invalid allele spec: `{}`", &tail)),
476            ) {
477                Ok(allele) => return Ok((gene, allele)),
478                Err(err) => {
479                    recent_error = err;
480                }
481            }
482        }
483        Err(recent_error)
484    }
485
486    /// Get an IMGT name, eg IGHV3-23
487    /// # Errors
488    /// If not recognised as a name, returns a description of the error.
489    pub fn from_imgt_name(s: &str) -> Result<Self, String> {
490        Self::from_imgt_name_internal(s).map(|(gene, _)| gene)
491    }
492
493    /// # Errors
494    /// If not recognised as a name, returns a description of the error.
495    fn from_imgt_name_internal(s: &str) -> Result<(Self, &str), String> {
496        #[expect(clippy::missing_panics_doc)] // Cannot panic
497        fn parse_family_name(s: &str) -> (Option<(Option<usize>, String)>, &str) {
498            let num = s.chars().take_while(char::is_ascii_digit).collect::<String>();
499            let tail = s
500                .chars()
501                .skip(num.len())
502                .take_while(char::is_ascii_alphabetic)
503                .collect::<String>();
504            let rest = &s[num.len() + tail.len()..];
505            if num.is_empty() && tail.is_empty() {
506                return (None, s);
507            }
508            let num = if num.is_empty() {
509                None
510            } else {
511                Some(num.parse().unwrap())
512            };
513            (Some((num, tail)), rest)
514        }
515
516        fn from_roman(s: &str) -> Option<usize> {
517            match s {
518                "Ⅰ" | "I" => Some(1),
519                "Ⅱ" | "II" => Some(2),
520                "Ⅲ" | "III" => Some(3),
521                "Ⅳ" | "IV" => Some(4),
522                "Ⅴ" | "V" => Some(5),
523                "Ⅵ" | "VI" => Some(6),
524                "Ⅶ" | "VII" => Some(7),
525                "Ⅷ" | "VIII" => Some(8),
526                "Ⅸ" | "IX" => Some(9),
527                "Ⅹ" | "X" => Some(10),
528                _ => None,
529            }
530        }
531
532        if s.starts_with("IG") {
533            let chain = s[2..3].parse().map_err(|()| format!("Invalid chain: `{}`", &s[2..3]))?;
534            let gene = s[3..4].parse().map_err(|()| format!("Invalid gene: `{}`", &s[3..4]))?;
535            let mut start = 4;
536            let number = if s.len() > 4 && &s[4..5] == "(" {
537                let end = s[5..]
538                    .find(')')
539                    .ok_or_else(|| format!("Invalid gene number `{}` out of `{}`", &s[4..], s))?;
540                start += end + 2;
541                Some(from_roman(&s[5..5 + end]).ok_or_else(|| {
542                    format!("Invalid roman numeral (or too big) `{}`", &s[5..5 + end])
543                })?)
544            } else {
545                None
546            };
547            let mut tail = s[start..].trim_start_matches('-');
548            let mut family = Vec::new();
549            while let (Some(branch), t) = parse_family_name(tail) {
550                family.push(branch);
551                tail = t.trim_start_matches('-');
552            }
553
554            Ok((
555                Self {
556                    chain,
557                    kind: gene,
558                    number,
559                    family,
560                },
561                tail,
562            ))
563        } else {
564            Err("Gene name does not start with IG")?
565        }
566    }
567}
568
569/// Any chain type of germline
570#[derive(
571    Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Decode, Encode,
572)]
573pub enum ChainType {
574    /// Heavy chain
575    Heavy = 0,
576    /// Light kappa chain
577    LightKappa,
578    /// Light lambda chain
579    LightLambda,
580    /// Fish I kind
581    Iota,
582}
583
584impl TryFrom<usize> for ChainType {
585    type Error = ();
586
587    fn try_from(i: usize) -> Result<Self, Self::Error> {
588        match i {
589            0 => Ok(Self::Heavy),
590            1 => Ok(Self::LightKappa),
591            2 => Ok(Self::LightLambda),
592            3 => Ok(Self::Iota),
593            _ => Err(()),
594        }
595    }
596}
597
598impl FromStr for ChainType {
599    type Err = ();
600
601    fn from_str(s: &str) -> Result<Self, Self::Err> {
602        match s {
603            "H" => Ok(Self::Heavy),
604            "κ" | "K" => Ok(Self::LightKappa),
605            "λ" | "L" => Ok(Self::LightLambda),
606            "ι" | "I" => Ok(Self::Iota),
607            _ => Err(()),
608        }
609    }
610}
611
612impl Display for ChainType {
613    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614        write!(f, "{}", match self {
615            Self::Heavy => "H",
616            Self::LightKappa => "K",
617            Self::LightLambda => "L",
618            Self::Iota => "I",
619        })
620    }
621}
622
623/// Any gene in a germline, eg variable, joining
624#[derive(
625    Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Decode, Encode,
626)]
627pub enum GeneType {
628    /// Variable
629    V,
630    /// Joining
631    J,
632    /// Constant, potentially with the type of constant given as well
633    C(Option<Constant>),
634}
635
636/// Any type of constant gene
637#[allow(missing_docs)]
638#[derive(
639    Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Decode, Encode,
640)]
641pub enum Constant {
642    A,
643    D,
644    E,
645    G,
646    M,
647    N,
648    O,
649    R,
650    T,
651    W,
652    Y,
653    Z,
654}
655
656impl FromStr for GeneType {
657    type Err = ();
658
659    fn from_str(s: &str) -> Result<Self, Self::Err> {
660        match s {
661            "V" => Ok(Self::V),
662            "J" => Ok(Self::J),
663            "C" => Ok(Self::C(None)),
664            "α" | "A" => Ok(Self::C(Some(Constant::A))),
665            "δ" | "D" => Ok(Self::C(Some(Constant::D))),
666            "ε" | "E" => Ok(Self::C(Some(Constant::E))),
667            "ɣ" | "G" => Ok(Self::C(Some(Constant::G))),
668            "μ" | "M" => Ok(Self::C(Some(Constant::M))),
669            "ν" | "N" => Ok(Self::C(Some(Constant::N))),
670            "ο" | "O" => Ok(Self::C(Some(Constant::O))),
671            "ρ" | "R" => Ok(Self::C(Some(Constant::R))),
672            "τ" | "T" => Ok(Self::C(Some(Constant::T))),
673            "ω" | "W" => Ok(Self::C(Some(Constant::W))),
674            "υ" | "Y" => Ok(Self::C(Some(Constant::Y))),
675            "ζ" | "Z" => Ok(Self::C(Some(Constant::Z))),
676            _ => Err(()),
677        }
678    }
679}
680
681impl Display for GeneType {
682    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
683        write!(f, "{}", match self {
684            Self::V => "V",
685            Self::J => "J",
686            Self::C(None) => "C",
687            Self::C(Some(Constant::A)) => "A",
688            Self::C(Some(Constant::D)) => "D",
689            Self::C(Some(Constant::E)) => "E",
690            Self::C(Some(Constant::G)) => "G",
691            Self::C(Some(Constant::M)) => "M",
692            Self::C(Some(Constant::N)) => "N",
693            Self::C(Some(Constant::O)) => "O",
694            Self::C(Some(Constant::R)) => "R",
695            Self::C(Some(Constant::T)) => "T",
696            Self::C(Some(Constant::W)) => "W",
697            Self::C(Some(Constant::Y)) => "Y",
698            Self::C(Some(Constant::Z)) => "Z",
699        })
700    }
701}
702
703#[expect(clippy::missing_panics_doc)]
704#[test]
705fn imgt_names() {
706    assert_eq!(
707        Gene::from_imgt_name_with_allele("IGHV3-23*03")
708            .map(|(g, a)| (g.to_string(), a))
709            .unwrap(),
710        ("IGHV3-23".to_string(), 3)
711    );
712    assert_eq!(
713        Gene::from_imgt_name_with_allele("IGKV6-d*01")
714            .map(|(g, a)| (g.to_string(), a))
715            .unwrap(),
716        ("IGKV6-d".to_string(), 1)
717    );
718}