diff --git a/crates/nu-command/src/formats/to/text.rs b/crates/nu-command/src/formats/to/text.rs index 2736fe24e5..858c5fce29 100644 --- a/crates/nu-command/src/formats/to/text.rs +++ b/crates/nu-command/src/formats/to/text.rs @@ -1,8 +1,6 @@ use chrono_humanize::HumanTime; use nu_engine::command_prelude::*; -use nu_protocol::{ - format_duration, shell_error::io::IoError, ByteStream, Config, PipelineMetadata, -}; +use nu_protocol::{format_duration, shell_error::io::IoError, ByteStream, PipelineMetadata}; use std::io::Write; const LINE_ENDING: &str = if cfg!(target_os = "windows") { @@ -50,7 +48,6 @@ impl Command for ToText { let no_newline = call.has_flag(engine_state, stack, "no-newline")?; let serialize_types = call.has_flag(engine_state, stack, "serialize")?; let input = input.try_expand_range()?; - let config = stack.get_config(engine_state); match input { PipelineData::Empty => Ok(Value::string(String::new(), head) @@ -62,8 +59,7 @@ impl Command for ToText { Value::Record { val, .. } => !val.is_empty(), _ => false, }; - let mut str = - local_into_string(engine_state, value, LINE_ENDING, &config, serialize_types); + let mut str = local_into_string(engine_state, value, LINE_ENDING, serialize_types); if add_trailing { str.push_str(LINE_ENDING); } @@ -98,7 +94,6 @@ impl Command for ToText { &engine_state_clone, val, LINE_ENDING, - &config, serialize_types, ); write!(buf, "{str}").map_err(&from_io_error)?; @@ -113,7 +108,6 @@ impl Command for ToText { &engine_state_clone, val, LINE_ENDING, - &config, serialize_types, ); str.push_str(LINE_ENDING); @@ -163,7 +157,6 @@ fn local_into_string( engine_state: &EngineState, value: Value, separator: &str, - config: &Config, serialize_types: bool, ) -> String { let span = value.span(); @@ -171,7 +164,7 @@ fn local_into_string( Value::Bool { val, .. } => val.to_string(), Value::Int { val, .. } => val.to_string(), Value::Float { val, .. } => val.to_string(), - Value::Filesize { val, .. } => config.filesize.display(val).to_string(), + Value::Filesize { val, .. } => val.to_string(), Value::Duration { val, .. } => format_duration(val), Value::Date { val, .. } => { format!("{} ({})", val.to_rfc2822(), HumanTime::from(val)) @@ -181,7 +174,7 @@ fn local_into_string( Value::Glob { val, .. } => val, Value::List { vals: val, .. } => val .into_iter() - .map(|x| local_into_string(engine_state, x, ", ", config, serialize_types)) + .map(|x| local_into_string(engine_state, x, ", ", serialize_types)) .collect::>() .join(separator), Value::Record { val, .. } => val @@ -191,7 +184,7 @@ fn local_into_string( format!( "{}: {}", x, - local_into_string(engine_state, y, ", ", config, serialize_types) + local_into_string(engine_state, y, ", ", serialize_types) ) }) .collect::>() @@ -221,7 +214,7 @@ fn local_into_string( // that critical here Value::Custom { val, .. } => val .to_base_value(span) - .map(|val| local_into_string(engine_state, val, separator, config, serialize_types)) + .map(|val| local_into_string(engine_state, val, separator, serialize_types)) .unwrap_or_else(|_| format!("<{}>", val.type_name())), } } diff --git a/crates/nu-command/src/random/binary.rs b/crates/nu-command/src/random/binary.rs index 2ef67e0f18..726b1000d7 100644 --- a/crates/nu-command/src/random/binary.rs +++ b/crates/nu-command/src/random/binary.rs @@ -46,7 +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.display(val).to_string(), + actual: engine_state.get_config().filesize.format(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 409d5e5c37..975f0f0c76 100644 --- a/crates/nu-command/src/random/chars.rs +++ b/crates/nu-command/src/random/chars.rs @@ -83,7 +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.display(val).to_string(), + actual: engine_state.get_config().filesize.format(val).to_string(), span: length_val.span(), }) } diff --git a/crates/nu-command/src/strings/format/filesize.rs b/crates/nu-command/src/strings/format/filesize.rs index 77c45fcac0..f5128031d6 100644 --- a/crates/nu-command/src/strings/format/filesize.rs +++ b/crates/nu-command/src/strings/format/filesize.rs @@ -1,9 +1,9 @@ use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; -use nu_protocol::{engine::StateWorkingSet, FilesizeUnit}; +use nu_protocol::{engine::StateWorkingSet, FilesizeFormatter, FilesizeUnit}; struct Arguments { - format: FilesizeUnit, + unit: FilesizeUnit, cell_paths: Option>, } @@ -61,10 +61,10 @@ impl Command for FormatFilesize { call: &Call, input: PipelineData, ) -> Result { - let format = parse_filesize_unit(call.req::>(engine_state, stack, 0)?)?; + let unit = parse_filesize_unit(call.req::>(engine_state, stack, 0)?)?; let cell_paths: Vec = call.rest(engine_state, stack, 1)?; let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); - let arg = Arguments { format, cell_paths }; + let arg = Arguments { unit, cell_paths }; operate( format_value_impl, arg, @@ -80,10 +80,10 @@ impl Command for FormatFilesize { call: &Call, input: PipelineData, ) -> Result { - let format = parse_filesize_unit(call.req_const::>(working_set, 0)?)?; + let unit = parse_filesize_unit(call.req_const::>(working_set, 0)?)?; let cell_paths: Vec = call.rest_const(working_set, 1)?; let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); - let arg = Arguments { format, cell_paths }; + let arg = Arguments { unit, cell_paths }; operate( format_value_impl, arg, @@ -127,7 +127,11 @@ fn parse_filesize_unit(format: Spanned) -> Result Value { let value_span = val.span(); match val { - Value::Filesize { val, .. } => Value::string(val.display(arg.format).to_string(), span), + Value::Filesize { val, .. } => FilesizeFormatter::new() + .unit(arg.unit) + .format(*val) + .to_string() + .into_value(span), Value::Error { .. } => val.clone(), _ => Value::error( ShellError::OnlySupportsThisInputType { diff --git a/crates/nu-protocol/src/config/filesize.rs b/crates/nu-protocol/src/config/filesize.rs index 75f27961c1..863f9d8293 100644 --- a/crates/nu-protocol/src/config/filesize.rs +++ b/crates/nu-protocol/src/config/filesize.rs @@ -1,74 +1,47 @@ -use super::{config_update_string_enum, prelude::*}; -use crate::{DisplayFilesize, Filesize, FilesizeUnit}; +use super::prelude::*; +use crate::{Filesize, FilesizeFormatter, FilesizeUnitFormat, FormattedFilesize}; +use nu_utils::get_system_locale; -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum FilesizeFormatUnit { - Metric, - Binary, - Unit(FilesizeUnit), -} - -impl From for FilesizeFormatUnit { - fn from(unit: FilesizeUnit) -> Self { - Self::Unit(unit) - } -} - -impl FromStr for FilesizeFormatUnit { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "metric" => Ok(Self::Metric), - "binary" => Ok(Self::Binary), - _ => { - if let Ok(unit) = s.parse() { - Ok(Self::Unit(unit)) - } else { - Err("'metric', 'binary', 'B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', or 'EiB'") - } - } - } - } -} - -impl IntoValue for FilesizeFormatUnit { +impl IntoValue for FilesizeUnitFormat { fn into_value(self, span: Span) -> Value { - match self { - FilesizeFormatUnit::Metric => "metric", - FilesizeFormatUnit::Binary => "binary", - FilesizeFormatUnit::Unit(unit) => unit.as_str(), - } - .into_value(span) + self.as_str().into_value(span) } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct FilesizeConfig { - pub unit: FilesizeFormatUnit, + pub unit: FilesizeUnitFormat, pub precision: Option, } impl FilesizeConfig { - pub fn display(&self, filesize: Filesize) -> DisplayFilesize { - let unit = match self.unit { - FilesizeFormatUnit::Metric => filesize.largest_metric_unit(), - FilesizeFormatUnit::Binary => filesize.largest_binary_unit(), - FilesizeFormatUnit::Unit(unit) => unit, - }; - filesize.display(unit).precision(self.precision) + pub fn formatter(&self) -> FilesizeFormatter { + FilesizeFormatter::new() + .unit(self.unit) + .precision(self.precision) + .locale(get_system_locale()) // TODO: cache this somewhere or pass in as argument + } + + pub fn format(&self, filesize: Filesize) -> FormattedFilesize { + self.formatter().format(filesize) } } impl Default for FilesizeConfig { fn default() -> Self { Self { - unit: FilesizeFormatUnit::Metric, + unit: FilesizeUnitFormat::Metric, precision: Some(1), } } } +impl From for FilesizeFormatter { + fn from(config: FilesizeConfig) -> Self { + config.formatter() + } +} + impl UpdateFromValue for FilesizeConfig { fn update<'a>( &mut self, @@ -84,17 +57,22 @@ impl UpdateFromValue for FilesizeConfig { for (col, val) in record.iter() { let path = &mut path.push(col); match col.as_str() { - "unit" => config_update_string_enum(&mut self.unit, val, path, errors), + "unit" => { + if let Ok(str) = val.as_str() { + match str.parse() { + Ok(unit) => self.unit = unit, + Err(_) => errors.invalid_value(path, "'metric', 'binary', 'B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', or 'EiB'", val), + } + } else { + errors.type_mismatch(path, Type::String, val) + } + } "precision" => match *val { Value::Nothing { .. } => self.precision = None, Value::Int { val, .. } if val >= 0 => self.precision = Some(val as usize), Value::Int { .. } => errors.invalid_value(path, "a non-negative integer", val), _ => errors.type_mismatch(path, Type::custom("int or nothing"), val), }, - "format" | "metric" => { - // TODO: remove after next release - errors.deprecated_option(path, "set $env.config.filesize.unit", val.span()) - } _ => errors.unknown_option(path, val), } } diff --git a/crates/nu-protocol/src/value/filesize.rs b/crates/nu-protocol/src/value/filesize.rs index f1cc97430b..cf748289de 100644 --- a/crates/nu-protocol/src/value/filesize.rs +++ b/crates/nu-protocol/src/value/filesize.rs @@ -1,7 +1,9 @@ use crate::{FromValue, IntoValue, ShellError, Span, Type, Value}; +use num_format::{Locale, WriteFormatted}; use serde::{Deserialize, Serialize}; use std::{ - fmt, + char, + fmt::{self, Write}, iter::Sum, ops::{Add, Mul, Neg, Sub}, str::FromStr, @@ -137,34 +139,6 @@ impl Filesize { EIB.. => FilesizeUnit::EiB, } } - - /// Returns a struct that can be used to display a [`Filesize`] scaled to the given - /// [`FilesizeUnit`]. - /// - /// You can use [`largest_binary_unit`](Filesize::largest_binary_unit) or - /// [`largest_metric_unit`](Filesize::largest_metric_unit) to automatically determine a - /// [`FilesizeUnit`] of appropriate scale for a specific [`Filesize`]. - /// - /// The default [`Display`](fmt::Display) implementation for [`Filesize`] is - /// `self.display(self.largest_metric_unit())`. - /// - /// # Examples - /// ``` - /// # use nu_protocol::{Filesize, FilesizeUnit}; - /// let filesize = Filesize::from_unit(4, FilesizeUnit::KiB).unwrap(); - /// - /// assert_eq!(filesize.display(FilesizeUnit::B).to_string(), "4096 B"); - /// assert_eq!(filesize.display(FilesizeUnit::KiB).to_string(), "4 KiB"); - /// assert_eq!(filesize.display(filesize.largest_binary_unit()).to_string(), "4 KiB"); - /// assert_eq!(filesize.display(filesize.largest_metric_unit()).to_string(), "4.096 kB"); - /// ``` - pub fn display(&self, unit: FilesizeUnit) -> DisplayFilesize { - DisplayFilesize { - filesize: *self, - unit, - precision: None, - } - } } impl From for Filesize { @@ -359,7 +333,7 @@ impl Sum for Option { impl fmt::Display for Filesize { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.display(self.largest_metric_unit())) + write!(f, "{}", FilesizeFormatter::new().format(*self)) } } @@ -368,9 +342,8 @@ impl fmt::Display for Filesize { /// This type contains both units with metric (SI) prefixes which are powers of 10 (e.g., kB = 1000 bytes) /// and units with binary prefixes which are powers of 2 (e.g., KiB = 1024 bytes). /// -/// The number of bytes in a [`FilesizeUnit`] can be obtained using -/// [`as_bytes`](FilesizeUnit::as_bytes). -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +/// The number of bytes in a [`FilesizeUnit`] can be obtained using [`as_bytes`](Self::as_bytes). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum FilesizeUnit { /// One byte B, @@ -432,12 +405,16 @@ impl FilesizeUnit { /// The symbol is exactly the same as the enum case name in Rust code except for /// [`FilesizeUnit::KB`] which is `kB`. /// + /// The returned string is the same exact string needed for a successful call to + /// [`parse`](str::parse) for a [`FilesizeUnit`]. + /// /// # Examples /// ``` /// # use nu_protocol::FilesizeUnit; /// assert_eq!(FilesizeUnit::B.as_str(), "B"); /// assert_eq!(FilesizeUnit::KB.as_str(), "kB"); /// assert_eq!(FilesizeUnit::KiB.as_str(), "KiB"); + /// assert_eq!(FilesizeUnit::KB.as_str().parse(), Ok(FilesizeUnit::KB)); /// ``` pub const fn as_str(&self) -> &'static str { match self { @@ -484,6 +461,12 @@ impl From for Filesize { } } +impl fmt::Display for FilesizeUnit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + /// The error returned when failing to parse a [`FilesizeUnit`]. /// /// This occurs when the string being parsed does not exactly match the name of one of the @@ -520,93 +503,344 @@ impl FromStr for FilesizeUnit { } } -impl fmt::Display for FilesizeUnit { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.as_str().fmt(f) +/// The different file size unit display formats for a [`FilesizeFormatter`]. +/// +/// To see more information about each possible format, see the documentation for each of the enum +/// cases of [`FilesizeUnitFormat`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum FilesizeUnitFormat { + /// [`Metric`](Self::Metric) will make a [`FilesizeFormatter`] use the + /// [`largest_metric_unit`](Filesize::largest_metric_unit) of a [`Filesize`] when formatting it. + Metric, + /// [`Binary`](Self::Binary) will make a [`FilesizeFormatter`] use the + /// [`largest_binary_unit`](Filesize::largest_binary_unit) of a [`Filesize`] when formatting it. + Binary, + /// [`FilesizeUnitFormat::Unit`] will make a [`FilesizeFormatter`] use the provided + /// [`FilesizeUnit`] when formatting all [`Filesize`]s. + Unit(FilesizeUnit), +} + +impl FilesizeUnitFormat { + /// Returns a string representation of a [`FilesizeUnitFormat`]. + /// + /// The returned string is the same exact string needed for a successful call to + /// [`parse`](str::parse) for a [`FilesizeUnitFormat`]. + /// + /// # Examples + /// ``` + /// # use nu_protocol::{FilesizeUnit, FilesizeUnitFormat}; + /// assert_eq!(FilesizeUnitFormat::Metric.as_str(), "metric"); + /// assert_eq!(FilesizeUnitFormat::Binary.as_str(), "binary"); + /// assert_eq!(FilesizeUnitFormat::Unit(FilesizeUnit::KB).as_str(), "kB"); + /// assert_eq!(FilesizeUnitFormat::Metric.as_str().parse(), Ok(FilesizeUnitFormat::Metric)); + /// ``` + pub const fn as_str(&self) -> &'static str { + match self { + Self::Metric => "metric", + Self::Binary => "binary", + Self::Unit(unit) => unit.as_str(), + } + } + + /// Returns `true` for [`DisplayFilesizeUnit::Metric`] or if the underlying [`FilesizeUnit`] + /// is metric according to [`FilesizeUnit::is_metric`]. + /// + /// Note that this returns `true` for [`FilesizeUnit::B`] as well. + pub const fn is_metric(&self) -> bool { + match self { + Self::Metric => true, + Self::Binary => false, + Self::Unit(unit) => unit.is_metric(), + } + } + + /// Returns `true` for [`DisplayFilesizeUnit::Binary`] or if the underlying [`FilesizeUnit`] + /// is binary according to [`FilesizeUnit::is_binary`]. + /// + /// Note that this returns `true` for [`FilesizeUnit::B`] as well. + pub const fn is_binary(&self) -> bool { + match self { + Self::Metric => false, + Self::Binary => true, + Self::Unit(unit) => unit.is_binary(), + } } } -#[derive(Debug)] -pub struct DisplayFilesize { - filesize: Filesize, - unit: FilesizeUnit, - precision: Option, +impl From for FilesizeUnitFormat { + fn from(unit: FilesizeUnit) -> Self { + Self::Unit(unit) + } } -impl DisplayFilesize { +impl fmt::Display for FilesizeUnitFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// The error returned when failing to parse a [`DisplayFilesizeUnit`]. +/// +/// This occurs when the string being parsed does not exactly match any of: +/// - `metric` +/// - `binary` +/// - The name of any of the enum cases in [`FilesizeUnit`]. The exception is [`FilesizeUnit::KB`] which must be `kB`. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)] +pub struct ParseFilesizeUnitFormatError(()); + +impl fmt::Display for ParseFilesizeUnitFormatError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "invalid file size unit format") + } +} + +impl FromStr for FilesizeUnitFormat { + type Err = ParseFilesizeUnitFormatError; + + fn from_str(s: &str) -> Result { + Ok(match s { + "metric" => Self::Metric, + "binary" => Self::Binary, + s => Self::Unit(s.parse().map_err(|_| ParseFilesizeUnitFormatError(()))?), + }) + } +} + +/// A configurable formatter for [`Filesize`]s. +/// +/// [`FilesizeFormatter`] is a builder struct that you can modify via the following methods: +/// - [`unit`](Self::unit) +/// - [`precision`](Self::precision) +/// - [`locale`](Self::locale) +/// +/// For more information, see the documentation for each of those methods. +/// +/// # Examples +/// ``` +/// # use nu_protocol::{Filesize, FilesizeFormatter, FilesizeUnit}; +/// # use num_format::Locale; +/// let filesize = Filesize::from_unit(4, FilesizeUnit::KiB).unwrap(); +/// let formatter = FilesizeFormatter::new(); +/// +/// assert_eq!(formatter.unit(FilesizeUnit::B).format(filesize).to_string(), "4096 B"); +/// assert_eq!(formatter.unit(FilesizeUnit::KiB).format(filesize).to_string(), "4 KiB"); +/// assert_eq!(formatter.precision(2).format(filesize).to_string(), "4.09 kB"); +/// assert_eq!( +/// formatter +/// .unit(FilesizeUnit::B) +/// .locale(Locale::en) +/// .format(filesize) +/// .to_string(), +/// "4,096 B", +/// ); +/// ``` +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct FilesizeFormatter { + unit: FilesizeUnitFormat, + precision: Option, + locale: Locale, +} + +impl FilesizeFormatter { + /// Create a new, default [`FilesizeFormatter`]. + /// + /// The default formatter has: + /// - a [`unit`](Self::unit) of [`FilesizeUnitFormat::Metric`]. + /// - a [`precision`](Self::precision) of `None`. + /// - a [`locale`](Self::locale) of [`Locale::en_US_POSIX`] + /// (a very plain format with no thousands separators). + pub fn new() -> Self { + FilesizeFormatter { + unit: FilesizeUnitFormat::Metric, + precision: None, + locale: Locale::en_US_POSIX, + } + } + + /// Set the [`FilesizeUnitFormat`] used by the formatter. + /// + /// A [`FilesizeUnit`] or a [`FilesizeUnitFormat`] can be provided to this method. + /// [`FilesizeUnitFormat::Metric`] and [`FilesizeUnitFormat::Binary`] will use a unit of an + /// appropriate scale for each [`Filesize`], whereas providing a [`FilesizeUnit`] will use that + /// unit to format all [`Filesize`]s. + /// + /// # Examples + /// ``` + /// # use nu_protocol::{Filesize, FilesizeFormatter, FilesizeUnit, FilesizeUnitFormat}; + /// let formatter = FilesizeFormatter::new().precision(1); + /// + /// let filesize = Filesize::from_unit(4, FilesizeUnit::KiB).unwrap(); + /// assert_eq!(formatter.unit(FilesizeUnit::B).format(filesize).to_string(), "4096 B"); + /// assert_eq!(formatter.unit(FilesizeUnitFormat::Binary).format(filesize).to_string(), "4.0 KiB"); + /// + /// let filesize = Filesize::from_unit(4, FilesizeUnit::MiB).unwrap(); + /// assert_eq!(formatter.unit(FilesizeUnitFormat::Metric).format(filesize).to_string(), "4.1 MB"); + /// assert_eq!(formatter.unit(FilesizeUnitFormat::Binary).format(filesize).to_string(), "4.0 MiB"); + /// ``` + pub fn unit(mut self, unit: impl Into) -> Self { + self.unit = unit.into(); + self + } + + /// Set the number of digits to display after the decimal place. + /// + /// Note that digits after the decimal place will never be shown if: + /// - [`unit`](Self::unit) is [`FilesizeUnit::B`], + /// - [`unit`](Self::unit) is [`FilesizeUnitFormat::Metric`] and the number of bytes + /// is less than [`FilesizeUnit::KB`] + /// - [`unit`](Self::unit) is [`FilesizeUnitFormat::Binary`] and the number of bytes + /// is less than [`FilesizeUnit::KiB`]. + /// + /// Additionally, the precision specified in the format string + /// (i.e., [`std::fmt::Formatter::precision`]) will take precedence if is specified. + /// If the format string precision and the [`FilesizeFormatter`]'s precision are both `None`, + /// then all digits after the decimal place, if any, are shown. + /// + /// # Examples + /// ``` + /// # use nu_protocol::{Filesize, FilesizeFormatter, FilesizeUnit, FilesizeUnitFormat}; + /// let filesize = Filesize::from_unit(4, FilesizeUnit::KiB).unwrap(); + /// let formatter = FilesizeFormatter::new(); + /// + /// assert_eq!(formatter.precision(2).format(filesize).to_string(), "4.09 kB"); + /// assert_eq!(formatter.precision(0).format(filesize).to_string(), "4 kB"); + /// assert_eq!(formatter.precision(None).format(filesize).to_string(), "4.096 kB"); + /// assert_eq!( + /// formatter + /// .precision(None) + /// .unit(FilesizeUnit::KiB) + /// .format(filesize) + /// .to_string(), + /// "4 KiB", + /// ); + /// assert_eq!( + /// formatter + /// .unit(FilesizeUnit::B) + /// .precision(2) + /// .format(filesize) + /// .to_string(), + /// "4096 B", + /// ); + /// assert_eq!(format!("{:.2}", formatter.precision(0).format(filesize)), "4.09 kB"); + /// ``` pub fn precision(mut self, precision: impl Into>) -> Self { self.precision = precision.into(); self } + + /// Set the [`Locale`] to use when formatting the numeric portion of a [`Filesize`]. + /// + /// The [`Locale`] determines the decimal place character, minus sign character, + /// digit grouping method, and digit separator character. + /// + /// # Examples + /// ``` + /// # use nu_protocol::{Filesize, FilesizeFormatter, FilesizeUnit, FilesizeUnitFormat}; + /// # use num_format::Locale; + /// let filesize = Filesize::from_unit(-4, FilesizeUnit::MiB).unwrap(); + /// let formatter = FilesizeFormatter::new().unit(FilesizeUnit::KB).precision(1); + /// + /// assert_eq!(formatter.format(filesize).to_string(), "-4194.3 kB"); + /// assert_eq!(formatter.locale(Locale::en).format(filesize).to_string(), "-4,194.3 kB"); + /// assert_eq!(formatter.locale(Locale::rm).format(filesize).to_string(), "\u{2212}4’194.3 kB"); + /// let filesize = Filesize::from_unit(-4, FilesizeUnit::GiB).unwrap(); + /// assert_eq!(formatter.locale(Locale::ta).format(filesize).to_string(), "-42,94,967.2 kB"); + /// ``` + pub fn locale(mut self, locale: Locale) -> Self { + self.locale = locale; + self + } + + /// Format a [`Filesize`] into a [`FormattedFilesize`] which implements [`fmt::Display`]. + /// + /// # Examples + /// ``` + /// # use nu_protocol::{Filesize, FilesizeFormatter, FilesizeUnit}; + /// let filesize = Filesize::from_unit(4, FilesizeUnit::KB).unwrap(); + /// let formatter = FilesizeFormatter::new(); + /// + /// assert_eq!(format!("{}", formatter.format(filesize)), "4 kB"); + /// assert_eq!(formatter.format(filesize).to_string(), "4 kB"); + /// ``` + pub fn format(&self, filesize: Filesize) -> FormattedFilesize { + FormattedFilesize { + format: *self, + filesize, + } + } } -impl fmt::Display for DisplayFilesize { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - filesize: Filesize(filesize), +impl Default for FilesizeFormatter { + fn default() -> Self { + Self::new() + } +} + +/// The resulting struct from calling [`FilesizeFormatter::format`] on a [`Filesize`]. +/// +/// The only purpose of this struct is to implement [`fmt::Display`]. +#[derive(Debug, Clone)] +pub struct FormattedFilesize { + format: FilesizeFormatter, + filesize: Filesize, +} + +impl fmt::Display for FormattedFilesize { + fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { filesize, format } = *self; + let FilesizeFormatter { unit, precision, - } = *self; - let precision = precision.or(f.precision()); - match unit { - FilesizeUnit::B => write!(f, "{filesize} B"), - FilesizeUnit::KiB - | FilesizeUnit::MiB - | FilesizeUnit::GiB - | FilesizeUnit::TiB - | FilesizeUnit::PiB - | FilesizeUnit::EiB => { - // This won't give exact results for large filesizes and/or units. - let val = filesize as f64 / unit.as_bytes() as f64; - if let Some(precision) = precision { - write!(f, "{val:.precision$} {unit}") - } else { - write!(f, "{val} {unit}") + locale, + } = format; + let unit = match unit { + FilesizeUnitFormat::Metric => filesize.largest_metric_unit(), + FilesizeUnitFormat::Binary => filesize.largest_binary_unit(), + FilesizeUnitFormat::Unit(unit) => unit, + }; + let Filesize(filesize) = filesize; + let precision = f.precision().or(precision); + + let bytes = unit.as_bytes() as i64; + let whole = filesize / bytes; + let fract = (filesize % bytes).unsigned_abs(); + + f.write_formatted(&whole, &locale) + .map_err(|_| std::fmt::Error)?; + + if unit != FilesizeUnit::B && precision != Some(0) && !(precision.is_none() && fract == 0) { + f.write_str(locale.decimal())?; + + let bytes = unit.as_bytes(); + let mut fract = fract * 10; + let mut i = 0; + loop { + let q = fract / bytes; + let r = fract % bytes; + // Quick soundness proof: + // r <= bytes by definition of remainder `%` + // => 10 * r <= 10 * bytes + // => fract <= 10 * bytes before next iteration, fract = r * 10 + // => fract / bytes <= 10 + // => q <= 10 next iteration, q = fract / bytes + debug_assert!(q <= 10); + f.write_char(char::from_digit(q as u32, 10).expect("q <= 10"))?; + i += 1; + if r == 0 || precision.is_some_and(|p| i >= p) { + break; } + fract = r * 10; } - FilesizeUnit::KB - | FilesizeUnit::GB - | FilesizeUnit::MB - | FilesizeUnit::TB - | FilesizeUnit::PB - | FilesizeUnit::EB => { - // Format an exact, possibly fractional, string representation of `filesize`. - let bytes = unit.as_bytes() as i64; - let whole = filesize / bytes; - let fract = (filesize % bytes).unsigned_abs(); - if precision == Some(0) || (precision.is_none() && fract == 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]; - let stop = precision.unwrap_or(usize::MAX).min(buf.len()); - let bytes = bytes.unsigned_abs(); - let mut fract = fract * 10; - let mut i = 0; - loop { - let q = fract / bytes; - let r = fract % bytes; - debug_assert!(q < 10); - buf[i] += q as u8; - i += 1; - if r == 0 || i >= stop { - break; - } - fract = r * 10; - } - // Safety: all the characters in `buf` are valid UTF-8. - let fract = unsafe { std::str::from_utf8_unchecked(&buf[..i]) }; - - 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.display(*val).to_string(), + Value::Filesize { val, .. } => config.filesize.format(*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/locale.rs b/crates/nu-utils/src/locale.rs index f4979ecf58..663756814b 100644 --- a/crates/nu-utils/src/locale.rs +++ b/crates/nu-utils/src/locale.rs @@ -22,9 +22,16 @@ pub fn get_system_locale() -> Locale { #[cfg(debug_assertions)] pub fn get_system_locale_string() -> Option { - std::env::var(LOCALE_OVERRIDE_ENV_VAR) - .ok() - .or_else(sys_locale::get_locale) + std::env::var(LOCALE_OVERRIDE_ENV_VAR).ok().or_else( + #[cfg(not(test))] + { + sys_locale::get_locale + }, + #[cfg(test)] + { + || Some(Locale::en_US_POSIX.name().to_owned()) + }, + ) } #[cfg(not(debug_assertions))]