mirror of
https://github.com/nushell/nushell.git
synced 2025-02-13 00:50:28 +01:00
Seq with dates (#2746)
* seq with dates - wip * everything seems to be working, yay! * clippy
This commit is contained in:
parent
5a75e11b0e
commit
b412ff92c0
@ -279,6 +279,7 @@ pub fn create_default_context(interactive: bool) -> Result<EvaluationContext, Bo
|
|||||||
whole_stream_command(UrlHost),
|
whole_stream_command(UrlHost),
|
||||||
whole_stream_command(UrlQuery),
|
whole_stream_command(UrlQuery),
|
||||||
whole_stream_command(Seq),
|
whole_stream_command(Seq),
|
||||||
|
whole_stream_command(SeqDates),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
#[cfg(feature = "clipboard-cli")]
|
#[cfg(feature = "clipboard-cli")]
|
||||||
|
@ -99,6 +99,7 @@ pub(crate) mod run_external;
|
|||||||
pub(crate) mod save;
|
pub(crate) mod save;
|
||||||
pub(crate) mod select;
|
pub(crate) mod select;
|
||||||
pub(crate) mod seq;
|
pub(crate) mod seq;
|
||||||
|
pub(crate) mod seq_dates;
|
||||||
pub(crate) mod shells;
|
pub(crate) mod shells;
|
||||||
pub(crate) mod shuffle;
|
pub(crate) mod shuffle;
|
||||||
pub(crate) mod size;
|
pub(crate) mod size;
|
||||||
@ -240,6 +241,7 @@ pub(crate) use run_external::RunExternalCommand;
|
|||||||
pub(crate) use save::Save;
|
pub(crate) use save::Save;
|
||||||
pub(crate) use select::Select;
|
pub(crate) use select::Select;
|
||||||
pub(crate) use seq::Seq;
|
pub(crate) use seq::Seq;
|
||||||
|
pub(crate) use seq_dates::SeqDates;
|
||||||
pub(crate) use shells::Shells;
|
pub(crate) use shells::Shells;
|
||||||
pub(crate) use shuffle::Shuffle;
|
pub(crate) use shuffle::Shuffle;
|
||||||
pub(crate) use size::Size;
|
pub(crate) use size::Size;
|
||||||
|
379
crates/nu-cli/src/commands/seq_dates.rs
Normal file
379
crates/nu-cli/src/commands/seq_dates.rs
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
use crate::commands::WholeStreamCommand;
|
||||||
|
use crate::prelude::*;
|
||||||
|
use chrono::naive::NaiveDate;
|
||||||
|
use chrono::{Duration, Local};
|
||||||
|
use nu_errors::ShellError;
|
||||||
|
use nu_protocol::{value::I64Ext, value::StrExt, value::StringExt, value::U64Ext};
|
||||||
|
use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value};
|
||||||
|
use nu_source::Tagged;
|
||||||
|
|
||||||
|
pub struct SeqDates;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SeqDatesArgs {
|
||||||
|
separator: Option<Tagged<String>>,
|
||||||
|
output_format: Option<Tagged<String>>,
|
||||||
|
input_format: Option<Tagged<String>>,
|
||||||
|
begin_date: Option<Tagged<String>>,
|
||||||
|
end_date: Option<Tagged<String>>,
|
||||||
|
increment: Option<Tagged<i64>>,
|
||||||
|
days: Option<Tagged<u64>>,
|
||||||
|
reverse: Tagged<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<OutputStream, ShellError> {
|
||||||
|
seq_dates(args, registry).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
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<OutputStream, ShellError> {
|
||||||
|
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<char> = 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<Value> = 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<NaiveDate, &'static str> {
|
||||||
|
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<Value>,
|
||||||
|
input_format: Option<Value>,
|
||||||
|
beginning_date: Option<String>,
|
||||||
|
ending_date: Option<String>,
|
||||||
|
increment: Value,
|
||||||
|
day_count: Option<Value>,
|
||||||
|
reverse: bool,
|
||||||
|
) -> Result<OutputStream, ShellError> {
|
||||||
|
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<Value> = 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 {})?)
|
||||||
|
}
|
||||||
|
}
|
@ -400,6 +400,14 @@ impl Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// View the Value as signed 64-bit, if possible
|
||||||
|
pub fn as_i64(&self) -> Result<i64, ShellError> {
|
||||||
|
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
|
/// View the Value as boolean, if possible
|
||||||
pub fn as_bool(&self) -> Result<bool, ShellError> {
|
pub fn as_bool(&self) -> Result<bool, ShellError> {
|
||||||
match &self.value {
|
match &self.value {
|
||||||
|
@ -69,14 +69,37 @@ impl Primitive {
|
|||||||
ShellError::range_error(
|
ShellError::range_error(
|
||||||
ExpectedRange::U64,
|
ExpectedRange::U64,
|
||||||
&format!("{}", int).spanned(span),
|
&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(|| {
|
Primitive::Decimal(decimal) => decimal.to_u64().ok_or_else(|| {
|
||||||
ShellError::range_error(
|
ShellError::range_error(
|
||||||
ExpectedRange::U64,
|
ExpectedRange::U64,
|
||||||
&format!("{}", decimal).spanned(span),
|
&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<i64, ShellError> {
|
||||||
|
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(
|
other => Err(ShellError::type_error(
|
||||||
|
Loading…
Reference in New Issue
Block a user