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 (Type::Any, Type::table()),
26 (
27 Type::List(Box::new(Type::Date)),
28 Type::List(Box::new(Type::String)),
29 ),
30 (
31 Type::List(Box::new(Type::String)),
32 Type::List(Box::new(Type::String)),
33 ),
34 ])
35 .allow_variants_without_examples(true) .switch("list", "Lists strftime cheatsheet.", Some('l'))
37 .optional(
38 "format string",
39 SyntaxShape::String,
40 "The desired format date.",
41 )
42 .category(Category::Strings)
43 }
44
45 fn description(&self) -> &str {
46 "Format a given date using a format string."
47 }
48
49 fn search_terms(&self) -> Vec<&str> {
50 vec!["fmt", "strftime"]
51 }
52
53 fn examples(&self) -> Vec<Example<'_>> {
54 vec![
55 Example {
56 description: "Format a given date-time using the default format (RFC 2822).",
57 example: "'2021-10-22 20:00:12 +01:00' | into datetime | 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 as a string using the default format (RFC 2822).",
65 example: r#""2021-10-22 20:00:12 +01:00" | format date"#,
66 result: Some(Value::string(
67 "Fri, 22 Oct 2021 20:00:12 +0100".to_string(),
68 Span::test_data(),
69 )),
70 },
71 Example {
72 description: "Format a given date-time according to the RFC 3339 standard.",
73 example: r#"'2021-10-22 20:00:12 +01:00' | into datetime | format date "%+""#,
74 result: Some(Value::string(
75 "2021-10-22T20:00:12+01:00".to_string(),
76 Span::test_data(),
77 )),
78 },
79 Example {
80 description: "Format the current date-time using a given format string.",
81 example: r#"date now | format date "%Y-%m-%d %H:%M:%S""#,
82 result: None,
83 },
84 Example {
85 description: "Format the current date using a given format string.",
86 example: r#"date now | format date "%Y-%m-%d %H:%M:%S""#,
87 result: None,
88 },
89 Example {
90 description: "Format a given date using a given format string.",
91 example: r#""2021-10-22 20:00:12 +01:00" | format date "%Y-%m-%d""#,
92 result: Some(Value::test_string("2021-10-22")),
93 },
94 Example {
95 description: "Format a list of date strings using a given format string.",
96 example: r#"["2021-10-22 20:00:12 +01:00", "2021-10-23 20:00:12 +01:00"] | format date "%Y-%m-%d""#,
97 result: Some(Value::list(
98 vec![
99 Value::test_string("2021-10-22"),
100 Value::test_string("2021-10-23"),
101 ],
102 Span::test_data(),
103 )),
104 },
105 Example {
106 description: "Format a list of datetimes using a given format string.",
107 example: r#"[2021-10-22T20:00:12+01:00, 2021-10-23T20:00:12+01:00] | format date "%Y-%m-%d""#,
108 result: Some(Value::list(
109 vec![
110 Value::test_string("2021-10-22"),
111 Value::test_string("2021-10-23"),
112 ],
113 Span::test_data(),
114 )),
115 },
116 ]
117 }
118
119 fn is_const(&self) -> bool {
120 true
121 }
122
123 fn run(
124 &self,
125 engine_state: &EngineState,
126 stack: &mut Stack,
127 call: &Call,
128 input: PipelineData,
129 ) -> Result<PipelineData, ShellError> {
130 let list = call.has_flag(engine_state, stack, "list")?;
131 let format = call.opt::<Spanned<String>>(engine_state, stack, 0)?;
132 let locale = get_locale(|name| stack.get_env_var(engine_state, name)?.as_str().ok());
133
134 run(engine_state, call, input, list, format, locale)
135 }
136
137 fn run_const(
138 &self,
139 working_set: &StateWorkingSet,
140 call: &Call,
141 input: PipelineData,
142 ) -> Result<PipelineData, ShellError> {
143 let list = call.has_flag_const(working_set, "list")?;
144 let format = call.opt_const::<Spanned<String>>(working_set, 0)?;
145 let locale = get_locale(|name| working_set.get_env_var(name)?.as_str().ok());
146
147 run(working_set.permanent(), call, input, list, format, locale)
148 }
149}
150
151fn get_locale<'a, F>(env_getter: F) -> Locale
152where
153 F: Fn(&str) -> Option<&'a str> + 'a,
154{
155 nu_utils::get_locale_from_env_vars(Some("LC_TIME"), env_getter)
156 .and_then(|s| Locale::try_from(s.as_ref()).ok())
157 .unwrap_or(Locale::en_US)
158}
159
160fn run(
161 engine_state: &EngineState,
162 call: &Call,
163 input: PipelineData,
164 list: bool,
165 format: Option<Spanned<String>>,
166 locale: Locale,
167) -> Result<PipelineData, ShellError> {
168 let head = call.head;
169 if list {
170 return Ok(PipelineData::value(
171 generate_strftime_list(head, false),
172 None,
173 ));
174 }
175
176 if let PipelineData::Empty = input {
178 return Err(ShellError::PipelineEmpty { dst_span: head });
179 }
180 input.map(
181 move |value| match &format {
182 Some(format) => format_helper(value, format.item.as_str(), format.span, head, locale),
183 None => format_helper_rfc2822(value, head),
184 },
185 engine_state.signals(),
186 )
187}
188
189fn format_from<Tz: TimeZone>(
190 date_time: DateTime<Tz>,
191 formatter: &str,
192 span: Span,
193 locale: Locale,
194) -> Value
195where
196 Tz::Offset: Display,
197{
198 let mut formatter_buf = String::new();
199 let processed_formatter = formatter
201 .replace("%J", "%Y%m%d") .replace("%Q", "%H%M%S"); let format = date_time.format_localized(&processed_formatter, locale);
204
205 match formatter_buf.write_fmt(format_args!("{format}")) {
206 Ok(_) => Value::string(formatter_buf, span),
207 Err(_) => Value::error(
208 ShellError::TypeMismatch {
209 err_message: "invalid format".to_string(),
210 span,
211 },
212 span,
213 ),
214 }
215}
216
217fn format_helper(
218 value: Value,
219 formatter: &str,
220 formatter_span: Span,
221 head_span: Span,
222 locale: Locale,
223) -> Value {
224 match value {
225 Value::Date { val, .. } => format_from(val, formatter, formatter_span, locale),
226 Value::String { val, .. } => {
227 let dt = parse_date_from_string(&val, formatter_span);
228
229 match dt {
230 Ok(x) => format_from(x, formatter, formatter_span, locale),
231 Err(e) => e,
232 }
233 }
234 _ => Value::error(
235 ShellError::OnlySupportsThisInputType {
236 exp_input_type: "date, string (that represents datetime)".into(),
237 wrong_type: value.get_type().to_string(),
238 dst_span: head_span,
239 src_span: value.span(),
240 },
241 head_span,
242 ),
243 }
244}
245
246fn format_helper_rfc2822(value: Value, span: Span) -> Value {
247 let val_span = value.span();
248 match value {
249 Value::Date { val, .. } => Value::string(
250 {
251 if val.year() >= 0 && val.year() <= 9999 {
252 val.to_rfc2822()
253 } else {
254 return Value::error(
255 ShellError::Generic(
256 GenericError::new(
257 "Can't convert date to RFC 2822 format.",
258 "the RFC 2822 format only supports years 0 through 9999",
259 val_span,
260 )
261 .with_help(r#"use the RFC 3339 format option: "%+""#),
262 ),
263 span,
264 );
265 }
266 },
267 span,
268 ),
269 Value::String { val, .. } => {
270 let dt = parse_date_from_string(&val, val_span);
271 match dt {
272 Ok(x) => Value::string(
273 {
274 if x.year() >= 0 && x.year() <= 9999 {
275 x.to_rfc2822()
276 } else {
277 return Value::error(
278 ShellError::Generic(
279 GenericError::new(
280 "Can't convert date to RFC 2822 format.",
281 "the RFC 2822 format only supports years 0 through 9999",
282 val_span,
283 )
284 .with_help(r#"use the RFC 3339 format option: "%+""#),
285 ),
286 span,
287 );
288 }
289 },
290 span,
291 ),
292 Err(e) => e,
293 }
294 }
295 _ => Value::error(
296 ShellError::OnlySupportsThisInputType {
297 exp_input_type: "date, string (that represents datetime)".into(),
298 wrong_type: value.get_type().to_string(),
299 dst_span: span,
300 src_span: val_span,
301 },
302 span,
303 ),
304 }
305}
306
307#[cfg(test)]
308mod test {
309 use super::*;
310
311 #[test]
312 fn test_examples() -> nu_test_support::Result {
313 nu_test_support::test().examples(FormatDate)
314 }
315}