Skip to main content

nu_command/strings/format/
date.rs

1use crate::{generate_strftime_list, parse_date_from_string};
2use chrono::{DateTime, Datelike, Locale, TimeZone};
3use nu_engine::command_prelude::*;
4use nu_protocol::shell_error::generic::GenericError;
5
6use std::fmt::{Display, Write};
7
8#[derive(Clone)]
9pub struct FormatDate;
10
11impl Command for FormatDate {
12    fn name(&self) -> &str {
13        "format date"
14    }
15
16    fn signature(&self) -> Signature {
17        Signature::build("format date")
18            .input_output_types(vec![
19                (Type::Date, Type::String),
20                (Type::String, Type::String),
21                (Type::Nothing, Type::table()),
22                // FIXME Type::Any input added to disable pipeline input type checking, as run-time checks can raise undesirable type errors
23                // which aren't caught by the parser. see https://github.com/nushell/nushell/pull/14922 for more details
24                // only applicable for --list flag
25                (Type::Any, Type::table()),
26            ])
27            .allow_variants_without_examples(true) // https://github.com/nushell/nushell/issues/7032
28            .switch("list", "Lists strftime cheatsheet.", Some('l'))
29            .optional(
30                "format string",
31                SyntaxShape::String,
32                "The desired format date.",
33            )
34            .category(Category::Strings)
35    }
36
37    fn description(&self) -> &str {
38        "Format a given date using a format string."
39    }
40
41    fn search_terms(&self) -> Vec<&str> {
42        vec!["fmt", "strftime"]
43    }
44
45    fn examples(&self) -> Vec<Example<'_>> {
46        vec![
47            Example {
48                description: "Format a given date-time using the default format (RFC 2822).",
49                example: "'2021-10-22 20:00:12 +01:00' | into datetime | format date",
50                result: Some(Value::string(
51                    "Fri, 22 Oct 2021 20:00:12 +0100".to_string(),
52                    Span::test_data(),
53                )),
54            },
55            Example {
56                description: "Format a given date-time as a string using the default format (RFC 2822).",
57                example: r#""2021-10-22 20:00:12 +01:00" | format date"#,
58                result: Some(Value::string(
59                    "Fri, 22 Oct 2021 20:00:12 +0100".to_string(),
60                    Span::test_data(),
61                )),
62            },
63            Example {
64                description: "Format a given date-time according to the RFC 3339 standard.",
65                example: r#"'2021-10-22 20:00:12 +01:00' | into datetime | format date "%+""#,
66                result: Some(Value::string(
67                    "2021-10-22T20:00:12+01:00".to_string(),
68                    Span::test_data(),
69                )),
70            },
71            Example {
72                description: "Format the current date-time using a given format string.",
73                example: r#"date now | format date "%Y-%m-%d %H:%M:%S""#,
74                result: None,
75            },
76            Example {
77                description: "Format the current date using a given format string.",
78                example: r#"date now | format date "%Y-%m-%d %H:%M:%S""#,
79                result: None,
80            },
81            Example {
82                description: "Format a given date using a given format string.",
83                example: r#""2021-10-22 20:00:12 +01:00" | format date "%Y-%m-%d""#,
84                result: Some(Value::test_string("2021-10-22")),
85            },
86        ]
87    }
88
89    fn is_const(&self) -> bool {
90        true
91    }
92
93    fn run(
94        &self,
95        engine_state: &EngineState,
96        stack: &mut Stack,
97        call: &Call,
98        input: PipelineData,
99    ) -> Result<PipelineData, ShellError> {
100        let list = call.has_flag(engine_state, stack, "list")?;
101        let format = call.opt::<Spanned<String>>(engine_state, stack, 0)?;
102        let locale = get_locale(|name| stack.get_env_var(engine_state, name)?.as_str().ok());
103
104        run(engine_state, call, input, list, format, locale)
105    }
106
107    fn run_const(
108        &self,
109        working_set: &StateWorkingSet,
110        call: &Call,
111        input: PipelineData,
112    ) -> Result<PipelineData, ShellError> {
113        let list = call.has_flag_const(working_set, "list")?;
114        let format = call.opt_const::<Spanned<String>>(working_set, 0)?;
115        let locale = get_locale(|name| working_set.get_env_var(name)?.as_str().ok());
116
117        run(working_set.permanent(), call, input, list, format, locale)
118    }
119}
120
121fn get_locale<'a, F>(env_getter: F) -> Locale
122where
123    F: Fn(&str) -> Option<&'a str> + 'a,
124{
125    nu_utils::get_locale_from_env_vars(Some("LC_TIME"), env_getter)
126        .and_then(|s| Locale::try_from(s.as_ref()).ok())
127        .unwrap_or(Locale::en_US)
128}
129
130fn run(
131    engine_state: &EngineState,
132    call: &Call,
133    input: PipelineData,
134    list: bool,
135    format: Option<Spanned<String>>,
136    locale: Locale,
137) -> Result<PipelineData, ShellError> {
138    let head = call.head;
139    if list {
140        return Ok(PipelineData::value(
141            generate_strftime_list(head, false),
142            None,
143        ));
144    }
145
146    // This doesn't match explicit nulls
147    if let PipelineData::Empty = input {
148        return Err(ShellError::PipelineEmpty { dst_span: head });
149    }
150    input.map(
151        move |value| match &format {
152            Some(format) => format_helper(value, format.item.as_str(), format.span, head, locale),
153            None => format_helper_rfc2822(value, head),
154        },
155        engine_state.signals(),
156    )
157}
158
159fn format_from<Tz: TimeZone>(
160    date_time: DateTime<Tz>,
161    formatter: &str,
162    span: Span,
163    locale: Locale,
164) -> Value
165where
166    Tz::Offset: Display,
167{
168    let mut formatter_buf = String::new();
169    // Handle custom format specifiers for compact formats
170    let processed_formatter = formatter
171        .replace("%J", "%Y%m%d") // %J for joined date (YYYYMMDD)
172        .replace("%Q", "%H%M%S"); // %Q for sequential time (HHMMSS)
173    let format = date_time.format_localized(&processed_formatter, locale);
174
175    match formatter_buf.write_fmt(format_args!("{format}")) {
176        Ok(_) => Value::string(formatter_buf, span),
177        Err(_) => Value::error(
178            ShellError::TypeMismatch {
179                err_message: "invalid format".to_string(),
180                span,
181            },
182            span,
183        ),
184    }
185}
186
187fn format_helper(
188    value: Value,
189    formatter: &str,
190    formatter_span: Span,
191    head_span: Span,
192    locale: Locale,
193) -> Value {
194    match value {
195        Value::Date { val, .. } => format_from(val, formatter, formatter_span, locale),
196        Value::String { val, .. } => {
197            let dt = parse_date_from_string(&val, formatter_span);
198
199            match dt {
200                Ok(x) => format_from(x, formatter, formatter_span, locale),
201                Err(e) => e,
202            }
203        }
204        _ => Value::error(
205            ShellError::OnlySupportsThisInputType {
206                exp_input_type: "date, string (that represents datetime)".into(),
207                wrong_type: value.get_type().to_string(),
208                dst_span: head_span,
209                src_span: value.span(),
210            },
211            head_span,
212        ),
213    }
214}
215
216fn format_helper_rfc2822(value: Value, span: Span) -> Value {
217    let val_span = value.span();
218    match value {
219        Value::Date { val, .. } => Value::string(
220            {
221                if val.year() >= 0 && val.year() <= 9999 {
222                    val.to_rfc2822()
223                } else {
224                    return Value::error(
225                        ShellError::Generic(
226                            GenericError::new(
227                                "Can't convert date to RFC 2822 format.",
228                                "the RFC 2822 format only supports years 0 through 9999",
229                                val_span,
230                            )
231                            .with_help(r#"use the RFC 3339 format option: "%+""#),
232                        ),
233                        span,
234                    );
235                }
236            },
237            span,
238        ),
239        Value::String { val, .. } => {
240            let dt = parse_date_from_string(&val, val_span);
241            match dt {
242                Ok(x) => Value::string(
243                    {
244                        if x.year() >= 0 && x.year() <= 9999 {
245                            x.to_rfc2822()
246                        } else {
247                            return Value::error(
248                                ShellError::Generic(
249                                    GenericError::new(
250                                        "Can't convert date to RFC 2822 format.",
251                                        "the RFC 2822 format only supports years 0 through 9999",
252                                        val_span,
253                                    )
254                                    .with_help(r#"use the RFC 3339 format option: "%+""#),
255                                ),
256                                span,
257                            );
258                        }
259                    },
260                    span,
261                ),
262                Err(e) => e,
263            }
264        }
265        _ => Value::error(
266            ShellError::OnlySupportsThisInputType {
267                exp_input_type: "date, string (that represents datetime)".into(),
268                wrong_type: value.get_type().to_string(),
269                dst_span: span,
270                src_span: val_span,
271            },
272            span,
273        ),
274    }
275}
276
277#[cfg(test)]
278mod test {
279    use super::*;
280
281    #[test]
282    fn test_examples() -> nu_test_support::Result {
283        nu_test_support::test().examples(FormatDate)
284    }
285}