From b2ad51839bf5c45ae664d4be95d471c885cac9cf Mon Sep 17 00:00:00 2001 From: Yury Krivopalov Date: Sun, 22 Oct 2017 10:57:49 +0300 Subject: [PATCH] Add numfmt --- Cargo.toml | 2 + Makefile | 2 + src/numfmt/Cargo.toml | 16 ++ src/numfmt/main.rs | 5 + src/numfmt/numfmt.rs | 370 ++++++++++++++++++++++++++++++++++++++++++ tests/test_numfmt.rs | 108 ++++++++++++ tests/tests.rs | 1 + 7 files changed, 504 insertions(+) create mode 100644 src/numfmt/Cargo.toml create mode 100644 src/numfmt/main.rs create mode 100644 src/numfmt/numfmt.rs create mode 100644 tests/test_numfmt.rs diff --git a/Cargo.toml b/Cargo.toml index e81ad29d8..3148e261c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ unix = [ "mkfifo", "mknod", "nice", + "numfmt", "nohup", "pathchk", "pinky", @@ -192,6 +193,7 @@ nice = { optional=true, path="src/nice" } nl = { optional=true, path="src/nl" } nohup = { optional=true, path="src/nohup" } nproc = { optional=true, path="src/nproc" } +numfmt = { optional=true, path="src/numfmt" } od = { optional=true, path="src/od" } paste = { optional=true, path="src/paste" } pathchk = { optional=true, path="src/pathchk" } diff --git a/Makefile b/Makefile index 6b650b99a..2035c9400 100644 --- a/Makefile +++ b/Makefile @@ -69,6 +69,7 @@ PROGS := \ more \ mv \ nl \ + numfmt \ nproc \ od \ paste \ @@ -171,6 +172,7 @@ TEST_PROGS := \ mktemp \ mv \ nl \ + numfmt \ od \ paste \ pathchk \ diff --git a/src/numfmt/Cargo.toml b/src/numfmt/Cargo.toml new file mode 100644 index 000000000..04482581b --- /dev/null +++ b/src/numfmt/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "numfmt" +version = "0.0.1" +authors = [] + +[lib] +name = "uu_numfmt" +path = "numfmt.rs" + +[dependencies] +getopts = "0.2.14" +uucore = { path="../uucore" } + +[[bin]] +name = "numfmt" +path = "main.rs" diff --git a/src/numfmt/main.rs b/src/numfmt/main.rs new file mode 100644 index 000000000..ce5428394 --- /dev/null +++ b/src/numfmt/main.rs @@ -0,0 +1,5 @@ +extern crate uu_numfmt; + +fn main() { + std::process::exit(uu_numfmt::uumain(std::env::args().collect())); +} diff --git a/src/numfmt/numfmt.rs b/src/numfmt/numfmt.rs new file mode 100644 index 000000000..6ea84f894 --- /dev/null +++ b/src/numfmt/numfmt.rs @@ -0,0 +1,370 @@ +#![crate_name = "uu_numfmt"] + +/* + * This file is part of the uutils coreutils package. + * + * (c) Yury Krivopalov + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +extern crate getopts; + +use getopts::{Matches, Options}; +use std::io::BufRead; +use std::fmt; + +static NAME: &'static str = "numfmt"; +static VERSION: &'static str = env!("CARGO_PKG_VERSION"); + +type Result = std::result::Result; + +enum TransformDirection { + From, + To, +} + +enum Unit { + Auto, + Si, + Iec, + IecI, +} + +enum Suffix { + K, + M, + G, + T, + P, + E, + Ki, + Mi, + Gi, + Ti, + Pi, + Ei, +} + +impl fmt::Display for Suffix { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Suffix::K => write!(f, "K"), + Suffix::M => write!(f, "M"), + Suffix::G => write!(f, "G"), + Suffix::T => write!(f, "T"), + Suffix::P => write!(f, "P"), + Suffix::E => write!(f, "E"), + Suffix::Ki => write!(f, "Ki"), + Suffix::Mi => write!(f, "Mi"), + Suffix::Gi => write!(f, "Gi"), + Suffix::Ti => write!(f, "Ti"), + Suffix::Pi => write!(f, "Pi"), + Suffix::Ei => write!(f, "Ei"), + } + } +} + +fn parse_suffix(s: String) -> Result<(f64, Option)> { + let mut iter = s.chars(); + let (suffix, suffix_len) = match iter.next_back() { + Some('K') => Ok((Some(Suffix::K), 1)), + Some('M') => Ok((Some(Suffix::M), 1)), + Some('G') => Ok((Some(Suffix::G), 1)), + Some('T') => Ok((Some(Suffix::T), 1)), + Some('P') => Ok((Some(Suffix::P), 1)), + Some('E') => Ok((Some(Suffix::E), 1)), + Some('i') => { + match iter.next_back() { + Some('K') => Ok((Some(Suffix::Ki), 2)), + Some('M') => Ok((Some(Suffix::Mi), 2)), + Some('G') => Ok((Some(Suffix::Gi), 2)), + Some('T') => Ok((Some(Suffix::Ti), 2)), + Some('P') => Ok((Some(Suffix::Pi), 2)), + Some('E') => Ok((Some(Suffix::Ei), 2)), + _ => Err("Failed to parse suffix"), + } + } + _ => Ok((None, 0)), + }?; + + let number = s[..s.len() - suffix_len].parse::().map_err(|err| { + err.to_string() + })?; + + Ok((number, suffix)) +} + +fn parse_unit(s: String) -> Result { + match &s[..] { + "auto" => Ok(Unit::Auto), + "si" => Ok(Unit::Si), + "iec" => Ok(Unit::Iec), + "iec-i" => Ok(Unit::IecI), + _ => Err("Unsupported unit is specified".to_owned()), + } +} + +struct TransformOptions { + direction: TransformDirection, + unit: Unit, +} + +struct NumfmtOptions { + transform: TransformOptions, + padding: isize, + header: usize, +} + +fn remove_suffix(i: f64, s: Option, u: &Unit) -> Result { + match (s, u) { + (None, _) => Ok(i), + (Some(Suffix::K), &Unit::Auto) | + (Some(Suffix::K), &Unit::Si) => Ok(i * 1000.), + (Some(Suffix::M), &Unit::Auto) | + (Some(Suffix::M), &Unit::Si) => Ok(i * 1000_000.), + (Some(Suffix::G), &Unit::Auto) | + (Some(Suffix::G), &Unit::Si) => Ok(i * 1000_000_000.), + (Some(Suffix::T), &Unit::Auto) | + (Some(Suffix::T), &Unit::Si) => Ok(i * 1000_000_000_000.), + (Some(Suffix::P), &Unit::Auto) | + (Some(Suffix::P), &Unit::Si) => Ok(i * 1000_000_000_000_000.), + (Some(Suffix::E), &Unit::Auto) | + (Some(Suffix::E), &Unit::Si) => Ok(i * 1000_000_000_000_000_000.), + + (Some(Suffix::Ki), &Unit::Auto) | + (Some(Suffix::Ki), &Unit::IecI) | + (Some(Suffix::K), &Unit::Iec) => Ok(i * 1024.), + (Some(Suffix::Mi), &Unit::Auto) | + (Some(Suffix::Mi), &Unit::IecI) | + (Some(Suffix::M), &Unit::Iec) => Ok(i * 1048576.), + (Some(Suffix::Gi), &Unit::Auto) | + (Some(Suffix::Gi), &Unit::IecI) | + (Some(Suffix::G), &Unit::Iec) => Ok(i * 1073741824.), + (Some(Suffix::Ti), &Unit::Auto) | + (Some(Suffix::Ti), &Unit::IecI) | + (Some(Suffix::T), &Unit::Iec) => Ok(i * 1099511627776.), + (Some(Suffix::Pi), &Unit::Auto) | + (Some(Suffix::Pi), &Unit::IecI) | + (Some(Suffix::P), &Unit::Iec) => Ok(i * 1125899906842624.), + (Some(Suffix::Ei), &Unit::Auto) | + (Some(Suffix::Ei), &Unit::IecI) | + (Some(Suffix::E), &Unit::Iec) => Ok(i * 1152921504606846976.), + + (_, _) => Err("This suffix is unsupported for specified unit".to_owned()), + } +} + +fn transform_from(s: String, unit: &Unit) -> Result { + let (i, suffix) = parse_suffix(s)?; + remove_suffix(i, suffix, unit).map(|n| n.round().to_string()) +} + +fn consider_suffix(i: f64, u: &Unit) -> Result<(f64, Option)> { + match *u { + Unit::Si => { + match i { + _ if i < 1000. => Ok((i, None)), + _ if i < 1000_000. => Ok((i / 1000., Some(Suffix::K))), + _ if i < 1000_000_000. => Ok((i / 1000_000., Some(Suffix::M))), + _ if i < 1000_000_000_000. => Ok((i / 1000_000_000., Some(Suffix::G))), + _ if i < 1000_000_000_000_000. => Ok((i / 1000_000_000_000., Some(Suffix::T))), + _ if i < 1000_000_000_000_000_000. => Ok( + (i / 1000_000_000_000_000., Some(Suffix::P)), + ), + _ if i < 1000_000_000_000_000_000_000. => Ok(( + i / 1000_000_000_000_000_000., + Some(Suffix::E), + )), + _ => Err("Number is too big and unsupported".to_owned()), + } + } + Unit::Iec => { + match i { + _ if i < 1024. => Ok((i, None)), + _ if i < 1048576. => Ok((i / 1024., Some(Suffix::K))), + _ if i < 1073741824. => Ok((i / 1048576., Some(Suffix::M))), + _ if i < 1099511627776. => Ok((i / 1073741824., Some(Suffix::G))), + _ if i < 1125899906842624. => Ok((i / 1099511627776., Some(Suffix::T))), + _ if i < 1152921504606846976. => Ok((i / 1125899906842624., Some(Suffix::P))), + _ if i < 1180591620717411303424. => Ok((i / 1152921504606846976., Some(Suffix::E))), + _ => Err("Number is too big and unsupported".to_owned()), + } + } + Unit::IecI => { + match i { + _ if i < 1024. => Ok((i, None)), + _ if i < 1048576. => Ok((i / 1024., Some(Suffix::Ki))), + _ if i < 1073741824. => Ok((i / 1048576., Some(Suffix::Mi))), + _ if i < 1099511627776. => Ok((i / 1073741824., Some(Suffix::Gi))), + _ if i < 1125899906842624. => Ok((i / 1099511627776., Some(Suffix::Ti))), + _ if i < 1152921504606846976. => Ok((i / 1125899906842624., Some(Suffix::Pi))), + _ if i < 1180591620717411303424. => Ok( + (i / 1152921504606846976., Some(Suffix::Ei)), + ), + _ => Err("Number is too big and unsupported".to_owned()), + } + } + Unit::Auto => Err("Unit 'auto' isn't supported with --to options".to_owned()), + } +} + +fn transform_to(s: String, unit: &Unit) -> Result { + let i = s.parse::().map_err(|err| err.to_string())?; + let (i2, s) = consider_suffix(i, unit)?; + Ok(match s { + None => format!("{}", i2), + Some(s) => format!("{:.1}{}", i2, s), + }) +} + +fn format_string(source: String, options: &NumfmtOptions) -> Result { + let number = match options.transform.direction { + TransformDirection::From => transform_from(source, &options.transform.unit)?, + TransformDirection::To => transform_to(source, &options.transform.unit)?, + }; + + Ok(match options.padding { + p if p == 0 => number, + p if p > 0 => format!("{:>padding$}", number, padding = p as usize), + p => format!("{: Result { + let transform = if args.opt_present("from") { + TransformOptions { + direction: TransformDirection::From, + unit: parse_unit(args.opt_str("from").ok_or("'--from' should have argument")?)?, + } + } else if args.opt_present("to") { + TransformOptions { + direction: TransformDirection::To, + unit: parse_unit(args.opt_str("to").ok_or("'--to' should have argument")?)?, + } + } else { + return Err("Either '--from' or '--to' should be specified".to_owned()); + }; + + let padding = match args.opt_str("padding") { + Some(s) => s.parse::().map_err(|err| err.to_string()), + None => Ok(0), + }?; + + let header = match args.opt_default("header", "1") { + Some(s) => s.parse::().map_err(|err| err.to_string()), + None => Ok(0), + }?; + + Ok(NumfmtOptions { + transform: transform, + padding: padding, + header: header, + }) +} + +fn handle_args(args: &Vec, options: NumfmtOptions) -> Result<()> { + for l in args { + println!("{}", format_string(l.clone(), &options)?) + } + Ok(()) +} + +fn handle_stdin(options: NumfmtOptions) -> Result<()> { + let stdin = std::io::stdin(); + let locked_stdin = stdin.lock(); + + let mut lines = locked_stdin.lines(); + for l in lines.by_ref().take(options.header) { + l.map(|s| println!("{}", s)).map_err(|e| e.to_string())? + } + + for l in lines { + l.map_err(|e| e.to_string()).and_then(|l| { + let l = format_string(l, &options)?; + Ok(println!("{}", l)) + })? + } + Ok(()) +} + +pub fn uumain(args: Vec) -> i32 { + let mut opts = Options::new(); + + opts.optflag("h", "help", "display this help and exit"); + opts.optflag("V", "version", "output version information and exit"); + opts.optopt( + "", + "from", + "auto-scale input numbers to UNITs; default is 'none'; see UNIT above", + "UNIT", + ); + opts.optopt( + "", + "to", + "auto-scale output numbers to UNITs; see Unit above", + "UNIT", + ); + opts.optopt( + "", + "padding", + "pad the output to N characters; positive N will right-align; negative N will left-align; padding is ignored if the output is wider than N", + "N" + ); + opts.optflagopt( + "", + "header", + "print (without converting) the first N header lines; N defaults to 1 if not specified", + "N", + ); + + let matches = opts.parse(&args[1..]).unwrap(); + if matches.opt_present("help") { + println!("{} {}", NAME, VERSION); + println!(""); + println!("Usage:"); + println!(" {0} [STRING]... [OPTION]...", NAME); + println!(""); + print!( + "{}", + opts.usage("Convert numbers from/to human-readable strings") + ); + println!( + "UNIT options: + none no auto-scaling is done; suffixes will trigger an error + + auto accept optional single/two letter suffix: + + 1K = 1000, 1Ki = 1024, 1M = 1000000, 1Mi = 1048576, + + si accept optional single letter suffix: + + 1K = 1000, 1M = 1000000, ... + + iec accept optional single letter suffix: + + 1K = 1024, 1M = 1048576, ... + + iec-i accept optional two-letter suffix: + + 1Ki = 1024, 1Mi = 1048576, ..." + ); + + return 0; + } + if matches.opt_present("version") { + println!("{} {}", NAME, VERSION); + return 0; + } + + let options = parse_options(&matches).unwrap(); + + if matches.free.len() == 0 { + handle_stdin(options).unwrap() + } else { + handle_args(&matches.free, options).unwrap() + }; + + 0 +} diff --git a/tests/test_numfmt.rs b/tests/test_numfmt.rs new file mode 100644 index 000000000..9ab95a5c9 --- /dev/null +++ b/tests/test_numfmt.rs @@ -0,0 +1,108 @@ +use common::util::*; + +#[test] +fn test_from_si() { + new_ucmd!() + .args(&["--from=si"]) + .pipe_in("1000\n1.1M\n0.1G") + .run() + .stdout_is("1000\n1100000\n100000000"); +} + +#[test] +fn test_from_iec() { + new_ucmd!() + .args(&["--from=iec"]) + .pipe_in("1024\n1.1M\n0.1G") + .run() + .stdout_is("1024\n1153434\n107374182"); +} + +#[test] +fn test_from_iec_i() { + new_ucmd!() + .args(&["--from=iec-i"]) + .pipe_in("1024\n1.1Mi\n0.1Gi") + .run() + .stdout_is("1024\n1153434\n107374182"); +} + +#[test] +fn test_from_auto() { + new_ucmd!() + .args(&["--from=auto"]) + .pipe_in("1K\n1Ki") + .run() + .stdout_is("1000\n1024"); +} + +#[test] +fn test_to_si() { + new_ucmd!() + .args(&["--to=si"]) + .pipe_in("1000\n1100000\n100000000") + .run() + .stdout_is("1.0K\n1.1M\n100.0M"); +} + +#[test] +fn test_to_iec() { + new_ucmd!() + .args(&["--to=iec"]) + .pipe_in("1024\n1153434\n107374182") + .run() + .stdout_is("1.0K\n1.1M\n102.4M"); +} + +#[test] +fn test_to_iec_i() { + new_ucmd!() + .args(&["--to=iec-i"]) + .pipe_in("1024\n1153434\n107374182") + .run() + .stdout_is("1.0Ki\n1.1Mi\n102.4Mi"); +} + +#[test] +fn test_input_from_free_arguments() { + new_ucmd!() + .args(&["--from=si", "1K", "1.1M", "0.1G"]) + .run() + .stdout_is("1000\n1100000\n100000000"); +} + +#[test] +fn test_padding() { + new_ucmd!() + .args(&["--from=si", "--padding=8"]) + .pipe_in("1K\n1.1M\n0.1G") + .run() + .stdout_is(" 1000\n 1100000\n100000000"); +} + +#[test] +fn test_negative_padding() { + new_ucmd!() + .args(&["--from=si", "--padding=-8"]) + .pipe_in("1K\n1.1M\n0.1G") + .run() + .stdout_is("1000 \n1100000 \n100000000"); +} + +#[test] +fn test_header() { + new_ucmd!() + .args(&["--from=si", "--header=2"]) + .pipe_in("header\nheader2\n1K\n1.1M\n0.1G") + .run() + .stdout_is("header\nheader2\n1000\n1100000\n100000000"); +} + +#[test] +fn test_header_default() { + new_ucmd!() + .args(&["--from=si", "--header"]) + .pipe_in("header\n1K\n1.1M\n0.1G") + .run() + .stdout_is("header\n1000\n1100000\n100000000"); +} diff --git a/tests/tests.rs b/tests/tests.rs index b4b16c13e..f8d20ec0e 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -63,6 +63,7 @@ generic! { "ls", test_ls; "mkdir", test_mkdir; "mktemp", test_mktemp; + "numfmt", test_numfmt; "nl", test_nl; "od", test_od; "paste", test_paste;