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