1use chrono::{Local, TimeZone};
2use human_date_parser::{ParseResult, from_human_time};
3use nu_engine::command_prelude::*;
4
5#[derive(Clone)]
6pub struct DateFromHuman;
7
8impl Command for DateFromHuman {
9 fn name(&self) -> &str {
10 "date from-human"
11 }
12
13 fn signature(&self) -> Signature {
14 Signature::build("date from-human")
15 .input_output_types(vec![
16 (Type::String, Type::Date),
17 (Type::Nothing, Type::table()),
18 (
19 Type::List(Box::new(Type::Date)),
20 Type::List(Box::new(Type::Date)),
21 ),
22 (
23 Type::List(Box::new(Type::String)),
24 Type::List(Box::new(Type::Date)),
25 ),
26 ])
27 .allow_variants_without_examples(true)
28 .switch(
29 "list",
30 "Show human-readable datetime parsing examples.",
31 Some('l'),
32 )
33 .category(Category::Date)
34 }
35
36 fn description(&self) -> &str {
37 "Convert a human readable datetime string to a datetime."
38 }
39
40 fn search_terms(&self) -> Vec<&str> {
41 vec![
42 "relative",
43 "now",
44 "today",
45 "tomorrow",
46 "yesterday",
47 "weekday",
48 "weekday_name",
49 "timezone",
50 ]
51 }
52
53 fn run(
54 &self,
55 engine_state: &EngineState,
56 stack: &mut Stack,
57 call: &Call,
58 input: PipelineData,
59 ) -> Result<PipelineData, ShellError> {
60 if call.has_flag(engine_state, stack, "list")? {
61 return Ok(list_human_readable_examples(call.head).into_pipeline_data());
62 }
63 let head = call.head;
64 if let PipelineData::Empty = input {
66 return Err(ShellError::PipelineEmpty { dst_span: head });
67 }
68 input.map(move |value| helper(value, head), engine_state.signals())
69 }
70
71 fn examples(&self) -> Vec<Example<'_>> {
72 vec![
73 Example {
74 description: "Parsing human readable datetime.",
75 example: "'Today at 18:30' | date from-human",
76 result: None,
77 },
78 Example {
79 description: "Parsing human readable datetime.",
80 example: "'Last Friday at 19:45' | date from-human",
81 result: None,
82 },
83 Example {
84 description: "Parsing human readable datetime.",
85 example: "'In 5 minutes and 30 seconds' | date from-human",
86 result: None,
87 },
88 Example {
89 description: "Show human-readable datetime parsing examples.",
90 example: "date from-human --list",
91 result: None,
92 },
93 Example {
94 description: "Convert a list of human-readable datetime strings to datetimes.",
95 example: r#"["Today at 18:30", "Tomorrow at 09:00"] | date from-human"#,
96 result: None,
97 },
98 ]
99 }
100}
101
102fn helper(value: Value, head: Span) -> Value {
103 let span = value.span();
104 let input_val = match value {
105 Value::String { val, .. } => val,
106 other => {
107 return Value::error(
108 ShellError::OnlySupportsThisInputType {
109 exp_input_type: "string".to_string(),
110 wrong_type: other.get_type().to_string(),
111 dst_span: head,
112 src_span: span,
113 },
114 span,
115 );
116 }
117 };
118
119 let now = Local::now();
120
121 if let Ok(date) = from_human_time(&input_val, now.naive_local()) {
122 match date {
123 ParseResult::Date(date) => {
124 let time = now.time();
125 let combined = date.and_time(time);
126 let local_offset = *now.offset();
127 let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined)
128 .single()
129 .unwrap_or_default();
130 return Value::date(dt_fixed, span);
131 }
132 ParseResult::DateTime(date) => {
133 let local_offset = *now.offset();
134 let dt_fixed = match local_offset.from_local_datetime(&date) {
135 chrono::LocalResult::Single(dt) => dt,
136 chrono::LocalResult::Ambiguous(_, _) => {
137 return Value::error(
138 ShellError::DatetimeParseError {
139 msg: "Ambiguous datetime".to_string(),
140 span,
141 },
142 span,
143 );
144 }
145 chrono::LocalResult::None => {
146 return Value::error(
147 ShellError::DatetimeParseError {
148 msg: "Invalid datetime".to_string(),
149 span,
150 },
151 span,
152 );
153 }
154 };
155 return Value::date(dt_fixed, span);
156 }
157 ParseResult::Time(time) => {
158 let date = now.date_naive();
159 let combined = date.and_time(time);
160 let local_offset = *now.offset();
161 let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined)
162 .single()
163 .unwrap_or_default();
164 return Value::date(dt_fixed, span);
165 }
166 }
167 }
168
169 match from_human_time(&input_val, now.naive_local()) {
170 Ok(date) => match date {
171 ParseResult::Date(date) => {
172 let time = now.time();
173 let combined = date.and_time(time);
174 let local_offset = *now.offset();
175 let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined)
176 .single()
177 .unwrap_or_default();
178 Value::date(dt_fixed, span)
179 }
180 ParseResult::DateTime(date) => {
181 let local_offset = *now.offset();
182 let dt_fixed = match local_offset.from_local_datetime(&date) {
183 chrono::LocalResult::Single(dt) => dt,
184 chrono::LocalResult::Ambiguous(_, _) => {
185 return Value::error(
186 ShellError::DatetimeParseError {
187 msg: "Ambiguous datetime".to_string(),
188 span,
189 },
190 span,
191 );
192 }
193 chrono::LocalResult::None => {
194 return Value::error(
195 ShellError::DatetimeParseError {
196 msg: "Invalid datetime".to_string(),
197 span,
198 },
199 span,
200 );
201 }
202 };
203 Value::date(dt_fixed, span)
204 }
205 ParseResult::Time(time) => {
206 let date = now.date_naive();
207 let combined = date.and_time(time);
208 let local_offset = *now.offset();
209 let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined)
210 .single()
211 .unwrap_or_default();
212 Value::date(dt_fixed, span)
213 }
214 },
215 Err(_) => Value::error(
216 ShellError::IncorrectValue {
217 msg: "Cannot parse as humanized date".to_string(),
218 val_span: head,
219 call_span: span,
220 },
221 span,
222 ),
223 }
224}
225
226fn list_human_readable_examples(span: Span) -> Value {
227 let examples: Vec<String> = vec![
228 "Today 18:30".into(),
229 "2022-11-07 13:25:30".into(),
230 "15:20 Friday".into(),
231 "This Friday 17:00".into(),
232 "13:25, Next Tuesday".into(),
233 "Last Friday at 19:45".into(),
234 "In 3 days".into(),
235 "In 2 hours".into(),
236 "10 hours and 5 minutes ago".into(),
237 "1 years ago".into(),
238 "A year ago".into(),
239 "A month ago".into(),
240 "A week ago".into(),
241 "A day ago".into(),
242 "An hour ago".into(),
243 "A minute ago".into(),
244 "A second ago".into(),
245 "Now".into(),
246 ];
247
248 let records = examples
249 .iter()
250 .map(|s| {
251 Value::record(
252 record! {
253 "parseable human datetime examples" => Value::test_string(s.to_string()),
254 "result" => helper(Value::test_string(s.to_string()), span),
255 },
256 span,
257 )
258 })
259 .collect::<Vec<Value>>();
260
261 Value::list(records, span)
262}
263
264#[cfg(test)]
265mod test {
266 use super::*;
267
268 #[test]
269 fn test_examples() -> nu_test_support::Result {
270 nu_test_support::test().examples(DateFromHuman)
271 }
272}