nu_command/generators/
seq_date.rs

1use chrono::{Duration, Local, NaiveDate, NaiveDateTime};
2use nu_engine::command_prelude::*;
3use nu_protocol::FromValue;
4
5use std::fmt::Write;
6
7const NANOSECONDS_IN_DAY: i64 = 1_000_000_000i64 * 60i64 * 60i64 * 24i64;
8
9#[derive(Clone)]
10pub struct SeqDate;
11
12impl Command for SeqDate {
13    fn name(&self) -> &str {
14        "seq date"
15    }
16
17    fn description(&self) -> &str {
18        "Print sequences of dates."
19    }
20
21    fn signature(&self) -> nu_protocol::Signature {
22        Signature::build("seq date")
23            .input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::String)))])
24            .named(
25                "output-format",
26                SyntaxShape::String,
27                "prints dates in this format (defaults to %Y-%m-%d)",
28                Some('o'),
29            )
30            .named(
31                "input-format",
32                SyntaxShape::String,
33                "give argument dates in this format (defaults to %Y-%m-%d)",
34                Some('i'),
35            )
36            .named(
37                "begin-date",
38                SyntaxShape::String,
39                "beginning date range",
40                Some('b'),
41            )
42            .named("end-date", SyntaxShape::String, "ending date", Some('e'))
43            .named(
44                "increment",
45                SyntaxShape::OneOf(vec![SyntaxShape::Duration, SyntaxShape::Int]),
46                "increment dates by this duration (defaults to days if integer)",
47                Some('n'),
48            )
49            .named(
50                "days",
51                SyntaxShape::Int,
52                "number of days to print (ignored if periods is used)",
53                Some('d'),
54            )
55            .named(
56                "periods",
57                SyntaxShape::Int,
58                "number of periods to print",
59                Some('p'),
60            )
61            .switch("reverse", "print dates in reverse", Some('r'))
62            .category(Category::Generators)
63    }
64
65    fn examples(&self) -> Vec<Example<'_>> {
66        vec![
67            Example {
68                description: "Return a list of the next 10 days in the YYYY-MM-DD format",
69                example: "seq date --days 10",
70                result: None,
71            },
72            Example {
73                description: "Return the previous 10 days in the YYYY-MM-DD format",
74                example: "seq date --days 10 --reverse",
75                result: None,
76            },
77            Example {
78                description: "Return the previous 10 days, starting today, in the MM/DD/YYYY format",
79                example: "seq date --days 10 -o '%m/%d/%Y' --reverse",
80                result: None,
81            },
82            Example {
83                description: "Return the first 10 days in January, 2020",
84                example: "seq date --begin-date '2020-01-01' --end-date '2020-01-10' --increment 1day",
85                result: Some(Value::list(
86                    vec![
87                        Value::test_string("2020-01-01"),
88                        Value::test_string("2020-01-02"),
89                        Value::test_string("2020-01-03"),
90                        Value::test_string("2020-01-04"),
91                        Value::test_string("2020-01-05"),
92                        Value::test_string("2020-01-06"),
93                        Value::test_string("2020-01-07"),
94                        Value::test_string("2020-01-08"),
95                        Value::test_string("2020-01-09"),
96                        Value::test_string("2020-01-10"),
97                    ],
98                    Span::test_data(),
99                )),
100            },
101            Example {
102                description: "Return the first 10 days in January, 2020 using --days flag",
103                example: "seq date --begin-date '2020-01-01' --days 10 --increment 1day",
104                result: Some(Value::list(
105                    vec![
106                        Value::test_string("2020-01-01"),
107                        Value::test_string("2020-01-02"),
108                        Value::test_string("2020-01-03"),
109                        Value::test_string("2020-01-04"),
110                        Value::test_string("2020-01-05"),
111                        Value::test_string("2020-01-06"),
112                        Value::test_string("2020-01-07"),
113                        Value::test_string("2020-01-08"),
114                        Value::test_string("2020-01-09"),
115                        Value::test_string("2020-01-10"),
116                    ],
117                    Span::test_data(),
118                )),
119            },
120            Example {
121                description: "Return the first five 5-minute periods starting January 1, 2020",
122                example: "seq date --begin-date '2020-01-01' --periods 5 --increment 5min --output-format '%Y-%m-%d %H:%M:%S'",
123                result: Some(Value::list(
124                    vec![
125                        Value::test_string("2020-01-01 00:00:00"),
126                        Value::test_string("2020-01-01 00:05:00"),
127                        Value::test_string("2020-01-01 00:10:00"),
128                        Value::test_string("2020-01-01 00:15:00"),
129                        Value::test_string("2020-01-01 00:20:00"),
130                    ],
131                    Span::test_data(),
132                )),
133            },
134            Example {
135                description: "print every fifth day between January 1st 2020 and January 31st 2020",
136                example: "seq date --begin-date '2020-01-01' --end-date '2020-01-31' --increment 5day",
137                result: Some(Value::list(
138                    vec![
139                        Value::test_string("2020-01-01"),
140                        Value::test_string("2020-01-06"),
141                        Value::test_string("2020-01-11"),
142                        Value::test_string("2020-01-16"),
143                        Value::test_string("2020-01-21"),
144                        Value::test_string("2020-01-26"),
145                        Value::test_string("2020-01-31"),
146                    ],
147                    Span::test_data(),
148                )),
149            },
150            Example {
151                description: "increment defaults to days if no duration is supplied",
152                example: "seq date --begin-date '2020-01-01' --end-date '2020-01-31' --increment 5",
153                result: Some(Value::list(
154                    vec![
155                        Value::test_string("2020-01-01"),
156                        Value::test_string("2020-01-06"),
157                        Value::test_string("2020-01-11"),
158                        Value::test_string("2020-01-16"),
159                        Value::test_string("2020-01-21"),
160                        Value::test_string("2020-01-26"),
161                        Value::test_string("2020-01-31"),
162                    ],
163                    Span::test_data(),
164                )),
165            },
166            Example {
167                description: "print every six hours starting January 1st, 2020 until January 3rd, 2020",
168                example: "seq date --begin-date '2020-01-01' --end-date '2020-01-03' --increment 6hr --output-format '%Y-%m-%d %H:%M:%S'",
169                result: Some(Value::list(
170                    vec![
171                        Value::test_string("2020-01-01 00:00:00"),
172                        Value::test_string("2020-01-01 06:00:00"),
173                        Value::test_string("2020-01-01 12:00:00"),
174                        Value::test_string("2020-01-01 18:00:00"),
175                        Value::test_string("2020-01-02 00:00:00"),
176                        Value::test_string("2020-01-02 06:00:00"),
177                        Value::test_string("2020-01-02 12:00:00"),
178                        Value::test_string("2020-01-02 18:00:00"),
179                        Value::test_string("2020-01-03 00:00:00"),
180                    ],
181                    Span::test_data(),
182                )),
183            },
184        ]
185    }
186
187    fn run(
188        &self,
189        engine_state: &EngineState,
190        stack: &mut Stack,
191        call: &Call,
192        _input: PipelineData,
193    ) -> Result<PipelineData, ShellError> {
194        let output_format: Option<Spanned<String>> =
195            call.get_flag(engine_state, stack, "output-format")?;
196        let input_format: Option<Spanned<String>> =
197            call.get_flag(engine_state, stack, "input-format")?;
198        let begin_date: Option<Spanned<String>> =
199            call.get_flag(engine_state, stack, "begin-date")?;
200        let end_date: Option<Spanned<String>> = call.get_flag(engine_state, stack, "end-date")?;
201
202        let increment = match call.get_flag::<Value>(engine_state, stack, "increment")? {
203            Some(increment) => {
204                let span = increment.span();
205                match increment {
206                    Value::Int { val, .. } => Some(
207                        val.checked_mul(NANOSECONDS_IN_DAY)
208                            .ok_or_else(|| ShellError::GenericError {
209                                error: "increment is too large".into(),
210                                msg: "increment is too large".into(),
211                                span: Some(span),
212                                help: None,
213                                inner: vec![],
214                            })?
215                            .into_spanned(span),
216                    ),
217                    Value::Duration { val, .. } => Some(val.into_spanned(span)),
218                    _ => None,
219                }
220            }
221            None => None,
222        };
223
224        let days: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "days")?;
225        let periods: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "periods")?;
226        let reverse = call.has_flag(engine_state, stack, "reverse")?;
227
228        let out_format = match output_format {
229            Some(s) => Some(Value::string(s.item, s.span)),
230            _ => None,
231        };
232
233        let in_format = match input_format {
234            Some(s) => Some(Value::string(s.item, s.span)),
235            _ => None,
236        };
237
238        let begin = match begin_date {
239            Some(s) => Some(s.item),
240            _ => None,
241        };
242
243        let end = match end_date {
244            Some(s) => Some(s.item),
245            _ => None,
246        };
247
248        let inc = match increment {
249            Some(i) => Value::int(i.item, i.span),
250            _ => Value::int(NANOSECONDS_IN_DAY, call.head),
251        };
252
253        let day_count = days.map(|i| Value::int(i.item, i.span));
254
255        let period_count = periods.map(|i| Value::int(i.item, i.span));
256
257        let mut rev = false;
258        if reverse {
259            rev = reverse;
260        }
261
262        Ok(run_seq_dates(
263            out_format,
264            in_format,
265            begin,
266            end,
267            inc,
268            day_count,
269            period_count,
270            rev,
271            call.head,
272        )?
273        .into_pipeline_data())
274    }
275}
276
277#[allow(clippy::unnecessary_lazy_evaluations)]
278pub fn parse_date_string(s: &str, format: &str) -> Result<NaiveDateTime, &'static str> {
279    NaiveDateTime::parse_from_str(s, format).or_else(|_| {
280        // If parsing as DateTime fails, try parsing as Date before throwing error
281        let date = NaiveDate::parse_from_str(s, format).map_err(|_| "Failed to parse date.")?;
282        date.and_hms_opt(0, 0, 0)
283            .ok_or_else(|| "Failed to convert NaiveDate to NaiveDateTime.")
284    })
285}
286
287#[allow(clippy::too_many_arguments)]
288pub fn run_seq_dates(
289    output_format: Option<Value>,
290    input_format: Option<Value>,
291    beginning_date: Option<String>,
292    ending_date: Option<String>,
293    increment: Value,
294    day_count: Option<Value>,
295    period_count: Option<Value>,
296    reverse: bool,
297    call_span: Span,
298) -> Result<Value, ShellError> {
299    let today = Local::now().naive_local();
300    // if cannot convert , it will return error
301    let increment_span = increment.span();
302    let mut step_size: i64 = i64::from_value(increment)?;
303
304    if step_size == 0 {
305        return Err(ShellError::GenericError {
306            error: "increment cannot be 0".into(),
307            msg: "increment cannot be 0".into(),
308            span: Some(increment_span),
309            help: None,
310            inner: vec![],
311        });
312    }
313
314    let in_format = match input_format {
315        Some(i) => match i.coerce_into_string() {
316            Ok(v) => v,
317            Err(e) => {
318                return Err(ShellError::GenericError {
319                    error: e.to_string(),
320                    msg: "".into(),
321                    span: None,
322                    help: Some("error with input_format as_string".into()),
323                    inner: vec![],
324                });
325            }
326        },
327        _ => "%Y-%m-%d".to_string(),
328    };
329
330    let out_format = match output_format {
331        Some(o) => match o.coerce_into_string() {
332            Ok(v) => v,
333            Err(e) => {
334                return Err(ShellError::GenericError {
335                    error: e.to_string(),
336                    msg: "".into(),
337                    span: None,
338                    help: Some("error with output_format as_string".into()),
339                    inner: vec![],
340                });
341            }
342        },
343        _ => "%Y-%m-%d".to_string(),
344    };
345
346    let start_date = match beginning_date {
347        Some(d) => match parse_date_string(&d, &in_format) {
348            Ok(nd) => nd,
349            Err(e) => {
350                return Err(ShellError::GenericError {
351                    error: e.to_string(),
352                    msg: "Failed to parse date".into(),
353                    span: Some(call_span),
354                    help: None,
355                    inner: vec![],
356                });
357            }
358        },
359        _ => today,
360    };
361
362    let mut end_date = match ending_date {
363        Some(d) => match parse_date_string(&d, &in_format) {
364            Ok(nd) => nd,
365            Err(e) => {
366                return Err(ShellError::GenericError {
367                    error: e.to_string(),
368                    msg: "Failed to parse date".into(),
369                    span: Some(call_span),
370                    help: None,
371                    inner: vec![],
372                });
373            }
374        },
375        _ => today,
376    };
377
378    let mut days_to_output = match day_count {
379        Some(d) => i64::from_value(d)?,
380        None => 0i64,
381    };
382
383    let mut periods_to_output = match period_count {
384        Some(d) => i64::from_value(d)?,
385        None => 0i64,
386    };
387
388    // Make the signs opposite if we're created dates in reverse direction
389    if reverse {
390        step_size *= -1;
391        days_to_output *= -1;
392        periods_to_output *= -1;
393    }
394
395    // --days is ignored when --periods is set
396    if periods_to_output != 0 {
397        end_date = periods_to_output
398            .checked_sub(1)
399            .and_then(|val| val.checked_mul(step_size.abs()))
400            .map(Duration::nanoseconds)
401            .and_then(|inc| start_date.checked_add_signed(inc))
402            .ok_or_else(|| ShellError::GenericError {
403                error: "incrementing by the number of periods is too large".into(),
404                msg: "incrementing by the number of periods is too large".into(),
405                span: Some(call_span),
406                help: None,
407                inner: vec![],
408            })?;
409    } else if days_to_output != 0 {
410        end_date = days_to_output
411            .checked_sub(1)
412            .and_then(Duration::try_days)
413            .and_then(|days| start_date.checked_add_signed(days))
414            .ok_or_else(|| ShellError::GenericError {
415                error: "int value too large".into(),
416                msg: "int value too large".into(),
417                span: Some(call_span),
418                help: None,
419                inner: vec![],
420            })?;
421    }
422
423    // conceptually counting down with a positive step or counting up with a negative step
424    // makes no sense, attempt to do what one means by inverting the signs in those cases.
425    if (start_date > end_date) && (step_size > 0) || (start_date < end_date) && step_size < 0 {
426        step_size = -step_size;
427    }
428
429    let is_out_of_range =
430        |next| (step_size > 0 && next > end_date) || (step_size < 0 && next < end_date);
431
432    // Bounds are enforced by i64 conversion above
433    let step_size = Duration::nanoseconds(step_size);
434
435    let mut next = start_date;
436    if is_out_of_range(next) {
437        return Err(ShellError::GenericError {
438            error: "date is out of range".into(),
439            msg: "date is out of range".into(),
440            span: Some(call_span),
441            help: None,
442            inner: vec![],
443        });
444    }
445
446    let mut ret = vec![];
447    loop {
448        let mut date_string = String::new();
449        match write!(date_string, "{}", next.format(&out_format)) {
450            Ok(_) => {}
451            Err(e) => {
452                return Err(ShellError::GenericError {
453                    error: "Invalid output format".into(),
454                    msg: e.to_string(),
455                    span: Some(call_span),
456                    help: None,
457                    inner: vec![],
458                });
459            }
460        }
461        ret.push(Value::string(date_string, call_span));
462        if let Some(n) = next.checked_add_signed(step_size) {
463            next = n;
464        } else {
465            return Err(ShellError::GenericError {
466                error: "date overflow".into(),
467                msg: "adding the increment overflowed".into(),
468                span: Some(call_span),
469                help: None,
470                inner: vec![],
471            });
472        }
473
474        if is_out_of_range(next) {
475            break;
476        }
477    }
478
479    Ok(Value::list(ret, call_span))
480}
481
482#[cfg(test)]
483mod test {
484    use super::*;
485
486    #[test]
487    fn test_examples() {
488        use crate::test_examples;
489
490        test_examples(SeqDate {})
491    }
492}