diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 1a805e4c6a..233dc75e0e 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -22,11 +22,14 @@ glob = "0.3.0" thiserror = "1.0.29" sysinfo = "0.20.4" chrono = { version = "0.4.19", features = ["serde"] } +chrono-humanize = "0.2.1" +chrono-tz = "0.6.0" terminal_size = "0.1.17" lscolors = { version = "0.8.0", features = ["crossterm"] } bytesize = "1.1.0" dialoguer = "0.9.0" rayon = "1.5.1" +titlecase = "1.1.0" [features] trash-support = ["trash"] diff --git a/crates/nu-command/src/date/command.rs b/crates/nu-command/src/date/command.rs new file mode 100644 index 0000000000..3206b3b7c2 --- /dev/null +++ b/crates/nu-command/src/date/command.rs @@ -0,0 +1,47 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + IntoPipelineData, PipelineData, ShellError, Signature, Value, +}; + +#[derive(Clone)] +pub struct Date; + +impl Command for Date { + fn name(&self) -> &str { + "date" + } + + fn signature(&self) -> Signature { + Signature::build("date") + } + + fn usage(&self) -> &str { + "date" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + date(engine_state, stack, call) + } +} + +fn date( + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, +) -> Result { + let head = call.head; + + Ok(Value::String { + val: get_full_help(&Date.signature(), &Date.examples(), engine_state), + span: head, + } + .into_pipeline_data()) +} diff --git a/crates/nu-command/src/date/format.rs b/crates/nu-command/src/date/format.rs new file mode 100644 index 0000000000..c657e8f074 --- /dev/null +++ b/crates/nu-command/src/date/format.rs @@ -0,0 +1,117 @@ +use chrono::Local; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Example, PipelineData, Signature, Span, Spanned, SyntaxShape, Value, +}; + +use super::utils::{parse_date_from_string, unsupported_input_error}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "date format" + } + + fn signature(&self) -> Signature { + Signature::build("date format").required( + "format string", + SyntaxShape::String, + "the desired date format", + ) + } + + fn usage(&self) -> &str { + "Format a given date using the given format string." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let formatter: Spanned = call.req(engine_state, stack, 0)?; + input.map( + move |value| format_helper(value, &formatter, head), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Format a given date using the given format string.", + example: "date format '%Y-%m-%d'", + result: Some(Value::String { + val: Local::now().format("%Y-%m-%d").to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "Format a given date using the given format string.", + example: r#"date format "%Y-%m-%d %H:%M:%S""#, + result: Some(Value::String { + val: Local::now().format("%Y-%m-%d %H:%M:%S").to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "Format a given date using the given format string.", + example: r#""2021-10-22 20:00:12 +01:00" | date format "%Y-%m-%d""#, + result: Some(Value::String { + val: "2021-10-22".into(), + span: Span::unknown(), + }), + }, + ] + } +} + +fn format_helper(value: Value, formatter: &Spanned, span: Span) -> Value { + match value { + Value::Date { val, span: _ } => Value::String { + val: val.format(formatter.item.as_str()).to_string(), + span: span, + }, + Value::String { val, span: _ } => { + let dt = parse_date_from_string(val); + if dt.is_ok() { + Value::String { + val: dt.unwrap().format(formatter.item.as_str()).to_string(), + span: span, + } + } else { + dt.unwrap_err() + } + } + Value::Nothing { span: _ } => { + let dt = Local::now(); + Value::String { + val: dt + .with_timezone(dt.offset()) + .format(formatter.item.as_str()) + .to_string(), + span: span, + } + } + _ => unsupported_input_error(), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/date/humanize.rs b/crates/nu-command/src/date/humanize.rs new file mode 100644 index 0000000000..692b0029e2 --- /dev/null +++ b/crates/nu-command/src/date/humanize.rs @@ -0,0 +1,106 @@ +use crate::date::utils::parse_date_from_string; +use chrono::prelude::*; +use chrono::{DateTime, FixedOffset, Local}; +use chrono_humanize::HumanTime; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, Value}; +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "date humanize" + } + + fn signature(&self) -> Signature { + Signature::build("date humanize") + } + + fn usage(&self) -> &str { + "Print a 'humanized' format for the date, relative to now." + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + input.map(move |value| helper(value, head), engine_state.ctrlc.clone()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Print a 'humanized' format for the date, relative to now.", + example: "date humanize", + result: Some(Value::String { + val: "now".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "Print a 'humanized' format for the date, relative to now.", + example: r#""2021-10-22 20:00:12 +01:00" | date humanize"#, + result: { + let s = Local.ymd(2021, 10, 22).and_hms(20, 00, 12); + Some(Value::String { + val: HumanTime::from(s).to_string(), + span: Span::unknown(), + }) + }, + }, + ] + } +} + +fn helper(value: Value, head: Span) -> Value { + match value { + Value::Nothing { span: _ } => { + let dt = Local::now(); + Value::String { + val: humanize_date(dt.with_timezone(dt.offset())), + span: head, + } + } + Value::String { val, span: _ } => { + let dt = parse_date_from_string(val); + match dt { + Ok(x) => Value::String { + val: humanize_date(x), + span: head, + }, + Err(e) => e, + } + } + Value::Date { val, span: _ } => Value::String { + val: humanize_date(val), + span: head, + }, + _ => Value::Error { + error: ShellError::UnsupportedInput( + String::from("Date cannot be parsed / date format is not supported"), + Span::unknown(), + ), + }, + } +} + +fn humanize_date(dt: DateTime) -> String { + HumanTime::from(dt).to_string() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/date/list_timezone.rs b/crates/nu-command/src/date/list_timezone.rs new file mode 100644 index 0000000000..9442ebd560 --- /dev/null +++ b/crates/nu-command/src/date/list_timezone.rs @@ -0,0 +1,47 @@ +use chrono_tz::TZ_VARIANTS; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{IntoInterruptiblePipelineData, PipelineData, Signature, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "date list-timezone" + } + + fn signature(&self) -> Signature { + Signature::build("date list-timezone") + } + + fn usage(&self) -> &str { + "List supported time zones." + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let span = call.head; + + let tzs: Vec = TZ_VARIANTS + .iter() + .map(move |x| { + let cols = vec!["timezone".into()]; + let vals = vec![Value::String { + val: x.name().to_string(), + span: span, + }]; + Value::Record { cols, vals, span } + }) + .collect(); + + Ok(tzs + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) + } +} diff --git a/crates/nu-command/src/date/mod.rs b/crates/nu-command/src/date/mod.rs new file mode 100644 index 0000000000..0f0d33198b --- /dev/null +++ b/crates/nu-command/src/date/mod.rs @@ -0,0 +1,17 @@ +mod command; +mod format; +mod humanize; +mod list_timezone; +mod now; +mod parser; +mod to_table; +mod to_timezone; +mod utils; + +pub use command::Date; +pub use format::SubCommand as DateFormat; +pub use humanize::SubCommand as DateHumanize; +pub use list_timezone::SubCommand as DateListTimezones; +pub use now::SubCommand as DateNow; +pub use to_table::SubCommand as DateToTable; +pub use to_timezone::SubCommand as DateToTimezone; diff --git a/crates/nu-command/src/date/now.rs b/crates/nu-command/src/date/now.rs new file mode 100644 index 0000000000..19ba492441 --- /dev/null +++ b/crates/nu-command/src/date/now.rs @@ -0,0 +1,36 @@ +use chrono::Local; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, IntoPipelineData, PipelineData, Signature, Value}; +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "date now" + } + + fn signature(&self) -> Signature { + Signature::build("date now") + } + + fn usage(&self) -> &str { + "Get the current date." + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let head = call.head; + let dt = Local::now(); + Ok(Value::Date { + val: dt.with_timezone(dt.offset()), + span: head, + } + .into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/date/parser.rs b/crates/nu-command/src/date/parser.rs new file mode 100644 index 0000000000..d2d1f3a851 --- /dev/null +++ b/crates/nu-command/src/date/parser.rs @@ -0,0 +1,107 @@ +// Modified from chrono::format::scan + +use chrono::{DateTime, FixedOffset, Local, Offset, TimeZone}; +use chrono_tz::Tz; +use titlecase::titlecase; + +#[derive(Debug, Clone, PartialEq, Eq, Copy)] +pub enum ParseErrorKind { + /// Given field is out of permitted range. + OutOfRange, + + /// The input string has some invalid character sequence for given formatting items. + Invalid, + + /// The input string has been prematurely ended. + TooShort, +} + +pub fn datetime_in_timezone( + dt: &DateTime, + s: &str, +) -> Result, ParseErrorKind> { + match timezone_offset_internal(s, true, true) { + Ok(offset) => match FixedOffset::east_opt(offset) { + Some(offset) => Ok(dt.with_timezone(&offset)), + None => Err(ParseErrorKind::OutOfRange), + }, + Err(ParseErrorKind::Invalid) => { + if s.to_lowercase() == "local" { + Ok(dt.with_timezone(Local::now().offset())) + } else { + let tz: Tz = parse_timezone_internal(s)?; + let offset = tz.offset_from_utc_datetime(&dt.naive_utc()).fix(); + Ok(dt.with_timezone(&offset)) + } + } + Err(e) => Err(e), + } +} + +fn parse_timezone_internal(s: &str) -> Result { + if let Ok(tz) = s.parse() { + Ok(tz) + } else if let Ok(tz) = titlecase(s).parse() { + Ok(tz) + } else if let Ok(tz) = s.to_uppercase().parse() { + Ok(tz) + } else { + Err(ParseErrorKind::Invalid) + } +} + +fn timezone_offset_internal( + mut s: &str, + consume_colon: bool, + allow_missing_minutes: bool, +) -> Result { + fn digits(s: &str) -> Result<(u8, u8), ParseErrorKind> { + let b = s.as_bytes(); + if b.len() < 2 { + Err(ParseErrorKind::TooShort) + } else { + Ok((b[0], b[1])) + } + } + let negative = match s.as_bytes().first() { + Some(&b'+') => false, + Some(&b'-') => true, + Some(_) => return Err(ParseErrorKind::Invalid), + None => return Err(ParseErrorKind::TooShort), + }; + s = &s[1..]; + + // hours (00--99) + let hours = match digits(s)? { + (h1 @ b'0'..=b'9', h2 @ b'0'..=b'9') => i32::from((h1 - b'0') * 10 + (h2 - b'0')), + _ => return Err(ParseErrorKind::Invalid), + }; + s = &s[2..]; + + // colons (and possibly other separators) + if consume_colon { + s = s.trim_start_matches(|c: char| c == ':' || c.is_whitespace()); + } + + // minutes (00--59) + // if the next two items are digits then we have to add minutes + let minutes = if let Ok(ds) = digits(s) { + match ds { + (m1 @ b'0'..=b'5', m2 @ b'0'..=b'9') => i32::from((m1 - b'0') * 10 + (m2 - b'0')), + (b'6'..=b'9', b'0'..=b'9') => return Err(ParseErrorKind::OutOfRange), + _ => return Err(ParseErrorKind::Invalid), + } + } else if allow_missing_minutes { + 0 + } else { + return Err(ParseErrorKind::TooShort); + }; + match s.len() { + len if len >= 2 => &s[2..], + len if len == 0 => s, + _ => return Err(ParseErrorKind::TooShort), + }; + + let seconds = hours * 3600 + minutes * 60; + Ok(if negative { -seconds } else { seconds }) +} diff --git a/crates/nu-command/src/date/to_table.rs b/crates/nu-command/src/date/to_table.rs new file mode 100644 index 0000000000..eee501055f --- /dev/null +++ b/crates/nu-command/src/date/to_table.rs @@ -0,0 +1,179 @@ +use crate::date::utils::{parse_date_from_string, unsupported_input_error}; +use chrono::{DateTime, Datelike, FixedOffset, Local, Timelike}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "date to-table" + } + + fn signature(&self) -> Signature { + Signature::build("date to-table") + } + + fn usage(&self) -> &str { + "Print the date in a structured table." + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + input.map(move |value| helper(value, head), engine_state.ctrlc.clone()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Print the date in a structured table.", + example: "date to-table", + result: None, + }, + Example { + description: "Print the date in a structured table.", + example: "date now | date to-table", + result: None, + }, + Example { + description: "Print the date in a structured table.", + example: " '2020-04-12 22:10:57 +0200' | date to-table", + result: { + let span = Span::unknown(); + let cols = vec![ + "year".into(), + "month".into(), + "day".into(), + "hour".into(), + "minute".into(), + "second".into(), + "timezone".into(), + ]; + let vals = vec![ + Value::Int { + val: 2020, + span: span, + }, + Value::Int { + val: 04, + span: span, + }, + Value::Int { + val: 12, + span: span, + }, + Value::Int { + val: 22, + span: span, + }, + Value::Int { + val: 10, + span: span, + }, + Value::Int { + val: 57, + span: span, + }, + Value::String { + val: "+02:00".to_string(), + span: span, + }, + ]; + Some(Value::Record { + cols: cols, + vals: vals, + span: span, + }) + }, + }, + ] + } +} + +fn parse_date_into_table(date: Result, Value>, head: Span) -> Value { + let cols = vec![ + "year".into(), + "month".into(), + "day".into(), + "hour".into(), + "minute".into(), + "second".into(), + "timezone".into(), + ]; + match date { + Ok(x) => { + let vals = vec![ + Value::Int { + val: x.year() as i64, + span: head, + }, + Value::Int { + val: x.month() as i64, + span: head, + }, + Value::Int { + val: x.day() as i64, + span: head, + }, + Value::Int { + val: x.hour() as i64, + span: head, + }, + Value::Int { + val: x.minute() as i64, + span: head, + }, + Value::Int { + val: x.second() as i64, + span: head, + }, + Value::String { + val: x.offset().to_string(), + span: head, + }, + ]; + Value::Record { + cols, + vals, + span: head, + } + } + Err(e) => e, + } +} + +fn helper(val: Value, head: Span) -> Value { + match val { + Value::String { val, span: _ } => { + let date = parse_date_from_string(val); + parse_date_into_table(date, head) + } + Value::Nothing { span: _ } => { + let now = Local::now(); + let n = now.with_timezone(now.offset()); + parse_date_into_table(Ok(n), head) + } + Value::Date { val, span: _ } => parse_date_into_table(Ok(val), head), + _ => unsupported_input_error(), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/date/to_timezone.rs b/crates/nu-command/src/date/to_timezone.rs new file mode 100644 index 0000000000..7abcdd6e63 --- /dev/null +++ b/crates/nu-command/src/date/to_timezone.rs @@ -0,0 +1,136 @@ +use super::parser::datetime_in_timezone; +use crate::date::utils::{parse_date_from_string, unsupported_input_error}; +use chrono::{DateTime, Local}; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +use chrono::{FixedOffset, TimeZone}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "date to-timezone" + } + + fn signature(&self) -> Signature { + Signature::build("date to-timezone").required( + "time zone", + SyntaxShape::String, + "time zone description", + ) + } + + fn usage(&self) -> &str { + "Convert a date to a given time zone." + } + + fn extra_usage(&self) -> &str { + "Use 'date list-timezone' to list all supported time zones." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let timezone: Spanned = call.req(engine_state, stack, 0)?; + + //Ok(PipelineData::new()) + input.map( + move |value| helper(value, head, &timezone), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get the current date in UTC+05:00", + example: "date now | date to-timezone +0500", + result: None, + }, + Example { + description: "Get the current local date", + example: "date now | date to-timezone local", + result: None, + }, + Example { + description: "Get the current date in Hawaii", + example: "date now | date to-timezone US/Hawaii", + result: None, + }, + Example { + description: "Get the current date in Hawaii", + example: r#""2020-10-10 10:00:00 +02:00" | date to-timezone "+0500""#, + // result: None + // The following should be the result of the test, but it fails. Cannot figure it out why. + result: { + let dt = FixedOffset::east(5 * 3600) + .ymd(2020, 10, 10) + .and_hms(13, 00, 00); + + Some(Value::Date { + val: dt, + span: Span::unknown(), + }) + }, + }, + ] + } +} + +fn helper(value: Value, head: Span, timezone: &Spanned) -> Value { + match value { + Value::Date { val, span: _ } => _to_timezone(val, timezone, head), + Value::String { val, span: _ } => { + let time = parse_date_from_string(val); + + if time.is_ok() { + let dt = time.unwrap(); + _to_timezone(dt, timezone, head) + } else { + time.unwrap_err() + } + } + + Value::Nothing { span: _ } => { + let dt = Local::now(); + + _to_timezone(dt.with_timezone(dt.offset()), timezone, head) + } + _ => unsupported_input_error(), + } +} + +fn _to_timezone(dt: DateTime, timezone: &Spanned, span: Span) -> Value { + match datetime_in_timezone(&dt, &timezone.item.as_str()) { + Ok(dt) => Value::Date { + val: dt, + span: span, + }, + Err(_) => Value::Error { + error: ShellError::UnsupportedInput(String::from("invalid time zone"), Span::unknown()), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/date/utils.rs b/crates/nu-command/src/date/utils.rs new file mode 100644 index 0000000000..d1320766fc --- /dev/null +++ b/crates/nu-command/src/date/utils.rs @@ -0,0 +1,43 @@ +use chrono::{DateTime, FixedOffset}; +use nu_protocol::{ShellError, Span, Value}; + +pub fn unsupported_input_error() -> Value { + Value::Error { + error: ShellError::UnsupportedInput( + String::from( + "Only dates with timezones are supported. The following formats are allowed \n + * %Y-%m-%d %H:%M:%S %z -- 2020-04-12 22:10:57 +02:00 \n + * %Y-%m-%d %H:%M:%S%.6f %z -- 2020-04-12 22:10:57.213231 +02:00 \n + * rfc3339 -- 2020-04-12T22:10:57+02:00 \n + * rfc2822 -- Tue, 1 Jul 2003 10:52:37 +0200", + ), + Span::unknown(), + ), + } +} + +pub fn parse_date_from_string(input: String) -> Result, Value> { + let datetime = DateTime::parse_from_str(&input, "%Y-%m-%d %H:%M:%S %z"); // "2020-04-12 22:10:57 +02:00"; + match datetime { + Ok(x) => Ok(x), + Err(_) => { + let datetime = DateTime::parse_from_str(&input, "%Y-%m-%d %H:%M:%S%.6f %z"); // "2020-04-12 22:10:57.213231 +02:00"; + match datetime { + Ok(x) => Ok(x), + Err(_) => { + let datetime = DateTime::parse_from_rfc3339(&input); // "2020-04-12T22:10:57+02:00"; + match datetime { + Ok(x) => Ok(x), + Err(_) => { + let datetime = DateTime::parse_from_rfc2822(&input); // "Tue, 1 Jul 2003 10:52:37 +0200"; + match datetime { + Ok(x) => Ok(x), + Err(_) => Err(unsupported_input_error()), + } + } + } + } + } + } + } +} diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 77993b7e55..d6556a1c63 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -27,6 +27,13 @@ pub fn create_default_context() -> EngineState { BuildString, Cd, Cp, + Date, + DateFormat, + DateHumanize, + DateListTimezones, + DateNow, + DateToTable, + DateToTimezone, Def, Do, Each, diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs index f5879d5c96..f842dc2db8 100644 --- a/crates/nu-command/src/example_test.rs +++ b/crates/nu-command/src/example_test.rs @@ -7,7 +7,7 @@ use nu_protocol::{ use crate::To; -use super::{From, Into, Math, Split}; +use super::{Date, From, Into, Math, Split}; pub fn test_examples(cmd: impl Command + 'static) { let examples = cmd.examples(); @@ -22,6 +22,7 @@ pub fn test_examples(cmd: impl Command + 'static) { working_set.add_decl(Box::new(Into)); working_set.add_decl(Box::new(Split)); working_set.add_decl(Box::new(Math)); + working_set.add_decl(Box::new(Date)); // Adding the command that is being tested to the working set working_set.add_decl(Box::new(cmd)); diff --git a/crates/nu-command/src/lib.rs b/crates/nu-command/src/lib.rs index 151c6076cf..667d99be09 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -1,5 +1,6 @@ mod conversions; mod core_commands; +mod date; mod default_context; mod env; mod example_test; @@ -14,6 +15,7 @@ mod viewers; pub use conversions::*; pub use core_commands::*; +pub use date::*; pub use default_context::*; pub use env::*; pub use example_test::test_examples; diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index b8b2c3041a..b6ea1e2768 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -377,6 +377,9 @@ impl PartialOrd for Value { (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => { compare_floats(*lhs, *rhs) } + (Value::Date { val: lhs, .. }, Value::Date { val: rhs, .. }) => { + lhs.date().to_string().partial_cmp(&rhs.date().to_string()) + } (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => { lhs.partial_cmp(rhs) }