alkale 2.0.0

A simple LL(1) lexer library for Rust.
Documentation
//! Module that contains [`NumericalBase`] and default implementations of it.

/// Indicates that a type is a numerical base, such as binary or decimal.
pub trait NumericalBase {
    /// Returns the number of unique digits in this base. For base-10,
    /// this is 10, etc.
    fn position_value(&self) -> u8;

    /// Returns true if the input character is a valid digit in this base.
    fn includes(&self, ch: char) -> bool;

    /// Returns the value of a digit character in this base.
    ///
    /// See [`value_of`](NumericalBase::value_of) for a safe version.
    ///
    /// # Safety
    /// Undefined behavior if the input character is not included in this base.
    /// (i.e. if it does not return true for [`includes`][NumericalBase::includes])
    unsafe fn value_of_unchecked(&self, ch: char) -> u8;

    /// Returns the value of a digit character in this base, or [`None`]
    /// if the digit is not included.
    #[inline]
    fn value_of(&self, ch: char) -> Option<u8> {
        self.includes(ch)
            // SAFETY: Method only called if preconditions are met.
            .then(|| unsafe { self.value_of_unchecked(ch) })
    }
}

/// Represents the set of standard bases used by built-in number parsers.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum StandardBase {
    /// Represents base 16, hexadecimal. It uses digits `0123456789ABCDEF`. Letters may be upper or lowercase.
    Hexadecimal,
    /// Represents base 10, decimal. It uses digits `0123456789`.
    Decimal,
    /// Represents base 8, octal. It uses digits `01234567`.
    Octal,
    /// Represents base 2, binary. It uses digits `01`.
    Binary,
}

impl NumericalBase for StandardBase {
    #[inline]
    fn position_value(&self) -> u8 {
        match self {
            Self::Hexadecimal => 16,
            Self::Decimal => 10,
            Self::Octal => 8,
            Self::Binary => 2,
        }
    }

    #[inline]
    fn includes(&self, ch: char) -> bool {
        match self {
            Self::Hexadecimal => ch.is_ascii_hexdigit(),
            Self::Decimal => ch.is_ascii_digit(),
            Self::Octal => matches!(ch, '0'..='7'),
            Self::Binary => ch == '0' || ch == '1',
        }
    }

    #[inline]
    #[expect(clippy::arithmetic_side_effects)]
    unsafe fn value_of_unchecked(&self, ch: char) -> u8 {
        match self {
            Self::Hexadecimal => {
                let ascii_value = ch as u8;

                if ascii_value >= b'A' {
                    ascii_value.to_ascii_uppercase() - b'A' + 10
                } else {
                    ascii_value - b'0'
                }
            }
            _ => (ch as u8) - b'0',
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::common::numeric::base::{NumericalBase, StandardBase};

    fn assert_contains(base: &impl NumericalBase, char: char, value: u8) {
        assert!(base.includes(char));
        assert_eq!(base.value_of(char), Some(value));
    }

    #[test]
    fn binary() {
        let base = StandardBase::Binary;

        assert_contains(&base, '0', 0);
        assert_contains(&base, '1', 1);

        assert!(!base.includes('2'));

        assert_eq!(base.position_value(), 2);
    }

    #[test]
    fn octal() {
        let base = StandardBase::Octal;

        assert_contains(&base, '0', 0);
        assert_contains(&base, '1', 1);
        assert_contains(&base, '2', 2);
        assert_contains(&base, '3', 3);
        assert_contains(&base, '4', 4);
        assert_contains(&base, '5', 5);
        assert_contains(&base, '6', 6);
        assert_contains(&base, '7', 7);

        assert!(!base.includes('8'));

        assert_eq!(base.position_value(), 8);
    }

    #[test]
    fn decimal() {
        let base = StandardBase::Decimal;

        assert_contains(&base, '0', 0);
        assert_contains(&base, '1', 1);
        assert_contains(&base, '2', 2);
        assert_contains(&base, '3', 3);
        assert_contains(&base, '4', 4);
        assert_contains(&base, '5', 5);
        assert_contains(&base, '6', 6);
        assert_contains(&base, '7', 7);
        assert_contains(&base, '8', 8);
        assert_contains(&base, '9', 9);

        assert!(!base.includes('A'));

        assert_eq!(base.position_value(), 10);
    }

    #[test]
    fn hexadecimal() {
        let base = StandardBase::Hexadecimal;

        assert_contains(&base, '0', 0);
        assert_contains(&base, '1', 1);
        assert_contains(&base, '2', 2);
        assert_contains(&base, '3', 3);
        assert_contains(&base, '4', 4);
        assert_contains(&base, '5', 5);
        assert_contains(&base, '6', 6);
        assert_contains(&base, '7', 7);
        assert_contains(&base, '8', 8);
        assert_contains(&base, '9', 9);
        assert_contains(&base, 'A', 10);
        assert_contains(&base, 'B', 11);
        assert_contains(&base, 'C', 12);
        assert_contains(&base, 'D', 13);
        assert_contains(&base, 'E', 14);
        assert_contains(&base, 'F', 15);

        assert!(!base.includes('G'));

        assert_eq!(base.position_value(), 16);
    }

    #[test]
    fn invalid_base_char() {
        assert_eq!(StandardBase::Decimal.value_of('A'), None);
    }
}