1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-30 12:37:49 +00:00

uucore: format: num_parser: Turn parser into a trait

We call the function extended_parse, so that we do not clash
with other parsing functions in other traits.

- Also implement parser for ExtendedBigDecimal (straightforward).
- Base doesn't need to be public anymore.
- Rename the error to ExtendedParserError.
This commit is contained in:
Nicolas Boichat 2025-03-18 10:29:44 +01:00
parent 8bbec16115
commit 40a7c65980
2 changed files with 293 additions and 288 deletions

View file

@ -5,7 +5,7 @@
use crate::{ use crate::{
error::set_exit_code, error::set_exit_code,
features::format::num_parser::{ParseError, ParsedNumber}, features::format::num_parser::{ExtendedParser, ExtendedParserError},
quoting_style::{Quotes, QuotingStyle, escape_name}, quoting_style::{Quotes, QuotingStyle, escape_name},
show_error, show_warning, show_error, show_warning,
}; };
@ -56,7 +56,7 @@ impl<'a, T: Iterator<Item = &'a FormatArgument>> ArgumentIter<'a> for T {
}; };
match next { match next {
FormatArgument::UnsignedInt(n) => *n, FormatArgument::UnsignedInt(n) => *n,
FormatArgument::Unparsed(s) => extract_value(ParsedNumber::parse_u64(s), s), FormatArgument::Unparsed(s) => extract_value(u64::extended_parse(s), s),
_ => 0, _ => 0,
} }
} }
@ -67,7 +67,7 @@ impl<'a, T: Iterator<Item = &'a FormatArgument>> ArgumentIter<'a> for T {
}; };
match next { match next {
FormatArgument::SignedInt(n) => *n, FormatArgument::SignedInt(n) => *n,
FormatArgument::Unparsed(s) => extract_value(ParsedNumber::parse_i64(s), s), FormatArgument::Unparsed(s) => extract_value(i64::extended_parse(s), s),
_ => 0, _ => 0,
} }
} }
@ -78,7 +78,7 @@ impl<'a, T: Iterator<Item = &'a FormatArgument>> ArgumentIter<'a> for T {
}; };
match next { match next {
FormatArgument::Float(n) => *n, FormatArgument::Float(n) => *n,
FormatArgument::Unparsed(s) => extract_value(ParsedNumber::parse_f64(s), s), FormatArgument::Unparsed(s) => extract_value(f64::extended_parse(s), s),
_ => 0.0, _ => 0.0,
} }
} }
@ -91,7 +91,7 @@ impl<'a, T: Iterator<Item = &'a FormatArgument>> ArgumentIter<'a> for T {
} }
} }
fn extract_value<T: Default>(p: Result<T, ParseError<'_, T>>, input: &str) -> T { fn extract_value<T: Default>(p: Result<T, ExtendedParserError<'_, T>>, input: &str) -> T {
match p { match p {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
@ -103,15 +103,15 @@ fn extract_value<T: Default>(p: Result<T, ParseError<'_, T>>, input: &str) -> T
}, },
); );
match e { match e {
ParseError::Overflow => { ExtendedParserError::Overflow => {
show_error!("{}: Numerical result out of range", input.quote()); show_error!("{}: Numerical result out of range", input.quote());
Default::default() Default::default()
} }
ParseError::NotNumeric => { ExtendedParserError::NotNumeric => {
show_error!("{}: expected a numeric value", input.quote()); show_error!("{}: expected a numeric value", input.quote());
Default::default() Default::default()
} }
ParseError::PartialMatch(v, rest) => { ExtendedParserError::PartialMatch(v, rest) => {
let bytes = input.as_encoded_bytes(); let bytes = input.as_encoded_bytes();
if !bytes.is_empty() && bytes[0] == b'\'' { if !bytes.is_empty() && bytes[0] == b'\'' {
show_warning!( show_warning!(

View file

@ -8,8 +8,8 @@
// spell-checker:ignore powf copysign prec inity bigdecimal extendedbigdecimal biguint // spell-checker:ignore powf copysign prec inity bigdecimal extendedbigdecimal biguint
use bigdecimal::{ use bigdecimal::{
num_bigint::{BigInt, BigUint, Sign},
BigDecimal, BigDecimal,
num_bigint::{BigInt, BigUint, Sign},
}; };
use num_traits::ToPrimitive; use num_traits::ToPrimitive;
use num_traits::Zero; use num_traits::Zero;
@ -18,7 +18,7 @@ use crate::format::extendedbigdecimal::ExtendedBigDecimal;
/// Base for number parsing /// Base for number parsing
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
pub enum Base { enum Base {
/// Binary base /// Binary base
Binary = 2, Binary = 2,
@ -53,7 +53,7 @@ impl Base {
/// Type returned if a number could not be parsed in its entirety /// Type returned if a number could not be parsed in its entirety
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum ParseError<'a, T> { pub enum ExtendedParserError<'a, T> {
/// The input as a whole makes no sense /// The input as a whole makes no sense
NotNumeric, NotNumeric,
/// The beginning of the input made sense and has been parsed, /// The beginning of the input made sense and has been parsed,
@ -65,11 +65,14 @@ pub enum ParseError<'a, T> {
Overflow, Overflow,
} }
impl<'a, T> ParseError<'a, T> { impl<'a, T> ExtendedParserError<'a, T> {
fn map<U>(self, f: impl FnOnce(T, &'a str) -> ParseError<'a, U>) -> ParseError<'a, U> { fn map<U>(
self,
f: impl FnOnce(T, &'a str) -> ExtendedParserError<'a, U>,
) -> ExtendedParserError<'a, U> {
match self { match self {
Self::NotNumeric => ParseError::NotNumeric, Self::NotNumeric => ExtendedParserError::NotNumeric,
Self::Overflow => ParseError::Overflow, Self::Overflow => ExtendedParserError::Overflow,
Self::PartialMatch(v, s) => f(v, s), Self::PartialMatch(v, s) => f(v, s),
} }
} }
@ -77,375 +80,377 @@ impl<'a, T> ParseError<'a, T> {
/// A number parser for binary, octal, decimal, hexadecimal and single characters. /// A number parser for binary, octal, decimal, hexadecimal and single characters.
/// ///
/// TODO: we just keep an ExtendedBigDecimal internally, so we don't really need this /// It is implemented for `u64`/`i64`, where no fractional part is parsed,
/// struct actually. /// and `f64` float, where octal is not allowed.
/// pub trait ExtendedParser {
/// If the fractional part cannot be represented on a `u64`, parsing continues // We pick a hopefully different name for our parser, to avoid clash with standard traits.
/// silently by ignoring non-significant digits. fn extended_parse(input: &str) -> Result<Self, ExtendedParserError<'_, Self>>
pub struct ParsedNumber { where
number: ExtendedBigDecimal, Self: Sized;
} }
impl ParsedNumber { impl ExtendedParser for i64 {
fn into_i64(self) -> Option<i64> {
match self.number {
ExtendedBigDecimal::BigDecimal(bd) => {
let (digits, scale) = bd.into_bigint_and_scale();
if scale == 0 {
i64::try_from(digits).ok()
} else {
None
}
}
ExtendedBigDecimal::MinusZero => Some(0),
_ => None,
}
}
/// Parse a number as i64. No fractional part is allowed. /// Parse a number as i64. No fractional part is allowed.
pub fn parse_i64(input: &str) -> Result<i64, ParseError<'_, i64>> { fn extended_parse(input: &str) -> Result<i64, ExtendedParserError<'_, i64>> {
match Self::parse(input, true) { fn into_i64(ebd: ExtendedBigDecimal) -> Option<i64> {
Ok(v) => v.into_i64().ok_or(ParseError::Overflow), match ebd {
ExtendedBigDecimal::BigDecimal(bd) => {
let (digits, scale) = bd.into_bigint_and_scale();
if scale == 0 {
i64::try_from(digits).ok()
} else {
None
}
}
ExtendedBigDecimal::MinusZero => Some(0),
_ => None,
}
}
match parse(input, true) {
Ok(v) => into_i64(v).ok_or(ExtendedParserError::Overflow),
Err(e) => Err(e.map(|v, rest| { Err(e) => Err(e.map(|v, rest| {
v.into_i64() into_i64(v)
.map(|v| ParseError::PartialMatch(v, rest)) .map(|v| ExtendedParserError::PartialMatch(v, rest))
.unwrap_or(ParseError::Overflow) .unwrap_or(ExtendedParserError::Overflow)
})), })),
} }
} }
}
fn into_u64(self) -> Option<u64> { impl ExtendedParser for u64 {
match self.number {
ExtendedBigDecimal::BigDecimal(bd) => {
let (digits, scale) = bd.into_bigint_and_scale();
if scale == 0 {
u64::try_from(digits).ok()
} else {
None
}
}
_ => None,
}
}
/// Parse a number as u64. No fractional part is allowed. /// Parse a number as u64. No fractional part is allowed.
pub fn parse_u64(input: &str) -> Result<u64, ParseError<'_, u64>> { fn extended_parse(input: &str) -> Result<u64, ExtendedParserError<'_, u64>> {
match Self::parse(input, true) { fn into_u64(ebd: ExtendedBigDecimal) -> Option<u64> {
Ok(v) => v.into_u64().ok_or(ParseError::Overflow), match ebd {
ExtendedBigDecimal::BigDecimal(bd) => {
let (digits, scale) = bd.into_bigint_and_scale();
if scale == 0 {
u64::try_from(digits).ok()
} else {
None
}
}
_ => None,
}
}
match parse(input, true) {
Ok(v) => into_u64(v).ok_or(ExtendedParserError::Overflow),
Err(e) => Err(e.map(|v, rest| { Err(e) => Err(e.map(|v, rest| {
v.into_u64() into_u64(v)
.map(|v| ParseError::PartialMatch(v, rest)) .map(|v| ExtendedParserError::PartialMatch(v, rest))
.unwrap_or(ParseError::Overflow) .unwrap_or(ExtendedParserError::Overflow)
})), })),
} }
} }
}
fn into_f64(self) -> f64 { impl ExtendedParser for f64 {
match self.number {
ExtendedBigDecimal::BigDecimal(bd) => bd.to_f64().unwrap(),
ExtendedBigDecimal::MinusZero => -0.0,
ExtendedBigDecimal::Nan => f64::NAN,
ExtendedBigDecimal::MinusNan => -f64::NAN,
ExtendedBigDecimal::Infinity => f64::INFINITY,
ExtendedBigDecimal::MinusInfinity => -f64::INFINITY,
}
}
/// Parse a number as f64 /// Parse a number as f64
pub fn parse_f64(input: &str) -> Result<f64, ParseError<'_, f64>> { fn extended_parse(input: &str) -> Result<f64, ExtendedParserError<'_, f64>> {
match Self::parse(input, false) { // TODO: This is generic, so this should probably be implemented as an ExtendedBigDecimal trait (ToPrimitive).
Ok(v) => Ok(v.into_f64()), fn into_f64(ebd: ExtendedBigDecimal) -> f64 {
Err(e) => Err(e.map(|v, rest| ParseError::PartialMatch(v.into_f64(), rest))), match ebd {
} ExtendedBigDecimal::BigDecimal(bd) => bd.to_f64().unwrap(),
} ExtendedBigDecimal::MinusZero => -0.0,
ExtendedBigDecimal::Nan => f64::NAN,
fn parse_special_value(input: &str, negative: bool) -> Result<Self, ParseError<'_, Self>> { ExtendedBigDecimal::MinusNan => -f64::NAN,
let prefix = input ExtendedBigDecimal::Infinity => f64::INFINITY,
.chars() ExtendedBigDecimal::MinusInfinity => -f64::INFINITY,
.take(3)
.map(|c| c.to_ascii_lowercase())
.collect::<String>();
let special = Self {
number: match prefix.as_str() {
"inf" => {
if negative {
ExtendedBigDecimal::MinusInfinity
} else {
ExtendedBigDecimal::Infinity
}
}
"nan" => {
if negative {
ExtendedBigDecimal::MinusNan
} else {
ExtendedBigDecimal::Nan
}
}
_ => return Err(ParseError::NotNumeric),
},
};
if input.len() == 3 {
Ok(special)
} else {
Err(ParseError::PartialMatch(special, &input[3..]))
}
}
#[allow(clippy::cognitive_complexity)]
fn parse(input: &str, integral_only: bool) -> Result<Self, ParseError<'_, Self>> {
// Parse the "'" prefix separately
if let Some(rest) = input.strip_prefix('\'') {
let mut chars = rest.char_indices().fuse();
let v = chars.next().map(|(_, c)| Self {
number: ExtendedBigDecimal::BigDecimal(u32::from(c).into()),
});
return match (v, chars.next()) {
(Some(v), None) => Ok(v),
(Some(v), Some((i, _))) => Err(ParseError::PartialMatch(v, &rest[i..])),
(None, _) => Err(ParseError::NotNumeric),
};
}
let trimmed_input = input.trim_ascii_start();
// Initial minus sign
let (negative, unsigned) = if let Some(trimmed_input) = trimmed_input.strip_prefix('-') {
(true, trimmed_input)
} else {
(false, trimmed_input)
};
// Parse an optional base prefix ("0b" / "0B" / "0" / "0x" / "0X"). "0" is octal unless a
// fractional part is allowed in which case it is an insignificant leading 0. A "0" prefix
// will not be consumed in case the parsable string contains only "0": the leading extra "0"
// will have no influence on the result.
let (base, rest) = if let Some(rest) = unsigned.strip_prefix('0') {
if let Some(rest) = rest.strip_prefix(['b', 'B']) {
(Base::Binary, rest)
} else if let Some(rest) = rest.strip_prefix(['x', 'X']) {
(Base::Hexadecimal, rest)
} else if integral_only {
(Base::Octal, unsigned)
} else {
(Base::Decimal, unsigned)
} }
}
match parse(input, false) {
Ok(v) => Ok(into_f64(v)),
Err(e) => Err(e.map(|v, rest| ExtendedParserError::PartialMatch(into_f64(v), rest))),
}
}
}
fn parse_special_value(
input: &str,
negative: bool,
) -> Result<ExtendedBigDecimal, ExtendedParserError<'_, ExtendedBigDecimal>> {
let prefix = input
.chars()
.take(3)
.map(|c| c.to_ascii_lowercase())
.collect::<String>();
let special = match prefix.as_str() {
"inf" => {
if negative {
ExtendedBigDecimal::MinusInfinity
} else {
ExtendedBigDecimal::Infinity
}
}
"nan" => {
if negative {
ExtendedBigDecimal::MinusNan
} else {
ExtendedBigDecimal::Nan
}
}
_ => return Err(ExtendedParserError::NotNumeric),
};
if input.len() == 3 {
Ok(special)
} else {
Err(ExtendedParserError::PartialMatch(special, &input[3..]))
}
}
#[allow(clippy::cognitive_complexity)]
fn parse(
input: &str,
integral_only: bool,
) -> Result<ExtendedBigDecimal, ExtendedParserError<'_, ExtendedBigDecimal>> {
// Parse the "'" prefix separately
if let Some(rest) = input.strip_prefix('\'') {
let mut chars = rest.char_indices().fuse();
let v = chars
.next()
.map(|(_, c)| ExtendedBigDecimal::BigDecimal(u32::from(c).into()));
return match (v, chars.next()) {
(Some(v), None) => Ok(v),
(Some(v), Some((i, _))) => Err(ExtendedParserError::PartialMatch(v, &rest[i..])),
(None, _) => Err(ExtendedParserError::NotNumeric),
};
}
let trimmed_input = input.trim_ascii_start();
// Initial minus sign
let (negative, unsigned) = if let Some(trimmed_input) = trimmed_input.strip_prefix('-') {
(true, trimmed_input)
} else {
(false, trimmed_input)
};
// Parse an optional base prefix ("0b" / "0B" / "0" / "0x" / "0X"). "0" is octal unless a
// fractional part is allowed in which case it is an insignificant leading 0. A "0" prefix
// will not be consumed in case the parsable string contains only "0": the leading extra "0"
// will have no influence on the result.
let (base, rest) = if let Some(rest) = unsigned.strip_prefix('0') {
if let Some(rest) = rest.strip_prefix(['b', 'B']) {
(Base::Binary, rest)
} else if let Some(rest) = rest.strip_prefix(['x', 'X']) {
(Base::Hexadecimal, rest)
} else if integral_only {
(Base::Octal, unsigned)
} else { } else {
(Base::Decimal, unsigned) (Base::Decimal, unsigned)
};
if rest.is_empty() {
return Err(ParseError::NotNumeric);
} }
} else {
(Base::Decimal, unsigned)
};
if rest.is_empty() {
return Err(ExtendedParserError::NotNumeric);
}
// Parse the integral part of the number // Parse the integral part of the number
let mut chars = rest.chars().enumerate().fuse().peekable(); let mut chars = rest.chars().enumerate().fuse().peekable();
let mut digits = BigUint::zero(); let mut digits = BigUint::zero();
let mut scale = 0i64; let mut scale = 0i64;
while let Some(d) = chars.peek().and_then(|&(_, c)| base.digit(c)) {
chars.next();
digits = digits * base as u8 + d;
}
// Parse the fractional part of the number if there can be one and the input contains
// a '.' decimal separator.
if matches!(chars.peek(), Some(&(_, '.')))
&& matches!(base, Base::Decimal | Base::Hexadecimal)
&& !integral_only
{
chars.next();
while let Some(d) = chars.peek().and_then(|&(_, c)| base.digit(c)) { while let Some(d) = chars.peek().and_then(|&(_, c)| base.digit(c)) {
chars.next(); chars.next();
digits = digits * base as u8 + d; (digits, scale) = (digits * base as u8 + d, scale + 1);
} }
}
// Parse the fractional part of the number if there can be one and the input contains // If nothing has been parsed, check if this is a special value, or declare the parsing unsuccessful
// a '.' decimal separator. if let Some((0, _)) = chars.peek() {
if matches!(chars.peek(), Some(&(_, '.'))) if integral_only {
&& matches!(base, Base::Decimal | Base::Hexadecimal) return Err(ExtendedParserError::NotNumeric);
&& !integral_only
{
chars.next();
while let Some(d) = chars.peek().and_then(|&(_, c)| base.digit(c)) {
chars.next();
(digits, scale) = (digits * base as u8 + d, scale + 1);
}
}
// If nothing has been parsed, check if this is a special value, or declare the parsing unsuccessful
if let Some((0, _)) = chars.peek() {
if integral_only {
return Err(ParseError::NotNumeric);
} else {
return Self::parse_special_value(unsigned, negative);
}
}
// TODO: Might be nice to implement a ExtendedBigDecimal copysign or negation function to move away some of this logic...
let ebd = if digits == BigUint::zero() && negative {
ExtendedBigDecimal::MinusZero
} else { } else {
let sign = if negative { Sign::Minus } else { Sign::Plus }; return parse_special_value(unsigned, negative);
let signed_digits = BigInt::from_biguint(sign, digits); }
let bd = if scale == 0 { }
BigDecimal::from_bigint(signed_digits, 0)
} else if base == Base::Decimal { // TODO: Might be nice to implement a ExtendedBigDecimal copysign or negation function to move away some of this logic...
BigDecimal::from_bigint(signed_digits, scale) let ebd = if digits == BigUint::zero() && negative {
} else { ExtendedBigDecimal::MinusZero
// Base is not 10, init at scale 0 then divide by base**scale. } else {
BigDecimal::from_bigint(signed_digits, 0) / (base as u64).pow(scale as u32) let sign = if negative { Sign::Minus } else { Sign::Plus };
}; let signed_digits = BigInt::from_biguint(sign, digits);
ExtendedBigDecimal::BigDecimal(bd) let bd = if scale == 0 {
BigDecimal::from_bigint(signed_digits, 0)
} else if base == Base::Decimal {
BigDecimal::from_bigint(signed_digits, scale)
} else {
// Base is not 10, init at scale 0 then divide by base**scale.
BigDecimal::from_bigint(signed_digits, 0) / (base as u64).pow(scale as u32)
}; };
ExtendedBigDecimal::BigDecimal(bd)
};
// Return what has been parsed so far. It there are extra characters, mark the // Return what has been parsed so far. It there are extra characters, mark the
// parsing as a partial match. // parsing as a partial match.
let parsed = Self { number: ebd }; if let Some((first_unparsed, _)) = chars.next() {
if let Some((first_unparsed, _)) = chars.next() { Err(ExtendedParserError::PartialMatch(
Err(ParseError::PartialMatch(parsed, &rest[first_unparsed..])) ebd,
} else { &rest[first_unparsed..],
Ok(parsed) ))
} } else {
Ok(ebd)
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ParseError, ParsedNumber}; use super::{ExtendedParser, ExtendedParserError};
#[test] #[test]
fn test_decimal_u64() { fn test_decimal_u64() {
assert_eq!(Ok(123), ParsedNumber::parse_u64("123")); assert_eq!(Ok(123), u64::extended_parse("123"));
assert_eq!( assert_eq!(Ok(u64::MAX), u64::extended_parse(&format!("{}", u64::MAX)));
Ok(u64::MAX),
ParsedNumber::parse_u64(&format!("{}", u64::MAX))
);
assert!(matches!( assert!(matches!(
ParsedNumber::parse_u64("-123"), u64::extended_parse("-123"),
Err(ParseError::Overflow) Err(ExtendedParserError::Overflow)
)); ));
assert!(matches!( assert!(matches!(
ParsedNumber::parse_u64(""), u64::extended_parse(""),
Err(ParseError::NotNumeric) Err(ExtendedParserError::NotNumeric)
)); ));
assert!(matches!( assert!(matches!(
ParsedNumber::parse_u64("123.15"), u64::extended_parse("123.15"),
Err(ParseError::PartialMatch(123, ".15")) Err(ExtendedParserError::PartialMatch(123, ".15"))
)); ));
} }
#[test] #[test]
fn test_decimal_i64() { fn test_decimal_i64() {
assert_eq!(Ok(123), ParsedNumber::parse_i64("123")); assert_eq!(Ok(123), i64::extended_parse("123"));
assert_eq!(Ok(-123), ParsedNumber::parse_i64("-123")); assert_eq!(Ok(-123), i64::extended_parse("-123"));
assert!(matches!( assert!(matches!(
ParsedNumber::parse_i64("--123"), i64::extended_parse("--123"),
Err(ParseError::NotNumeric) Err(ExtendedParserError::NotNumeric)
)); ));
assert_eq!( assert_eq!(Ok(i64::MAX), i64::extended_parse(&format!("{}", i64::MAX)));
Ok(i64::MAX), assert_eq!(Ok(i64::MIN), i64::extended_parse(&format!("{}", i64::MIN)));
ParsedNumber::parse_i64(&format!("{}", i64::MAX))
);
assert_eq!(
Ok(i64::MIN),
ParsedNumber::parse_i64(&format!("{}", i64::MIN))
);
assert!(matches!( assert!(matches!(
ParsedNumber::parse_i64(&format!("{}", u64::MAX)), i64::extended_parse(&format!("{}", u64::MAX)),
Err(ParseError::Overflow) Err(ExtendedParserError::Overflow)
)); ));
assert!(matches!( assert!(matches!(
ParsedNumber::parse_i64(&format!("{}", i64::MAX as u64 + 1)), i64::extended_parse(&format!("{}", i64::MAX as u64 + 1)),
Err(ParseError::Overflow) Err(ExtendedParserError::Overflow)
)); ));
} }
#[test] #[test]
fn test_decimal_f64() { fn test_decimal_f64() {
assert_eq!(Ok(123.0), ParsedNumber::parse_f64("123")); assert_eq!(Ok(123.0), f64::extended_parse("123"));
assert_eq!(Ok(-123.0), ParsedNumber::parse_f64("-123")); assert_eq!(Ok(-123.0), f64::extended_parse("-123"));
assert_eq!(Ok(123.0), ParsedNumber::parse_f64("123.")); assert_eq!(Ok(123.0), f64::extended_parse("123."));
assert_eq!(Ok(-123.0), ParsedNumber::parse_f64("-123.")); assert_eq!(Ok(-123.0), f64::extended_parse("-123."));
assert_eq!(Ok(123.0), ParsedNumber::parse_f64("123.0")); assert_eq!(Ok(123.0), f64::extended_parse("123.0"));
assert_eq!(Ok(-123.0), ParsedNumber::parse_f64("-123.0")); assert_eq!(Ok(-123.0), f64::extended_parse("-123.0"));
assert_eq!(Ok(123.15), ParsedNumber::parse_f64("123.15")); assert_eq!(Ok(123.15), f64::extended_parse("123.15"));
assert_eq!(Ok(-123.15), ParsedNumber::parse_f64("-123.15")); assert_eq!(Ok(-123.15), f64::extended_parse("-123.15"));
assert_eq!(Ok(0.15), ParsedNumber::parse_f64(".15")); assert_eq!(Ok(0.15), f64::extended_parse(".15"));
assert_eq!(Ok(-0.15), ParsedNumber::parse_f64("-.15")); assert_eq!(Ok(-0.15), f64::extended_parse("-.15"));
assert_eq!( assert_eq!(
Ok(0.15), Ok(0.15),
ParsedNumber::parse_f64(".150000000000000000000000000231313") f64::extended_parse(".150000000000000000000000000231313")
); );
assert!(matches!(ParsedNumber::parse_f64("1.2.3"), assert!(matches!(f64::extended_parse("1.2.3"),
Err(ParseError::PartialMatch(f, ".3")) if f == 1.2)); Err(ExtendedParserError::PartialMatch(f, ".3")) if f == 1.2));
assert_eq!(Ok(f64::INFINITY), ParsedNumber::parse_f64("inf")); assert_eq!(Ok(f64::INFINITY), f64::extended_parse("inf"));
assert_eq!(Ok(f64::NEG_INFINITY), ParsedNumber::parse_f64("-inf")); assert_eq!(Ok(f64::NEG_INFINITY), f64::extended_parse("-inf"));
assert!(ParsedNumber::parse_f64("NaN").unwrap().is_nan()); assert!(f64::extended_parse("NaN").unwrap().is_nan());
assert!(ParsedNumber::parse_f64("NaN").unwrap().is_sign_positive()); assert!(f64::extended_parse("NaN").unwrap().is_sign_positive());
assert!(ParsedNumber::parse_f64("-NaN").unwrap().is_nan()); assert!(f64::extended_parse("-NaN").unwrap().is_nan());
assert!(ParsedNumber::parse_f64("-NaN").unwrap().is_sign_negative()); assert!(f64::extended_parse("-NaN").unwrap().is_sign_negative());
assert!(matches!(ParsedNumber::parse_f64("-infinity"), assert!(matches!(f64::extended_parse("-infinity"),
Err(ParseError::PartialMatch(f, "inity")) if f == f64::NEG_INFINITY)); Err(ExtendedParserError::PartialMatch(f, "inity")) if f == f64::NEG_INFINITY));
assert!(ParsedNumber::parse_f64(&format!("{}", u64::MAX)).is_ok()); assert!(f64::extended_parse(&format!("{}", u64::MAX)).is_ok());
assert!(ParsedNumber::parse_f64(&format!("{}", i64::MIN)).is_ok()); assert!(f64::extended_parse(&format!("{}", i64::MIN)).is_ok());
} }
#[test] #[test]
fn test_hexadecimal() { fn test_hexadecimal() {
assert_eq!(Ok(0x123), ParsedNumber::parse_u64("0x123")); assert_eq!(Ok(0x123), u64::extended_parse("0x123"));
assert_eq!(Ok(0x123), ParsedNumber::parse_u64("0X123")); assert_eq!(Ok(0x123), u64::extended_parse("0X123"));
assert_eq!(Ok(0xfe), ParsedNumber::parse_u64("0xfE")); assert_eq!(Ok(0xfe), u64::extended_parse("0xfE"));
assert_eq!(Ok(-0x123), ParsedNumber::parse_i64("-0x123")); assert_eq!(Ok(-0x123), i64::extended_parse("-0x123"));
assert_eq!(Ok(0.5), ParsedNumber::parse_f64("0x.8")); assert_eq!(Ok(0.5), f64::extended_parse("0x.8"));
assert_eq!(Ok(0.0625), ParsedNumber::parse_f64("0x.1")); assert_eq!(Ok(0.0625), f64::extended_parse("0x.1"));
assert_eq!(Ok(15.007_812_5), ParsedNumber::parse_f64("0xf.02")); assert_eq!(Ok(15.007_812_5), f64::extended_parse("0xf.02"));
} }
#[test] #[test]
fn test_octal() { fn test_octal() {
assert_eq!(Ok(0), ParsedNumber::parse_u64("0")); assert_eq!(Ok(0), u64::extended_parse("0"));
assert_eq!(Ok(0o123), ParsedNumber::parse_u64("0123")); assert_eq!(Ok(0o123), u64::extended_parse("0123"));
assert_eq!(Ok(0o123), ParsedNumber::parse_u64("00123")); assert_eq!(Ok(0o123), u64::extended_parse("00123"));
assert_eq!(Ok(0), ParsedNumber::parse_u64("00")); assert_eq!(Ok(0), u64::extended_parse("00"));
assert!(matches!( assert!(matches!(
ParsedNumber::parse_u64("008"), u64::extended_parse("008"),
Err(ParseError::PartialMatch(0, "8")) Err(ExtendedParserError::PartialMatch(0, "8"))
)); ));
assert!(matches!( assert!(matches!(
ParsedNumber::parse_u64("08"), u64::extended_parse("08"),
Err(ParseError::PartialMatch(0, "8")) Err(ExtendedParserError::PartialMatch(0, "8"))
)); ));
assert!(matches!( assert!(matches!(
ParsedNumber::parse_u64("0."), u64::extended_parse("0."),
Err(ParseError::PartialMatch(0, ".")) Err(ExtendedParserError::PartialMatch(0, "."))
)); ));
} }
#[test] #[test]
fn test_binary() { fn test_binary() {
assert_eq!(Ok(0b1011), ParsedNumber::parse_u64("0b1011")); assert_eq!(Ok(0b1011), u64::extended_parse("0b1011"));
assert_eq!(Ok(0b1011), ParsedNumber::parse_u64("0B1011")); assert_eq!(Ok(0b1011), u64::extended_parse("0B1011"));
} }
#[test] #[test]
fn test_parsing_with_leading_whitespace() { fn test_parsing_with_leading_whitespace() {
assert_eq!(Ok(1), ParsedNumber::parse_u64(" 0x1")); assert_eq!(Ok(1), u64::extended_parse(" 0x1"));
assert_eq!(Ok(-2), ParsedNumber::parse_i64(" -0x2")); assert_eq!(Ok(-2), i64::extended_parse(" -0x2"));
assert_eq!(Ok(-3), ParsedNumber::parse_i64(" \t-0x3")); assert_eq!(Ok(-3), i64::extended_parse(" \t-0x3"));
assert_eq!(Ok(-4), ParsedNumber::parse_i64(" \n-0x4")); assert_eq!(Ok(-4), i64::extended_parse(" \n-0x4"));
assert_eq!(Ok(-5), ParsedNumber::parse_i64(" \n\t\u{000d}-0x5")); assert_eq!(Ok(-5), i64::extended_parse(" \n\t\u{000d}-0x5"));
// Ensure that trailing whitespace is still a partial match // Ensure that trailing whitespace is still a partial match
assert_eq!( assert_eq!(
Err(ParseError::PartialMatch(6, " ")), Err(ExtendedParserError::PartialMatch(6, " ")),
ParsedNumber::parse_u64("0x6 ") u64::extended_parse("0x6 ")
); );
assert_eq!( assert_eq!(
Err(ParseError::PartialMatch(7, "\t")), Err(ExtendedParserError::PartialMatch(7, "\t")),
ParsedNumber::parse_u64("0x7\t") u64::extended_parse("0x7\t")
); );
assert_eq!( assert_eq!(
Err(ParseError::PartialMatch(8, "\n")), Err(ExtendedParserError::PartialMatch(8, "\n")),
ParsedNumber::parse_u64("0x8\n") u64::extended_parse("0x8\n")
); );
// Ensure that unicode non-ascii whitespace is a partial match // Ensure that unicode non-ascii whitespace is a partial match
assert_eq!( assert_eq!(
Err(ParseError::NotNumeric), Err(ExtendedParserError::NotNumeric),
ParsedNumber::parse_i64("\u{2029}-0x9") i64::extended_parse("\u{2029}-0x9")
); );
// Ensure that whitespace after the number has "started" is not allowed // Ensure that whitespace after the number has "started" is not allowed
assert_eq!( assert_eq!(
Err(ParseError::NotNumeric), Err(ExtendedParserError::NotNumeric),
ParsedNumber::parse_i64("- 0x9") i64::extended_parse("- 0x9")
); );
} }
} }