Files
IronCalc/base/src/functions/statistical/standard_dev.rs
Nicolás Hatcher 6822505602 UPDATE: Adds 56 functions in the Statistical section
Uses statrs for numerical functions

REFACTOR: Put statistical functions on its own module

This might seem counter-intuitive but the wasm build after this refactor
is 1528 bytes smaller :)
2025-11-25 01:20:03 +01:00

520 lines
20 KiB
Rust

use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::expressions::parser::ArrayNode;
use crate::expressions::types::CellReferenceIndex;
use crate::{
calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model,
};
impl Model {
pub(crate) fn fn_stdev_p(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut sum = 0.0;
let mut sumsq = 0.0;
let mut count: u64 = 0;
#[inline]
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
*sum += value;
*sumsq += value * value;
*count += 1;
}
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
let row1 = left.row;
let mut row2 = right.row;
let column1 = left.column;
let mut column2 = right.column;
if row1 == 1 && row2 == LAST_ROW {
row2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_row,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
if column1 == 1 && column2 == LAST_COLUMN {
column2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_column,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
for row in row1..row2 + 1 {
for column in column1..(column2 + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric
}
}
}
}
}
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
ArrayNode::Error(error) => {
return CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {
// ignore non-numeric
}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric
}
}
}
if count == 0 {
return CalcResult::new_error(
Error::DIV,
cell,
"STDEV.P with no numeric data".to_string(),
);
}
let n = count as f64;
let mut var = (sumsq - (sum * sum) / n) / n;
// clamp tiny negatives from FP noise
if var < 0.0 && var > -1e-12 {
var = 0.0;
}
CalcResult::Number(var.sqrt())
}
pub(crate) fn fn_stdev_s(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut sum = 0.0;
let mut sumsq = 0.0;
let mut count: u64 = 0;
#[inline]
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
*sum += value;
*sumsq += value * value;
*count += 1;
}
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
let row1 = left.row;
let mut row2 = right.row;
let column1 = left.column;
let mut column2 = right.column;
if row1 == 1 && row2 == LAST_ROW {
row2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_row,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
if column1 == 1 && column2 == LAST_COLUMN {
column2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_column,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
for row in row1..row2 + 1 {
for column in column1..(column2 + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric
}
}
}
}
}
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
ArrayNode::Error(error) => {
return CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {
// ignore non-numeric
}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric
}
}
}
if count <= 1 {
return CalcResult::new_error(
Error::DIV,
cell,
"STDEV.S requires at least two numeric values".to_string(),
);
}
let n = count as f64;
let mut var = (sumsq - (sum * sum) / n) / (n - 1.0);
if var < 0.0 && var > -1e-12 {
var = 0.0;
}
CalcResult::Number(var.sqrt())
}
pub(crate) fn fn_stdeva(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut sum = 0.0;
let mut sumsq = 0.0;
let mut count: u64 = 0;
#[inline]
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
*sum += value;
*sumsq += value * value;
*count += 1;
}
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
let row1 = left.row;
let mut row2 = right.row;
let column1 = left.column;
let mut column2 = right.column;
if row1 == 1 && row2 == LAST_ROW {
row2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_row,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
if column1 == 1 && column2 == LAST_COLUMN {
column2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_column,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
for row in row1..row2 + 1 {
for column in column1..(column2 + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
CalcResult::String(_) => {
accumulate(&mut sum, &mut sumsq, &mut count, 0.0);
}
CalcResult::Boolean(value) => {
let val = if value { 1.0 } else { 0.0 };
accumulate(&mut sum, &mut sumsq, &mut count, val);
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric for now
}
}
}
}
}
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
ArrayNode::Error(error) => {
return CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {
// ignore non-numeric for now
}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric for now
}
}
}
if count <= 1 {
return CalcResult::new_error(
Error::DIV,
cell,
"STDEVA requires at least two numeric values".to_string(),
);
}
let n = count as f64;
let mut var = (sumsq - (sum * sum) / n) / (n - 1.0);
if var < 0.0 && var > -1e-12 {
var = 0.0;
}
CalcResult::Number(var.sqrt())
}
pub(crate) fn fn_stdevpa(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.is_empty() {
return CalcResult::new_args_number_error(cell);
}
let mut sum = 0.0;
let mut sumsq = 0.0;
let mut count: u64 = 0;
#[inline]
fn accumulate(sum: &mut f64, sumsq: &mut f64, count: &mut u64, value: f64) {
*sum += value;
*sumsq += value * value;
*count += 1;
}
for arg in args {
match self.evaluate_node_in_context(arg, cell) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
CalcResult::Range { left, right } => {
if left.sheet != right.sheet {
return CalcResult::new_error(
Error::VALUE,
cell,
"Ranges are in different sheets".to_string(),
);
}
let row1 = left.row;
let mut row2 = right.row;
let column1 = left.column;
let mut column2 = right.column;
if row1 == 1 && row2 == LAST_ROW {
row2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_row,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
if column1 == 1 && column2 == LAST_COLUMN {
column2 = match self.workbook.worksheet(left.sheet) {
Ok(s) => s.dimension().max_column,
Err(_) => {
return CalcResult::new_error(
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", left.sheet),
);
}
};
}
for row in row1..row2 + 1 {
for column in column1..(column2 + 1) {
match self.evaluate_cell(CellReferenceIndex {
sheet: left.sheet,
row,
column,
}) {
CalcResult::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
CalcResult::String(_) => {
accumulate(&mut sum, &mut sumsq, &mut count, 0.0);
}
CalcResult::Boolean(value) => {
let val = if value { 1.0 } else { 0.0 };
accumulate(&mut sum, &mut sumsq, &mut count, val);
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric for now
}
}
}
}
}
CalcResult::Array(array) => {
for row in array {
for value in row {
match value {
ArrayNode::Number(value) => {
accumulate(&mut sum, &mut sumsq, &mut count, value);
}
ArrayNode::Error(error) => {
return CalcResult::Error {
error,
origin: cell,
message: "Error in array".to_string(),
}
}
_ => {
// ignore non-numeric for now
}
}
}
}
}
error @ CalcResult::Error { .. } => return error,
_ => {
// ignore non-numeric for now
}
}
}
if count == 0 {
return CalcResult::new_error(
Error::DIV,
cell,
"STDEVPA with no numeric data".to_string(),
);
}
let n = count as f64;
let mut var = (sumsq - (sum * sum) / n) / n;
if var < 0.0 && var > -1e-12 {
var = 0.0;
}
CalcResult::Number(var.sqrt())
}
}