From 2c74c679b59f5b8f91c0fbf44630505c6a3bf3bd Mon Sep 17 00:00:00 2001 From: Ian Manske Date: Tue, 19 Nov 2024 19:20:00 -0800 Subject: [PATCH] Add filesize precision --- crates/nu-command/src/formats/to/text.rs | 2 +- crates/nu-command/src/random/binary.rs | 7 +-- crates/nu-command/src/random/chars.rs | 7 +-- crates/nu-protocol/src/config/filesize.rs | 39 ++++++++----- crates/nu-protocol/src/config/helper.rs | 14 +++++ crates/nu-protocol/src/value/filesize.rs | 57 +++++++++++++------ crates/nu-protocol/src/value/mod.rs | 2 +- .../src/sample_config/default_config.nu | 1 + 8 files changed, 86 insertions(+), 43 deletions(-) diff --git a/crates/nu-command/src/formats/to/text.rs b/crates/nu-command/src/formats/to/text.rs index 67858ebfdb..e4512b4ba8 100644 --- a/crates/nu-command/src/formats/to/text.rs +++ b/crates/nu-command/src/formats/to/text.rs @@ -141,7 +141,7 @@ fn local_into_string(value: Value, separator: &str, config: &Config) -> String { Value::Bool { val, .. } => val.to_string(), Value::Int { val, .. } => val.to_string(), Value::Float { val, .. } => val.to_string(), - Value::Filesize { val, .. } => config.filesize.unit.display(val).to_string(), + Value::Filesize { val, .. } => config.filesize.display(val).to_string(), Value::Duration { val, .. } => format_duration(val), Value::Date { val, .. } => { format!("{} ({})", val.to_rfc2822(), HumanTime::from(val)) diff --git a/crates/nu-command/src/random/binary.rs b/crates/nu-command/src/random/binary.rs index 8dc9be73fb..2ef67e0f18 100644 --- a/crates/nu-command/src/random/binary.rs +++ b/crates/nu-command/src/random/binary.rs @@ -46,12 +46,7 @@ impl Command for SubCommand { Value::Filesize { val, .. } => { usize::try_from(val).map_err(|_| ShellError::InvalidValue { valid: "a non-negative int or filesize".into(), - actual: engine_state - .get_config() - .filesize - .unit - .display(val) - .to_string(), + actual: engine_state.get_config().filesize.display(val).to_string(), span: length_val.span(), }) } diff --git a/crates/nu-command/src/random/chars.rs b/crates/nu-command/src/random/chars.rs index b93541a2e3..409d5e5c37 100644 --- a/crates/nu-command/src/random/chars.rs +++ b/crates/nu-command/src/random/chars.rs @@ -83,12 +83,7 @@ fn chars( Value::Filesize { val, .. } => { usize::try_from(val).map_err(|_| ShellError::InvalidValue { valid: "a non-negative int or filesize".into(), - actual: engine_state - .get_config() - .filesize - .unit - .display(val) - .to_string(), + actual: engine_state.get_config().filesize.display(val).to_string(), span: length_val.span(), }) } diff --git a/crates/nu-protocol/src/config/filesize.rs b/crates/nu-protocol/src/config/filesize.rs index 6b4c3e89c7..72ed0617ff 100644 --- a/crates/nu-protocol/src/config/filesize.rs +++ b/crates/nu-protocol/src/config/filesize.rs @@ -1,5 +1,5 @@ use super::{config_update_string_enum, prelude::*}; -use crate::{self as nu_protocol, DisplayFilesize, Filesize, FilesizeUnit}; +use crate::{DisplayFilesize, Filesize, FilesizeUnit}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum FilesizeFormatUnit { @@ -8,17 +8,6 @@ pub enum FilesizeFormatUnit { Unit(FilesizeUnit), } -impl FilesizeFormatUnit { - pub fn display(&self, filesize: Filesize) -> DisplayFilesize { - let unit = match self { - Self::Decimal => filesize.largest_decimal_unit(), - Self::Binary => filesize.largest_binary_unit(), - Self::Unit(unit) => *unit, - }; - filesize.display(unit) - } -} - impl From for FilesizeFormatUnit { fn from(unit: FilesizeUnit) -> Self { Self::Unit(unit) @@ -54,15 +43,28 @@ impl IntoValue for FilesizeFormatUnit { } } -#[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct FilesizeConfig { pub unit: FilesizeFormatUnit, + pub precision: u64, +} + +impl FilesizeConfig { + pub fn display(&self, filesize: Filesize) -> DisplayFilesize { + let unit = match self.unit { + FilesizeFormatUnit::Decimal => filesize.largest_decimal_unit(), + FilesizeFormatUnit::Binary => filesize.largest_binary_unit(), + FilesizeFormatUnit::Unit(unit) => unit, + }; + filesize.display(unit).precision(self.precision as usize) + } } impl Default for FilesizeConfig { fn default() -> Self { Self { unit: FilesizeFormatUnit::Decimal, + precision: 1, } } } @@ -83,6 +85,7 @@ impl UpdateFromValue for FilesizeConfig { let path = &mut path.push(col); match col.as_str() { "unit" => config_update_string_enum(&mut self.unit, val, path, errors), + "precision" => self.precision.update(value, path, errors), "format" | "metric" => { // TODO: remove after next release errors.deprecated_option(path, "set $env.config.filesize.unit", val.span()) @@ -92,3 +95,13 @@ impl UpdateFromValue for FilesizeConfig { } } } + +impl IntoValue for FilesizeConfig { + fn into_value(self, span: Span) -> Value { + record! { + "unit" => self.unit.into_value(span), + "precision" => (self.precision as i64).into_value(span), + } + .into_value(span) + } +} diff --git a/crates/nu-protocol/src/config/helper.rs b/crates/nu-protocol/src/config/helper.rs index e52ae6d83f..dfee350b3d 100644 --- a/crates/nu-protocol/src/config/helper.rs +++ b/crates/nu-protocol/src/config/helper.rs @@ -91,6 +91,20 @@ impl UpdateFromValue for i64 { } } +impl UpdateFromValue for u64 { + fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) { + if let Ok(val) = value.as_int() { + if let Ok(val) = val.try_into() { + *self = val; + } else { + errors.invalid_value(path, "a non-negative integer", value); + } + } else { + errors.type_mismatch(path, Type::Int, value); + } + } +} + impl UpdateFromValue for usize { fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) { if let Ok(val) = value.as_int() { diff --git a/crates/nu-protocol/src/value/filesize.rs b/crates/nu-protocol/src/value/filesize.rs index 237b3f5a34..757a6da11d 100644 --- a/crates/nu-protocol/src/value/filesize.rs +++ b/crates/nu-protocol/src/value/filesize.rs @@ -162,6 +162,7 @@ impl Filesize { DisplayFilesize { filesize: *self, unit, + precision: None, } } } @@ -529,11 +530,24 @@ impl fmt::Display for FilesizeUnit { pub struct DisplayFilesize { filesize: Filesize, unit: FilesizeUnit, + precision: Option, +} + +impl DisplayFilesize { + pub fn precision(mut self, precision: usize) -> Self { + self.precision = Some(precision); + self + } } impl fmt::Display for DisplayFilesize { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { filesize, unit } = *self; + let Self { + filesize, + unit, + precision, + } = *self; + let precision = precision.or(f.precision()); match unit { FilesizeUnit::B => write!(f, "{} B", filesize.0), FilesizeUnit::KiB @@ -542,8 +556,13 @@ impl fmt::Display for DisplayFilesize { | FilesizeUnit::TiB | FilesizeUnit::PiB | FilesizeUnit::EiB => { + let val = filesize.0 as f64 / unit.as_bytes() as f64; // This won't give exact results for large filesizes and/or units. - write!(f, "{} {unit}", filesize.0 as f64 / unit.as_bytes() as f64) + if let Some(precision) = precision { + write!(f, "{val:.precision$} {unit}") + } else { + write!(f, "{val} {unit}") + } } FilesizeUnit::KB | FilesizeUnit::GB @@ -554,31 +573,37 @@ impl fmt::Display for DisplayFilesize { // Format an exact, possibly fractional, string representation of `filesize`. let bytes = unit.as_bytes() as i64; let whole = filesize.0 / bytes; - let mut fract = (filesize.0 % bytes).unsigned_abs(); - if fract == 0 || f.precision() == Some(0) { + let fract = (filesize.0 % bytes).unsigned_abs(); + if fract == 0 || precision == Some(0) { write!(f, "{whole} {unit}") } else { // fract <= bytes by nature of `%` and bytes <= EB = 10 ^ 18 // So, the longest string for the fractional portion can be 18 characters. let buf = &mut [b'0'; 18]; - for d in buf.iter_mut().rev() { - *d += (fract % 10) as u8; - fract /= 10; - if fract == 0 { + let stop = precision.unwrap_or(buf.len()); + let mut fract = fract; + let mut power = bytes.unsigned_abs() / 10; + let mut i = 0; + loop { + let q = fract / power; + let r = fract % power; + debug_assert!(q < 10); + buf[i] += q as u8; + i += 1; + if r == 0 || i >= stop { break; } + fract = r; + power /= 10; } - let power = bytes.ilog10() as usize; - debug_assert_eq!(bytes, 10_i64.pow(power as u32), "an exact power of 10"); // Safety: all the characters in `buf` are valid UTF-8. - let fract = - unsafe { std::str::from_utf8_unchecked(&buf[(buf.len() - power)..]) }; + let fract = unsafe { std::str::from_utf8_unchecked(&buf[..i]) }; - match f.precision() { - Some(p) if p <= power => write!(f, "{whole}.{} {unit}", &fract[..p]), - Some(p) => write!(f, "{whole}.{fract:0 write!(f, "{whole}.{} {unit}", fract.trim_end_matches('0')), + if let Some(p) = precision { + write!(f, "{whole}.{fract:0 val.to_string(), Value::Int { val, .. } => val.to_string(), Value::Float { val, .. } => val.to_string(), - Value::Filesize { val, .. } => config.filesize.unit.display(*val).to_string(), + Value::Filesize { val, .. } => config.filesize.display(*val).to_string(), Value::Duration { val, .. } => format_duration(*val), Value::Date { val, .. } => match &config.datetime_format.normal { Some(format) => self.format_datetime(val, format), diff --git a/crates/nu-utils/src/sample_config/default_config.nu b/crates/nu-utils/src/sample_config/default_config.nu index fec7ba1cf7..0f5262ead5 100644 --- a/crates/nu-utils/src/sample_config/default_config.nu +++ b/crates/nu-utils/src/sample_config/default_config.nu @@ -231,6 +231,7 @@ $env.config = { # You can also set this to a particular unit to use that unit to display all file sizes. # The available units are: B, kB, KiB, MB, MiB, GB, GiB, TB, TiB, PB, PiB, EB, or EiB. unit: decimal + precision: 1 # the number of digits to display after the decimal point for file sizes } cursor_shape: {