yowl 0.1.0

Primitives for reading and writing the SMILES language
Documentation
use super::{
    error::ReadError, missing_character, read_charge, read_configuration, read_symbol,
    scanner::Scanner,
};
use crate::feature::{AtomKind, VirtualHydrogen};

pub fn read_bracket(scanner: &mut Scanner) -> Result<Option<AtomKind>, ReadError> {
    if scanner.peek() == Some(&'[') {
        scanner.pop();
    } else {
        return Ok(None);
    }

    let isotope = read_isotope(scanner);
    let symbol = read_symbol(scanner)?;
    let configuration = read_configuration(scanner);
    let hcount = read_hcount(scanner);
    let charge = read_charge(scanner);
    let map = read_map(scanner)?;

    match scanner.peek() {
        Some(']') => {
            scanner.pop();

            Ok(Some(AtomKind::Bracket {
                isotope,
                symbol,
                configuration,
                hcount,
                charge,
                map,
            }))
        }
        None => Err(ReadError::EndOfLine),
        _ => Err(ReadError::Character(scanner.cursor())),
    }
}

fn read_hcount(scanner: &mut Scanner) -> Option<VirtualHydrogen> {
    match scanner.peek() {
        Some('H') => {
            scanner.pop();

            match scanner.peek() {
                Some('0'..='9') => match scanner.pop() {
                    Some('0') => Some(VirtualHydrogen::H0),
                    Some('1') => Some(VirtualHydrogen::H1),
                    Some('2') => Some(VirtualHydrogen::H2),
                    Some('3') => Some(VirtualHydrogen::H3),
                    Some('4') => Some(VirtualHydrogen::H4),
                    Some('5') => Some(VirtualHydrogen::H5),
                    Some('6') => Some(VirtualHydrogen::H6),
                    Some('7') => Some(VirtualHydrogen::H7),
                    Some('8') => Some(VirtualHydrogen::H8),
                    Some('9') => Some(VirtualHydrogen::H9),
                    _ => Some(VirtualHydrogen::H1),
                },
                _ => Some(VirtualHydrogen::H1),
            }
        }
        _ => None,
    }
}

fn read_isotope(scanner: &mut Scanner) -> Option<u16> {
    let mut digits = String::new();

    for _ in 0..3 {
        match scanner.peek() {
            Some('0'..='9') => digits.push(*scanner.pop().expect("digit")),
            _ => break,
        }
    }

    if digits.is_empty() {
        None
    } else {
        Some(digits.parse::<u16>().expect("number"))
    }
}

fn read_map(scanner: &mut Scanner) -> Result<Option<u16>, ReadError> {
    match scanner.peek() {
        Some(':') => {
            scanner.pop();

            let mut digits = String::new();

            match scanner.pop() {
                Some(next) => {
                    if next.is_ascii_digit() {
                        digits.push(*next);
                    } else {
                        return Err(ReadError::Character(scanner.cursor() - 1));
                    }
                }
                None => return Err(missing_character(scanner)),
            }

            for _ in 0..2 {
                match scanner.peek() {
                    Some('0'..='9') => digits.push(*scanner.pop().expect("digit")),
                    _ => break,
                }
            }

            Ok(Some(digits.parse::<u16>().expect("number")))
        }
        _ => Ok(None),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::feature::{BracketAromatic, BracketSymbol, Charge, Configuration};
    use pretty_assertions::assert_eq;

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

        assert_eq!(read_bracket(&mut scanner), Err(ReadError::Character(6)))
    }

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

        assert_eq!(read_bracket(&mut scanner), Err(ReadError::Character(4)))
    }

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

        assert_eq!(read_bracket(&mut scanner), Err(ReadError::Character(1)))
    }

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

        assert_eq!(read_bracket(&mut scanner), Err(ReadError::EndOfLine))
    }

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

        assert_eq!(read_bracket(&mut scanner), Err(ReadError::Character(3)))
    }

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

        assert_eq!(read_bracket(&mut scanner), Err(ReadError::EndOfLine))
    }

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

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

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

        assert_eq!(
            read_bracket(&mut scanner),
            Ok(Some(AtomKind::Bracket {
                isotope: None,
                symbol: BracketSymbol::Star,
                configuration: None,
                hcount: None,
                charge: None,
                map: None
            }))
        )
    }

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

        assert_eq!(
            read_bracket(&mut scanner),
            Ok(Some(AtomKind::Bracket {
                isotope: Some(999.try_into().unwrap()),
                symbol: BracketSymbol::Star,
                configuration: None,
                hcount: None,
                charge: None,
                map: None
            }))
        )
    }

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

        assert_eq!(
            read_bracket(&mut scanner),
            Ok(Some(AtomKind::Bracket {
                isotope: None,
                symbol: BracketSymbol::Star,
                configuration: Some(Configuration::TH1),
                hcount: None,
                charge: None,
                map: None
            }))
        )
    }

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

        assert_eq!(
            read_bracket(&mut scanner),
            Ok(Some(AtomKind::Bracket {
                isotope: None,
                symbol: BracketSymbol::Star,
                configuration: None,
                hcount: Some(VirtualHydrogen::H2),
                charge: None,
                map: None
            }))
        )
    }

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

        assert_eq!(
            read_bracket(&mut scanner),
            Ok(Some(AtomKind::Bracket {
                isotope: None,
                symbol: BracketSymbol::Star,
                configuration: None,
                hcount: None,
                charge: Some(Charge::One),
                map: None
            }))
        )
    }

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

        assert_eq!(
            read_bracket(&mut scanner),
            Ok(Some(AtomKind::Bracket {
                isotope: None,
                symbol: BracketSymbol::Star,
                configuration: None,
                hcount: None,
                charge: None,
                map: Some(999u16.try_into().unwrap())
            }))
        )
    }

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

        assert_eq!(
            read_bracket(&mut scanner),
            Ok(Some(AtomKind::Bracket {
                isotope: None,
                symbol: BracketSymbol::Aromatic(BracketAromatic::S),
                configuration: None,
                hcount: None,
                charge: Some(Charge::One),
                map: None
            }))
        )
    }
}