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