From f9cf171c0a3ebe3f5ed61906d89e72489e1ecb5d Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Mon, 9 Aug 2021 14:33:16 -0500 Subject: [PATCH] Added a formatter for generic radix floats. - Added more tests for binary floats. - Added support for mandatory signs in integer writers. - Added support for generic radix floats. - Added extensive unittests for generic radix floats. - Implemented the write float ToLexical API. - Added the WriteFloat trait. Disabled some QuickCheck tests due to the following bug: https://github.com/BurntSushi/quickcheck/issues/295 --- lexical-asm/Cargo.toml | 1 + lexical-benchmark/write-integer/Cargo.toml | 1 + lexical-core/Cargo.toml | 1 + lexical-size/Cargo.toml.in | 1 + lexical-util/src/algorithm.rs | 7 + lexical-util/src/num.rs | 22 + lexical-util/tests/algorithm_tests.rs | 18 + lexical-util/tests/num_tests.rs | 2 + lexical-write-float/Cargo.toml | 1 + lexical-write-float/src/algorithm.rs | 22 + lexical-write-float/src/api.rs | 144 ++++++ lexical-write-float/src/binary.rs | 10 +- lexical-write-float/src/compact.rs | 7 +- lexical-write-float/src/hex.rs | 21 + lexical-write-float/src/lib.rs | 5 +- lexical-write-float/src/radix.rs | 551 +++++++++++++++++++++ lexical-write-float/src/write.rs | 152 ++++++ lexical-write-float/tests/api_tests.rs | 1 + lexical-write-float/tests/binary_tests.rs | 118 +---- lexical-write-float/tests/hex_tests.rs | 1 + lexical-write-float/tests/parse_radix.rs | 85 ++++ lexical-write-float/tests/radix_tests.rs | 347 +++++++++++++ lexical-write-integer/Cargo.toml | 2 + lexical-write-integer/src/api.rs | 26 +- lexical-write-integer/src/write.rs | 6 +- lexical-write-integer/tests/api_tests.rs | 13 + 26 files changed, 1451 insertions(+), 114 deletions(-) create mode 100644 lexical-write-float/src/api.rs create mode 100644 lexical-write-float/src/write.rs create mode 100644 lexical-write-float/tests/api_tests.rs create mode 100644 lexical-write-float/tests/hex_tests.rs create mode 100644 lexical-write-float/tests/parse_radix.rs create mode 100644 lexical-write-float/tests/radix_tests.rs diff --git a/lexical-asm/Cargo.toml b/lexical-asm/Cargo.toml index 4c17079..6b3ad1c 100644 --- a/lexical-asm/Cargo.toml +++ b/lexical-asm/Cargo.toml @@ -56,6 +56,7 @@ format = [ "lexical-util/format", "lexical-parse-integer/format", "lexical-parse-float/format", + "lexical-write-integer/format", "lexical-write-float/format" ] compact = [ diff --git a/lexical-benchmark/write-integer/Cargo.toml b/lexical-benchmark/write-integer/Cargo.toml index 8f3db28..41b5050 100644 --- a/lexical-benchmark/write-integer/Cargo.toml +++ b/lexical-benchmark/write-integer/Cargo.toml @@ -28,6 +28,7 @@ default = ["std"] std = ["lexical-util/std", "lexical-write-integer/std"] radix = ["lexical-util/radix", "lexical-write-integer/radix"] power-of-two = ["lexical-util/power-of-two", "lexical-write-integer/power-of-two"] +format = ["lexical-util/format", "lexical-write-integer/format"] [[bench]] name = "json" diff --git a/lexical-core/Cargo.toml b/lexical-core/Cargo.toml index 1b39a7a..be33e13 100644 --- a/lexical-core/Cargo.toml +++ b/lexical-core/Cargo.toml @@ -76,6 +76,7 @@ radix = [ format = [ "lexical-parse-integer/format", "lexical-parse-float/format", + "lexical-write-integer/format", "lexical-write-float/format" ] # Reduce code size at the cost of performance. diff --git a/lexical-size/Cargo.toml.in b/lexical-size/Cargo.toml.in index b959bfc..9683a2e 100644 --- a/lexical-size/Cargo.toml.in +++ b/lexical-size/Cargo.toml.in @@ -58,6 +58,7 @@ format = [ "lexical-util/format", "lexical-parse-integer/format", "lexical-parse-float/format", + "lexical-write-integer/format", "lexical-write-float/format" ] compact = [ diff --git a/lexical-util/src/algorithm.rs b/lexical-util/src/algorithm.rs index bea158a..d198808 100644 --- a/lexical-util/src/algorithm.rs +++ b/lexical-util/src/algorithm.rs @@ -30,6 +30,13 @@ pub fn rtrim_char_count(slc: &[u8], c: u8) -> usize { slc.iter().rev().take_while(|&&si| si == c).count() } +/// Count the number of leading characters equal to a given value. +#[inline] +#[cfg(feature = "write")] +pub fn ltrim_char_count(slc: &[u8], c: u8) -> usize { + slc.iter().take_while(|&&si| si == c).count() +} + /// Trim character from the end (right-side) of a slice. #[inline] #[cfg(feature = "write")] diff --git a/lexical-util/src/num.rs b/lexical-util/src/num.rs index 66e9c6c..04bfa7b 100644 --- a/lexical-util/src/num.rs +++ b/lexical-util/src/num.rs @@ -491,6 +491,8 @@ pub trait Float: Number + ops::Neg { // Re-export the to and from bits methods. fn to_bits(self) -> Self::Unsigned; fn from_bits(u: Self::Unsigned) -> Self; + fn ln(self) -> Self; + fn floor(self) -> Self; fn is_sign_positive(self) -> bool; fn is_sign_negative(self) -> bool; @@ -744,6 +746,16 @@ impl Float for f32 { f32::from_bits(u) } + #[inline] + fn ln(self) -> f32 { + f32::ln(self) + } + + #[inline] + fn floor(self) -> f32 { + f32::floor(self) + } + #[inline] fn is_sign_positive(self) -> bool { f32::is_sign_positive(self) @@ -782,6 +794,16 @@ impl Float for f64 { f64::from_bits(u) } + #[inline] + fn ln(self) -> f64 { + f64::ln(self) + } + + #[inline] + fn floor(self) -> f64 { + f64::floor(self) + } + #[inline] fn is_sign_positive(self) -> bool { f64::is_sign_positive(self) diff --git a/lexical-util/tests/algorithm_tests.rs b/lexical-util/tests/algorithm_tests.rs index 76dfa4a..a8b1907 100644 --- a/lexical-util/tests/algorithm_tests.rs +++ b/lexical-util/tests/algorithm_tests.rs @@ -11,6 +11,24 @@ fn copy_to_dest_test() { assert_eq!(&dst[..5], src); } +#[test] +#[cfg(feature = "write")] +fn ltrim_char_test() { + let w = "0001"; + let x = "1010"; + let y = "1.00"; + let z = "1e05"; + + assert_eq!(algorithm::ltrim_char_count(w.as_bytes(), b'0'), 3); + assert_eq!(algorithm::ltrim_char_count(x.as_bytes(), b'0'), 0); + assert_eq!(algorithm::ltrim_char_count(x.as_bytes(), b'1'), 1); + assert_eq!(algorithm::ltrim_char_count(y.as_bytes(), b'0'), 0); + assert_eq!(algorithm::ltrim_char_count(y.as_bytes(), b'1'), 1); + assert_eq!(algorithm::ltrim_char_count(z.as_bytes(), b'0'), 0); + assert_eq!(algorithm::ltrim_char_count(z.as_bytes(), b'1'), 1); + assert_eq!(algorithm::ltrim_char_count(z.as_bytes(), b'5'), 0); +} + #[test] #[cfg(feature = "write")] fn rtrim_char_test() { diff --git a/lexical-util/tests/num_tests.rs b/lexical-util/tests/num_tests.rs index b19807f..d35df55 100644 --- a/lexical-util/tests/num_tests.rs +++ b/lexical-util/tests/num_tests.rs @@ -197,6 +197,8 @@ fn check_float(mut x: T) { assert_eq!(T::from_bits(x.to_bits()), x); let _ = x.is_sign_positive(); let _ = x.is_sign_negative(); + let _ = x.ln(); + let _ = x.floor(); // Check properties let _ = x.to_bits() & T::SIGN_MASK; diff --git a/lexical-write-float/Cargo.toml b/lexical-write-float/Cargo.toml index 7e5768a..cdc0f3e 100644 --- a/lexical-write-float/Cargo.toml +++ b/lexical-write-float/Cargo.toml @@ -31,6 +31,7 @@ features = [] static_assertions = "1" [dev-dependencies] +approx = "0.5.0" quickcheck = "1.0.3" proptest = "0.10.1" fraction = "0.8.0" diff --git a/lexical-write-float/src/algorithm.rs b/lexical-write-float/src/algorithm.rs index dbd5bad..21d1d8b 100644 --- a/lexical-write-float/src/algorithm.rs +++ b/lexical-write-float/src/algorithm.rs @@ -1 +1,23 @@ +//! Implementation of the Dragonbox algorithm. + +#![cfg(not(feature = "compact"))] #![doc(hidden)] + +use crate::options::Options; +use lexical_util::num::Float; + +// TODO(ahuszagh) Implement... + +/// Optimized float-to-string algorithm for decimal strings. +/// # Safety +/// +/// Safe as long as the float isn't special (NaN or Infinity), and `bytes` +/// is large enough to hold the significant digits. +#[allow(unused)] // TODO(ahuszagh) Remove... +pub unsafe fn write_float( + float: F, + bytes: &mut [u8], + options: &Options, +) -> usize { + todo!(); +} diff --git a/lexical-write-float/src/api.rs b/lexical-write-float/src/api.rs new file mode 100644 index 0000000..23b1f13 --- /dev/null +++ b/lexical-write-float/src/api.rs @@ -0,0 +1,144 @@ +//! Implements the algorithm in terms of the lexical API. + +#![doc(hidden)] + +use crate::options::Options; +use crate::write::WriteFloat; +use lexical_util::format::{NumberFormat, STANDARD}; +use lexical_util::{to_lexical, to_lexical_with_options}; + +/// Check if a buffer is sufficiently large. +fn check_buffer(len: usize, options: &Options) -> bool { + let format = NumberFormat::<{ FORMAT }> {}; + + // At least 2 for the decimal point and sign. + let mut count: usize = 2; + + // First need to calculate maximum number of digits from leading or + // trailing zeros, IE, the exponent break. + if !format.no_exponent_notation() { + let min_exp = options.negative_exponent_break().map_or(-5, |x| x.get()); + let max_exp = options.positive_exponent_break().map_or(9, |x| x.get()); + let exp = min_exp.abs().max(max_exp) as usize; + if cfg!(feature = "power-of-two") && exp < 13 { + // 11 for the exponent digits in binary, 1 for the sign, 1 for the symbol + count += 13; + } else if exp < 5 { + // 3 for the exponent digits in decimal, 1 for the sign, 1 for the symbol + count += 5; + } else { + // More leading or trailing zeros than the exponent digits. + count += exp; + } + } else if cfg!(feature = "power-of-two") { + // Min is 2^-1075. + count += 1075; + } else { + // Min is 10^-324. + count += 324; + } + + // Now add the number of significant digits. + let radix = format.radix(); + let formatted_digits = if radix == 10 { + // Really should be 18, but add some extra to be cautious. + 28 + } else { + // BINARY: + // 53 significant mantissa bits for binary, add a few extra. + // RADIX: + // Our limit is `delta`. The maximum relative delta is 2.22e-16, + // around 1. If we have values below 1, our delta is smaller, but + // the max fraction is also a lot smaller. Above, and our fraction + // must be < 1.0, so our delta is less significant. Therefore, + // if our fraction is just less than 1, for a float near 2.0, + // we can do at **maximum** 33 digits (for base 3). Let's just + // assume it's a lot higher, and go with 64. + 64 + }; + let digits = if let Some(max_digits) = options.max_significant_digits() { + formatted_digits.min(max_digits.get()) + } else { + formatted_digits + }; + let digits = if let Some(min_digits) = options.min_significant_digits() { + digits.max(min_digits.get()) + } else { + formatted_digits + }; + count += digits; + + len > count +} + +// API + +const DEFAULT_OPTIONS: Options = Options::new(); + +// Implement ToLexical for numeric type. +macro_rules! float_to_lexical { + ($($t:tt $(, #[$meta:meta])? ; )*) => ($( + impl ToLexical for $t { + $(#[$meta:meta])? + unsafe fn to_lexical_unchecked<'a>(self, bytes: &'a mut [u8]) + -> &'a mut [u8] + { + debug_assert!(check_buffer::<{ STANDARD }>(bytes.len(), &DEFAULT_OPTIONS)); + // SAFETY: safe if `check_buffer::(bytes.len(), &options)`. + unsafe { + let len = self.write_float::<{ STANDARD }>(bytes, &DEFAULT_OPTIONS); + &mut index_unchecked_mut!(bytes[..len]) + } + } + + $(#[$meta:meta])? + fn to_lexical<'a>(self, bytes: &'a mut [u8]) + -> &'a mut [u8] + { + assert!(check_buffer::<{ STANDARD }>(bytes.len(), &DEFAULT_OPTIONS)); + // SAFETY: safe since `check_buffer::(bytes.len(), &options)`. + unsafe { self.to_lexical_unchecked(bytes) } + } + } + + impl ToLexicalWithOptions for $t { + type Options = Options; + + $(#[$meta:meta])? + unsafe fn to_lexical_with_options_unchecked<'a, const FORMAT: u128>( + self, + bytes: &'a mut [u8], + options: &Self::Options, + ) -> &'a mut [u8] + { + assert!(NumberFormat::<{ FORMAT }> {}.is_valid()); + debug_assert!(check_buffer::<{ FORMAT }>(bytes.len(), &options)); + // SAFETY: safe if `check_buffer::(bytes.len(), &options)`. + unsafe { + let len = self.write_float::<{ FORMAT }>(bytes, &options); + &mut index_unchecked_mut!(bytes[..len]) + } + } + + $(#[$meta:meta])? + fn to_lexical_with_options<'a, const FORMAT: u128>( + self, + bytes: &'a mut [u8], + options: &Self::Options, + ) -> &'a mut [u8] + { + assert!(NumberFormat::<{ FORMAT }> {}.is_valid()); + assert!(check_buffer::<{ FORMAT }>(bytes.len(), &options)); + // SAFETY: safe since `check_buffer::(bytes.len(), &options)`. + unsafe { self.to_lexical_with_options_unchecked::(bytes, options) } + } + } + )*) +} + +to_lexical! {} +to_lexical_with_options! {} +float_to_lexical! { + f32 ; + f64 ; +} diff --git a/lexical-write-float/src/binary.rs b/lexical-write-float/src/binary.rs index a0f9adb..2a965e8 100644 --- a/lexical-write-float/src/binary.rs +++ b/lexical-write-float/src/binary.rs @@ -601,7 +601,7 @@ where let zero_digits = fast_ceildiv(zero_bits, bits_per_digit) as usize; // Write our 0 digits. - // SAFETY: must be safe since since `bytes.len() < BUFFER_SIZE - 2`. + // SAFETY: safe if `bytes.len() > BUFFER_SIZE - 2`. unsafe { index_unchecked_mut!(bytes[0]) = b'0'; index_unchecked_mut!(bytes[1]) = decimal_point; @@ -691,7 +691,7 @@ where let mut cursor: usize; if leading_digits >= count { // We have more leading digits than digits we wrote: can write - // any additional digits, and then just write the remaining ones. + // any additional digits, and then just write the remaining zeros. // SAFETY: safe if the buffer is large enough to hold the significant digits. unsafe { let digits = &mut index_unchecked_mut!(bytes[count..leading_digits]); @@ -747,9 +747,8 @@ where /// Optimized float-to-string algorithm for power of 2 radixes. /// /// This assumes the float is: -/// 1). Non-zero -/// 2). Non-special (NaN or Infinite). -/// 3). Non-negative. +/// 1). Non-special (NaN or Infinite). +/// 2). Non-negative. /// /// # Safety /// @@ -776,6 +775,7 @@ where let format = NumberFormat::<{ FORMAT }> {}; assert!(format.is_valid()); debug_assert!(!float.is_special()); + debug_assert!(float >= F::ZERO); // Quickly calculate the number of bits we would have written. // This simulates writing the digits, so we can calculate the diff --git a/lexical-write-float/src/compact.rs b/lexical-write-float/src/compact.rs index 06dc071..840c8de 100644 --- a/lexical-write-float/src/compact.rs +++ b/lexical-write-float/src/compact.rs @@ -35,9 +35,9 @@ use lexical_write_integer::write::WriteInteger; /// with Integers", by Florian Loitsch, available online at: /// . /// -/// # Preconditions -/// -/// `float` must not be special (NaN or Infinity). +/// This assumes the float is: +/// 1). Non-special (NaN or Infinite). +/// 2). Non-negative. /// /// # Safety /// @@ -55,6 +55,7 @@ pub unsafe fn write_float( let format = NumberFormat::<{ FORMAT }> {}; assert!(format.is_valid()); debug_assert!(!float.is_special()); + debug_assert!(float >= F::ZERO); // Write our mantissa digits to a temporary buffer. let digits: mem::MaybeUninit<[u8; 32]> = mem::MaybeUninit::uninit(); diff --git a/lexical-write-float/src/hex.rs b/lexical-write-float/src/hex.rs index 2f4571b..8e26118 100644 --- a/lexical-write-float/src/hex.rs +++ b/lexical-write-float/src/hex.rs @@ -1,2 +1,23 @@ +//! Optimized float serializer for hexadecimal floats. + #![cfg(feature = "power-of-two")] #![doc(hidden)] + +use crate::options::Options; +use lexical_util::num::Float; + +// TODO(ahuszagh) Implement... + +/// Optimized float-to-string algorithm for decimal strings. +/// # Safety +/// +/// Safe as long as the float isn't special (NaN or Infinity), and `bytes` +/// is large enough to hold the significant digits. +#[allow(unused)] // TODO(ahuszagh) Remove... +pub unsafe fn write_float( + float: F, + bytes: &mut [u8], + options: &Options, +) -> usize { + todo!(); +} diff --git a/lexical-write-float/src/lib.rs b/lexical-write-float/src/lib.rs index 52ad055..2610c06 100644 --- a/lexical-write-float/src/lib.rs +++ b/lexical-write-float/src/lib.rs @@ -67,8 +67,11 @@ pub mod hex; pub mod options; pub mod radix; +mod api; +mod write; + // Re-exports -//pub use self::api::{ToLexical, ToLexicalWithOptions}; +pub use self::api::{ToLexical, ToLexicalWithOptions}; pub use self::options::{Options, OptionsBuilder}; pub use lexical_util::constants::{FormattedSize, BUFFER_SIZE}; pub use lexical_util::format::{NumberFormatBuilder, STANDARD}; diff --git a/lexical-write-float/src/radix.rs b/lexical-write-float/src/radix.rs index 77f5b09..96e5c3c 100644 --- a/lexical-write-float/src/radix.rs +++ b/lexical-write-float/src/radix.rs @@ -1,2 +1,553 @@ +//! Adaptation of the V8 ftoa algorithm with a custom radix. +//! +//! This algorithm is adapted from the V8 codebase, +//! and may be found [here](https://github.com/v8/v8). +//! +//! # Unsupported Features +//! +//! This does not support a few features from the format packed struct, +//! most notably, it will never write numbers in scientific notation. +//! Scientific notation must be disabled. + #![cfg(feature = "radix")] #![doc(hidden)] + +use crate::options::{Options, RoundMode}; +use core::mem; +use lexical_util::algorithm::{ltrim_char_count, rtrim_char_count}; +use lexical_util::constants::{FormattedSize, BUFFER_SIZE}; +use lexical_util::digit::{char_to_digit_const, digit_to_char_const}; +use lexical_util::format::NumberFormat; +use lexical_util::num::Float; +use lexical_write_integer::write::WriteInteger; + +// ALGORITHM +// --------- + +/// Naive float-to-string algorithm for generic radixes. +/// +/// This assumes the float is: +/// 1). Non-special (NaN or Infinite). +/// 2). Non-negative. +/// +/// # Safety +/// +/// Safe as long as `bytes` is large enough to hold the number of +/// significant digits, any (optional) leading or trailing zeros, +/// and the scientific exponent. +/// +/// # Panics +/// +/// Panics if exponent notation is used. +pub unsafe fn write_float( + float: F, + bytes: &mut [u8], + options: &Options, +) -> usize +where + ::Unsigned: WriteInteger + FormattedSize, +{ + // PRECONDITIONS + + // Assert no special cases remain, no negative numbers, and a valid format. + let format = NumberFormat::<{ FORMAT }> {}; + assert!(format.is_valid()); + debug_assert!(!float.is_special()); + debug_assert!(float >= F::ZERO); + debug_assert!(F::BITS <= 64); + + // Validate our options: we don't support different exponent bases here. + debug_assert!(format.mantissa_radix() == format.exponent_base()); + + // Temporary buffer for the result. We start with the decimal point in the + // middle and write to the left for the integer part and to the right for the + // fractional part. 1024 characters for the exponent and 52 for the mantissa + // either way, with additional space for sign, decimal point and string + // termination should be sufficient. + const SIZE: usize = 2200; + let buffer: mem::MaybeUninit<[u8; SIZE]> = mem::MaybeUninit::uninit(); + // SAFETY: safe, since we never read bytes that weren't written. + let mut buffer = unsafe { buffer.assume_init() }; + //let buffer = buffer.as_mut_ptr(); + let initial_cursor: usize = SIZE / 2; + let mut integer_cursor = initial_cursor; + let mut fraction_cursor = initial_cursor; + let base = F::as_cast(format.radix()); + + // Split the float into an integer part and a fractional part. + let mut integer = float.floor(); + let mut fraction = float - integer; + + // We only compute fractional digits up to the input double's precision. + let mut delta = F::as_cast(0.5) * (float.next_positive() - float); + delta = F::ZERO.next_positive().max_finite(delta); + debug_assert!(delta > F::ZERO); + + // Write our fraction digits. + // SAFETY: we have 1100 digits, which is enough for any float f64 or smaller. + if fraction > delta { + loop { + // Shift up by one digit. + fraction *= base; + delta *= base; + // Write digit. + let digit = fraction.as_u32(); + let c = digit_to_char_const(digit, format.radix()); + unsafe { index_unchecked_mut!(buffer[fraction_cursor]) = c }; + fraction_cursor += 1; + // Calculate remainder. + fraction -= F::as_cast(digit); + // Round to even. + if fraction > F::as_cast(0.5) || (fraction == F::as_cast(0.5) && (digit & 1) != 0) { + if fraction + delta > F::ONE { + // We need to back trace already written digits in case of carry-over. + loop { + fraction_cursor -= 1; + if fraction_cursor == initial_cursor - 1 { + // Carry over to the integer part. + integer += F::ONE; + break; + } + // Reconstruct digit. + let c = unsafe { index_unchecked!(buffer[fraction_cursor]) }; + if let Some(digit) = char_to_digit_const(c, format.radix()) { + let idx = digit + 1; + let c = digit_to_char_const(idx, format.radix()); + unsafe { index_unchecked_mut!(buffer[fraction_cursor]) = c }; + fraction_cursor += 1; + break; + } + } + break; + } + } + + if delta >= fraction { + break; + } + } + } + + // Compute integer digits. Fill unrepresented digits with zero. + // SAFETY: we have 1100 digits, which is enough for any float f64 or smaller. + // We do this first, so we can do extended precision control later. + while (integer / base).exponent() > 0 { + integer /= base; + integer_cursor -= 1; + unsafe { index_unchecked_mut!(buffer[integer_cursor]) = b'0' }; + } + + loop { + let remainder = integer % base; + integer_cursor -= 1; + let idx = remainder.as_u32(); + let c = digit_to_char_const(idx, format.radix()); + unsafe { index_unchecked_mut!(buffer[integer_cursor]) = c }; + integer = (integer - remainder) / base; + + if integer <= F::ZERO { + break; + } + } + + // Write our exponent. + let sci_exp = if float == F::ZERO { + 0 + } else { + naive_exponent(float, format.radix()) + }; + let min_exp = options.negative_exponent_break().map_or(-5, |x| x.get()); + let max_exp = options.positive_exponent_break().map_or(9, |x| x.get()); + if !format.no_exponent_notation() + && (format.required_exponent_notation() || sci_exp < min_exp || sci_exp > max_exp) + { + // Write digits in scientific notation. + unsafe { + write_float_scientific::( + sci_exp, + &mut buffer, + bytes, + initial_cursor, + integer_cursor, + fraction_cursor, + options, + ) + } + } else { + // Write digits without scientific notation. + unsafe { + write_float_nonscientific::( + &mut buffer, + bytes, + initial_cursor, + integer_cursor, + fraction_cursor, + options, + ) + } + } +} + +// Store the first digit and up to `BUFFER_SIZE - 20` digits +// that occur from left-to-right in the decimal representation. +// For example, for the number 123.45, store the first digit `1` +// and `2345` as the remaining values. Then, decide on-the-fly +// if we need scientific or regular formatting. +// +// BUFFER_SIZE +// - 1 # first digit +// - 1 # period +// - 1 # +/- sign +// - 2 # e and +/- sign +// - 9 # max exp is 308, in radix2 is 9 +// - 1 # null terminator +// = 15 characters of formatting required +// Just pad it a bit, we don't want memory corruption. +const MAX_NONDIGIT_LENGTH: usize = 25; +const MAX_DIGIT_LENGTH: usize = BUFFER_SIZE - MAX_NONDIGIT_LENGTH; + +/// Round-up the last digit. +/// +/// # Safety +/// +/// Safe as long as `count <= digits.len()`. +pub unsafe fn round_up(digits: &mut [u8], count: usize, radix: u32) -> usize { + debug_assert!(count <= digits.len()); + + let mut index = count; + let max_digit = digit_to_char_const(radix - 1, radix); + while index != 0 { + // SAFETY: safe if `count <= digits.len()`, since then + // `index > 0 && index <= digits.len()`. + let c = unsafe { index_unchecked!(digits[index - 1]) }; + if c < max_digit { + // SAFETY: safe since `index > 0 && index <= digits.len()`. + unsafe { index_unchecked_mut!(digits[index - 1]) = c + 1 }; + return index; + } + // Don't have to assign b'0' otherwise, since we're just carrying + // to the next digit. + index -= 1; + } + + // Means all digits were b'9': we need to round up. + // SAFETY: safe since `digits.len() > 1`. + unsafe { index_unchecked_mut!(digits[0]) = b'1' }; + + 1 +} + +/// Round mantissa to the nearest value, returning only the number +/// of significant digits. Also returns the number of bits of the mantissa. +#[inline] +pub unsafe fn truncate_and_round( + buffer: &mut [u8], + start: usize, + end: usize, + radix: u32, + options: &Options, +) -> usize { + // Get the number of max digits, and then calculate if we need to round. + let count = end - start; + let max_digits = if let Some(digits) = options.max_significant_digits() { + digits.get() + } else { + return count; + }; + + if max_digits >= count { + return count; + } + if options.round_mode() == RoundMode::Truncate { + // Don't round input, just shorten number of digits emitted. + return max_digits; + } + + // Need to add the number of leading zeros to the digits count. + let max_digits = { + let digits = unsafe { &mut index_unchecked_mut!(buffer[start..start + max_digits]) }; + max_digits + ltrim_char_count(digits, b'0') + }; + + // We need to round-nearest, tie-even, so we need to handle + // the truncation **here**. If the representation is above + // halfway at all, we need to round up, even if 1 bit. + // SAFETY: safe since `max_digits < count`, and `max_digits > 0`. + let last = unsafe { index_unchecked!(buffer[start + max_digits - 1]) }; + let first = unsafe { index_unchecked!(buffer[start + max_digits]) }; + let halfway = digit_to_char_const(radix / 2, radix); + let rem = radix % 2; + if first < halfway { + // Just truncate, going to round-down anyway. + max_digits + } else if first > halfway { + // Round-up always. + // SAFETY: safe if `start <= end, because `max_digits < count`. + let digits = unsafe { &mut index_unchecked_mut!(buffer[start..start + max_digits]) }; + unsafe { round_up(digits, max_digits, radix) } + } else if rem == 0 { + // Even radix, our halfway point `$c00000.....`. + // SAFETY: safe if `start <= end, because `max_digits < count`. + let truncated = unsafe { &index_unchecked!(buffer[start + max_digits + 1..end]) }; + if truncated.iter().all(|&x| x == b'0') && last & 1 == 0 { + // At an exact halfway point, and even, round-down. + max_digits + } else { + // Above halfway or at halfway and even, round-up + // SAFETY: safe if `count <= digits.len()`, because `max_digits < count`. + let digits = unsafe { &mut index_unchecked_mut!(buffer[start..start + max_digits]) }; + unsafe { round_up(digits, max_digits, radix) } + } + } else { + // Odd radix, our halfway point is `$c$c$c$c$c$c....`. Cannot halfway points. + // SAFETY: safe if `start <= end, because `max_digits < count`. + let truncated = unsafe { &index_unchecked!(buffer[start + max_digits + 1..end]) }; + for &c in truncated.iter() { + if c < halfway { + return max_digits; + } else if c > halfway { + // Above halfway + // SAFETY: safe if `count <= digits.len()`, because `max_digits < count`. + let digits = + unsafe { &mut index_unchecked_mut!(buffer[start..start + max_digits]) }; + return unsafe { round_up(digits, max_digits, radix) }; + } + } + return max_digits; + } +} + +/// Write float to string in scientific notation. +/// +/// # Safety +/// +/// Safe as long as `bytes` is large enough to hold the number of digits +/// and the scientific notation's exponent digits. +/// +/// # Preconditions +/// +/// The mantissa must be truncated and rounded, prior to calling this, +/// based on the number of maximum digits. In addition, `exponent_base` +/// and `mantissa_radix` in `FORMAT` must be identical. +#[inline] +pub unsafe fn write_float_scientific( + sci_exp: i32, + buffer: &mut [u8], + bytes: &mut [u8], + initial_cursor: usize, + integer_cursor: usize, + fraction_cursor: usize, + options: &Options, +) -> usize { + // PRECONDITIONS + debug_assert!(bytes.len() >= BUFFER_SIZE); + + // Config options. + let format = NumberFormat::<{ FORMAT }> {}; + assert!(format.is_valid()); + let decimal_point = format.decimal_point(); + let exponent_character = format.exponent(); + + // Round and truncate the number of significant digits. + let start: usize; + let end: usize; + if sci_exp <= 0 { + start = ((initial_cursor as i32) - sci_exp - 1) as usize; + end = fraction_cursor.min(start + MAX_DIGIT_LENGTH + 1); + } else { + start = integer_cursor; + end = fraction_cursor.min(start + MAX_DIGIT_LENGTH + 1); + } + // SAFETY: safe since `start + count <= end && end <= buffer.len()`. + let count = unsafe { truncate_and_round(buffer, start, end, format.radix(), options) }; + // SAFETY: safe since `start + count <= end`. + let digits = unsafe { &index_unchecked!(buffer[start..start + count]) }; + + // Non-exponent portion. + // Get as many digits as possible, up to `MAX_DIGIT_LENGTH+1` + // since we are ignoring the digit for the first digit, + // or the number of written digits. + // SAFETY: safe since the buffer must be larger than `M::FORMATTED_SIZE`. + let count = unsafe { + index_unchecked_mut!(bytes[0] = digits[0]); + index_unchecked_mut!(bytes[1]) = decimal_point; + let src = digits.as_ptr().add(1); + let dst = &mut index_unchecked_mut!(bytes[2..count + 1]); + copy_nonoverlapping_unchecked!(dst, src, count - 1); + let zeros = rtrim_char_count(&index_unchecked!(bytes[2..count + 1]), b'0'); + count - zeros + }; + // Extra 1 since we have the decimal point. + let mut cursor = count + 1; + + // Determine if we need to add more trailing zeros. + let mut exact_count: usize = count; + if let Some(min_digits) = options.min_significant_digits() { + exact_count = min_digits.get().max(count); + } + + // Write any trailing digits to the output. + // SAFETY: bytes cannot be empty. + if !format.no_exponent_without_fraction() && cursor == 2 && options.trim_floats() { + // Need to trim floats from trailing zeros, and we have only a decimal. + cursor -= 1; + } else if exact_count < 2 { + // Need to have at least 1 digit, the trailing `.0`. + unsafe { index_unchecked_mut!(bytes[cursor]) = b'0' }; + cursor += 1; + } else if exact_count > count { + // NOTE: Neither `exact_count >= count >= 2`. + // We need to write `exact_count - (cursor - 1)` digits, since + // cursor includes the decimal point. + let digits_end = exact_count + 1; + // SAFETY: this is safe as long as the buffer was large enough + // to hold `min_significant_digits + 1`. + unsafe { + slice_fill_unchecked!(&mut index_unchecked_mut!(bytes[cursor..digits_end]), b'0'); + } + cursor = digits_end; + } + + // Now, write our scientific notation. + unsafe { index_unchecked_mut!(bytes[cursor]) = exponent_character }; + cursor += 1; + + // We've handled the zero case: write the sign for the exponent. + let positive_exp: u32; + if sci_exp < 0 { + unsafe { index_unchecked_mut!(bytes[cursor]) = b'-' }; + cursor += 1; + positive_exp = sci_exp.wrapping_neg() as u32; + } else if cfg!(feature = "format") && format.required_exponent_sign() { + unsafe { index_unchecked_mut!(bytes[cursor]) = b'+' }; + cursor += 1; + positive_exp = sci_exp as u32; + } else { + positive_exp = sci_exp as u32; + } + + // Write our exponent digits. + // SAFETY: safe since bytes must be large enough to store all digits. + cursor += unsafe { + positive_exp.write_exponent::(&mut index_unchecked_mut!(bytes[cursor..])) + }; + + cursor +} + +/// Write float to string without scientific notation. +/// +/// # Safety +/// +/// Safe as long as `bytes` is large enough to hold the number of +/// significant digits and the leading zeros. +#[inline] +pub unsafe fn write_float_nonscientific( + buffer: &mut [u8], + bytes: &mut [u8], + initial_cursor: usize, + integer_cursor: usize, + fraction_cursor: usize, + options: &Options, +) -> usize { + // PRECONDITIONS + debug_assert!(bytes.len() >= BUFFER_SIZE); + + // Config options. + let format = NumberFormat::<{ FORMAT }> {}; + assert!(format.is_valid()); + let decimal_point = format.decimal_point(); + + // Round and truncate the number of significant digits. + let start = integer_cursor; + let end = fraction_cursor.min(start + MAX_DIGIT_LENGTH + 1); + // SAFETY: safe since `start + count <= end && end <= buffer.len()`. + let mut count = unsafe { truncate_and_round(buffer, start, end, format.radix(), options) }; + // SAFETY: safe since `start + count <= end`. + let digits = unsafe { &index_unchecked!(buffer[start..start + count]) }; + + // Write the integer component. + let integer_length = initial_cursor - start; + let integer_count = count.min(integer_length); + // SAFETY: safe if the buffer is large enough to hold the significant digits. + unsafe { + let src = digits.as_ptr(); + let dst = &mut index_unchecked_mut!(bytes[..integer_count]); + copy_nonoverlapping_unchecked!(dst, src, integer_count); + } + if integer_count < integer_length { + // We have more leading digits than digits we wrote: can write + // any additional digits, and then just write the remaining zeros. + // SAFETY: safe if the buffer is large enough to hold the significant digits. + unsafe { + let digits = &mut index_unchecked_mut!(bytes[integer_count..integer_length]); + slice_fill_unchecked!(digits, b'0'); + } + } + let mut cursor = integer_length; + + // SAFETY: safe if the buffer is large enough to hold the significant digits. + unsafe { index_unchecked_mut!(bytes[cursor]) = decimal_point }; + cursor += 1; + + // Write the fraction component. + let digits = unsafe { &index_unchecked!(digits[integer_count..]) }; + let fraction_count = count.saturating_sub(integer_length); + if fraction_count > 0 { + // Need to write additional fraction digits. + // SAFETY: safe if the buffer is large enough to hold the significant digits. + unsafe { + let src = digits.as_ptr(); + let dst = &mut index_unchecked_mut!(bytes[cursor..cursor + fraction_count]); + copy_nonoverlapping_unchecked!(dst, src, fraction_count); + let zeros = rtrim_char_count(&index_unchecked!(bytes[cursor..cursor + count]), b'0'); + cursor += fraction_count - zeros; + } + } else if options.trim_floats() { + // Remove the decimal point, went too far. + cursor -= 1; + } else { + unsafe { index_unchecked_mut!(bytes[cursor]) = b'0' }; + cursor += 1; + count += 1; + } + + // Determine if we need to add more trailing zeros. + let mut exact_count: usize = count; + if let Some(min_digits) = options.min_significant_digits() { + exact_count = min_digits.get().max(count); + } + + // Write any trailing digits to the output. + // SAFETY: bytes cannot be empty. + if (fraction_count > 0 || !options.trim_floats()) && exact_count > count { + // NOTE: Neither `exact_count >= count >= 2`. + // We need to write `exact_count - (cursor - 1)` digits, since + // cursor includes the decimal point. + let digits_end = cursor + exact_count - count; + // SAFETY: this is safe as long as the buffer was large enough + // to hold `min_significant_digits + 1`. + unsafe { + slice_fill_unchecked!(&mut index_unchecked_mut!(bytes[cursor..digits_end]), b'0'); + } + cursor = digits_end; + } + + cursor +} + +// MATH +// ---- + +/// Calculate the naive exponent from a minimal value. +/// +/// Don't export this for float, since it's specialized for radix. +#[inline] +fn naive_exponent(float: F, radix: u32) -> i32 { + // floor returns the minimal value, which is our + // desired exponent + // ln(1.1e-5) -> -4.95 -> -5 + // ln(1.1e5) -> -5.04 -> 5 + debug_assert!(float != F::ZERO); + (float.ln() / F::as_cast(radix).ln()).floor().as_i32() +} diff --git a/lexical-write-float/src/write.rs b/lexical-write-float/src/write.rs new file mode 100644 index 0000000..2a99930 --- /dev/null +++ b/lexical-write-float/src/write.rs @@ -0,0 +1,152 @@ +//! Shared trait and methods for writing floats. + +#![doc(hidden)] + +#[cfg(not(feature = "compact"))] +use crate::algorithm::write_float as write_float_decimal; +#[cfg(feature = "power-of-two")] +use crate::binary; +/// Select the back-end. +#[cfg(feature = "compact")] +use crate::compact::write_float as write_float_decimal; +#[cfg(feature = "power-of-two")] +use crate::hex; +#[cfg(feature = "radix")] +use crate::radix; + +use crate::options::Options; +use lexical_util::constants::FormattedSize; +use lexical_util::format::NumberFormat; +use lexical_util::num::Float; +use lexical_write_integer::write::WriteInteger; + +/// Write float trait. +pub trait WriteFloat: Float { + /// Forward write integer parameters to an unoptimized backend. + /// + /// # Safety + /// + /// Safe as long as the buffer can hold [`FORMATTED_SIZE`] elements + /// (or [`FORMATTED_SIZE_DECIMAL`] for decimal). If using custom digit + /// precision control (such as specifying a minimum number of significant + /// digits), or disabling scientific notation, then more digits may be + /// required (up to `1075` for the leading or trailing zeros, `1` for + /// the sign and `1` for the decimal point). So, + /// `1077 + min_significant_digits.max(52)`, so ~1200 for a reasonable + /// threshold. + /// + /// # Panics + /// + /// Panics if the number format is invalid, or if scientific notation + /// is used and the exponent base does not equal the mantissa radix + /// and the format is not a hexadecimal float. + /// + /// [`FORMATTED_SIZE`]: lexical_util::constants::FormattedSize::FORMATTED_SIZE + /// [`FORMATTED_SIZE_DECIMAL`]: lexical_util::constants::FormattedSize::FORMATTED_SIZE_DECIMAL + #[inline] + unsafe fn write_float(self, bytes: &mut [u8], options: &Options) -> usize + where + Self::Unsigned: FormattedSize + WriteInteger, + { + // Validate our format options. + let format = NumberFormat:: {}; + assert!(format.is_valid()); + // Avoid any false assumptions for 128-bit floats. + assert!(Self::BITS <= 64); + + #[cfg(feature = "power-of-two")] + { + let radix = format.radix(); + let exponent_base = format.exponent_base(); + if radix != exponent_base { + assert!(radix == 16); + assert!(exponent_base == 2); + assert!(format.exponent_radix() == 10); + } + } + + let (float, count, bytes) = if self < Self::ZERO { + // SAFETY: safe if `bytes.len() > 1`. + unsafe { index_unchecked_mut!(bytes[0]) = b'-' }; + (-self, 1, unsafe { &mut index_unchecked_mut!(bytes[1..]) }) + } else if cfg!(feature = "format") && format.required_mantissa_sign() { + // SAFETY: safe if `bytes.len() > 1`. + unsafe { index_unchecked_mut!(bytes[0]) = b'+' }; + (self, 1, unsafe { &mut index_unchecked_mut!(bytes[1..]) }) + } else { + (self, 0, bytes) + }; + + // Handle special values. + if !self.is_special() { + #[cfg(all(feature = "power-of-two", not(feature = "radix")))] + { + // SAFETY: safe if the buffer can hold the significant digits + let radix = format.radix(); + let exponent_base = format.exponent_base(); + let exponent_radix = format.exponent_radix(); + count + + if radix == 10 { + unsafe { write_float_decimal::<_, FORMAT>(float, bytes, options) } + } else if radix == 16 && exponent_base == 2 && exponent_radix == 10 { + unsafe { hex::write_float::<_, FORMAT>(float, bytes, options) } + } else { + unsafe { binary::write_float::<_, FORMAT>(float, bytes, options) } + } + } + + #[cfg(feature = "radix")] + { + // SAFETY: safe if the buffer can hold the significant digits + let radix = format.radix(); + let exponent_base = format.exponent_base(); + let exponent_radix = format.exponent_radix(); + count + + if radix == 10 { + unsafe { write_float_decimal::<_, FORMAT>(float, bytes, options) } + } else if radix == 16 && exponent_base == 2 && exponent_radix == 10 { + unsafe { hex::write_float::<_, FORMAT>(float, bytes, options) } + } else if matches!(radix, 2 | 4 | 8 | 16 | 32) { + unsafe { binary::write_float::<_, FORMAT>(float, bytes, options) } + } else { + unsafe { radix::write_float::<_, FORMAT>(float, bytes, options) } + } + } + + #[cfg(not(feature = "radix"))] + { + // SAFETY: safe if the buffer can hold the significant digits + count + unsafe { write_float_decimal::<_, FORMAT>(float, bytes, options) } + } + } else if self.is_nan() { + // SAFETY: safe is the buffer is longer than the NaN string. + // The NaN string must be <= 50 characters. + let length = options.nan_string().len(); + unsafe { + let src = options.nan_string().as_ptr(); + let dst = &mut index_unchecked_mut!(bytes[..length]); + copy_nonoverlapping_unchecked!(dst, src, length); + } + count + length + } else { + // is_inf + // SAFETY: safe is the buffer is longer than the Inf string. + // The Inf string must be <= 50 characters. + let length = options.inf_string().len(); + unsafe { + let src = options.inf_string().as_ptr(); + let dst = &mut index_unchecked_mut!(bytes[..length]); + copy_nonoverlapping_unchecked!(dst, src, length); + } + count + length + } + } +} + +macro_rules! write_float_impl { + ($($t:ty)*) => ($( + impl WriteFloat for $t {} + )*) +} + +write_float_impl! { f32 f64 } diff --git a/lexical-write-float/tests/api_tests.rs b/lexical-write-float/tests/api_tests.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lexical-write-float/tests/api_tests.rs @@ -0,0 +1 @@ + diff --git a/lexical-write-float/tests/binary_tests.rs b/lexical-write-float/tests/binary_tests.rs index 77fe47a..d62b160 100644 --- a/lexical-write-float/tests/binary_tests.rs +++ b/lexical-write-float/tests/binary_tests.rs @@ -1,14 +1,15 @@ #![cfg(feature = "power-of-two")] +mod parse_radix; + use core::num; -use fraction::{BigFraction, ToPrimitive}; use lexical_util::constants::{FormattedSize, BUFFER_SIZE}; use lexical_util::format::NumberFormatBuilder; use lexical_util::num::{Float, Integer}; -use lexical_util::step::u64_step; use lexical_write_float::options::RoundMode; use lexical_write_float::{binary, Options}; use lexical_write_integer::write::WriteInteger; +use parse_radix::{parse_f32, parse_f64}; use proptest::prelude::*; use quickcheck::quickcheck; @@ -948,97 +949,16 @@ fn write_float_test() { write_float::<_, BINARY>(23.45678901234567890f64, &truncate, "10110.0"); } -// NOTE: -// These are extremely naive, inefficient binary parsers based off -// of the documentation in `binary.rs`. See the Python code there -// for a more legible example. - -macro_rules! parse_float { - ($name:ident, $t:ident, $cb:ident) => { - fn $name(string: &[u8], radix: u32) -> $t { - let index = string.iter().position(|&x| x == b'.').unwrap(); - let integer = &string[..index]; - let rest = &string[index + 1..]; - let fraction: &[u8]; - let exponent: i32; - if let Some(index) = rest.iter().position(|&x| x == b'e') { - fraction = &rest[..index]; - let exp_digits = unsafe { std::str::from_utf8_unchecked(&rest[index + 1..]) }; - exponent = i32::from_str_radix(exp_digits, radix).unwrap(); - } else { - fraction = rest; - exponent = 0; - } - - // Now need to reconstruct our integer. - let step = u64_step(radix); - let pow = BigFraction::new((radix as u128).pow(step as u32), 1u64); - let mut fint = BigFraction::new(0u64, 1u64); - let mut index = 0; - while index < integer.len() { - let count = step.min(integer.len() - index); - let end = index + count; - let digits = unsafe { std::str::from_utf8_unchecked(&integer[index..end]) }; - let tmp = u64::from_str_radix(digits, radix).unwrap(); - fint *= pow.clone(); - fint += BigFraction::new(tmp, 1u64); - index = end; - } - - // Scale it to the exponent. - // Note that these should always be exact, since we can hold - // all powers-of-two exactly. - if exponent >= 0 { - fint *= BigFraction::from((radix as f64).powi(exponent)); - } else { - fint /= BigFraction::from((radix as f64).powi(-exponent)); - } - - // Now need to reconstruct our fraction. - let mut ffrac = BigFraction::new(0u64, 1u64); - let mut index = 0; - while index < fraction.len() { - let count = step.min(fraction.len() - index); - let end = index + count; - let digits = unsafe { std::str::from_utf8_unchecked(&fraction[index..end]) }; - let tmp = u64::from_str_radix(digits, radix).unwrap(); - ffrac *= pow.clone(); - ffrac += BigFraction::new(tmp, 1u64); - index = end; - } - - let exp_shift = fraction.len() as i32 - exponent; - let ffrac_exp_num; - let ffrac_exp_den; - if exp_shift > 0 { - ffrac_exp_num = 0; - ffrac_exp_den = exp_shift; - } else { - ffrac_exp_num = -exp_shift; - ffrac_exp_den = 0; - } - - ffrac *= BigFraction::from((radix as f64).powi(ffrac_exp_num)); - ffrac /= BigFraction::from((radix as f64).powi(ffrac_exp_den)); - - (fint + ffrac).$cb().unwrap() - } - }; -} - -parse_float!(parse_f32, f32, to_f32); -parse_float!(parse_f64, f64, to_f64); - quickcheck! { fn f32_binary_quickcheck(f: f32) -> bool { let mut buffer = [b'\x00'; BUFFER_SIZE]; let options = Options::builder().build().unwrap(); - let f = f.abs(); if f.is_special() { true } else { + let f = f.abs(); let count = unsafe { binary::write_float::<_, BINARY>(f, &mut buffer, &options) }; - let roundtrip = parse_f32(&buffer[..count], 2); + let roundtrip = parse_f32(&buffer[..count], 2, b'e'); roundtrip == f } } @@ -1046,12 +966,12 @@ quickcheck! { fn f32_octal_quickcheck(f: f32) -> bool { let mut buffer = [b'\x00'; BUFFER_SIZE]; let options = Options::builder().build().unwrap(); - let f = f.abs(); if f.is_special() { true } else { + let f = f.abs(); let count = unsafe { binary::write_float::<_, OCTAL>(f, &mut buffer, &options) }; - let roundtrip = parse_f32(&buffer[..count], 8); + let roundtrip = parse_f32(&buffer[..count], 8, b'e'); roundtrip == f } } @@ -1059,12 +979,12 @@ quickcheck! { fn f64_binary_quickcheck(f: f64) -> bool { let mut buffer = [b'\x00'; BUFFER_SIZE]; let options = Options::builder().build().unwrap(); - let f = f.abs(); if f.is_special() { true } else { + let f = f.abs(); let count = unsafe { binary::write_float::<_, BINARY>(f, &mut buffer, &options) }; - let roundtrip = parse_f64(&buffer[..count], 2); + let roundtrip = parse_f64(&buffer[..count], 2, b'e'); roundtrip == f } } @@ -1072,12 +992,12 @@ quickcheck! { fn f64_octal_quickcheck(f: f64) -> bool { let mut buffer = [b'\x00'; BUFFER_SIZE]; let options = Options::builder().build().unwrap(); - let f = f.abs(); if f.is_special() { true } else { + let f = f.abs(); let count = unsafe { binary::write_float::<_, OCTAL>(f, &mut buffer, &options) }; - let roundtrip = parse_f64(&buffer[..count], 8); + let roundtrip = parse_f64(&buffer[..count], 8, b'e'); roundtrip == f } } @@ -1088,10 +1008,10 @@ proptest! { fn f32_binary_proptest(f in f32::MIN..f32::MAX) { let mut buffer = [b'\x00'; BUFFER_SIZE]; let options = Options::builder().build().unwrap(); - let f = f.abs(); if !f.is_special() { + let f = f.abs(); let count = unsafe { binary::write_float::<_, BINARY>(f, &mut buffer, &options) }; - let roundtrip = parse_f32(&buffer[..count], 2); + let roundtrip = parse_f32(&buffer[..count], 2, b'e'); prop_assert_eq!(roundtrip, f) } } @@ -1100,10 +1020,10 @@ proptest! { fn f32_octal_proptest(f in f32::MIN..f32::MAX) { let mut buffer = [b'\x00'; BUFFER_SIZE]; let options = Options::builder().build().unwrap(); - let f = f.abs(); if !f.is_special() { + let f = f.abs(); let count = unsafe { binary::write_float::<_, OCTAL>(f, &mut buffer, &options) }; - let roundtrip = parse_f32(&buffer[..count], 8); + let roundtrip = parse_f32(&buffer[..count], 8, b'e'); prop_assert_eq!(roundtrip, f) } } @@ -1112,10 +1032,10 @@ proptest! { fn f64_binary_proptest(f in f64::MIN..f64::MAX) { let mut buffer = [b'\x00'; BUFFER_SIZE]; let options = Options::builder().build().unwrap(); - let f = f.abs(); if !f.is_special() { + let f = f.abs(); let count = unsafe { binary::write_float::<_, BINARY>(f, &mut buffer, &options) }; - let roundtrip = parse_f64(&buffer[..count], 2); + let roundtrip = parse_f64(&buffer[..count], 2, b'e'); prop_assert_eq!(roundtrip, f) } } @@ -1124,10 +1044,10 @@ proptest! { fn f64_octal_proptest(f in f64::MIN..f64::MAX) { let mut buffer = [b'\x00'; BUFFER_SIZE]; let options = Options::builder().build().unwrap(); - let f = f.abs(); if !f.is_special() { + let f = f.abs(); let count = unsafe { binary::write_float::<_, OCTAL>(f, &mut buffer, &options) }; - let roundtrip = parse_f64(&buffer[..count], 8); + let roundtrip = parse_f64(&buffer[..count], 8, b'e'); prop_assert_eq!(roundtrip, f) } } diff --git a/lexical-write-float/tests/hex_tests.rs b/lexical-write-float/tests/hex_tests.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lexical-write-float/tests/hex_tests.rs @@ -0,0 +1 @@ + diff --git a/lexical-write-float/tests/parse_radix.rs b/lexical-write-float/tests/parse_radix.rs new file mode 100644 index 0000000..56f82d4 --- /dev/null +++ b/lexical-write-float/tests/parse_radix.rs @@ -0,0 +1,85 @@ +#![cfg(feature = "power-of-two")] + +use fraction::{BigFraction, ToPrimitive}; +use lexical_util::step::u64_step; + +// NOTE: +// These are extremely naive, inefficient binary parsers based off +// of the documentation in `binary.rs`. See the Python code there +// for a more legible example. + +macro_rules! parse_float { + ($name:ident, $t:ident, $cb:ident) => { + pub fn $name(string: &[u8], radix: u32, exp: u8) -> $t { + let index = string.iter().position(|&x| x == b'.').unwrap(); + let integer = &string[..index]; + let rest = &string[index + 1..]; + let fraction: &[u8]; + let exponent: i32; + if let Some(index) = rest.iter().position(|&x| x == exp) { + fraction = &rest[..index]; + let exp_digits = unsafe { std::str::from_utf8_unchecked(&rest[index + 1..]) }; + exponent = i32::from_str_radix(exp_digits, radix).unwrap(); + } else { + fraction = rest; + exponent = 0; + } + + // Now need to reconstruct our integer. + let step = u64_step(radix); + let pow = BigFraction::new((radix as u128).pow(step as u32), 1u64); + let mut fint = BigFraction::new(0u64, 1u64); + let mut index = 0; + while index < integer.len() { + let count = step.min(integer.len() - index); + let end = index + count; + let digits = unsafe { std::str::from_utf8_unchecked(&integer[index..end]) }; + let tmp = u64::from_str_radix(digits, radix).unwrap(); + fint *= pow.clone(); + fint += BigFraction::new(tmp, 1u64); + index = end; + } + + // Scale it to the exponent. + // Note that these should always be exact, since we can hold + // all powers-of-two exactly. + if exponent >= 0 { + fint *= BigFraction::from((radix as f64).powi(exponent)); + } else { + fint /= BigFraction::from((radix as f64).powi(-exponent)); + } + + // Now need to reconstruct our fraction. + let mut ffrac = BigFraction::new(0u64, 1u64); + let mut index = 0; + while index < fraction.len() { + let count = step.min(fraction.len() - index); + let end = index + count; + let digits = unsafe { std::str::from_utf8_unchecked(&fraction[index..end]) }; + let tmp = u64::from_str_radix(digits, radix).unwrap(); + ffrac *= pow.clone(); + ffrac += BigFraction::new(tmp, 1u64); + index = end; + } + + let exp_shift = fraction.len() as i32 - exponent; + let ffrac_exp_num; + let ffrac_exp_den; + if exp_shift > 0 { + ffrac_exp_num = 0; + ffrac_exp_den = exp_shift; + } else { + ffrac_exp_num = -exp_shift; + ffrac_exp_den = 0; + } + + ffrac *= BigFraction::from((radix as f64).powi(ffrac_exp_num)); + ffrac /= BigFraction::from((radix as f64).powi(ffrac_exp_den)); + + (fint + ffrac).$cb().unwrap() + } + }; +} + +parse_float!(parse_f32, f32, to_f32); +parse_float!(parse_f64, f64, to_f64); diff --git a/lexical-write-float/tests/radix_tests.rs b/lexical-write-float/tests/radix_tests.rs new file mode 100644 index 0000000..b3adde4 --- /dev/null +++ b/lexical-write-float/tests/radix_tests.rs @@ -0,0 +1,347 @@ +#![cfg(feature = "radix")] + +mod parse_radix; + +use approx::{assert_relative_eq, relative_eq}; +use core::num; +use lexical_util::constants::{FormattedSize, BUFFER_SIZE}; +use lexical_util::format::{NumberFormat, NumberFormatBuilder}; +use lexical_util::num::Float; +use lexical_write_float::options::RoundMode; +use lexical_write_float::{radix, Options}; +use lexical_write_integer::write::WriteInteger; +use parse_radix::{parse_f32, parse_f64}; +use proptest::prelude::*; +// FIXME: +// Quickcheck is currently disabled, due to bugs described below. +// Tracking issue is https://github.com/BurntSushi/quickcheck/issues/295 +// use quickcheck::quickcheck; + +const BASE3: u128 = NumberFormatBuilder::from_radix(3); +const BASE5: u128 = NumberFormatBuilder::from_radix(5); + +const F32_DATA: [f32; 31] = [ + 0., + 0.1, + 1., + 1.1, + 12., + 12.1, + 123., + 123.1, + 1234., + 1234.1, + 12345., + 12345.1, + 123456., + 123456.1, + 1234567., + 1234567.1, + 12345678., + 12345678.1, + 123456789., + 123456789.1, + 123456789.12, + 123456789.123, + 123456789.1234, + 123456789.12345, + 1.2345678912345e8, + 1.2345e+8, + 1.2345e+11, + 1.2345e+38, + 1.2345e-8, + 1.2345e-11, + 1.2345e-38, +]; +const F64_DATA: [f64; 33] = [ + 0., + 0.1, + 1., + 1.1, + 12., + 12.1, + 123., + 123.1, + 1234., + 1234.1, + 12345., + 12345.1, + 123456., + 123456.1, + 1234567., + 1234567.1, + 12345678., + 12345678.1, + 123456789., + 123456789.1, + 123456789.12, + 123456789.123, + 123456789.1234, + 123456789.12345, + 1.2345678912345e8, + 1.2345e+8, + 1.2345e+11, + 1.2345e+38, + 1.2345e+308, + 1.2345e-8, + 1.2345e-11, + 1.2345e-38, + 1.2345e-299, +]; + +fn write_float(f: T, options: &Options, expected: &str) +where + ::Unsigned: WriteInteger + FormattedSize, +{ + let mut buffer = [b'\x00'; BUFFER_SIZE]; + let count = unsafe { radix::write_float::<_, FORMAT>(f, &mut buffer, options) }; + let actual = unsafe { std::str::from_utf8_unchecked(&buffer[..count]) }; + assert_eq!(actual, expected); +} + +#[test] +fn write_float_test() { + // Check no formatting, binary, and when exponent notation is used. + let options = Options::builder().build().unwrap(); + write_float::<_, BASE3>(0.0f64, &options, "0.0"); + write_float::<_, BASE3>(1.0f64, &options, "1.0"); + write_float::<_, BASE3>(2.0f64, &options, "2.0"); + write_float::<_, BASE3>(0.49999999999f64, &options, "0.111111111111111111111101200020121"); + write_float::<_, BASE3>(0.5f64, &options, "0.1111111111111111111111111111111112"); + write_float::<_, BASE3>(0.75f64, &options, "0.202020202020202020202020202020202"); + write_float::<_, BASE3>(0.9998475842097241f64, &options, "0.22222222"); + + // Try changing the exponent limits. + let options = Options::builder() + .negative_exponent_break(num::NonZeroI32::new(-6)) + .positive_exponent_break(num::NonZeroI32::new(10)) + .build() + .unwrap(); + write_float::<_, BASE3>(1501.2344967901236f64, &options, "2001121.02002222112101212200212222"); + write_float::<_, BASE3>( + 0.02290702051986883f64, + &options, + "0.000121200212201201002110120212011", + ); + write_float::<_, BASE3>(10e9f64, &options, "2.21210220202122010101e202"); + + // Check max digits. + let options = + Options::builder().max_significant_digits(num::NonZeroUsize::new(5)).build().unwrap(); + write_float::<_, BASE3>(0.0f64, &options, "0.0"); + write_float::<_, BASE3>(1.0f64, &options, "1.0"); + write_float::<_, BASE3>(2.0f64, &options, "2.0"); + write_float::<_, BASE3>(0.49999999999f64, &options, "0.11111"); + write_float::<_, BASE3>(0.5f64, &options, "0.11112"); + write_float::<_, BASE3>(0.75f64, &options, "0.20202"); + write_float::<_, BASE3>(0.9998475842097241f64, &options, "1.0"); + + // Check min digits. + let options = + Options::builder().min_significant_digits(num::NonZeroUsize::new(5)).build().unwrap(); + write_float::<_, BASE3>(0.0f64, &options, "0.0000"); + write_float::<_, BASE3>(1.0f64, &options, "1.0000"); + write_float::<_, BASE3>(2.0f64, &options, "2.0000"); + write_float::<_, BASE3>(0.49999999999f64, &options, "0.111111111111111111111101200020121"); + + // Check max digits and trim floats. + let options = Options::builder() + .max_significant_digits(num::NonZeroUsize::new(5)) + .trim_floats(true) + .build() + .unwrap(); + write_float::<_, BASE3>(0.2345678901234567890f64, &options, "0.0201"); + write_float::<_, BASE3>(23.45678901234567890f64, &options, "212.11"); + write_float::<_, BASE3>(93.82715604938272f64, &options, "10111"); + write_float::<_, BASE3>(375.3086241975309f64, &options, "111220"); + + // Test the round mode. + let truncate = Options::builder() + .max_significant_digits(num::NonZeroUsize::new(2)) + .round_mode(RoundMode::Truncate) + .build() + .unwrap(); + let round = Options::builder() + .max_significant_digits(num::NonZeroUsize::new(2)) + .round_mode(RoundMode::Round) + .build() + .unwrap(); + write_float::<_, BASE3>(23.45678901234567890f64, &round, "220.0"); + write_float::<_, BASE3>(23.45678901234567890f64, &truncate, "210.0"); +} + +macro_rules! test_radix { + ($parse:ident, $f:ident, $radix:expr, $buffer:ident, $options:ident) => {{ + const FORMAT: u128 = NumberFormatBuilder::from_radix($radix); + let format = NumberFormat:: {}; + let count = unsafe { radix::write_float::<_, FORMAT>($f, &mut $buffer, &$options) }; + let roundtrip = $parse(&$buffer[..count], $radix, format.exponent()); + assert_relative_eq!($f, roundtrip, epsilon = 1e-6, max_relative = 3e-6); + }}; +} + +macro_rules! test_all { + ($parse:ident, $f:ident, $buffer:ident, $options:ident) => {{ + test_radix!($parse, $f, 3, $buffer, $options); + test_radix!($parse, $f, 5, $buffer, $options); + test_radix!($parse, $f, 6, $buffer, $options); + test_radix!($parse, $f, 7, $buffer, $options); + test_radix!($parse, $f, 9, $buffer, $options); + test_radix!($parse, $f, 11, $buffer, $options); + test_radix!($parse, $f, 12, $buffer, $options); + test_radix!($parse, $f, 13, $buffer, $options); + test_radix!($parse, $f, 14, $buffer, $options); + test_radix!($parse, $f, 15, $buffer, $options); + test_radix!($parse, $f, 17, $buffer, $options); + test_radix!($parse, $f, 18, $buffer, $options); + test_radix!($parse, $f, 19, $buffer, $options); + test_radix!($parse, $f, 20, $buffer, $options); + test_radix!($parse, $f, 21, $buffer, $options); + test_radix!($parse, $f, 22, $buffer, $options); + test_radix!($parse, $f, 23, $buffer, $options); + test_radix!($parse, $f, 24, $buffer, $options); + test_radix!($parse, $f, 25, $buffer, $options); + test_radix!($parse, $f, 26, $buffer, $options); + test_radix!($parse, $f, 27, $buffer, $options); + test_radix!($parse, $f, 28, $buffer, $options); + test_radix!($parse, $f, 29, $buffer, $options); + test_radix!($parse, $f, 30, $buffer, $options); + test_radix!($parse, $f, 31, $buffer, $options); + test_radix!($parse, $f, 33, $buffer, $options); + test_radix!($parse, $f, 34, $buffer, $options); + test_radix!($parse, $f, 35, $buffer, $options); + test_radix!($parse, $f, 36, $buffer, $options); + }}; +} + +#[test] +fn f32_radix_roundtrip_test() { + let mut buffer = [b'\x00'; 1200]; + let options = Options::new(); + for &f in F32_DATA.iter() { + test_all!(parse_f32, f, buffer, options); + } +} + +#[test] +fn f64_radix_roundtrip_test() { + let mut buffer = [b'\x00'; BUFFER_SIZE]; + let options = Options::new(); + for &f in F64_DATA.iter() { + test_all!(parse_f64, f, buffer, options); + } +} + +// FIXME: +// There's an issue in quickcheck with the following inputs: +// f32::from_bits(0b11001111000000000000000000000000); // -2147483600.0 +// f64::from_bits(0b1100001111100000000000000000000000000000000000000000000000000000) // f=-9223372036854776000.0 +// These repeat infinitely, preventing the test harness from working. +//quickcheck! { +// fn f32_base3_quickcheck(f: f32) -> bool { +// let mut buffer = [b'\x00'; BUFFER_SIZE]; +// let options = Options::builder().build().unwrap(); +// if f.is_special() { +// true +// } else { +// let f = f.abs(); +// let count = unsafe { radix::write_float::<_, BASE3>(f, &mut buffer, &options) }; +// let roundtrip = parse_f32(&buffer[..count], 3, b'e'); +// relative_eq!(f, roundtrip, epsilon=1e-6, max_relative=1e-6) +// } +// } +// +// fn f32_base5_quickcheck(f: f32) -> bool { +// let mut buffer = [b'\x00'; BUFFER_SIZE]; +// let options = Options::builder().build().unwrap(); +// if f.is_special() { +// true +// } else { +// let f = f.abs(); +// let count = unsafe { radix::write_float::<_, BASE5>(f, &mut buffer, &options) }; +// let roundtrip = parse_f32(&buffer[..count], 5, b'e'); +// relative_eq!(f, roundtrip, epsilon=1e-6, max_relative=1e-6) +// } +// } +// +// fn f64_base3_quickcheck(f: f64) -> bool { +// let mut buffer = [b'\x00'; BUFFER_SIZE]; +// let options = Options::builder().build().unwrap(); +// if f.is_special() { +// true +// } else { +// let f = f.abs(); +// let count = unsafe { radix::write_float::<_, BASE3>(f, &mut buffer, &options) }; +// let roundtrip = parse_f64(&buffer[..count], 3, b'e'); +// relative_eq!(f, roundtrip, epsilon=1e-6, max_relative=1e-6) +// } +// } +// +// fn f64_base5_quickcheck(f: f64) -> bool { +// let mut buffer = [b'\x00'; BUFFER_SIZE]; +// let options = Options::builder().build().unwrap(); +// if f.is_special() { +// true +// } else { +// println!("f={:?}", f); +// let f = f.abs(); +// let count = unsafe { radix::write_float::<_, BASE5>(f, &mut buffer, &options) }; +// let roundtrip = parse_f64(&buffer[..count], 5, b'e'); +// relative_eq!(f, roundtrip, epsilon=1e-6, max_relative=1e-6) +// } +// } +//} + +proptest! { + #[test] + fn f32_base3_proptest(f in f32::MIN..f32::MAX) { + let mut buffer = [b'\x00'; BUFFER_SIZE]; + let options = Options::builder().build().unwrap(); + if !f.is_special() { + let f = f.abs(); + let count = unsafe { radix::write_float::<_, BASE3>(f, &mut buffer, &options) }; + let roundtrip = parse_f32(&buffer[..count], 3, b'e'); + let equal = relative_eq!(f, roundtrip, epsilon=1e-6, max_relative=1e-6); + prop_assert!(equal) + } + } + + #[test] + fn f32_base5_proptest(f in f32::MIN..f32::MAX) { + let mut buffer = [b'\x00'; BUFFER_SIZE]; + let options = Options::builder().build().unwrap(); + if !f.is_special() { + let f = f.abs(); + let count = unsafe { radix::write_float::<_, BASE5>(f, &mut buffer, &options) }; + let roundtrip = parse_f32(&buffer[..count], 5, b'e'); + let equal = relative_eq!(f, roundtrip, epsilon=1e-6, max_relative=1e-6); + prop_assert!(equal) + } + } + + #[test] + fn f64_base3_proptest(f in f64::MIN..f64::MAX) { + let mut buffer = [b'\x00'; BUFFER_SIZE]; + let options = Options::builder().build().unwrap(); + if !f.is_special() { + let f = f.abs(); + let count = unsafe { radix::write_float::<_, BASE3>(f, &mut buffer, &options) }; + let roundtrip = parse_f64(&buffer[..count], 3, b'e'); + let equal = relative_eq!(f, roundtrip, epsilon=1e-6, max_relative=1e-6); + prop_assert!(equal) + } + } + + #[test] + fn f64_base5_proptest(f in f64::MIN..f64::MAX) { + let mut buffer = [b'\x00'; BUFFER_SIZE]; + let options = Options::builder().build().unwrap(); + if !f.is_special() { + let f = f.abs(); + let count = unsafe { radix::write_float::<_, BASE5>(f, &mut buffer, &options) }; + let roundtrip = parse_f64(&buffer[..count], 5, b'e'); + let equal = relative_eq!(f, roundtrip, epsilon=1e-6, max_relative=1e-6); + prop_assert!(equal) + } + } +} diff --git a/lexical-write-integer/Cargo.toml b/lexical-write-integer/Cargo.toml index 5d63cec..dbcd48a 100644 --- a/lexical-write-integer/Cargo.toml +++ b/lexical-write-integer/Cargo.toml @@ -36,6 +36,8 @@ std = ["lexical-util/std"] power-of-two = ["lexical-util/power-of-two"] # Add support for writing non-decimal integer strings. radix = ["lexical-util/radix", "power-of-two"] +# Add support for writing custom integer formats. +format = ["lexical-util/format"] # Reduce code size at the cost of performance. compact = ["lexical-util/compact"] # Ensure only safe indexing is used. diff --git a/lexical-write-integer/src/api.rs b/lexical-write-integer/src/api.rs index 9425bf8..7fb0c1a 100644 --- a/lexical-write-integer/src/api.rs +++ b/lexical-write-integer/src/api.rs @@ -23,7 +23,18 @@ where Narrow: WriteInteger, Wide: WriteInteger, { - unsafe { value.write_mantissa::(buffer) } + let format = NumberFormat:: {}; + if cfg!(feature = "format") && format.required_mantissa_sign() { + // SAFETY: safe as long as there is at least `FORMATTED_SIZE` elements. + unsafe { + index_unchecked_mut!(buffer[0]) = b'+'; + let buffer = &mut index_unchecked_mut!(buffer[1..]); + value.write_mantissa::(buffer) + 1 + } + } else { + // SAFETY: safe as long as there is at least `FORMATTED_SIZE` elements. + unsafe { value.write_mantissa::(buffer) } + } } // SIGNED @@ -37,13 +48,14 @@ where #[inline] unsafe fn signed( value: Narrow, - mut buffer: &mut [u8], + buffer: &mut [u8], ) -> usize where Narrow: SignedInteger, Wide: SignedInteger, Unsigned: WriteInteger, { + let format = NumberFormat:: {}; if value < Narrow::ZERO { // Need to cast the value to the same size as unsigned type, since if // the value is **exactly** `Narrow::MIN`, and it it is then cast @@ -54,7 +66,15 @@ where // SAFETY: safe as long as there is at least `FORMATTED_SIZE` elements. unsafe { index_unchecked_mut!(buffer[0]) = b'-'; - buffer = &mut index_unchecked_mut!(buffer[1..]); + let buffer = &mut index_unchecked_mut!(buffer[1..]); + unsigned.write_mantissa::(buffer) + 1 + } + } else if cfg!(feature = "format") && format.required_mantissa_sign() { + let unsigned = Unsigned::as_cast(value); + // SAFETY: safe as long as there is at least `FORMATTED_SIZE` elements. + unsafe { + index_unchecked_mut!(buffer[0]) = b'+'; + let buffer = &mut index_unchecked_mut!(buffer[1..]); unsigned.write_mantissa::(buffer) + 1 } } else { diff --git a/lexical-write-integer/src/write.rs b/lexical-write-integer/src/write.rs index ff631b7..26150a1 100644 --- a/lexical-write-integer/src/write.rs +++ b/lexical-write-integer/src/write.rs @@ -58,7 +58,7 @@ pub trait WriteInteger: Compact { /// /// # Preconditions /// - /// `value` must be non-negative and unsigned. + /// `self` must be non-negative and unsigned. /// /// # Safety /// @@ -90,7 +90,7 @@ pub trait WriteInteger: Decimal { /// /// # Preconditions /// - /// `value` must be non-negative and unsigned. + /// `self` must be non-negative and unsigned. /// /// # Safety /// @@ -120,7 +120,7 @@ pub trait WriteInteger: Decimal + Radix { /// /// # Preconditions /// - /// `value` must be non-negative and unsigned. + /// `self` must be non-negative and unsigned. /// /// # Safety /// diff --git a/lexical-write-integer/tests/api_tests.rs b/lexical-write-integer/tests/api_tests.rs index a545c4a..4c938b5 100644 --- a/lexical-write-integer/tests/api_tests.rs +++ b/lexical-write-integer/tests/api_tests.rs @@ -6,6 +6,8 @@ use core::str::{from_utf8_unchecked, FromStr}; use lexical_util::constants::FormattedSize; #[cfg(feature = "radix")] use lexical_util::constants::BUFFER_SIZE; +#[cfg(feature = "format")] +use lexical_util::format::NumberFormatBuilder; use lexical_util::format::STANDARD; use lexical_write_integer::{Options, ToLexical, ToLexicalWithOptions}; use proptest::prelude::*; @@ -29,6 +31,17 @@ macro_rules! roundtrip_impl { roundtrip_impl! { u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize } +#[test] +#[cfg(feature = "format")] +fn mandatory_sign_test() { + let mut buffer = [b'\x00'; 16]; + let options = Options::new(); + const FORMAT: u128 = NumberFormatBuilder::new().required_mantissa_sign(true).build(); + assert_eq!(b"+0", 0i8.to_lexical_with_options::<{ FORMAT }>(&mut buffer, &options)); + assert_eq!(b"-1", (-1i8).to_lexical_with_options::<{ FORMAT }>(&mut buffer, &options)); + assert_eq!(b"+1", 1i8.to_lexical_with_options::<{ FORMAT }>(&mut buffer, &options)); +} + #[test] fn u8_test() { let mut buffer = [b'\x00'; 16];