Skip to main content

nu_command/generators/
seq_date.rs

1use chrono::{Duration, Local, NaiveDate, NaiveDateTime};
2use nu_engine::command_prelude::*;
3use nu_protocol::{FromValue, shell_error::generic::GenericError};
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(|| {
209                                ShellError::Generic(GenericError::new(
210                                    "increment is too large",
211                                    "increment is too large",
212                                    span,
213                                ))
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::Generic(GenericError::new(
306            "increment cannot be 0",
307            "increment cannot be 0",
308            increment_span,
309        )));
310    }
311
312    let in_format = match input_format {
313        Some(i) => match i.coerce_into_string() {
314            Ok(v) => v,
315            Err(e) => {
316                return Err(ShellError::Generic(
317                    GenericError::new_internal(e.to_string(), "")
318                        .with_help("error with input_format as_string"),
319                ));
320            }
321        },
322        _ => "%Y-%m-%d".to_string(),
323    };
324
325    let out_format = match output_format {
326        Some(o) => match o.coerce_into_string() {
327            Ok(v) => v,
328            Err(e) => {
329                return Err(ShellError::Generic(
330                    GenericError::new_internal(e.to_string(), "")
331                        .with_help("error with output_format as_string"),
332                ));
333            }
334        },
335        _ => "%Y-%m-%d".to_string(),
336    };
337
338    let start_date = match beginning_date {
339        Some(d) => match parse_date_string(&d, &in_format) {
340            Ok(nd) => nd,
341            Err(e) => {
342                return Err(ShellError::Generic(GenericError::new(
343                    e.to_string(),
344                    "Failed to parse date",
345                    call_span,
346                )));
347            }
348        },
349        _ => today,
350    };
351
352    let mut end_date = match ending_date {
353        Some(d) => match parse_date_string(&d, &in_format) {
354            Ok(nd) => nd,
355            Err(e) => {
356                return Err(ShellError::Generic(GenericError::new(
357                    e.to_string(),
358                    "Failed to parse date",
359                    call_span,
360                )));
361            }
362        },
363        _ => today,
364    };
365
366    let mut days_to_output = match day_count {
367        Some(d) => i64::from_value(d)?,
368        None => 0i64,
369    };
370
371    let mut periods_to_output = match period_count {
372        Some(d) => i64::from_value(d)?,
373        None => 0i64,
374    };
375
376    // Make the signs opposite if we're created dates in reverse direction
377    if reverse {
378        step_size *= -1;
379        days_to_output *= -1;
380        periods_to_output *= -1;
381    }
382
383    // --days is ignored when --periods is set
384    if periods_to_output != 0 {
385        end_date = periods_to_output
386            .checked_sub(1)
387            .and_then(|val| val.checked_mul(step_size.abs()))
388            .map(Duration::nanoseconds)
389            .and_then(|inc| start_date.checked_add_signed(inc))
390            .ok_or_else(|| {
391                ShellError::Generic(GenericError::new(
392                    "incrementing by the number of periods is too large",
393                    "incrementing by the number of periods is too large",
394                    call_span,
395                ))
396            })?;
397    } else if days_to_output != 0 {
398        end_date = days_to_output
399            .checked_sub(1)
400            .and_then(Duration::try_days)
401            .and_then(|days| start_date.checked_add_signed(days))
402            .ok_or_else(|| {
403                ShellError::Generic(GenericError::new(
404                    "int value too large",
405                    "int value too large",
406                    call_span,
407                ))
408            })?;
409    }
410
411    // conceptually counting down with a positive step or counting up with a negative step
412    // makes no sense, attempt to do what one means by inverting the signs in those cases.
413    if (start_date > end_date) && (step_size > 0) || (start_date < end_date) && step_size < 0 {
414        step_size = -step_size;
415    }
416
417    let is_out_of_range =
418        |next| (step_size > 0 && next > end_date) || (step_size < 0 && next < end_date);
419
420    // Bounds are enforced by i64 conversion above
421    let step_size = Duration::nanoseconds(step_size);
422
423    let mut next = start_date;
424    if is_out_of_range(next) {
425        return Err(ShellError::Generic(GenericError::new(
426            "date is out of range",
427            "date is out of range",
428            call_span,
429        )));
430    }
431
432    let mut ret = vec![];
433    loop {
434        let mut date_string = String::new();
435        match write!(date_string, "{}", next.format(&out_format)) {
436            Ok(_) => {}
437            Err(e) => {
438                return Err(ShellError::Generic(GenericError::new(
439                    "Invalid output format",
440                    e.to_string(),
441                    call_span,
442                )));
443            }
444        }
445        ret.push(Value::string(date_string, call_span));
446        if let Some(n) = next.checked_add_signed(step_size) {
447            next = n;
448        } else {
449            return Err(ShellError::Generic(GenericError::new(
450                "date overflow",
451                "adding the increment overflowed",
452                call_span,
453            )));
454        }
455
456        if is_out_of_range(next) {
457            break;
458        }
459    }
460
461    Ok(Value::list(ret, call_span))
462}
463
464#[cfg(test)]
465mod test {
466    use super::*;
467
468    #[test]
469    fn test_examples() -> nu_test_support::Result {
470        nu_test_support::test().examples(SeqDate)
471    }
472}