Financial functions

IRR

Known larger differences on 424 of 765 recorded answers

Of 765 recorded answers (734 in the answer bank and 31 workbook cells), 424 do not match: 308 differ by more than 16 ULP, 96 differ by at most 16 ULP, 20 differ in kind or value. How we measure this · See the Rust code

Local names in 40 languages
LanguageName in Excel
ArabicIRR
BulgarianIRR
CzechIRR
DanishIA
GermanIKV
GreekIRR
English (Australia)IRR
English (United Kingdom)IRR
English (United States)IRR
SpanishTIR
EstonianIRR
FinnishSISÄINEN.KORKO
FrenchIRR
HebrewIRR
CroatianIRR (internal rate of return, interna stopa povrata)
HungarianBMR
IndonesianIRR
ItalianTIR.COST
JapaneseIRR
KoreanIRR
LithuanianIRR
LatvianIRR
NorwegianIR
DutchIR
PolishIRR
Portuguese (Brazil)IRR
Portuguese (Portugal)TIR
RomanianIRR
RussianВСД
SlovakIRR
SlovenianIRR
Serbian (Latin)IRR
SwedishIR
ThaiIRR
TurkishİÇ_VERİM_ORANI
UkrainianIRR
VietnameseIRR
Chinese (Simplified)IRR
Chinese (Hong Kong)IRR
Chinese (Traditional)IRR

Names from Microsoft's support site.

What it does

IRR(values, [guess])

IRR(values,[guess]) returns a periodic rate that makes an ordered cash-flow series’ present value zero. The first admitted flow occurs at time zero; later flows occur at equally spaced periods. Amounts can vary. Guess defaults to 0.1.

The mathematical equation is sum(c_i/(1+r)^i)=0, with zero-based i. A series with several sign changes can have multiple acceptable roots.

Try it

Input cells (optional)

Example: {"B2": 3, "B3": 4}. A1 holds the formula. Other cells need values before a formula can read them.

Enter a formula, then calculate.

Runs locally in your browser with OneCalc. Its answer may differ from Excel. Use English function names and commas between arguments.

=IRR({-1000,1250}) returns about 0.25: one period turns a thousand outflow into 1250 inflow.

=IRR({-1000,0,1250}) is about 0.1180339887. The explicit zero preserves a second period, giving (1+r)^2=1.25. An ignored blank would remove that position.

=IRR({-100,230,-132},0.05) is about 0.1; a guess of 0.3 leads to about 0.2 in the current route. Both mathematical roots solve 100*(1+r)^2-230*(1+r)+132=0.

Quirks

Microsoft requires at least one positive and one negative value. It ignores text, logicals and empty cells in an array/reference, so a missing amount and numeric zero have different timing effects. Supply the flows in their intended sequence.

The current collector accepts a row or column vector and gives #REF! for a rectangle with both dimensions above one. A contained error becomes #VALUE!, whereas a whole-argument scalar error preserves its code. These are current source rules, not a claim about every Excel input route.

Microsoft documents twenty tries, accuracy within 0.00001 percent and #NUM! on nonconvergence. The current candidate has distinct two-flow and general routes; its later refinement and publication rules should not be mistaken for that entire private iteration schedule.

IRR with a direct non-number gives #NUM!, and the guess is still checked

Text or a logical typed directly as the values argument of IRR is not an error of type: "", "2", "x", "0", "-2", TRUE and FALSE all give #NUM!, never #VALUE!. The guess is read anyway, and its error wins: with any of those values, a guess of "x" gives #VALUE! and a guess of NA() gives #N/A, while 0.1 or an empty guess leaves #NUM!. A direct error in values wins over the guess: IRR(NA(),"x") is #N/A.

FormulaExcelMathematical value
=IRR("2")
Excel 16.0.20527.20088 Current Channel, Windows 64-bit (NT 10.0.26200), x86-64, AMD Ryzen 7 4800H, en-ZA
Inputs and reference value

Typed formula.

#NUM!—
=IRR(TRUE)
Excel 16.0.20527.20088 Current Channel, Windows 64-bit (NT 10.0.26200), x86-64, AMD Ryzen 7 4800H, en-ZA
Inputs and reference value

Typed formula.

#NUM!—
=IRR("2","x")
Excel 16.0.20527.20088 Current Channel, Windows 64-bit (NT 10.0.26200), x86-64, AMD Ryzen 7 4800H, en-ZA
Inputs and reference value

Typed formula.

#VALUE!—
=IRR("2",NA())
Excel 16.0.20527.20088 Current Channel, Windows 64-bit (NT 10.0.26200), x86-64, AMD Ryzen 7 4800H, en-ZA
Inputs and reference value

Typed formula.

#N/A—
=IRR("2",)
Excel 16.0.20527.20088 Current Channel, Windows 64-bit (NT 10.0.26200), x86-64, AMD Ryzen 7 4800H, en-ZA
Inputs and reference value

Typed formula.

#NUM!—
=IRR(NA())
Excel 16.0.20527.20088 Current Channel, Windows 64-bit (NT 10.0.26200), x86-64, AMD Ryzen 7 4800H, en-ZA
Inputs and reference value

Typed formula.

#N/A—
=IRR(NA(),"x")
Excel 16.0.20527.20088 Current Channel, Windows 64-bit (NT 10.0.26200), x86-64, AMD Ryzen 7 4800H, en-ZA
Inputs and reference value

Typed formula.

#N/A—

Inputs, build details and evidence

How to implement

Collect admitted numbers in order and validate their signs. The current internal residual forms base=1+r with ordinary binary64 arithmetic, evaluates the runtime crt_pow(base,i) for each index, divides the corresponding flow and adds forward. Its domain excludes rates at/below -0.999999999. This residual is not the public NPV wrapper’s repeated-factor loop.

Two admitted flows

The current route iterates in q=1+r, rather than returning the closed-form ratio immediately:

  1. Set current=1+guess, previous=1, and previous_value=F(0).
  2. Evaluate value=F(current-1). If it differs from previous_value, use secant step value*(current-previous)/(value-previous_value). If equal, use the analytic derivative and step value/Fprime(current-1); a zero derivative fails.
  3. Set next=current-step. If its stored bits equal current, return next-1. Otherwise move the previous state forward and repeat, for at most twenty iterations. Failure gives #NUM!.

For -1000,1250, the mathematical q is 1.25 and returned rate one quarter. The q-space iteration and final subtraction still matter to less convenient low-bit results.

More than two flows

The current bounded candidate starts from zero and guess, tries secant steps, then analytic Newton steps when a secant is unavailable, and otherwise halves a positive rate or restarts at 0.1. Its search permits 100 iterations with residual magnitude at most 1e-8. A successful seed gets up to 64 analytic Newton refinements, stopping at a stored-rate fixpoint or noncontracting step.

Publication then scans sixteen adjacent binary64 rates on each side for a smaller absolute residual. It locates both ends of the contiguous equal-absolute-residual plateau by doubling the distance in ordered-rate encodings and bisecting after leaving the plateau. If that plateau spans zero it returns zero; otherwise it returns the midpoint encoding, decoded as a rate. This is distinct from averaging the endpoint numbers.

For negative rates, the current ordered key complements all bits of a negative binary64 value; for a nonnegative value it sets the high bit. This gives a monotone key and keeps the two zero representations adjacent. Decode with the inverse transformation. Plateau search has bounded distance probes and bisections and can fail rather than scan indefinitely.

The residual producer, root-selection basins, return plateaus and remaining recorded numerical differences remain open. Matching a small residual or a mathematical closed form alone does not identify Excel’s private solver or certify this function.

Rust code

Show the CalcFunc source of this function

Quoted from the public repository DnaCalc/DnaCalc, crates calc-func and calc-fml, at commit 9e1884369a67. Comments are the source's own. A kernel shared with other functions appears once, on its module page.

IRR · 24 lines · calc-func/src/catalogue/financial.rs on GitHub
18 irr_fn "IRR" (1, 2) [R "values", V "guess"] |s, a, sv| {
    let mut values = resolved(s, a, 0, sv)?;
    // Fn7 scalar readings: held cells keep the collector's held-value rule,
    // even when the range is one cell. Direct text and logicals are ignored
    // too. Admit the guess before these scalar collections; a direct error
    // and larger arrays retain their existing validation order.
    let scalar = values.callable_value().is_none()
        && (matches!(a[0].core_ref(), CoreRef::Reference(_))
            && !matches!(values.core_ref(), CoreRef::Array(_))
            || matches!(values.core_ref(), CoreRef::Text(_) | CoreRef::Logical(_) | CoreRef::Empty));
    let mut g = None;
    if scalar {
        g = guess(s, a, 1, sv)?;
        values = CalcValue::array(CalcArray::from_scalar(values));
    }
    let cashflows = match cash::collect_irr_values_from_eval(&values) {
        Ok(cashflows) => cashflows,
        Err(error) => return cash_err(error),
    };
    if !scalar {
        g = guess(s, a, 1, sv)?;
    }
    out(s, cash::irr_kernel(&cashflows, g))
}
collect_irr_values_from_eval · 40 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
/// Excel `IRR` collects the values array/range with these rules (verified vs live
/// Excel 16.0 b20026, identical for array constants and ranges):
/// numbers are included; text, logical, and blank/empty cells are **ignored**;
/// and **any** error cell maps to `#VALUE!` (the specific code is not propagated).
/// A whole-argument scalar error still propagates as usual. This differs from the
/// strict positional collector used by XNPV/XIRR, whose values must stay aligned
/// with the parallel dates array.
pub(crate) fn collect_irr_values_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() {
                match cell.core_ref() {
                    CoreRef::Number(n) => out.push(n),
                    CoreRef::Error(_) => {
                        return Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Value));
                    }
                    CoreRef::Text(_)
                    | CoreRef::Logical(_)
                    | CoreRef::Empty
                    | CoreRef::Missing => {} // ignored, like Excel
                    CoreRef::Array(_) | CoreRef::Reference(_) => {
                        return Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Value));
                    }
                }
            }
            Ok(out)
        }
        CoreRef::Text(_) | CoreRef::Logical(_) | CoreRef::Reference(_) => {
            Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Value))
        }
        _ => Err(CashflowRateEvalError::Domain(WorksheetErrorCode::Value)),
    }
}
periodic_npv_with_t0 · 16 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
fn periodic_npv_with_t0(rate: f64, cashflows: &[f64]) -> Result<f64, WorksheetErrorCode> {
    validate_cashflows(cashflows)?;
    if !rate.is_finite() || rate <= MIN_VALID_RATE {
        return Err(WorksheetErrorCode::Num);
    }
    let base = 1.0 + rate;
    let mut total = 0.0;
    for (idx, cashflow) in cashflows.iter().enumerate() {
        total += *cashflow / crt_pow(base, idx as f64);
    }
    if total.is_finite() {
        Ok(total)
    } else {
        Err(WorksheetErrorCode::Num)
    }
}
periodic_npv_derivative · 17 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
fn periodic_npv_derivative(rate: f64, cashflows: &[f64]) -> Result<f64, WorksheetErrorCode> {
    validate_cashflows(cashflows)?;
    if !rate.is_finite() || rate <= MIN_VALID_RATE {
        return Err(WorksheetErrorCode::Num);
    }
    let base = 1.0 + rate;
    let mut total = 0.0;
    for (idx, cashflow) in cashflows.iter().enumerate().skip(1) {
        let period = idx as f64;
        total += -period * *cashflow / crt_pow(base, period + 1.0);
    }
    if total.is_finite() {
        Ok(total)
    } else {
        Err(WorksheetErrorCode::Num)
    }
}
ordered_rate · 4 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
fn ordered_rate(rate: f64) -> u64 {
    let bits = rate.to_bits();
    if rate.is_sign_negative() { !bits } else { bits | (1 << 63) }
}
rate_from_ordered · 3 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
fn rate_from_ordered(key: u64) -> f64 {
    f64::from_bits(if key & (1 << 63) == 0 { !key } else { key & !(1 << 63) })
}
residual_plateau_end · 51 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
/// Find the end of the residual plateau by doubling the distance, then bisecting its last
/// interval. Near zero, `1 + rate` can be constant over millions of rates: walking them one
/// by one is unbounded in practice. There are only 64 bits in the ordered representation,
/// so each end needs at most 64 distance probes and 64 bisections. A plateau with no finite
/// end returns #NUM!.
fn residual_plateau_end<F>(
    root: u64,
    residual: u64,
    lower: bool,
    f: &mut F,
) -> Result<u64, WorksheetErrorCode>
where
    F: FnMut(f64) -> Result<f64, WorksheetErrorCode>,
{
    let mut inside = root;
    let mut on_plateau = |rate| match f(rate) {
        Ok(value) => Ok(value.abs().to_bits() == residual),
        // A probe outside the NPV domain bounds the plateau just as a different residual
        // does. Bisect back into the valid interval instead of rejecting the root.
        Err(WorksheetErrorCode::Num) => Ok(false),
        Err(code) => Err(code),
    };
    for bit in 0..64 {
        let distance = 1u64 << bit;
        let outside = if lower { root.saturating_sub(distance) } else { root.saturating_add(distance) };
        let candidate = rate_from_ordered(outside);
        if candidate.is_finite() && on_plateau(candidate)? {
            inside = outside;
            continue;
        }
        let mut outside = outside;
        for _ in 0..64 {
            if inside.abs_diff(outside) <= 1 {
                // No finite end exists when even the last finite rate is on the plateau.
                if !rate_from_ordered(outside).is_finite() {
                    return Err(WorksheetErrorCode::Num);
                }
                return Ok(inside);
            }
            let midpoint = inside.min(outside) + inside.abs_diff(outside) / 2;
            let candidate = rate_from_ordered(midpoint);
            if candidate.is_finite() && on_plateau(candidate)? {
                inside = midpoint;
            } else {
                outside = midpoint;
            }
        }
        return Err(WorksheetErrorCode::Num);
    }
    Err(WorksheetErrorCode::Num)
}
midpoint_of_local_min_abs_residual_plateau · 42 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
fn midpoint_of_local_min_abs_residual_plateau<F>(
    root: f64,
    mut f: F,
) -> Result<f64, WorksheetErrorCode>
where
    F: FnMut(f64) -> Result<f64, WorksheetErrorCode>,
{
    let mut best_rate = root;
    let mut best_abs_bits = f(root)?.abs().to_bits();

    let mut lower_probe = root;
    let mut upper_probe = root;
    for _ in 0..IRR_PUBLICATION_ULP_SCAN_RADIUS {
        lower_probe = lower_probe.next_down();
        if lower_probe.is_finite() {
            let abs_bits = f(lower_probe)?.abs().to_bits();
            if abs_bits < best_abs_bits {
                best_abs_bits = abs_bits;
                best_rate = lower_probe;
            }
        }

        upper_probe = upper_probe.next_up();
        if upper_probe.is_finite() {
            let abs_bits = f(upper_probe)?.abs().to_bits();
            if abs_bits < best_abs_bits {
                best_abs_bits = abs_bits;
                best_rate = upper_probe;
            }
        }
    }

    let root = ordered_rate(best_rate);
    let lower = residual_plateau_end(root, best_abs_bits, true, &mut f)?;
    let upper = residual_plateau_end(root, best_abs_bits, false, &mut f)?;
    // Both zero representations are in the same plateau. Publish the exact zero root rather
    // than letting the different spacings on either side choose a subnormal rate.
    if rate_from_ordered(lower) <= 0.0 && rate_from_ordered(upper) >= 0.0 {
        return Ok(0.0);
    }
    Ok(rate_from_ordered(lower + (upper - lower) / 2))
}
irr_two_cashflows · 30 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
/// Two-period cashflows have a single pole in q = 1 + rate. The recorded
/// rows favor a q-space secant fixpoint, including the final subtraction.
/// Equal residuals use the analytic Newton step, including an initial guess of
/// zero. A bounded solve keeps nonconvergence as the worksheet numeric error.
fn irr_two_cashflows(cashflows: &[f64], guess: f64) -> Result<f64, WorksheetErrorCode> {
    let mut current = 1.0 + guess;
    let mut previous = 1.0;
    let mut previous_value = periodic_npv_with_t0(0.0, cashflows)?;
    for _ in 0..20 {
        let rate = current - 1.0;
        let value = periodic_npv_with_t0(rate, cashflows)?;
        let step = if value == previous_value {
            let derivative = periodic_npv_derivative(rate, cashflows)?;
            if derivative == 0.0 {
                return Err(WorksheetErrorCode::Num);
            }
            value / derivative
        } else {
            value * (current - previous) / (value - previous_value)
        };
        let next = current - step;
        if next.to_bits() == current.to_bits() {
            return Ok(next - 1.0);
        }
        previous = current;
        previous_value = value;
        current = next;
    }
    Err(WorksheetErrorCode::Num)
}
irr_kernel · 12 lines · calc-func/src/ported/cashflow_rate_family.rs on GitHub
pub(crate) fn irr_kernel(cashflows: &[f64], guess: Option<f64>) -> Result<f64, WorksheetErrorCode> {
    validate_cashflows(cashflows)?;
    if cashflows.len() == 2 {
        return irr_two_cashflows(cashflows, guess.unwrap_or(0.1));
    }
    let root = bounded_rate_solve(
        guess.unwrap_or(0.1),
        |rate| periodic_npv_with_t0(rate, cashflows),
        |rate| periodic_npv_derivative(rate, cashflows),
    )?;
    midpoint_of_local_min_abs_residual_plateau(root, |rate| periodic_npv_with_t0(rate, cashflows))
}

Kernels shared with other functions

Argument and result plumbing

No implementation has been published with a Handbook test suite for this function.

Version differences

The recorded environments below distinguish a reading from a same-input comparison. Counts describe captured inputs in each environment; overlapping captures and comparisons are not totals of unique worksheet calls.

Excel 16.0 buildPlatform and CPUCaptured inputs
20131Windows, bitness not recorded, AMD Ryzen 7 4800H with Radeon Graphics217

The record contains no same-input comparison between these environments. Reading different inputs on different builds does not establish either a version change or cross-build agreement.

Recorded environments and comparison details.

Excel 16.0.20527.20088

Windows 64-bit (NT 10.0.26200), x86-64, AMD Ryzen 7 4800H, en-ZA

=IRR("2") returned #NUM!. =IRR(TRUE) returned #NUM!. 5 further recorded controls are attached. Inputs were supplied through formula literals. These are controls from this environment, not a same-input comparison with another build.

Observed formulas and results
  • =IRR("2") → #NUM! (formula-literal)
  • =IRR(TRUE) → #NUM! (formula-literal)
  • =IRR("2","x") → #VALUE! (formula-literal)
  • =IRR("2",NA()) → #N/A (formula-literal)
  • =IRR("2",) → #NUM! (formula-literal)
  • =IRR(NA()) → #N/A (formula-literal)
  • =IRR(NA(),"x") → #N/A (formula-literal)

Excel 16.0 build 20131

Windows, bitness not recorded, AMD Ryzen 7 4800H with Radeon Graphics

217 captured inputs are indexed for this environment. Capture dates: 2026-07-11 to 2026-07-11. This is a reading count, not a comparison with another build.

Alternatives

XIRR uses actual dates. RATE assumes one repeated payment; NPV evaluates a supplied periodic rate. MIRR uses separate financing and reinvestment rates.

Named variants

No named variants are recorded yet.

Suggest a correction or contribute an implementation · For agents