endf_parser 0.2.0

A Rust library for parsing ENDF-6 format nuclear data.
Documentation
//! Provide utilities for parsing ENDF-6 format integers.
//!
//! ENDF integers format is described in section `0.6.2` of ENDF-6 Formats Manual.
//!
//! ENDF-6 format available at [ENDF-6 Formats Manual](https://www.nndc.bnl.gov/csewg/docs/endf-manual.pdf).

use std::fmt;

/// ENDF integer length (chars), FORTRAN `I11` specification.
pub const ENDF_INTEGER_LENGTH: usize = 11;
/// ENDF integer max absolute value.
pub const ENDF_INTEGER_MAX_ABSOLUTE_VALUE: u64 = 9_999_999_999; // 64 bits integer, overflows u32
/// Maximum number of digits in ENDF integers, ENDF-6 format limitation.
pub const ENDF_INTEGER_MAX_DIGITS: u32 = 10;
/// ENDF integer radix.
pub const ENDF_INTEGER_RADIX: u32 = 10;
/// ENDF integer regex.
pub const ENDF_INTEGER_REGEX: &str = r"^[ ]*[-+]?\d{1,10}[ ]*$";

/// Error returned when parsing ENDF integer fails.
#[derive(Debug, Eq, PartialEq)]
pub enum ParseEndfIntegerError {
    /// Empty string
    Empty,
    /// String without integer part
    EmptyIntegerPart,
    /// Invalid digit
    InvalidDigit { invalid: char },
    /// Invalid number sign
    InvalidSign { invalid: char },
    /// Non ASCII string
    NonASCII,
    /// ENDF integer overflow
    Overflow,
    /// String too long
    TooLong,
}

impl fmt::Display for ParseEndfIntegerError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ParseEndfIntegerError::Empty => write!(f, "empty string"),
            ParseEndfIntegerError::EmptyIntegerPart => write!(f, "empty integer part"),
            ParseEndfIntegerError::InvalidDigit { invalid: x } => {
                write!(f, "invalid digit: '{}'", x)
            }
            ParseEndfIntegerError::InvalidSign { invalid: s } => write!(f, "invalid sign '{}'", s),
            ParseEndfIntegerError::NonASCII => write!(f, "non ascii string"),
            ParseEndfIntegerError::Overflow => write!(f, "endf integer overflow"),
            ParseEndfIntegerError::TooLong => write!(f, "too long string"),
        }
    }
}

/// Parse ENDF integer.
/// # Format
/// ENDF integers can be read with FORTRAN77 `I11` format specification.
///
/// ENDF integers have the following format: `±nnnnnnnnnn` with:
/// - zero or more leading blanks followed by
/// - a leading sign `'-'` or `'+'` for negative or positive respectively (positive sign is optional)
/// - `n` a digit between `0` and `9`
/// - full integer length is less than or equal to `11`
/// # Preconditions
/// Following preconditions must hold or a `ParseEndfIntegerError` is returned:
/// - `integer` is ASCII
/// - `integer` is not empty
/// - `integer` is not blank (combination of chars: `' ', '\t', '\n' and '\r'`)
/// - `integer` is included in [ -9_999_999_999; +9_999_999_999 ]
/// - `integer` length is less than or equal to `11`
/// - `integer` respect the ENDF integer format (see [Format](#format))
/// # Examples
/// ## From &str
/// ```
/// let x = endf_parser::primitive::integer::parse("1");
/// assert_eq!(1, x.unwrap());
/// ```
/// ```
/// let x = endf_parser::primitive::integer::parse("-1");
/// assert_eq!(-1, x.unwrap());
/// ```
///
/// ```
/// let x = endf_parser::primitive::integer::parse("+1");
/// assert_eq!(1, x.unwrap());
/// ```
/// ```
/// let x = endf_parser::primitive::integer::parse("+1234567890");
/// assert_eq!(1_234_567_890, x.unwrap());
/// ```
/// ## From String
/// ```
/// let x = endf_parser::primitive::integer::parse(&String::from("+1234567890")[..]);
/// assert_eq!(1_234_567_890, x.unwrap())
/// ```
/// # Errors
/// If parsing fails, `ParseEndfIntegerError` is returned.
/// # Regex
/// The input integer can be tested with the [ENDF_INTEGER_REGEX](constant.ENDF_INTEGER_REGEX.html) regex.
/// # Reference
/// ENDF integer format is described in section `0.6.2` of [ENDF-6 Formats Manual](https://www.nndc.bnl.gov/csewg/docs/endf-manual.pdf)
pub fn parse(integer: &str) -> Result<i64, ParseEndfIntegerError> {
    if !integer.is_ascii() {
        return Err(ParseEndfIntegerError::NonASCII);
    }
    if integer.len() > ENDF_INTEGER_LENGTH {
        return Err(ParseEndfIntegerError::TooLong);
    }
    // At this point, integer is full ASCII and has a correct length.
    // ASCII => (iteration over chars <=> iteration over bytes)
    // (length <= 11) => integer won't overflow i64/u64
    let bytes = integer.trim().as_bytes();
    // Extract sign
    let (negative, digits) = match bytes.first() {
        Some(&b'-') => (true, &bytes[1..]),
        Some(&b'+') => (false, &bytes[1..]),
        Some(x) if &b'0' <= x && x <= &b'9' => (false, bytes),
        Some(x) => {
            return Err(ParseEndfIntegerError::InvalidSign {
                invalid: *x as char,
            })
        }
        None => return Err(ParseEndfIntegerError::Empty),
    };
    // Extract integer part
    if digits.is_empty() {
        return Err(ParseEndfIntegerError::EmptyIntegerPart);
    }
    let mut number = 0;
    for &digit in digits {
        if b'0' <= digit && digit <= b'9' {
            number = number * 10 + (digit - b'0') as i64
        } else {
            return Err(ParseEndfIntegerError::InvalidDigit {
                invalid: digit as char,
            });
        }
    }
    // Check overflow
    if number > ENDF_INTEGER_MAX_ABSOLUTE_VALUE as i64 {
        return Err(ParseEndfIntegerError::Overflow);
    }
    // Apply sign
    if negative {
        Ok(-number)
    } else {
        Ok(number)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use regex::Regex;

    #[test]
    fn parse_empty() {
        let integer = "";
        let expected = ParseEndfIntegerError::Empty;
        let actual = parse(integer).unwrap_err();
        assert_eq!(expected, actual);
    }

    #[test]
    fn parse_whitespace() {
        // Rust allows following whitespace:
        // - ' ' (space)
        // - '\n' (new line)
        // - '\t' (tab)
        // - '\r' (line feed)
        let expected = ParseEndfIntegerError::Empty;
        assert_eq!(expected, parse(" ").unwrap_err());
        assert_eq!(expected, parse("\n").unwrap_err());
        assert_eq!(expected, parse("\t").unwrap_err());
        assert_eq!(expected, parse("\r").unwrap_err());
        assert_eq!(expected, parse(" \n\t\r").unwrap_err());
    }

    #[test]
    fn parse_sign_only() {
        let expected = ParseEndfIntegerError::EmptyIntegerPart;
        assert_eq!(expected, parse("-").unwrap_err());
        assert_eq!(expected, parse("+").unwrap_err());
    }

    #[test]
    fn parse_invalid() {
        let integer = "string";
        let expected = ParseEndfIntegerError::InvalidSign { invalid: 's' };
        assert_eq!(expected, parse(integer).unwrap_err());
    }

    #[test]
    fn parse_invalid_digit() {
        let integer = "1.0";
        let expected = ParseEndfIntegerError::InvalidDigit { invalid: '.' };
        assert_eq!(expected, parse(integer).unwrap_err());
        let integer = "1e+25";
        let expected = ParseEndfIntegerError::InvalidDigit { invalid: 'e' };
        assert_eq!(expected, parse(integer).unwrap_err());
        let integer = "1+2";
        let expected = ParseEndfIntegerError::InvalidDigit { invalid: '+' };
        assert_eq!(expected, parse(integer).unwrap_err());
        let integer = "-1        2";
        let expected = ParseEndfIntegerError::InvalidDigit { invalid: ' ' };
        assert_eq!(expected, parse(integer).unwrap_err());
    }

    #[test]
    fn parse_invalid_sign() {
        let integer = "%1234567890";
        let expected = ParseEndfIntegerError::InvalidSign { invalid: '%' };
        assert_eq!(expected, parse(integer).unwrap_err());
    }

    #[test]
    fn parse_non_ascii() {
        let integer = "µ";
        let expected = ParseEndfIntegerError::NonASCII;
        assert_eq!(expected, parse(integer).unwrap_err());
    }

    #[test]
    fn parse_overflow() {
        let integer = "10000000000";
        let expected = ParseEndfIntegerError::Overflow;
        assert_eq!(expected, parse(integer).unwrap_err());
    }

    #[test]
    fn parse_invalid_length() {
        let integer = "+11111111111";
        let expected = ParseEndfIntegerError::TooLong;
        assert_eq!(expected, parse(integer).unwrap_err());
        let integer = "-11111111111";
        let expected = ParseEndfIntegerError::TooLong;
        assert_eq!(expected, parse(integer).unwrap_err());
    }

    #[test]
    fn parse_valid_near_zero() {
        assert_eq!(0, parse("0").unwrap());
        assert_eq!(1, parse("1").unwrap());
        assert_eq!(-1, parse("-1").unwrap());
        assert_eq!(1, parse("+1").unwrap());
    }

    #[test]
    fn parse_valid_limits() {
        assert_eq!(9_999_999_999, parse("9999999999").unwrap());
        assert_eq!(9_999_999_999, parse("+9999999999").unwrap());
        assert_eq!(9_999_999_999, parse(" 9999999999").unwrap());
        assert_eq!(-9_999_999_999, parse("-9999999999").unwrap());
    }

    #[test]
    fn parse_valid_positive_unsigned() {
        assert_eq!(1, parse("          1").unwrap());
        assert_eq!(12, parse("         12").unwrap());
        assert_eq!(123, parse("        123").unwrap());
        assert_eq!(1234, parse("       1234").unwrap());
        assert_eq!(12345, parse("      12345").unwrap());
        assert_eq!(123_456, parse("     123456").unwrap());
        assert_eq!(1_234_567, parse("    1234567").unwrap());
        assert_eq!(12_345_678, parse("   12345678").unwrap());
        assert_eq!(123_456_789, parse("  123456789").unwrap());
        assert_eq!(1_234_567_890, parse(" 1234567890").unwrap());
    }

    #[test]
    fn parse_valid_positive_signed() {
        assert_eq!(1, parse("         +1").unwrap());
        assert_eq!(12, parse("        +12").unwrap());
        assert_eq!(123, parse("       +123").unwrap());
        assert_eq!(1234, parse("      +1234").unwrap());
        assert_eq!(12345, parse("     +12345").unwrap());
        assert_eq!(123_456, parse("    +123456").unwrap());
        assert_eq!(1_234_567, parse("   +1234567").unwrap());
        assert_eq!(12_345_678, parse("  +12345678").unwrap());
        assert_eq!(123_456_789, parse(" +123456789").unwrap());
        assert_eq!(1_234_567_890, parse("+1234567890").unwrap());
    }

    #[test]
    fn parse_valid_negative_signed() {
        assert_eq!(-1, parse("         -1").unwrap());
        assert_eq!(-12, parse("        -12").unwrap());
        assert_eq!(-123, parse("       -123").unwrap());
        assert_eq!(-1234, parse("      -1234").unwrap());
        assert_eq!(-12345, parse("     -12345").unwrap());
        assert_eq!(-123_456, parse("    -123456").unwrap());
        assert_eq!(-1_234_567, parse("   -1234567").unwrap());
        assert_eq!(-12_345_678, parse("  -12345678").unwrap());
        assert_eq!(-123_456_789, parse(" -123456789").unwrap());
        assert_eq!(-1_234_567_890, parse("-1234567890").unwrap());
    }

    #[test]
    fn parse_valid_zero_padded() {
        assert_eq!(1, parse("00000000001").unwrap());
        assert_eq!(12, parse("00000000012").unwrap());
        assert_eq!(123, parse("00000000123").unwrap());
        assert_eq!(1234, parse("00000001234").unwrap());
        assert_eq!(12345, parse("00000012345").unwrap());
        assert_eq!(123_456, parse("00000123456").unwrap());
        assert_eq!(1_234_567, parse("00001234567").unwrap());
        assert_eq!(12_345_678, parse("00012345678").unwrap());
        assert_eq!(123_456_789, parse("00123456789").unwrap());
        assert_eq!(1_234_567_890, parse("01234567890").unwrap());
    }

    #[test]
    fn regex() {
        let regex = Regex::new(ENDF_INTEGER_REGEX).unwrap();
        assert!(regex.is_match("1"));
        assert!(regex.is_match("+1"));
        assert!(regex.is_match("-1"));
        assert!(regex.is_match(" 1234567890"));
        assert!(regex.is_match("-1234567890"));
        assert!(regex.is_match("+1234567890"));
        assert!(regex.is_match("          1"));
        assert!(regex.is_match("     1     "));
        assert!(regex.is_match("1          "));
        assert!(regex.is_match("         +1"));
        assert!(regex.is_match("    +1     "));
        assert!(regex.is_match("+1         "));
        assert!(regex.is_match("         -1"));
        assert!(regex.is_match("    -1     "));
        assert!(regex.is_match("-1         "));

        assert!(!regex.is_match("12345678901"));
        assert!(!regex.is_match("-"));
        assert!(!regex.is_match("+"));
        assert!(!regex.is_match("1.23456789"));
        assert!(!regex.is_match("-1.2345e+1"));
        assert!(!regex.is_match("-1.2345e-1"));
        assert!(!regex.is_match("invalid"));
        assert!(!regex.is_match("+         1"));
        assert!(!regex.is_match("-         1"));
    }
}