Shared Rust kernels

ported/cashflow_rate_family

All modules · quoted at commit 9e1884369a67 of DnaCalc/DnaCalc. Comments are the source's own.

numeric_from_cell · 12 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
fn numeric_from_cell(cell: &CalcValue) -> Result<f64, CashflowRateEvalError> {
    match cell.core_ref() {
        CoreRef::Number(n) => Ok(n),
        CoreRef::Error(code) => Err(CashflowRateEvalError::Domain(code)),
        CoreRef::Text(_)
        | CoreRef::Logical(_)
        | CoreRef::Empty
        | CoreRef::Missing
        | CoreRef::Array(_)
        | CoreRef::Reference(_) => Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Value)),
    }
}

Used by 3 functions.

scalar_number_from_eval · 19 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
pub(crate) fn scalar_number_from_eval(value: &CalcValue) -> Result<f64, CashflowRateEvalError> {
    match value.core_ref() {
        CoreRef::Number(n) => Ok(n),
        CoreRef::Logical(flag) => Ok(if flag { 1.0 } else { 0.0 }),
        CoreRef::Error(code) => Err(CashflowRateEvalError::Domain(code)),
        CoreRef::Array(array) => {
            let shape = array.shape();
            if shape.rows == 1 && shape.cols == 1 {
                numeric_from_cell(array.get(0, 0).expect("single cell"))
            } else {
                Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Value))
            }
        }
        CoreRef::Text(_) | CoreRef::Reference(_) => {
            Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Value))
        }
        _ => Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Value)),
    }
}

Used by 3 functions.

collect_numeric_vector_from_eval · 21 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
pub(crate) fn collect_numeric_vector_from_eval(value: &CalcValue) -> Result<Vec<f64>, CashflowRateEvalError> {
    match value.core_ref() {
        CoreRef::Number(n) => Ok(vec![n]),
        CoreRef::Error(code) => Err(CashflowRateEvalError::Domain(code)),
        CoreRef::Array(array) => {
            let shape = array.shape();
            if shape.rows > 1 && shape.cols > 1 {
                return Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Ref));
            }
            let mut out = Vec::with_capacity(shape.rows * shape.cols);
            for cell in array.iter_row_major() {
                out.push(numeric_from_cell(cell)?);
            }
            Ok(out)
        }
        CoreRef::Text(_) | CoreRef::Logical(_) | CoreRef::Reference(_) => {
            Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Value))
        }
        _ => Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Value)),
    }
}

Used by 2 functions.

serial_from_number · 10 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
fn serial_from_number(value: f64) -> Result<i64, CashflowRateEvalError> {
    if !value.is_finite() {
        return Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Num));
    }
    let serial = value.trunc() as i64;
    if serial < 0 {
        return Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Num));
    }
    Ok(serial)
}

Used by 2 functions.

collect_serial_vector_from_eval · 21 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
pub(crate) fn collect_serial_vector_from_eval(value: &CalcValue) -> Result<Vec<i64>, CashflowRateEvalError> {
    match value.core_ref() {
        CoreRef::Number(n) => Ok(vec![serial_from_number(n)?]),
        CoreRef::Error(code) => Err(CashflowRateEvalError::Domain(code)),
        CoreRef::Array(array) => {
            let shape = array.shape();
            if shape.rows > 1 && shape.cols > 1 {
                return Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Ref));
            }
            let mut out = Vec::with_capacity(shape.rows * shape.cols);
            for cell in array.iter_row_major() {
                out.push(serial_from_number(numeric_from_cell(cell)?)?);
            }
            Ok(out)
        }
        CoreRef::Text(_) | CoreRef::Logical(_) | CoreRef::Reference(_) => {
            Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Value))
        }
        _ => Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Value)),
    }
}

Used by 2 functions.

validate_cashflows · 14 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
fn validate_cashflows(cashflows: &[f64]) -> Result<(), WorksheetErrorCode> {
    if cashflows.len() < 2 {
        return Err(WorksheetErrorCode::Num);
    }
    if !cashflows.iter().all(|value| value.is_finite()) {
        return Err(WorksheetErrorCode::Value);
    }
    let has_positive = cashflows.iter().any(|value| *value > 0.0);
    let has_negative = cashflows.iter().any(|value| *value < 0.0);
    if !has_positive || !has_negative {
        return Err(WorksheetErrorCode::Num);
    }
    Ok(())
}

Used by 2 functions.

validate_xnpv_inputs · 13 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
fn validate_xnpv_inputs(values: &[f64], dates: &[i64]) -> Result<(), WorksheetErrorCode> {
    if values.len() != dates.len() || dates.is_empty() {
        return Err(WorksheetErrorCode::Num);
    }
    if !values.iter().all(|v| v.is_finite()) {
        return Err(WorksheetErrorCode::Value);
    }
    let anchor = dates[0];
    if dates.iter().any(|date| *date < anchor) {
        return Err(WorksheetErrorCode::Num);
    }
    Ok(())
}

Used by 2 functions.

bounded_rate_solve · 62 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
fn bounded_rate_solve<F, D>(
    guess: f64,
    mut f: F,
    mut derivative: D,
) -> Result<f64, WorksheetErrorCode>
where
    F: FnMut(f64) -> Result<f64, WorksheetErrorCode>,
    D: FnMut(f64) -> Result<f64, WorksheetErrorCode>,
{
    let mut prev_rate = 0.0;
    let mut prev_value = f(prev_rate)?;
    let mut current_rate = if guess <= MIN_VALID_RATE { 0.1 } else { guess };
    let mut current_value = f(current_rate)?;

    for _ in 0..ROOT_MAX_ITERATIONS {
        if current_value.abs() <= ROOT_TOLERANCE {
            return polish_rate(current_rate, &mut f, &mut derivative);
        }

        let secant_next = if (current_value - prev_value).abs() > ROOT_DERIVATIVE_EPS {
            let next = current_rate
                - current_value * (current_rate - prev_rate) / (current_value - prev_value);
            (next.is_finite() && next > MIN_VALID_RATE).then_some(next)
        } else {
            None
        };

        let newton_next = if let Ok(deriv) = derivative(current_rate) {
            if deriv.abs() > ROOT_DERIVATIVE_EPS {
                let next = current_rate - current_value / deriv;
                (next.is_finite() && next > MIN_VALID_RATE).then_some(next)
            } else {
                None
            }
        } else {
            None
        };

        let next_rate = secant_next.or(newton_next).unwrap_or_else(|| {
            if current_rate > 0.0 {
                current_rate / 2.0
            } else {
                0.1
            }
        });

        prev_rate = current_rate;
        prev_value = current_value;
        current_rate = if next_rate <= MIN_VALID_RATE {
            0.1
        } else {
            next_rate
        };
        current_value = f(current_rate)?;
    }

    if current_value.abs() <= ROOT_TOLERANCE {
        polish_rate(current_rate, &mut f, &mut derivative)
    } else {
        Err(WorksheetErrorCode::Num)
    }
}

Used by 2 functions.

polish_rate · 41 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
/// Once the bracketed solve is within `ROOT_TOLERANCE` of a root, the residual
/// `|NPV|` can still leave the *rate* far (10^4–10^5 ULP) from the true root when
/// the NPV slope is gentle — beyond the `IRR_PUBLICATION_ULP_SCAN_RADIUS` plateau
/// scan that selects Excel's published double. Polish the seed with analytic
/// Newton to a rate fixpoint so the plateau sits adjacent to the |NPV|-minimal
/// double (for a representable exact root this lands on it bit-for-bit).
fn polish_rate<F, D>(seed: f64, f: &mut F, derivative: &mut D) -> Result<f64, WorksheetErrorCode>
where
    F: FnMut(f64) -> Result<f64, WorksheetErrorCode>,
    D: FnMut(f64) -> Result<f64, WorksheetErrorCode>,
{
    let mut rate = seed;
    let mut last_step = f64::INFINITY;
    for _ in 0..ROOT_POLISH_ITERATIONS {
        let value = f(rate)?;
        let deriv = match derivative(rate) {
            // Currency units do not decide whether a nonzero slope can polish a rate.
            Ok(d) if d != 0.0 => d,
            _ => break,
        };
        let next = rate - value / deriv;
        if !next.is_finite() || next <= MIN_VALID_RATE {
            break;
        }
        if next.to_bits() == rate.to_bits() {
            break; // bit-level fixpoint
        }
        let step = (next - rate).abs();
        if step >= last_step {
            // No longer contracting (rounding noise near the root): keep whichever
            // of the adjacent candidates has the smaller residual.
            if f(next)?.abs() <= value.abs() {
                rate = next;
            }
            break;
        }
        last_step = step;
        rate = next;
    }
    Ok(rate)
}

Used by 2 functions.

map_cashflow_rate_error_to_ws · 6 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
pub fn map_cashflow_rate_error_to_ws(error: &CashflowRateEvalError) -> WorksheetErrorCode {
    match error {
        CashflowRateEvalError::ArityMismatch { .. } => WorksheetErrorCode::Value,
        CashflowRateEvalError::Domain(code) => *code,
    }
}

Used by 3 functions.