From 52f2ab68985cb6f79b63f343835b1c67ff8f030a Mon Sep 17 00:00:00 2001 From: Daniel Rocco Date: Mon, 15 Mar 2021 11:20:33 -0400 Subject: [PATCH] numfmt: implement --delimiter closes #1454 --- src/uu/numfmt/src/format.rs | 43 +++++++++++++--- src/uu/numfmt/src/numfmt.rs | 16 ++++++ src/uu/numfmt/src/options.rs | 2 + tests/by-util/test_numfmt.rs | 95 ++++++++++++++++++++++++++++++++++++ 4 files changed, 150 insertions(+), 6 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index e7286c44e..ebe380569 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -226,12 +226,31 @@ fn format_string( }) } -/// Format a line of text according to the selected options. -/// -/// Given a line of text `s`, split the line into fields, transform and format -/// any selected numeric fields, and print the result to stdout. Fields not -/// selected for conversion are passed through unmodified. -pub fn format_and_print(s: &str, options: &NumfmtOptions) -> Result<()> { +fn format_and_print_delimited(s: &str, options: &NumfmtOptions) -> Result<()> { + let delimiter = options.delimiter.as_ref().unwrap(); + + for (n, field) in (1..).zip(s.split(delimiter)) { + let field_selected = uucore::ranges::contain(&options.fields, n); + + // print delimiter before second and subsequent fields + if n > 1 { + print!("{}", delimiter); + } + + if field_selected { + print!("{}", format_string(&field.trim_start(), options, None)?); + } else { + // print unselected field without conversion + print!("{}", field); + } + } + + println!(); + + Ok(()) +} + +fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> { for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { s: Some(s) }) { let field_selected = uucore::ranges::contain(&options.fields, n); @@ -263,3 +282,15 @@ pub fn format_and_print(s: &str, options: &NumfmtOptions) -> Result<()> { Ok(()) } + +/// Format a line of text according to the selected options. +/// +/// Given a line of text `s`, split the line into fields, transform and format +/// any selected numeric fields, and print the result to stdout. Fields not +/// selected for conversion are passed through unmodified. +pub fn format_and_print(s: &str, options: &NumfmtOptions) -> Result<()> { + match &options.delimiter { + Some(_) => format_and_print_delimited(s, options), + None => format_and_print_whitespace(s, options), + } +} diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index e37792669..29c422a89 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -128,11 +128,20 @@ fn parse_options(args: &ArgMatches) -> Result { None => unreachable!(), }; + let delimiter = args.value_of(options::DELIMITER).map_or(Ok(None), |arg| { + if arg.len() == 1 { + Ok(Some(arg.to_string())) + } else { + Err("the delimiter must be a single character".to_string()) + } + })?; + Ok(NumfmtOptions { transform, padding, header, fields, + delimiter, }) } @@ -145,6 +154,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .usage(&usage[..]) .after_help(LONG_HELP) .setting(AppSettings::AllowNegativeNumbers) + .arg( + Arg::with_name(options::DELIMITER) + .short("d") + .long(options::DELIMITER) + .value_name("X") + .help("use X instead of whitespace for field delimiter"), + ) .arg( Arg::with_name(options::FIELD) .long(options::FIELD) diff --git a/src/uu/numfmt/src/options.rs b/src/uu/numfmt/src/options.rs index ab0340d4e..17f0a6fbe 100644 --- a/src/uu/numfmt/src/options.rs +++ b/src/uu/numfmt/src/options.rs @@ -1,6 +1,7 @@ use crate::units::Transform; use uucore::ranges::Range; +pub const DELIMITER: &str = "delimiter"; pub const FIELD: &str = "field"; pub const FIELD_DEFAULT: &str = "1"; pub const FROM: &str = "from"; @@ -22,4 +23,5 @@ pub struct NumfmtOptions { pub padding: isize, pub header: usize, pub fields: Vec, + pub delimiter: Option, } diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index c22db3bf5..64fc5360d 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -383,3 +383,98 @@ fn test_field_df_example() { .succeeds() .stdout_is_fixture("df_expected.txt"); } + +#[test] +fn test_delimiter_must_not_be_empty() { + new_ucmd!().args(&["-d"]).fails(); +} + +#[test] +fn test_delimiter_must_not_be_more_than_one_character() { + new_ucmd!() + .args(&["--delimiter", "sad"]) + .fails() + .stderr_is("numfmt: the delimiter must be a single character"); +} + +#[test] +fn test_delimiter_only() { + new_ucmd!() + .args(&["-d", ","]) + .pipe_in("1234,56") + .succeeds() + .stdout_only("1234,56\n"); +} + +#[test] +fn test_line_is_field_with_no_delimiter() { + new_ucmd!() + .args(&["-d,", "--to=iec"]) + .pipe_in("123456") + .succeeds() + .stdout_only("121K\n"); +} + +#[test] +fn test_delimiter_to_si() { + new_ucmd!() + .args(&["-d=,", "--to=si"]) + .pipe_in("1234,56") + .succeeds() + .stdout_only("1.3K,56\n"); +} + +#[test] +fn test_delimiter_skips_leading_whitespace() { + new_ucmd!() + .args(&["-d=,", "--to=si"]) + .pipe_in(" \t 1234,56") + .succeeds() + .stdout_only("1.3K,56\n"); +} + +#[test] +fn test_delimiter_preserves_leading_whitespace_in_unselected_fields() { + new_ucmd!() + .args(&["-d=|", "--to=si"]) + .pipe_in(" 1000| 2000") + .succeeds() + .stdout_only("1.0K| 2000\n"); +} + +#[test] +fn test_delimiter_from_si() { + new_ucmd!() + .args(&["-d=,", "--from=si"]) + .pipe_in("1.2K,56") + .succeeds() + .stdout_only("1200,56\n"); +} + +#[test] +fn test_delimiter_overrides_whitespace_separator() { + // GNU numfmt reports this as “invalid suffix” + new_ucmd!() + .args(&["-d,"]) + .pipe_in("1 234,56") + .fails() + .stderr_is("numfmt: invalid number: ‘1 234’\n"); +} + +#[test] +fn test_delimiter_with_padding() { + new_ucmd!() + .args(&["-d=|", "--to=si", "--padding=5"]) + .pipe_in("1000|2000") + .succeeds() + .stdout_only(" 1.0K|2000\n"); +} + +#[test] +fn test_delimiter_with_padding_and_fields() { + new_ucmd!() + .args(&["-d=|", "--to=si", "--padding=5", "--field=-"]) + .pipe_in("1000|2000") + .succeeds() + .stdout_only(" 1.0K| 2.0K\n"); +}