From b412ff92c0a477de52588a8a5d4291776712bced Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Wed, 11 Nov 2020 14:35:02 -0600 Subject: [PATCH] Seq with dates (#2746) * seq with dates - wip * everything seems to be working, yay! * clippy --- crates/nu-cli/src/cli.rs | 1 + crates/nu-cli/src/commands.rs | 2 + crates/nu-cli/src/commands/seq_dates.rs | 379 ++++++++++++++++++++++ crates/nu-protocol/src/value.rs | 8 + crates/nu-protocol/src/value/primitive.rs | 27 +- 5 files changed, 415 insertions(+), 2 deletions(-) create mode 100644 crates/nu-cli/src/commands/seq_dates.rs diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index dcdd5e231a..47fbba2934 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -279,6 +279,7 @@ pub fn create_default_context(interactive: bool) -> Result>, + output_format: Option>, + input_format: Option>, + begin_date: Option>, + end_date: Option>, + increment: Option>, + days: Option>, + reverse: Tagged, +} + +#[async_trait] +impl WholeStreamCommand for SeqDates { + fn name(&self) -> &str { + "seq date" + } + + fn signature(&self) -> Signature { + Signature::build("seq date") + .named( + "separator", + SyntaxShape::String, + "separator character (defaults to \\n)", + Some('s'), + ) + .named( + "output_format", + SyntaxShape::String, + "prints dates in this format (defaults to %Y-%m-%d)", + Some('o'), + ) + .named( + "input_format", + SyntaxShape::String, + "give argument dates in this format (defaults to %Y-%m-%d)", + Some('i'), + ) + .named( + "begin_date", + SyntaxShape::String, + "beginning date range", + Some('b'), + ) + .named("end_date", SyntaxShape::String, "ending date", Some('e')) + .named( + "increment", + SyntaxShape::Int, + "increment dates by this number", + Some('n'), + ) + .named( + "days", + SyntaxShape::Int, + "number of days to print", + Some('d'), + ) + .switch("reverse", "print dates in reverse", Some('r')) + } + + fn usage(&self) -> &str { + "print sequences of dates" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + seq_dates(args, registry).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "print the next 10 days in YYYY-MM-DD format with newline separator", + example: "seq date --days 10", + result: None, + }, + Example { + description: "print the previous 10 days in YYYY-MM-DD format with newline separator", + example: "seq date --days 10 -r", + result: None, + }, + Example { + description: "print the previous 10 days starting today in MM/DD/YYYY format with newline separator", + example: "seq date --days 10 -o '%m/%d/%Y' -r", + result: None, + }, + Example { + description: "print the first 10 days in January, 2020", + example: "seq date -b '2020-01-01' -e '2020-01-10'", + result: Some(vec![ + UntaggedValue::string("2020-01-01").into(), + UntaggedValue::string("2020-01-02").into(), + UntaggedValue::string("2020-01-03").into(), + UntaggedValue::string("2020-01-04").into(), + UntaggedValue::string("2020-01-05").into(), + UntaggedValue::string("2020-01-06").into(), + UntaggedValue::string("2020-01-07").into(), + UntaggedValue::string("2020-01-08").into(), + UntaggedValue::string("2020-01-09").into(), + UntaggedValue::string("2020-01-10").into(), + ]), + }, + Example { + description: "print every fifth day between January 1st 2020 and January 31st 2020", + example: "seq date -b '2020-01-01' -e '2020-01-31' -n 5", + result: Some(vec![ + UntaggedValue::string("2020-01-01").into(), + UntaggedValue::string("2020-01-06").into(), + UntaggedValue::string("2020-01-11").into(), + UntaggedValue::string("2020-01-16").into(), + UntaggedValue::string("2020-01-21").into(), + UntaggedValue::string("2020-01-26").into(), + UntaggedValue::string("2020-01-31").into(), + ]), + }, + Example { + description: "starting on May 5th, 2020, print the next 10 days in your locale's date format, colon separated", + example: "seq date -o %x -s ':' -d 10 -b '2020-05-01'", + result: None, + }, + ] + } +} + +async fn seq_dates( + args: CommandArgs, + registry: &CommandRegistry, +) -> Result { + let registry = registry.clone(); + let _name = args.call_info.name_tag.clone(); + + let ( + SeqDatesArgs { + separator, + output_format, + input_format, + begin_date, + end_date, + increment, + days, + reverse, + }, + _, + ) = args.process(®istry).await?; + + let sep: String = match separator { + Some(s) => { + if s.item == r"\t" { + '\t'.to_string() + } else if s.item == r"\n" { + '\n'.to_string() + } else if s.item == r"\r" { + '\r'.to_string() + } else { + let vec_s: Vec = s.chars().collect(); + if vec_s.is_empty() { + return Err(ShellError::labeled_error( + "Expected a single separator char from --separator", + "requires a single character string input", + &s.tag, + )); + }; + vec_s.iter().collect() + } + } + _ => '\n'.to_string(), + }; + + let outformat = match output_format { + Some(s) => Some(s.item.to_string_value(s.tag)), + _ => None, + }; + + let informat = match input_format { + Some(s) => Some(s.item.to_string_value(s.tag)), + _ => None, + }; + + let begin = match begin_date { + Some(s) => Some(s.item), + _ => None, + }; + + let end = match end_date { + Some(s) => Some(s.item), + _ => None, + }; + + let inc = match increment { + Some(i) => { + let clone = i.clone(); + i.to_value(clone.tag) + } + _ => (1 as i64).to_value_create_tag(), + }; + + let day_count: Option = match days { + Some(i) => Some(i.item.to_value(i.tag)), + _ => None, + }; + + let mut rev = false; + if *reverse { + rev = *reverse; + } + + run_seq_dates(sep, outformat, informat, begin, end, inc, day_count, rev) +} + +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::too_many_arguments)] +pub fn run_seq_dates( + separator: String, + output_format: Option, + input_format: Option, + beginning_date: Option, + ending_date: Option, + increment: Value, + day_count: Option, + reverse: bool, +) -> Result { + let today = Local::today().naive_local(); + let mut step_size: i64 = increment + .as_i64() + .expect("unable to change increment to i64"); + + if step_size == 0 { + return Err(ShellError::labeled_error( + "increment cannot be 0", + "increment cannot be 0", + increment.tag, + )); + } + + let in_format = match input_format { + Some(i) => i.as_string().map_err(|e| { + ShellError::labeled_error( + e.to_string(), + "error with input_format as_string", + i.tag.span, + ) + })?, + None => "%Y-%m-%d".to_string(), + }; + + let out_format = match output_format { + Some(o) => o.as_string().map_err(|e| { + ShellError::labeled_error( + e.to_string(), + "error with output_format as_string", + o.tag.span, + ) + })?, + None => "%Y-%m-%d".to_string(), + }; + + let start_date = match beginning_date { + Some(d) => match parse_date_string(&d, &in_format) { + Ok(nd) => nd, + Err(e) => { + return Err(ShellError::labeled_error( + e, + "Failed to parse date", + Tag::unknown(), + )) + } + }, + _ => today, + }; + + let mut end_date = match ending_date { + Some(d) => match parse_date_string(&d, &in_format) { + Ok(nd) => nd, + Err(e) => { + return Err(ShellError::labeled_error( + e, + "Failed to parse date", + Tag::unknown(), + )) + } + }, + _ => today, + }; + + let mut days_to_output = match day_count { + Some(d) => d.as_i64()?, + None => 0i64, + }; + + // Make the signs opposite if we're created dates in reverse direction + if reverse { + step_size *= -1; + days_to_output *= -1; + } + + if days_to_output != 0 { + end_date = match start_date.checked_add_signed(Duration::days(days_to_output)) { + Some(date) => date, + None => { + return Err(ShellError::labeled_error( + "integer value too large", + "integer value too large", + Tag::unknown(), + )); + } + } + } + + // conceptually counting down with a positive step or counting up with a negative step + // makes no sense, attempt to do what one means by inverting the signs in those cases. + if (start_date > end_date) && (step_size > 0) || (start_date < end_date) && step_size < 0 { + step_size = -step_size; + } + + let is_out_of_range = + |next| (step_size > 0 && next > end_date) || (step_size < 0 && next < end_date); + + let mut next = start_date; + if is_out_of_range(next) { + return Err(ShellError::labeled_error( + "date is out of range", + "date is out of range", + Tag::unknown(), + )); + } + + let mut ret_str = String::from(""); + loop { + ret_str.push_str(&format!("{}", next.format(&out_format))); + // TODO: check this value is good + next += Duration::days(step_size); + + if is_out_of_range(next) { + break; + } + + ret_str.push_str(&separator); + } + + let rows: Vec = ret_str + .lines() + .map(|v| v.to_str_value_create_tag()) + .collect(); + Ok(futures::stream::iter(rows.into_iter().map(ReturnSuccess::value)).to_output_stream()) +} + +#[cfg(test)] +mod tests { + use super::SeqDates; + use super::ShellError; + + #[test] + fn examples_work_as_expected() -> Result<(), ShellError> { + use crate::examples::test as test_examples; + + Ok(test_examples(SeqDates {})?) + } +} diff --git a/crates/nu-protocol/src/value.rs b/crates/nu-protocol/src/value.rs index 053e0bbc4e..67c9bafccd 100644 --- a/crates/nu-protocol/src/value.rs +++ b/crates/nu-protocol/src/value.rs @@ -400,6 +400,14 @@ impl Value { } } + /// View the Value as signed 64-bit, if possible + pub fn as_i64(&self) -> Result { + match &self.value { + UntaggedValue::Primitive(primitive) => primitive.as_i64(self.tag.span), + _ => Err(ShellError::type_error("integer", self.spanned_type_name())), + } + } + /// View the Value as boolean, if possible pub fn as_bool(&self) -> Result { match &self.value { diff --git a/crates/nu-protocol/src/value/primitive.rs b/crates/nu-protocol/src/value/primitive.rs index 3c45a47ceb..02ce1a6d4d 100644 --- a/crates/nu-protocol/src/value/primitive.rs +++ b/crates/nu-protocol/src/value/primitive.rs @@ -69,14 +69,37 @@ impl Primitive { ShellError::range_error( ExpectedRange::U64, &format!("{}", int).spanned(span), - "converting an integer into a 64-bit integer", + "converting an integer into an unsigned 64-bit integer", ) }), Primitive::Decimal(decimal) => decimal.to_u64().ok_or_else(|| { ShellError::range_error( ExpectedRange::U64, &format!("{}", decimal).spanned(span), - "converting a decimal into a 64-bit integer", + "converting a decimal into an unsigned 64-bit integer", + ) + }), + other => Err(ShellError::type_error( + "number", + other.type_name().spanned(span), + )), + } + } + + pub fn as_i64(&self, span: Span) -> Result { + match self { + Primitive::Int(int) => int.to_i64().ok_or_else(|| { + ShellError::range_error( + ExpectedRange::I64, + &format!("{}", int).spanned(span), + "converting an integer into a signed 64-bit integer", + ) + }), + Primitive::Decimal(decimal) => decimal.to_i64().ok_or_else(|| { + ShellError::range_error( + ExpectedRange::I64, + &format!("{}", decimal).spanned(span), + "converting a decimal into a signed 64-bit integer", ) }), other => Err(ShellError::type_error(