Math commands can work with bounded ranges and produce list of numbers (#15319)

No associated issue, but follows up #15135. See also discussion on
[discord](https://discord.com/channels/601130461678272522/1349139634281513093/1349139639356624966)
with @sholderbach

# Description

### Math commands `range -> list<number>`

This enables the following math commands:
- abs
- ceil
- floor
- log
- round

to work with ranges. When a range is given, the command will apply the
command on each item of the range, thus producing a list of number as
output.

Example

![image](https://github.com/user-attachments/assets/cff12724-5b26-4dbb-a979-a91c1b5652fc)

The commands still do not work work with unbounded ranges:


![image](https://github.com/user-attachments/assets/40c766a8-763f-461d-971b-2d58d11fc3a6)

And I left out the "mode" command because I think it does not make sense
to use it on ranges...

### Math commands `range -> number`

This was the topic of my previous PR, but for whatever reason I didn't
do `math variance` and `math stddev`.
I had to use `input.try_expand_range` to convert the range into a list
before computing the variance/stddev.


![image](https://github.com/user-attachments/assets/803954e7-1c2a-4c86-8b16-e16518131138)

And same, does not work in infinite ranges:


![image](https://github.com/user-attachments/assets/8bfaae2b-34cc-453d-8764-e42c815d28d3)

### Also done:
- find link in documentation

# User-Facing Changes
- Command signatures changes
- ability to use some commands with unbounded ranges
- ability to use variance and stddev with bounded ranges

# Tests + Formatting
Cargo fmt and clippy OK
Tests OK

# After Submitting
I guess nothing, or maybe release notes?
This commit is contained in:
Loïc Riegel 2025-03-20 17:35:50 +01:00 committed by GitHub
parent dfba62da00
commit 2ea2a904e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 314 additions and 24 deletions

View File

@ -1,4 +1,6 @@
use crate::math::utils::ensure_bounded;
use nu_engine::command_prelude::*;
use nu_protocol::Range;
#[derive(Clone)]
pub struct MathAbs;
@ -21,6 +23,7 @@ impl Command for MathAbs {
Type::List(Box::new(Type::Duration)),
Type::List(Box::new(Type::Duration)),
),
(Type::Range, Type::List(Box::new(Type::Number))),
])
.allow_variants_without_examples(true)
.category(Category::Math)
@ -46,6 +49,19 @@ impl Command for MathAbs {
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
if let PipelineData::Value(
Value::Range {
ref val,
internal_span,
},
..,
) = input
{
match &**val {
Range::IntRange(range) => ensure_bounded(range.end(), internal_span, head)?,
Range::FloatRange(range) => ensure_bounded(range.end(), internal_span, head)?,
}
}
input.map(move |value| abs_helper(value, head), engine_state.signals())
}
@ -56,6 +72,19 @@ impl Command for MathAbs {
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
if let PipelineData::Value(
Value::Range {
ref val,
internal_span,
},
..,
) = input
{
match &**val {
Range::IntRange(range) => ensure_bounded(range.end(), internal_span, head)?,
Range::FloatRange(range) => ensure_bounded(range.end(), internal_span, head)?,
}
}
input.map(
move |value| abs_helper(value, head),
working_set.permanent().signals(),

View File

@ -1,4 +1,6 @@
use crate::math::utils::ensure_bounded;
use nu_engine::command_prelude::*;
use nu_protocol::Range;
#[derive(Clone)]
pub struct MathCeil;
@ -16,6 +18,7 @@ impl Command for MathCeil {
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Int)),
),
(Type::Range, Type::List(Box::new(Type::Number))),
])
.allow_variants_without_examples(true)
.category(Category::Math)
@ -45,6 +48,19 @@ impl Command for MathCeil {
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
if let PipelineData::Value(
Value::Range {
ref val,
internal_span,
},
..,
) = input
{
match &**val {
Range::IntRange(range) => ensure_bounded(range.end(), internal_span, head)?,
Range::FloatRange(range) => ensure_bounded(range.end(), internal_span, head)?,
}
}
input.map(move |value| operate(value, head), engine_state.signals())
}
@ -59,6 +75,19 @@ impl Command for MathCeil {
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
if let PipelineData::Value(
Value::Range {
ref val,
internal_span,
},
..,
) = input
{
match &**val {
Range::IntRange(range) => ensure_bounded(range.end(), internal_span, head)?,
Range::FloatRange(range) => ensure_bounded(range.end(), internal_span, head)?,
}
}
input.map(
move |value| operate(value, head),
working_set.permanent().signals(),

View File

@ -1,4 +1,6 @@
use crate::math::utils::ensure_bounded;
use nu_engine::command_prelude::*;
use nu_protocol::Range;
#[derive(Clone)]
pub struct MathFloor;
@ -16,6 +18,7 @@ impl Command for MathFloor {
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Int)),
),
(Type::Range, Type::List(Box::new(Type::Number))),
])
.allow_variants_without_examples(true)
.category(Category::Math)
@ -45,6 +48,19 @@ impl Command for MathFloor {
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
if let PipelineData::Value(
Value::Range {
ref val,
internal_span,
},
..,
) = input
{
match &**val {
Range::IntRange(range) => ensure_bounded(range.end(), internal_span, head)?,
Range::FloatRange(range) => ensure_bounded(range.end(), internal_span, head)?,
}
}
input.map(move |value| operate(value, head), engine_state.signals())
}
@ -59,6 +75,19 @@ impl Command for MathFloor {
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
if let PipelineData::Value(
Value::Range {
ref val,
internal_span,
},
..,
) = input
{
match &**val {
Range::IntRange(range) => ensure_bounded(range.end(), internal_span, head)?,
Range::FloatRange(range) => ensure_bounded(range.end(), internal_span, head)?,
}
}
input.map(
move |value| operate(value, head),
working_set.permanent().signals(),

View File

@ -1,4 +1,6 @@
use crate::math::utils::ensure_bounded;
use nu_engine::command_prelude::*;
use nu_protocol::Range;
use nu_protocol::Signals;
#[derive(Clone)]
@ -22,6 +24,7 @@ impl Command for MathLog {
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Float)),
),
(Type::Range, Type::List(Box::new(Type::Number))),
])
.allow_variants_without_examples(true)
.category(Category::Math)
@ -46,7 +49,21 @@ impl Command for MathLog {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let base: Spanned<f64> = call.req(engine_state, stack, 0)?;
if let PipelineData::Value(
Value::Range {
ref val,
internal_span,
},
..,
) = input
{
match &**val {
Range::IntRange(range) => ensure_bounded(range.end(), internal_span, head)?,
Range::FloatRange(range) => ensure_bounded(range.end(), internal_span, head)?,
}
}
log(base, call.head, input, engine_state.signals())
}
@ -56,7 +73,21 @@ impl Command for MathLog {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let base: Spanned<f64> = call.req_const(working_set, 0)?;
if let PipelineData::Value(
Value::Range {
ref val,
internal_span,
},
..,
) = input
{
match &**val {
Range::IntRange(range) => ensure_bounded(range.end(), internal_span, head)?,
Range::FloatRange(range) => ensure_bounded(range.end(), internal_span, head)?,
}
}
log(base, call.head, input, working_set.permanent().signals())
}

View File

@ -110,7 +110,7 @@ impl Command for MathMode {
}
}
pub fn mode(values: &[Value], _span: Span, head: Span) -> Result<Value, ShellError> {
pub fn mode(values: &[Value], span: Span, head: Span) -> Result<Value, ShellError> {
//In e-q, Value doesn't implement Hash or Eq, so we have to get the values inside
// But f64 doesn't implement Hash, so we get the binary representation to use as
// key in the HashMap
@ -130,11 +130,11 @@ pub fn mode(values: &[Value], _span: Span, head: Span) -> Result<Value, ShellErr
NumberTypes::Filesize,
)),
Value::Error { error, .. } => Err(*error.clone()),
other => Err(ShellError::UnsupportedInput {
_ => Err(ShellError::UnsupportedInput {
msg: "Unable to give a result with this input".to_string(),
input: "value originates from here".into(),
msg_span: head,
input_span: other.span(),
input_span: span,
}),
})
.collect::<Result<Vec<HashableType>, ShellError>>()?;

View File

@ -1,4 +1,6 @@
use crate::math::utils::ensure_bounded;
use nu_engine::command_prelude::*;
use nu_protocol::Range;
#[derive(Clone)]
pub struct MathRound;
@ -16,6 +18,7 @@ impl Command for MathRound {
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Number)),
),
(Type::Range, Type::List(Box::new(Type::Number))),
])
.allow_variants_without_examples(true)
.named(
@ -52,6 +55,19 @@ impl Command for MathRound {
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
if let PipelineData::Value(
Value::Range {
ref val,
internal_span,
},
..,
) = input
{
match &**val {
Range::IntRange(range) => ensure_bounded(range.end(), internal_span, head)?,
Range::FloatRange(range) => ensure_bounded(range.end(), internal_span, head)?,
}
}
input.map(
move |value| operate(value, head, precision_param),
engine_state.signals(),
@ -70,6 +86,19 @@ impl Command for MathRound {
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
if let PipelineData::Value(
Value::Range {
ref val,
internal_span,
},
..,
) = input
{
match &**val {
Range::IntRange(range) => ensure_bounded(range.end(), internal_span, head)?,
Range::FloatRange(range) => ensure_bounded(range.end(), internal_span, head)?,
}
}
input.map(
move |value| operate(value, head, precision_param),
working_set.permanent().signals(),

View File

@ -1,4 +1,6 @@
use crate::math::utils::ensure_bounded;
use nu_engine::command_prelude::*;
use nu_protocol::Range;
#[derive(Clone)]
pub struct MathSqrt;
@ -16,6 +18,7 @@ impl Command for MathSqrt {
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Float)),
),
(Type::Range, Type::List(Box::new(Type::Number))),
])
.allow_variants_without_examples(true)
.category(Category::Math)
@ -45,6 +48,19 @@ impl Command for MathSqrt {
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
if let PipelineData::Value(
Value::Range {
ref val,
internal_span,
},
..,
) = input
{
match &**val {
Range::IntRange(range) => ensure_bounded(range.end(), internal_span, head)?,
Range::FloatRange(range) => ensure_bounded(range.end(), internal_span, head)?,
}
}
input.map(move |value| operate(value, head), engine_state.signals())
}
@ -59,6 +75,19 @@ impl Command for MathSqrt {
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
if let PipelineData::Value(
Value::Range {
ref val,
internal_span,
},
..,
) = input
{
match &**val {
Range::IntRange(range) => ensure_bounded(range.end(), internal_span, head)?,
Range::FloatRange(range) => ensure_bounded(range.end(), internal_span, head)?,
}
}
input.map(
move |value| operate(value, head),
working_set.permanent().signals(),

View File

@ -14,6 +14,7 @@ impl Command for MathStddev {
Signature::build("math stddev")
.input_output_types(vec![
(Type::List(Box::new(Type::Number)), Type::Number),
(Type::Range, Type::Number),
(Type::table(), Type::record()),
(Type::record(), Type::record()),
])
@ -53,6 +54,18 @@ impl Command for MathStddev {
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let sample = call.has_flag(engine_state, stack, "sample")?;
let name = call.head;
let span = input.span().unwrap_or(name);
let input: PipelineData = match input.try_expand_range() {
Err(_) => {
return Err(ShellError::IncorrectValue {
msg: "Range must be bounded".to_string(),
val_span: span,
call_span: name,
});
}
Ok(val) => val,
};
run_with_function(call, input, compute_stddev(sample))
}
@ -63,6 +76,18 @@ impl Command for MathStddev {
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let sample = call.has_flag_const(working_set, "sample")?;
let name = call.head;
let span = input.span().unwrap_or(name);
let input: PipelineData = match input.try_expand_range() {
Err(_) => {
return Err(ShellError::IncorrectValue {
msg: "Range must be bounded".to_string(),
val_span: span,
call_span: name,
});
}
Ok(val) => val,
};
run_with_function(call, input, compute_stddev(sample))
}

View File

@ -13,6 +13,7 @@ impl Command for MathVariance {
Signature::build("math variance")
.input_output_types(vec![
(Type::List(Box::new(Type::Number)), Type::Number),
(Type::Range, Type::Number),
(Type::table(), Type::record()),
(Type::record(), Type::record()),
])
@ -45,6 +46,18 @@ impl Command for MathVariance {
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let sample = call.has_flag(engine_state, stack, "sample")?;
let name = call.head;
let span = input.span().unwrap_or(name);
let input: PipelineData = match input.try_expand_range() {
Err(_) => {
return Err(ShellError::IncorrectValue {
msg: "Range must be bounded".to_string(),
val_span: span,
call_span: name,
});
}
Ok(val) => val,
};
run_with_function(call, input, compute_variance(sample))
}
@ -55,6 +68,18 @@ impl Command for MathVariance {
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let sample = call.has_flag_const(working_set, "sample")?;
let name = call.head;
let span = input.span().unwrap_or(name);
let input: PipelineData = match input.try_expand_range() {
Err(_) => {
return Err(ShellError::IncorrectValue {
msg: "Range must be bounded".to_string(),
val_span: span,
call_span: name,
});
}
Ok(val) => val,
};
run_with_function(call, input, compute_variance(sample))
}

View File

@ -7,8 +7,16 @@ fn const_abs() {
}
#[test]
fn cannot_abs_range() {
let actual = nu!("0..5 | math abs");
fn can_abs_range_into_list() {
let actual = nu!("-1.5..-10.5 | math abs");
let expected = nu!("1.5..10.5");
assert!(actual.err.contains("nu::parser::input_type_mismatch"));
assert_eq!(actual.out, expected.out);
}
#[test]
fn cannot_abs_infinite_range() {
let actual = nu!("0.. | math abs");
assert!(actual.err.contains("nu::shell::incorrect_value"));
}

View File

@ -7,8 +7,16 @@ fn const_ceil() {
}
#[test]
fn cannot_ceil_range() {
let actual = nu!("0..5 | math ceil");
fn can_ceil_range_into_list() {
let actual = nu!("(1.8)..(3.8) | math ceil");
let expected = nu!("[2 3 4]");
assert!(actual.err.contains("nu::parser::input_type_mismatch"));
assert_eq!(actual.out, expected.out);
}
#[test]
fn cannot_ceil_infinite_range() {
let actual = nu!("0.. | math ceil");
assert!(actual.err.contains("nu::shell::incorrect_value"));
}

View File

@ -7,8 +7,16 @@ fn const_floor() {
}
#[test]
fn cannot_floor_range() {
fn can_floor_range_into_list() {
let actual = nu!("(1.8)..(3.8) | math floor");
let expected = nu!("[1 2 3]");
assert_eq!(actual.out, expected.out);
}
#[test]
fn cannot_floor_infinite_range() {
let actual = nu!("0.. | math floor");
assert!(actual.err.contains("nu::parser::input_type_mismatch"));
assert!(actual.err.contains("nu::shell::incorrect_value"));
}

View File

@ -7,8 +7,16 @@ fn const_log() {
}
#[test]
fn cannot_log_range() {
let actual = nu!("0.. | math log 2");
fn can_log_range_into_list() {
let actual = nu!("1..5 | math log 2");
let expected = nu!("[1 2 3 4 5] | math log 2");
assert!(actual.err.contains("nu::parser::input_type_mismatch"));
assert_eq!(actual.out, expected.out);
}
#[test]
fn cannot_log_infinite_range() {
let actual = nu!("1.. | math log 2");
assert!(actual.err.contains("nu::shell::incorrect_value"));
}

View File

@ -42,8 +42,16 @@ fn const_round() {
}
#[test]
fn cannot_round_infinite_range() {
let actual = nu!("0..5 | math round");
fn can_round_range_into_list() {
let actual = nu!("(1.0)..(1.2)..(2.0) | math round");
let expected = nu!("[1 1 1 2 2 2]");
assert!(actual.err.contains("nu::parser::input_type_mismatch"));
assert_eq!(actual.out, expected.out);
}
#[test]
fn cannot_round_infinite_range() {
let actual = nu!("0.. | math round");
assert!(actual.err.contains("nu::shell::incorrect_value"));
}

View File

@ -28,8 +28,16 @@ fn const_sqrt() {
}
#[test]
fn cannot_sqrt_range() {
fn can_sqrt_range() {
let actual = nu!("0..5 | math sqrt");
let expected = nu!("[0 1 2 3 4 5] | math sqrt");
assert!(actual.err.contains("nu::parser::input_type_mismatch"));
assert_eq!(actual.out, expected.out);
}
#[test]
fn cannot_sqrt_infinite_range() {
let actual = nu!("0.. | math sqrt");
assert!(actual.err.contains("nu::shell::incorrect_value"));
}

View File

@ -7,8 +7,16 @@ fn const_avg() {
}
#[test]
fn cannot_stddev_range() {
fn can_stddev_range() {
let actual = nu!("0..5 | math stddev");
let expected = nu!("[0 1 2 3 4 5] | math stddev");
assert!(actual.err.contains("nu::parser::input_type_mismatch"));
assert_eq!(actual.out, expected.out);
}
#[test]
fn cannot_stddev_infinite_range() {
let actual = nu!("0.. | math stddev");
assert!(actual.err.contains("nu::shell::incorrect_value"));
}

View File

@ -7,8 +7,16 @@ fn const_variance() {
}
#[test]
fn cannot_variance_range() {
fn can_variance_range() {
let actual = nu!("0..5 | math variance");
let expected = nu!("[0 1 2 3 4 5] | math variance");
assert!(actual.err.contains("nu::parser::input_type_mismatch"));
assert_eq!(actual.out, expected.out);
}
#[test]
fn cannot_variance_infinite_range() {
let actual = nu!("0.. | math variance");
assert!(actual.err.contains("nu::shell::incorrect_value"));
}

View File

@ -9,4 +9,4 @@ A complementary (currently stale) resource has been the [Nushell contributor boo
- [Developer FAQ](FAQ.md)
- [How to/SOPs](HOWTOS.md)
- [Platform support policy](PLATFORM_SUPPORT.md)
- [Our Rust style](devdocs/rust_style.md)
- [Our Rust style](rust_style.md)