Skip to main content

nu_command/date/
from_human.rs

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        // This doesn't match explicit nulls
65        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}