mirror of
https://github.com/nushell/nushell.git
synced 2025-04-02 12:19:48 +02:00
We only have one valid `datetime` type, but the string representation of that type was `date`. This PR updates the string representation of the `datetime` type to be `datetime` and updates other affected dependencies: * A `describe` example that used `date` * The style computer automatically recognized the new change, but also changed the default `date: purple` to `datetime: purple`. * Likewise, changed the `default_config.nu` to populate `$env.config.color_config.datetime` * Likewise, the dark and light themes in `std/config` * Updates tests * Unrelated, but changed the `into value` error messages to use *"datetime"* if there's an issue. Fixes #9916 and perhaps others. ## Breaking Changes: * Code that expected `describe` to return a `date` will now return a `datetime` * User configs and themes that override `$env.config.color_config.date` will need to be updated to use `datetime`
526 lines
19 KiB
Rust
526 lines
19 KiB
Rust
use crate::parse_date_from_string;
|
|
use fancy_regex::{Regex, RegexBuilder};
|
|
use nu_engine::command_prelude::*;
|
|
use nu_protocol::PipelineIterator;
|
|
use std::collections::HashSet;
|
|
use std::sync::LazyLock;
|
|
|
|
#[derive(Clone)]
|
|
pub struct IntoValue;
|
|
|
|
impl Command for IntoValue {
|
|
fn name(&self) -> &str {
|
|
"into value"
|
|
}
|
|
|
|
fn signature(&self) -> Signature {
|
|
Signature::build("into value")
|
|
.input_output_types(vec![(Type::table(), Type::table())])
|
|
.named(
|
|
"columns",
|
|
SyntaxShape::List(Box::new(SyntaxShape::Any)),
|
|
"list of columns to update",
|
|
Some('c'),
|
|
)
|
|
.switch(
|
|
"prefer-filesizes",
|
|
"For ints display them as human-readable file sizes",
|
|
Some('f'),
|
|
)
|
|
.allow_variants_without_examples(true)
|
|
.category(Category::Filters)
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
"Infer Nushell datatype for each cell."
|
|
}
|
|
|
|
fn search_terms(&self) -> Vec<&str> {
|
|
vec!["convert", "conversion"]
|
|
}
|
|
|
|
fn examples(&self) -> Vec<Example> {
|
|
vec![
|
|
Example {
|
|
description: "Infer Nushell values for each cell.",
|
|
example: "$table | into value",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Infer Nushell values for each cell in the given columns.",
|
|
example: "$table | into value -c [column1, column5]",
|
|
result: None,
|
|
},
|
|
]
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
input: PipelineData,
|
|
) -> Result<PipelineData, ShellError> {
|
|
let metadata = input.metadata();
|
|
let span = call.head;
|
|
let display_as_filesizes = call.has_flag(engine_state, stack, "prefer-filesizes")?;
|
|
|
|
// the columns to update
|
|
let columns: Option<Value> = call.get_flag(engine_state, stack, "columns")?;
|
|
let columns: Option<HashSet<String>> = match columns {
|
|
Some(val) => Some(
|
|
val.into_list()?
|
|
.into_iter()
|
|
.map(Value::coerce_into_string)
|
|
.collect::<Result<HashSet<String>, ShellError>>()?,
|
|
),
|
|
None => None,
|
|
};
|
|
|
|
Ok(UpdateCellIterator {
|
|
input: input.into_iter(),
|
|
columns,
|
|
display_as_filesizes,
|
|
span,
|
|
}
|
|
.into_pipeline_data(span, engine_state.signals().clone())
|
|
.set_metadata(metadata))
|
|
}
|
|
}
|
|
|
|
struct UpdateCellIterator {
|
|
input: PipelineIterator,
|
|
columns: Option<HashSet<String>>,
|
|
display_as_filesizes: bool,
|
|
span: Span,
|
|
}
|
|
|
|
impl Iterator for UpdateCellIterator {
|
|
type Item = Value;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
match self.input.next() {
|
|
Some(val) => {
|
|
if let Some(ref cols) = self.columns {
|
|
if !val.columns().any(|c| cols.contains(c)) {
|
|
return Some(val);
|
|
}
|
|
}
|
|
|
|
let span = val.span();
|
|
match val {
|
|
Value::Record { val, .. } => Some(Value::record(
|
|
val.into_owned()
|
|
.into_iter()
|
|
.map(|(col, val)| match &self.columns {
|
|
Some(cols) if !cols.contains(&col) => (col, val),
|
|
_ => (
|
|
col,
|
|
match process_cell(val, self.display_as_filesizes, span) {
|
|
Ok(val) => val,
|
|
Err(err) => Value::error(err, span),
|
|
},
|
|
),
|
|
})
|
|
.collect(),
|
|
span,
|
|
)),
|
|
val => match process_cell(val, self.display_as_filesizes, self.span) {
|
|
Ok(val) => Some(val),
|
|
Err(err) => Some(Value::error(err, self.span)),
|
|
},
|
|
}
|
|
}
|
|
None => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
// This function will check each cell to see if it matches a regular expression
|
|
// for a particular datatype. If it does, it will convert the cell to that datatype.
|
|
fn process_cell(val: Value, display_as_filesizes: bool, span: Span) -> Result<Value, ShellError> {
|
|
// step 1: convert value to string
|
|
let val_str = val.coerce_str().unwrap_or_default();
|
|
|
|
// step 2: bounce string up against regexes
|
|
if BOOLEAN_RE.is_match(&val_str).unwrap_or(false) {
|
|
let bval = val_str
|
|
.parse::<bool>()
|
|
.map_err(|_| ShellError::CantConvert {
|
|
to_type: "string".to_string(),
|
|
from_type: "bool".to_string(),
|
|
span,
|
|
help: Some(format!(
|
|
r#""{val_str}" does not represent a valid boolean value"#
|
|
)),
|
|
})?;
|
|
|
|
Ok(Value::bool(bval, span))
|
|
} else if FLOAT_RE.is_match(&val_str).unwrap_or(false) {
|
|
let fval = val_str
|
|
.parse::<f64>()
|
|
.map_err(|_| ShellError::CantConvert {
|
|
to_type: "float".to_string(),
|
|
from_type: "string".to_string(),
|
|
span,
|
|
help: Some(format!(
|
|
r#""{val_str}" does not represent a valid floating point value"#
|
|
)),
|
|
})?;
|
|
|
|
Ok(Value::float(fval, span))
|
|
} else if INTEGER_RE.is_match(&val_str).unwrap_or(false) {
|
|
let ival = val_str
|
|
.parse::<i64>()
|
|
.map_err(|_| ShellError::CantConvert {
|
|
to_type: "int".to_string(),
|
|
from_type: "string".to_string(),
|
|
span,
|
|
help: Some(format!(
|
|
r#""{val_str}" does not represent a valid integer value"#
|
|
)),
|
|
})?;
|
|
|
|
if display_as_filesizes {
|
|
Ok(Value::filesize(ival, span))
|
|
} else {
|
|
Ok(Value::int(ival, span))
|
|
}
|
|
} else if INTEGER_WITH_DELIMS_RE.is_match(&val_str).unwrap_or(false) {
|
|
let mut val_str = val_str.into_owned();
|
|
val_str.retain(|x| !['_', ','].contains(&x));
|
|
|
|
let ival = val_str
|
|
.parse::<i64>()
|
|
.map_err(|_| ShellError::CantConvert {
|
|
to_type: "int".to_string(),
|
|
from_type: "string".to_string(),
|
|
span,
|
|
help: Some(format!(
|
|
r#""{val_str}" does not represent a valid integer value"#
|
|
)),
|
|
})?;
|
|
|
|
if display_as_filesizes {
|
|
Ok(Value::filesize(ival, span))
|
|
} else {
|
|
Ok(Value::int(ival, span))
|
|
}
|
|
} else if DATETIME_DMY_RE.is_match(&val_str).unwrap_or(false) {
|
|
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
|
to_type: "datetime".to_string(),
|
|
from_type: "string".to_string(),
|
|
span,
|
|
help: Some(format!(
|
|
r#""{val_str}" does not represent a valid DATETIME_MDY_RE value"#
|
|
)),
|
|
})?;
|
|
|
|
Ok(Value::date(dt, span))
|
|
} else if DATETIME_YMD_RE.is_match(&val_str).unwrap_or(false) {
|
|
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
|
to_type: "datetime".to_string(),
|
|
from_type: "string".to_string(),
|
|
span,
|
|
help: Some(format!(
|
|
r#""{val_str}" does not represent a valid DATETIME_YMD_RE value"#
|
|
)),
|
|
})?;
|
|
|
|
Ok(Value::date(dt, span))
|
|
} else if DATETIME_YMDZ_RE.is_match(&val_str).unwrap_or(false) {
|
|
let dt = parse_date_from_string(&val_str, span).map_err(|_| ShellError::CantConvert {
|
|
to_type: "datetime".to_string(),
|
|
from_type: "string".to_string(),
|
|
span,
|
|
help: Some(format!(
|
|
r#""{val_str}" does not represent a valid DATETIME_YMDZ_RE value"#
|
|
)),
|
|
})?;
|
|
|
|
Ok(Value::date(dt, span))
|
|
} else {
|
|
// If we don't know what it is, just return whatever it was passed in as
|
|
Ok(val)
|
|
}
|
|
}
|
|
|
|
// region: datatype regexes
|
|
const DATETIME_DMY_PATTERN: &str = r#"(?x)
|
|
^
|
|
['"]? # optional quotes
|
|
(?:\d{1,2}) # day
|
|
[-/] # separator
|
|
(?P<month>[01]?\d{1}) # month
|
|
[-/] # separator
|
|
(?:\d{4,}) # year
|
|
(?:
|
|
[T\ ] # separator
|
|
(?:\d{2}) # hour
|
|
:? # separator
|
|
(?:\d{2}) # minute
|
|
(?:
|
|
:? # separator
|
|
(?:\d{2}) # second
|
|
(?:
|
|
\.(?:\d{1,9}) # subsecond
|
|
)?
|
|
)?
|
|
)?
|
|
['"]? # optional quotes
|
|
$
|
|
"#;
|
|
|
|
static DATETIME_DMY_RE: LazyLock<Regex> = LazyLock::new(|| {
|
|
Regex::new(DATETIME_DMY_PATTERN).expect("datetime_dmy_pattern should be valid")
|
|
});
|
|
const DATETIME_YMD_PATTERN: &str = r#"(?x)
|
|
^
|
|
['"]? # optional quotes
|
|
(?:\d{4,}) # year
|
|
[-/] # separator
|
|
(?P<month>[01]?\d{1}) # month
|
|
[-/] # separator
|
|
(?:\d{1,2}) # day
|
|
(?:
|
|
[T\ ] # separator
|
|
(?:\d{2}) # hour
|
|
:? # separator
|
|
(?:\d{2}) # minute
|
|
(?:
|
|
:? # separator
|
|
(?:\d{2}) # seconds
|
|
(?:
|
|
\.(?:\d{1,9}) # subsecond
|
|
)?
|
|
)?
|
|
)?
|
|
['"]? # optional quotes
|
|
$
|
|
"#;
|
|
static DATETIME_YMD_RE: LazyLock<Regex> = LazyLock::new(|| {
|
|
Regex::new(DATETIME_YMD_PATTERN).expect("datetime_ymd_pattern should be valid")
|
|
});
|
|
//2023-03-24 16:44:17.865147299 -05:00
|
|
const DATETIME_YMDZ_PATTERN: &str = r#"(?x)
|
|
^
|
|
['"]? # optional quotes
|
|
(?:\d{4,}) # year
|
|
[-/] # separator
|
|
(?P<month>[01]?\d{1}) # month
|
|
[-/] # separator
|
|
(?:\d{1,2}) # day
|
|
[T\ ] # separator
|
|
(?:\d{2}) # hour
|
|
:? # separator
|
|
(?:\d{2}) # minute
|
|
(?:
|
|
:? # separator
|
|
(?:\d{2}) # second
|
|
(?:
|
|
\.(?:\d{1,9}) # subsecond
|
|
)?
|
|
)?
|
|
\s? # optional space
|
|
(?:
|
|
# offset (e.g. +01:00)
|
|
[+-](?:\d{2})
|
|
:?
|
|
(?:\d{2})
|
|
# or Zulu suffix
|
|
|Z
|
|
)
|
|
['"]? # optional quotes
|
|
$
|
|
"#;
|
|
static DATETIME_YMDZ_RE: LazyLock<Regex> = LazyLock::new(|| {
|
|
Regex::new(DATETIME_YMDZ_PATTERN).expect("datetime_ymdz_pattern should be valid")
|
|
});
|
|
|
|
static FLOAT_RE: LazyLock<Regex> = LazyLock::new(|| {
|
|
Regex::new(r"^\s*[-+]?((\d*\.\d+)([eE][-+]?\d+)?|inf|NaN|(\d+)[eE][-+]?\d+|\d+\.)$")
|
|
.expect("float pattern should be valid")
|
|
});
|
|
|
|
static INTEGER_RE: LazyLock<Regex> =
|
|
LazyLock::new(|| Regex::new(r"^\s*-?(\d+)$").expect("integer pattern should be valid"));
|
|
|
|
static INTEGER_WITH_DELIMS_RE: LazyLock<Regex> = LazyLock::new(|| {
|
|
Regex::new(r"^\s*-?(\d{1,3}([,_]\d{3})+)$")
|
|
.expect("integer with delimiters pattern should be valid")
|
|
});
|
|
|
|
static BOOLEAN_RE: LazyLock<Regex> = LazyLock::new(|| {
|
|
RegexBuilder::new(r"^\s*(true)$|^(false)$")
|
|
.case_insensitive(true)
|
|
.build()
|
|
.expect("boolean pattern should be valid")
|
|
});
|
|
// endregion:
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_examples() {
|
|
use crate::test_examples;
|
|
|
|
test_examples(IntoValue {})
|
|
}
|
|
|
|
#[test]
|
|
fn test_float_parse() {
|
|
// The regex should work on all these but nushell's float parser is more strict
|
|
assert!(FLOAT_RE.is_match("0.1").unwrap());
|
|
assert!(FLOAT_RE.is_match("3.0").unwrap());
|
|
assert!(FLOAT_RE.is_match("3.00001").unwrap());
|
|
assert!(FLOAT_RE.is_match("-9.9990e-003").unwrap());
|
|
assert!(FLOAT_RE.is_match("9.9990e+003").unwrap());
|
|
assert!(FLOAT_RE.is_match("9.9990E+003").unwrap());
|
|
assert!(FLOAT_RE.is_match("9.9990E+003").unwrap());
|
|
assert!(FLOAT_RE.is_match(".5").unwrap());
|
|
assert!(FLOAT_RE.is_match("2.5E-10").unwrap());
|
|
assert!(FLOAT_RE.is_match("2.5e10").unwrap());
|
|
assert!(FLOAT_RE.is_match("NaN").unwrap());
|
|
assert!(FLOAT_RE.is_match("-NaN").unwrap());
|
|
assert!(FLOAT_RE.is_match("-inf").unwrap());
|
|
assert!(FLOAT_RE.is_match("inf").unwrap());
|
|
assert!(FLOAT_RE.is_match("-7e-05").unwrap());
|
|
assert!(FLOAT_RE.is_match("7e-05").unwrap());
|
|
assert!(FLOAT_RE.is_match("+7e+05").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_int_parse() {
|
|
assert!(INTEGER_RE.is_match("0").unwrap());
|
|
assert!(INTEGER_RE.is_match("1").unwrap());
|
|
assert!(INTEGER_RE.is_match("10").unwrap());
|
|
assert!(INTEGER_RE.is_match("100").unwrap());
|
|
assert!(INTEGER_RE.is_match("1000").unwrap());
|
|
assert!(INTEGER_RE.is_match("10000").unwrap());
|
|
assert!(INTEGER_RE.is_match("100000").unwrap());
|
|
assert!(INTEGER_RE.is_match("1000000").unwrap());
|
|
assert!(INTEGER_RE.is_match("10000000").unwrap());
|
|
assert!(INTEGER_RE.is_match("100000000").unwrap());
|
|
assert!(INTEGER_RE.is_match("1000000000").unwrap());
|
|
assert!(INTEGER_RE.is_match("10000000000").unwrap());
|
|
assert!(INTEGER_RE.is_match("100000000000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("1_000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("10_000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("100_000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("1_000_000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("10_000_000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("100_000_000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("1_000_000_000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("10_000_000_000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("100_000_000_000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("1,000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("10,000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("100,000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("1,000,000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("10,000,000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("100,000,000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("1,000,000,000").unwrap());
|
|
assert!(INTEGER_WITH_DELIMS_RE.is_match("10,000,000,000").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_bool_parse() {
|
|
assert!(BOOLEAN_RE.is_match("true").unwrap());
|
|
assert!(BOOLEAN_RE.is_match("false").unwrap());
|
|
assert!(!BOOLEAN_RE.is_match("1").unwrap());
|
|
assert!(!BOOLEAN_RE.is_match("0").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_datetime_ymdz_pattern() {
|
|
assert!(DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00Z").unwrap());
|
|
assert!(DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00.123456789Z")
|
|
.unwrap());
|
|
assert!(DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00+01:00")
|
|
.unwrap());
|
|
assert!(DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00.123456789+01:00")
|
|
.unwrap());
|
|
assert!(DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00-01:00")
|
|
.unwrap());
|
|
assert!(DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00.123456789-01:00")
|
|
.unwrap());
|
|
assert!(DATETIME_YMDZ_RE.is_match("'2022-01-01T00:00:00Z'").unwrap());
|
|
|
|
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00").unwrap());
|
|
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00.").unwrap());
|
|
assert!(!DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00.123456789")
|
|
.unwrap());
|
|
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00+01").unwrap());
|
|
assert!(!DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00+01:0")
|
|
.unwrap());
|
|
assert!(!DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00+1:00")
|
|
.unwrap());
|
|
assert!(!DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00.123456789+01")
|
|
.unwrap());
|
|
assert!(!DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00.123456789+01:0")
|
|
.unwrap());
|
|
assert!(!DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00.123456789+1:00")
|
|
.unwrap());
|
|
assert!(!DATETIME_YMDZ_RE.is_match("2022-01-01T00:00:00-01").unwrap());
|
|
assert!(!DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00-01:0")
|
|
.unwrap());
|
|
assert!(!DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00-1:00")
|
|
.unwrap());
|
|
assert!(!DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00.123456789-01")
|
|
.unwrap());
|
|
assert!(!DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00.123456789-01:0")
|
|
.unwrap());
|
|
assert!(!DATETIME_YMDZ_RE
|
|
.is_match("2022-01-01T00:00:00.123456789-1:00")
|
|
.unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_datetime_ymd_pattern() {
|
|
assert!(DATETIME_YMD_RE.is_match("2022-01-01").unwrap());
|
|
assert!(DATETIME_YMD_RE.is_match("2022/01/01").unwrap());
|
|
assert!(DATETIME_YMD_RE.is_match("2022-01-01T00:00:00").unwrap());
|
|
assert!(DATETIME_YMD_RE
|
|
.is_match("2022-01-01T00:00:00.000000000")
|
|
.unwrap());
|
|
assert!(DATETIME_YMD_RE.is_match("'2022-01-01'").unwrap());
|
|
|
|
// The regex isn't this specific, but it would be nice if it were
|
|
// assert!(!DATETIME_YMD_RE.is_match("2022-13-01").unwrap());
|
|
// assert!(!DATETIME_YMD_RE.is_match("2022-01-32").unwrap());
|
|
// assert!(!DATETIME_YMD_RE.is_match("2022-01-01T24:00:00").unwrap());
|
|
// assert!(!DATETIME_YMD_RE.is_match("2022-01-01T00:60:00").unwrap());
|
|
// assert!(!DATETIME_YMD_RE.is_match("2022-01-01T00:00:60").unwrap());
|
|
assert!(!DATETIME_YMD_RE
|
|
.is_match("2022-01-01T00:00:00.0000000000")
|
|
.unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_datetime_dmy_pattern() {
|
|
assert!(DATETIME_DMY_RE.is_match("31-12-2021").unwrap());
|
|
assert!(DATETIME_DMY_RE.is_match("01/01/2022").unwrap());
|
|
assert!(DATETIME_DMY_RE.is_match("15-06-2023 12:30").unwrap());
|
|
assert!(!DATETIME_DMY_RE.is_match("2022-13-01").unwrap());
|
|
assert!(!DATETIME_DMY_RE.is_match("2022-01-32").unwrap());
|
|
assert!(!DATETIME_DMY_RE.is_match("2022-01-01 24:00").unwrap());
|
|
}
|
|
}
|