Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions base/src/expressions/parser/static_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,10 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::MaxA => vec![Signature::Vector; arg_count],
Function::Median => vec![Signature::Vector; arg_count],
Function::MinA => vec![Signature::Vector; arg_count],
Function::Quartile => vec![Signature::Vector, Signature::Scalar],
Function::QuartileExc => vec![Signature::Vector, Signature::Scalar],
Function::QuartileInc => vec![Signature::Vector, Signature::Scalar],
Function::Rank => vec![Signature::Scalar, Signature::Vector, Signature::Scalar],
Function::RankAvg => vec![Signature::Scalar, Signature::Vector, Signature::Scalar],
Function::RankEq => vec![Signature::Scalar, Signature::Vector, Signature::Scalar],
Function::Skew => vec![Signature::Vector; arg_count],
Expand Down Expand Up @@ -1353,6 +1357,10 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::MaxA => StaticResult::Scalar,
Function::Median => StaticResult::Scalar,
Function::MinA => StaticResult::Scalar,
Function::Quartile => StaticResult::Scalar,
Function::QuartileExc => StaticResult::Scalar,
Function::QuartileInc => StaticResult::Scalar,
Function::Rank => StaticResult::Scalar,
Function::RankAvg => StaticResult::Scalar,
Function::RankEq => StaticResult::Scalar,
Function::Skew => StaticResult::Scalar,
Expand Down
28 changes: 25 additions & 3 deletions base/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,10 @@ pub enum Function {
Phi,
PoissonDist,
// Prob,
// QuartileExc,
// QuartileInc,
Quartile,
QuartileExc,
QuartileInc,
Rank,
RankAvg,
RankEq,
Skew,
Expand Down Expand Up @@ -428,6 +430,14 @@ macro_rules! impl_function_lookup {
impl Functions {
pub fn lookup(&self, name: &str) -> Option<Function> {
let key = name.to_uppercase();
// New functions without localization support
match key.as_str() {
"RANK" => return Some(Function::Rank),
"QUARTILE" => return Some(Function::Quartile),
"QUARTILE.INC" => return Some(Function::QuartileInc),
"QUARTILE.EXC" => return Some(Function::QuartileExc),
_ => {}
}
$(
if self.$field == key {
return Some(Function::$variant);
Expand Down Expand Up @@ -1019,6 +1029,10 @@ impl Function {
Function::Pearson => functions.pearson.clone(),
Function::Phi => functions.phi.clone(),
Function::PoissonDist => functions.poissondist.clone(),
Function::Quartile => "QUARTILE".to_string(),
Function::QuartileExc => "QUARTILE.EXC".to_string(),
Function::QuartileInc => "QUARTILE.INC".to_string(),
Function::Rank => "RANK".to_string(),
Function::RankAvg => functions.rankavg.clone(),
Function::RankEq => functions.rankeq.clone(),
Function::Skew => functions.skew.clone(),
Expand Down Expand Up @@ -1168,7 +1182,7 @@ impl Function {
Function::Steyx => functions.steyx.clone(),
}
}
pub fn into_iter() -> IntoIter<Function, 345> {
pub fn into_iter() -> IntoIter<Function, 349> {
[
Function::And,
Function::False,
Expand Down Expand Up @@ -1506,6 +1520,10 @@ impl Function {
Function::Large,
Function::Median,
Function::Small,
Function::Quartile,
Function::QuartileExc,
Function::QuartileInc,
Function::Rank,
Function::RankAvg,
Function::RankEq,
Function::Skew,
Expand Down Expand Up @@ -2001,6 +2019,10 @@ impl<'a> Model<'a> {
Function::MaxA => self.fn_maxa(args, cell),
Function::Median => self.fn_median(args, cell),
Function::MinA => self.fn_mina(args, cell),
Function::Quartile => self.fn_quartile(args, cell),
Function::QuartileExc => self.fn_quartile_exc(args, cell),
Function::QuartileInc => self.fn_quartile_inc(args, cell),
Function::Rank => self.fn_rank(args, cell),
Function::RankAvg => self.fn_rank_avg(args, cell),
Function::RankEq => self.fn_rank_eq(args, cell),
Function::Skew => self.fn_skew(args, cell),
Expand Down
1 change: 1 addition & 0 deletions base/src/functions/statistical/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod normal;
mod pearson;
mod phi;
mod poisson;
mod quartile;
mod rank_eq_avg;
mod standard_dev;
mod standardize;
Expand Down
134 changes: 134 additions & 0 deletions base/src/functions/statistical/quartile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use std::cmp::Ordering;

use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};

impl<'a> Model<'a> {
/// Helper to collect numeric values for QUARTILE functions
fn collect_quartile_values(
&mut self,
arg: &Node,
cell: CellReferenceIndex,
) -> Result<Vec<f64>, CalcResult> {
let values = match self.evaluate_node_in_context(arg, cell) {
CalcResult::Array(array) => match self.values_from_array(array) {
Ok(v) => v,
Err(e) => {
return Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: format!("Unsupported array argument: {}", e),
})
}
},
CalcResult::Range { left, right } => self.values_from_range(left, right)?,
CalcResult::Number(value) => vec![Some(value)],
_ => {
return Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Unsupported argument type".to_string(),
})
}
};

let numeric_values: Vec<f64> = values.into_iter().flatten().collect();
Ok(numeric_values)
}

/// QUARTILE(array, quart)
/// Compatibility function - same as QUARTILE.INC
pub(crate) fn fn_quartile(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
self.fn_quartile_inc(args, cell)
}

/// QUARTILE.INC(array, quart)
/// Returns the quartile of a data set (inclusive method)
pub(crate) fn fn_quartile_inc(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let values = match self.collect_quartile_values(&args[0], cell) {
Ok(v) => v,
Err(e) => return e,
};
let quart = match self.get_number(&args[1], cell) {
Ok(v) => v,
Err(e) => return e,
};
self.quartile_impl(values, quart, true, cell)
}

/// QUARTILE.EXC(array, quart)
/// Returns the quartile of a data set (exclusive method)
pub(crate) fn fn_quartile_exc(
&mut self,
args: &[Node],
cell: CellReferenceIndex,
) -> CalcResult {
if args.len() != 2 {
return CalcResult::new_args_number_error(cell);
}
let values = match self.collect_quartile_values(&args[0], cell) {
Ok(v) => v,
Err(e) => return e,
};
let quart = match self.get_number(&args[1], cell) {
Ok(v) => v,
Err(e) => return e,
};
self.quartile_impl(values, quart, false, cell)
}

/// Internal implementation for QUARTILE functions
fn quartile_impl(
&self,
mut values: Vec<f64>,
quart: f64,
inclusive: bool,
cell: CellReferenceIndex,
) -> CalcResult {
if quart.fract() != 0.0 {
return CalcResult::new_error(Error::NUM, cell, "Invalid quart".to_string());
}
let q_int = quart as i32;
if inclusive {
if !(0..=4).contains(&q_int) {
return CalcResult::new_error(Error::NUM, cell, "Invalid quart".to_string());
}
} else if !(1..=3).contains(&q_int) {
return CalcResult::new_error(Error::NUM, cell, "Invalid quart".to_string());
}

if values.is_empty() {
return CalcResult::new_error(Error::NUM, cell, "Empty array".to_string());
}
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let n = values.len() as f64;
let k = quart / 4.0;

if inclusive {
let index = k * (n - 1.0);
let i = index.floor() as usize;
let f = index - (i as f64);
if i + 1 >= values.len() {
return CalcResult::Number(values[i]);
}
CalcResult::Number(values[i] + f * (values[i + 1] - values[i]))
} else {
let r = k * (n + 1.0);
if r <= 1.0 || r >= n {
return CalcResult::new_error(Error::NUM, cell, "Invalid quart".to_string());
}
let i = r.floor() as usize;
let f = r - (i as f64);
CalcResult::Number(values[i - 1] + f * (values[i] - values[i - 1]))
}
}
}
6 changes: 6 additions & 0 deletions base/src/functions/statistical/rank_eq_avg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,10 @@ impl<'a> Model<'a> {

CalcResult::Number(rank)
}

/// RANK(number, ref, [order])
/// Compatibility function - same as RANK.EQ
pub(crate) fn fn_rank(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
self.fn_rank_eq(args, cell)
}
}
1 change: 1 addition & 0 deletions base/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ mod test_extend;
mod test_floor;
mod test_fn_datevalue_timevalue;
mod test_fn_fv;
mod test_fn_quartile_rank;
mod test_fn_round;
mod test_fn_type;
mod test_frozen_rows_and_columns;
Expand Down
Loading