mirror of
https://github.com/uutils/coreutils
synced 2024-11-17 02:08:09 +00:00
Merge pull request #2613 from jfinkels/seq-width-fractional
seq: correctly compute width due to decimal places
This commit is contained in:
commit
77cfafa511
3 changed files with 381 additions and 67 deletions
190
src/uu/seq/src/digits.rs
Normal file
190
src/uu/seq/src/digits.rs
Normal file
|
@ -0,0 +1,190 @@
|
|||
//! Counting number of digits needed to represent a number.
|
||||
//!
|
||||
//! The [`num_integral_digits`] and [`num_fractional_digits`] functions
|
||||
//! count the number of digits needed to represent a number in decimal
|
||||
//! notation (like "123.456").
|
||||
use std::convert::TryInto;
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use uucore::display::Quotable;
|
||||
|
||||
/// The number of digits after the decimal point in a given number.
|
||||
///
|
||||
/// The input `s` is a string representing a number, either an integer
|
||||
/// or a floating point number in either decimal notation or scientific
|
||||
/// notation. This function returns the number of digits after the
|
||||
/// decimal point needed to print the number in decimal notation.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// assert_eq!(num_fractional_digits("123.45e-1").unwrap(), 3);
|
||||
/// ```
|
||||
pub fn num_fractional_digits(s: &str) -> Result<usize, ParseIntError> {
|
||||
match (s.find('.'), s.find('e')) {
|
||||
// For example, "123456".
|
||||
(None, None) => Ok(0),
|
||||
|
||||
// For example, "123e456".
|
||||
(None, Some(j)) => {
|
||||
let exponent: i64 = s[j + 1..].parse()?;
|
||||
if exponent < 0 {
|
||||
Ok(-exponent as usize)
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
// For example, "123.456".
|
||||
(Some(i), None) => Ok(s.len() - (i + 1)),
|
||||
|
||||
// For example, "123.456e789".
|
||||
(Some(i), Some(j)) if i < j => {
|
||||
// Because of the match guard, this subtraction will not underflow.
|
||||
let num_digits_between_decimal_point_and_e = (j - (i + 1)) as i64;
|
||||
let exponent: i64 = s[j + 1..].parse()?;
|
||||
if num_digits_between_decimal_point_and_e < exponent {
|
||||
Ok(0)
|
||||
} else {
|
||||
Ok((num_digits_between_decimal_point_and_e - exponent)
|
||||
.try_into()
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
_ => crash!(
|
||||
1,
|
||||
"invalid floating point argument: {}\n Try '{} --help' for more information.",
|
||||
s.quote(),
|
||||
uucore::execution_phrase()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of digits before the decimal point in a given number.
|
||||
///
|
||||
/// The input `s` is a string representing a number, either an integer
|
||||
/// or a floating point number in either decimal notation or scientific
|
||||
/// notation. This function returns the number of digits before the
|
||||
/// decimal point needed to print the number in decimal notation.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// assert_eq!(num_fractional_digits("123.45e-1").unwrap(), 2);
|
||||
/// ```
|
||||
pub fn num_integral_digits(s: &str) -> Result<usize, ParseIntError> {
|
||||
match (s.find('.'), s.find('e')) {
|
||||
// For example, "123456".
|
||||
(None, None) => Ok(s.len()),
|
||||
|
||||
// For example, "123e456".
|
||||
(None, Some(j)) => {
|
||||
let exponent: i64 = s[j + 1..].parse()?;
|
||||
let total = j as i64 + exponent;
|
||||
if total < 1 {
|
||||
Ok(1)
|
||||
} else {
|
||||
Ok(total.try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
// For example, "123.456".
|
||||
(Some(i), None) => Ok(i),
|
||||
|
||||
// For example, "123.456e789".
|
||||
(Some(i), Some(j)) => {
|
||||
let exponent: i64 = s[j + 1..].parse()?;
|
||||
let minimum: usize = {
|
||||
let integral_part: f64 = crash_if_err!(1, s[..j].parse());
|
||||
if integral_part == -0.0 && integral_part.is_sign_negative() {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
};
|
||||
|
||||
let total = i as i64 + exponent;
|
||||
if total < minimum as i64 {
|
||||
Ok(minimum)
|
||||
} else {
|
||||
Ok(total.try_into().unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
mod test_num_integral_digits {
|
||||
use crate::num_integral_digits;
|
||||
|
||||
#[test]
|
||||
fn test_integer() {
|
||||
assert_eq!(num_integral_digits("123").unwrap(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decimal() {
|
||||
assert_eq!(num_integral_digits("123.45").unwrap(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scientific_no_decimal_positive_exponent() {
|
||||
assert_eq!(num_integral_digits("123e4").unwrap(), 3 + 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scientific_with_decimal_positive_exponent() {
|
||||
assert_eq!(num_integral_digits("123.45e6").unwrap(), 3 + 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scientific_no_decimal_negative_exponent() {
|
||||
assert_eq!(num_integral_digits("123e-4").unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scientific_with_decimal_negative_exponent() {
|
||||
assert_eq!(num_integral_digits("123.45e-6").unwrap(), 1);
|
||||
assert_eq!(num_integral_digits("123.45e-1").unwrap(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
mod test_num_fractional_digits {
|
||||
use crate::num_fractional_digits;
|
||||
|
||||
#[test]
|
||||
fn test_integer() {
|
||||
assert_eq!(num_fractional_digits("123").unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decimal() {
|
||||
assert_eq!(num_fractional_digits("123.45").unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scientific_no_decimal_positive_exponent() {
|
||||
assert_eq!(num_fractional_digits("123e4").unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scientific_with_decimal_positive_exponent() {
|
||||
assert_eq!(num_fractional_digits("123.45e6").unwrap(), 0);
|
||||
assert_eq!(num_fractional_digits("123.45e1").unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scientific_no_decimal_negative_exponent() {
|
||||
assert_eq!(num_fractional_digits("123e-4").unwrap(), 4);
|
||||
assert_eq!(num_fractional_digits("123e-1").unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scientific_with_decimal_negative_exponent() {
|
||||
assert_eq!(num_fractional_digits("123.45e-6").unwrap(), 8);
|
||||
assert_eq!(num_fractional_digits("123.45e-1").unwrap(), 3);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,11 @@ use num_traits::{Num, ToPrimitive};
|
|||
use std::cmp;
|
||||
use std::io::{stdout, ErrorKind, Write};
|
||||
use std::str::FromStr;
|
||||
|
||||
mod digits;
|
||||
use crate::digits::num_fractional_digits;
|
||||
use crate::digits::num_integral_digits;
|
||||
|
||||
use uucore::display::Quotable;
|
||||
|
||||
static ABOUT: &str = "Display numbers from FIRST to LAST, in steps of INCREMENT.";
|
||||
|
@ -62,38 +67,6 @@ impl Number {
|
|||
Number::F64(n) => n,
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of characters needed to print the integral part of the number.
|
||||
///
|
||||
/// The number of characters includes one character to represent the
|
||||
/// minus sign ("-") if this number is negative.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use num_bigint::{BigInt, Sign};
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Number::BigInt(BigInt::new(Sign::Plus, vec![123])).num_digits(),
|
||||
/// 3
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// Number::BigInt(BigInt::new(Sign::Minus, vec![123])).num_digits(),
|
||||
/// 4
|
||||
/// );
|
||||
/// assert_eq!(Number::F64(123.45).num_digits(), 3);
|
||||
/// assert_eq!(Number::MinusZero.num_digits(), 2);
|
||||
/// ```
|
||||
fn num_digits(&self) -> usize {
|
||||
match self {
|
||||
Number::MinusZero => 2,
|
||||
Number::BigInt(n) => n.to_string().len(),
|
||||
Number::F64(n) => {
|
||||
let s = n.to_string();
|
||||
s.find('.').unwrap_or_else(|| s.len())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Number {
|
||||
|
@ -155,20 +128,49 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
};
|
||||
|
||||
let mut largest_dec = 0;
|
||||
let mut padding = 0;
|
||||
let first = if numbers.len() > 1 {
|
||||
let slice = numbers[0];
|
||||
let len = slice.len();
|
||||
let dec = slice.find('.').unwrap_or(len);
|
||||
largest_dec = len - dec;
|
||||
largest_dec = num_fractional_digits(slice).unwrap_or_else(|_| {
|
||||
crash!(
|
||||
1,
|
||||
"invalid floating point argument: {}\n Try '{} --help' for more information.",
|
||||
slice.quote(),
|
||||
uucore::execution_phrase()
|
||||
)
|
||||
});
|
||||
padding = num_integral_digits(slice).unwrap_or_else(|_| {
|
||||
crash!(
|
||||
1,
|
||||
"invalid floating point argument: {}\n Try '{} --help' for more information.",
|
||||
slice.quote(),
|
||||
uucore::execution_phrase()
|
||||
)
|
||||
});
|
||||
crash_if_err!(1, slice.parse())
|
||||
} else {
|
||||
Number::BigInt(BigInt::one())
|
||||
};
|
||||
let increment = if numbers.len() > 2 {
|
||||
let slice = numbers[1];
|
||||
let len = slice.len();
|
||||
let dec = slice.find('.').unwrap_or(len);
|
||||
largest_dec = cmp::max(largest_dec, len - dec);
|
||||
let dec = num_fractional_digits(slice).unwrap_or_else(|_| {
|
||||
crash!(
|
||||
1,
|
||||
"invalid floating point argument: {}\n Try '{} --help' for more information.",
|
||||
slice.quote(),
|
||||
uucore::execution_phrase()
|
||||
)
|
||||
});
|
||||
let int_digits = num_integral_digits(slice).unwrap_or_else(|_| {
|
||||
crash!(
|
||||
1,
|
||||
"invalid floating point argument: {}\n Try '{} --help' for more information.",
|
||||
slice.quote(),
|
||||
uucore::execution_phrase()
|
||||
)
|
||||
});
|
||||
largest_dec = cmp::max(largest_dec, dec);
|
||||
padding = cmp::max(padding, int_digits);
|
||||
crash_if_err!(1, slice.parse())
|
||||
} else {
|
||||
Number::BigInt(BigInt::one())
|
||||
|
@ -183,16 +185,18 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
}
|
||||
let last: Number = {
|
||||
let slice = numbers[numbers.len() - 1];
|
||||
let int_digits = num_integral_digits(slice).unwrap_or_else(|_| {
|
||||
crash!(
|
||||
1,
|
||||
"invalid floating point argument: {}\n Try '{} --help' for more information.",
|
||||
slice.quote(),
|
||||
uucore::execution_phrase()
|
||||
)
|
||||
});
|
||||
padding = cmp::max(padding, int_digits);
|
||||
crash_if_err!(1, slice.parse())
|
||||
};
|
||||
if largest_dec > 0 {
|
||||
largest_dec -= 1;
|
||||
}
|
||||
|
||||
let padding = first
|
||||
.num_digits()
|
||||
.max(increment.num_digits())
|
||||
.max(last.num_digits());
|
||||
let result = match (first, last, increment) {
|
||||
(Number::MinusZero, Number::BigInt(last), Number::BigInt(increment)) => print_seq_integers(
|
||||
(BigInt::zero(), increment, last),
|
||||
|
@ -286,18 +290,24 @@ fn print_seq(
|
|||
let mut stdout = stdout.lock();
|
||||
let (first, increment, last) = range;
|
||||
let mut i = 0isize;
|
||||
let is_first_minus_zero = first == -0.0 && first.is_sign_negative();
|
||||
let mut value = first + i as f64 * increment;
|
||||
let mut is_first_iteration = true;
|
||||
while !done_printing(&value, &increment, &last) {
|
||||
if !is_first_iteration {
|
||||
write!(stdout, "{}", separator)?;
|
||||
}
|
||||
let mut width = padding;
|
||||
if is_first_iteration && is_first_minus_zero {
|
||||
write!(stdout, "-")?;
|
||||
width -= 1;
|
||||
}
|
||||
is_first_iteration = false;
|
||||
let istr = format!("{:.*}", largest_dec, value);
|
||||
let ilen = istr.len();
|
||||
let before_dec = istr.find('.').unwrap_or(ilen);
|
||||
if pad && before_dec < padding {
|
||||
for _ in 0..(padding - before_dec) {
|
||||
if pad && before_dec < width {
|
||||
for _ in 0..(width - before_dec) {
|
||||
write!(stdout, "0")?;
|
||||
}
|
||||
}
|
||||
|
@ -362,23 +372,3 @@ fn print_seq_integers(
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Number;
|
||||
use num_bigint::{BigInt, Sign};
|
||||
|
||||
#[test]
|
||||
fn test_number_num_digits() {
|
||||
assert_eq!(
|
||||
Number::BigInt(BigInt::new(Sign::Plus, vec![123])).num_digits(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
Number::BigInt(BigInt::new(Sign::Minus, vec![123])).num_digits(),
|
||||
4
|
||||
);
|
||||
assert_eq!(Number::F64(123.45).num_digits(), 3);
|
||||
assert_eq!(Number::MinusZero.num_digits(), 2);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,56 @@ fn test_rejects_non_floats() {
|
|||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_float() {
|
||||
new_ucmd!()
|
||||
.args(&["1e2.3"])
|
||||
.fails()
|
||||
.no_stdout()
|
||||
.stderr_contains("invalid floating point argument: '1e2.3'")
|
||||
.stderr_contains("for more information.");
|
||||
new_ucmd!()
|
||||
.args(&["1e2.3", "2"])
|
||||
.fails()
|
||||
.no_stdout()
|
||||
.stderr_contains("invalid floating point argument: '1e2.3'")
|
||||
.stderr_contains("for more information.");
|
||||
new_ucmd!()
|
||||
.args(&["1", "1e2.3"])
|
||||
.fails()
|
||||
.no_stdout()
|
||||
.stderr_contains("invalid floating point argument: '1e2.3'")
|
||||
.stderr_contains("for more information.");
|
||||
new_ucmd!()
|
||||
.args(&["1e2.3", "2", "3"])
|
||||
.fails()
|
||||
.no_stdout()
|
||||
.stderr_contains("invalid floating point argument: '1e2.3'")
|
||||
.stderr_contains("for more information.");
|
||||
new_ucmd!()
|
||||
.args(&["1", "1e2.3", "3"])
|
||||
.fails()
|
||||
.no_stdout()
|
||||
.stderr_contains("invalid floating point argument: '1e2.3'")
|
||||
.stderr_contains("for more information.");
|
||||
new_ucmd!()
|
||||
.args(&["1", "2", "1e2.3"])
|
||||
.fails()
|
||||
.no_stdout()
|
||||
.stderr_contains("invalid floating point argument: '1e2.3'")
|
||||
.stderr_contains("for more information.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_width_invalid_float() {
|
||||
new_ucmd!()
|
||||
.args(&["-w", "1e2.3"])
|
||||
.fails()
|
||||
.no_stdout()
|
||||
.stderr_contains("invalid floating point argument: '1e2.3'")
|
||||
.stderr_contains("for more information.");
|
||||
}
|
||||
|
||||
// ---- Tests for the big integer based path ----
|
||||
|
||||
#[test]
|
||||
|
@ -178,6 +228,90 @@ fn test_width_negative_zero() {
|
|||
.no_stderr();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_width_negative_zero_scientific_notation() {
|
||||
new_ucmd!()
|
||||
.args(&["-w", "-0e0", "1"])
|
||||
.succeeds()
|
||||
.stdout_is("-0\n01\n")
|
||||
.no_stderr();
|
||||
|
||||
new_ucmd!()
|
||||
.args(&["-w", "-0e+1", "1"])
|
||||
.succeeds()
|
||||
.stdout_is("-00\n001\n")
|
||||
.no_stderr();
|
||||
|
||||
new_ucmd!()
|
||||
.args(&["-w", "-0.000e0", "1"])
|
||||
.succeeds()
|
||||
.stdout_is("-0.000\n01.000\n")
|
||||
.no_stderr();
|
||||
|
||||
new_ucmd!()
|
||||
.args(&["-w", "-0.000e-2", "1"])
|
||||
.succeeds()
|
||||
.stdout_is("-0.00000\n01.00000\n")
|
||||
.no_stderr();
|
||||
|
||||
new_ucmd!()
|
||||
.args(&["-w", "-0.000e5", "1"])
|
||||
.succeeds()
|
||||
.stdout_is("-000000\n0000001\n")
|
||||
.no_stderr();
|
||||
|
||||
new_ucmd!()
|
||||
.args(&["-w", "-0.000e5", "1"])
|
||||
.succeeds()
|
||||
.stdout_is("-000000\n0000001\n")
|
||||
.no_stderr();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_width_decimal_scientific_notation_increment() {
|
||||
new_ucmd!()
|
||||
.args(&["-w", ".1", "1e-2", ".11"])
|
||||
.succeeds()
|
||||
.stdout_is("0.10\n0.11\n")
|
||||
.no_stderr();
|
||||
|
||||
new_ucmd!()
|
||||
.args(&["-w", ".0", "1.500e-1", ".2"])
|
||||
.succeeds()
|
||||
.stdout_is("0.0000\n0.1500\n")
|
||||
.no_stderr();
|
||||
}
|
||||
|
||||
/// Test that trailing zeros in the start argument contribute to precision.
|
||||
#[test]
|
||||
fn test_width_decimal_scientific_notation_trailing_zeros_start() {
|
||||
new_ucmd!()
|
||||
.args(&["-w", ".1000", "1e-2", ".11"])
|
||||
.succeeds()
|
||||
.stdout_is("0.1000\n0.1100\n")
|
||||
.no_stderr();
|
||||
}
|
||||
|
||||
/// Test that trailing zeros in the increment argument contribute to precision.
|
||||
#[test]
|
||||
fn test_width_decimal_scientific_notation_trailing_zeros_increment() {
|
||||
new_ucmd!()
|
||||
.args(&["-w", "1e-1", "0.0100", ".11"])
|
||||
.succeeds()
|
||||
.stdout_is("0.1000\n0.1100\n")
|
||||
.no_stderr();
|
||||
}
|
||||
|
||||
/// Test that trailing zeros in the end argument do not contribute to width.
|
||||
#[test]
|
||||
fn test_width_decimal_scientific_notation_trailing_zeros_end() {
|
||||
new_ucmd!()
|
||||
.args(&["-w", "1e-1", "1e-2", ".1100"])
|
||||
.succeeds()
|
||||
.stdout_is("0.10\n0.11\n")
|
||||
.no_stderr();
|
||||
}
|
||||
|
||||
// TODO This is duplicated from `test_yes.rs`; refactor them.
|
||||
/// Run `seq`, capture some of the output, close the pipe, and verify it.
|
||||
fn run(args: &[&str], expected: &[u8]) {
|
||||
|
|
Loading…
Reference in a new issue