mirror of
https://github.com/nushell/nushell.git
synced 2025-05-22 15:45:35 +00:00
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> This PR seeks to generalize the `seq date` command so that it can receive any duration as an `--increment`. Whereas the current command can only output a list of dates spaced at least 1 day apart, the new command can output a list of datetimes that are spaced apart by any duration. For example: ``` > seq date --begin-date 2025-01-01 --end-date 2025-01-02 --increment 6hr --output-format "%Y-%m-%d %H:%M:%S" ╭───┬─────────────────────╮ │ 0 │ 2025-01-01 00:00:00 │ │ 1 │ 2025-01-01 06:00:00 │ │ 2 │ 2025-01-01 12:00:00 │ │ 3 │ 2025-01-01 18:00:00 │ │ 4 │ 2025-01-02 00:00:00 │ ╰───┴─────────────────────╯ ``` Note that the default behavior remains unchanged: ``` > seq date --begin-date 2025-01-01 --end-date 2025-01-02 ╭───┬────────────╮ │ 0 │ 2025-01-01 │ │ 1 │ 2025-01-02 │ ╰───┴────────────╯ ``` The default output format also remains unchanged: ``` > seq date --begin-date 2025-01-01 --end-date 2025-01-02 --increment 6hr ╭───┬────────────╮ │ 0 │ 2025-01-01 │ │ 1 │ 2025-01-01 │ │ 2 │ 2025-01-01 │ │ 3 │ 2025-01-01 │ │ 4 │ 2025-01-02 │ ╰───┴────────────╯ ``` # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> ## Breaking Changes * The `--increment` argument no longer accepts just an integer and requires a duration ``` # NEW BEHAVIOR > seq date --begin-date 2025-01-01 --end-date 2025-01-02 --increment 1 Error: nu::parser::parse_mismatch × Parse mismatch during operation. ╭─[entry #13:1:68] 1 │ seq date --begin-date 2025-01-01 --end-date 2025-01-02 --increment 1 · ┬ · ╰── expected duration with valid units ╰──── ``` EDIT: Break Change is mitigated. `--increment` accepts either an integer or duration. ## Bug Fix * The `--days` argument had an off-by-one error and would print 1 too many elements in the output. For example, ``` # OLD BEHAVIOR > seq date -b 2025-01-01 --days 5 --increment 1 ╭───┬────────────╮ │ 0 │ 2025-01-01 │ │ 1 │ 2025-01-02 │ │ 2 │ 2025-01-03 │ │ 3 │ 2025-01-04 │ │ 4 │ 2025-01-05 │ │ 5 │ 2025-01-06 │ <-- Extra element ╰───┴────────────╯ # NEW BEHAVIOR > seq date -b 2025-01-01 --days 5 --increment 1day ╭───┬────────────╮ │ 0 │ 2025-01-01 │ │ 1 │ 2025-01-02 │ │ 2 │ 2025-01-03 │ │ 3 │ 2025-01-04 │ │ 4 │ 2025-01-05 │ ╰───┴────────────╯ ``` ## New Argument * A `--periods` argument is introduced to indicate the number of output elements, regardless of the `--increment` value. Importantly, the `--days` argument is ignored when `--periods` is set. ``` # NEW BEHAVIOR > seq date -b 2025-01-01 --days 5 --periods 10 --increment 1day ╭───┬────────────╮ │ 0 │ 2025-01-01 │ │ 1 │ 2025-01-02 │ │ 2 │ 2025-01-03 │ │ 3 │ 2025-01-04 │ │ 4 │ 2025-01-05 │ │ 5 │ 2025-01-06 │ │ 6 │ 2025-01-07 │ │ 7 │ 2025-01-08 │ │ 8 │ 2025-01-09 │ │ 9 │ 2025-01-10 │ ╰───┴────────────╯ ``` Note that the `--days` and `--periods` arguments differ in their functions. The `--periods` value determines the number of elements in the output that are always spaced `--increment` apart. The `--days` value determines the bookends `--begin-date` and `--end-date` when only one is set, though the number of elements may differ based on the `--increment` value. ``` # NEW BEHAVIOR > seq date -e 2025-01-01 --days 2 --increment 5hr --output-format "%Y-%m-%d %H:%M:%S" ╭───┬─────────────────────╮ │ 0 │ 2025-01-23 22:25:05 │ │ 1 │ 2025-01-24 03:25:05 │ │ 2 │ 2025-01-24 08:25:05 │ │ 3 │ 2025-01-24 13:25:05 │ │ 4 │ 2025-01-24 18:25:05 │ ╰───┴─────────────────────╯ ``` # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> I added several examples for each user-facing change in `generators/seq_date.rs` and some tests in `tests/commands/seq_date.rs`. # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
497 lines
18 KiB
Rust
497 lines
18 KiB
Rust
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;
|
|
|
|
impl Command for SeqDate {
|
|
fn name(&self) -> &str {
|
|
"seq date"
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
"Print sequences of dates."
|
|
}
|
|
|
|
fn signature(&self) -> nu_protocol::Signature {
|
|
Signature::build("seq date")
|
|
.input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::String)))])
|
|
.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::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 (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)
|
|
}
|
|
|
|
fn examples(&self) -> Vec<Example> {
|
|
vec![
|
|
Example {
|
|
description: "Return a list of the next 10 days in the YYYY-MM-DD format",
|
|
example: "seq date --days 10",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Return the previous 10 days in the YYYY-MM-DD format",
|
|
example: "seq date --days 10 --reverse",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description:
|
|
"Return the previous 10 days, starting today, in the MM/DD/YYYY format",
|
|
example: "seq date --days 10 -o '%m/%d/%Y' --reverse",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Return the first 10 days in January, 2020",
|
|
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"),
|
|
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 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 5day",
|
|
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: "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(),
|
|
)),
|
|
},
|
|
]
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
_input: PipelineData,
|
|
) -> Result<PipelineData, ShellError> {
|
|
let output_format: Option<Spanned<String>> =
|
|
call.get_flag(engine_state, stack, "output-format")?;
|
|
let input_format: Option<Spanned<String>> =
|
|
call.get_flag(engine_state, stack, "input-format")?;
|
|
let begin_date: Option<Spanned<String>> =
|
|
call.get_flag(engine_state, stack, "begin-date")?;
|
|
let end_date: Option<Spanned<String>> = call.get_flag(engine_state, stack, "end-date")?;
|
|
|
|
let increment = match call.get_flag::<Value>(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<Spanned<i64>> = call.get_flag(engine_state, stack, "days")?;
|
|
let periods: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "periods")?;
|
|
let reverse = call.has_flag(engine_state, stack, "reverse")?;
|
|
|
|
let out_format = match output_format {
|
|
Some(s) => Some(Value::string(s.item, s.span)),
|
|
_ => None,
|
|
};
|
|
|
|
let in_format = 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(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,
|
|
period_count,
|
|
rev,
|
|
call.head,
|
|
)?
|
|
.into_pipeline_data())
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::unnecessary_lazy_evaluations)]
|
|
pub fn parse_date_string(s: &str, format: &str) -> Result<NaiveDateTime, &'static str> {
|
|
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)]
|
|
pub fn run_seq_dates(
|
|
output_format: Option<Value>,
|
|
input_format: Option<Value>,
|
|
beginning_date: Option<String>,
|
|
ending_date: Option<String>,
|
|
increment: Value,
|
|
day_count: Option<Value>,
|
|
period_count: Option<Value>,
|
|
reverse: bool,
|
|
call_span: Span,
|
|
) -> Result<Value, ShellError> {
|
|
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)?;
|
|
|
|
if step_size == 0 {
|
|
return Err(ShellError::GenericError {
|
|
error: "increment cannot be 0".into(),
|
|
msg: "increment cannot be 0".into(),
|
|
span: Some(increment_span),
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
}
|
|
|
|
let in_format = match input_format {
|
|
Some(i) => match i.coerce_into_string() {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
return Err(ShellError::GenericError {
|
|
error: e.to_string(),
|
|
msg: "".into(),
|
|
span: None,
|
|
help: Some("error with input_format as_string".into()),
|
|
inner: vec![],
|
|
});
|
|
}
|
|
},
|
|
_ => "%Y-%m-%d".to_string(),
|
|
};
|
|
|
|
let out_format = match output_format {
|
|
Some(o) => match o.coerce_into_string() {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
return Err(ShellError::GenericError {
|
|
error: e.to_string(),
|
|
msg: "".into(),
|
|
span: None,
|
|
help: Some("error with output_format as_string".into()),
|
|
inner: vec![],
|
|
});
|
|
}
|
|
},
|
|
_ => "%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::GenericError {
|
|
error: e.to_string(),
|
|
msg: "Failed to parse date".into(),
|
|
span: Some(call_span),
|
|
help: None,
|
|
inner: vec![],
|
|
})
|
|
}
|
|
},
|
|
_ => 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::GenericError {
|
|
error: e.to_string(),
|
|
msg: "Failed to parse date".into(),
|
|
span: Some(call_span),
|
|
help: None,
|
|
inner: vec![],
|
|
})
|
|
}
|
|
},
|
|
_ => today,
|
|
};
|
|
|
|
let mut days_to_output = match day_count {
|
|
Some(d) => i64::from_value(d)?,
|
|
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;
|
|
}
|
|
|
|
// --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))
|
|
.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
|
|
// 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);
|
|
|
|
// 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) {
|
|
return Err(ShellError::GenericError {
|
|
error: "date is out of range".into(),
|
|
msg: "date is out of range".into(),
|
|
span: Some(call_span),
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
}
|
|
|
|
let mut ret = vec![];
|
|
loop {
|
|
let mut date_string = String::new();
|
|
match write!(date_string, "{}", next.format(&out_format)) {
|
|
Ok(_) => {}
|
|
Err(e) => {
|
|
return Err(ShellError::GenericError {
|
|
error: "Invalid output format".into(),
|
|
msg: e.to_string(),
|
|
span: Some(call_span),
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
}
|
|
}
|
|
ret.push(Value::string(date_string, call_span));
|
|
if let Some(n) = next.checked_add_signed(step_size) {
|
|
next = n;
|
|
} else {
|
|
return Err(ShellError::GenericError {
|
|
error: "date overflow".into(),
|
|
msg: "adding the increment overflowed".into(),
|
|
span: Some(call_span),
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
}
|
|
|
|
if is_out_of_range(next) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
Ok(Value::list(ret, call_span))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_examples() {
|
|
use crate::test_examples;
|
|
|
|
test_examples(SeqDate {})
|
|
}
|
|
}
|