use chrono::{DateTime, Local, Locale, TimeZone}; use nu_engine::CallExt; use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, }; use nu_utils::locale::get_system_locale_string; use std::fmt::{Display, Write}; use super::utils::parse_date_from_string; #[derive(Clone)] pub struct SubCommand; impl Command for SubCommand { fn name(&self) -> &str { "date format" } fn signature(&self) -> Signature { Signature::build("date format") .input_output_types(vec![ (Type::Date, Type::String), (Type::String, Type::String), ]) .allow_variants_without_examples(true) // https://github.com/nushell/nushell/issues/7032 .switch("list", "lists strftime cheatsheet", Some('l')) .optional( "format string", SyntaxShape::String, "the desired date format", ) .category(Category::Date) } fn usage(&self) -> &str { "Format a given date using a format string." } fn search_terms(&self) -> Vec<&str> { vec!["fmt", "strftime"] } fn run( &self, engine_state: &EngineState, stack: &mut Stack, call: &Call, input: PipelineData, ) -> Result { let head = call.head; if call.has_flag("list") { return Ok(PipelineData::Value( generate_strftime_list(head, false), None, )); } let format = call.opt::>(engine_state, stack, 0)?; // This doesn't match explicit nulls if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } input.map( move |value| match &format { Some(format) => format_helper(value, format.item.as_str(), format.span, head), None => format_helper_rfc2822(value, head), }, engine_state.ctrlc.clone(), ) } fn examples(&self) -> Vec { vec![ // TODO: This should work but does not; see https://github.com/nushell/nushell/issues/7032 // Example { // description: "Format a given date-time using the default format (RFC 2822).", // example: r#"'2021-10-22 20:00:12 +01:00' | into datetime | date format"#, // result: Some(Value::String { // val: "Fri, 22 Oct 2021 20:00:12 +0100".to_string(), // span: Span::test_data(), // }), // }, Example { description: "Format a given date-time as a string using the default format (RFC 2822).", example: r#""2021-10-22 20:00:12 +01:00" | date format"#, result: Some(Value::String { val: "Fri, 22 Oct 2021 20:00:12 +0100".to_string(), span: Span::test_data(), }), }, Example { description: "Format the current date-time using a given format string.", example: r#"date now | date format "%Y-%m-%d %H:%M:%S""#, result: None, }, Example { description: "Format the current date using a given format string.", example: r#"date now | date format "%Y-%m-%d %H:%M:%S""#, result: None, }, Example { description: "Format a given date using a given format string.", example: r#""2021-10-22 20:00:12 +01:00" | date format "%Y-%m-%d""#, result: Some(Value::test_string("2021-10-22")), }, ] } } fn format_from(date_time: DateTime, formatter: &str, span: Span) -> Value where Tz::Offset: Display, { let mut formatter_buf = String::new(); let locale: Locale = get_system_locale_string() .map(|l| l.replace('-', "_")) // `chrono::Locale` needs something like `xx_xx`, rather than `xx-xx` .unwrap_or_else(|| String::from("en_US")) .as_str() .try_into() .unwrap_or(Locale::en_US); let format = date_time.format_localized(formatter, locale); match formatter_buf.write_fmt(format_args!("{format}")) { Ok(_) => Value::String { val: formatter_buf, span, }, Err(_) => Value::Error { error: Box::new(ShellError::TypeMismatch { err_message: "invalid format".to_string(), span, }), }, } } fn format_helper(value: Value, formatter: &str, formatter_span: Span, head_span: Span) -> Value { match value { Value::Date { val, .. } => format_from(val, formatter, formatter_span), Value::String { val, .. } => { let dt = parse_date_from_string(&val, formatter_span); match dt { Ok(x) => format_from(x, formatter, formatter_span), Err(e) => e, } } _ => Value::Error { error: Box::new(ShellError::DatetimeParseError( value.debug_value(), head_span, )), }, } } fn format_helper_rfc2822(value: Value, span: Span) -> Value { match value { Value::Date { val, span: _ } => Value::String { val: val.to_rfc2822(), span, }, Value::String { val, span: val_span, } => { let dt = parse_date_from_string(&val, val_span); match dt { Ok(x) => Value::String { val: x.to_rfc2822(), span, }, Err(e) => e, } } _ => Value::Error { error: Box::new(ShellError::DatetimeParseError(value.debug_value(), span)), }, } } /// Generates a table containing available datetime format specifiers /// /// # Arguments /// * `head` - use the call's head /// * `show_parse_only_formats` - whether parse-only format specifiers (that can't be outputted) should be shown. Should only be used for `into datetime`, not `date format` pub(crate) fn generate_strftime_list(head: Span, show_parse_only_formats: bool) -> Value { let column_names = vec![ "Specification".into(), "Example".into(), "Description".into(), ]; let now = Local::now(); struct FormatSpecification<'a> { spec: &'a str, description: &'a str, } let specifications = vec![ FormatSpecification { spec: "%Y", description: "The full proleptic Gregorian year, zero-padded to 4 digits.", }, FormatSpecification { spec: "%C", description: "The proleptic Gregorian year divided by 100, zero-padded to 2 digits.", }, FormatSpecification { spec: "%y", description: "The proleptic Gregorian year modulo 100, zero-padded to 2 digits.", }, FormatSpecification { spec: "%m", description: "Month number (01--12), zero-padded to 2 digits.", }, FormatSpecification { spec: "%b", description: "Abbreviated month name. Always 3 letters.", }, FormatSpecification { spec: "%B", description: "Full month name. Also accepts corresponding abbreviation in parsing.", }, FormatSpecification { spec: "%h", description: "Same as %b.", }, FormatSpecification { spec: "%d", description: "Day number (01--31), zero-padded to 2 digits.", }, FormatSpecification { spec: "%e", description: "Same as %d but space-padded. Same as %_d.", }, FormatSpecification { spec: "%a", description: "Abbreviated weekday name. Always 3 letters.", }, FormatSpecification { spec: "%A", description: "Full weekday name. Also accepts corresponding abbreviation in parsing.", }, FormatSpecification { spec: "%w", description: "Sunday = 0, Monday = 1, ..., Saturday = 6.", }, FormatSpecification { spec: "%u", description: "Monday = 1, Tuesday = 2, ..., Sunday = 7. (ISO 8601)", }, FormatSpecification { spec: "%U", description: "Week number starting with Sunday (00--53), zero-padded to 2 digits.", }, FormatSpecification { spec: "%W", description: "Same as %U, but week 1 starts with the first Monday in that year instead.", }, FormatSpecification { spec: "%G", description: "Same as %Y but uses the year number in ISO 8601 week date.", }, FormatSpecification { spec: "%g", description: "Same as %y but uses the year number in ISO 8601 week date.", }, FormatSpecification { spec: "%V", description: "Same as %U but uses the week number in ISO 8601 week date (01--53).", }, FormatSpecification { spec: "%j", description: "Day of the year (001--366), zero-padded to 3 digits.", }, FormatSpecification { spec: "%D", description: "Month-day-year format. Same as %m/%d/%y.", }, FormatSpecification { spec: "%x", description: "Locale's date representation (e.g., 12/31/99).", }, FormatSpecification { spec: "%F", description: "Year-month-day format (ISO 8601). Same as %Y-%m-%d.", }, FormatSpecification { spec: "%v", description: "Day-month-year format. Same as %e-%b-%Y.", }, FormatSpecification { spec: "%H", description: "Hour number (00--23), zero-padded to 2 digits.", }, FormatSpecification { spec: "%k", description: "Same as %H but space-padded. Same as %_H.", }, FormatSpecification { spec: "%I", description: "Hour number in 12-hour clocks (01--12), zero-padded to 2 digits.", }, FormatSpecification { spec: "%l", description: "Same as %I but space-padded. Same as %_I.", }, FormatSpecification { spec: "%P", description: "am or pm in 12-hour clocks.", }, FormatSpecification { spec: "%p", description: "AM or PM in 12-hour clocks.", }, FormatSpecification { spec: "%M", description: "Minute number (00--59), zero-padded to 2 digits.", }, FormatSpecification { spec: "%S", description: "Second number (00--60), zero-padded to 2 digits.", }, FormatSpecification { spec: "%f", description: "The fractional seconds (in nanoseconds) since last whole second.", }, FormatSpecification { spec: "%.f", description: "Similar to .%f but left-aligned. These all consume the leading dot.", }, FormatSpecification { spec: "%.3f", description: "Similar to .%f but left-aligned but fixed to a length of 3.", }, FormatSpecification { spec: "%.6f", description: "Similar to .%f but left-aligned but fixed to a length of 6.", }, FormatSpecification { spec: "%.9f", description: "Similar to .%f but left-aligned but fixed to a length of 9.", }, FormatSpecification { spec: "%3f", description: "Similar to %.3f but without the leading dot.", }, FormatSpecification { spec: "%6f", description: "Similar to %.6f but without the leading dot.", }, FormatSpecification { spec: "%9f", description: "Similar to %.9f but without the leading dot.", }, FormatSpecification { spec: "%R", description: "Hour-minute format. Same as %H:%M.", }, FormatSpecification { spec: "%T", description: "Hour-minute-second format. Same as %H:%M:%S.", }, FormatSpecification { spec: "%X", description: "Locale's time representation (e.g., 23:13:48).", }, FormatSpecification { spec: "%r", description: "Hour-minute-second format in 12-hour clocks. Same as %I:%M:%S %p.", }, FormatSpecification { spec: "%Z", description: "Local time zone name. Skips all non-whitespace characters during parsing.", }, FormatSpecification { spec: "%z", description: "Offset from the local time to UTC (with UTC being +0000).", }, FormatSpecification { spec: "%:z", description: "Same as %z but with a colon.", }, FormatSpecification { spec: "%c", description: "Locale's date and time (e.g., Thu Mar 3 23:05:25 2005).", }, FormatSpecification { spec: "%+", description: "ISO 8601 / RFC 3339 date & time format.", }, FormatSpecification { spec: "%s", description: "UNIX timestamp, the number of seconds since 1970-01-01", }, FormatSpecification { spec: "%t", description: "Literal tab (\\t).", }, FormatSpecification { spec: "%n", description: "Literal newline (\\n).", }, FormatSpecification { spec: "%%", description: "Literal percent sign.", }, ]; let mut records = specifications .iter() .map(|s| Value::Record { cols: column_names.clone(), vals: vec![ Value::string(s.spec, head), Value::string(now.format(s.spec).to_string(), head), Value::string(s.description, head), ], span: head, }) .collect::>(); if show_parse_only_formats { // now.format("%#z") will panic since it is parse-only // so here we emulate how it will look: let example = now .format("%:z") // e.g. +09:30 .to_string() .get(0..3) // +09:30 -> +09 .unwrap_or("") .to_string(); records.push(Value::Record { cols: column_names, vals: vec![ Value::string("%#z", head), Value::String { val: example, span: head, }, Value::string( "Parsing only: Same as %z but allows minutes to be missing or present.", head, ), ], span: head, }); } Value::List { vals: records, span: head, } } #[cfg(test)] mod test { use super::*; #[test] fn test_examples() { use crate::test_examples; test_examples(SubCommand {}) } }