#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![warn(clippy::cargo)]
#![allow(clippy::manual_let_else)]
#![cfg_attr(test, allow(clippy::non_ascii_literal))]
#![allow(unknown_lints)]
#![warn(missing_docs)]
#![warn(missing_debug_implementations)]
#![warn(missing_copy_implementations)]
#![warn(rust_2018_idioms)]
#![warn(rust_2021_compatibility)]
#![warn(trivial_casts, trivial_numeric_casts)]
#![warn(unused_qualifications)]
#![warn(variant_size_differences)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_alias))]
#![no_std]
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
mod readme {}
extern crate alloc;
#[cfg(feature = "std")]
extern crate std;
mod error;
mod parser;
mod radix;
mod subject;
mod whitespace;
pub use error::{ArgumentError, Error, InvalidRadixError, InvalidRadixExceptionKind};
use parser::{Sign, State as ParseState};
pub use radix::Radix;
use radix::RADIX_TABLE;
use subject::IntegerString;
pub fn parse<T>(subject: &T, radix: Option<i64>) -> Result<i64, Error<'_>>
where
T: AsRef<[u8]> + ?Sized,
{
let subject = subject.as_ref();
parse_inner(subject, radix)
}
fn parse_inner(subject: &[u8], radix: Option<i64>) -> Result<i64, Error<'_>> {
let subject = IntegerString::try_from(subject)?;
let radix = if let Some(radix) = radix {
Radix::try_base_from_str_and_i64(subject, radix)?
} else {
None
};
let mut state = ParseState::new(subject);
let mut chars = whitespace::trim(subject.as_bytes()).iter().copied().peekable();
match chars.peek() {
Some(b'+') => {
state = state.set_sign(Sign::Positive)?;
chars.next();
}
Some(b'-') => {
state = state.set_sign(Sign::Negative)?;
chars.next();
}
Some(_) => {}
None => return Err(subject.into()),
}
let radix = match chars.peek() {
Some(b'0') => {
chars.next();
match (chars.peek(), radix) {
(Some(b'b' | b'B'), None | Some(2)) => {
chars.next();
2
}
(Some(b'o' | b'O'), None | Some(8)) => {
chars.next();
8
}
(Some(b'd' | b'D'), None | Some(10)) => {
chars.next();
10
}
(Some(b'x' | b'X'), None | Some(16)) => {
chars.next();
16
}
(Some(b'b' | b'B' | b'o' | b'O' | b'd' | b'D' | b'x' | b'X'), Some(_)) => return Err(subject.into()),
(None, _) => return Ok(0),
(Some(_), None) => 8,
(Some(_), Some(radix)) => radix,
}
}
Some(_) => radix.unwrap_or(10),
None => return Err(subject.into()),
};
loop {
if chars.next_if_eq(&b'0').is_some() {
if chars.next_if_eq(&b'_').is_some() {
match chars.peek() {
None | Some(b'_') => return Err(subject.into()),
Some(_) => {}
}
}
} else if let Some(b'_') = chars.peek() {
return Err(subject.into());
} else {
break;
}
}
loop {
match chars.next() {
Some(b'_') => match chars.peek() {
None | Some(b'_') => return Err(subject.into()),
Some(_) => {}
},
Some(b) if RADIX_TABLE[usize::from(b)] <= radix => {
state = state.collect_digit(b);
}
Some(_) => return Err(subject.into()),
None => break,
}
}
let src = state.into_numeric_string()?;
i64::from_str_radix(&src, radix).map_err(|_| subject.into())
}
#[cfg(test)]
mod tests {
use crate::parse;
#[test]
fn parse_int_max() {
let result = parse("9_223_372_036_854_775_807", None);
assert_eq!(result.unwrap(), i64::MAX);
let result = parse("+9_223_372_036_854_775_807", None);
assert_eq!(result.unwrap(), i64::MAX);
}
#[test]
fn parse_int_min() {
let result = parse("-9_223_372_036_854_775_808", None);
assert_eq!(result.unwrap(), i64::MIN);
}
#[test]
fn leading_zero_does_not_imply_octal_when_given_radix() {
let result = parse("017", Some(12));
assert_eq!(result.unwrap(), 19);
let result = parse("-017", Some(12));
assert_eq!(result.unwrap(), -19);
}
#[test]
fn squeeze_leading_zeros() {
let result = parse("0x0000000000000011", Some(16));
assert_eq!(result.unwrap(), 17);
let result = parse("-0x0000000000000011", Some(16));
assert_eq!(result.unwrap(), -17);
let result = parse("0x00_00000000000011", Some(16));
assert_eq!(result.unwrap(), 17);
let result = parse("-0x00_00000000000011", Some(16));
assert_eq!(result.unwrap(), -17);
let result = parse("0x0_0_0_11", Some(16));
assert_eq!(result.unwrap(), 17);
let result = parse("-0x0_0_0_11", Some(16));
assert_eq!(result.unwrap(), -17);
let result = parse("-0x00000_15", Some(16));
assert_eq!(result.unwrap(), -21);
}
#[test]
fn squeeze_leading_zeros_is_octal_when_octal_digits() {
let result = parse("000000000000000000000000000000000000000123", None);
assert_eq!(result.unwrap(), 83);
}
#[test]
fn squeeze_leading_is_invalid_when_non_octal_digits() {
parse("000000000000000000000000000000000000000987", None).unwrap_err();
}
#[test]
fn squeeze_leading_zeros_enforces_no_double_underscore() {
parse("0x___11", Some(16)).unwrap_err();
parse("-0x___11", Some(16)).unwrap_err();
parse("0x0___11", Some(16)).unwrap_err();
parse("-0x0___11", Some(16)).unwrap_err();
parse("0x_0__11", Some(16)).unwrap_err();
parse("-0x_0__11", Some(16)).unwrap_err();
parse("0x_00__11", Some(16)).unwrap_err();
parse("-0x_00__11", Some(16)).unwrap_err();
}
#[test]
fn no_digits_with_base_prefix() {
parse("0x", None).unwrap_err();
parse("0b", None).unwrap_err();
parse("0o", None).unwrap_err();
parse("o", None).unwrap_err();
parse("0d", None).unwrap_err();
parse("0X", None).unwrap_err();
parse("0B", None).unwrap_err();
parse("0O", None).unwrap_err();
parse("O", None).unwrap_err();
parse("0D", None).unwrap_err();
}
#[test]
fn no_digits_with_base_prefix_neg() {
parse("-0x", None).unwrap_err();
parse("-0b", None).unwrap_err();
parse("-0o", None).unwrap_err();
parse("-o", None).unwrap_err();
parse("-0d", None).unwrap_err();
parse("-0X", None).unwrap_err();
parse("-0B", None).unwrap_err();
parse("-0O", None).unwrap_err();
parse("-O", None).unwrap_err();
parse("-0D", None).unwrap_err();
}
#[test]
fn no_digits_with_invalid_base_prefix() {
parse("0z", None).unwrap_err();
parse("0z", Some(12)).unwrap_err();
}
#[test]
fn no_digits_with_invalid_base_prefix_neg() {
parse("-0z", None).unwrap_err();
parse("-0z", Some(12)).unwrap_err();
}
#[test]
fn binary_alpha_requires_zero_prefix() {
parse("B1", None).unwrap_err();
parse("b1", None).unwrap_err();
}
#[test]
fn binary_parses() {
let result = parse("0B1111", None);
assert_eq!(result.unwrap(), 15);
let result = parse("0b1111", None);
assert_eq!(result.unwrap(), 15);
let result = parse("-0B1111", None);
assert_eq!(result.unwrap(), -15);
let result = parse("-0b1111", None);
assert_eq!(result.unwrap(), -15);
}
#[test]
fn binary_with_given_2_radix_parses() {
let result = parse("0B1111", Some(2));
assert_eq!(result.unwrap(), 15);
let result = parse("0b1111", Some(2));
assert_eq!(result.unwrap(), 15);
let result = parse("-0B1111", Some(2));
assert_eq!(result.unwrap(), -15);
let result = parse("-0b1111", Some(2));
assert_eq!(result.unwrap(), -15);
}
#[test]
fn binary_with_mismatched_radix_is_err() {
parse("0B1111", Some(24)).unwrap_err();
parse("0b1111", Some(24)).unwrap_err();
parse("-0B1111", Some(24)).unwrap_err();
parse("-0b1111", Some(24)).unwrap_err();
}
#[test]
fn binary_with_digits_out_of_radix_is_err() {
parse("0B1111AH", None).unwrap_err();
parse("0b1111ah", None).unwrap_err();
}
#[test]
fn octal_alpha_requires_zero_prefix() {
parse("O7", None).unwrap_err();
parse("o7", None).unwrap_err();
}
#[test]
fn octal_parses() {
let result = parse("0O17", None);
assert_eq!(result.unwrap(), 15);
let result = parse("0o17", None);
assert_eq!(result.unwrap(), 15);
let result = parse("-0O17", None);
assert_eq!(result.unwrap(), -15);
let result = parse("-0o17", None);
assert_eq!(result.unwrap(), -15);
}
#[test]
fn octal_with_given_8_radix_parses() {
let result = parse("0O17", Some(8));
assert_eq!(result.unwrap(), 15);
let result = parse("0o17", Some(8));
assert_eq!(result.unwrap(), 15);
let result = parse("-0O17", Some(8));
assert_eq!(result.unwrap(), -15);
let result = parse("-0o17", Some(8));
assert_eq!(result.unwrap(), -15);
}
#[test]
fn octal_no_alpha_parses() {
let result = parse("017", None);
assert_eq!(result.unwrap(), 15);
let result = parse("-017", None);
assert_eq!(result.unwrap(), -15);
}
#[test]
fn octal_no_alpha_with_given_8_radix_parses() {
let result = parse("017", Some(8));
assert_eq!(result.unwrap(), 15);
let result = parse("-017", Some(8));
assert_eq!(result.unwrap(), -15);
}
#[test]
fn octal_with_mismatched_radix_is_err() {
parse("0O17", Some(24)).unwrap_err();
parse("0o17", Some(24)).unwrap_err();
parse("-0O17", Some(24)).unwrap_err();
parse("-0o17", Some(24)).unwrap_err();
}
#[test]
fn octal_with_digits_out_of_radix_is_err() {
parse("0O17AH", None).unwrap_err();
parse("0o17ah", None).unwrap_err();
}
#[test]
fn decimal_alpha_requires_zero_prefix() {
parse("D9", None).unwrap_err();
parse("d9", None).unwrap_err();
}
#[test]
fn decimal_parses() {
let result = parse("0D15", None);
assert_eq!(result.unwrap(), 15);
let result = parse("0d15", None);
assert_eq!(result.unwrap(), 15);
let result = parse("-0D15", None);
assert_eq!(result.unwrap(), -15);
let result = parse("-0d15", None);
assert_eq!(result.unwrap(), -15);
}
#[test]
fn decimal_with_given_10_radix_parses() {
let result = parse("0D15", Some(10));
assert_eq!(result.unwrap(), 15);
let result = parse("0d15", Some(10));
assert_eq!(result.unwrap(), 15);
let result = parse("-0D15", Some(10));
assert_eq!(result.unwrap(), -15);
let result = parse("-0d15", Some(10));
assert_eq!(result.unwrap(), -15);
}
#[test]
fn decimal_with_mismatched_radix_is_err() {
parse("0D15", Some(24)).unwrap_err();
parse("0d15", Some(24)).unwrap_err();
parse("-0D15", Some(24)).unwrap_err();
parse("-0d15", Some(24)).unwrap_err();
}
#[test]
fn decimal_with_digits_out_of_radix_is_err() {
parse("0D15AH", None).unwrap_err();
parse("0d15ah", None).unwrap_err();
}
#[test]
fn hex_alpha_requires_zero_prefix() {
parse("XF", None).unwrap_err();
parse("xF", None).unwrap_err();
parse("Xf", None).unwrap_err();
parse("xf", None).unwrap_err();
}
#[test]
fn hex_parses() {
let result = parse("0XF", None);
assert_eq!(result.unwrap(), 15);
let result = parse("0xF", None);
assert_eq!(result.unwrap(), 15);
let result = parse("-0XF", None);
assert_eq!(result.unwrap(), -15);
let result = parse("-0xF", None);
assert_eq!(result.unwrap(), -15);
let result = parse("0Xf", None);
assert_eq!(result.unwrap(), 15);
let result = parse("0xf", None);
assert_eq!(result.unwrap(), 15);
let result = parse("-0Xf", None);
assert_eq!(result.unwrap(), -15);
let result = parse("-0xf", None);
assert_eq!(result.unwrap(), -15);
}
#[test]
fn hex_with_given_16_radix_parses() {
let result = parse("0XF", Some(16));
assert_eq!(result.unwrap(), 15);
let result = parse("0xF", Some(16));
assert_eq!(result.unwrap(), 15);
let result = parse("-0XF", Some(16));
assert_eq!(result.unwrap(), -15);
let result = parse("-0xF", Some(16));
assert_eq!(result.unwrap(), -15);
let result = parse("0Xf", Some(16));
assert_eq!(result.unwrap(), 15);
let result = parse("0xf", Some(16));
assert_eq!(result.unwrap(), 15);
let result = parse("-0Xf", Some(16));
assert_eq!(result.unwrap(), -15);
let result = parse("-0xf", Some(16));
assert_eq!(result.unwrap(), -15);
}
#[test]
fn hex_with_mismatched_radix_is_err() {
parse("0XF", Some(24)).unwrap_err();
parse("0xF", Some(24)).unwrap_err();
parse("0Xf", Some(24)).unwrap_err();
parse("0xf", Some(24)).unwrap_err();
parse("-0XF", Some(24)).unwrap_err();
parse("-0xF", Some(24)).unwrap_err();
parse("-0Xf", Some(24)).unwrap_err();
parse("-0xf", Some(24)).unwrap_err();
}
#[test]
fn hex_with_digits_out_of_radix_is_err() {
parse("0XFAH", None).unwrap_err();
parse("0xFah", None).unwrap_err();
parse("0XfAH", None).unwrap_err();
parse("0xfah", None).unwrap_err();
}
#[test]
fn digits_out_of_radix_is_err() {
parse("17AH", Some(12)).unwrap_err();
parse("17ah", Some(12)).unwrap_err();
parse("17AH", None).unwrap_err();
parse("17ah", None).unwrap_err();
}
#[test]
fn parsing_is_case_insensitive() {
let result = parse("abcdefgxyz", Some(36));
assert_eq!(result.unwrap(), 1_047_601_316_316_923);
let result = parse("ABCDEFGXYZ", Some(36));
assert_eq!(result.unwrap(), 1_047_601_316_316_923);
}
#[test]
fn leading_underscore_is_err() {
parse("0x_0000001234567", None).unwrap_err();
parse("0_x0000001234567", None).unwrap_err();
parse("___0x0000001234567", None).unwrap_err();
}
#[test]
fn double_underscore_is_err() {
parse("0x111__11", None).unwrap_err();
}
#[test]
fn trailing_underscore_is_err() {
parse("0x111_11_", None).unwrap_err();
parse("0x00000_", None).unwrap_err();
}
#[test]
fn all_spaces_is_err() {
parse(" ", None).unwrap_err();
}
#[test]
fn empty_is_err() {
parse("", None).unwrap_err();
}
#[test]
fn more_than_one_sign_is_err() {
parse("++12", None).unwrap_err();
parse("+-12", None).unwrap_err();
parse("-+12", None).unwrap_err();
parse("--12", None).unwrap_err();
}
#[test]
fn zero_radix_is_default() {
let result = parse("0x111", Some(0));
assert_eq!(result.unwrap(), 273);
let result = parse("111", Some(0));
assert_eq!(result.unwrap(), 111);
}
#[test]
fn negative_one_radix_is_default() {
let result = parse("0x123f", Some(-1));
assert_eq!(result.unwrap(), 4671);
let result = parse("111", Some(-1));
assert_eq!(result.unwrap(), 111);
}
#[test]
fn one_radix_is_err() {
parse("0x123f", Some(1)).unwrap_err();
parse("111", Some(1)).unwrap_err();
}
#[test]
fn out_of_range_radix_is_err() {
parse("0x123f", Some(1200)).unwrap_err();
parse("123", Some(1200)).unwrap_err();
parse("123", Some(-1200)).unwrap_err();
}
#[test]
fn literals_with_negative_out_of_range_radix_ignore_radix() {
let result = parse("0x123f", Some(-1200));
assert_eq!(result.unwrap(), 4671);
}
#[test]
fn negative_radix_in_valid_range_is_parsed() {
let result = parse("111", Some(-2));
assert_eq!(result.unwrap(), 7);
let result = parse("111", Some(-10));
assert_eq!(result.unwrap(), 111);
let result = parse("111", Some(-36));
assert_eq!(result.unwrap(), 1333);
}
#[test]
fn all_valid_radixes() {
let test_cases = [
("111", 2, 7),
("111", 3, 13),
("111", 4, 21),
("111", 5, 31),
("111", 6, 43),
("111", 7, 57),
("111", 8, 73),
("111", 9, 91),
("111", 10, 111),
("111", 11, 133),
("111", 12, 157),
("111", 13, 183),
("111", 14, 211),
("111", 15, 241),
("111", 16, 273),
("111", 17, 307),
("111", 18, 343),
("111", 19, 381),
("111", 20, 421),
("111", 21, 463),
("111", 22, 507),
("111", 23, 553),
("111", 24, 601),
("111", 25, 651),
("111", 26, 703),
("111", 27, 757),
("111", 28, 813),
("111", 29, 871),
("111", 30, 931),
("111", 31, 993),
("111", 32, 1057),
("111", 33, 1123),
("111", 34, 1191),
("111", 35, 1261),
("111", 36, 1333),
("111", -2, 7),
("111", -3, 13),
("111", -4, 21),
("111", -5, 31),
("111", -6, 43),
("111", -7, 57),
("111", -8, 73),
("111", -9, 91),
("111", -10, 111),
("111", -11, 133),
("111", -12, 157),
("111", -13, 183),
("111", -14, 211),
("111", -15, 241),
("111", -16, 273),
("111", -17, 307),
("111", -18, 343),
("111", -19, 381),
("111", -20, 421),
("111", -21, 463),
("111", -22, 507),
("111", -23, 553),
("111", -24, 601),
("111", -25, 651),
("111", -26, 703),
("111", -27, 757),
("111", -28, 813),
("111", -29, 871),
("111", -30, 931),
("111", -31, 993),
("111", -32, 1057),
("111", -33, 1123),
("111", -34, 1191),
("111", -35, 1261),
("111", -36, 1333),
];
for (subject, radix, output) in test_cases {
let result = parse(subject, Some(radix));
assert_eq!(
result.unwrap(),
output,
"Mismatched output for test case ({subject}, {radix}, {output})"
);
}
}
#[test]
fn int_max_radix_does_not_panic() {
parse("111", Some(i64::MAX)).unwrap_err();
}
#[test]
fn int_min_radix_does_not_panic() {
parse("111", Some(i64::MIN)).unwrap_err();
}
#[test]
fn decimal_zero() {
let result = parse("0", None);
assert_eq!(result.unwrap(), 0);
let result = parse("0", Some(2));
assert_eq!(result.unwrap(), 0);
let result = parse("0", Some(8));
assert_eq!(result.unwrap(), 0);
let result = parse("0", Some(10));
assert_eq!(result.unwrap(), 0);
let result = parse("0", Some(16));
assert_eq!(result.unwrap(), 0);
let result = parse("0", Some(36));
assert_eq!(result.unwrap(), 0);
}
#[test]
fn decimal_zero_whitespace() {
let result = parse("0 ", None);
assert_eq!(result.unwrap(), 0);
let result = parse(" 0", None);
assert_eq!(result.unwrap(), 0);
let result = parse(" 0 ", None);
assert_eq!(result.unwrap(), 0);
}
#[test]
fn trailing_whitespace() {
let result = parse("1 ", None);
assert_eq!(result.unwrap(), 1);
}
#[test]
fn all_ascii_whitespace_is_trimmed_from_end() {
let result = parse("93 ", None);
assert_eq!(result.unwrap(), 93);
let result = parse("93\n", None);
assert_eq!(result.unwrap(), 93);
let result = parse("93\t", None);
assert_eq!(result.unwrap(), 93);
let result = parse("93\u{000A}", None);
assert_eq!(result.unwrap(), 93);
let result = parse("93\u{000C}", None);
assert_eq!(result.unwrap(), 93);
let result = parse("93\u{000D}", None);
assert_eq!(result.unwrap(), 93);
}
#[test]
fn all_ascii_whitespace_is_trimmed_from_start() {
let result = parse(" 93", None);
assert_eq!(result.unwrap(), 93);
let result = parse("\n93", None);
assert_eq!(result.unwrap(), 93);
let result = parse("\t93", None);
assert_eq!(result.unwrap(), 93);
let result = parse("\u{000A}93", None);
assert_eq!(result.unwrap(), 93);
let result = parse("\u{000C}93", None);
assert_eq!(result.unwrap(), 93);
let result = parse("\u{000D}93", None);
assert_eq!(result.unwrap(), 93);
}
#[test]
fn inputs_with_both_leading_and_trailing_whitespace_are_parsed() {
let result = parse(" 93 ", None);
assert_eq!(result.unwrap(), 93);
let result = parse("\n 93 \n", None);
assert_eq!(result.unwrap(), 93);
let result = parse("\t 93 \t", None);
assert_eq!(result.unwrap(), 93);
let result = parse("\u{000A} 93 \u{000A}", None);
assert_eq!(result.unwrap(), 93);
let result = parse("\u{000C} 93 \u{000C}", None);
assert_eq!(result.unwrap(), 93);
let result = parse("\u{000D} 93 \u{000D}", None);
assert_eq!(result.unwrap(), 93);
}
#[test]
fn negative_radix_leading_whitespace() {
let result = parse(" 0123", Some(-6));
assert_eq!(result.unwrap(), 83);
let result = parse(" 0x123", Some(-6));
assert_eq!(result.unwrap(), 291);
}
#[test]
fn trim_vertical_tab() {
let result = parse(b" \x0B 27", None);
assert_eq!(result.unwrap(), 27);
}
}