mirror of
https://github.com/nushell/nushell.git
synced 2025-04-29 15:44:28 +02:00
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> This PR makes two changes related to [run-time pipeline input type checking](https://github.com/nushell/nushell/pull/14741): 1. The check which bypasses type checking for commands with only `Type::Nothing` input types has been expanded to work with commands with multiple `Type::Nothing` inputs for different outputs. For example, `ast` has three input/output type pairs, but all of the inputs are `Type::Nothing`: ``` ╭───┬─────────┬────────╮ │ # │ input │ output │ ├───┼─────────┼────────┤ │ 0 │ nothing │ table │ │ 1 │ nothing │ record │ │ 2 │ nothing │ string │ ╰───┴─────────┴────────╯ ``` Before this PR, passing a value (which would otherwise be ignored) to `ast` caused a run-time type error: ``` Error: nu:🐚:only_supports_this_input_type × Input type not supported. ╭─[entry #1:1:6] 1 │ echo 123 | ast -j -f "hi" · ─┬─ ─┬─ · │ ╰── only nothing, nothing, and nothing input data is supported · ╰── input type: int ╰──── ``` After this PR, no error is raised. This doesn't really matter for `ast` (the only other built-in command with a similar input/output type signature is `cal`), but it's more logically consistent. 2. Bypasses input type-checking (parse-time ***and*** run-time) for some (not all, see below) commands which have both a `Type::Nothing` input and some other non-nothing `Type` input. This is accomplished by adding a `Type::Any` input with the same output as the corresponding `Type::Nothing` input/output pair. This is necessary because some commands are intended to operate on an argument with empty pipeline input, or operate on an empty pipeline input with no argument. This causes issues when a value is implicitly passed to one of these commands. I [discovered this issue](https://discord.com/channels/601130461678272522/615962413203718156/1329945784346611712) when working with an example where the `open` command is used in `sort-by` closure: ```nushell ls | sort-by { open -r $in.name | lines | length } ``` Before this PR (but after the run-time input type checking PR), this error is raised: ``` Error: nu:🐚:only_supports_this_input_type × Input type not supported. ╭─[entry #1:1:1] 1 │ ls | sort-by { open -r $in.name | lines | length } · ─┬ ──┬─ · │ ╰── only nothing and string input data is supported · ╰── input type: record<name: string, type: string, size: filesize, modified: date> ╰──── ``` While this error is technically correct, we don't actually want to return an error here since `open` ignores its pipeline input when an argument is passed. This would be a parse-time error as well if the parser was able to infer that the closure input type was a record, but our type inference isn't that robust currently, so this technically incorrect form snuck by type checking until #14741. However, there are some commands with the same kind of type signature where this behavior is actually desirable. This means we can't just bypass type-checking for any command with a `Type::Nothing` input. These commands operate on true `null` values, rather than ignoring their input. For example, `length` returns `0` when passed a `null` value. It's correct, and even desirable, to throw a run-time error when `length` is passed an unexpected type. For example, a string, which should instead be measured with `str length`: ```nushell ["hello" "world"] | sort-by { length } # => Error: nu:🐚:only_supports_this_input_type # => # => × Input type not supported. # => ╭─[entry #32:1:10] # => 1 │ ["hello" "world"] | sort-by { length } # => · ───┬─── ───┬── # => · │ ╰── only list<any>, binary, and nothing input data is supported # => · ╰── input type: string # => ╰──── ``` We need a more robust way for commands to express how they handle the `Type::Nothing` input case. I think a possible solution here is to allow commands to express that they operate on `PipelineData::Empty`, rather than `Value::Nothing`. Then, a command like `open` could have an empty pipeline input type rather than a `Type::Nothing`, and the parse-time and run-time pipeline input type checks know that `open` will safely ignore an incorrectly typed input. That being said, we have a release coming up and the above solution might take a while to implement, so while unfortunate, bypassing input type-checking for these problematic commands serves as a workaround to avoid breaking changes in the release until a more robust solution is implemented. This PR bypasses input type-checking for the following commands: * `load-env`: can take record of envvars as input or argument * `nu-check`: checks input string or filename argument * `open`: can take filename as input or argument * `polars when`: can be used with input, or can be chained with another `polars when` * `stor insert`: data record can be passed as input or argument * `stor update`: data record can be passed as input or argument * `format date`: `--list` ignores input value * `into datetime`: `--list` ignores input value (also added a `Type::Nothing` input which was missing from this command) These commands have a similar input/output signature to the above commands, but are working as intended: * `cd`: The input/output signature was actually incorrect, `cd` always ignores its input. I fixed this in this PR. * `generate` * `get` * `history import` * `interleave` * `into bool` * `length` # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> As a temporary workaround, pipeline input type-checking for the following commands has been bypassed to avoid undesirable run-time input type checking errors which were previously not caught at parse-time: * `open` * `load-env` * `format date` * `into datetime` * `nu-check` * `stor insert` * `stor update` * `polars when` # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> CI became green in the time it took me to type the description 😄 # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. --> N/A
708 lines
26 KiB
Rust
708 lines
26 KiB
Rust
use crate::{generate_strftime_list, parse_date_from_string};
|
|
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, TimeZone, Utc};
|
|
use human_date_parser::{from_human_time, ParseResult};
|
|
use nu_cmd_base::input_handler::{operate, CmdArgument};
|
|
use nu_engine::command_prelude::*;
|
|
|
|
struct Arguments {
|
|
zone_options: Option<Spanned<Zone>>,
|
|
format_options: Option<DatetimeFormat>,
|
|
cell_paths: Option<Vec<CellPath>>,
|
|
}
|
|
|
|
impl CmdArgument for Arguments {
|
|
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
|
|
self.cell_paths.take()
|
|
}
|
|
}
|
|
|
|
// In case it may be confused with chrono::TimeZone
|
|
#[derive(Clone, Debug)]
|
|
enum Zone {
|
|
Utc,
|
|
Local,
|
|
East(u8),
|
|
West(u8),
|
|
Error, // we want Nushell to cast it instead of Rust
|
|
}
|
|
|
|
impl Zone {
|
|
fn new(i: i64) -> Self {
|
|
if i.abs() <= 12 {
|
|
// guaranteed here
|
|
if i >= 0 {
|
|
Self::East(i as u8) // won't go out of range
|
|
} else {
|
|
Self::West(-i as u8) // same here
|
|
}
|
|
} else {
|
|
Self::Error // Out of range
|
|
}
|
|
}
|
|
fn from_string(s: &str) -> Self {
|
|
match s.to_ascii_lowercase().as_str() {
|
|
"utc" | "u" => Self::Utc,
|
|
"local" | "l" => Self::Local,
|
|
_ => Self::Error,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct SubCommand;
|
|
|
|
impl Command for SubCommand {
|
|
fn name(&self) -> &str {
|
|
"into datetime"
|
|
}
|
|
|
|
fn signature(&self) -> Signature {
|
|
Signature::build("into datetime")
|
|
.input_output_types(vec![
|
|
(Type::Date, Type::Date),
|
|
(Type::Int, Type::Date),
|
|
(Type::String, Type::Date),
|
|
(Type::List(Box::new(Type::String)), Type::List(Box::new(Type::Date))),
|
|
(Type::table(), Type::table()),
|
|
(Type::record(), Type::record()),
|
|
(Type::Nothing, Type::table()),
|
|
// FIXME Type::Any input added to disable pipeline input type checking, as run-time checks can raise undesirable type errors
|
|
// which aren't caught by the parser. see https://github.com/nushell/nushell/pull/14922 for more details
|
|
// only applicable for --list flag
|
|
(Type::Any, Type::table()),
|
|
])
|
|
.allow_variants_without_examples(true)
|
|
.named(
|
|
"timezone",
|
|
SyntaxShape::String,
|
|
"Specify timezone if the input is a Unix timestamp. Valid options: 'UTC' ('u') or 'LOCAL' ('l')",
|
|
Some('z'),
|
|
)
|
|
.named(
|
|
"offset",
|
|
SyntaxShape::Int,
|
|
"Specify timezone by offset from UTC if the input is a Unix timestamp, like '+8', '-4'",
|
|
Some('o'),
|
|
)
|
|
.named(
|
|
"format",
|
|
SyntaxShape::String,
|
|
"Specify expected format of INPUT string to parse to datetime. Use --list to see options",
|
|
Some('f'),
|
|
)
|
|
.switch(
|
|
"list",
|
|
"Show all possible variables for use in --format flag",
|
|
Some('l'),
|
|
)
|
|
.switch(
|
|
"list-human",
|
|
"Show human-readable datetime parsing examples",
|
|
Some('n'),
|
|
)
|
|
.rest(
|
|
"rest",
|
|
SyntaxShape::CellPath,
|
|
"For a data structure input, convert data at the given cell paths.",
|
|
)
|
|
.category(Category::Conversions)
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
input: PipelineData,
|
|
) -> Result<PipelineData, ShellError> {
|
|
if call.has_flag(engine_state, stack, "list")? {
|
|
Ok(generate_strftime_list(call.head, true).into_pipeline_data())
|
|
} else if call.has_flag(engine_state, stack, "list-human")? {
|
|
Ok(list_human_readable_examples(call.head).into_pipeline_data())
|
|
} else {
|
|
let cell_paths = call.rest(engine_state, stack, 0)?;
|
|
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
|
|
|
|
// if zone-offset is specified, then zone will be neglected
|
|
let timezone = call.get_flag::<Spanned<String>>(engine_state, stack, "timezone")?;
|
|
let zone_options =
|
|
match &call.get_flag::<Spanned<i64>>(engine_state, stack, "offset")? {
|
|
Some(zone_offset) => Some(Spanned {
|
|
item: Zone::new(zone_offset.item),
|
|
span: zone_offset.span,
|
|
}),
|
|
None => timezone.as_ref().map(|zone| Spanned {
|
|
item: Zone::from_string(&zone.item),
|
|
span: zone.span,
|
|
}),
|
|
};
|
|
|
|
let format_options = call
|
|
.get_flag::<String>(engine_state, stack, "format")?
|
|
.as_ref()
|
|
.map(|fmt| DatetimeFormat(fmt.to_string()));
|
|
|
|
let args = Arguments {
|
|
format_options,
|
|
zone_options,
|
|
cell_paths,
|
|
};
|
|
operate(action, args, input, call.head, engine_state.signals())
|
|
}
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
"Convert text or timestamp into a datetime."
|
|
}
|
|
|
|
fn search_terms(&self) -> Vec<&str> {
|
|
vec!["convert", "timezone", "UTC"]
|
|
}
|
|
|
|
fn examples(&self) -> Vec<Example> {
|
|
let example_result_1 = |nanos: i64| {
|
|
Some(Value::date(
|
|
Utc.timestamp_nanos(nanos).into(),
|
|
Span::test_data(),
|
|
))
|
|
};
|
|
vec![
|
|
Example {
|
|
description: "Convert timestamp string to datetime with timezone offset",
|
|
example: "'27.02.2021 1:55 pm +0000' | into datetime",
|
|
#[allow(clippy::inconsistent_digit_grouping)]
|
|
result: example_result_1(1614434100_000000000),
|
|
},
|
|
Example {
|
|
description: "Convert standard timestamp string to datetime with timezone offset",
|
|
example: "'2021-02-27T13:55:40.2246+00:00' | into datetime",
|
|
#[allow(clippy::inconsistent_digit_grouping)]
|
|
result: example_result_1(1614434140_224600000),
|
|
},
|
|
Example {
|
|
description:
|
|
"Convert non-standard timestamp string, with timezone offset, to datetime using a custom format",
|
|
example: "'20210227_135540+0000' | into datetime --format '%Y%m%d_%H%M%S%z'",
|
|
#[allow(clippy::inconsistent_digit_grouping)]
|
|
result: example_result_1(1614434140_000000000),
|
|
},
|
|
Example {
|
|
description: "Convert non-standard timestamp string, without timezone offset, to datetime with custom formatting",
|
|
example: "'16.11.1984 8:00 am' | into datetime --format '%d.%m.%Y %H:%M %P'",
|
|
#[allow(clippy::inconsistent_digit_grouping)]
|
|
result: Some(Value::date(
|
|
Local
|
|
.from_local_datetime(
|
|
&NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P")
|
|
.expect("date calculation should not fail in test"),
|
|
)
|
|
.unwrap()
|
|
.with_timezone(Local::now().offset()),
|
|
Span::test_data(),
|
|
)),
|
|
},
|
|
Example {
|
|
description:
|
|
"Convert nanosecond-precision unix timestamp to a datetime with offset from UTC",
|
|
example: "1614434140123456789 | into datetime --offset -5",
|
|
#[allow(clippy::inconsistent_digit_grouping)]
|
|
result: example_result_1(1614434140_123456789),
|
|
},
|
|
Example {
|
|
description: "Convert standard (seconds) unix timestamp to a UTC datetime",
|
|
example: "1614434140 | into datetime -f '%s'",
|
|
#[allow(clippy::inconsistent_digit_grouping)]
|
|
result: example_result_1(1614434140_000000000),
|
|
},
|
|
Example {
|
|
description: "Using a datetime as input simply returns the value",
|
|
example: "2021-02-27T13:55:40 | into datetime",
|
|
#[allow(clippy::inconsistent_digit_grouping)]
|
|
result: example_result_1(1614434140_000000000),
|
|
},
|
|
Example {
|
|
description: "Convert list of timestamps to datetimes",
|
|
example: r#"["2023-03-30 10:10:07 -05:00", "2023-05-05 13:43:49 -05:00", "2023-06-05 01:37:42 -05:00"] | into datetime"#,
|
|
result: Some(Value::list(
|
|
vec![
|
|
Value::date(
|
|
DateTime::parse_from_str(
|
|
"2023-03-30 10:10:07 -05:00",
|
|
"%Y-%m-%d %H:%M:%S %z",
|
|
)
|
|
.expect("date calculation should not fail in test"),
|
|
Span::test_data(),
|
|
),
|
|
Value::date(
|
|
DateTime::parse_from_str(
|
|
"2023-05-05 13:43:49 -05:00",
|
|
"%Y-%m-%d %H:%M:%S %z",
|
|
)
|
|
.expect("date calculation should not fail in test"),
|
|
Span::test_data(),
|
|
),
|
|
Value::date(
|
|
DateTime::parse_from_str(
|
|
"2023-06-05 01:37:42 -05:00",
|
|
"%Y-%m-%d %H:%M:%S %z",
|
|
)
|
|
.expect("date calculation should not fail in test"),
|
|
Span::test_data(),
|
|
),
|
|
],
|
|
Span::test_data(),
|
|
)),
|
|
},
|
|
Example {
|
|
description: "Parsing human readable datetimes",
|
|
example: "'Today at 18:30' | into datetime",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Parsing human readable datetimes",
|
|
example: "'Last Friday at 19:45' | into datetime",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Parsing human readable datetimes",
|
|
example: "'In 5 minutes and 30 seconds' | into datetime",
|
|
result: None,
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct DatetimeFormat(String);
|
|
|
|
fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
|
let timezone = &args.zone_options;
|
|
let dateformat = &args.format_options;
|
|
|
|
// noop if the input is already a datetime
|
|
if matches!(input, Value::Date { .. }) {
|
|
return input.clone();
|
|
}
|
|
|
|
// Let's try dtparse first
|
|
if matches!(input, Value::String { .. }) && dateformat.is_none() {
|
|
let span = input.span();
|
|
if let Ok(input_val) = input.coerce_str() {
|
|
match parse_date_from_string(&input_val, span) {
|
|
Ok(date) => return Value::date(date, span),
|
|
Err(_) => {
|
|
if let Ok(date) = from_human_time(&input_val) {
|
|
match date {
|
|
ParseResult::Date(date) => {
|
|
let time = Local::now().time();
|
|
let combined = date.and_time(time);
|
|
let local_offset = *Local::now().offset();
|
|
let dt_fixed =
|
|
TimeZone::from_local_datetime(&local_offset, &combined)
|
|
.single()
|
|
.unwrap_or_default();
|
|
return Value::date(dt_fixed, span);
|
|
}
|
|
ParseResult::DateTime(date) => {
|
|
return Value::date(date.fixed_offset(), span)
|
|
}
|
|
ParseResult::Time(time) => {
|
|
let date = Local::now().date_naive();
|
|
let combined = date.and_time(time);
|
|
let local_offset = *Local::now().offset();
|
|
let dt_fixed =
|
|
TimeZone::from_local_datetime(&local_offset, &combined)
|
|
.single()
|
|
.unwrap_or_default();
|
|
return Value::date(dt_fixed, span);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
const HOUR: i32 = 60 * 60;
|
|
|
|
// Check to see if input looks like a Unix timestamp (i.e. can it be parsed to an int?)
|
|
let timestamp = match input {
|
|
Value::Int { val, .. } => Ok(*val),
|
|
Value::String { val, .. } => val.parse::<i64>(),
|
|
// Propagate errors by explicitly matching them before the final case.
|
|
Value::Error { .. } => return input.clone(),
|
|
other => {
|
|
return Value::error(
|
|
ShellError::OnlySupportsThisInputType {
|
|
exp_input_type: "string and int".into(),
|
|
wrong_type: other.get_type().to_string(),
|
|
dst_span: head,
|
|
src_span: other.span(),
|
|
},
|
|
head,
|
|
);
|
|
}
|
|
};
|
|
|
|
if dateformat.is_none() {
|
|
if let Ok(ts) = timestamp {
|
|
return match timezone {
|
|
// note all these `.timestamp_nanos()` could overflow if we didn't check range in `<date> | into int`.
|
|
|
|
// default to UTC
|
|
None => Value::date(Utc.timestamp_nanos(ts).into(), head),
|
|
Some(Spanned { item, span }) => match item {
|
|
Zone::Utc => {
|
|
let dt = Utc.timestamp_nanos(ts);
|
|
Value::date(dt.into(), *span)
|
|
}
|
|
Zone::Local => {
|
|
let dt = Local.timestamp_nanos(ts);
|
|
Value::date(dt.into(), *span)
|
|
}
|
|
Zone::East(i) => match FixedOffset::east_opt((*i as i32) * HOUR) {
|
|
Some(eastoffset) => {
|
|
let dt = eastoffset.timestamp_nanos(ts);
|
|
Value::date(dt, *span)
|
|
}
|
|
None => Value::error(
|
|
ShellError::DatetimeParseError {
|
|
msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
|
|
span: *span,
|
|
},
|
|
*span,
|
|
),
|
|
},
|
|
Zone::West(i) => match FixedOffset::west_opt((*i as i32) * HOUR) {
|
|
Some(westoffset) => {
|
|
let dt = westoffset.timestamp_nanos(ts);
|
|
Value::date(dt, *span)
|
|
}
|
|
None => Value::error(
|
|
ShellError::DatetimeParseError {
|
|
msg: input.to_abbreviated_string(&nu_protocol::Config::default()),
|
|
span: *span,
|
|
},
|
|
*span,
|
|
),
|
|
},
|
|
Zone::Error => Value::error(
|
|
// This is an argument error, not an input error
|
|
ShellError::TypeMismatch {
|
|
err_message: "Invalid timezone or offset".to_string(),
|
|
span: *span,
|
|
},
|
|
*span,
|
|
),
|
|
},
|
|
};
|
|
};
|
|
}
|
|
|
|
// If input is not a timestamp, try parsing it as a string
|
|
let span = input.span();
|
|
|
|
let parse_as_string = |val: &str| {
|
|
match dateformat {
|
|
Some(dt) => match DateTime::parse_from_str(val, &dt.0) {
|
|
Ok(d) => Value::date ( d, head ),
|
|
Err(reason) => {
|
|
match NaiveDateTime::parse_from_str(val, &dt.0) {
|
|
Ok(d) => {
|
|
let local_offset = *Local::now().offset();
|
|
let dt_fixed =
|
|
TimeZone::from_local_datetime(&local_offset, &d)
|
|
.single()
|
|
.unwrap_or_default();
|
|
|
|
Value::date (dt_fixed,head)
|
|
}
|
|
Err(_) => {
|
|
Value::error (
|
|
ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt.0), from_type: reason.to_string(), span: head, help: Some("you can use `into datetime` without a format string to enable flexible parsing".to_string()) },
|
|
head,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// Tries to automatically parse the date
|
|
// (i.e. without a format string)
|
|
// and assumes the system's local timezone if none is specified
|
|
None => match parse_date_from_string(val, span) {
|
|
Ok(date) => Value::date (
|
|
date,
|
|
span,
|
|
),
|
|
Err(err) => err,
|
|
},
|
|
}
|
|
};
|
|
|
|
match input {
|
|
Value::String { val, .. } => parse_as_string(val),
|
|
Value::Int { val, .. } => parse_as_string(&val.to_string()),
|
|
|
|
// Propagate errors by explicitly matching them before the final case.
|
|
Value::Error { .. } => input.clone(),
|
|
other => Value::error(
|
|
ShellError::OnlySupportsThisInputType {
|
|
exp_input_type: "string".into(),
|
|
wrong_type: other.get_type().to_string(),
|
|
dst_span: head,
|
|
src_span: other.span(),
|
|
},
|
|
head,
|
|
),
|
|
}
|
|
}
|
|
|
|
fn list_human_readable_examples(span: Span) -> Value {
|
|
let examples: Vec<String> = vec![
|
|
"Today 18:30".into(),
|
|
"2022-11-07 13:25:30".into(),
|
|
"15:20 Friday".into(),
|
|
"This Friday 17:00".into(),
|
|
"13:25, Next Tuesday".into(),
|
|
"Last Friday at 19:45".into(),
|
|
"In 3 days".into(),
|
|
"In 2 hours".into(),
|
|
"10 hours and 5 minutes ago".into(),
|
|
"1 years ago".into(),
|
|
"A year ago".into(),
|
|
"A month ago".into(),
|
|
"A week ago".into(),
|
|
"A day ago".into(),
|
|
"An hour ago".into(),
|
|
"A minute ago".into(),
|
|
"A second ago".into(),
|
|
"Now".into(),
|
|
];
|
|
|
|
let records = examples
|
|
.iter()
|
|
.map(|s| {
|
|
Value::record(
|
|
record! {
|
|
"parseable human datetime examples" => Value::test_string(s.to_string()),
|
|
"result" => action(&Value::test_string(s.to_string()), &Arguments { zone_options: None, format_options: None, cell_paths: None }, span)
|
|
},
|
|
span,
|
|
)
|
|
})
|
|
.collect::<Vec<Value>>();
|
|
|
|
Value::list(records, span)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use super::{action, DatetimeFormat, SubCommand, Zone};
|
|
use nu_protocol::Type::Error;
|
|
|
|
#[test]
|
|
fn test_examples() {
|
|
use crate::test_examples;
|
|
|
|
test_examples(SubCommand {})
|
|
}
|
|
|
|
#[test]
|
|
fn takes_a_date_format_with_timezone() {
|
|
let date_str = Value::test_string("16.11.1984 8:00 am +0000");
|
|
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()));
|
|
let args = Arguments {
|
|
zone_options: None,
|
|
format_options: fmt_options,
|
|
cell_paths: None,
|
|
};
|
|
let actual = action(&date_str, &args, Span::test_data());
|
|
let expected = Value::date(
|
|
DateTime::parse_from_str("16.11.1984 8:00 am +0000", "%d.%m.%Y %H:%M %P %z").unwrap(),
|
|
Span::test_data(),
|
|
);
|
|
assert_eq!(actual, expected)
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn takes_a_date_format_without_timezone() {
|
|
// Ignoring this test for now because we changed the human-date-parser to use
|
|
// the users timezone instead of UTC. We may continue to tweak this behavior.
|
|
// Another hacky solution is to set the timezone to UTC in the test, which works
|
|
// on MacOS and Linux but hasn't been tested on Windows. Plus it kind of defeats
|
|
// the purpose of a "without_timezone" test.
|
|
// std::env::set_var("TZ", "UTC");
|
|
let date_str = Value::test_string("16.11.1984 8:00 am");
|
|
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P".to_string()));
|
|
let args = Arguments {
|
|
zone_options: None,
|
|
format_options: fmt_options,
|
|
cell_paths: None,
|
|
};
|
|
let actual = action(&date_str, &args, Span::test_data());
|
|
let expected = Value::date(
|
|
Local
|
|
.from_local_datetime(
|
|
&NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P")
|
|
.unwrap(),
|
|
)
|
|
.unwrap()
|
|
.with_timezone(Local::now().offset()),
|
|
Span::test_data(),
|
|
);
|
|
|
|
assert_eq!(actual, expected)
|
|
}
|
|
|
|
#[test]
|
|
fn takes_iso8601_date_format() {
|
|
let date_str = Value::test_string("2020-08-04T16:39:18+00:00");
|
|
let args = Arguments {
|
|
zone_options: None,
|
|
format_options: None,
|
|
cell_paths: None,
|
|
};
|
|
let actual = action(&date_str, &args, Span::test_data());
|
|
let expected = Value::date(
|
|
DateTime::parse_from_str("2020-08-04T16:39:18+00:00", "%Y-%m-%dT%H:%M:%S%z").unwrap(),
|
|
Span::test_data(),
|
|
);
|
|
assert_eq!(actual, expected)
|
|
}
|
|
|
|
#[test]
|
|
fn takes_timestamp_offset() {
|
|
let date_str = Value::test_string("1614434140000000000");
|
|
let timezone_option = Some(Spanned {
|
|
item: Zone::East(8),
|
|
span: Span::test_data(),
|
|
});
|
|
let args = Arguments {
|
|
zone_options: timezone_option,
|
|
format_options: None,
|
|
cell_paths: None,
|
|
};
|
|
let actual = action(&date_str, &args, Span::test_data());
|
|
let expected = Value::date(
|
|
DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(),
|
|
Span::test_data(),
|
|
);
|
|
|
|
assert_eq!(actual, expected)
|
|
}
|
|
|
|
#[test]
|
|
fn takes_timestamp_offset_as_int() {
|
|
let date_int = Value::test_int(1_614_434_140_000_000_000);
|
|
let timezone_option = Some(Spanned {
|
|
item: Zone::East(8),
|
|
span: Span::test_data(),
|
|
});
|
|
let args = Arguments {
|
|
zone_options: timezone_option,
|
|
format_options: None,
|
|
cell_paths: None,
|
|
};
|
|
let actual = action(&date_int, &args, Span::test_data());
|
|
let expected = Value::date(
|
|
DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(),
|
|
Span::test_data(),
|
|
);
|
|
|
|
assert_eq!(actual, expected)
|
|
}
|
|
|
|
#[test]
|
|
fn takes_int_with_formatstring() {
|
|
let date_int = Value::test_int(1_614_434_140);
|
|
let fmt_options = Some(DatetimeFormat("%s".to_string()));
|
|
let args = Arguments {
|
|
zone_options: None,
|
|
format_options: fmt_options,
|
|
cell_paths: None,
|
|
};
|
|
let actual = action(&date_int, &args, Span::test_data());
|
|
let expected = Value::date(
|
|
DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(),
|
|
Span::test_data(),
|
|
);
|
|
|
|
assert_eq!(actual, expected)
|
|
}
|
|
|
|
#[test]
|
|
fn takes_timestamp() {
|
|
let date_str = Value::test_string("1614434140000000000");
|
|
let timezone_option = Some(Spanned {
|
|
item: Zone::Local,
|
|
span: Span::test_data(),
|
|
});
|
|
let args = Arguments {
|
|
zone_options: timezone_option,
|
|
format_options: None,
|
|
cell_paths: None,
|
|
};
|
|
let actual = action(&date_str, &args, Span::test_data());
|
|
let expected = Value::date(
|
|
Local.timestamp_opt(1614434140, 0).unwrap().into(),
|
|
Span::test_data(),
|
|
);
|
|
|
|
assert_eq!(actual, expected)
|
|
}
|
|
|
|
#[test]
|
|
fn takes_datetime() {
|
|
let timezone_option = Some(Spanned {
|
|
item: Zone::Local,
|
|
span: Span::test_data(),
|
|
});
|
|
let args = Arguments {
|
|
zone_options: timezone_option,
|
|
format_options: None,
|
|
cell_paths: None,
|
|
};
|
|
let expected = Value::date(
|
|
Local.timestamp_opt(1614434140, 0).unwrap().into(),
|
|
Span::test_data(),
|
|
);
|
|
let actual = action(&expected, &args, Span::test_data());
|
|
|
|
assert_eq!(actual, expected)
|
|
}
|
|
|
|
#[test]
|
|
fn takes_timestamp_without_timezone() {
|
|
let date_str = Value::test_string("1614434140000000000");
|
|
let args = Arguments {
|
|
zone_options: None,
|
|
format_options: None,
|
|
cell_paths: None,
|
|
};
|
|
let actual = action(&date_str, &args, Span::test_data());
|
|
|
|
let expected = Value::date(
|
|
Utc.timestamp_opt(1614434140, 0).unwrap().into(),
|
|
Span::test_data(),
|
|
);
|
|
|
|
assert_eq!(actual, expected)
|
|
}
|
|
|
|
#[test]
|
|
fn communicates_parsing_error_given_an_invalid_datetimelike_string() {
|
|
let date_str = Value::test_string("16.11.1984 8:00 am Oops0000");
|
|
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()));
|
|
let args = Arguments {
|
|
zone_options: None,
|
|
format_options: fmt_options,
|
|
cell_paths: None,
|
|
};
|
|
let actual = action(&date_str, &args, Span::test_data());
|
|
|
|
assert_eq!(actual.get_type(), Error);
|
|
}
|
|
}
|