diff --git a/crates/nu-command/src/generators/seq_date.rs b/crates/nu-command/src/generators/seq_date.rs index 4d378a73bb..ae91b8ad0c 100644 --- a/crates/nu-command/src/generators/seq_date.rs +++ b/crates/nu-command/src/generators/seq_date.rs @@ -1,9 +1,11 @@ -use chrono::{Duration, Local, NaiveDate}; +use chrono::{Duration, Local, NaiveDate, NaiveDateTime}; use nu_engine::command_prelude::*; use nu_protocol::FromValue; use std::fmt::Write; +const NANOSECONDS_IN_DAY: i64 = 1_000_000_000i64 * 60i64 * 60i64 * 24i64; + #[derive(Clone)] pub struct SeqDate; @@ -40,16 +42,22 @@ impl Command for SeqDate { .named("end-date", SyntaxShape::String, "ending date", Some('e')) .named( "increment", - SyntaxShape::Int, - "increment dates by this number", + SyntaxShape::OneOf(vec![SyntaxShape::Duration, SyntaxShape::Int]), + "increment dates by this duration (defaults to days if integer)", Some('n'), ) .named( "days", SyntaxShape::Int, - "number of days to print", + "number of days to print (ignored if periods is used)", Some('d'), ) + .named( + "periods", + SyntaxShape::Int, + "number of periods to print", + Some('p'), + ) .switch("reverse", "print dates in reverse", Some('r')) .category(Category::Generators) } @@ -74,7 +82,8 @@ impl Command for SeqDate { }, Example { description: "Return the first 10 days in January, 2020", - example: "seq date --begin-date '2020-01-01' --end-date '2020-01-10'", + example: + "seq date --begin-date '2020-01-01' --end-date '2020-01-10' --increment 1day", result: Some(Value::list( vec![ Value::test_string("2020-01-01"), @@ -91,9 +100,45 @@ impl Command for SeqDate { Span::test_data(), )), }, + Example { + description: "Return the first 10 days in January, 2020 using --days flag", + example: + "seq date --begin-date '2020-01-01' --days 10 --increment 1day", + result: Some(Value::list( + vec![ + Value::test_string("2020-01-01"), + Value::test_string("2020-01-02"), + Value::test_string("2020-01-03"), + Value::test_string("2020-01-04"), + Value::test_string("2020-01-05"), + Value::test_string("2020-01-06"), + Value::test_string("2020-01-07"), + Value::test_string("2020-01-08"), + Value::test_string("2020-01-09"), + Value::test_string("2020-01-10"), + ], + Span::test_data(), + )), + }, + Example { + description: "Return the first five 5-minute periods starting January 1, 2020", + example: + "seq date --begin-date '2020-01-01' --periods 5 --increment 5min --output-format '%Y-%m-%d %H:%M:%S'", + result: Some(Value::list( + vec![ + Value::test_string("2020-01-01 00:00:00"), + Value::test_string("2020-01-01 00:05:00"), + Value::test_string("2020-01-01 00:10:00"), + Value::test_string("2020-01-01 00:15:00"), + Value::test_string("2020-01-01 00:20:00"), + ], + Span::test_data(), + )), + }, Example { description: "print every fifth day between January 1st 2020 and January 31st 2020", - example: "seq date --begin-date '2020-01-01' --end-date '2020-01-31' --increment 5", + example: + "seq date --begin-date '2020-01-01' --end-date '2020-01-31' --increment 5day", result: Some(Value::list( vec![ Value::test_string("2020-01-01"), @@ -107,6 +152,42 @@ impl Command for SeqDate { Span::test_data(), )), }, + Example { + description: "increment defaults to days if no duration is supplied", + example: + "seq date --begin-date '2020-01-01' --end-date '2020-01-31' --increment 5", + result: Some(Value::list( + vec![ + Value::test_string("2020-01-01"), + Value::test_string("2020-01-06"), + Value::test_string("2020-01-11"), + Value::test_string("2020-01-16"), + Value::test_string("2020-01-21"), + Value::test_string("2020-01-26"), + Value::test_string("2020-01-31"), + ], + Span::test_data(), + )), + }, + Example { + description: "print every six hours starting January 1st, 2020 until January 3rd, 2020", + example: + "seq date --begin-date '2020-01-01' --end-date '2020-01-03' --increment 6hr --output-format '%Y-%m-%d %H:%M:%S'", + result: Some(Value::list( + vec![ + Value::test_string("2020-01-01 00:00:00"), + Value::test_string("2020-01-01 06:00:00"), + Value::test_string("2020-01-01 12:00:00"), + Value::test_string("2020-01-01 18:00:00"), + Value::test_string("2020-01-02 00:00:00"), + Value::test_string("2020-01-02 06:00:00"), + Value::test_string("2020-01-02 12:00:00"), + Value::test_string("2020-01-02 18:00:00"), + Value::test_string("2020-01-03 00:00:00"), + ], + Span::test_data(), + )), + }, ] } @@ -124,8 +205,28 @@ impl Command for SeqDate { let begin_date: Option> = call.get_flag(engine_state, stack, "begin-date")?; let end_date: Option> = call.get_flag(engine_state, stack, "end-date")?; - let increment: Option> = call.get_flag(engine_state, stack, "increment")?; + + let increment = match call.get_flag::(engine_state, stack, "increment")? { + Some(increment) => match increment { + Value::Int { val, internal_span } => Some( + val.checked_mul(NANOSECONDS_IN_DAY) + .ok_or_else(|| ShellError::GenericError { + error: "increment is too large".into(), + msg: "increment is too large".into(), + span: Some(internal_span), + help: None, + inner: vec![], + })? + .into_spanned(internal_span), + ), + Value::Duration { val, internal_span } => Some(val.into_spanned(internal_span)), + _ => None, + }, + None => None, + }; + let days: Option> = call.get_flag(engine_state, stack, "days")?; + let periods: Option> = call.get_flag(engine_state, stack, "periods")?; let reverse = call.has_flag(engine_state, stack, "reverse")?; let out_format = match output_format { @@ -150,29 +251,41 @@ impl Command for SeqDate { let inc = match increment { Some(i) => Value::int(i.item, i.span), - _ => Value::int(1_i64, call.head), + _ => Value::int(NANOSECONDS_IN_DAY, call.head), }; let day_count = days.map(|i| Value::int(i.item, i.span)); + let period_count = periods.map(|i| Value::int(i.item, i.span)); + let mut rev = false; if reverse { rev = reverse; } Ok(run_seq_dates( - out_format, in_format, begin, end, inc, day_count, rev, call.head, + out_format, + in_format, + begin, + end, + inc, + day_count, + period_count, + rev, + call.head, )? .into_pipeline_data()) } } -pub fn parse_date_string(s: &str, format: &str) -> Result { - let d = match NaiveDate::parse_from_str(s, format) { - Ok(d) => d, - Err(_) => return Err("Failed to parse date."), - }; - Ok(d) +#[allow(clippy::unnecessary_lazy_evaluations)] +pub fn parse_date_string(s: &str, format: &str) -> Result { + NaiveDateTime::parse_from_str(s, format).or_else(|_| { + // If parsing as DateTime fails, try parsing as Date before throwing error + let date = NaiveDate::parse_from_str(s, format).map_err(|_| "Failed to parse date.")?; + date.and_hms_opt(0, 0, 0) + .ok_or_else(|| "Failed to convert NaiveDate to NaiveDateTime.") + }) } #[allow(clippy::too_many_arguments)] @@ -183,10 +296,11 @@ pub fn run_seq_dates( ending_date: Option, increment: Value, day_count: Option, + period_count: Option, reverse: bool, call_span: Span, ) -> Result { - let today = Local::now().date_naive(); + let today = Local::now().naive_local(); // if cannot convert , it will return error let increment_span = increment.span(); let mut step_size: i64 = i64::from_value(increment)?; @@ -270,27 +384,44 @@ pub fn run_seq_dates( None => 0i64, }; + let mut periods_to_output = match period_count { + Some(d) => i64::from_value(d)?, + None => 0i64, + }; + // Make the signs opposite if we're created dates in reverse direction if reverse { step_size *= -1; days_to_output *= -1; + periods_to_output *= -1; } - if days_to_output != 0 { - end_date = match Duration::try_days(days_to_output) + // --days is ignored when --periods is set + if periods_to_output != 0 { + end_date = periods_to_output + .checked_sub(1) + .and_then(|val| val.checked_mul(step_size.abs())) + .map(Duration::nanoseconds) + .and_then(|inc| start_date.checked_add_signed(inc)) + .ok_or_else(|| ShellError::GenericError { + error: "incrementing by the number of periods is too large".into(), + msg: "incrementing by the number of periods is too large".into(), + span: Some(call_span), + help: None, + inner: vec![], + })?; + } else if days_to_output != 0 { + end_date = days_to_output + .checked_sub(1) + .and_then(Duration::try_days) .and_then(|days| start_date.checked_add_signed(days)) - { - Some(date) => date, - None => { - return Err(ShellError::GenericError { - error: "int value too large".into(), - msg: "int value too large".into(), - span: Some(call_span), - help: None, - inner: vec![], - }); - } - } + .ok_or_else(|| ShellError::GenericError { + error: "int value too large".into(), + msg: "int value too large".into(), + span: Some(call_span), + help: None, + inner: vec![], + })?; } // conceptually counting down with a positive step or counting up with a negative step @@ -302,15 +433,8 @@ pub fn run_seq_dates( let is_out_of_range = |next| (step_size > 0 && next > end_date) || (step_size < 0 && next < end_date); - let Some(step_size) = Duration::try_days(step_size) else { - return Err(ShellError::GenericError { - error: "increment magnitude is too large".into(), - msg: "increment magnitude is too large".into(), - span: Some(call_span), - help: None, - inner: vec![], - }); - }; + // Bounds are enforced by i64 conversion above + let step_size = Duration::nanoseconds(step_size); let mut next = start_date; if is_out_of_range(next) { diff --git a/crates/nu-command/tests/commands/seq_date.rs b/crates/nu-command/tests/commands/seq_date.rs index c333c87773..f9f6ca818f 100644 --- a/crates/nu-command/tests/commands/seq_date.rs +++ b/crates/nu-command/tests/commands/seq_date.rs @@ -1,8 +1,17 @@ use nu_test_support::nu; #[test] -fn fails_when_output_format_contains_time() { - let actual = nu!("seq date --output-format '%H-%M-%S'"); +fn fails_on_datetime_input() { + let actual = nu!("seq date --begin-date (date now)"); - assert!(actual.err.contains("Invalid output format")); + assert!(actual.err.contains("Type mismatch")) +} + +#[test] +fn fails_when_increment_not_integer_or_duration() { + let actual = nu!("seq date --begin-date 2020-01-01 --increment 1.1"); + + assert!(actual + .err + .contains("expected one of a list of accepted shapes: [Duration, Int]")) }