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