#![doc(html_root_url = "https://docs.rs/btoi/0.5.0")]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![no_std]
extern crate num_traits;
#[cfg(feature = "std")]
extern crate std;
#[cfg(test)]
#[macro_use]
extern crate quickcheck;
use core::fmt;
use num_traits::{Bounded, CheckedAdd, CheckedMul, CheckedSub, FromPrimitive, Saturating, Zero};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseIntegerError {
kind: ParseIntegerErrorKind,
}
impl ParseIntegerError {
pub fn kind(&self) -> ParseIntegerErrorKind {
self.kind
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ParseIntegerErrorKind {
Empty,
InvalidDigit,
PosOverflow,
NegOverflow,
}
impl fmt::Display for ParseIntegerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match self.kind {
ParseIntegerErrorKind::Empty => "cannot parse integer without digits",
ParseIntegerErrorKind::InvalidDigit => "invalid digit found",
ParseIntegerErrorKind::PosOverflow => "integer too large to fit in target type",
ParseIntegerErrorKind::NegOverflow => "integer too small to fit in target type",
})
}
}
#[cfg(feature = "std")]
impl std::error::Error for ParseIntegerError {}
#[track_caller]
pub fn btou_radix<I>(bytes: &[u8], radix: u32) -> Result<I, ParseIntegerError>
where
I: FromPrimitive + Zero + CheckedAdd + CheckedMul,
{
assert!(
(2..=36).contains(&radix),
"radix must lie in the range 2..=36, found {}",
radix
);
let base = I::from_u32(radix).expect("radix can be represented as integer");
if bytes.is_empty() {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::Empty,
});
}
let mut result = I::zero();
for &digit in bytes {
let mul = result.checked_mul(&base);
let x = match char::from(digit).to_digit(radix).and_then(I::from_u32) {
Some(x) => x,
None => {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::InvalidDigit,
})
}
};
result = match mul {
Some(result) => result,
None => {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::PosOverflow,
})
}
};
result = match result.checked_add(&x) {
Some(result) => result,
None => {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::PosOverflow,
})
}
};
}
Ok(result)
}
#[track_caller]
pub fn btou<I>(bytes: &[u8]) -> Result<I, ParseIntegerError>
where
I: FromPrimitive + Zero + CheckedAdd + CheckedMul,
{
btou_radix(bytes, 10)
}
#[track_caller]
pub fn btoi_radix<I>(bytes: &[u8], radix: u32) -> Result<I, ParseIntegerError>
where
I: FromPrimitive + Zero + CheckedAdd + CheckedSub + CheckedMul,
{
assert!(
(2..=36).contains(&radix),
"radix must lie in the range 2..=36, found {}",
radix
);
let base = I::from_u32(radix).expect("radix can be represented as integer");
if bytes.is_empty() {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::Empty,
});
}
let digits = match bytes[0] {
b'+' => return btou_radix(&bytes[1..], radix),
b'-' => &bytes[1..],
_ => return btou_radix(bytes, radix),
};
if digits.is_empty() {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::Empty,
});
}
let mut result = I::zero();
for &digit in digits {
let mul = result.checked_mul(&base);
let x = match char::from(digit).to_digit(radix).and_then(I::from_u32) {
Some(x) => x,
None => {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::InvalidDigit,
})
}
};
result = match mul {
Some(result) => result,
None => {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::NegOverflow,
})
}
};
result = match result.checked_sub(&x) {
Some(result) => result,
None => {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::NegOverflow,
})
}
};
}
Ok(result)
}
#[track_caller]
pub fn btoi<I>(bytes: &[u8]) -> Result<I, ParseIntegerError>
where
I: FromPrimitive + Zero + CheckedAdd + CheckedSub + CheckedMul,
{
btoi_radix(bytes, 10)
}
#[track_caller]
pub fn btou_saturating_radix<I>(bytes: &[u8], radix: u32) -> Result<I, ParseIntegerError>
where
I: FromPrimitive + Zero + CheckedMul + Saturating + Bounded,
{
assert!(
(2..=36).contains(&radix),
"radix must lie in the range 2..=36, found {}",
radix
);
let base = I::from_u32(radix).expect("radix can be represented as integer");
if bytes.is_empty() {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::Empty,
});
}
let mut result = I::zero();
for &digit in bytes {
let mul = result.checked_mul(&base);
let x = match char::from(digit).to_digit(radix).and_then(I::from_u32) {
Some(x) => x,
None => {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::InvalidDigit,
})
}
};
result = match mul {
Some(result) => result,
None => return Ok(I::max_value()),
};
result = result.saturating_add(x);
}
Ok(result)
}
#[track_caller]
pub fn btou_saturating<I>(bytes: &[u8]) -> Result<I, ParseIntegerError>
where
I: FromPrimitive + Zero + CheckedMul + Saturating + Bounded,
{
btou_saturating_radix(bytes, 10)
}
#[track_caller]
pub fn btoi_saturating_radix<I>(bytes: &[u8], radix: u32) -> Result<I, ParseIntegerError>
where
I: FromPrimitive + Zero + CheckedMul + Saturating + Bounded,
{
assert!(
(2..=36).contains(&radix),
"radix must lie in the range 2..=36, found {}",
radix
);
let base = I::from_u32(radix).expect("radix can be represented as integer");
if bytes.is_empty() {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::Empty,
});
}
let digits = match bytes[0] {
b'+' => return btou_saturating_radix(&bytes[1..], radix),
b'-' => &bytes[1..],
_ => return btou_saturating_radix(bytes, radix),
};
if digits.is_empty() {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::Empty,
});
}
let mut result = I::zero();
for &digit in digits {
let mul = result.checked_mul(&base);
let x = match char::from(digit).to_digit(radix).and_then(I::from_u32) {
Some(x) => x,
None => {
return Err(ParseIntegerError {
kind: ParseIntegerErrorKind::InvalidDigit,
})
}
};
result = match mul {
Some(result) => result,
None => return Ok(I::min_value()),
};
result = result.saturating_sub(x);
}
Ok(result)
}
#[track_caller]
pub fn btoi_saturating<I>(bytes: &[u8]) -> Result<I, ParseIntegerError>
where
I: FromPrimitive + Zero + CheckedMul + Saturating + Bounded,
{
btoi_saturating_radix(bytes, 10)
}
#[cfg(test)]
mod tests {
#[cfg(feature = "std")]
extern crate std;
#[cfg(feature = "std")]
use std::{format, str, string::ToString as _, vec::Vec};
use super::*;
quickcheck! {
#[cfg(feature = "std")]
fn btou_identity(n: u32) -> bool {
Ok(n) == btou(n.to_string().as_bytes())
}
#[cfg(feature = "std")]
fn btou_binary_identity(n: u64) -> bool {
Ok(n) == btou_radix(format!("{:b}", n).as_bytes(), 2)
}
#[cfg(feature = "std")]
fn btou_octal_identity(n: u64) -> bool {
Ok(n) == btou_radix(format!("{:o}", n).as_bytes(), 8)
}
#[cfg(feature = "std")]
fn btou_lower_hex_identity(n: u64) -> bool {
Ok(n) == btou_radix(format!("{:x}", n).as_bytes(), 16)
}
#[cfg(feature = "std")]
fn btou_upper_hex_identity(n: u64) -> bool {
Ok(n) == btou_radix(format!("{:X}", n).as_bytes(), 16)
}
#[cfg(feature = "std")]
fn btoi_identity(n: i32) -> bool {
Ok(n) == btoi(n.to_string().as_bytes())
}
#[cfg(feature = "std")]
fn btou_saturating_identity(n: u32) -> bool {
Ok(n) == btou_saturating(n.to_string().as_bytes())
}
#[cfg(feature = "std")]
fn btoi_saturating_identity(n: i32) -> bool {
Ok(n) == btoi_saturating(n.to_string().as_bytes())
}
#[cfg(feature = "std")]
fn btoi_radix_std(bytes: Vec<u8>, radix: u32) -> bool {
let radix = radix % 35 + 2; str::from_utf8(&bytes).ok().and_then(|src| i32::from_str_radix(src, radix).ok()) ==
btoi_radix(&bytes, radix).ok()
}
}
#[test]
fn test_lone_minus() {
assert!(btoi::<isize>(b"-").is_err());
assert!(btoi_radix::<isize>(b"-", 16).is_err());
assert!(btoi_saturating::<isize>(b"-").is_err());
assert!(btoi_saturating_radix::<isize>(b"-", 8).is_err());
assert!(btou::<isize>(b"-").is_err());
assert!(btou_radix::<isize>(b"-", 16).is_err());
assert!(btou_saturating::<isize>(b"-").is_err());
assert!(btou_saturating_radix::<isize>(b"-", 8).is_err());
}
}