From 1f292dd8346f2c931a7f57f7f30c37e503b8923a Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Mon, 4 Jul 2022 10:25:05 +0200 Subject: [PATCH] numfmt: implement "--to-unit" & "--from-unit" --- src/uu/numfmt/src/format.rs | 16 ++--- src/uu/numfmt/src/numfmt.rs | 110 ++++++++++++++++++++++++++++++++++- src/uu/numfmt/src/options.rs | 6 ++ tests/by-util/test_numfmt.rs | 32 ++++++++++ 4 files changed, 155 insertions(+), 9 deletions(-) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 97f00b3c8..0ad7224d2 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -1,6 +1,6 @@ use uucore::display::Quotable; -use crate::options::{NumfmtOptions, RoundMethod}; +use crate::options::{NumfmtOptions, RoundMethod, TransformOptions}; use crate::units::{DisplayableSuffix, RawSuffix, Result, Suffix, Unit, IEC_BASES, SI_BASES}; /// Iterate over a line's fields, where each field is a contiguous sequence of @@ -127,10 +127,11 @@ fn remove_suffix(i: f64, s: Option, u: &Unit) -> Result { } } -fn transform_from(s: &str, opts: &Unit) -> Result { +fn transform_from(s: &str, opts: &TransformOptions) -> Result { let (i, suffix) = parse_suffix(s)?; + let i = i * (opts.from_unit as f64); - remove_suffix(i, suffix, opts).map(|n| if n < 0.0 { -n.abs().ceil() } else { n.ceil() }) + remove_suffix(i, suffix, &opts.from).map(|n| if n < 0.0 { -n.abs().ceil() } else { n.ceil() }) } /// Divide numerator by denominator, with rounding. @@ -206,8 +207,9 @@ fn consider_suffix(n: f64, u: &Unit, round_method: RoundMethod) -> Result<(f64, } } -fn transform_to(s: f64, opts: &Unit, round_method: RoundMethod) -> Result { - let (i2, s) = consider_suffix(s, opts, round_method)?; +fn transform_to(s: f64, opts: &TransformOptions, round_method: RoundMethod) -> Result { + let (i2, s) = consider_suffix(s, &opts.to, round_method)?; + let i2 = i2 / (opts.to_unit as f64); Ok(match s { None => format!("{}", i2), Some(s) if i2.abs() < 10.0 => format!("{:.1}{}", i2, DisplayableSuffix(s)), @@ -227,8 +229,8 @@ fn format_string( }; let number = transform_to( - transform_from(source_without_suffix, &options.transform.from)?, - &options.transform.to, + transform_from(source_without_suffix, &options.transform)?, + &options.transform, options.round, )?; diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index b87dc1122..c73516990 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -13,6 +13,7 @@ use crate::options::*; use crate::units::{Result, Unit}; use clap::{crate_version, Arg, ArgMatches, Command}; use std::io::{BufRead, Write}; +use units::{IEC_BASES, SI_BASES}; use uucore::display::Quotable; use uucore::error::UResult; use uucore::format_usage; @@ -96,11 +97,66 @@ fn parse_unit(s: &str) -> Result { } } +// Parses a unit size. Suffixes are turned into their integer representations. For example, 'K' +// will return `Ok(1000)`, and '2K' will return `Ok(2000)`. +fn parse_unit_size(s: &str) -> Result { + let number: String = s.chars().take_while(char::is_ascii_digit).collect(); + let suffix = &s[number.len()..]; + + if number.is_empty() || "0".repeat(number.len()) != number { + if let Some(multiplier) = parse_unit_size_suffix(suffix) { + if number.is_empty() { + return Ok(multiplier); + } + + if let Ok(n) = number.parse::() { + return Ok(n * multiplier); + } + } + } + + Err(format!("invalid unit size: {}", s.quote())) +} + +// Parses a suffix of a unit size and returns the corresponding multiplier. For example, +// the suffix 'K' will return `Some(1000)`, and 'Ki' will return `Some(1024)`. +// +// If the suffix is empty, `Some(1)` is returned. +// +// If the suffix is unknown, `None` is returned. +fn parse_unit_size_suffix(s: &str) -> Option { + if s.is_empty() { + return Some(1); + } + + let suffix = s.chars().next().unwrap(); + + if let Some(i) = ['K', 'M', 'G', 'T', 'P', 'E'] + .iter() + .position(|&ch| ch == suffix) + { + return match s.len() { + 1 => Some(SI_BASES[i + 1] as usize), + 2 if s.ends_with('i') => Some(IEC_BASES[i + 1] as usize), + _ => None, + }; + } + + None +} + fn parse_options(args: &ArgMatches) -> Result { let from = parse_unit(args.value_of(options::FROM).unwrap())?; let to = parse_unit(args.value_of(options::TO).unwrap())?; + let from_unit = parse_unit_size(args.value_of(options::FROM_UNIT).unwrap())?; + let to_unit = parse_unit_size(args.value_of(options::TO_UNIT).unwrap())?; - let transform = TransformOptions { from, to }; + let transform = TransformOptions { + from, + from_unit, + to, + to_unit, + }; let padding = match args.value_of(options::PADDING) { Some(s) => s @@ -222,6 +278,13 @@ pub fn uu_app<'a>() -> Command<'a> { .value_name("UNIT") .default_value(options::FROM_DEFAULT), ) + .arg( + Arg::new(options::FROM_UNIT) + .long(options::FROM_UNIT) + .help("specify the input unit size") + .value_name("N") + .default_value(options::FROM_UNIT_DEFAULT), + ) .arg( Arg::new(options::TO) .long(options::TO) @@ -229,6 +292,13 @@ pub fn uu_app<'a>() -> Command<'a> { .value_name("UNIT") .default_value(options::TO_DEFAULT), ) + .arg( + Arg::new(options::TO_UNIT) + .long(options::TO_UNIT) + .help("the output unit size") + .value_name("N") + .default_value(options::TO_UNIT_DEFAULT), + ) .arg( Arg::new(options::PADDING) .long(options::PADDING) @@ -280,7 +350,10 @@ pub fn uu_app<'a>() -> Command<'a> { #[cfg(test)] mod tests { - use super::{handle_buffer, NumfmtOptions, Range, RoundMethod, TransformOptions, Unit}; + use super::{ + handle_buffer, parse_unit_size, parse_unit_size_suffix, NumfmtOptions, Range, RoundMethod, + TransformOptions, Unit, + }; use std::io::{BufReader, Error, ErrorKind, Read}; struct MockBuffer {} @@ -294,7 +367,9 @@ mod tests { NumfmtOptions { transform: TransformOptions { from: Unit::None, + from_unit: 1, to: Unit::None, + to_unit: 1, }, padding: 10, header: 1, @@ -338,4 +413,35 @@ mod tests { let result = handle_buffer(BufReader::new(&input_value[..]), &get_valid_options()); assert!(result.is_ok(), "did not return Ok for valid input"); } + + #[test] + fn test_parse_unit_size() { + assert_eq!(1, parse_unit_size("1").unwrap()); + assert_eq!(1, parse_unit_size("01").unwrap()); + assert!(parse_unit_size("1.1").is_err()); + assert!(parse_unit_size("0").is_err()); + assert!(parse_unit_size("-1").is_err()); + assert!(parse_unit_size("A").is_err()); + assert!(parse_unit_size("18446744073709551616").is_err()); + } + + #[test] + fn test_parse_unit_size_with_suffix() { + assert_eq!(1000, parse_unit_size("K").unwrap()); + assert_eq!(1024, parse_unit_size("Ki").unwrap()); + assert_eq!(2000, parse_unit_size("2K").unwrap()); + assert_eq!(2048, parse_unit_size("2Ki").unwrap()); + assert!(parse_unit_size("0K").is_err()); + } + + #[test] + fn test_parse_unit_size_suffix() { + assert_eq!(1, parse_unit_size_suffix("").unwrap()); + assert_eq!(1000, parse_unit_size_suffix("K").unwrap()); + assert_eq!(1024, parse_unit_size_suffix("Ki").unwrap()); + assert_eq!(1000 * 1000, parse_unit_size_suffix("M").unwrap()); + assert_eq!(1024 * 1024, parse_unit_size_suffix("Mi").unwrap()); + assert!(parse_unit_size_suffix("Kii").is_none()); + assert!(parse_unit_size_suffix("A").is_none()); + } } diff --git a/src/uu/numfmt/src/options.rs b/src/uu/numfmt/src/options.rs index f61d4c704..43227ea1b 100644 --- a/src/uu/numfmt/src/options.rs +++ b/src/uu/numfmt/src/options.rs @@ -6,6 +6,8 @@ pub const FIELD: &str = "field"; pub const FIELD_DEFAULT: &str = "1"; pub const FROM: &str = "from"; pub const FROM_DEFAULT: &str = "none"; +pub const FROM_UNIT: &str = "from-unit"; +pub const FROM_UNIT_DEFAULT: &str = "1"; pub const HEADER: &str = "header"; pub const HEADER_DEFAULT: &str = "1"; pub const NUMBER: &str = "NUMBER"; @@ -14,10 +16,14 @@ pub const ROUND: &str = "round"; pub const SUFFIX: &str = "suffix"; pub const TO: &str = "to"; pub const TO_DEFAULT: &str = "none"; +pub const TO_UNIT: &str = "to-unit"; +pub const TO_UNIT_DEFAULT: &str = "1"; pub struct TransformOptions { pub from: Unit, + pub from_unit: usize, pub to: Unit, + pub to_unit: usize, } pub struct NumfmtOptions { diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 53ed58d99..257f6f7ee 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -607,3 +607,35 @@ fn test_invalid_padding_value() { .stderr_contains(format!("invalid padding value '{}'", padding_value)); } } + +#[test] +fn test_from_unit() { + new_ucmd!() + .args(&["--from-unit=512", "4"]) + .succeeds() + .stdout_is("2048\n"); +} + +#[test] +fn test_to_unit() { + new_ucmd!() + .args(&["--to-unit=512", "2048"]) + .succeeds() + .stdout_is("4\n"); +} + +#[test] +fn test_invalid_unit_size() { + let commands = vec!["from", "to"]; + let invalid_sizes = vec!["A", "0", "18446744073709551616"]; + + for command in commands { + for invalid_size in &invalid_sizes { + new_ucmd!() + .arg(format!("--{}-unit={}", command, invalid_size)) + .fails() + .code_is(1) + .stderr_contains(format!("invalid unit size: '{}'", invalid_size)); + } + } +}