diff --git a/Cargo.lock b/Cargo.lock index 53b194d5b..a00377e63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2845,7 +2845,7 @@ dependencies = [ [[package]] name = "reedline" version = "0.2.0" -source = "git+https://github.com/nushell/reedline?branch=main#9ec02cb7383fcd16971caa32996b8eb5e6a32704" +source = "git+https://github.com/nushell/reedline?branch=main#56025adb65f1c27078d64e5d6220827a6f0ebdb3" dependencies = [ "chrono", "crossterm", diff --git a/crates/nu-command/src/calendar/mod.rs b/crates/nu-command/src/calendar/mod.rs deleted file mode 100644 index 09f61b8ce..000000000 --- a/crates/nu-command/src/calendar/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod cal; - -pub use cal::Cal; diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 7f97ea178..573ac257a 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -295,6 +295,7 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { // Generators bind_command! { Cal, + SeqDate, }; // Hash diff --git a/crates/nu-command/src/calendar/cal.rs b/crates/nu-command/src/generators/cal.rs similarity index 100% rename from crates/nu-command/src/calendar/cal.rs rename to crates/nu-command/src/generators/cal.rs diff --git a/crates/nu-command/src/generators/mod.rs b/crates/nu-command/src/generators/mod.rs new file mode 100644 index 000000000..6adadb601 --- /dev/null +++ b/crates/nu-command/src/generators/mod.rs @@ -0,0 +1,5 @@ +mod cal; +mod seq_date; + +pub use cal::Cal; +pub use seq_date::SeqDate; diff --git a/crates/nu-command/src/generators/seq_date.rs b/crates/nu-command/src/generators/seq_date.rs new file mode 100644 index 000000000..0837f6c1d --- /dev/null +++ b/crates/nu-command/src/generators/seq_date.rs @@ -0,0 +1,370 @@ +use chrono::naive::NaiveDate; +use chrono::{Duration, Local}; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Spanned, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SeqDate; + +impl Command for SeqDate { + fn name(&self) -> &str { + "seq date" + } + + fn usage(&self) -> &str { + "print sequences of dates" + } + + fn signature(&self) -> nu_protocol::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')) + .category(Category::Generators) + } + + fn examples(&self) -> Vec { + let span = Span::test_data(); + + 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(Value::List { + vals: vec![ + Value::String { val: "2020-01-01".into(), span, }, + Value::String { val: "2020-01-02".into(), span, }, + Value::String { val: "2020-01-03".into(), span, }, + Value::String { val: "2020-01-04".into(), span, }, + Value::String { val: "2020-01-05".into(), span, }, + Value::String { val: "2020-01-06".into(), span, }, + Value::String { val: "2020-01-07".into(), span, }, + Value::String { val: "2020-01-08".into(), span, }, + Value::String { val: "2020-01-09".into(), span, }, + Value::String { val: "2020-01-10".into(), span, }, + ], + span, + }), + }, + 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(Value::List { + vals: vec![ + Value::String { val: "2020-01-01".into(), span, }, + Value::String { val: "2020-01-06".into(), span, }, + Value::String { val: "2020-01-11".into(), span, }, + Value::String { val: "2020-01-16".into(), span, }, + Value::String { val: "2020-01-21".into(), span, }, + Value::String { val: "2020-01-26".into(), span, }, + Value::String { val: "2020-01-31".into(), span, }, + ], + span, + }), + }, + 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, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let separator: Option> = call.get_flag(engine_state, stack, "separator")?; + let output_format: Option> = + call.get_flag(engine_state, stack, "output-format")?; + let input_format: Option> = + call.get_flag(engine_state, stack, "input-format")?; + 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 days: Option> = call.get_flag(engine_state, stack, "days")?; + let reverse = call.has_flag("reverse"); + + 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.item.chars().collect(); + if vec_s.is_empty() { + return Err(ShellError::SpannedLabeledError( + "Expected a single separator char from --separator".to_string(), + "requires a single character string input".to_string(), + s.span, + )); + }; + vec_s.iter().collect() + } + } + _ => '\n'.to_string(), + }; + + let outformat = match output_format { + Some(s) => Some(Value::string(s.item, s.span)), + _ => None, + }; + + let informat = match input_format { + Some(s) => Some(Value::string(s.item, s.span)), + _ => 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) => Value::int(i.item, i.span), + _ => Value::int(1_i64, Span::test_data()), + }; + + let day_count = days.map(|i| Value::int(i.item, i.span)); + + let mut rev = false; + if reverse { + rev = reverse; + } + + Ok( + run_seq_dates(sep, outformat, informat, begin, end, inc, day_count, rev)? + .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::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::SpannedLabeledError( + "increment cannot be 0".to_string(), + "increment cannot be 0".to_string(), + increment.span()?, + )); + } + + let in_format = match input_format { + Some(i) => match i.as_string() { + Ok(v) => v, + Err(e) => { + return Err(ShellError::LabeledError( + e.to_string(), + "error with input_format as_string".to_string(), + )); + } + }, + _ => "%Y-%m-%d".to_string(), + }; + + let out_format = match output_format { + Some(i) => match i.as_string() { + Ok(v) => v, + Err(e) => { + return Err(ShellError::LabeledError( + e.to_string(), + "error with output_format as_string".to_string(), + )); + } + }, + _ => "%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::SpannedLabeledError( + e.to_string(), + "Failed to parse date".to_string(), + Span::test_data(), + )) + } + }, + _ => 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::SpannedLabeledError( + e.to_string(), + "Failed to parse date".to_string(), + Span::test_data(), + )) + } + }, + _ => 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::SpannedLabeledError( + "integer value too large".to_string(), + "integer value too large".to_string(), + Span::test_data(), + )); + } + } + } + + // 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::SpannedLabeledError( + "date is out of range".to_string(), + "date is out of range".to_string(), + Span::test_data(), + )); + } + + let mut ret_str = String::from(""); + loop { + ret_str.push_str(&next.format(&out_format).to_string()); + 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| Value::string(v, Span::test_data())) + .collect(); + + Ok(Value::List { + vals: rows, + span: Span::test_data(), + }) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SeqDate {}) + } +} diff --git a/crates/nu-command/src/lib.rs b/crates/nu-command/src/lib.rs index 5d82ffe0e..3caca1972 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -1,4 +1,3 @@ -mod calendar; mod conversions; mod core_commands; mod date; @@ -9,6 +8,7 @@ mod experimental; mod filesystem; mod filters; mod formats; +mod generators; mod hash; mod math; mod network; @@ -20,7 +20,6 @@ mod strings; mod system; mod viewers; -pub use calendar::*; pub use conversions::*; pub use core_commands::*; pub use date::*; @@ -32,6 +31,7 @@ pub use experimental::*; pub use filesystem::*; pub use filters::*; pub use formats::*; +pub use generators::*; pub use hash::*; pub use math::*; pub use network::*;