ironcalc_base 0.7.1

Open source spreadsheet engine
Documentation
use statrs::distribution::{Binomial, Discrete, DiscreteCDF};

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

impl<'a> Model<'a> {
    pub(crate) fn fn_binom_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
        if args.len() != 4 {
            return CalcResult::new_args_number_error(cell);
        }

        // number_s
        let number_s = match self.get_number_no_bools(&args[0], cell) {
            Ok(f) => f.trunc(),
            Err(e) => return e,
        };

        // trials
        let trials = match self.get_number_no_bools(&args[1], cell) {
            Ok(f) => f.trunc(),
            Err(e) => return e,
        };

        // probability_s
        let p = match self.get_number_no_bools(&args[2], cell) {
            Ok(f) => f,
            Err(e) => return e,
        };

        // cumulative (logical)
        let cumulative = match self.get_boolean(&args[3], cell) {
            Ok(b) => b,
            Err(e) => return e,
        };

        // Domain checks
        if trials < 0.0
            || number_s < 0.0
            || number_s > trials
            || p.is_nan()
            || !(0.0..=1.0).contains(&p)
        {
            return CalcResult::new_error(
                Error::NUM,
                cell,
                "Invalid parameters for BINOM.DIST".to_string(),
            );
        }

        // Limit to u64
        if trials > u64::MAX as f64 {
            return CalcResult::new_error(
                Error::NUM,
                cell,
                "Number of trials too large".to_string(),
            );
        }

        let n = trials as u64;
        let k = number_s as u64;

        let dist = match Binomial::new(p, n) {
            Ok(d) => d,
            Err(_) => {
                return CalcResult::new_error(
                    Error::NUM,
                    cell,
                    "Invalid parameters for binomial distribution".to_string(),
                )
            }
        };

        let prob = if cumulative { dist.cdf(k) } else { dist.pmf(k) };

        if prob.is_nan() || prob.is_infinite() {
            return CalcResult::new_error(
                Error::NUM,
                cell,
                "Invalid result for BINOM.DIST".to_string(),
            );
        }

        CalcResult::Number(prob)
    }

    pub(crate) fn fn_binom_dist_range(
        &mut self,
        args: &[Node],
        cell: CellReferenceIndex,
    ) -> CalcResult {
        if args.len() < 3 || args.len() > 4 {
            return CalcResult::new_args_number_error(cell);
        }

        // trials
        let trials = match self.get_number_no_bools(&args[0], cell) {
            Ok(f) => f.trunc(),
            Err(e) => return e,
        };

        // probability_s
        let p = match self.get_number_no_bools(&args[1], cell) {
            Ok(f) => f,
            Err(e) => return e,
        };

        // number_s (lower)
        let number_s = match self.get_number_no_bools(&args[2], cell) {
            Ok(f) => f.trunc(),
            Err(e) => return e,
        };

        // number_s2 (upper, optional)
        let number_s2 = if args.len() == 4 {
            match self.get_number_no_bools(&args[3], cell) {
                Ok(f) => f.trunc(),
                Err(e) => return e,
            }
        } else {
            number_s
        };

        if trials < 0.0
            || number_s < 0.0
            || number_s2 < 0.0
            || number_s > number_s2
            || number_s2 > trials
            || p.is_nan()
            || !(0.0..=1.0).contains(&p)
        {
            return CalcResult::new_error(
                Error::NUM,
                cell,
                "Invalid parameters for BINOM.DIST.RANGE".to_string(),
            );
        }

        if trials > u64::MAX as f64 {
            return CalcResult::new_error(
                Error::NUM,
                cell,
                "Number of trials too large".to_string(),
            );
        }

        let n = trials as u64;
        let lower = number_s as u64;
        let upper = number_s2 as u64;

        let dist = match Binomial::new(p, n) {
            Ok(d) => d,
            Err(_) => {
                return CalcResult::new_error(
                    Error::NUM,
                    cell,
                    "Invalid parameters for binomial distribution".to_string(),
                )
            }
        };

        let prob = if lower == 0 {
            dist.cdf(upper)
        } else {
            let cdf_upper = dist.cdf(upper);
            let cdf_below_lower = dist.cdf(lower - 1);
            cdf_upper - cdf_below_lower
        };

        if prob.is_nan() || prob.is_infinite() {
            return CalcResult::new_error(
                Error::NUM,
                cell,
                "Invalid result for BINOM.DIST.RANGE".to_string(),
            );
        }

        CalcResult::Number(prob)
    }

    pub(crate) fn fn_binom_inv(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
        if args.len() != 3 {
            return CalcResult::new_args_number_error(cell);
        }

        // trials
        let trials = match self.get_number_no_bools(&args[0], cell) {
            Ok(f) => f.trunc(),
            Err(e) => return e,
        };

        // probability_s
        let p = match self.get_number_no_bools(&args[1], cell) {
            Ok(f) => f,
            Err(e) => return e,
        };

        // alpha
        let alpha = match self.get_number_no_bools(&args[2], cell) {
            Ok(f) => f,
            Err(e) => return e,
        };

        if trials < 0.0
            || trials > u64::MAX as f64
            || p.is_nan()
            || !(0.0..=1.0).contains(&p)
            || alpha.is_nan()
            || !(0.0..=1.0).contains(&alpha)
        {
            return CalcResult::new_error(
                Error::NUM,
                cell,
                "Invalid parameters for BINOM.INV".to_string(),
            );
        }

        let n = trials as u64;

        let dist = match Binomial::new(p, n) {
            Ok(d) => d,
            Err(_) => {
                return CalcResult::new_error(
                    Error::NUM,
                    cell,
                    "Invalid parameters for binomial distribution".to_string(),
                )
            }
        };

        // DiscreteCDF::inverse_cdf returns u64 for binomial
        let k = statrs::distribution::DiscreteCDF::inverse_cdf(&dist, alpha);

        CalcResult::Number(k as f64)
    }

    pub(crate) fn fn_negbinom_dist(
        &mut self,
        args: &[Node],
        cell: CellReferenceIndex,
    ) -> CalcResult {
        use statrs::distribution::{Discrete, DiscreteCDF, NegativeBinomial};

        if args.len() != 4 {
            return CalcResult::new_args_number_error(cell);
        }

        let number_f = match self.get_number_no_bools(&args[0], cell) {
            Ok(f) => f.trunc(),
            Err(e) => return e,
        };
        let number_s = match self.get_number_no_bools(&args[1], cell) {
            Ok(f) => f.trunc(),
            Err(e) => return e,
        };
        let probability_s = match self.get_number_no_bools(&args[2], cell) {
            Ok(f) => f,
            Err(e) => return e,
        };
        let cumulative = match self.get_boolean(&args[3], cell) {
            Ok(b) => b,
            Err(e) => return e,
        };

        if number_f < 0.0 || number_s < 1.0 || !(0.0..=1.0).contains(&probability_s) {
            return CalcResult::Error {
                error: Error::NUM,
                origin: cell,
                message: "Invalid parameter for NEGBINOM.DIST".to_string(),
            };
        }

        // Guard against absurdly large failures that won't fit in u64
        if number_f > (u64::MAX as f64) {
            return CalcResult::Error {
                error: Error::NUM,
                origin: cell,
                message: "Invalid parameter for NEGBINOM.DIST".to_string(),
            };
        }

        let dist = match NegativeBinomial::new(number_s, probability_s) {
            Ok(d) => d,
            Err(_) => {
                return CalcResult::Error {
                    error: Error::NUM,
                    origin: cell,
                    message: "Invalid parameter for NEGBINOM.DIST".to_string(),
                }
            }
        };

        let f_u = number_f as u64;
        let result = if cumulative {
            dist.cdf(f_u)
        } else {
            dist.pmf(f_u)
        };

        if !result.is_finite() {
            return CalcResult::Error {
                error: Error::NUM,
                origin: cell,
                message: "Invalid parameter for NEGBINOM.DIST".to_string(),
            };
        }

        CalcResult::Number(result)
    }
}