geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use ecow::EcoString;
use num_bigint::BigInt;
use num_traits::ToPrimitive;

pub(super) fn decimal(source: &EcoString) -> Option<BigInt> {
    parse_digits(source, 10, true)
}

pub(super) fn radix(source: &EcoString, base: &BigInt) -> Option<BigInt> {
    let base = base.to_u32().filter(|base| (2..=36).contains(base))?;
    parse_digits(source, base, false)
}

pub(super) fn format_radix(value: &BigInt, base: &BigInt) -> Option<EcoString> {
    let base = base.to_u32().filter(|base| (2..=36).contains(base))?;
    Some(value.to_str_radix(base).to_uppercase().into())
}

fn parse_digits(source: &EcoString, base: u32, allow_plus: bool) -> Option<BigInt> {
    let bytes = source.as_bytes();
    let digits = match bytes.first() {
        Some(b'-') => &bytes[1..],
        Some(b'+') if allow_plus => &bytes[1..],
        _ => bytes,
    };
    if digits.is_empty() || !digits.iter().all(|byte| (*byte as char).is_digit(base)) {
        return None;
    }
    BigInt::parse_bytes(bytes, base)
}

#[cfg(test)]
mod tests {
    use super::{decimal, format_radix, radix};
    use num_bigint::BigInt;

    #[test]
    fn parses_the_official_signed_decimal_grammar() {
        assert_eq!(decimal(&"0".into()), Some(BigInt::from(0)));
        assert_eq!(decimal(&"-12".into()), Some(BigInt::from(-12)));
        assert_eq!(decimal(&"+12".into()), Some(BigInt::from(12)));

        for source in ["", "+", "-", "1.0", " 1", "1 ", "12x"] {
            assert_eq!(decimal(&source.into()), None, "{source}");
        }
    }

    #[test]
    fn parses_and_formats_radices_from_two_through_thirty_six() {
        assert_eq!(radix(&"101".into(), &2.into()), Some(BigInt::from(5)));
        assert_eq!(radix(&"-1C".into(), &36.into()), Some(BigInt::from(-48)));
        assert_eq!(radix(&"ff".into(), &16.into()), Some(BigInt::from(255)));
        assert_eq!(radix(&"+10".into(), &2.into()), None);
        assert_eq!(radix(&"2".into(), &2.into()), None);
        assert_eq!(radix(&"".into(), &10.into()), None);
        assert_eq!(radix(&"10".into(), &1.into()), None);
        assert_eq!(radix(&"10".into(), &37.into()), None);
        assert_eq!(radix(&"10".into(), &BigInt::from(10u8).pow(100)), None);

        assert_eq!(format_radix(&255.into(), &16.into()), Some("FF".into()));
        assert_eq!(format_radix(&(-48).into(), &36.into()), Some("-1C".into()));
        assert_eq!(format_radix(&1.into(), &1.into()), None);
        assert_eq!(format_radix(&1.into(), &37.into()), None);
        assert_eq!(format_radix(&1.into(), &BigInt::from(10u8).pow(100)), None,);
    }
}