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) => match increment {
204                Value::Int { val, internal_span } => Some(
205                    val.checked_mul(NANOSECONDS_IN_DAY)
206                        .ok_or_else(|| ShellError::GenericError {
207                            error: "increment is too large".into(),
208                            msg: "increment is too large".into(),
209                            span: Some(internal_span),
210                            help: None,
211                            inner: vec![],
212                        })?
213                        .into_spanned(internal_span),
214                ),
215                Value::Duration { val, internal_span } => Some(val.into_spanned(internal_span)),
216                _ => None,
217            },
218            None => None,
219        };
220
221        let days: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "days")?;
222        let periods: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "periods")?;
223        let reverse = call.has_flag(engine_state, stack, "reverse")?;
224
225        let out_format = match output_format {
226            Some(s) => Some(Value::string(s.item, s.span)),
227            _ => None,
228        };
229
230        let in_format = match input_format {
231            Some(s) => Some(Value::string(s.item, s.span)),
232            _ => None,
233        };
234
235        let begin = match begin_date {
236            Some(s) => Some(s.item),
237            _ => None,
238        };
239
240        let end = match end_date {
241            Some(s) => Some(s.item),
242            _ => None,
243        };
244
245        let inc = match increment {
246            Some(i) => Value::int(i.item, i.span),
247            _ => Value::int(NANOSECONDS_IN_DAY, call.head),
248        };
249
250        let day_count = days.map(|i| Value::int(i.item, i.span));
251
252        let period_count = periods.map(|i| Value::int(i.item, i.span));
253
254        let mut rev = false;
255        if reverse {
256            rev = reverse;
257        }
258
259        Ok(run_seq_dates(
260            out_format,
261            in_format,
262            begin,
263            end,
264            inc,
265            day_count,
266            period_count,
267            rev,
268            call.head,
269        )?
270        .into_pipeline_data())
271    }
272}
273
274#[allow(clippy::unnecessary_lazy_evaluations)]
275pub fn parse_date_string(s: &str, format: &str) -> Result<NaiveDateTime, &'static str> {
276    NaiveDateTime::parse_from_str(s, format).or_else(|_| {
277        // If parsing as DateTime fails, try parsing as Date before throwing error
278        let date = NaiveDate::parse_from_str(s, format).map_err(|_| "Failed to parse date.")?;
279        date.and_hms_opt(0, 0, 0)
280            .ok_or_else(|| "Failed to convert NaiveDate to NaiveDateTime.")
281    })
282}
283
284#[allow(clippy::too_many_arguments)]
285pub fn run_seq_dates(
286    output_format: Option<Value>,
287    input_format: Option<Value>,
288    beginning_date: Option<String>,
289    ending_date: Option<String>,
290    increment: Value,
291    day_count: Option<Value>,
292    period_count: Option<Value>,
293    reverse: bool,
294    call_span: Span,
295) -> Result<Value, ShellError> {
296    let today = Local::now().naive_local();
297    // if cannot convert , it will return error
298    let increment_span = increment.span();
299    let mut step_size: i64 = i64::from_value(increment)?;
300
301    if step_size == 0 {
302        return Err(ShellError::GenericError {
303            error: "increment cannot be 0".into(),
304            msg: "increment cannot be 0".into(),
305            span: Some(increment_span),
306            help: None,
307            inner: vec![],
308        });
309    }
310
311    let in_format = match input_format {
312        Some(i) => match i.coerce_into_string() {
313            Ok(v) => v,
314            Err(e) => {
315                return Err(ShellError::GenericError {
316                    error: e.to_string(),
317                    msg: "".into(),
318                    span: None,
319                    help: Some("error with input_format as_string".into()),
320                    inner: vec![],
321                });
322            }
323        },
324        _ => "%Y-%m-%d".to_string(),
325    };
326
327    let out_format = match output_format {
328        Some(o) => match o.coerce_into_string() {
329            Ok(v) => v,
330            Err(e) => {
331                return Err(ShellError::GenericError {
332                    error: e.to_string(),
333                    msg: "".into(),
334                    span: None,
335                    help: Some("error with output_format as_string".into()),
336                    inner: vec![],
337                });
338            }
339        },
340        _ => "%Y-%m-%d".to_string(),
341    };
342
343    let start_date = match beginning_date {
344        Some(d) => match parse_date_string(&d, &in_format) {
345            Ok(nd) => nd,
346            Err(e) => {
347                return Err(ShellError::GenericError {
348                    error: e.to_string(),
349                    msg: "Failed to parse date".into(),
350                    span: Some(call_span),
351                    help: None,
352                    inner: vec![],
353                });
354            }
355        },
356        _ => today,
357    };
358
359    let mut end_date = match ending_date {
360        Some(d) => match parse_date_string(&d, &in_format) {
361            Ok(nd) => nd,
362            Err(e) => {
363                return Err(ShellError::GenericError {
364                    error: e.to_string(),
365                    msg: "Failed to parse date".into(),
366                    span: Some(call_span),
367                    help: None,
368                    inner: vec![],
369                });
370            }
371        },
372        _ => today,
373    };
374
375    let mut days_to_output = match day_count {
376        Some(d) => i64::from_value(d)?,
377        None => 0i64,
378    };
379
380    let mut periods_to_output = match period_count {
381        Some(d) => i64::from_value(d)?,
382        None => 0i64,
383    };
384
385    // Make the signs opposite if we're created dates in reverse direction
386    if reverse {
387        step_size *= -1;
388        days_to_output *= -1;
389        periods_to_output *= -1;
390    }
391
392    // --days is ignored when --periods is set
393    if periods_to_output != 0 {
394        end_date = periods_to_output
395            .checked_sub(1)
396            .and_then(|val| val.checked_mul(step_size.abs()))
397            .map(Duration::nanoseconds)
398            .and_then(|inc| start_date.checked_add_signed(inc))
399            .ok_or_else(|| ShellError::GenericError {
400                error: "incrementing by the number of periods is too large".into(),
401                msg: "incrementing by the number of periods is too large".into(),
402                span: Some(call_span),
403                help: None,
404                inner: vec![],
405            })?;
406    } else if days_to_output != 0 {
407        end_date = days_to_output
408            .checked_sub(1)
409            .and_then(Duration::try_days)
410            .and_then(|days| start_date.checked_add_signed(days))
411            .ok_or_else(|| ShellError::GenericError {
412                error: "int value too large".into(),
413                msg: "int value too large".into(),
414                span: Some(call_span),
415                help: None,
416                inner: vec![],
417            })?;
418    }
419
420    // conceptually counting down with a positive step or counting up with a negative step
421    // makes no sense, attempt to do what one means by inverting the signs in those cases.
422    if (start_date > end_date) && (step_size > 0) || (start_date < end_date) && step_size < 0 {
423        step_size = -step_size;
424    }
425
426    let is_out_of_range =
427        |next| (step_size > 0 && next > end_date) || (step_size < 0 && next < end_date);
428
429    // Bounds are enforced by i64 conversion above
430    let step_size = Duration::nanoseconds(step_size);
431
432    let mut next = start_date;
433    if is_out_of_range(next) {
434        return Err(ShellError::GenericError {
435            error: "date is out of range".into(),
436            msg: "date is out of range".into(),
437            span: Some(call_span),
438            help: None,
439            inner: vec![],
440        });
441    }
442
443    let mut ret = vec![];
444    loop {
445        let mut date_string = String::new();
446        match write!(date_string, "{}", next.format(&out_format)) {
447            Ok(_) => {}
448            Err(e) => {
449                return Err(ShellError::GenericError {
450                    error: "Invalid output format".into(),
451                    msg: e.to_string(),
452                    span: Some(call_span),
453                    help: None,
454                    inner: vec![],
455                });
456            }
457        }
458        ret.push(Value::string(date_string, call_span));
459        if let Some(n) = next.checked_add_signed(step_size) {
460            next = n;
461        } else {
462            return Err(ShellError::GenericError {
463                error: "date overflow".into(),
464                msg: "adding the increment overflowed".into(),
465                span: Some(call_span),
466                help: None,
467                inner: vec![],
468            });
469        }
470
471        if is_out_of_range(next) {
472            break;
473        }
474    }
475
476    Ok(Value::list(ret, call_span))
477}
478
479#[cfg(test)]
480mod test {
481    use super::*;
482
483    #[test]
484    fn test_examples() {
485        use crate::test_examples;
486
487        test_examples(SeqDate {})
488    }
489}