knave-expand 0.13.2

The reading, scoping and expansion stages of knave
Documentation
use super::form::{Number, NumberValue};

pub enum NumberError {
    Malformed,
    TooLarge,
}

pub struct NumberLiteral<'a> {
    text: &'a str,
}

impl<'a> NumberLiteral<'a> {
    const SIGNS: [char; 2] = ['+', '-'];
    const DIGIT_SEPARATOR: char = '_';
    const DECIMAL_POINT: char = '.';
    const EXPONENT_MARKERS: [char; 2] = ['e', 'E'];

    pub fn new(text: &'a str) -> Self {
        Self { text }
    }

    pub fn is_number_shaped(&self) -> bool {
        self.unsigned()
            .starts_with(|character: char| character.is_ascii_digit())
    }

    pub fn parse(&self) -> Result<Number, NumberError> {
        let value = if Self::is_digit_run(self.unsigned()) {
            NumberValue::Integer(self.parse_integer()?)
        } else if Self::is_float(self.unsigned()) {
            NumberValue::Float(self.parse_float())
        } else {
            return Err(NumberError::Malformed);
        };
        Ok(Number::new(self.text, value))
    }

    fn unsigned(&self) -> &'a str {
        self.text.strip_prefix(Self::SIGNS).unwrap_or(self.text)
    }

    fn parse_integer(&self) -> Result<i128, NumberError> {
        self.cleaned().parse().map_err(|_| NumberError::TooLarge)
    }

    fn parse_float(&self) -> f64 {
        self.cleaned()
            .parse()
            .expect("validated float literal parses")
    }

    fn cleaned(&self) -> String {
        let without_plus = self.text.strip_prefix('+').unwrap_or(self.text);
        without_plus.replace(Self::DIGIT_SEPARATOR, "")
    }

    fn is_float(text: &str) -> bool {
        match text.split_once(Self::EXPONENT_MARKERS) {
            Some((mantissa, exponent)) => {
                Self::is_mantissa(mantissa) && Self::is_exponent(exponent)
            }
            None => Self::is_fraction(text),
        }
    }

    fn is_mantissa(text: &str) -> bool {
        Self::is_digit_run(text) || Self::is_fraction(text)
    }

    fn is_fraction(text: &str) -> bool {
        text.split_once(Self::DECIMAL_POINT)
            .is_some_and(|(whole, fraction)| {
                Self::is_digit_run(whole) && Self::is_digit_run(fraction)
            })
    }

    fn is_exponent(text: &str) -> bool {
        Self::is_digit_run(text.strip_prefix(Self::SIGNS).unwrap_or(text))
    }

    fn is_digit_run(text: &str) -> bool {
        text.starts_with(|character: char| character.is_ascii_digit())
            && text
                .chars()
                .all(|character| character.is_ascii_digit() || character == Self::DIGIT_SEPARATOR)
    }
}