diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 4db87c4095..e4c8fcda1e 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -64,8 +64,12 @@ pub fn create_default_context() -> EngineState { Math, MathAbs, MathAvg, + MathCeil, + MathFloor, MathMax, + MathMedian, MathMin, + MathMode, MathProduct, MathRound, MathSqrt, diff --git a/crates/nu-command/src/math/ceil.rs b/crates/nu-command/src/math/ceil.rs new file mode 100644 index 0000000000..a23720ffbe --- /dev/null +++ b/crates/nu-command/src/math/ceil.rs @@ -0,0 +1,73 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math ceil" + } + + fn signature(&self) -> Signature { + Signature::build("math ceil") + } + + fn usage(&self) -> &str { + "Applies the ceil function to a list of numbers" + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + input.map( + move |value| operate(value, head), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Apply the ceil function to a list of numbers", + example: "[1.5 2.3 -3.1] | math ceil", + result: Some(Value::List { + vals: vec![Value::test_int(2), Value::test_int(3), Value::test_int(-3)], + span: Span::unknown(), + }), + }] + } +} + +fn operate(value: Value, head: Span) -> Value { + match value { + Value::Int { .. } => value, + Value::Float { val, span } => Value::Float { + val: val.ceil(), + span, + }, + other => Value::Error { + error: ShellError::UnsupportedInput( + String::from("Only numerical values are supported"), + other.span().unwrap_or(head), + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/floor.rs b/crates/nu-command/src/math/floor.rs new file mode 100644 index 0000000000..ea1e4cbb43 --- /dev/null +++ b/crates/nu-command/src/math/floor.rs @@ -0,0 +1,73 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math floor" + } + + fn signature(&self) -> Signature { + Signature::build("math floor") + } + + fn usage(&self) -> &str { + "Applies the floor function to a list of numbers" + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + input.map( + move |value| operate(value, head), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Apply the floor function to a list of numbers", + example: "[1.5 2.3 -3.1] | math floor", + result: Some(Value::List { + vals: vec![Value::test_int(1), Value::test_int(2), Value::test_int(-4)], + span: Span::unknown(), + }), + }] + } +} + +fn operate(value: Value, head: Span) -> Value { + match value { + Value::Int { .. } => value, + Value::Float { val, span } => Value::Float { + val: val.floor(), + span, + }, + other => Value::Error { + error: ShellError::UnsupportedInput( + String::from("Only numerical values are supported"), + other.span().unwrap_or(head), + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/median.rs b/crates/nu-command/src/math/median.rs new file mode 100644 index 0000000000..1204485fca --- /dev/null +++ b/crates/nu-command/src/math/median.rs @@ -0,0 +1,121 @@ +use crate::math::avg::average; +use crate::math::utils::run_with_function; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math median" + } + + fn signature(&self) -> Signature { + Signature::build("math median") + } + + fn usage(&self) -> &str { + "Gets the median of a list of numbers" + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + run_with_function(call, input, median) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get the median of a list of numbers", + example: "[3 8 9 12 12 15] | math median", + result: Some(Value::Float { + val: 10.5, + span: Span::unknown(), + }), + }] + } +} + +enum Pick { + MedianAverage, + Median, +} + +pub fn median(values: &[Value], head: &Span) -> Result { + let take = if values.len() % 2 == 0 { + Pick::MedianAverage + } else { + Pick::Median + }; + + let mut sorted = vec![]; + + for item in values { + sorted.push(item.clone()); + } + + if let Some(Err(values)) = values + .windows(2) + .map(|elem| { + if elem[0].partial_cmp(&elem[1]).is_none() { + return Err(ShellError::OperatorMismatch { + op_span: *head, + lhs_ty: elem[0].get_type(), + lhs_span: elem[0].span()?, + rhs_ty: elem[1].get_type(), + rhs_span: elem[1].span()?, + }); + } + Ok(elem[0].partial_cmp(&elem[1]).unwrap()) + }) + .find(|elem| elem.is_err()) + { + return Err(values); + } + + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + match take { + Pick::Median => { + let idx = (values.len() as f64 / 2.0).floor() as usize; + let out = sorted + .get(idx) + .ok_or_else(|| ShellError::UnsupportedInput("Empty input".to_string(), *head))?; + Ok(out.clone()) + } + Pick::MedianAverage => { + let idx_end = (values.len() / 2) as usize; + let idx_start = idx_end - 1; + + let left = sorted + .get(idx_start) + .ok_or_else(|| ShellError::UnsupportedInput("Empty input".to_string(), *head))? + .clone(); + + let right = sorted + .get(idx_end) + .ok_or_else(|| ShellError::UnsupportedInput("Empty input".to_string(), *head))? + .clone(); + + average(&[left, right], head) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/mod.rs b/crates/nu-command/src/math/mod.rs index 66eff72d82..bf392f68c1 100644 --- a/crates/nu-command/src/math/mod.rs +++ b/crates/nu-command/src/math/mod.rs @@ -1,8 +1,12 @@ mod abs; mod avg; +mod ceil; pub mod command; +mod floor; mod max; +mod median; mod min; +mod mode; mod product; mod reducers; mod round; @@ -12,9 +16,13 @@ mod utils; pub use abs::SubCommand as MathAbs; pub use avg::SubCommand as MathAvg; +pub use ceil::SubCommand as MathCeil; pub use command::MathCommand as Math; +pub use floor::SubCommand as MathFloor; pub use max::SubCommand as MathMax; +pub use median::SubCommand as MathMedian; pub use min::SubCommand as MathMin; +pub use mode::SubCommand as MathMode; pub use product::SubCommand as MathProduct; pub use round::SubCommand as MathRound; pub use sqrt::SubCommand as MathSqrt; diff --git a/crates/nu-command/src/math/mode.rs b/crates/nu-command/src/math/mode.rs new file mode 100644 index 0000000000..baa8f2e739 --- /dev/null +++ b/crates/nu-command/src/math/mode.rs @@ -0,0 +1,174 @@ +use crate::math::utils::run_with_function; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, Value}; +use std::cmp::Ordering; + +#[derive(Clone)] +pub struct SubCommand; + +#[derive(Hash, Eq, PartialEq, Debug)] +enum NumberTypes { + Float, + Int, + Duration, + Filesize, +} + +#[derive(Hash, Eq, PartialEq, Debug)] +struct HashableType { + bytes: [u8; 8], + original_type: NumberTypes, +} + +impl HashableType { + fn new(bytes: [u8; 8], original_type: NumberTypes) -> HashableType { + HashableType { + bytes, + original_type, + } + } +} + +impl Command for SubCommand { + fn name(&self) -> &str { + "math mode" + } + + fn signature(&self) -> Signature { + Signature::build("math mode") + } + + fn usage(&self) -> &str { + "Gets the most frequent element(s) from a list of numbers or tables" + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + run_with_function(call, input, mode) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get the mode(s) of a list of numbers", + example: "[3 3 9 12 12 15] | math mode", + result: Some(Value::List { + vals: vec![Value::test_int(3), Value::test_int(12)], + span: Span::unknown(), + }), + }] + } +} + +pub fn mode(values: &[Value], head: &Span) -> Result { + if let Some(Err(values)) = values + .windows(2) + .map(|elem| { + if elem[0].partial_cmp(&elem[1]).is_none() { + return Err(ShellError::OperatorMismatch { + op_span: *head, + lhs_ty: elem[0].get_type(), + lhs_span: elem[0].span()?, + rhs_ty: elem[1].get_type(), + rhs_span: elem[1].span()?, + }); + } + Ok(elem[0].partial_cmp(&elem[1]).unwrap()) + }) + .find(|elem| elem.is_err()) + { + return Err(values); + } + //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 + let hashable_values: Result, ShellError> = values + .iter() + .map(|val| match val { + Value::Int { val, .. } => Ok(HashableType::new(val.to_ne_bytes(), NumberTypes::Int)), + Value::Duration { val, .. } => { + Ok(HashableType::new(val.to_ne_bytes(), NumberTypes::Duration)) + } + Value::Float { val, .. } => { + Ok(HashableType::new(val.to_ne_bytes(), NumberTypes::Float)) + } + Value::Filesize { val, .. } => { + Ok(HashableType::new(val.to_ne_bytes(), NumberTypes::Filesize)) + } + other => Err(ShellError::UnsupportedInput( + "Unable to give a result with this input".to_string(), + other.span().unwrap(), + )), + }) + .collect::, ShellError>>(); + if let Err(not_hashable) = hashable_values { + return Err(not_hashable); + } + + let mut frequency_map = std::collections::HashMap::new(); + for v in hashable_values.unwrap() { + let counter = frequency_map.entry(v).or_insert(0); + *counter += 1; + } + + let mut max_freq = -1; + let mut modes = Vec::::new(); + for (value, frequency) in &frequency_map { + match max_freq.cmp(frequency) { + Ordering::Less => { + max_freq = *frequency; + modes.clear(); + modes.push(recreate_value(value, *head)); + } + Ordering::Equal => { + modes.push(recreate_value(value, *head)); + } + Ordering::Greater => (), + } + } + + modes.sort_by(|a, b| a.partial_cmp(b).unwrap()); + Ok(Value::List { + vals: modes, + span: *head, + }) +} + +fn recreate_value(hashable_value: &HashableType, head: Span) -> Value { + let bytes = hashable_value.bytes; + match &hashable_value.original_type { + NumberTypes::Int => Value::Int { + val: i64::from_ne_bytes(bytes), + span: head, + }, + NumberTypes::Float => Value::Float { + val: f64::from_ne_bytes(bytes), + span: head, + }, + NumberTypes::Duration => Value::Duration { + val: i64::from_ne_bytes(bytes), + span: head, + }, + NumberTypes::Filesize => Value::Filesize { + val: i64::from_ne_bytes(bytes), + span: head, + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +}