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