balsa 0.3.2

Reference implementation for the Balsa molecular line notation.
Documentation
use lyn::Scanner;

use super::{digit, element, missing_character, nonzero, selection, Error};
use crate::feature::{
    AtomParity, Bracket, Charge, Isotope, Symbol, VirtualHydrogen,
};

pub fn bracket(scanner: &mut Scanner) -> Result<Option<Bracket>, Error> {
    if !scanner.take(&'[') {
        return Ok(None);
    }

    let result = Ok(Some(Bracket {
        isotope: isotope(scanner),
        symbol: match symbol(scanner)? {
            Some(symbol) => symbol,
            None => return Err(missing_character(scanner)),
        },
        parity: atom_parity(scanner),
        hydrogens: virtual_hydrogen(scanner),
        charge: charge(scanner),
    }));

    if scanner.take(&']') {
        result
    } else {
        Err(missing_character(scanner))
    }
}

fn isotope(scanner: &mut Scanner) -> Option<Isotope> {
    let mut sum = match nonzero(scanner) {
        Some(digit) => digit as u16,
        None => return None,
    };

    for _ in 0..2 {
        sum = match digit(scanner) {
            Some(digit) => sum * 10 + digit as u16,
            None => return Some(Isotope::new(sum).expect("isotope")),
        };
    }

    Some(Isotope::new(sum).expect("isotope"))
}

fn symbol(scanner: &mut Scanner) -> Result<Option<Symbol>, Error> {
    if let Some(element) = element(scanner)? {
        Ok(Some(Symbol::Element(element)))
    } else if let Some(selection) = selection(scanner) {
        Ok(Some(Symbol::Selection(selection)))
    } else if star(scanner) {
        Ok(Some(Symbol::Star))
    } else {
        Ok(None)
    }
}

fn star(scanner: &mut Scanner) -> bool {
    scanner.take(&'*')
}

fn atom_parity(scanner: &mut Scanner) -> Option<AtomParity> {
    if scanner.take(&'@') {
        if scanner.take(&'@') {
            Some(AtomParity::Clockwise)
        } else {
            Some(AtomParity::Counterclockwise)
        }
    } else {
        None
    }
}

fn virtual_hydrogen(scanner: &mut Scanner) -> Option<VirtualHydrogen> {
    if scanner.take(&'H') {
        match nonzero(scanner) {
            Some(digit) => Some(VirtualHydrogen::new(digit).expect("digit")),
            _ => Some(VirtualHydrogen::default()),
        }
    } else {
        None
    }
}

fn charge(scanner: &mut Scanner) -> Option<Charge> {
    if scanner.take(&'+') {
        match nonzero(scanner) {
            Some(digit) => Some(Charge::new(digit as i8).expect("charge")),
            None => Some(Charge::Plus),
        }
    } else if scanner.take(&'-') {
        match nonzero(scanner) {
            Some(digit) => Some(Charge::new(digit as i8 * -1).expect("charge")),
            None => Some(Charge::Minus),
        }
    } else {
        None
    }
}

#[cfg(test)]
mod isotope {
    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn leading_zero() {
        let mut scanner = Scanner::new("007");

        assert_eq!(isotope(&mut scanner), None)
    }

    #[test]
    fn single_digit() {
        let mut scanner = Scanner::new("7");

        assert_eq!(isotope(&mut scanner), Some(Isotope::new(7).unwrap()))
    }

    #[test]
    fn double_digit() {
        let mut scanner = Scanner::new("42");

        assert_eq!(isotope(&mut scanner), Some(Isotope::new(42).unwrap()))
    }

    #[test]
    fn triple_digit() {
        let mut scanner = Scanner::new("999");

        assert_eq!(isotope(&mut scanner), Some(Isotope::new(999).unwrap()))
    }
}

#[cfg(test)]
mod tests {
    use crate::feature::{Element, Selection};
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn no_open() {
        let mut scanner = Scanner::new("X");

        assert_eq!(bracket(&mut scanner), Ok(None))
    }

    #[test]
    fn open_no_close() {
        let mut scanner = Scanner::new("[");

        assert_eq!(bracket(&mut scanner), Err(Error::EndOfLine))
    }

    #[test]
    fn open_invalid() {
        let mut scanner = Scanner::new("[0");

        assert_eq!(bracket(&mut scanner), Err(Error::Character(1)))
    }

    #[test]
    fn open_element_no_close() {
        let mut scanner = Scanner::new("[C");

        assert_eq!(bracket(&mut scanner), Err(Error::EndOfLine))
    }

    #[test]
    fn open_isotope_no_close() {
        let mut scanner = Scanner::new("[1");

        assert_eq!(bracket(&mut scanner), Err(Error::EndOfLine))
    }

    #[test]
    fn isotope_zero() {
        let mut scanner = Scanner::new("[0C]");

        assert_eq!(bracket(&mut scanner), Err(Error::Character(1)))
    }

    #[test]
    fn isotope_overflow() {
        let mut scanner = Scanner::new("[1234C]");

        assert_eq!(bracket(&mut scanner), Err(Error::Character(4)))
    }

    #[test]
    fn charge_overflow() {
        let mut scanner = Scanner::new("[C+10]");

        assert_eq!(bracket(&mut scanner), Err(Error::Character(4)))
    }

    #[test]
    fn element() {
        let mut scanner = Scanner::new("[C]");

        assert_eq!(
            bracket(&mut scanner),
            Ok(Some(Bracket {
                symbol: Symbol::Element(Element::C),
                ..Default::default()
            }))
        )
    }

    #[test]
    fn selected_element() {
        let mut scanner = Scanner::new("[c]");

        assert_eq!(
            bracket(&mut scanner),
            Ok(Some(Bracket {
                symbol: Symbol::Selection(Selection::C),
                ..Default::default()
            }))
        )
    }

    #[test]
    fn open_star_close() {
        let mut scanner = Scanner::new("[*]");

        assert_eq!(bracket(&mut scanner), Ok(Some(Bracket::default())))
    }

    #[test]
    fn element_with_isotope() {
        let mut scanner = Scanner::new("[1H]");

        assert_eq!(
            bracket(&mut scanner),
            Ok(Some(Bracket {
                symbol: Symbol::Element(Element::H),
                isotope: Some(Isotope::new(1).unwrap()),
                ..Default::default()
            }))
        )
    }

    #[test]
    fn element_with_stereodescriptor() {
        let mut scanner = Scanner::new("[C@]");

        assert_eq!(
            bracket(&mut scanner),
            Ok(Some(Bracket {
                symbol: Symbol::Element(Element::C),
                parity: Some(AtomParity::Counterclockwise),
                ..Default::default()
            }))
        )
    }

    #[test]
    fn element_with_virtual_hydrogen() {
        let mut scanner = Scanner::new("[CH1]");

        assert_eq!(
            bracket(&mut scanner),
            Ok(Some(Bracket {
                symbol: Symbol::Element(Element::C),
                hydrogens: Some(VirtualHydrogen::H1),
                ..Default::default()
            }))
        )
    }

    #[test]
    fn element_with_charge() {
        let mut scanner = Scanner::new("[C+1]");

        assert_eq!(
            bracket(&mut scanner),
            Ok(Some(Bracket {
                symbol: Symbol::Element(Element::C),
                charge: Some(Charge::Plus1),
                ..Default::default()
            }))
        )
    }

    #[test]
    fn kitchen_sink() {
        let mut scanner = Scanner::new("[12C@H1+2]");

        assert_eq!(
            bracket(&mut scanner),
            Ok(Some(Bracket {
                symbol: Symbol::Element(Element::C),
                isotope: Some(Isotope::new(12).unwrap()),
                parity: Some(AtomParity::Counterclockwise),
                hydrogens: Some(VirtualHydrogen::H1),
                charge: Some(Charge::Plus2),
            }))
        )
    }
}