numfmt: implement "--to-unit" & "--from-unit"

This commit is contained in:
Daniel Hofstetter 2022-07-04 10:25:05 +02:00
parent 37b754f462
commit 1f292dd834
4 changed files with 155 additions and 9 deletions

View file

@ -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<Suffix>, u: &Unit) -> Result<f64> {
}
}
fn transform_from(s: &str, opts: &Unit) -> Result<f64> {
fn transform_from(s: &str, opts: &TransformOptions) -> Result<f64> {
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<String> {
let (i2, s) = consider_suffix(s, opts, round_method)?;
fn transform_to(s: f64, opts: &TransformOptions, round_method: RoundMethod) -> Result<String> {
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,
)?;

View file

@ -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<Unit> {
}
}
// 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<usize> {
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::<usize>() {
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<usize> {
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<NumfmtOptions> {
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());
}
}

View file

@ -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 {

View file

@ -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));
}
}
}