Skip to content

Commit

Permalink
Merge pull request #156 from Alexhuszagh/issue_97
Browse files Browse the repository at this point in the history
Ensure leading invalid digits for floats correctly error.
  • Loading branch information
Alexhuszagh authored Sep 20, 2024
2 parents f07e3db + 7317a23 commit 2cf88c8
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Inlining inconsistency between public API methods (credit to @zheland)
- Incorrectly accepting leading zeros when `no_integer_leading_zeros` was enabled.
- Have consistent errors when an invalid leading digit is found for floating point numbers to always be `Error::InvalidDigit`.

## [1.0.1] 2024-09-16

Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,6 @@ A benchmarks for values with a large integers.

![Simple Int64](https://github.com/Alexhuszagh/lexical-benchmarks/raw/main/results/da4728e/plot/random_simple_int64%20-%20write%20float%20-%20dtoa,fmt,lexical,ryu.png)


**Random**

![Random](https://github.com/Alexhuszagh/lexical-benchmarks/raw/main/results/da4728e/plot/json%20-%20write%20float%20-%20dtoa,fmt,lexical,ryu.png)
Expand Down
62 changes: 62 additions & 0 deletions lexical-core/tests/issue_97_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#![cfg(all(feature = "parse", feature = "format"))]

use core::num;

use lexical_core::{Error, FromLexical, FromLexicalWithOptions, NumberFormatBuilder};

#[test]
fn issue_97_test() {
const FMT: u128 = NumberFormatBuilder::new()
.digit_separator(num::NonZeroU8::new(b'_'))
.internal_digit_separator(true)
.build();

let fopts = lexical_core::ParseFloatOptions::new();
let iopts = lexical_core::ParseIntegerOptions::new();

assert_eq!(
i64::from_lexical_with_options::<FMT>(b"_1234", &iopts),
Err(Error::InvalidDigit(0))
);
assert_eq!(
i64::from_lexical_with_options::<FMT>(b"1234_", &iopts),
Err(Error::InvalidDigit(4))
);

assert_eq!(
f64::from_lexical_with_options::<FMT>(b"_1234", &fopts),
Err(Error::InvalidDigit(0))
);
assert_eq!(
f64::from_lexical_with_options::<FMT>(b"1234_", &fopts),
Err(Error::InvalidDigit(4))
);

assert_eq!(
f64::from_lexical_with_options::<FMT>(b"_12.34", &fopts),
Err(Error::InvalidDigit(0))
);
assert_eq!(
f64::from_lexical_with_options::<FMT>(b"12.34_", &fopts),
Err(Error::InvalidDigit(5))
);

assert_eq!(f64::from_lexical_with_options::<FMT>(b"1_2.34", &fopts), Ok(12.34));
}

#[test]
fn issue_97_nofmt_test() {
assert_eq!(i64::from_lexical(b"_1234"), Err(Error::InvalidDigit(0)));
assert_eq!(i64::from_lexical(b"1234_"), Err(Error::InvalidDigit(4)));

assert_eq!(f64::from_lexical(b"_1234"), Err(Error::InvalidDigit(0)));
assert_eq!(f64::from_lexical(b"1234_"), Err(Error::InvalidDigit(4)));

assert_eq!(f64::from_lexical(b"_12.34"), Err(Error::InvalidDigit(0)));
assert_eq!(f64::from_lexical(b"12.34_"), Err(Error::InvalidDigit(5)));

assert_eq!(f64::from_lexical(b"_.34"), Err(Error::InvalidDigit(0)));
assert_eq!(f64::from_lexical(b"0_0.34"), Err(Error::InvalidDigit(1)));

assert_eq!(f64::from_lexical(b".34"), Ok(0.34));
}
27 changes: 21 additions & 6 deletions lexical-parse-float/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,8 @@ pub fn parse_partial_number<'a, const FORMAT: u128>(
let mut implicit_exponent: i64;
let int_end = n_digits as i64;
let mut fraction_digits = None;
if byte.first_is_cased(decimal_point) {
let has_decimal = byte.first_is_cased(decimal_point);
if has_decimal {
// SAFETY: byte cannot be empty due to first_is
unsafe { byte.step_unchecked() };
let before = byte.clone();
Expand Down Expand Up @@ -599,19 +600,33 @@ pub fn parse_partial_number<'a, const FORMAT: u128>(
}
}

// NOTE: Check if we have our exponent **BEFORE** checking if the
// mantissa is empty, so we can ensure
let has_exponent = byte
.first_is(exponent_character, format.case_sensitive_exponent() && cfg!(feature = "format"));

// check to see if we have any inval;id leading zeros
n_digits += n_after_dot;
if format.required_mantissa_digits() && n_digits == 0 {
return Err(Error::EmptyMantissa(byte.cursor()));
let any_digits = start.clone().integer_iter().peek().is_some();
// NOTE: This is because numbers like `_12.34` have significant digits,
// they just don't have a valid digit (#97).
if has_decimal || has_exponent || !any_digits {
return Err(Error::EmptyMantissa(byte.cursor()));
} else {
return Err(Error::InvalidDigit(start.cursor()));
}
}

// EXPONENT

// Handle scientific notation.
let mut explicit_exponent = 0_i64;
let is_exponent = byte
.first_is(exponent_character, format.case_sensitive_exponent() && cfg!(feature = "format"));
if is_exponent {
// SAFETY: byte cannot be empty due to `first_is` from `is_exponent`.`
if has_exponent {
// NOTE: See above for the safety invariant above `required_mantissa_digits`.
// This is separated for correctness concerns, and therefore the two cannot
// be on the same line.
// SAFETY: byte cannot be empty due to `first_is` from `has_exponent`.`
unsafe { byte.step_unchecked() };

// Check float format syntax checks.
Expand Down

0 comments on commit 2cf88c8

Please sign in to comment.