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