Repository: nushell/engine-q
Pull request #268 (2021-10-31 07:02:22)

Port date commands to enqine-q
I ported the date commands to engine-q. One thing I am not sure about is the `to-table` command. The result is a "column" table and not a "row" table. Is my understanding correct that 1-row tables are automatically pivoted to vertical tables?

Looking forward to feedback :-)

Issue reference: https://github.com/nushell/engine-q/issues/242


Patch:
diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml
index 1a805e4..233dc75 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 0000000..3206b3b
--- /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<PipelineData, ShellError> {
+        date(engine_state, stack, call)
+    }
+}
+
+fn date(
+    engine_state: &EngineState,
+    _stack: &mut Stack,
+    call: &Call,
+) -> Result<PipelineData, ShellError> {
+    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 0000000..abee665
--- /dev/null
+++ b/crates/nu-command/src/date/format.rs
@@ -0,0 +1,116 @@
+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<nu_protocol::PipelineData, nu_protocol::ShellError> {
+        let head = call.head;
+        let formatter: Spanned<String> = call.req(engine_state, stack, 0)?;
+        input.map(
+            move |value| format_helper(value, &formatter, head),
+            engine_state.ctrlc.clone(),
+        )
+    }
+
+    fn examples(&self) -> Vec<Example> {
+        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<String>, span: Span) -> Value {
+    match value {
+        Value::Date { val, span: _ } => Value::String {
+            val: val.format(formatter.item.as_str()).to_string(),
+            span,
+        },
+        Value::String { val, span: _ } => {
+            let dt = parse_date_from_string(val);
+            match dt {
+                Ok(x) => Value::String {
+                    val: x.format(formatter.item.as_str()).to_string(),
+                    span,
+                },
+                Err(e) => e,
+            }
+        }
+        Value::Nothing { span: _ } => {
+            let dt = Local::now();
+            Value::String {
+                val: dt
+                    .with_timezone(dt.offset())
+                    .format(formatter.item.as_str())
+                    .to_string(),
+                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 0000000..692b002
--- /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<nu_protocol::PipelineData, nu_protocol::ShellError> {
+        let head = call.head;
+        input.map(move |value| helper(value, head), engine_state.ctrlc.clone())
+    }
+
+    fn examples(&self) -> Vec<Example> {
+        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<FixedOffset>) -> 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 0000000..7e4ba8e
--- /dev/null
+++ b/crates/nu-command/src/date/list_timezone.rs
@@ -0,0 +1,44 @@
+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<nu_protocol::PipelineData, nu_protocol::ShellError> {
+        let span = call.head;
+
+        Ok(TZ_VARIANTS
+            .iter()
+            .map(move |x| {
+                let cols = vec!["timezone".into()];
+                let vals = vec![Value::String {
+                    val: x.name().to_string(),
+                    span,
+                }];
+                Value::Record { cols, vals, span }
+            })
+            .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 0000000..0f0d331
--- /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 0000000..dbd0f08
--- /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::{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<nu_protocol::PipelineData, nu_protocol::ShellError> {
+        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 0000000..d2d1f3a
--- /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<FixedOffset>,
+    s: &str,
+) -> Result<DateTime<FixedOffset>, 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<Tz, ParseErrorKind> {
+    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<i32, ParseErrorKind> {
+    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 0000000..91bd59a
--- /dev/null
+++ b/crates/nu-command/src/date/to_table.rs
@@ -0,0 +1,163 @@
+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<nu_protocol::PipelineData, nu_protocol::ShellError> {
+        let head = call.head;
+        input.map(move |value| helper(value, head), engine_state.ctrlc.clone())
+    }
+
+    fn examples(&self) -> Vec<Example> {
+        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 },
+                        Value::Int { val: 4, span },
+                        Value::Int { val: 12, span },
+                        Value::Int { val: 22, span },
+                        Value::Int { val: 10, span },
+                        Value::Int { val: 57, span },
+                        Value::String {
+                            val: "+02:00".to_string(),
+                            span,
+                        },
+                    ];
+                    Some(Value::List {
+                        vals: vec![Value::Record { cols, vals, span }],
+                        span,
+                    })
+                },
+            },
+        ]
+    }
+}
+
+fn parse_date_into_table(date: Result<DateTime<FixedOffset>, 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::List {
+                vals: vec![Value::Record {
+                    cols,
+                    vals,
+                    span: head,
+                }],
+                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 0000000..0b3d508
--- /dev/null
+++ b/crates/nu-command/src/date/to_timezone.rs
@@ -0,0 +1,129 @@
+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<nu_protocol::PipelineData, nu_protocol::ShellError> {
+        let head = call.head;
+        let timezone: Spanned<String> = 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<Example> {
+        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<String>) -> Value {
+    match value {
+        Value::Date { val, span: _ } => _to_timezone(val, timezone, head),
+        Value::String { val, span: _ } => {
+            let time = parse_date_from_string(val);
+            match time {
+                Ok(dt) => _to_timezone(dt, timezone, head),
+                Err(e) => e,
+            }
+        }
+
+        Value::Nothing { span: _ } => {
+            let dt = Local::now();
+            _to_timezone(dt.with_timezone(dt.offset()), timezone, head)
+        }
+        _ => unsupported_input_error(),
+    }
+}
+
+fn _to_timezone(dt: DateTime<FixedOffset>, timezone: &Spanned<String>, span: Span) -> Value {
+    match datetime_in_timezone(&dt, timezone.item.as_str()) {
+        Ok(dt) => Value::Date { val: dt, 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 0000000..d132076
--- /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<DateTime<FixedOffset>, 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 32cb38a..756412c 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/lib.rs b/crates/nu-command/src/lib.rs
index 151c607..667d99b 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 b8b2c30..b6ea1e2 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)
             }


Test patch:
diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs
index f5879d5..f842dc2 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));