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::*;
4
5use nu_utils::locale::{LOCALE_OVERRIDE_ENV_VAR, get_system_locale_string};
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: r#"'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
103        // get the locale first so we can use the proper get_env_var functions since this is a const command
104        // we can override the locale by setting $env.NU_TEST_LOCALE_OVERRIDE or $env.LC_TIME
105        let locale = if let Some(loc) = engine_state
106            .get_env_var(LOCALE_OVERRIDE_ENV_VAR)
107            .or_else(|| engine_state.get_env_var("LC_TIME"))
108        {
109            let locale_str = loc.as_str()?.split('.').next().unwrap_or("en_US");
110            locale_str.try_into().unwrap_or(Locale::en_US)
111        } else {
112            get_system_locale_string()
113                .map(|l| l.replace('-', "_"))
114                .unwrap_or_else(|| String::from("en_US"))
115                .as_str()
116                .try_into()
117                .unwrap_or(Locale::en_US)
118        };
119
120        run(engine_state, call, input, list, format, locale)
121    }
122
123    fn run_const(
124        &self,
125        working_set: &StateWorkingSet,
126        call: &Call,
127        input: PipelineData,
128    ) -> Result<PipelineData, ShellError> {
129        let list = call.has_flag_const(working_set, "list")?;
130        let format = call.opt_const::<Spanned<String>>(working_set, 0)?;
131
132        // get the locale first so we can use the proper get_env_var functions since this is a const command
133        // we can override the locale by setting $env.NU_TEST_LOCALE_OVERRIDE or $env.LC_TIME
134        let locale = if let Some(loc) = working_set
135            .get_env_var(LOCALE_OVERRIDE_ENV_VAR)
136            .or_else(|| working_set.get_env_var("LC_TIME"))
137        {
138            let locale_str = loc.as_str()?.split('.').next().unwrap_or("en_US");
139            locale_str.try_into().unwrap_or(Locale::en_US)
140        } else {
141            get_system_locale_string()
142                .map(|l| l.replace('-', "_"))
143                .unwrap_or_else(|| String::from("en_US"))
144                .as_str()
145                .try_into()
146                .unwrap_or(Locale::en_US)
147        };
148
149        run(working_set.permanent(), call, input, list, format, locale)
150    }
151}
152
153fn run(
154    engine_state: &EngineState,
155    call: &Call,
156    input: PipelineData,
157    list: bool,
158    format: Option<Spanned<String>>,
159    locale: Locale,
160) -> Result<PipelineData, ShellError> {
161    let head = call.head;
162    if list {
163        return Ok(PipelineData::Value(
164            generate_strftime_list(head, false),
165            None,
166        ));
167    }
168
169    // This doesn't match explicit nulls
170    if matches!(input, PipelineData::Empty) {
171        return Err(ShellError::PipelineEmpty { dst_span: head });
172    }
173    input.map(
174        move |value| match &format {
175            Some(format) => format_helper(value, format.item.as_str(), format.span, head, locale),
176            None => format_helper_rfc2822(value, head),
177        },
178        engine_state.signals(),
179    )
180}
181
182fn format_from<Tz: TimeZone>(
183    date_time: DateTime<Tz>,
184    formatter: &str,
185    span: Span,
186    locale: Locale,
187) -> Value
188where
189    Tz::Offset: Display,
190{
191    let mut formatter_buf = String::new();
192    let format = date_time.format_localized(formatter, locale);
193
194    match formatter_buf.write_fmt(format_args!("{format}")) {
195        Ok(_) => Value::string(formatter_buf, span),
196        Err(_) => Value::error(
197            ShellError::TypeMismatch {
198                err_message: "invalid format".to_string(),
199                span,
200            },
201            span,
202        ),
203    }
204}
205
206fn format_helper(
207    value: Value,
208    formatter: &str,
209    formatter_span: Span,
210    head_span: Span,
211    locale: Locale,
212) -> Value {
213    match value {
214        Value::Date { val, .. } => format_from(val, formatter, formatter_span, locale),
215        Value::String { val, .. } => {
216            let dt = parse_date_from_string(&val, formatter_span);
217
218            match dt {
219                Ok(x) => format_from(x, formatter, formatter_span, locale),
220                Err(e) => e,
221            }
222        }
223        _ => Value::error(
224            ShellError::OnlySupportsThisInputType {
225                exp_input_type: "date, string (that represents datetime)".into(),
226                wrong_type: value.get_type().to_string(),
227                dst_span: head_span,
228                src_span: value.span(),
229            },
230            head_span,
231        ),
232    }
233}
234
235fn format_helper_rfc2822(value: Value, span: Span) -> Value {
236    let val_span = value.span();
237    match value {
238        Value::Date { val, .. } => Value::string(
239            {
240                if val.year() >= 0 {
241                    val.to_rfc2822()
242                } else {
243                    val.to_rfc3339()
244                }
245            },
246            span,
247        ),
248        Value::String { val, .. } => {
249            let dt = parse_date_from_string(&val, val_span);
250            match dt {
251                Ok(x) => Value::string(
252                    {
253                        if x.year() >= 0 {
254                            x.to_rfc2822()
255                        } else {
256                            x.to_rfc3339()
257                        }
258                    },
259                    span,
260                ),
261                Err(e) => e,
262            }
263        }
264        _ => Value::error(
265            ShellError::OnlySupportsThisInputType {
266                exp_input_type: "date, string (that represents datetime)".into(),
267                wrong_type: value.get_type().to_string(),
268                dst_span: span,
269                src_span: val_span,
270            },
271            span,
272        ),
273    }
274}
275
276#[cfg(test)]
277mod test {
278    use super::*;
279
280    #[test]
281    fn test_examples() {
282        use crate::test_examples;
283
284        test_examples(FormatDate {})
285    }
286}