small refactoring around units and add tests (#15746)

Closes #14469

# Description
- ~~Implement the ``--unit`` conversion in "into int" command~~
- New ``ShellError::InvalidUnit`` unit if users enter wrong units
- Made ``ShellError::CantConvertToDuration`` more generic: became
``CantConvertToUnit``
- Tried to improve the way we parse units and get the supported units.
It's not complete, though, I will continue this refactoring in another
PR. But I already did some small refactorings in the "format duration"
and "format filesize" commands
- Add tests for "format filesize" and "format duration"

# User-Facing Changes

```nu
~> 1MB | format filesize sec
Error: nu:🐚:invalid_unit

  × Invalid unit
   ╭─[entry #7:1:23]
 1 │ 1MB | format filesize sec
   ·                       ─┬─
   ·                        ╰── encountered here
   ╰────
  help: Supported units are: B, kB, MB, GB, TB, PB, EB, KiB, MiB, GiB, TiB, PiB, EiB

```
This commit is contained in:
Loïc Riegel
2025-05-17 00:41:26 +02:00
committed by GitHub
parent 70ba5d9d68
commit 58a8f30a25
16 changed files with 258 additions and 107 deletions

View File

@ -13,7 +13,7 @@ mod pipeline;
mod range;
mod table;
mod traverse;
mod unit;
pub mod unit;
mod value_with_unit;
pub use attribute::*;

View File

@ -1,5 +1,24 @@
use crate::{Filesize, FilesizeUnit, IntoValue, ShellError, Span, Value};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
pub const SUPPORTED_DURATION_UNITS: [&str; 9] =
["ns", "us", "µs", "ms", "sec", "min", "hr", "day", "wk"];
/// The error returned when failing to parse a [`Unit`].
///
/// This occurs when the string being parsed does not exactly match the name of one of the
/// enum cases in [`Unit`].
#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
pub struct ParseUnitError(());
impl fmt::Display for ParseUnitError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "invalid file size or duration unit")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Unit {
@ -78,4 +97,53 @@ impl Unit {
},
}
}
/// Returns the symbol [`str`] for a [`Unit`].
///
/// The returned string is the same exact string needed for a successful call to
/// [`parse`](str::parse) for a [`Unit`].
///
/// # Examples
/// ```
/// # use nu_protocol::{Unit, FilesizeUnit};
/// assert_eq!(Unit::Nanosecond.as_str(), "ns");
/// assert_eq!(Unit::Filesize(FilesizeUnit::B).as_str(), "B");
/// assert_eq!(Unit::Second.as_str().parse(), Ok(Unit::Second));
/// assert_eq!(Unit::Filesize(FilesizeUnit::KB).as_str().parse(), Ok(Unit::Filesize(FilesizeUnit::KB)));
/// ```
pub const fn as_str(&self) -> &'static str {
match self {
Unit::Filesize(u) => u.as_str(),
Unit::Nanosecond => "ns",
Unit::Microsecond => "us",
Unit::Millisecond => "ms",
Unit::Second => "sec",
Unit::Minute => "min",
Unit::Hour => "hr",
Unit::Day => "day",
Unit::Week => "wk",
}
}
}
impl FromStr for Unit {
type Err = ParseUnitError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(filesize_unit) = FilesizeUnit::from_str(s) {
return Ok(Unit::Filesize(filesize_unit));
};
match s {
"ns" => Ok(Unit::Nanosecond),
"us" | "µs" => Ok(Unit::Microsecond),
"ms" => Ok(Unit::Millisecond),
"sec" => Ok(Unit::Second),
"min" => Ok(Unit::Minute),
"hr" => Ok(Unit::Hour),
"day" => Ok(Unit::Day),
"wk" => Ok(Unit::Week),
_ => Err(ParseUnitError(())),
}
}
}

View File

@ -76,7 +76,7 @@ pub enum ShellError {
exp_input_type: String,
#[label("expected: {exp_input_type}")]
dst_span: Span,
#[label("value originates from here")]
#[label("value originates here")]
src_span: Span,
},
@ -431,14 +431,20 @@ pub enum ShellError {
help: Option<String>,
},
#[error("Can't convert string `{details}` to duration.")]
#[diagnostic(code(nu::shell::cant_convert_with_value))]
CantConvertToDuration {
details: String,
#[label("can't be converted to duration")]
dst_span: Span,
#[label("this string value...")]
src_span: Span,
/// Failed to convert a value of one type into a different type by specifying a unit.
///
/// ## Resolution
///
/// Check that the provided value can be converted in the provided: only Durations can be converted to duration units, and only Filesize can be converted to filesize units.
#[error("Can't convert {from_type} to the specified unit.")]
#[diagnostic(code(nu::shell::cant_convert_value_to_unit))]
CantConvertToUnit {
to_type: String,
from_type: String,
#[label("can't convert {from_type} to {to_type}")]
span: Span,
#[label("conversion originates here")]
unit_span: Span,
#[help]
help: Option<String>,
},
@ -1238,6 +1244,22 @@ This is an internal Nushell error, please file an issue https://github.com/nushe
span: Span,
},
/// Invalid unit
///
/// ## Resolution
///
/// Correct unit
#[error("Invalid unit")]
#[diagnostic(
code(nu::shell::invalid_unit),
help("Supported units are: {supported_units}")
)]
InvalidUnit {
supported_units: String,
#[label("encountered here")]
span: Span,
},
/// Tried spreading a non-list inside a list or command call.
///
/// ## Resolution

View File

@ -27,7 +27,7 @@ mod ty;
mod value;
pub use alias::*;
pub use ast::Unit;
pub use ast::unit::*;
pub use config::*;
pub use did_you_mean::did_you_mean;
pub use engine::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID};

View File

@ -10,6 +10,10 @@ use std::{
};
use thiserror::Error;
pub const SUPPORTED_FILESIZE_UNITS: [&str; 13] = [
"B", "kB", "MB", "GB", "TB", "PB", "EB", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB",
];
/// A signed number of bytes.
///
/// [`Filesize`] is a wrapper around [`i64`]. Whereas [`i64`] is a dimensionless value, [`Filesize`] represents a