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 let locale = get_locale(|name| stack.get_env_var(engine_state, name))?;
104
105 run(engine_state, call, input, list, format, locale)
106 }
107
108 fn run_const(
109 &self,
110 working_set: &StateWorkingSet,
111 call: &Call,
112 input: PipelineData,
113 ) -> Result<PipelineData, ShellError> {
114 let list = call.has_flag_const(working_set, "list")?;
115 let format = call.opt_const::<Spanned<String>>(working_set, 0)?;
116 let locale = get_locale(|name| working_set.get_env_var(name))?;
117
118 run(working_set.permanent(), call, input, list, format, locale)
119 }
120}
121
122fn get_locale<'a, F>(env_getter: F) -> Result<Locale, ShellError>
123where
124 F: Fn(&str) -> Option<&'a Value> + 'a,
125{
126 static LOCALE_VAR_NAMES: &[&str] = &[LOCALE_OVERRIDE_ENV_VAR, "LC_ALL", "LC_TIME", "LANG"];
129
130 let locale_var = LOCALE_VAR_NAMES
133 .iter()
134 .find_map(|name| env_getter(name))
135 .map(Value::as_str)
136 .transpose()?;
137
138 let locale = match locale_var {
139 Some(loc) => (loc.split('.').next())
140 .and_then(|s| Locale::try_from(s).ok())
141 .unwrap_or(Locale::en_US),
142 None => get_system_locale_string()
143 .map(|l| l.replace('-', "_"))
144 .and_then(|s| Locale::try_from(s.as_str()).ok())
145 .unwrap_or(Locale::en_US),
146 };
147 Ok(locale)
148}
149
150fn run(
151 engine_state: &EngineState,
152 call: &Call,
153 input: PipelineData,
154 list: bool,
155 format: Option<Spanned<String>>,
156 locale: Locale,
157) -> Result<PipelineData, ShellError> {
158 let head = call.head;
159 if list {
160 return Ok(PipelineData::value(
161 generate_strftime_list(head, false),
162 None,
163 ));
164 }
165
166 if let PipelineData::Empty = input {
168 return Err(ShellError::PipelineEmpty { dst_span: head });
169 }
170 input.map(
171 move |value| match &format {
172 Some(format) => format_helper(value, format.item.as_str(), format.span, head, locale),
173 None => format_helper_rfc2822(value, head),
174 },
175 engine_state.signals(),
176 )
177}
178
179fn format_from<Tz: TimeZone>(
180 date_time: DateTime<Tz>,
181 formatter: &str,
182 span: Span,
183 locale: Locale,
184) -> Value
185where
186 Tz::Offset: Display,
187{
188 let mut formatter_buf = String::new();
189 let processed_formatter = formatter
191 .replace("%J", "%Y%m%d") .replace("%Q", "%H%M%S"); let format = date_time.format_localized(&processed_formatter, locale);
194
195 match formatter_buf.write_fmt(format_args!("{format}")) {
196 Ok(_) => Value::string(formatter_buf, span),
197 Err(_) => Value::error(
198 ShellError::TypeMismatch {
199 err_message: "invalid format".to_string(),
200 span,
201 },
202 span,
203 ),
204 }
205}
206
207fn format_helper(
208 value: Value,
209 formatter: &str,
210 formatter_span: Span,
211 head_span: Span,
212 locale: Locale,
213) -> Value {
214 match value {
215 Value::Date { val, .. } => format_from(val, formatter, formatter_span, locale),
216 Value::String { val, .. } => {
217 let dt = parse_date_from_string(&val, formatter_span);
218
219 match dt {
220 Ok(x) => format_from(x, formatter, formatter_span, locale),
221 Err(e) => e,
222 }
223 }
224 _ => Value::error(
225 ShellError::OnlySupportsThisInputType {
226 exp_input_type: "date, string (that represents datetime)".into(),
227 wrong_type: value.get_type().to_string(),
228 dst_span: head_span,
229 src_span: value.span(),
230 },
231 head_span,
232 ),
233 }
234}
235
236fn format_helper_rfc2822(value: Value, span: Span) -> Value {
237 let val_span = value.span();
238 match value {
239 Value::Date { val, .. } => Value::string(
240 {
241 if val.year() >= 0 && val.year() <= 9999 {
242 val.to_rfc2822()
243 } else {
244 return Value::error(
245 ShellError::Generic(
246 GenericError::new(
247 "Can't convert date to RFC 2822 format.",
248 "the RFC 2822 format only supports years 0 through 9999",
249 val_span,
250 )
251 .with_help(r#"use the RFC 3339 format option: "%+""#),
252 ),
253 span,
254 );
255 }
256 },
257 span,
258 ),
259 Value::String { val, .. } => {
260 let dt = parse_date_from_string(&val, val_span);
261 match dt {
262 Ok(x) => Value::string(
263 {
264 if x.year() >= 0 && x.year() <= 9999 {
265 x.to_rfc2822()
266 } else {
267 return Value::error(
268 ShellError::Generic(
269 GenericError::new(
270 "Can't convert date to RFC 2822 format.",
271 "the RFC 2822 format only supports years 0 through 9999",
272 val_span,
273 )
274 .with_help(r#"use the RFC 3339 format option: "%+""#),
275 ),
276 span,
277 );
278 }
279 },
280 span,
281 ),
282 Err(e) => e,
283 }
284 }
285 _ => Value::error(
286 ShellError::OnlySupportsThisInputType {
287 exp_input_type: "date, string (that represents datetime)".into(),
288 wrong_type: value.get_type().to_string(),
289 dst_span: span,
290 src_span: val_span,
291 },
292 span,
293 ),
294 }
295}
296
297#[cfg(test)]
298mod test {
299 use super::*;
300
301 #[test]
302 fn test_examples() -> nu_test_support::Result {
303 nu_test_support::test().examples(FormatDate)
304 }
305}