Skip to content

Commit

Permalink
numfmt: implement --format
Browse files Browse the repository at this point in the history
  • Loading branch information
cakebaker committed Jul 28, 2022
1 parent 4e72e28 commit ee13bc4
Show file tree
Hide file tree
Showing 5 changed files with 659 additions and 11 deletions.
93 changes: 87 additions & 6 deletions src/uu/numfmt/src/format.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// spell-checker:ignore powf
use uucore::display::Quotable;

use crate::options::{NumfmtOptions, RoundMethod, TransformOptions};
Expand Down Expand Up @@ -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();
Expand All @@ -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] {
Expand All @@ -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)),
})
Expand All @@ -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
Expand All @@ -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<()> {
Expand Down Expand Up @@ -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));
}
}
54 changes: 50 additions & 4 deletions src/uu/numfmt/src/numfmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]...";

Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -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)?;

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {}
Expand All @@ -377,6 +422,7 @@ mod tests {
delimiter: None,
round: RoundMethod::Nearest,
suffix: None,
format: FormatOptions::default(),
}
}

Expand Down
Loading

0 comments on commit ee13bc4

Please sign in to comment.