diff --git a/Cargo.lock b/Cargo.lock index 70ac84f067..9d6cab3419 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3144,6 +3144,7 @@ dependencies = [ "bytes 0.5.6", "calamine", "chrono", + "chrono-tz", "clap", "clipboard", "codespan-reporting", @@ -3217,6 +3218,7 @@ dependencies = [ "term", "term_size", "termcolor", + "titlecase", "toml", "trash", "umask", @@ -5624,6 +5626,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "titlecase" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f565e410cfc24c2f2a89960b023ca192689d7f77d3f8d4f4af50c2d8affe1117" +dependencies = [ + "lazy_static 1.4.0", + "regex 1.4.2", +] + [[package]] name = "tokio" version = "0.1.22" diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 8db023f25f..abb9f48d53 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -31,6 +31,7 @@ byte-unit = "4.0.9" bytes = "0.5.6" calamine = "0.16.1" chrono = {version = "0.4.15", features = ["serde"]} +chrono-tz = "0.5.3" clap = "2.33.3" clipboard = {version = "0.5.0", optional = true} codespan-reporting = "0.9.5" @@ -91,6 +92,7 @@ tempfile = "3.1.0" term = {version = "0.6.1", optional = true} term_size = "0.3.2" termcolor = "1.1.0" +titlecase = "1.0" toml = "0.5.6" trash = {version = "1.2.0", optional = true} unicode-segmentation = "1.6.0" diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 073fbddf8c..438d830092 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -86,8 +86,10 @@ pub fn create_default_context(interactive: bool) -> Result, - raw: Option, + table: bool, } #[async_trait] @@ -24,11 +24,11 @@ impl WholeStreamCommand for Date { fn signature(&self) -> Signature { Signature::build("date format") .required("format", SyntaxShape::String, "strftime format") - .switch("raw", "print date without tables", Some('r')) + .switch("table", "print date in a table", Some('t')) } fn usage(&self) -> &str { - "format the current date using the given format string." + "Format a given date using the given format string." } async fn run( @@ -38,6 +38,21 @@ impl WholeStreamCommand for Date { ) -> Result { format(args, registry).await } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Format the current date", + example: "date now | date format '%Y.%m.%d_%H %M %S,%z'", + result: None, + }, + Example { + description: "Format the current date and print in a table", + example: "date now | date format -t '%Y-%m-%d_%H:%M:%S %z'", + result: None, + }, + ] + } } pub async fn format( @@ -46,20 +61,46 @@ pub async fn format( ) -> Result { let registry = registry.clone(); let tag = args.call_info.name_tag.clone(); - let (FormatArgs { format, raw }, _) = args.process(®istry).await?; + let (FormatArgs { format, table }, input) = args.process(®istry).await?; - let dt_fmt = format.to_string(); + Ok(input + .map(move |value| match value { + Value { + value: UntaggedValue::Primitive(Primitive::Date(dt)), + .. + } => { + let mut output = String::new(); + if let Err(fmt::Error) = + write(&mut output, format_args!("{}", dt.format(&format.item))) + { + Err(ShellError::labeled_error( + "The date format is invalid", + "invalid strftime format", + &format.tag, + )) + } else { + let value = if table { + let mut indexmap = IndexMap::new(); + indexmap.insert( + "formatted".to_string(), + UntaggedValue::string(&output).into_value(&tag), + ); - let value = { - let local: DateTime = Local::now(); - if let Some(true) = raw { - UntaggedValue::string(date_to_value_raw(local, dt_fmt)).into_untagged_value() - } else { - date_to_value(local, tag, dt_fmt) - } - }; + UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&tag) + } else { + UntaggedValue::string(&output).into_value(&tag) + }; - Ok(OutputStream::one(value)) + ReturnSuccess::value(value) + } + } + _ => Err(ShellError::labeled_error( + "Expected a date from pipeline", + "requires date input", + &tag, + )), + }) + .to_output_stream()) } #[cfg(test)] diff --git a/crates/nu-cli/src/commands/date/list_timezone.rs b/crates/nu-cli/src/commands/date/list_timezone.rs new file mode 100644 index 0000000000..7f82e25afa --- /dev/null +++ b/crates/nu-cli/src/commands/date/list_timezone.rs @@ -0,0 +1,82 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use chrono_tz::TZ_VARIANTS; +use indexmap::IndexMap; +use nu_errors::ShellError; +use nu_protocol::{Dictionary, ReturnSuccess, Signature, UntaggedValue}; + +pub struct Date; + +#[async_trait] +impl WholeStreamCommand for Date { + fn name(&self) -> &str { + "date list-timezone" + } + + fn signature(&self) -> Signature { + Signature::build("date list-timezone") + } + + fn usage(&self) -> &str { + "List supported time zones." + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + list_timezone(args, registry).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "List all supported time zones", + example: "date list-timezone", + result: None, + }, + Example { + description: "List all supported European time zones", + example: "date list-timezone | where timezone =~ Europe", + result: None, + }, + ] + } +} + +async fn list_timezone( + args: CommandArgs, + registry: &CommandRegistry, +) -> Result { + let args = args.evaluate_once(®istry).await?; + let tag = args.call_info.name_tag.clone(); + + let list = TZ_VARIANTS.iter().map(move |tz| { + let mut entries = IndexMap::new(); + + entries.insert( + "timezone".to_string(), + UntaggedValue::string(tz.name()).into_value(&tag), + ); + + Ok(ReturnSuccess::Value( + UntaggedValue::Row(Dictionary { entries }).into_value(&tag), + )) + }); + + Ok(futures::stream::iter(list).to_output_stream()) +} + +#[cfg(test)] +mod tests { + use super::Date; + use super::ShellError; + + #[test] + fn examples_work_as_expected() -> Result<(), ShellError> { + use crate::examples::test as test_examples; + + Ok(test_examples(Date {})?) + } +} diff --git a/crates/nu-cli/src/commands/date/mod.rs b/crates/nu-cli/src/commands/date/mod.rs index 45f77ce7f6..fb245128c1 100644 --- a/crates/nu-cli/src/commands/date/mod.rs +++ b/crates/nu-cli/src/commands/date/mod.rs @@ -1,11 +1,15 @@ pub mod command; pub mod format; +pub mod list_timezone; pub mod now; -pub mod utc; +pub mod to_table; +pub mod to_timezone; -mod utils; +mod parser; pub use command::Command as Date; pub use format::Date as DateFormat; +pub use list_timezone::Date as DateListTimeZone; pub use now::Date as DateNow; -pub use utc::Date as DateUTC; +pub use to_table::Date as DateToTable; +pub use to_timezone::Date as DateToTimeZone; diff --git a/crates/nu-cli/src/commands/date/now.rs b/crates/nu-cli/src/commands/date/now.rs index 235cf24f75..09bd129b63 100644 --- a/crates/nu-cli/src/commands/date/now.rs +++ b/crates/nu-cli/src/commands/date/now.rs @@ -1,10 +1,8 @@ +use crate::commands::WholeStreamCommand; use crate::prelude::*; use chrono::{DateTime, Local}; use nu_errors::ShellError; - -use crate::commands::date::utils::date_to_value; -use crate::commands::WholeStreamCommand; -use nu_protocol::Signature; +use nu_protocol::{Signature, UntaggedValue}; pub struct Date; @@ -19,7 +17,7 @@ impl WholeStreamCommand for Date { } fn usage(&self) -> &str { - "return the current date." + "Get the current date." } async fn run( @@ -35,16 +33,12 @@ pub async fn now( args: CommandArgs, registry: &CommandRegistry, ) -> Result { - let registry = registry.clone(); let args = args.evaluate_once(®istry).await?; let tag = args.call_info.name_tag.clone(); - let no_fmt = "".to_string(); + let now: DateTime = Local::now(); - let value = { - let local: DateTime = Local::now(); - date_to_value(local, tag, no_fmt) - }; + let value = UntaggedValue::date(now.with_timezone(now.offset())).into_value(&tag); Ok(OutputStream::one(value)) } diff --git a/crates/nu-cli/src/commands/date/parser.rs b/crates/nu-cli/src/commands/date/parser.rs new file mode 100644 index 0000000000..d2d1f3a851 --- /dev/null +++ b/crates/nu-cli/src/commands/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-cli/src/commands/date/to_table.rs b/crates/nu-cli/src/commands/date/to_table.rs new file mode 100644 index 0000000000..4fa2425d21 --- /dev/null +++ b/crates/nu-cli/src/commands/date/to_table.rs @@ -0,0 +1,113 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use chrono::{Datelike, Timelike}; +use indexmap::IndexMap; +use nu_errors::ShellError; +use nu_protocol::{Dictionary, Primitive, ReturnSuccess, Signature, UntaggedValue, Value}; + +pub struct Date; + +#[async_trait] +impl WholeStreamCommand for Date { + 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." + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + to_table(args, registry).await + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Print the current date in a table", + example: "date now | date to-table", + result: None, + }] + } +} + +async fn to_table( + args: CommandArgs, + registry: &CommandRegistry, +) -> Result { + let registry = registry.clone(); + let args = args.evaluate_once(®istry).await?; + let tag = args.call_info.name_tag.clone(); + let input = args.input; + + Ok(input + .map(move |value| match value { + Value { + value: UntaggedValue::Primitive(Primitive::Date(dt)), + .. + } => { + let mut indexmap = IndexMap::new(); + + indexmap.insert( + "year".to_string(), + UntaggedValue::int(dt.year()).into_value(&tag), + ); + indexmap.insert( + "month".to_string(), + UntaggedValue::int(dt.month()).into_value(&tag), + ); + indexmap.insert( + "day".to_string(), + UntaggedValue::int(dt.day()).into_value(&tag), + ); + indexmap.insert( + "hour".to_string(), + UntaggedValue::int(dt.hour()).into_value(&tag), + ); + indexmap.insert( + "minute".to_string(), + UntaggedValue::int(dt.minute()).into_value(&tag), + ); + indexmap.insert( + "second".to_string(), + UntaggedValue::int(dt.second()).into_value(&tag), + ); + + let tz = dt.offset(); + indexmap.insert( + "timezone".to_string(), + UntaggedValue::string(format!("{}", tz)).into_value(&tag), + ); + + let value = UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&tag); + + ReturnSuccess::value(value) + } + _ => Err(ShellError::labeled_error( + "Expected a date from pipeline", + "requires date input", + &tag, + )), + }) + .to_output_stream()) +} + +#[cfg(test)] +mod tests { + use super::Date; + use super::ShellError; + + #[test] + fn examples_work_as_expected() -> Result<(), ShellError> { + use crate::examples::test as test_examples; + + Ok(test_examples(Date {})?) + } +} diff --git a/crates/nu-cli/src/commands/date/to_timezone.rs b/crates/nu-cli/src/commands/date/to_timezone.rs new file mode 100644 index 0000000000..c32e23f920 --- /dev/null +++ b/crates/nu-cli/src/commands/date/to_timezone.rs @@ -0,0 +1,118 @@ +use crate::commands::date::parser::{datetime_in_timezone, ParseErrorKind}; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; +use nu_source::Tagged; + +pub struct Date; + +#[derive(Deserialize)] +struct DateToTimeZoneArgs { + timezone: Tagged, +} + +#[async_trait] +impl WholeStreamCommand for Date { + 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. + +Use `date list-timezone` to list all supported time zones. + " + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + to_timezone(args, registry).await + } + + 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, + }, + ] + } +} + +async fn to_timezone( + args: CommandArgs, + registry: &CommandRegistry, +) -> Result { + let registry = registry.clone(); + let tag = args.call_info.name_tag.clone(); + let (DateToTimeZoneArgs { timezone }, input) = args.process(®istry).await?; + + Ok(input + .map(move |value| match value { + Value { + value: UntaggedValue::Primitive(Primitive::Date(dt)), + .. + } => match datetime_in_timezone(&dt, &timezone.item) { + Ok(dt) => { + let value = UntaggedValue::date(dt).into_value(&tag); + + ReturnSuccess::value(value) + } + Err(e) => Err(ShellError::labeled_error( + error_message(e), + "invalid time zone", + &timezone.tag, + )), + }, + _ => Err(ShellError::labeled_error( + "Expected a date from pipeline", + "requires date input", + &tag, + )), + }) + .to_output_stream()) +} + +fn error_message(err: ParseErrorKind) -> &'static str { + match err { + ParseErrorKind::Invalid => "The time zone description is invalid", + ParseErrorKind::OutOfRange => "The time zone offset is out of range", + ParseErrorKind::TooShort => "The format of the time zone is invalid", + } +} + +#[cfg(test)] +mod tests { + use super::Date; + use super::ShellError; + + #[test] + fn examples_work_as_expected() -> Result<(), ShellError> { + use crate::examples::test as test_examples; + + Ok(test_examples(Date {})?) + } +} diff --git a/crates/nu-cli/src/commands/date/utc.rs b/crates/nu-cli/src/commands/date/utc.rs deleted file mode 100644 index 8a08778c29..0000000000 --- a/crates/nu-cli/src/commands/date/utc.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::prelude::*; -use chrono::{DateTime, Utc}; -use nu_errors::ShellError; - -use crate::commands::date::utils::date_to_value; -use crate::commands::WholeStreamCommand; -use nu_protocol::Signature; - -pub struct Date; - -#[async_trait] -impl WholeStreamCommand for Date { - fn name(&self) -> &str { - "date utc" - } - - fn signature(&self) -> Signature { - Signature::build("date utc") - } - - fn usage(&self) -> &str { - "return the current date in utc." - } - - async fn run( - &self, - args: CommandArgs, - registry: &CommandRegistry, - ) -> Result { - utc(args, registry).await - } -} - -pub async fn utc( - args: CommandArgs, - registry: &CommandRegistry, -) -> Result { - let registry = registry.clone(); - let args = args.evaluate_once(®istry).await?; - let tag = args.call_info.name_tag.clone(); - - let no_fmt = "".to_string(); - - let value = { - let local: DateTime = Utc::now(); - date_to_value(local, tag, no_fmt) - }; - - Ok(OutputStream::one(value)) -} - -#[cfg(test)] -mod tests { - use super::Date; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - Ok(test_examples(Date {})?) - } -} diff --git a/crates/nu-cli/src/commands/date/utils.rs b/crates/nu-cli/src/commands/date/utils.rs deleted file mode 100644 index 026a26dcb9..0000000000 --- a/crates/nu-cli/src/commands/date/utils.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::prelude::*; -use chrono::DateTime; -use nu_protocol::{Dictionary, Value}; - -use chrono::{Datelike, TimeZone, Timelike}; -use core::fmt::Display; -use indexmap::IndexMap; -use nu_protocol::UntaggedValue; - -pub fn date_to_value_raw(dt: DateTime, dt_format: String) -> String -where - T::Offset: Display, -{ - let result = dt.format(&dt_format); - format!("{}", result) -} - -pub fn date_to_value(dt: DateTime, tag: Tag, dt_format: String) -> Value -where - T::Offset: Display, -{ - let mut indexmap = IndexMap::new(); - - if dt_format.is_empty() { - indexmap.insert( - "year".to_string(), - UntaggedValue::int(dt.year()).into_value(&tag), - ); - indexmap.insert( - "month".to_string(), - UntaggedValue::int(dt.month()).into_value(&tag), - ); - indexmap.insert( - "day".to_string(), - UntaggedValue::int(dt.day()).into_value(&tag), - ); - indexmap.insert( - "hour".to_string(), - UntaggedValue::int(dt.hour()).into_value(&tag), - ); - indexmap.insert( - "minute".to_string(), - UntaggedValue::int(dt.minute()).into_value(&tag), - ); - indexmap.insert( - "second".to_string(), - UntaggedValue::int(dt.second()).into_value(&tag), - ); - - let tz = dt.offset(); - indexmap.insert( - "timezone".to_string(), - UntaggedValue::string(format!("{}", tz)).into_value(&tag), - ); - } else { - let result = dt.format(&dt_format); - indexmap.insert( - "formatted".to_string(), - UntaggedValue::string(format!("{}", result)).into_value(&tag), - ); - } - - UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&tag) -} diff --git a/crates/nu-data/src/base.rs b/crates/nu-data/src/base.rs index b49eedb31d..d2e5519892 100644 --- a/crates/nu-data/src/base.rs +++ b/crates/nu-data/src/base.rs @@ -1,7 +1,7 @@ pub(crate) mod shape; use bigdecimal::BigDecimal; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, FixedOffset, Utc}; use derive_new::new; use nu_errors::ShellError; use nu_protocol::{ @@ -75,8 +75,8 @@ pub enum CompareValues { Ints(BigInt, BigInt), Decimals(BigDecimal, BigDecimal), String(String, String), - Date(DateTime, DateTime), - DateDuration(DateTime, BigInt), + Date(DateTime, DateTime), + DateDuration(DateTime, BigInt), Booleans(bool, bool), } @@ -94,9 +94,10 @@ impl CompareValues { Span::unknown(), ) .expect("Could not convert nushell Duration into chrono Duration."); - let right: DateTime = Utc::now() + let right: DateTime = Utc::now() .checked_sub_signed(duration) - .expect("Data overflow"); + .expect("Data overflow") + .into(); right.cmp(left) } CompareValues::Booleans(left, right) => left.cmp(right), diff --git a/crates/nu-data/src/base/shape.rs b/crates/nu-data/src/base/shape.rs index 69a1e81f96..afe6ed6bba 100644 --- a/crates/nu-data/src/base/shape.rs +++ b/crates/nu-data/src/base/shape.rs @@ -1,6 +1,6 @@ // use crate::config::{Conf, NuConfig}; use bigdecimal::BigDecimal; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, FixedOffset}; use indexmap::map::IndexMap; use nu_protocol::RangeInclusion; use nu_protocol::{format_primitive, ColumnPath, Dictionary, Primitive, UntaggedValue, Value}; @@ -31,7 +31,7 @@ pub enum InlineShape { ColumnPath(ColumnPath), Pattern(String), Boolean(bool), - Date(DateTime), + Date(DateTime), Duration(BigInt), Path(PathBuf), Binary(usize), diff --git a/crates/nu-data/src/value.rs b/crates/nu-data/src/value.rs index e0c1897254..d77996f306 100644 --- a/crates/nu-data/src/value.rs +++ b/crates/nu-data/src/value.rs @@ -25,7 +25,7 @@ impl Date { let date = date.with_timezone(&chrono::offset::Utc); - Ok(UntaggedValue::Primitive(Primitive::Date(date))) + Ok(UntaggedValue::Primitive(Primitive::Date(date.into()))) } pub fn naive_from_str(s: Tagged<&str>) -> Result { @@ -38,7 +38,7 @@ impl Date { })?; Ok(UntaggedValue::Primitive(Primitive::Date( - DateTime::::from_utc(date.and_hms(12, 34, 56), Utc), + DateTime::::from_utc(date.and_hms(12, 34, 56), Utc).into(), ))) } } diff --git a/crates/nu-protocol/src/value.rs b/crates/nu-protocol/src/value.rs index 67c9bafccd..85102151e6 100644 --- a/crates/nu-protocol/src/value.rs +++ b/crates/nu-protocol/src/value.rs @@ -19,7 +19,7 @@ use crate::value::range::{Range, RangeInclusion}; use crate::ColumnPath; use bigdecimal::BigDecimal; use bigdecimal::FromPrimitive; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, FixedOffset, Utc}; use indexmap::IndexMap; use nu_errors::ShellError; use nu_source::{AnchorLocation, HasSpan, Span, Spanned, SpannedItem, Tag}; @@ -248,10 +248,11 @@ impl UntaggedValue { /// Helper for creating datatime values pub fn system_date(s: SystemTime) -> UntaggedValue { - UntaggedValue::Primitive(Primitive::Date(s.into())) + let utc: DateTime = s.into(); + UntaggedValue::Primitive(Primitive::Date(utc.into())) } - pub fn date(d: impl Into>) -> UntaggedValue { + pub fn date(d: impl Into>) -> UntaggedValue { UntaggedValue::Primitive(Primitive::Date(d.into())) } @@ -924,7 +925,7 @@ pub trait DateTimeExt { fn to_value_create_tag(&self) -> Value; } -impl DateTimeExt for DateTime { +impl DateTimeExt for DateTime { fn to_value(&self, the_tag: Tag) -> Value { Value { value: UntaggedValue::Primitive(Primitive::Date(*self)), diff --git a/crates/nu-protocol/src/value/primitive.rs b/crates/nu-protocol/src/value/primitive.rs index 3d06eac10c..e7f6271f23 100644 --- a/crates/nu-protocol/src/value/primitive.rs +++ b/crates/nu-protocol/src/value/primitive.rs @@ -3,7 +3,7 @@ use crate::value::column_path::ColumnPath; use crate::value::range::{Range, RangeInclusion}; use crate::value::{serde_bigdecimal, serde_bigint}; use bigdecimal::BigDecimal; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, FixedOffset, Utc}; use nu_errors::{ExpectedRange, ShellError}; use nu_source::{PrettyDebug, Span, SpannedItem}; use num_bigint::BigInt; @@ -42,8 +42,8 @@ pub enum Primitive { Pattern(String), /// A boolean value Boolean(bool), - /// A date value, in UTC - Date(DateTime), + /// A date value + Date(DateTime), /// A count in the number of nanoseconds #[serde(with = "serde_bigint")] Duration(BigInt), @@ -385,8 +385,8 @@ pub fn format_duration(duration: &BigInt) -> String { } #[allow(clippy::cognitive_complexity)] -/// Format a UTC date value into a humanized string (eg "1 week ago" instead of a formal date string) -pub fn format_date(d: &DateTime) -> String { +/// Format a date value into a humanized string (eg "1 week ago" instead of a formal date string) +pub fn format_date(d: &DateTime) -> String { let utc: DateTime = Utc::now(); let duration = utc.signed_duration_since(*d); diff --git a/crates/nu-test-support/src/value.rs b/crates/nu-test-support/src/value.rs index 000fe16044..e6f2168922 100644 --- a/crates/nu-test-support/src/value.rs +++ b/crates/nu-test-support/src/value.rs @@ -40,10 +40,9 @@ pub fn date(input: impl Into) -> Value { let date = NaiveDate::parse_from_str(key.borrow_tagged().item, "%Y-%m-%d") .expect("date from string failed"); - UntaggedValue::Primitive(Primitive::Date(DateTime::::from_utc( - date.and_hms(12, 34, 56), - Utc, - ))) + UntaggedValue::Primitive(Primitive::Date( + DateTime::::from_utc(date.and_hms(12, 34, 56), Utc).into(), + )) .into_untagged_value() } diff --git a/crates/nu_plugin_from_bson/src/from_bson.rs b/crates/nu_plugin_from_bson/src/from_bson.rs index 978e4c5402..a1a1f8fcb4 100644 --- a/crates/nu_plugin_from_bson/src/from_bson.rs +++ b/crates/nu_plugin_from_bson/src/from_bson.rs @@ -130,7 +130,9 @@ fn convert_bson_value_to_nu_value(v: &Bson, tag: impl Into) -> Result UntaggedValue::Primitive(Primitive::Date(*dt)).into_value(&tag), + Bson::UtcDatetime(dt) => { + UntaggedValue::Primitive(Primitive::Date((*dt).into())).into_value(&tag) + } Bson::Symbol(s) => { let mut collected = TaggedDictBuilder::new(tag.clone()); collected.insert_value( diff --git a/crates/nu_plugin_to_bson/src/to_bson.rs b/crates/nu_plugin_to_bson/src/to_bson.rs index 64fde09871..aefa9ddc03 100644 --- a/crates/nu_plugin_to_bson/src/to_bson.rs +++ b/crates/nu_plugin_to_bson/src/to_bson.rs @@ -29,7 +29,7 @@ pub fn value_to_bson_value(v: &Value) -> Result { .expect("Unimplemented BUG: What about big decimals?"), ), UntaggedValue::Primitive(Primitive::Duration(i)) => Bson::String(i.to_string()), - UntaggedValue::Primitive(Primitive::Date(d)) => Bson::UtcDatetime(*d), + UntaggedValue::Primitive(Primitive::Date(d)) => Bson::UtcDatetime((*d).into()), UntaggedValue::Primitive(Primitive::EndOfStream) => Bson::Null, UntaggedValue::Primitive(Primitive::BeginningOfStream) => Bson::Null, UntaggedValue::Primitive(Primitive::Decimal(d)) => {