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 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 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 if reverse {
387 step_size *= -1;
388 days_to_output *= -1;
389 periods_to_output *= -1;
390 }
391
392 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 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 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}