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 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 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 if reverse {
390 step_size *= -1;
391 days_to_output *= -1;
392 periods_to_output *= -1;
393 }
394
395 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 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 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}