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