Merge pull request #3751 from cakebaker/numfmt_format

numfmt: implement --format
This commit is contained in:
Sylvestre Ledru 2022-07-29 18:40:18 +02:00 committed by GitHub
commit 38679f1c1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 659 additions and 11 deletions

View file

@ -1,3 +1,4 @@
// spell-checker:ignore powf
use uucore::display::Quotable;
use crate::options::{NumfmtOptions, RoundMethod, TransformOptions};
@ -194,7 +195,19 @@ pub fn div_round(n: f64, d: f64, method: RoundMethod) -> f64 {
}
}
fn consider_suffix(n: f64, u: &Unit, round_method: RoundMethod) -> Result<(f64, Option<Suffix>)> {
// Rounds to the specified number of decimal points.
fn round_with_precision(n: f64, method: RoundMethod, precision: usize) -> f64 {
let p = 10.0_f64.powf(precision as f64);
method.round(p * n) / p
}
fn consider_suffix(
n: f64,
u: &Unit,
round_method: RoundMethod,
precision: usize,
) -> Result<(f64, Option<Suffix>)> {
use crate::units::RawSuffix::*;
let abs_n = n.abs();
@ -220,7 +233,11 @@ fn consider_suffix(n: f64, u: &Unit, round_method: RoundMethod) -> Result<(f64,
_ => return Err("Number is too big and unsupported".to_string()),
};
let v = div_round(n, bases[i], round_method);
let v = if precision > 0 {
round_with_precision(n / bases[i], round_method, precision)
} else {
div_round(n, bases[i], round_method)
};
// check if rounding pushed us into the next base
if v.abs() >= bases[1] {
@ -230,11 +247,31 @@ fn consider_suffix(n: f64, u: &Unit, round_method: RoundMethod) -> Result<(f64,
}
}
fn transform_to(s: f64, opts: &TransformOptions, round_method: RoundMethod) -> Result<String> {
let (i2, s) = consider_suffix(s, &opts.to, round_method)?;
fn transform_to(
s: f64,
opts: &TransformOptions,
round_method: RoundMethod,
precision: usize,
) -> Result<String> {
let (i2, s) = consider_suffix(s, &opts.to, round_method, precision)?;
let i2 = i2 / (opts.to_unit as f64);
Ok(match s {
None if precision > 0 => {
format!(
"{:.precision$}",
round_with_precision(i2, round_method, precision),
precision = precision
)
}
None => format!("{}", i2),
Some(s) if precision > 0 => {
format!(
"{:.precision$}{}",
i2,
DisplayableSuffix(s),
precision = precision
)
}
Some(s) if i2.abs() < 10.0 => format!("{:.1}{}", i2, DisplayableSuffix(s)),
Some(s) => format!("{:.0}{}", i2, DisplayableSuffix(s)),
})
@ -255,6 +292,7 @@ fn format_string(
transform_from(source_without_suffix, &options.transform)?,
&options.transform,
options.round,
options.format.precision,
)?;
// bring back the suffix before applying padding
@ -263,15 +301,34 @@ fn format_string(
None => number,
};
Ok(match implicit_padding.unwrap_or(options.padding) {
let padding = options
.format
.padding
.unwrap_or_else(|| implicit_padding.unwrap_or(options.padding));
let padded_number = match padding {
0 => number_with_suffix,
p if p > 0 && options.format.zero_padding => {
let zero_padded = format!("{:0>padding$}", number_with_suffix, padding = p as usize);
match implicit_padding.unwrap_or(options.padding) {
0 => zero_padded,
p if p > 0 => format!("{:>padding$}", zero_padded, padding = p as usize),
p => format!("{:<padding$}", zero_padded, padding = p.unsigned_abs()),
}
}
p if p > 0 => format!("{:>padding$}", number_with_suffix, padding = p as usize),
p => format!(
"{:<padding$}",
number_with_suffix,
padding = p.unsigned_abs()
),
})
};
Ok(format!(
"{}{}{}",
options.format.prefix, padded_number, options.format.suffix
))
}
fn format_and_print_delimited(s: &str, options: &NumfmtOptions) -> Result<()> {
@ -342,3 +399,27 @@ pub fn format_and_print(s: &str, options: &NumfmtOptions) -> Result<()> {
None => format_and_print_whitespace(s, options),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_round_with_precision() {
let rm = RoundMethod::FromZero;
assert_eq!(1.0, round_with_precision(0.12345, rm, 0));
assert_eq!(0.2, round_with_precision(0.12345, rm, 1));
assert_eq!(0.13, round_with_precision(0.12345, rm, 2));
assert_eq!(0.124, round_with_precision(0.12345, rm, 3));
assert_eq!(0.1235, round_with_precision(0.12345, rm, 4));
assert_eq!(0.12345, round_with_precision(0.12345, rm, 5));
let rm = RoundMethod::TowardsZero;
assert_eq!(0.0, round_with_precision(0.12345, rm, 0));
assert_eq!(0.1, round_with_precision(0.12345, rm, 1));
assert_eq!(0.12, round_with_precision(0.12345, rm, 2));
assert_eq!(0.123, round_with_precision(0.12345, rm, 3));
assert_eq!(0.1234, round_with_precision(0.12345, rm, 4));
assert_eq!(0.12345, round_with_precision(0.12345, rm, 5));
}
}

View file

@ -16,8 +16,8 @@ use std::io::{BufRead, Write};
use units::{IEC_BASES, SI_BASES};
use uucore::display::Quotable;
use uucore::error::UResult;
use uucore::format_usage;
use uucore::ranges::Range;
use uucore::{format_usage, InvalidEncodingHandling};
pub mod errors;
pub mod format;
@ -51,6 +51,12 @@ FIELDS supports cut(1) style field ranges:
-M from first to M'th field (inclusive)
- all fields
Multiple fields/ranges can be separated with commas
FORMAT must be suitable for printing one floating-point argument '%f'.
Optional quote (%'f) will enable --grouping (if supported by current locale).
Optional width value (%10f) will pad output. Optional zero (%010f) width
will zero pad the number. Optional negative values (%-10f) will left align.
Optional precision (%.1f) will override the input determined precision.
";
const USAGE: &str = "{} [OPTION]... [NUMBER]...";
@ -194,6 +200,15 @@ fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
v => Range::from_list(v)?,
};
let format = match args.value_of(options::FORMAT) {
Some(s) => s.parse()?,
None => FormatOptions::default(),
};
if format.grouping && to != Unit::None {
return Err("grouping cannot be combined with --to".to_string());
}
let delimiter = args.value_of(options::DELIMITER).map_or(Ok(None), |arg| {
if arg.len() == 1 {
Ok(Some(arg.to_string()))
@ -222,12 +237,35 @@ fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
delimiter,
round,
suffix,
format,
})
}
// If the --format argument and its value are provided separately, they are concatenated to avoid a
// potential clap error. For example: "--format --%f--" is changed to "--format=--%f--".
fn concat_format_arg_and_value(args: &[String]) -> Vec<String> {
let mut processed_args: Vec<String> = Vec::with_capacity(args.len());
let mut iter = args.iter().peekable();
while let Some(arg) = iter.next() {
if arg == "--format" && iter.peek().is_some() {
processed_args.push(format!("--format={}", iter.peek().unwrap()));
iter.next();
} else {
processed_args.push(arg.to_string());
}
}
processed_args
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().get_matches_from(args);
let args = args
.collect_str(InvalidEncodingHandling::Ignore)
.accept_any();
let matches = uu_app().get_matches_from(concat_format_arg_and_value(&args));
let options = parse_options(&matches).map_err(NumfmtError::IllegalArgument)?;
@ -271,6 +309,13 @@ pub fn uu_app<'a>() -> Command<'a> {
.value_name("FIELDS")
.default_value(options::FIELD_DEFAULT),
)
.arg(
Arg::new(options::FORMAT)
.long(options::FORMAT)
.help("use printf style floating-point FORMAT; see FORMAT below for details")
.takes_value(true)
.value_name("FORMAT"),
)
.arg(
Arg::new(options::FROM)
.long(options::FROM)
@ -351,8 +396,8 @@ pub fn uu_app<'a>() -> Command<'a> {
#[cfg(test)]
mod tests {
use super::{
handle_buffer, parse_unit_size, parse_unit_size_suffix, NumfmtOptions, Range, RoundMethod,
TransformOptions, Unit,
handle_buffer, parse_unit_size, parse_unit_size_suffix, FormatOptions, NumfmtOptions,
Range, RoundMethod, TransformOptions, Unit,
};
use std::io::{BufReader, Error, ErrorKind, Read};
struct MockBuffer {}
@ -377,6 +422,7 @@ mod tests {
delimiter: None,
round: RoundMethod::Nearest,
suffix: None,
format: FormatOptions::default(),
}
}

View file

@ -1,9 +1,12 @@
use std::str::FromStr;
use crate::units::Unit;
use uucore::ranges::Range;
pub const DELIMITER: &str = "delimiter";
pub const FIELD: &str = "field";
pub const FIELD_DEFAULT: &str = "1";
pub const FORMAT: &str = "format";
pub const FROM: &str = "from";
pub const FROM_DEFAULT: &str = "none";
pub const FROM_UNIT: &str = "from-unit";
@ -34,6 +37,7 @@ pub struct NumfmtOptions {
pub delimiter: Option<String>,
pub round: RoundMethod,
pub suffix: Option<String>,
pub format: FormatOptions,
}
#[derive(Clone, Copy)]
@ -68,3 +72,282 @@ impl RoundMethod {
}
}
}
// Represents the options extracted from the --format argument provided by the user.
#[derive(Debug, PartialEq)]
pub struct FormatOptions {
pub grouping: bool,
pub padding: Option<isize>,
pub precision: usize,
pub prefix: String,
pub suffix: String,
pub zero_padding: bool,
}
impl Default for FormatOptions {
fn default() -> Self {
Self {
grouping: false,
padding: None,
precision: 0,
prefix: String::from(""),
suffix: String::from(""),
zero_padding: false,
}
}
}
impl FromStr for FormatOptions {
type Err = String;
// The recognized format is: [PREFIX]%[0]['][-][N][.][N]f[SUFFIX]
//
// The format defines the printing of a floating point argument '%f'.
// An optional quote (%'f) enables --grouping.
// An optional width value (%10f) will pad the number.
// An optional zero (%010f) will zero pad the number.
// An optional negative value (%-10f) will left align.
// An optional precision (%.1f) determines the precision of the number.
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut iter = s.chars().peekable();
let mut options = Self::default();
let mut padding = String::from("");
let mut precision = String::from("");
let mut double_percentage_counter = 0;
// '%' chars in the prefix, if any, must appear in blocks of even length, for example: "%%%%" and
// "%% %%" are ok, "%%% %" is not ok. A single '%' is treated as the beginning of the
// floating point argument.
while let Some(c) = iter.next() {
match c {
'%' if iter.peek() == Some(&'%') => {
iter.next();
double_percentage_counter += 1;
for _ in 0..2 {
options.prefix.push('%');
}
}
'%' => break,
_ => options.prefix.push(c),
}
}
// GNU numfmt drops a char from the prefix for every '%%' in the prefix, so we do the same
for _ in 0..double_percentage_counter {
options.prefix.pop();
}
if iter.peek().is_none() {
return if options.prefix == s {
Err(format!("format '{}' has no % directive", s))
} else {
Err(format!("format '{}' ends in %", s))
};
}
// GNU numfmt allows to mix the characters " ", "'", and "0" in any way, so we do the same
while matches!(iter.peek(), Some(' ') | Some('\'') | Some('0')) {
match iter.next().unwrap() {
' ' => (),
'\'' => options.grouping = true,
'0' => options.zero_padding = true,
_ => unreachable!(),
}
}
if let Some('-') = iter.peek() {
iter.next();
match iter.peek() {
Some(c) if c.is_ascii_digit() => padding.push('-'),
_ => {
return Err(format!(
"invalid format '{}', directive must be %[0]['][-][N][.][N]f",
s
))
}
}
}
while let Some(c) = iter.peek() {
if c.is_ascii_digit() {
padding.push(*c);
iter.next();
} else {
break;
}
}
if !padding.is_empty() {
if let Ok(p) = padding.parse() {
options.padding = Some(p);
} else {
return Err(format!("invalid format '{}' (width overflow)", s));
}
}
if let Some('.') = iter.peek() {
iter.next();
if matches!(iter.peek(), Some(' ') | Some('+') | Some('-')) {
return Err(format!("invalid precision in format '{}'", s));
}
while let Some(c) = iter.peek() {
if c.is_ascii_digit() {
precision.push(*c);
iter.next();
} else {
break;
}
}
if !precision.is_empty() {
if let Ok(p) = precision.parse() {
options.precision = p;
} else {
return Err(format!("invalid precision in format '{}'", s));
}
}
}
if let Some('f') = iter.peek() {
iter.next();
} else {
return Err(format!(
"invalid format '{}', directive must be %[0]['][-][N][.][N]f",
s
));
}
// '%' chars in the suffix, if any, must appear in blocks of even length, otherwise
// it is an error. For example: "%%%%" and "%% %%" are ok, "%%% %" is not ok.
while let Some(c) = iter.next() {
if c != '%' {
options.suffix.push(c);
} else if iter.peek() == Some(&'%') {
for _ in 0..2 {
options.suffix.push('%');
}
iter.next();
} else {
return Err(format!("format '{}' has too many % directives", s));
}
}
Ok(options)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_format() {
assert_eq!(FormatOptions::default(), "%f".parse().unwrap());
assert_eq!(FormatOptions::default(), "% f".parse().unwrap());
}
#[test]
fn test_parse_format_with_invalid_formats() {
assert!("".parse::<FormatOptions>().is_err());
assert!("hello".parse::<FormatOptions>().is_err());
assert!("hello%".parse::<FormatOptions>().is_err());
assert!("%-f".parse::<FormatOptions>().is_err());
assert!("%d".parse::<FormatOptions>().is_err());
assert!("%4 f".parse::<FormatOptions>().is_err());
assert!("%f%".parse::<FormatOptions>().is_err());
assert!("%f%%%".parse::<FormatOptions>().is_err());
assert!("%%f".parse::<FormatOptions>().is_err());
assert!("%%%%f".parse::<FormatOptions>().is_err());
assert!("%.-1f".parse::<FormatOptions>().is_err());
assert!("%. 1f".parse::<FormatOptions>().is_err());
assert!("%18446744073709551616f".parse::<FormatOptions>().is_err());
assert!("%.18446744073709551616f".parse::<FormatOptions>().is_err());
}
#[test]
fn test_parse_format_with_prefix_and_suffix() {
let formats = vec![
("--%f", "--", ""),
("%f::", "", "::"),
("--%f::", "--", "::"),
("%f%%", "", "%%"),
("%%%f", "%", ""),
("%% %f", "%%", ""),
];
for (format, expected_prefix, expected_suffix) in formats {
let options: FormatOptions = format.parse().unwrap();
assert_eq!(expected_prefix, options.prefix);
assert_eq!(expected_suffix, options.suffix);
}
}
#[test]
fn test_parse_format_with_padding() {
let mut expected_options = FormatOptions::default();
let formats = vec![("%12f", Some(12)), ("%-12f", Some(-12))];
for (format, expected_padding) in formats {
expected_options.padding = expected_padding;
assert_eq!(expected_options, format.parse().unwrap());
}
}
#[test]
fn test_parse_format_with_precision() {
let mut expected_options = FormatOptions::default();
let formats = vec![
("%6.2f", Some(6), 2),
("%6.f", Some(6), 0),
("%.2f", None, 2),
("%.f", None, 0),
];
for (format, expected_padding, expected_precision) in formats {
expected_options.padding = expected_padding;
expected_options.precision = expected_precision;
assert_eq!(expected_options, format.parse().unwrap());
}
}
#[test]
fn test_parse_format_with_grouping() {
let expected_options = FormatOptions {
grouping: true,
..Default::default()
};
assert_eq!(expected_options, "%'f".parse().unwrap());
assert_eq!(expected_options, "% ' f".parse().unwrap());
assert_eq!(expected_options, "%'''''''f".parse().unwrap());
}
#[test]
fn test_parse_format_with_zero_padding() {
let expected_options = FormatOptions {
padding: Some(10),
zero_padding: true,
..Default::default()
};
assert_eq!(expected_options, "%010f".parse().unwrap());
assert_eq!(expected_options, "% 0 10f".parse().unwrap());
assert_eq!(expected_options, "%0000000010f".parse().unwrap());
}
#[test]
fn test_parse_format_with_grouping_and_zero_padding() {
let expected_options = FormatOptions {
grouping: true,
zero_padding: true,
..Default::default()
};
assert_eq!(expected_options, "%0'f".parse().unwrap());
assert_eq!(expected_options, "%'0f".parse().unwrap());
assert_eq!(expected_options, "%0'0'0'f".parse().unwrap());
assert_eq!(expected_options, "%'0'0'0f".parse().unwrap());
}
}

View file

@ -17,7 +17,7 @@ pub const IEC_BASES: [f64; 10] = [
pub type WithI = bool;
#[derive(PartialEq)]
#[derive(Clone, Copy, PartialEq)]
pub enum Unit {
Auto,
Si,

View file

@ -673,3 +673,241 @@ fn test_valid_but_forbidden_suffix() {
));
}
}
#[test]
fn test_format() {
new_ucmd!()
.args(&["--format=--%f--", "50"])
.succeeds()
.stdout_is("--50--\n");
}
#[test]
fn test_format_with_separate_value() {
new_ucmd!()
.args(&["--format", "--%f--", "50"])
.succeeds()
.stdout_is("--50--\n");
}
#[test]
fn test_format_padding_with_prefix_and_suffix() {
new_ucmd!()
.args(&["--format=--%6f--", "50"])
.succeeds()
.stdout_is("-- 50--\n");
}
#[test]
fn test_format_negative_padding_with_prefix_and_suffix() {
new_ucmd!()
.args(&["--format=--%-6f--", "50"])
.succeeds()
.stdout_is("--50 --\n");
}
#[test]
fn test_format_with_format_padding_overriding_padding_option() {
new_ucmd!()
.args(&["--format=%6f", "--padding=10", "1234"])
.succeeds()
.stdout_is(" 1234\n");
}
#[test]
fn test_format_with_format_padding_overriding_implicit_padding() {
new_ucmd!()
.args(&["--format=%6f", " 1234"])
.succeeds()
.stdout_is(" 1234\n");
}
#[test]
fn test_format_with_negative_format_padding_and_suffix() {
new_ucmd!()
.args(&["--format=%-6f", "1234 ?"])
.succeeds()
.stdout_is("1234 ?\n");
}
#[test]
fn test_format_with_zero_padding() {
let formats = vec!["%06f", "%0 6f"];
for format in formats {
new_ucmd!()
.args(&[format!("--format={}", format), String::from("1234")])
.succeeds()
.stdout_is("001234\n");
}
}
#[test]
fn test_format_with_zero_padding_and_padding_option() {
new_ucmd!()
.args(&["--format=%06f", "--padding=8", "1234"])
.succeeds()
.stdout_is(" 001234\n");
}
#[test]
fn test_format_with_zero_padding_and_negative_padding_option() {
new_ucmd!()
.args(&["--format=%06f", "--padding=-8", "1234"])
.succeeds()
.stdout_is("001234 \n");
}
#[test]
fn test_format_with_zero_padding_and_implicit_padding() {
new_ucmd!()
.args(&["--format=%06f", " 1234"])
.succeeds()
.stdout_is(" 001234\n");
}
#[test]
fn test_format_with_zero_padding_and_suffix() {
new_ucmd!()
.args(&["--format=%06f", "1234 ?"])
.succeeds()
.stdout_is("001234 ?\n");
}
#[test]
fn test_format_with_precision() {
let values = vec![("0.99", "1.0"), ("1", "1.0"), ("1.01", "1.1")];
for (input, expected) in values {
new_ucmd!()
.args(&["--format=%.1f", input])
.succeeds()
.stdout_is(format!("{}\n", expected));
}
let values = vec![("0.99", "0.99"), ("1", "1.00"), ("1.01", "1.01")];
for (input, expected) in values {
new_ucmd!()
.args(&["--format=%.2f", input])
.succeeds()
.stdout_is(format!("{}\n", expected));
}
}
#[test]
fn test_format_with_precision_and_down_rounding() {
let values = vec![("0.99", "0.9"), ("1", "1.0"), ("1.01", "1.0")];
for (input, expected) in values {
new_ucmd!()
.args(&["--format=%.1f", input, "--round=down"])
.succeeds()
.stdout_is(format!("{}\n", expected));
}
}
#[test]
fn test_format_with_precision_and_to_arg() {
let values = vec![("%.1f", "10.0G"), ("%.4f", "9.9913G")];
for (format, expected) in values {
new_ucmd!()
.args(&[
format!("--format={}", format),
"9991239123".to_string(),
"--to=si".to_string(),
])
.succeeds()
.stdout_is(format!("{}\n", expected));
}
}
#[test]
fn test_format_without_percentage_directive() {
let invalid_formats = vec!["", "hello"];
for invalid_format in invalid_formats {
new_ucmd!()
.arg(format!("--format={}", invalid_format))
.fails()
.code_is(1)
.stderr_contains(format!("format '{}' has no % directive", invalid_format));
}
}
#[test]
fn test_format_with_percentage_directive_at_end() {
let invalid_format = "hello%";
new_ucmd!()
.arg(format!("--format={}", invalid_format))
.fails()
.code_is(1)
.stderr_contains(format!("format '{}' ends in %", invalid_format));
}
#[test]
fn test_format_with_too_many_percentage_directives() {
let invalid_format = "%f %f";
new_ucmd!()
.arg(format!("--format={}", invalid_format))
.fails()
.code_is(1)
.stderr_contains(format!(
"format '{}' has too many % directives",
invalid_format
));
}
#[test]
fn test_format_with_invalid_format() {
let invalid_formats = vec!["%d", "% -43 f"];
for invalid_format in invalid_formats {
new_ucmd!()
.arg(format!("--format={}", invalid_format))
.fails()
.code_is(1)
.stderr_contains(format!(
"invalid format '{}', directive must be %[0]['][-][N][.][N]f",
invalid_format
));
}
}
#[test]
fn test_format_with_width_overflow() {
let invalid_format = "%18446744073709551616f";
new_ucmd!()
.arg(format!("--format={}", invalid_format))
.fails()
.code_is(1)
.stderr_contains(format!(
"invalid format '{}' (width overflow)",
invalid_format
));
}
#[test]
fn test_format_with_invalid_precision() {
let invalid_formats = vec!["%.-1f", "%.+1f", "%. 1f", "%.18446744073709551616f"];
for invalid_format in invalid_formats {
new_ucmd!()
.arg(format!("--format={}", invalid_format))
.fails()
.code_is(1)
.stderr_contains(format!("invalid precision in format '{}'", invalid_format));
}
}
#[test]
fn test_format_grouping_conflicts_with_to_option() {
new_ucmd!()
.args(&["--format=%'f", "--to=si"])
.fails()
.code_is(1)
.stderr_contains("grouping cannot be combined with --to");
}