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 (Type::Any, Type::table()),
27 ])
28 .allow_variants_without_examples(true) .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 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 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 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 let processed_formatter = formatter
205 .replace("%J", "%Y%m%d") .replace("%Q", "%H%M%S"); 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}