1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

seq:add floating point support (#6959)

* seq:enable parsing of hexadecimal floats

Turn on the float parser. Now it's possible to use hexadecimal floats as
parameters. For example,

    cargo run -- 0x1p-1 3
    0.5
    1.5
    2.5

Issue #6935

---------

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>
Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
This commit is contained in:
Alexander 2025-01-15 11:53:18 +01:00 committed by GitHub
parent 6600397a65
commit 05ada0d204
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 555 additions and 26 deletions

View file

@ -0,0 +1,404 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore extendedbigdecimal bigdecimal hexdigit numberparse
use crate::extendedbigdecimal::ExtendedBigDecimal;
use crate::number::PreciseNumber;
use crate::numberparse::ParseNumberError;
use bigdecimal::BigDecimal;
use num_traits::FromPrimitive;
/// The base of the hex number system
const HEX_RADIX: u32 = 16;
/// Parse a number from a floating-point hexadecimal exponent notation.
///
/// # Errors
/// Returns [`Err`] if:
/// - the input string is not a valid hexadecimal string
/// - the input data can't be interpreted as ['f64'] or ['BigDecimal']
///
/// # Examples
///
/// ```rust,ignore
/// let input = "0x1.4p-2";
/// let expected = 0.3125;
/// match input.parse_number::<PreciseNumber>().unwrap().number {
/// ExtendedBigDecimal::BigDecimal(bd) => assert_eq!(bd.to_f64().unwrap(),expected),
/// _ => unreachable!()
/// };
/// ```
pub fn parse_number(s: &str) -> Result<PreciseNumber, ParseNumberError> {
// Parse floating point parts
let (sign, remain) = parse_sign_multiplier(s.trim())?;
let remain = parse_hex_prefix(remain)?;
let (integral_part, remain) = parse_integral_part(remain)?;
let (fractional_part, remain) = parse_fractional_part(remain)?;
let (exponent_part, remain) = parse_exponent_part(remain)?;
// Check parts. Rise error if:
// - The input string is not fully consumed
// - Only integral part is presented
// - Only exponent part is presented
// - All 3 parts are empty
match (
integral_part,
fractional_part,
exponent_part,
remain.is_empty(),
) {
(_, _, _, false)
| (Some(_), None, None, _)
| (None, None, Some(_), _)
| (None, None, None, _) => return Err(ParseNumberError::Float),
_ => (),
};
// Build a number from parts
let integral_value = integral_part.unwrap_or(0.0);
let fractional_value = fractional_part.unwrap_or(0.0);
let exponent_value = (2.0_f64).powi(exponent_part.unwrap_or(0));
let value = sign * (integral_value + fractional_value) * exponent_value;
// Build a PreciseNumber
let number = BigDecimal::from_f64(value).ok_or(ParseNumberError::Float)?;
let num_fractional_digits = number.fractional_digit_count().max(0) as u64;
let num_integral_digits = if value.abs() < 1.0 {
0
} else {
number.digits() - num_fractional_digits
};
let num_integral_digits = num_integral_digits + if sign < 0.0 { 1 } else { 0 };
Ok(PreciseNumber::new(
ExtendedBigDecimal::BigDecimal(number),
num_integral_digits as usize,
num_fractional_digits as usize,
))
}
// Detect number precision similar to GNU coreutils. Refer to scan_arg in seq.c. There are still
// some differences from the GNU version, but this should be sufficient to test the idea.
pub fn parse_precision(s: &str) -> Option<usize> {
let hex_index = s.find(['x', 'X']);
let point_index = s.find('.');
if hex_index.is_some() {
// Hex value. Returns:
// - 0 for a hexadecimal integer (filled above)
// - None for a hexadecimal floating-point number (the default value of precision)
let power_index = s.find(['p', 'P']);
if point_index.is_none() && power_index.is_none() {
// No decimal point and no 'p' (power) => integer => precision = 0
return Some(0);
} else {
return None;
}
}
// This is a decimal floating point. The precision depends on two parameters:
// - the number of fractional digits
// - the exponent
// Let's detect the number of fractional digits
let fractional_length = if let Some(point_index) = point_index {
s[point_index + 1..]
.chars()
.take_while(|c| c.is_ascii_digit())
.count()
} else {
0
};
let mut precision = Some(fractional_length);
// Let's update the precision if exponent is present
if let Some(exponent_index) = s.find(['e', 'E']) {
let exponent_value: i32 = s[exponent_index + 1..].parse().unwrap_or(0);
if exponent_value < 0 {
precision = precision.map(|p| p + exponent_value.unsigned_abs() as usize);
} else {
precision = precision.map(|p| p - p.min(exponent_value as usize));
}
}
precision
}
/// Parse the sign multiplier.
///
/// If a sign is present, the function reads and converts it into a multiplier.
/// If no sign is present, a multiplier of 1.0 is used.
///
/// # Errors
///
/// Returns [`Err`] if the input string does not start with a recognized sign or '0' symbol.
fn parse_sign_multiplier(s: &str) -> Result<(f64, &str), ParseNumberError> {
if let Some(remain) = s.strip_prefix('-') {
Ok((-1.0, remain))
} else if let Some(remain) = s.strip_prefix('+') {
Ok((1.0, remain))
} else if s.starts_with('0') {
Ok((1.0, s))
} else {
Err(ParseNumberError::Float)
}
}
/// Parses the `0x` prefix in a case-insensitive manner.
///
/// # Errors
///
/// Returns [`Err`] if the input string does not contain the required prefix.
fn parse_hex_prefix(s: &str) -> Result<&str, ParseNumberError> {
if !(s.starts_with("0x") || s.starts_with("0X")) {
return Err(ParseNumberError::Float);
}
Ok(&s[2..])
}
/// Parse the integral part in hexadecimal notation.
///
/// The integral part is hexadecimal number located after the '0x' prefix and before '.' or 'p'
/// symbols. For example, the number 0x1.234p2 has an integral part 1.
///
/// This part is optional.
///
/// # Errors
///
/// Returns [`Err`] if the integral part is present but a hexadecimal number cannot be parsed from the input string.
fn parse_integral_part(s: &str) -> Result<(Option<f64>, &str), ParseNumberError> {
// This part is optional. Skip parsing if symbol is not a hex digit.
let length = s.chars().take_while(|c| c.is_ascii_hexdigit()).count();
if length > 0 {
let integer =
u64::from_str_radix(&s[..length], HEX_RADIX).map_err(|_| ParseNumberError::Float)?;
Ok((Some(integer as f64), &s[length..]))
} else {
Ok((None, s))
}
}
/// Parse the fractional part in hexadecimal notation.
///
/// The function calculates the sum of the digits after the '.' (dot) sign. Each Nth digit is
/// interpreted as digit / 16^n, where n represents the position after the dot starting from 1.
///
/// For example, the number 0x1.234p2 has a fractional part 234, which can be interpreted as
/// 2/16^1 + 3/16^2 + 4/16^3, where 16 is the radix of the hexadecimal number system. This equals
/// 0.125 + 0.01171875 + 0.0009765625 = 0.1376953125 in decimal. And this is exactly what the
/// function does.
///
/// This part is optional.
///
/// # Errors
///
/// Returns [`Err`] if the fractional part is present but a hexadecimal number cannot be parsed from the input string.
fn parse_fractional_part(s: &str) -> Result<(Option<f64>, &str), ParseNumberError> {
// This part is optional and follows after the '.' symbol. Skip parsing if the dot is not present.
if !s.starts_with('.') {
return Ok((None, s));
}
let s = &s[1..];
let mut multiplier = 1.0 / HEX_RADIX as f64;
let mut total = 0.0;
let mut length = 0;
for c in s.chars().take_while(|c| c.is_ascii_hexdigit()) {
let digit = c
.to_digit(HEX_RADIX)
.map(|x| x as u8)
.ok_or(ParseNumberError::Float)?;
total += (digit as f64) * multiplier;
multiplier /= HEX_RADIX as f64;
length += 1;
}
if length == 0 {
return Err(ParseNumberError::Float);
}
Ok((Some(total), &s[length..]))
}
/// Parse the exponent part in hexadecimal notation.
///
/// The exponent part is a decimal number located after the 'p' symbol.
/// For example, the number 0x1.234p2 has an exponent part 2.
///
/// This part is optional.
///
/// # Errors
///
/// Returns [`Err`] if the exponent part is presented but a decimal number cannot be parsed from
/// the input string.
fn parse_exponent_part(s: &str) -> Result<(Option<i32>, &str), ParseNumberError> {
// This part is optional and follows after 'p' or 'P' symbols. Skip parsing if the symbols are not present
if !(s.starts_with('p') || s.starts_with('P')) {
return Ok((None, s));
}
let s = &s[1..];
let length = s
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '-' || *c == '+')
.count();
if length == 0 {
return Err(ParseNumberError::Float);
}
let value = s[..length].parse().map_err(|_| ParseNumberError::Float)?;
Ok((Some(value), &s[length..]))
}
#[cfg(test)]
mod tests {
use super::{parse_number, parse_precision};
use crate::{numberparse::ParseNumberError, ExtendedBigDecimal};
use bigdecimal::BigDecimal;
use num_traits::ToPrimitive;
fn parse_big_decimal(s: &str) -> Result<BigDecimal, ParseNumberError> {
match parse_number(s)?.number {
ExtendedBigDecimal::BigDecimal(bd) => Ok(bd),
_ => Err(ParseNumberError::Float),
}
}
fn parse_f64(s: &str) -> Result<f64, ParseNumberError> {
parse_big_decimal(s)?
.to_f64()
.ok_or(ParseNumberError::Float)
}
#[test]
fn test_parse_precise_number_case_insensitive() {
assert_eq!(parse_f64("0x1P1").unwrap(), 2.0);
assert_eq!(parse_f64("0x1p1").unwrap(), 2.0);
}
#[test]
fn test_parse_precise_number_plus_minus_prefixes() {
assert_eq!(parse_f64("+0x1p1").unwrap(), 2.0);
assert_eq!(parse_f64("-0x1p1").unwrap(), -2.0);
}
#[test]
fn test_parse_precise_number_power_signs() {
assert_eq!(parse_f64("0x1p1").unwrap(), 2.0);
assert_eq!(parse_f64("0x1p+1").unwrap(), 2.0);
assert_eq!(parse_f64("0x1p-1").unwrap(), 0.5);
}
#[test]
fn test_parse_precise_number_hex() {
assert_eq!(parse_f64("0xd.dp-1").unwrap(), 6.90625);
}
#[test]
fn test_parse_precise_number_no_power() {
assert_eq!(parse_f64("0x123.a").unwrap(), 291.625);
}
#[test]
fn test_parse_precise_number_no_fractional() {
assert_eq!(parse_f64("0x333p-4").unwrap(), 51.1875);
}
#[test]
fn test_parse_precise_number_no_integral() {
assert_eq!(parse_f64("0x.9").unwrap(), 0.5625);
assert_eq!(parse_f64("0x.9p2").unwrap(), 2.25);
}
#[test]
fn test_parse_precise_number_from_valid_values() {
assert_eq!(parse_f64("0x1p1").unwrap(), 2.0);
assert_eq!(parse_f64("+0x1p1").unwrap(), 2.0);
assert_eq!(parse_f64("-0x1p1").unwrap(), -2.0);
assert_eq!(parse_f64("0x1p-1").unwrap(), 0.5);
assert_eq!(parse_f64("0x1.8").unwrap(), 1.5);
assert_eq!(parse_f64("-0x1.8").unwrap(), -1.5);
assert_eq!(parse_f64("0x1.8p2").unwrap(), 6.0);
assert_eq!(parse_f64("0x1.8p+2").unwrap(), 6.0);
assert_eq!(parse_f64("0x1.8p-2").unwrap(), 0.375);
assert_eq!(parse_f64("0x.8").unwrap(), 0.5);
assert_eq!(parse_f64("0x10p0").unwrap(), 16.0);
assert_eq!(parse_f64("0x0.0").unwrap(), 0.0);
assert_eq!(parse_f64("0x0p0").unwrap(), 0.0);
assert_eq!(parse_f64("0x0.0p0").unwrap(), 0.0);
assert_eq!(parse_f64("-0x.1p-3").unwrap(), -0.0078125);
assert_eq!(parse_f64("-0x.ep-3").unwrap(), -0.109375);
}
#[test]
fn test_parse_float_from_invalid_values() {
let expected_error = ParseNumberError::Float;
assert_eq!(parse_f64("").unwrap_err(), expected_error);
assert_eq!(parse_f64("1").unwrap_err(), expected_error);
assert_eq!(parse_f64("1p").unwrap_err(), expected_error);
assert_eq!(parse_f64("0x").unwrap_err(), expected_error);
assert_eq!(parse_f64("0xG").unwrap_err(), expected_error);
assert_eq!(parse_f64("0xp").unwrap_err(), expected_error);
assert_eq!(parse_f64("0xp3").unwrap_err(), expected_error);
assert_eq!(parse_f64("0x1").unwrap_err(), expected_error);
assert_eq!(parse_f64("0x1.").unwrap_err(), expected_error);
assert_eq!(parse_f64("0x1p").unwrap_err(), expected_error);
assert_eq!(parse_f64("0x1p+").unwrap_err(), expected_error);
assert_eq!(parse_f64("-0xx1p1").unwrap_err(), expected_error);
assert_eq!(parse_f64("0x1.k").unwrap_err(), expected_error);
assert_eq!(parse_f64("0x1").unwrap_err(), expected_error);
assert_eq!(parse_f64("-0x1pa").unwrap_err(), expected_error);
assert_eq!(parse_f64("0x1.1pk").unwrap_err(), expected_error);
assert_eq!(parse_f64("0x1.8p2z").unwrap_err(), expected_error);
assert_eq!(parse_f64("0x1p3.2").unwrap_err(), expected_error);
assert_eq!(parse_f64("-0x.ep-3z").unwrap_err(), expected_error);
}
#[test]
fn test_parse_precise_number_count_digits() {
let precise_num = parse_number("0x1.2").unwrap(); // 1.125 decimal
assert_eq!(precise_num.num_integral_digits, 1);
assert_eq!(precise_num.num_fractional_digits, 3);
let precise_num = parse_number("-0x1.2").unwrap(); // -1.125 decimal
assert_eq!(precise_num.num_integral_digits, 2);
assert_eq!(precise_num.num_fractional_digits, 3);
let precise_num = parse_number("0x123.8").unwrap(); // 291.5 decimal
assert_eq!(precise_num.num_integral_digits, 3);
assert_eq!(precise_num.num_fractional_digits, 1);
let precise_num = parse_number("-0x123.8").unwrap(); // -291.5 decimal
assert_eq!(precise_num.num_integral_digits, 4);
assert_eq!(precise_num.num_fractional_digits, 1);
}
#[test]
fn test_parse_precision_valid_values() {
assert_eq!(parse_precision("1"), Some(0));
assert_eq!(parse_precision("0x1"), Some(0));
assert_eq!(parse_precision("0x1.1"), None);
assert_eq!(parse_precision("0x1.1p2"), None);
assert_eq!(parse_precision("0x1.1p-2"), None);
assert_eq!(parse_precision(".1"), Some(1));
assert_eq!(parse_precision("1.1"), Some(1));
assert_eq!(parse_precision("1.12"), Some(2));
assert_eq!(parse_precision("1.12345678"), Some(8));
assert_eq!(parse_precision("1.12345678e-3"), Some(11));
assert_eq!(parse_precision("1.1e-1"), Some(2));
assert_eq!(parse_precision("1.1e-3"), Some(4));
}
#[test]
fn test_parse_precision_invalid_values() {
// Just to make sure it doesn't crash on incomplete values/bad format
// Good enough for now.
assert_eq!(parse_precision("1."), Some(0));
assert_eq!(parse_precision("1e"), Some(0));
assert_eq!(parse_precision("1e-"), Some(0));
assert_eq!(parse_precision("1e+"), Some(0));
assert_eq!(parse_precision("1em"), Some(0));
}
}

View file

@ -19,6 +19,8 @@ use crate::extendedbigdecimal::ExtendedBigDecimal;
pub struct PreciseNumber { pub struct PreciseNumber {
pub number: ExtendedBigDecimal, pub number: ExtendedBigDecimal,
pub num_integral_digits: usize, pub num_integral_digits: usize,
#[allow(dead_code)]
pub num_fractional_digits: usize, pub num_fractional_digits: usize,
} }

View file

@ -2,7 +2,7 @@
// //
// For the full copyright and license information, please view the LICENSE // For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code. // file that was distributed with this source code.
// spell-checker:ignore extendedbigdecimal bigdecimal numberparse // spell-checker:ignore extendedbigdecimal bigdecimal numberparse hexadecimalfloat
//! Parsing numbers for use in `seq`. //! Parsing numbers for use in `seq`.
//! //!
//! This module provides an implementation of [`FromStr`] for the //! This module provides an implementation of [`FromStr`] for the
@ -16,6 +16,7 @@ use num_traits::Num;
use num_traits::Zero; use num_traits::Zero;
use crate::extendedbigdecimal::ExtendedBigDecimal; use crate::extendedbigdecimal::ExtendedBigDecimal;
use crate::hexadecimalfloat;
use crate::number::PreciseNumber; use crate::number::PreciseNumber;
/// An error returned when parsing a number fails. /// An error returned when parsing a number fails.
@ -296,6 +297,14 @@ fn parse_decimal_and_exponent(
/// assert_eq!(actual, expected); /// assert_eq!(actual, expected);
/// ``` /// ```
fn parse_hexadecimal(s: &str) -> Result<PreciseNumber, ParseNumberError> { fn parse_hexadecimal(s: &str) -> Result<PreciseNumber, ParseNumberError> {
if s.find(['.', 'p', 'P']).is_some() {
hexadecimalfloat::parse_number(s)
} else {
parse_hexadecimal_integer(s)
}
}
fn parse_hexadecimal_integer(s: &str) -> Result<PreciseNumber, ParseNumberError> {
let (is_neg, s) = if s.starts_with('-') { let (is_neg, s) = if s.starts_with('-') {
(true, &s[3..]) (true, &s[3..])
} else { } else {

View file

@ -2,7 +2,7 @@
// //
// For the full copyright and license information, please view the LICENSE // For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code. // file that was distributed with this source code.
// spell-checker:ignore (ToDO) extendedbigdecimal numberparse // spell-checker:ignore (ToDO) bigdecimal extendedbigdecimal numberparse hexadecimalfloat
use std::ffi::OsString; use std::ffi::OsString;
use std::io::{stdout, ErrorKind, Write}; use std::io::{stdout, ErrorKind, Write};
@ -10,11 +10,13 @@ use clap::{crate_version, Arg, ArgAction, Command};
use num_traits::{ToPrimitive, Zero}; use num_traits::{ToPrimitive, Zero};
use uucore::error::{FromIo, UResult}; use uucore::error::{FromIo, UResult};
use uucore::format::{num_format, Format}; use uucore::format::{num_format, sprintf, Format, FormatArgument};
use uucore::{format_usage, help_about, help_usage}; use uucore::{format_usage, help_about, help_usage};
mod error; mod error;
mod extendedbigdecimal; mod extendedbigdecimal;
mod hexadecimalfloat;
// public to allow fuzzing // public to allow fuzzing
#[cfg(fuzzing)] #[cfg(fuzzing)]
pub mod number; pub mod number;
@ -72,6 +74,18 @@ fn split_short_args_with_value(args: impl uucore::Args) -> impl uucore::Args {
v.into_iter() v.into_iter()
} }
fn select_precision(
first: Option<usize>,
increment: Option<usize>,
last: Option<usize>,
) -> Option<usize> {
match (first, increment, last) {
(Some(0), Some(0), Some(0)) => Some(0),
(Some(f), Some(i), Some(_)) => Some(f.max(i)),
_ => None,
}
}
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().try_get_matches_from(split_short_args_with_value(args))?; let matches = uu_app().try_get_matches_from(split_short_args_with_value(args))?;
@ -99,32 +113,32 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
format: matches.get_one::<String>(OPT_FORMAT).map(|s| s.as_str()), format: matches.get_one::<String>(OPT_FORMAT).map(|s| s.as_str()),
}; };
let first = if numbers.len() > 1 { let (first, first_precision) = if numbers.len() > 1 {
match numbers[0].parse() { match numbers[0].parse() {
Ok(num) => num, Ok(num) => (num, hexadecimalfloat::parse_precision(numbers[0])),
Err(e) => return Err(SeqError::ParseError(numbers[0].to_string(), e).into()), Err(e) => return Err(SeqError::ParseError(numbers[0].to_string(), e).into()),
} }
} else { } else {
PreciseNumber::one() (PreciseNumber::one(), Some(0))
}; };
let increment = if numbers.len() > 2 { let (increment, increment_precision) = if numbers.len() > 2 {
match numbers[1].parse() { match numbers[1].parse() {
Ok(num) => num, Ok(num) => (num, hexadecimalfloat::parse_precision(numbers[1])),
Err(e) => return Err(SeqError::ParseError(numbers[1].to_string(), e).into()), Err(e) => return Err(SeqError::ParseError(numbers[1].to_string(), e).into()),
} }
} else { } else {
PreciseNumber::one() (PreciseNumber::one(), Some(0))
}; };
if increment.is_zero() { if increment.is_zero() {
return Err(SeqError::ZeroIncrement(numbers[1].to_string()).into()); return Err(SeqError::ZeroIncrement(numbers[1].to_string()).into());
} }
let last: PreciseNumber = { let (last, last_precision): (PreciseNumber, Option<usize>) = {
// We are guaranteed that `numbers.len()` is greater than zero // We are guaranteed that `numbers.len()` is greater than zero
// and at most three because of the argument specification in // and at most three because of the argument specification in
// `uu_app()`. // `uu_app()`.
let n: usize = numbers.len(); let n: usize = numbers.len();
match numbers[n - 1].parse() { match numbers[n - 1].parse() {
Ok(num) => num, Ok(num) => (num, hexadecimalfloat::parse_precision(numbers[n - 1])),
Err(e) => return Err(SeqError::ParseError(numbers[n - 1].to_string(), e).into()), Err(e) => return Err(SeqError::ParseError(numbers[n - 1].to_string(), e).into()),
} }
}; };
@ -133,9 +147,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
.num_integral_digits .num_integral_digits
.max(increment.num_integral_digits) .max(increment.num_integral_digits)
.max(last.num_integral_digits); .max(last.num_integral_digits);
let largest_dec = first
.num_fractional_digits let precision = select_precision(first_precision, increment_precision, last_precision);
.max(increment.num_fractional_digits);
let format = match options.format { let format = match options.format {
Some(f) => { Some(f) => {
@ -146,7 +159,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}; };
let result = print_seq( let result = print_seq(
(first.number, increment.number, last.number), (first.number, increment.number, last.number),
largest_dec, precision,
&options.separator, &options.separator,
&options.terminator, &options.terminator,
options.equal_width, options.equal_width,
@ -210,26 +223,42 @@ fn done_printing<T: Zero + PartialOrd>(next: &T, increment: &T, last: &T) -> boo
} }
} }
fn format_bigdecimal(value: &bigdecimal::BigDecimal) -> Option<String> {
let format_arguments = &[FormatArgument::Float(value.to_f64()?)];
let value_as_bytes = sprintf("%g", format_arguments).ok()?;
String::from_utf8(value_as_bytes).ok()
}
/// Write a big decimal formatted according to the given parameters. /// Write a big decimal formatted according to the given parameters.
fn write_value_float( fn write_value_float(
writer: &mut impl Write, writer: &mut impl Write,
value: &ExtendedBigDecimal, value: &ExtendedBigDecimal,
width: usize, width: usize,
precision: usize, precision: Option<usize>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let value_as_str = let value_as_str = match precision {
if *value == ExtendedBigDecimal::Infinity || *value == ExtendedBigDecimal::MinusInfinity { // format with precision: decimal floats and integers
format!("{value:>width$.precision$}") Some(precision) => match value {
} else { ExtendedBigDecimal::Infinity | ExtendedBigDecimal::MinusInfinity => {
format!("{value:>0width$.precision$}") format!("{value:>width$.precision$}")
}; }
_ => format!("{value:>0width$.precision$}"),
},
// format without precision: hexadecimal floats
None => match value {
ExtendedBigDecimal::BigDecimal(bd) => {
format_bigdecimal(bd).unwrap_or_else(|| "{value}".to_owned())
}
_ => format!("{value:>0width$}"),
},
};
write!(writer, "{value_as_str}") write!(writer, "{value_as_str}")
} }
/// Floating point based code path /// Floating point based code path
fn print_seq( fn print_seq(
range: RangeFloat, range: RangeFloat,
largest_dec: usize, precision: Option<usize>,
separator: &str, separator: &str,
terminator: &str, terminator: &str,
pad: bool, pad: bool,
@ -241,7 +270,13 @@ fn print_seq(
let (first, increment, last) = range; let (first, increment, last) = range;
let mut value = first; let mut value = first;
let padding = if pad { let padding = if pad {
padding + if largest_dec > 0 { largest_dec + 1 } else { 0 } let precision_value = precision.unwrap_or(0);
padding
+ if precision_value > 0 {
precision_value + 1
} else {
0
}
} else { } else {
0 0
}; };
@ -273,7 +308,7 @@ fn print_seq(
}; };
f.fmt(&mut stdout, float)?; f.fmt(&mut stdout, float)?;
} }
None => write_value_float(&mut stdout, &value, padding, largest_dec)?, None => write_value_float(&mut stdout, &value, padding, precision)?,
} }
// TODO Implement augmenting addition. // TODO Implement augmenting addition.
value = value + increment.clone(); value = value + increment.clone();

View file

@ -698,7 +698,7 @@ fn test_parse_error_hex() {
new_ucmd!() new_ucmd!()
.arg("0xlmnop") .arg("0xlmnop")
.fails() .fails()
.usage_error("invalid hexadecimal argument: '0xlmnop'"); .usage_error("invalid floating point argument: '0xlmnop'");
} }
#[test] #[test]
@ -826,3 +826,82 @@ fn test_parse_scientific_zero() {
.succeeds() .succeeds()
.stdout_only("0\n1\n"); .stdout_only("0\n1\n");
} }
#[test]
fn test_parse_valid_hexadecimal_float_two_args() {
let test_cases = [
(["0x1p-1", "2"], "0.5\n1.5\n"),
(["0x.8p16", "32768"], "32768\n"),
(["0xffff.4p-4", "4096"], "4095.95\n"),
(["0xA.A9p-1", "6"], "5.33008\n"),
(["0xa.a9p-1", "6"], "5.33008\n"),
(["0xffffffffffp-30", "1024"], "1024\n"), // spell-checker:disable-line
];
for (input_arguments, expected_output) in &test_cases {
new_ucmd!()
.args(input_arguments)
.succeeds()
.stdout_only(expected_output);
}
}
#[test]
fn test_parse_valid_hexadecimal_float_three_args() {
let test_cases = [
(["0x3.4p-1", "0x4p-1", "4"], "1.625\n3.625\n"),
(
["-0x.ep-3", "-0x.1p-3", "-0x.fp-3"],
"-0.109375\n-0.117188\n",
),
];
for (input_arguments, expected_output) in &test_cases {
new_ucmd!()
.args(input_arguments)
.succeeds()
.stdout_only(expected_output);
}
}
#[test]
fn test_parse_float_gnu_coreutils() {
// some values from GNU coreutils tests
new_ucmd!()
.args(&[".89999", "1e-7", ".8999901"])
.succeeds()
.stdout_only("0.8999900\n0.8999901\n");
new_ucmd!()
.args(&["0", "0.000001", "0.000003"])
.succeeds()
.stdout_only("0.000000\n0.000001\n0.000002\n0.000003\n");
}
#[ignore]
#[test]
fn test_parse_valid_hexadecimal_float_format_issues() {
// These tests detect differences in the representation of floating-point values with GNU seq.
// There are two key areas to investigate:
//
// 1. GNU seq uses long double (80-bit) types for values, while the current implementation
// relies on f64 (64-bit). This can lead to differences due to varying precision. However, it's
// likely not the primary cause, as even double (64-bit) values can differ when compared to
// f64.
//
// 2. GNU seq uses the %Lg format specifier for printing (see the "get_default_format" function
// ). It appears that Rust lacks a direct equivalent for this format. Additionally, %Lg
// can use %f (floating) or %e (scientific) depending on the precision. There also seem to be
// some differences in the behavior of C and Rust when displaying floating-point or scientific
// notation, at least without additional configuration.
//
// It makes sense to begin by experimenting with formats and attempting to replicate
// the printf("%Lg",...) behavior. Another area worth investigating is glibc, as reviewing its
// code may help uncover additional corner cases or test data that could reveal more issues.
//Test output: 0.00000000992804416455328464508056640625
new_ucmd!()
.args(&["0xa.a9p-30", "1"])
.succeeds()
.stdout_only("9.92804e-09\n1\n");
}