Skip to main content

formualizer_eval/builtins/financial/
tvm.rs

1//! Time Value of Money functions: PMT, PV, FV, NPV, NPER, RATE, IPMT, PPMT, XNPV, XIRR, DOLLARDE, DOLLARFR
2
3use crate::args::ArgSchema;
4use crate::function::Function;
5use crate::traits::{ArgumentHandle, CalcValue, FunctionContext};
6use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
7use formualizer_macros::func_caps;
8
9fn coerce_num(arg: &ArgumentHandle) -> Result<f64, ExcelError> {
10    let v = arg.value()?.into_literal();
11    coerce_literal_num(&v)
12}
13
14fn coerce_literal_num(v: &LiteralValue) -> Result<f64, ExcelError> {
15    match v {
16        LiteralValue::Number(f) => Ok(*f),
17        LiteralValue::Int(i) => Ok(*i as f64),
18        LiteralValue::Boolean(b) => Ok(if *b { 1.0 } else { 0.0 }),
19        LiteralValue::Empty => Ok(0.0),
20        LiteralValue::Error(e) => Err(e.clone()),
21        _ => Err(ExcelError::new_value()),
22    }
23}
24
25/// Calculates the constant payment amount for a fixed-rate annuity or loan.
26///
27/// Use this to solve for periodic payment size when rate, term, and present/future value
28/// targets are known.
29///
30/// # Remarks
31/// - `rate` is the interest rate per payment period (for example, annual rate / 12 for monthly payments).
32/// - Cash-flow sign convention: cash paid out is negative and cash received is positive.
33/// - `type = 0` means end-of-period payments; `type != 0` means beginning-of-period payments.
34/// - Returns `#NUM!` when `nper` is zero.
35/// - Propagates argument conversion and underlying value errors.
36///
37/// # Examples
38/// ```yaml,sandbox
39/// formula: =PMT(0.06/12, 360, 300000)
40/// result: -1798.6515754582708
41/// ```
42/// ```yaml,sandbox
43/// formula: =PMT(0.05/4, 20, -10000, 0, 1)
44/// result: 561.1890334005388
45/// ```
46/// ```yaml,docs
47/// related:
48///   - PV
49///   - FV
50///   - NPER
51///   - RATE
52/// faq:
53///   - q: "Why is `PMT` usually negative for a loan?"
54///     a: "TVM sign convention treats cash you pay as negative; with positive `pv`, payment outputs are typically negative."
55/// ```
56#[derive(Debug)]
57pub struct PmtFn;
58/// [formualizer-docgen:schema:start]
59/// Name: PMT
60/// Type: PmtFn
61/// Min args: 3
62/// Max args: variadic
63/// Variadic: true
64/// Signature: PMT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5...: number@scalar)
65/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
66/// Caps: PURE
67/// [formualizer-docgen:schema:end]
68impl Function for PmtFn {
69    func_caps!(PURE);
70    fn name(&self) -> &'static str {
71        "PMT"
72    }
73    fn min_args(&self) -> usize {
74        3
75    }
76    fn variadic(&self) -> bool {
77        true
78    }
79    fn arg_schema(&self) -> &'static [ArgSchema] {
80        use std::sync::LazyLock;
81        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
82            vec![
83                ArgSchema::number_lenient_scalar(), // rate
84                ArgSchema::number_lenient_scalar(), // nper
85                ArgSchema::number_lenient_scalar(), // pv
86                ArgSchema::number_lenient_scalar(), // fv (optional)
87                ArgSchema::number_lenient_scalar(), // type (optional)
88            ]
89        });
90        &SCHEMA[..]
91    }
92    fn eval<'a, 'b, 'c>(
93        &self,
94        args: &'c [ArgumentHandle<'a, 'b>],
95        _ctx: &dyn FunctionContext<'b>,
96    ) -> Result<CalcValue<'b>, ExcelError> {
97        let rate = coerce_num(&args[0])?;
98        let nper = coerce_num(&args[1])?;
99        let pv = coerce_num(&args[2])?;
100        let fv = if args.len() > 3 {
101            coerce_num(&args[3])?
102        } else {
103            0.0
104        };
105        let pmt_type = if args.len() > 4 {
106            coerce_num(&args[4])? as i32
107        } else {
108            0
109        };
110
111        if nper == 0.0 {
112            return Ok(CalcValue::Scalar(
113                LiteralValue::Error(ExcelError::new_num()),
114            ));
115        }
116
117        let pmt = if rate.abs() < 1e-10 {
118            // When rate is 0, PMT = -(pv + fv) / nper
119            -(pv + fv) / nper
120        } else {
121            // PMT = (rate * (pv * (1+rate)^nper + fv)) / ((1+rate)^nper - 1)
122            // With type adjustment for beginning of period
123            let factor = (1.0 + rate).powf(nper);
124            let type_adj = if pmt_type != 0 { 1.0 + rate } else { 1.0 };
125            -(rate * (pv * factor + fv)) / ((factor - 1.0) * type_adj)
126        };
127
128        Ok(CalcValue::Scalar(LiteralValue::Number(pmt)))
129    }
130}
131
132/// Calculates present value from periodic cash flows at a fixed rate.
133///
134/// Use this to discount a regular payment stream and optional terminal value back to time zero.
135///
136/// # Remarks
137/// - `rate` is the discount rate per period.
138/// - Cash-flow sign convention: inflows are positive and outflows are negative.
139/// - `type = 0` assumes payments at period end; `type != 0` assumes period start.
140/// - When `rate` is zero, present value is computed with simple arithmetic (no discounting).
141/// - Returns argument-related errors if coercion fails or an input is an error value.
142///
143/// # Examples
144/// ```yaml,sandbox
145/// formula: =PV(0.06/12, 360, -1798.65157545827)
146/// result: 299999.9999999998
147/// ```
148/// ```yaml,sandbox
149/// formula: =PV(0, 10, -500)
150/// result: 5000
151/// ```
152/// ```yaml,docs
153/// related:
154///   - PMT
155///   - FV
156///   - NPER
157///   - RATE
158/// faq:
159///   - q: "How does `type` change `PV`?"
160///     a: "`type=0` discounts end-of-period payments, while non-zero `type` treats payments as beginning-of-period (annuity due)."
161/// ```
162#[derive(Debug)]
163pub struct PvFn;
164/// [formualizer-docgen:schema:start]
165/// Name: PV
166/// Type: PvFn
167/// Min args: 3
168/// Max args: variadic
169/// Variadic: true
170/// Signature: PV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5...: number@scalar)
171/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
172/// Caps: PURE
173/// [formualizer-docgen:schema:end]
174impl Function for PvFn {
175    func_caps!(PURE);
176    fn name(&self) -> &'static str {
177        "PV"
178    }
179    fn min_args(&self) -> usize {
180        3
181    }
182    fn variadic(&self) -> bool {
183        true
184    }
185    fn arg_schema(&self) -> &'static [ArgSchema] {
186        use std::sync::LazyLock;
187        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
188            vec![
189                ArgSchema::number_lenient_scalar(),
190                ArgSchema::number_lenient_scalar(),
191                ArgSchema::number_lenient_scalar(),
192                ArgSchema::number_lenient_scalar(),
193                ArgSchema::number_lenient_scalar(),
194            ]
195        });
196        &SCHEMA[..]
197    }
198    fn eval<'a, 'b, 'c>(
199        &self,
200        args: &'c [ArgumentHandle<'a, 'b>],
201        _ctx: &dyn FunctionContext<'b>,
202    ) -> Result<CalcValue<'b>, ExcelError> {
203        let rate = coerce_num(&args[0])?;
204        let nper = coerce_num(&args[1])?;
205        let pmt = coerce_num(&args[2])?;
206        let fv = if args.len() > 3 {
207            coerce_num(&args[3])?
208        } else {
209            0.0
210        };
211        let pmt_type = if args.len() > 4 {
212            coerce_num(&args[4])? as i32
213        } else {
214            0
215        };
216
217        let pv = if rate.abs() < 1e-10 {
218            -fv - pmt * nper
219        } else {
220            let factor = (1.0 + rate).powf(nper);
221            let type_adj = if pmt_type != 0 { 1.0 + rate } else { 1.0 };
222            (-fv - pmt * type_adj * (factor - 1.0) / rate) / factor
223        };
224
225        Ok(CalcValue::Scalar(LiteralValue::Number(pv)))
226    }
227}
228
229/// Calculates future value from a fixed periodic rate and payment stream.
230///
231/// Use this to project an ending balance after compounding a present value and periodic payments.
232///
233/// # Remarks
234/// - `rate` is the interest rate per period.
235/// - Cash-flow sign convention: payments you make are negative; receipts are positive.
236/// - `type = 0` models end-of-period payments; `type != 0` models beginning-of-period payments.
237/// - When `rate` is zero, result is linear (`-pv - pmt * nper`).
238/// - Returns argument-related errors if coercion fails or an input is an error value.
239///
240/// # Examples
241/// ```yaml,sandbox
242/// formula: =FV(0.04/12, 120, -200)
243/// result: 29449.96094509572
244/// ```
245/// ```yaml,sandbox
246/// formula: =FV(0, 24, -150, 1000)
247/// result: 2600
248/// ```
249/// ```yaml,docs
250/// related:
251///   - PV
252///   - PMT
253///   - NPER
254///   - RATE
255/// faq:
256///   - q: "What happens when `rate` is zero in `FV`?"
257///     a: "It falls back to linear accumulation: `-pv - pmt * nper` with no compounding."
258/// ```
259#[derive(Debug)]
260pub struct FvFn;
261/// [formualizer-docgen:schema:start]
262/// Name: FV
263/// Type: FvFn
264/// Min args: 3
265/// Max args: variadic
266/// Variadic: true
267/// Signature: FV(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5...: number@scalar)
268/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
269/// Caps: PURE
270/// [formualizer-docgen:schema:end]
271impl Function for FvFn {
272    func_caps!(PURE);
273    fn name(&self) -> &'static str {
274        "FV"
275    }
276    fn min_args(&self) -> usize {
277        3
278    }
279    fn variadic(&self) -> bool {
280        true
281    }
282    fn arg_schema(&self) -> &'static [ArgSchema] {
283        use std::sync::LazyLock;
284        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
285            vec![
286                ArgSchema::number_lenient_scalar(),
287                ArgSchema::number_lenient_scalar(),
288                ArgSchema::number_lenient_scalar(),
289                ArgSchema::number_lenient_scalar(),
290                ArgSchema::number_lenient_scalar(),
291            ]
292        });
293        &SCHEMA[..]
294    }
295    fn eval<'a, 'b, 'c>(
296        &self,
297        args: &'c [ArgumentHandle<'a, 'b>],
298        _ctx: &dyn FunctionContext<'b>,
299    ) -> Result<CalcValue<'b>, ExcelError> {
300        let rate = coerce_num(&args[0])?;
301        let nper = coerce_num(&args[1])?;
302        let pmt = coerce_num(&args[2])?;
303        let pv = if args.len() > 3 {
304            coerce_num(&args[3])?
305        } else {
306            0.0
307        };
308        let pmt_type = if args.len() > 4 {
309            coerce_num(&args[4])? as i32
310        } else {
311            0
312        };
313
314        let fv = if rate.abs() < 1e-10 {
315            -pv - pmt * nper
316        } else {
317            let factor = (1.0 + rate).powf(nper);
318            let type_adj = if pmt_type != 0 { 1.0 + rate } else { 1.0 };
319            -pv * factor - pmt * type_adj * (factor - 1.0) / rate
320        };
321
322        Ok(CalcValue::Scalar(LiteralValue::Number(fv)))
323    }
324}
325
326/// Calculates net present value for equally spaced cash flows.
327///
328/// The first cash-flow argument is discounted one period from the present, matching spreadsheet
329/// `NPV` behavior for periodic series.
330///
331/// # Remarks
332/// - `rate` is the discount rate per period.
333/// - Cash-flow sign convention: investments/outflows are negative, returns/inflows are positive.
334/// - Non-numeric values are ignored; numeric values in arrays/ranges are consumed left-to-right.
335/// - Embedded error values inside provided cash-flow values are propagated as errors.
336/// - Returns argument coercion errors for invalid `rate` or direct scalar failures.
337///
338/// # Examples
339/// ```yaml,sandbox
340/// formula: =NPV(0.08, 4000, 5000, 6000)
341/// result: 12753.391251333636
342/// ```
343/// ```yaml,sandbox
344/// formula: =NPV(0.10, -5000, 2000, 2500, 3000)
345/// result: 1034.7653848780812
346/// ```
347/// ```yaml,docs
348/// related:
349///   - XNPV
350///   - IRR
351///   - MIRR
352/// faq:
353///   - q: "Is the first cash flow discounted at period 0 or period 1?"
354///     a: "`NPV` discounts the first supplied cash flow one full period, matching spreadsheet `NPV` behavior."
355/// ```
356#[derive(Debug)]
357pub struct NpvFn;
358/// [formualizer-docgen:schema:start]
359/// Name: NPV
360/// Type: NpvFn
361/// Min args: 2
362/// Max args: variadic
363/// Variadic: true
364/// Signature: NPV(arg1: number@scalar, arg2...: any@scalar)
365/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
366/// Caps: PURE
367/// [formualizer-docgen:schema:end]
368impl Function for NpvFn {
369    func_caps!(PURE);
370    fn name(&self) -> &'static str {
371        "NPV"
372    }
373    fn min_args(&self) -> usize {
374        2
375    }
376    fn variadic(&self) -> bool {
377        true
378    }
379    fn arg_schema(&self) -> &'static [ArgSchema] {
380        use std::sync::LazyLock;
381        static SCHEMA: LazyLock<Vec<ArgSchema>> =
382            LazyLock::new(|| vec![ArgSchema::number_lenient_scalar(), ArgSchema::any()]);
383        &SCHEMA[..]
384    }
385    fn eval<'a, 'b, 'c>(
386        &self,
387        args: &'c [ArgumentHandle<'a, 'b>],
388        _ctx: &dyn FunctionContext<'b>,
389    ) -> Result<CalcValue<'b>, ExcelError> {
390        let rate = coerce_num(&args[0])?;
391
392        let mut npv = 0.0;
393        let mut period = 1;
394
395        for arg in &args[1..] {
396            let v = arg.value()?.into_literal();
397            match v {
398                LiteralValue::Number(n) => {
399                    npv += n / (1.0 + rate).powi(period);
400                    period += 1;
401                }
402                LiteralValue::Int(i) => {
403                    npv += (i as f64) / (1.0 + rate).powi(period);
404                    period += 1;
405                }
406                LiteralValue::Error(e) => {
407                    return Ok(CalcValue::Scalar(LiteralValue::Error(e)));
408                }
409                LiteralValue::Array(arr) => {
410                    for row in arr {
411                        for cell in row {
412                            match cell {
413                                LiteralValue::Number(n) => {
414                                    npv += n / (1.0 + rate).powi(period);
415                                    period += 1;
416                                }
417                                LiteralValue::Int(i) => {
418                                    npv += (i as f64) / (1.0 + rate).powi(period);
419                                    period += 1;
420                                }
421                                LiteralValue::Error(e) => {
422                                    return Ok(CalcValue::Scalar(LiteralValue::Error(e)));
423                                }
424                                _ => {} // Skip non-numeric values
425                            }
426                        }
427                    }
428                }
429                _ => {} // Skip non-numeric values
430            }
431        }
432
433        Ok(CalcValue::Scalar(LiteralValue::Number(npv)))
434    }
435}
436
437/// Calculates the number of periods needed to satisfy a cash-flow target.
438///
439/// Use this to solve term length when periodic rate, payment, and value constraints are known.
440///
441/// # Remarks
442/// - `rate` is the interest rate per period.
443/// - Cash-flow sign convention: at least one of `pmt`, `pv`, or `fv` should usually have opposite sign.
444/// - `type = 0` means payments at period end; `type != 0` means period start.
445/// - Returns `#NUM!` when inputs imply no finite solution (for example, invalid logarithm domain).
446/// - Returns `#NUM!` when both `rate = 0` and `pmt = 0`.
447///
448/// # Examples
449/// ```yaml,sandbox
450/// formula: =NPER(0.06/12, -1798.65157545827, 300000)
451/// result: 360.00000000000045
452/// ```
453/// ```yaml,sandbox
454/// formula: =NPER(0, -250, 5000)
455/// result: 20
456/// ```
457/// ```yaml,docs
458/// related:
459///   - PMT
460///   - PV
461///   - RATE
462/// faq:
463///   - q: "Why does `NPER` return `#NUM!` for some sign combinations?"
464///     a: "If the logarithm domain is non-positive (or `rate=0` with `pmt=0`), there is no finite solution and `#NUM!` is returned."
465/// ```
466#[derive(Debug)]
467pub struct NperFn;
468/// [formualizer-docgen:schema:start]
469/// Name: NPER
470/// Type: NperFn
471/// Min args: 3
472/// Max args: variadic
473/// Variadic: true
474/// Signature: NPER(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5...: number@scalar)
475/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
476/// Caps: PURE
477/// [formualizer-docgen:schema:end]
478impl Function for NperFn {
479    func_caps!(PURE);
480    fn name(&self) -> &'static str {
481        "NPER"
482    }
483    fn min_args(&self) -> usize {
484        3
485    }
486    fn variadic(&self) -> bool {
487        true
488    }
489    fn arg_schema(&self) -> &'static [ArgSchema] {
490        use std::sync::LazyLock;
491        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
492            vec![
493                ArgSchema::number_lenient_scalar(),
494                ArgSchema::number_lenient_scalar(),
495                ArgSchema::number_lenient_scalar(),
496                ArgSchema::number_lenient_scalar(),
497                ArgSchema::number_lenient_scalar(),
498            ]
499        });
500        &SCHEMA[..]
501    }
502    fn eval<'a, 'b, 'c>(
503        &self,
504        args: &'c [ArgumentHandle<'a, 'b>],
505        _ctx: &dyn FunctionContext<'b>,
506    ) -> Result<CalcValue<'b>, ExcelError> {
507        let rate = coerce_num(&args[0])?;
508        let pmt = coerce_num(&args[1])?;
509        let pv = coerce_num(&args[2])?;
510        let fv = if args.len() > 3 {
511            coerce_num(&args[3])?
512        } else {
513            0.0
514        };
515        let pmt_type = if args.len() > 4 {
516            coerce_num(&args[4])? as i32
517        } else {
518            0
519        };
520
521        let nper = if rate.abs() < 1e-10 {
522            if pmt.abs() < 1e-10 {
523                return Ok(CalcValue::Scalar(
524                    LiteralValue::Error(ExcelError::new_num()),
525                ));
526            }
527            -(pv + fv) / pmt
528        } else {
529            let type_adj = if pmt_type != 0 { 1.0 + rate } else { 1.0 };
530            let pmt_adj = pmt * type_adj;
531            let numerator = pmt_adj - fv * rate;
532            let denominator = pv * rate + pmt_adj;
533            if numerator / denominator <= 0.0 {
534                return Ok(CalcValue::Scalar(
535                    LiteralValue::Error(ExcelError::new_num()),
536                ));
537            }
538            (numerator / denominator).ln() / (1.0 + rate).ln()
539        };
540
541        Ok(CalcValue::Scalar(LiteralValue::Number(nper)))
542    }
543}
544
545/// Solves for the periodic interest rate implied by annuity cash flows.
546///
547/// This function uses Newton-Raphson iteration and returns the per-period rate that satisfies
548/// the TVM equation.
549///
550/// # Remarks
551/// - Output is a rate per period; convert to annual terms externally if needed.
552/// - Cash-flow sign convention matters for convergence: use opposite signs for borrow/repay sides.
553/// - `guess` defaults to `0.1` and influences convergence speed and branch selection.
554/// - `type = 0` means end-of-period payments; `type != 0` means beginning-of-period payments.
555/// - Returns `#NUM!` on non-convergence, near-zero derivative, or unsatisfied numeric conditions.
556///
557/// # Examples
558/// ```yaml,sandbox
559/// formula: =RATE(360, -1798.65157545827, 300000)
560/// result: 0.005000000000000038
561/// ```
562/// ```yaml,sandbox
563/// formula: =RATE(12, -88.84878867834166, 1000)
564/// result: 0.010000000000005125
565/// ```
566/// ```yaml,docs
567/// related:
568///   - PMT
569///   - NPER
570///   - IRR
571/// faq:
572///   - q: "How important is `guess` for `RATE`?"
573///     a: "`RATE` uses Newton-Raphson from `guess` (default `0.1`); a poor starting point can lead to non-convergence and `#NUM!`."
574/// ```
575#[derive(Debug)]
576pub struct RateFn;
577/// [formualizer-docgen:schema:start]
578/// Name: RATE
579/// Type: RateFn
580/// Min args: 3
581/// Max args: variadic
582/// Variadic: true
583/// Signature: RATE(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar, arg6...: number@scalar)
584/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg6{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
585/// Caps: PURE
586/// [formualizer-docgen:schema:end]
587impl Function for RateFn {
588    func_caps!(PURE);
589    fn name(&self) -> &'static str {
590        "RATE"
591    }
592    fn min_args(&self) -> usize {
593        3
594    }
595    fn variadic(&self) -> bool {
596        true
597    }
598    fn arg_schema(&self) -> &'static [ArgSchema] {
599        use std::sync::LazyLock;
600        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
601            vec![
602                ArgSchema::number_lenient_scalar(),
603                ArgSchema::number_lenient_scalar(),
604                ArgSchema::number_lenient_scalar(),
605                ArgSchema::number_lenient_scalar(),
606                ArgSchema::number_lenient_scalar(),
607                ArgSchema::number_lenient_scalar(),
608            ]
609        });
610        &SCHEMA[..]
611    }
612    fn eval<'a, 'b, 'c>(
613        &self,
614        args: &'c [ArgumentHandle<'a, 'b>],
615        _ctx: &dyn FunctionContext<'b>,
616    ) -> Result<CalcValue<'b>, ExcelError> {
617        let nper = coerce_num(&args[0])?;
618        let pmt = coerce_num(&args[1])?;
619        let pv = coerce_num(&args[2])?;
620        let fv = if args.len() > 3 {
621            coerce_num(&args[3])?
622        } else {
623            0.0
624        };
625        let pmt_type = if args.len() > 4 {
626            coerce_num(&args[4])? as i32
627        } else {
628            0
629        };
630        let guess = if args.len() > 5 {
631            coerce_num(&args[5])?
632        } else {
633            0.1
634        };
635
636        // Newton-Raphson iteration to find rate
637        let mut rate = guess;
638        let max_iter = 100;
639        let tolerance = 1e-10;
640
641        for _ in 0..max_iter {
642            let type_adj = if pmt_type != 0 { 1.0 + rate } else { 1.0 };
643
644            if rate.abs() < 1e-10 {
645                // Special case for very small rate
646                let f = pv + pmt * nper + fv;
647                if f.abs() < tolerance {
648                    return Ok(CalcValue::Scalar(LiteralValue::Number(rate)));
649                }
650                rate = 0.01; // Nudge away from zero
651                continue;
652            }
653
654            let factor = (1.0 + rate).powf(nper);
655            let f = pv * factor + pmt * type_adj * (factor - 1.0) / rate + fv;
656
657            // Derivative
658            let factor_prime = nper * (1.0 + rate).powf(nper - 1.0);
659            let df = pv * factor_prime
660                + pmt * type_adj * (factor_prime / rate - (factor - 1.0) / (rate * rate));
661
662            if df.abs() < 1e-20 {
663                break;
664            }
665
666            let new_rate = rate - f / df;
667
668            if (new_rate - rate).abs() < tolerance {
669                return Ok(CalcValue::Scalar(LiteralValue::Number(new_rate)));
670            }
671
672            rate = new_rate;
673
674            // Prevent rate from going too negative
675            if rate < -0.99 {
676                rate = -0.99;
677            }
678        }
679
680        // If we didn't converge, return error
681        Ok(CalcValue::Scalar(
682            LiteralValue::Error(ExcelError::new_num()),
683        ))
684    }
685}
686
687/// Returns the interest-only component of a payment for a specific period.
688///
689/// Use this with `PMT` or `PPMT` to break a fixed payment into interest and principal pieces.
690///
691/// # Remarks
692/// - `rate` is the interest rate per payment period.
693/// - `per` is 1-based and must satisfy `1 <= per <= nper`.
694/// - Cash-flow sign convention: for a positive loan principal (`pv`), interest components are typically negative.
695/// - `type = 1` yields zero interest in period 1 (annuity-due first payment).
696/// - Returns `#NUM!` when `per` is outside valid bounds.
697///
698/// # Examples
699/// ```yaml,sandbox
700/// formula: =IPMT(0.06/12, 1, 360, 300000)
701/// result: -1500
702/// ```
703/// ```yaml,sandbox
704/// formula: =IPMT(0.06/12, 12, 360, 300000)
705/// result: -1483.1572957145672
706/// ```
707/// ```yaml,docs
708/// related:
709///   - PMT
710///   - PPMT
711///   - CUMIPMT
712/// faq:
713///   - q: "Why is `IPMT` period 1 equal to zero for `type=1`?"
714///     a: "With beginning-of-period payments, the first payment occurs before interest accrues, so period-1 interest is zero."
715/// ```
716#[derive(Debug)]
717pub struct IpmtFn;
718/// [formualizer-docgen:schema:start]
719/// Name: IPMT
720/// Type: IpmtFn
721/// Min args: 4
722/// Max args: variadic
723/// Variadic: true
724/// Signature: IPMT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar, arg6...: number@scalar)
725/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg6{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
726/// Caps: PURE
727/// [formualizer-docgen:schema:end]
728impl Function for IpmtFn {
729    func_caps!(PURE);
730    fn name(&self) -> &'static str {
731        "IPMT"
732    }
733    fn min_args(&self) -> usize {
734        4
735    }
736    fn variadic(&self) -> bool {
737        true
738    }
739    fn arg_schema(&self) -> &'static [ArgSchema] {
740        use std::sync::LazyLock;
741        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
742            vec![
743                ArgSchema::number_lenient_scalar(),
744                ArgSchema::number_lenient_scalar(),
745                ArgSchema::number_lenient_scalar(),
746                ArgSchema::number_lenient_scalar(),
747                ArgSchema::number_lenient_scalar(),
748                ArgSchema::number_lenient_scalar(),
749            ]
750        });
751        &SCHEMA[..]
752    }
753    fn eval<'a, 'b, 'c>(
754        &self,
755        args: &'c [ArgumentHandle<'a, 'b>],
756        _ctx: &dyn FunctionContext<'b>,
757    ) -> Result<CalcValue<'b>, ExcelError> {
758        let rate = coerce_num(&args[0])?;
759        let per = coerce_num(&args[1])?;
760        let nper = coerce_num(&args[2])?;
761        let pv = coerce_num(&args[3])?;
762        let fv = if args.len() > 4 {
763            coerce_num(&args[4])?
764        } else {
765            0.0
766        };
767        let pmt_type = if args.len() > 5 {
768            coerce_num(&args[5])? as i32
769        } else {
770            0
771        };
772
773        if per < 1.0 || per > nper {
774            return Ok(CalcValue::Scalar(
775                LiteralValue::Error(ExcelError::new_num()),
776            ));
777        }
778
779        // Calculate PMT first
780        let pmt = if rate.abs() < 1e-10 {
781            -(pv + fv) / nper
782        } else {
783            let factor = (1.0 + rate).powf(nper);
784            let type_adj = if pmt_type != 0 { 1.0 + rate } else { 1.0 };
785            -(rate * (pv * factor + fv)) / ((factor - 1.0) * type_adj)
786        };
787
788        // Calculate FV at start of period
789        let fv_at_start = if rate.abs() < 1e-10 {
790            -pv - pmt * (per - 1.0)
791        } else {
792            let factor = (1.0 + rate).powf(per - 1.0);
793            let type_adj = if pmt_type != 0 { 1.0 + rate } else { 1.0 };
794            -pv * factor - pmt * type_adj * (factor - 1.0) / rate
795        };
796
797        // Interest is rate * balance at start of period
798        // fv_at_start is negative of balance, so ipmt = fv_at_start * rate
799        let ipmt = if pmt_type != 0 && per == 1.0 {
800            0.0 // No interest in first period for annuity due
801        } else {
802            fv_at_start * rate
803        };
804
805        Ok(CalcValue::Scalar(LiteralValue::Number(ipmt)))
806    }
807}
808
809/// Returns the principal component of a payment for a specific period.
810///
811/// `PPMT` is computed as `PMT - IPMT` using the same rate, timing, and sign convention.
812///
813/// # Remarks
814/// - `rate` is the interest rate per payment period.
815/// - `per` is 1-based and must satisfy `1 <= per <= nper`.
816/// - Cash-flow sign convention: with a positive borrowed `pv`, principal components are usually negative.
817/// - `type = 1` means beginning-of-period payments.
818/// - Returns `#NUM!` when `per` is outside valid bounds.
819///
820/// # Examples
821/// ```yaml,sandbox
822/// formula: =PPMT(0.06/12, 1, 360, 300000)
823/// result: -298.6515754582708
824/// ```
825/// ```yaml,sandbox
826/// formula: =PPMT(0.06/12, 12, 360, 300000)
827/// result: -315.4942797437036
828/// ```
829/// ```yaml,docs
830/// related:
831///   - PMT
832///   - IPMT
833///   - CUMPRINC
834/// faq:
835///   - q: "How is `PPMT` computed?"
836///     a: "`PPMT` is computed as `PMT - IPMT` for the same `rate`, `per`, `nper`, `pv`, `fv`, and `type`."
837/// ```
838#[derive(Debug)]
839pub struct PpmtFn;
840/// [formualizer-docgen:schema:start]
841/// Name: PPMT
842/// Type: PpmtFn
843/// Min args: 4
844/// Max args: variadic
845/// Variadic: true
846/// Signature: PPMT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar, arg6...: number@scalar)
847/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg6{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
848/// Caps: PURE
849/// [formualizer-docgen:schema:end]
850impl Function for PpmtFn {
851    func_caps!(PURE);
852    fn name(&self) -> &'static str {
853        "PPMT"
854    }
855    fn min_args(&self) -> usize {
856        4
857    }
858    fn variadic(&self) -> bool {
859        true
860    }
861    fn arg_schema(&self) -> &'static [ArgSchema] {
862        use std::sync::LazyLock;
863        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
864            vec![
865                ArgSchema::number_lenient_scalar(),
866                ArgSchema::number_lenient_scalar(),
867                ArgSchema::number_lenient_scalar(),
868                ArgSchema::number_lenient_scalar(),
869                ArgSchema::number_lenient_scalar(),
870                ArgSchema::number_lenient_scalar(),
871            ]
872        });
873        &SCHEMA[..]
874    }
875    fn eval<'a, 'b, 'c>(
876        &self,
877        args: &'c [ArgumentHandle<'a, 'b>],
878        _ctx: &dyn FunctionContext<'b>,
879    ) -> Result<CalcValue<'b>, ExcelError> {
880        let rate = coerce_num(&args[0])?;
881        let per = coerce_num(&args[1])?;
882        let nper = coerce_num(&args[2])?;
883        let pv = coerce_num(&args[3])?;
884        let fv = if args.len() > 4 {
885            coerce_num(&args[4])?
886        } else {
887            0.0
888        };
889        let pmt_type = if args.len() > 5 {
890            coerce_num(&args[5])? as i32
891        } else {
892            0
893        };
894
895        if per < 1.0 || per > nper {
896            return Ok(CalcValue::Scalar(
897                LiteralValue::Error(ExcelError::new_num()),
898            ));
899        }
900
901        // Calculate PMT
902        let pmt = if rate.abs() < 1e-10 {
903            -(pv + fv) / nper
904        } else {
905            let factor = (1.0 + rate).powf(nper);
906            let type_adj = if pmt_type != 0 { 1.0 + rate } else { 1.0 };
907            -(rate * (pv * factor + fv)) / ((factor - 1.0) * type_adj)
908        };
909
910        // Calculate IPMT
911        let fv_at_start = if rate.abs() < 1e-10 {
912            -pv - pmt * (per - 1.0)
913        } else {
914            let factor = (1.0 + rate).powf(per - 1.0);
915            let type_adj = if pmt_type != 0 { 1.0 + rate } else { 1.0 };
916            -pv * factor - pmt * type_adj * (factor - 1.0) / rate
917        };
918
919        let ipmt = if pmt_type != 0 && per == 1.0 {
920            0.0
921        } else {
922            fv_at_start * rate
923        };
924
925        // PPMT = PMT - IPMT
926        let ppmt = pmt - ipmt;
927
928        Ok(CalcValue::Scalar(LiteralValue::Number(ppmt)))
929    }
930}
931
932/// Converts a nominal annual rate into an effective annual rate.
933///
934/// This is useful when nominal APR is quoted with periodic compounding and you need annualized
935/// yield including compounding effects.
936///
937/// # Remarks
938/// - `nominal_rate` is annual; `npery` is compounding periods per year.
939/// - `npery` is truncated to an integer before computation.
940/// - Sign convention is not cash-flow based; this function transforms rate conventions only.
941/// - Returns `#NUM!` when `nominal_rate <= 0` or `npery < 1`.
942/// - Result formula: `(1 + nominal_rate / npery)^npery - 1`.
943///
944/// # Examples
945/// ```yaml,sandbox
946/// formula: =EFFECT(0.12, 12)
947/// result: 0.12682503013196977
948/// ```
949/// ```yaml,sandbox
950/// formula: =EFFECT(0.08, 4)
951/// result: 0.08243215999999998
952/// ```
953/// ```yaml,docs
954/// related:
955///   - NOMINAL
956///   - RATE
957/// faq:
958///   - q: "Does `EFFECT` accept fractional compounding periods?"
959///     a: "`npery` is truncated to an integer first; values less than 1 (or non-positive `nominal_rate`) return `#NUM!`."
960/// ```
961#[derive(Debug)]
962pub struct EffectFn;
963/// [formualizer-docgen:schema:start]
964/// Name: EFFECT
965/// Type: EffectFn
966/// Min args: 2
967/// Max args: 2
968/// Variadic: false
969/// Signature: EFFECT(arg1: number@scalar, arg2: number@scalar)
970/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
971/// Caps: PURE
972/// [formualizer-docgen:schema:end]
973impl Function for EffectFn {
974    func_caps!(PURE);
975    fn name(&self) -> &'static str {
976        "EFFECT"
977    }
978    fn min_args(&self) -> usize {
979        2
980    }
981    fn arg_schema(&self) -> &'static [ArgSchema] {
982        use std::sync::LazyLock;
983        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
984            vec![
985                ArgSchema::number_lenient_scalar(),
986                ArgSchema::number_lenient_scalar(),
987            ]
988        });
989        &SCHEMA[..]
990    }
991    fn eval<'a, 'b, 'c>(
992        &self,
993        args: &'c [ArgumentHandle<'a, 'b>],
994        _ctx: &dyn FunctionContext<'b>,
995    ) -> Result<CalcValue<'b>, ExcelError> {
996        let nominal_rate = coerce_num(&args[0])?;
997        let npery = coerce_num(&args[1])?.trunc() as i32;
998
999        // Validation
1000        if nominal_rate <= 0.0 || npery < 1 {
1001            return Ok(CalcValue::Scalar(
1002                LiteralValue::Error(ExcelError::new_num()),
1003            ));
1004        }
1005
1006        // EFFECT = (1 + nominal_rate/npery)^npery - 1
1007        let effect = (1.0 + nominal_rate / npery as f64).powi(npery) - 1.0;
1008        Ok(CalcValue::Scalar(LiteralValue::Number(effect)))
1009    }
1010}
1011
1012/// Converts an effective annual rate into a nominal annual rate.
1013///
1014/// This is the inverse-style transformation of `EFFECT` for a chosen compounding frequency.
1015///
1016/// # Remarks
1017/// - `effect_rate` is annual effective yield; `npery` is periods per year.
1018/// - `npery` is truncated to an integer before computation.
1019/// - Sign convention is not cash-flow based; this function converts annual rate representation.
1020/// - Returns `#NUM!` when `effect_rate <= 0` or `npery < 1`.
1021/// - Result formula: `npery * ((1 + effect_rate)^(1/npery) - 1)`.
1022///
1023/// # Examples
1024/// ```yaml,sandbox
1025/// formula: =NOMINAL(0.12682503013196977, 12)
1026/// result: 0.1200000000000001
1027/// ```
1028/// ```yaml,sandbox
1029/// formula: =NOMINAL(0.08243216, 4)
1030/// result: 0.08000000000000007
1031/// ```
1032/// ```yaml,docs
1033/// related:
1034///   - EFFECT
1035///   - RATE
1036/// faq:
1037///   - q: "Is `NOMINAL` an exact inverse of `EFFECT`?"
1038///     a: "It is the corresponding transformation for the same integer `npery`; both functions require positive rates and `npery >= 1`."
1039/// ```
1040#[derive(Debug)]
1041pub struct NominalFn;
1042/// [formualizer-docgen:schema:start]
1043/// Name: NOMINAL
1044/// Type: NominalFn
1045/// Min args: 2
1046/// Max args: 2
1047/// Variadic: false
1048/// Signature: NOMINAL(arg1: number@scalar, arg2: number@scalar)
1049/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1050/// Caps: PURE
1051/// [formualizer-docgen:schema:end]
1052impl Function for NominalFn {
1053    func_caps!(PURE);
1054    fn name(&self) -> &'static str {
1055        "NOMINAL"
1056    }
1057    fn min_args(&self) -> usize {
1058        2
1059    }
1060    fn arg_schema(&self) -> &'static [ArgSchema] {
1061        use std::sync::LazyLock;
1062        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
1063            vec![
1064                ArgSchema::number_lenient_scalar(),
1065                ArgSchema::number_lenient_scalar(),
1066            ]
1067        });
1068        &SCHEMA[..]
1069    }
1070    fn eval<'a, 'b, 'c>(
1071        &self,
1072        args: &'c [ArgumentHandle<'a, 'b>],
1073        _ctx: &dyn FunctionContext<'b>,
1074    ) -> Result<CalcValue<'b>, ExcelError> {
1075        let effect_rate = coerce_num(&args[0])?;
1076        let npery = coerce_num(&args[1])?.trunc() as i32;
1077
1078        // Validation
1079        if effect_rate <= 0.0 || npery < 1 {
1080            return Ok(CalcValue::Scalar(
1081                LiteralValue::Error(ExcelError::new_num()),
1082            ));
1083        }
1084
1085        // NOMINAL = npery * ((1 + effect_rate)^(1/npery) - 1)
1086        let nominal = npery as f64 * ((1.0 + effect_rate).powf(1.0 / npery as f64) - 1.0);
1087        Ok(CalcValue::Scalar(LiteralValue::Number(nominal)))
1088    }
1089}
1090
1091/// Calculates periodic internal rate of return for regularly spaced cash flows.
1092///
1093/// The function iteratively finds the per-period rate where discounted cash flows sum to zero.
1094///
1095/// # Remarks
1096/// - Output is a rate per cash-flow period (not automatically annualized).
1097/// - Cash-flow sign convention: outflows are negative and inflows are positive.
1098/// - Non-numeric cells in arrays/ranges are ignored; direct scalar errors are propagated.
1099/// - A callable value input returns `#CALC!`.
1100/// - Returns `#NUM!` if fewer than two numeric cash flows are available, if derivative is near zero, or if iteration does not converge.
1101///
1102/// # Examples
1103/// ```yaml,sandbox
1104/// formula: =IRR({-10000,3000,4200,6800})
1105/// result: 0.16340560068898924
1106/// ```
1107/// ```yaml,sandbox
1108/// formula: =IRR({-5000,1200,1410,1875,1050}, 0.1)
1109/// result: 0.041848876015677466
1110/// ```
1111/// ```yaml,docs
1112/// related:
1113///   - MIRR
1114///   - NPV
1115///   - XIRR
1116/// faq:
1117///   - q: "Why can `IRR` return `#NUM!` even with numeric cash flows?"
1118///     a: "The Newton solve can fail if derivative terms become unstable or no convergent root is reached from the chosen guess."
1119/// ```
1120#[derive(Debug)]
1121pub struct IrrFn;
1122/// [formualizer-docgen:schema:start]
1123/// Name: IRR
1124/// Type: IrrFn
1125/// Min args: 1
1126/// Max args: variadic
1127/// Variadic: true
1128/// Signature: IRR(arg1: any@scalar, arg2...: number@scalar)
1129/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1130/// Caps: PURE
1131/// [formualizer-docgen:schema:end]
1132impl Function for IrrFn {
1133    func_caps!(PURE);
1134    fn name(&self) -> &'static str {
1135        "IRR"
1136    }
1137    fn min_args(&self) -> usize {
1138        1
1139    }
1140    fn variadic(&self) -> bool {
1141        true
1142    }
1143    fn arg_schema(&self) -> &'static [ArgSchema] {
1144        use std::sync::LazyLock;
1145        static SCHEMA: LazyLock<Vec<ArgSchema>> =
1146            LazyLock::new(|| vec![ArgSchema::any(), ArgSchema::number_lenient_scalar()]);
1147        &SCHEMA[..]
1148    }
1149    fn eval<'a, 'b, 'c>(
1150        &self,
1151        args: &'c [ArgumentHandle<'a, 'b>],
1152        _ctx: &dyn FunctionContext<'b>,
1153    ) -> Result<CalcValue<'b>, ExcelError> {
1154        // Collect cash flows
1155        let mut cashflows = Vec::new();
1156        let val = args[0].value()?;
1157        match val {
1158            CalcValue::Scalar(lit) => match lit {
1159                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
1160                LiteralValue::Array(arr) => {
1161                    for row in arr {
1162                        for cell in row {
1163                            if let Ok(n) = coerce_literal_num(&cell) {
1164                                cashflows.push(n);
1165                            }
1166                        }
1167                    }
1168                }
1169                other => cashflows.push(coerce_literal_num(&other)?),
1170            },
1171            CalcValue::Range(range) => {
1172                let (rows, cols) = range.dims();
1173                for r in 0..rows {
1174                    for c in 0..cols {
1175                        let cell = range.get_cell(r, c);
1176                        if let Ok(n) = coerce_literal_num(&cell) {
1177                            cashflows.push(n);
1178                        }
1179                    }
1180                }
1181            }
1182            CalcValue::Callable(_) => {
1183                return Ok(CalcValue::Scalar(LiteralValue::Error(
1184                    ExcelError::new(ExcelErrorKind::Calc)
1185                        .with_message("LAMBDA value must be invoked"),
1186                )));
1187            }
1188        }
1189
1190        if cashflows.len() < 2 {
1191            return Ok(CalcValue::Scalar(
1192                LiteralValue::Error(ExcelError::new_num()),
1193            ));
1194        }
1195
1196        // Initial guess
1197        let guess = if args.len() > 1 {
1198            coerce_num(&args[1])?
1199        } else {
1200            0.1
1201        };
1202
1203        // Newton-Raphson iteration to find IRR
1204        let mut rate = guess;
1205        const MAX_ITER: i32 = 100;
1206        const EPSILON: f64 = 1e-10;
1207
1208        for _ in 0..MAX_ITER {
1209            let mut npv = 0.0;
1210            let mut d_npv = 0.0;
1211
1212            for (i, &cf) in cashflows.iter().enumerate() {
1213                let factor = (1.0 + rate).powi(i as i32);
1214                npv += cf / factor;
1215                if i > 0 {
1216                    d_npv -= (i as f64) * cf / (factor * (1.0 + rate));
1217                }
1218            }
1219
1220            if d_npv.abs() < EPSILON {
1221                return Ok(CalcValue::Scalar(
1222                    LiteralValue::Error(ExcelError::new_num()),
1223                ));
1224            }
1225
1226            let new_rate = rate - npv / d_npv;
1227            if (new_rate - rate).abs() < EPSILON {
1228                return Ok(CalcValue::Scalar(LiteralValue::Number(new_rate)));
1229            }
1230            rate = new_rate;
1231        }
1232
1233        Ok(CalcValue::Scalar(
1234            LiteralValue::Error(ExcelError::new_num()),
1235        ))
1236    }
1237}
1238
1239/// Calculates modified internal rate of return with separate finance and reinvest rates.
1240///
1241/// Negative cash flows are discounted at `finance_rate` and positive cash flows are compounded at
1242/// `reinvest_rate`, then combined into a single periodic return.
1243///
1244/// # Remarks
1245/// - `finance_rate` and `reinvest_rate` are both rates per cash-flow period.
1246/// - Cash-flow sign convention: at least one negative and one positive cash flow are required.
1247/// - Non-numeric cells in arrays/ranges are ignored; direct scalar errors are propagated.
1248/// - A callable value input returns `#CALC!`.
1249/// - Returns `#NUM!` for insufficient cash flows, and `#DIV/0!` when computed positive/negative legs are invalid.
1250///
1251/// # Examples
1252/// ```yaml,sandbox
1253/// formula: =MIRR({-10000,3000,4200,6800}, 0.1, 0.12)
1254/// result: 0.15147133664676304
1255/// ```
1256/// ```yaml,sandbox
1257/// formula: =MIRR({-120000,39000,30000,21000,37000,46000}, 0.1, 0.12)
1258/// result: 0.1260941303659051
1259/// ```
1260/// ```yaml,docs
1261/// related:
1262///   - IRR
1263///   - NPV
1264///   - XNPV
1265/// faq:
1266///   - q: "Why does `MIRR` return `#DIV/0!` for some cash-flow sets?"
1267///     a: "`MIRR` needs both a negative leg and a positive leg; if discounted negatives or compounded positives are invalid, it returns `#DIV/0!`."
1268/// ```
1269#[derive(Debug)]
1270pub struct MirrFn;
1271/// [formualizer-docgen:schema:start]
1272/// Name: MIRR
1273/// Type: MirrFn
1274/// Min args: 3
1275/// Max args: 3
1276/// Variadic: false
1277/// Signature: MIRR(arg1: any@scalar, arg2: number@scalar, arg3: number@scalar)
1278/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1279/// Caps: PURE
1280/// [formualizer-docgen:schema:end]
1281impl Function for MirrFn {
1282    func_caps!(PURE);
1283    fn name(&self) -> &'static str {
1284        "MIRR"
1285    }
1286    fn min_args(&self) -> usize {
1287        3
1288    }
1289    fn arg_schema(&self) -> &'static [ArgSchema] {
1290        use std::sync::LazyLock;
1291        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
1292            vec![
1293                ArgSchema::any(),
1294                ArgSchema::number_lenient_scalar(),
1295                ArgSchema::number_lenient_scalar(),
1296            ]
1297        });
1298        &SCHEMA[..]
1299    }
1300    fn eval<'a, 'b, 'c>(
1301        &self,
1302        args: &'c [ArgumentHandle<'a, 'b>],
1303        _ctx: &dyn FunctionContext<'b>,
1304    ) -> Result<CalcValue<'b>, ExcelError> {
1305        // Collect cash flows
1306        let mut cashflows = Vec::new();
1307        let val = args[0].value()?;
1308        match val {
1309            CalcValue::Scalar(lit) => match lit {
1310                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
1311                LiteralValue::Array(arr) => {
1312                    for row in arr {
1313                        for cell in row {
1314                            if let Ok(n) = coerce_literal_num(&cell) {
1315                                cashflows.push(n);
1316                            }
1317                        }
1318                    }
1319                }
1320                other => cashflows.push(coerce_literal_num(&other)?),
1321            },
1322            CalcValue::Range(range) => {
1323                let (rows, cols) = range.dims();
1324                for r in 0..rows {
1325                    for c in 0..cols {
1326                        let cell = range.get_cell(r, c);
1327                        if let Ok(n) = coerce_literal_num(&cell) {
1328                            cashflows.push(n);
1329                        }
1330                    }
1331                }
1332            }
1333            CalcValue::Callable(_) => {
1334                return Ok(CalcValue::Scalar(LiteralValue::Error(
1335                    ExcelError::new(ExcelErrorKind::Calc)
1336                        .with_message("LAMBDA value must be invoked"),
1337                )));
1338            }
1339        }
1340
1341        let finance_rate = coerce_num(&args[1])?;
1342        let reinvest_rate = coerce_num(&args[2])?;
1343
1344        if cashflows.len() < 2 {
1345            return Ok(CalcValue::Scalar(
1346                LiteralValue::Error(ExcelError::new_num()),
1347            ));
1348        }
1349
1350        let n = cashflows.len() as i32;
1351
1352        // Present value of negative cash flows (discounted at finance_rate)
1353        let mut pv_neg = 0.0;
1354        // Future value of positive cash flows (compounded at reinvest_rate)
1355        let mut fv_pos = 0.0;
1356
1357        for (i, &cf) in cashflows.iter().enumerate() {
1358            if cf < 0.0 {
1359                pv_neg += cf / (1.0 + finance_rate).powi(i as i32);
1360            } else {
1361                fv_pos += cf * (1.0 + reinvest_rate).powi(n - 1 - i as i32);
1362            }
1363        }
1364
1365        if pv_neg >= 0.0 || fv_pos <= 0.0 {
1366            return Ok(CalcValue::Scalar(
1367                LiteralValue::Error(ExcelError::new_div()),
1368            ));
1369        }
1370
1371        // MIRR = (FV_pos / -PV_neg)^(1/(n-1)) - 1
1372        let mirr = (-fv_pos / pv_neg).powf(1.0 / (n - 1) as f64) - 1.0;
1373        Ok(CalcValue::Scalar(LiteralValue::Number(mirr)))
1374    }
1375}
1376
1377/// Returns cumulative interest paid between two inclusive payment periods.
1378///
1379/// Use this to total the interest component over a slice of an amortization schedule.
1380///
1381/// # Remarks
1382/// - `rate` is the interest rate per payment period.
1383/// - `start_period` and `end_period` are 1-based, inclusive integer periods.
1384/// - `type` must be `0` (end-of-period) or `1` (beginning-of-period).
1385/// - Sign convention follows this implementation's balance model; with positive `pv`, cumulative interest is typically positive.
1386/// - Returns `#NUM!` for invalid domain values (non-positive rate, invalid ranges, invalid type, or non-positive `pv`).
1387///
1388/// # Examples
1389/// ```yaml,sandbox
1390/// formula: =CUMIPMT(0.06/12, 360, 300000, 1, 12, 0)
1391/// result: 16929.385083045923
1392/// ```
1393/// ```yaml,sandbox
1394/// formula: =CUMIPMT(0.06/12, 360, 300000, 13, 24, 0)
1395/// result: 14681.09233746059
1396/// ```
1397/// ```yaml,docs
1398/// related:
1399///   - IPMT
1400///   - PMT
1401///   - CUMPRINC
1402/// faq:
1403///   - q: "Are `start_period` and `end_period` inclusive in `CUMIPMT`?"
1404///     a: "Yes. Both bounds are inclusive and interpreted as 1-based periods after truncation to integers."
1405/// ```
1406#[derive(Debug)]
1407pub struct CumipmtFn;
1408/// [formualizer-docgen:schema:start]
1409/// Name: CUMIPMT
1410/// Type: CumipmtFn
1411/// Min args: 6
1412/// Max args: 6
1413/// Variadic: false
1414/// Signature: CUMIPMT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar, arg6: number@scalar)
1415/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg6{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1416/// Caps: PURE
1417/// [formualizer-docgen:schema:end]
1418impl Function for CumipmtFn {
1419    func_caps!(PURE);
1420    fn name(&self) -> &'static str {
1421        "CUMIPMT"
1422    }
1423    fn min_args(&self) -> usize {
1424        6
1425    }
1426    fn arg_schema(&self) -> &'static [ArgSchema] {
1427        use std::sync::LazyLock;
1428        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
1429            vec![
1430                ArgSchema::number_lenient_scalar(),
1431                ArgSchema::number_lenient_scalar(),
1432                ArgSchema::number_lenient_scalar(),
1433                ArgSchema::number_lenient_scalar(),
1434                ArgSchema::number_lenient_scalar(),
1435                ArgSchema::number_lenient_scalar(),
1436            ]
1437        });
1438        &SCHEMA[..]
1439    }
1440    fn eval<'a, 'b, 'c>(
1441        &self,
1442        args: &'c [ArgumentHandle<'a, 'b>],
1443        _ctx: &dyn FunctionContext<'b>,
1444    ) -> Result<CalcValue<'b>, ExcelError> {
1445        let rate = coerce_num(&args[0])?;
1446        let nper = coerce_num(&args[1])?.trunc() as i32;
1447        let pv = coerce_num(&args[2])?;
1448        let start = coerce_num(&args[3])?.trunc() as i32;
1449        let end = coerce_num(&args[4])?.trunc() as i32;
1450        let pay_type = coerce_num(&args[5])?.trunc() as i32;
1451
1452        // Validation
1453        if rate <= 0.0
1454            || nper <= 0
1455            || pv <= 0.0
1456            || start < 1
1457            || end < start
1458            || end > nper
1459            || (pay_type != 0 && pay_type != 1)
1460        {
1461            return Ok(CalcValue::Scalar(
1462                LiteralValue::Error(ExcelError::new_num()),
1463            ));
1464        }
1465
1466        // Calculate PMT
1467        let pmt = if rate == 0.0 {
1468            -pv / nper as f64
1469        } else {
1470            -pv * rate * (1.0 + rate).powi(nper) / ((1.0 + rate).powi(nper) - 1.0)
1471        };
1472
1473        // Sum interest payments from start to end
1474        let mut cum_int = 0.0;
1475        let mut balance = pv;
1476
1477        for period in 1..=end {
1478            let interest = if pay_type == 1 && period == 1 {
1479                0.0
1480            } else {
1481                balance * rate
1482            };
1483
1484            if period >= start {
1485                cum_int += interest;
1486            }
1487
1488            let principal = pmt - interest;
1489            balance += principal;
1490        }
1491
1492        Ok(CalcValue::Scalar(LiteralValue::Number(cum_int)))
1493    }
1494}
1495
1496/// Returns cumulative principal paid between two inclusive payment periods.
1497///
1498/// Use this to measure principal reduction over a selected amortization window.
1499///
1500/// # Remarks
1501/// - `rate` is the interest rate per payment period.
1502/// - `start_period` and `end_period` are 1-based, inclusive integer periods.
1503/// - `type` must be `0` (end-of-period) or `1` (beginning-of-period).
1504/// - Sign convention follows payment direction; with positive `pv`, cumulative principal is typically negative.
1505/// - Returns `#NUM!` for invalid domain values (non-positive rate, invalid ranges, invalid type, or non-positive `pv`).
1506///
1507/// # Examples
1508/// ```yaml,sandbox
1509/// formula: =CUMPRINC(0.06/12, 360, 300000, 1, 12, 0)
1510/// result: -38513.20398854517
1511/// ```
1512/// ```yaml,sandbox
1513/// formula: =CUMPRINC(0.06/12, 360, 300000, 13, 24, 0)
1514/// result: -36264.91124295984
1515/// ```
1516/// ```yaml,docs
1517/// related:
1518///   - PPMT
1519///   - PMT
1520///   - CUMIPMT
1521/// faq:
1522///   - q: "Why is `CUMPRINC` often negative for loans?"
1523///     a: "With positive `pv`, payment cash outflows are negative in this convention, so cumulative principal is typically negative."
1524/// ```
1525#[derive(Debug)]
1526pub struct CumprincFn;
1527/// [formualizer-docgen:schema:start]
1528/// Name: CUMPRINC
1529/// Type: CumprincFn
1530/// Min args: 6
1531/// Max args: 6
1532/// Variadic: false
1533/// Signature: CUMPRINC(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar, arg6: number@scalar)
1534/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg4{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg5{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg6{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1535/// Caps: PURE
1536/// [formualizer-docgen:schema:end]
1537impl Function for CumprincFn {
1538    func_caps!(PURE);
1539    fn name(&self) -> &'static str {
1540        "CUMPRINC"
1541    }
1542    fn min_args(&self) -> usize {
1543        6
1544    }
1545    fn arg_schema(&self) -> &'static [ArgSchema] {
1546        use std::sync::LazyLock;
1547        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
1548            vec![
1549                ArgSchema::number_lenient_scalar(),
1550                ArgSchema::number_lenient_scalar(),
1551                ArgSchema::number_lenient_scalar(),
1552                ArgSchema::number_lenient_scalar(),
1553                ArgSchema::number_lenient_scalar(),
1554                ArgSchema::number_lenient_scalar(),
1555            ]
1556        });
1557        &SCHEMA[..]
1558    }
1559    fn eval<'a, 'b, 'c>(
1560        &self,
1561        args: &'c [ArgumentHandle<'a, 'b>],
1562        _ctx: &dyn FunctionContext<'b>,
1563    ) -> Result<CalcValue<'b>, ExcelError> {
1564        let rate = coerce_num(&args[0])?;
1565        let nper = coerce_num(&args[1])?.trunc() as i32;
1566        let pv = coerce_num(&args[2])?;
1567        let start = coerce_num(&args[3])?.trunc() as i32;
1568        let end = coerce_num(&args[4])?.trunc() as i32;
1569        let pay_type = coerce_num(&args[5])?.trunc() as i32;
1570
1571        // Validation
1572        if rate <= 0.0
1573            || nper <= 0
1574            || pv <= 0.0
1575            || start < 1
1576            || end < start
1577            || end > nper
1578            || (pay_type != 0 && pay_type != 1)
1579        {
1580            return Ok(CalcValue::Scalar(
1581                LiteralValue::Error(ExcelError::new_num()),
1582            ));
1583        }
1584
1585        // Calculate PMT
1586        let pmt = if rate == 0.0 {
1587            -pv / nper as f64
1588        } else {
1589            -pv * rate * (1.0 + rate).powi(nper) / ((1.0 + rate).powi(nper) - 1.0)
1590        };
1591
1592        // Sum principal payments from start to end
1593        let mut cum_princ = 0.0;
1594        let mut balance = pv;
1595
1596        for period in 1..=end {
1597            let interest = if pay_type == 1 && period == 1 {
1598                0.0
1599            } else {
1600                balance * rate
1601            };
1602
1603            let principal = pmt - interest;
1604
1605            if period >= start {
1606                cum_princ += principal;
1607            }
1608
1609            balance += principal;
1610        }
1611
1612        Ok(CalcValue::Scalar(LiteralValue::Number(cum_princ)))
1613    }
1614}
1615
1616/// Calculates annualized net present value for irregularly dated cash flows.
1617///
1618/// Discounting uses an actual-day offset divided by 365 from the first provided date.
1619///
1620/// # Remarks
1621/// - `rate` is an annual discount rate.
1622/// - Cash-flow sign convention: outflows are negative and inflows are positive.
1623/// - `values` and `dates` are flattened to numeric entries; non-numeric entries are ignored.
1624/// - Scalar error inputs are propagated; callable inputs return `#CALC!`.
1625/// - Returns `#NUM!` when `values` and `dates` lengths differ or no numeric pair exists.
1626///
1627/// # Examples
1628/// ```yaml,sandbox
1629/// formula: =XNPV(0.10, {-10000,2750,4250,3250,2750}, {0,365,730,1095,1460})
1630/// result: 332.4567993989465
1631/// ```
1632/// ```yaml,sandbox
1633/// formula: =XNPV(0.08, {-5000,1200,1800,2400}, {0,180,365,730})
1634/// result: -120.41078799700836
1635/// ```
1636/// ```yaml,docs
1637/// related:
1638///   - NPV
1639///   - XIRR
1640///   - MIRR
1641/// faq:
1642///   - q: "How are dates interpreted in `XNPV`?"
1643///     a: "Each cash flow is discounted by `(date_i - first_date) / 365`, so dates must align one-to-one with values."
1644/// ```
1645#[derive(Debug)]
1646pub struct XnpvFn;
1647/// [formualizer-docgen:schema:start]
1648/// Name: XNPV
1649/// Type: XnpvFn
1650/// Min args: 3
1651/// Max args: 3
1652/// Variadic: false
1653/// Signature: XNPV(arg1: number@scalar, arg2: any@scalar, arg3: any@scalar)
1654/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg3{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
1655/// Caps: PURE
1656/// [formualizer-docgen:schema:end]
1657impl Function for XnpvFn {
1658    func_caps!(PURE);
1659    fn name(&self) -> &'static str {
1660        "XNPV"
1661    }
1662    fn min_args(&self) -> usize {
1663        3
1664    }
1665    fn arg_schema(&self) -> &'static [ArgSchema] {
1666        use std::sync::LazyLock;
1667        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
1668            vec![
1669                ArgSchema::number_lenient_scalar(), // rate
1670                ArgSchema::any(),                   // values
1671                ArgSchema::any(),                   // dates
1672            ]
1673        });
1674        &SCHEMA[..]
1675    }
1676    fn eval<'a, 'b, 'c>(
1677        &self,
1678        args: &'c [ArgumentHandle<'a, 'b>],
1679        _ctx: &dyn FunctionContext<'b>,
1680    ) -> Result<CalcValue<'b>, ExcelError> {
1681        let rate = coerce_num(&args[0])?;
1682
1683        // Collect values
1684        let mut values = Vec::new();
1685        let val = args[1].value()?;
1686        match val {
1687            CalcValue::Scalar(lit) => match lit {
1688                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
1689                LiteralValue::Array(arr) => {
1690                    for row in arr {
1691                        for cell in row {
1692                            if let Ok(n) = coerce_literal_num(&cell) {
1693                                values.push(n);
1694                            }
1695                        }
1696                    }
1697                }
1698                other => values.push(coerce_literal_num(&other)?),
1699            },
1700            CalcValue::Range(range) => {
1701                let (rows, cols) = range.dims();
1702                for r in 0..rows {
1703                    for c in 0..cols {
1704                        let cell = range.get_cell(r, c);
1705                        if let Ok(n) = coerce_literal_num(&cell) {
1706                            values.push(n);
1707                        }
1708                    }
1709                }
1710            }
1711            CalcValue::Callable(_) => {
1712                return Ok(CalcValue::Scalar(LiteralValue::Error(
1713                    ExcelError::new(ExcelErrorKind::Calc)
1714                        .with_message("LAMBDA value must be invoked"),
1715                )));
1716            }
1717        }
1718
1719        // Collect dates
1720        let mut dates = Vec::new();
1721        let date_val = args[2].value()?;
1722        match date_val {
1723            CalcValue::Scalar(lit) => match lit {
1724                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
1725                LiteralValue::Array(arr) => {
1726                    for row in arr {
1727                        for cell in row {
1728                            if let Ok(n) = coerce_literal_num(&cell) {
1729                                dates.push(n);
1730                            }
1731                        }
1732                    }
1733                }
1734                other => dates.push(coerce_literal_num(&other)?),
1735            },
1736            CalcValue::Range(range) => {
1737                let (rows, cols) = range.dims();
1738                for r in 0..rows {
1739                    for c in 0..cols {
1740                        let cell = range.get_cell(r, c);
1741                        if let Ok(n) = coerce_literal_num(&cell) {
1742                            dates.push(n);
1743                        }
1744                    }
1745                }
1746            }
1747            CalcValue::Callable(_) => {
1748                return Ok(CalcValue::Scalar(LiteralValue::Error(
1749                    ExcelError::new(ExcelErrorKind::Calc)
1750                        .with_message("LAMBDA value must be invoked"),
1751                )));
1752            }
1753        }
1754
1755        // Validate that values and dates have the same length
1756        if values.len() != dates.len() || values.is_empty() {
1757            return Ok(CalcValue::Scalar(
1758                LiteralValue::Error(ExcelError::new_num()),
1759            ));
1760        }
1761
1762        // Calculate XNPV: Sum of values[i] / (1 + rate)^((dates[i] - dates[0]) / 365)
1763        let first_date = dates[0];
1764        let mut xnpv = 0.0;
1765
1766        for (i, &value) in values.iter().enumerate() {
1767            let days_from_start = dates[i] - first_date;
1768            let years = days_from_start / 365.0;
1769            xnpv += value / (1.0 + rate).powf(years);
1770        }
1771
1772        Ok(CalcValue::Scalar(LiteralValue::Number(xnpv)))
1773    }
1774}
1775
1776/// Helper function to calculate XNPV given rate, values, and dates
1777fn calculate_xnpv(rate: f64, values: &[f64], dates: &[f64]) -> f64 {
1778    if values.is_empty() || dates.is_empty() {
1779        return 0.0;
1780    }
1781    let first_date = dates[0];
1782    let mut xnpv = 0.0;
1783    for (i, &value) in values.iter().enumerate() {
1784        let days_from_start = dates[i] - first_date;
1785        let years = days_from_start / 365.0;
1786        xnpv += value / (1.0 + rate).powf(years);
1787    }
1788    xnpv
1789}
1790
1791/// Helper function to calculate the derivative of XNPV with respect to rate
1792fn calculate_xnpv_derivative(rate: f64, values: &[f64], dates: &[f64]) -> f64 {
1793    if values.is_empty() || dates.is_empty() {
1794        return 0.0;
1795    }
1796    let first_date = dates[0];
1797    let mut d_xnpv = 0.0;
1798    for (i, &value) in values.iter().enumerate() {
1799        let days_from_start = dates[i] - first_date;
1800        let years = days_from_start / 365.0;
1801        // d/dr [value / (1+r)^years] = -years * value / (1+r)^(years+1)
1802        d_xnpv -= years * value / (1.0 + rate).powf(years + 1.0);
1803    }
1804    d_xnpv
1805}
1806
1807/// Calculates annualized internal rate of return for irregularly dated cash flows.
1808///
1809/// The solver uses Newton-Raphson on `XNPV(rate, values, dates) = 0` with day-count basis 365.
1810///
1811/// # Remarks
1812/// - Output is an annualized rate.
1813/// - Cash-flow sign convention requires at least one negative and one positive value.
1814/// - `guess` defaults to `0.1` and can materially affect convergence.
1815/// - Non-numeric entries in value/date arrays are ignored; callable inputs return `#CALC!`.
1816/// - Returns `#NUM!` for mismatched lengths, insufficient valid points, missing sign change, derivative failure, or non-convergence.
1817///
1818/// # Examples
1819/// ```yaml,sandbox
1820/// formula: =XIRR({-10000,2750,4250,3250,2750}, {0,365,730,1095,1460})
1821/// result: 0.11541278310055854
1822/// ```
1823/// ```yaml,sandbox
1824/// formula: =XIRR({-5000,1200,1800,2400}, {0,180,365,730}, 0.1)
1825/// result: 0.06001829492127762
1826/// ```
1827/// ```yaml,docs
1828/// related:
1829///   - XNPV
1830///   - IRR
1831///   - NPV
1832/// faq:
1833///   - q: "What data shape does `XIRR` require?"
1834///     a: "`values` and `dates` must have equal numeric length with at least one positive and one negative cash flow, or `#NUM!` is returned."
1835/// ```
1836#[derive(Debug)]
1837pub struct XirrFn;
1838/// [formualizer-docgen:schema:start]
1839/// Name: XIRR
1840/// Type: XirrFn
1841/// Min args: 2
1842/// Max args: variadic
1843/// Variadic: true
1844/// Signature: XIRR(arg1: any@scalar, arg2: any@scalar, arg3...: number@scalar)
1845/// Arg schema: arg1{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg2{kinds=any,required=true,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}; arg3{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
1846/// Caps: PURE
1847/// [formualizer-docgen:schema:end]
1848impl Function for XirrFn {
1849    func_caps!(PURE);
1850    fn name(&self) -> &'static str {
1851        "XIRR"
1852    }
1853    fn min_args(&self) -> usize {
1854        2
1855    }
1856    fn variadic(&self) -> bool {
1857        true
1858    }
1859    fn arg_schema(&self) -> &'static [ArgSchema] {
1860        use std::sync::LazyLock;
1861        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
1862            vec![
1863                ArgSchema::any(),                   // values
1864                ArgSchema::any(),                   // dates
1865                ArgSchema::number_lenient_scalar(), // guess (optional)
1866            ]
1867        });
1868        &SCHEMA[..]
1869    }
1870    fn eval<'a, 'b, 'c>(
1871        &self,
1872        args: &'c [ArgumentHandle<'a, 'b>],
1873        _ctx: &dyn FunctionContext<'b>,
1874    ) -> Result<CalcValue<'b>, ExcelError> {
1875        // Collect values
1876        let mut values = Vec::new();
1877        let val = args[0].value()?;
1878        match val {
1879            CalcValue::Scalar(lit) => match lit {
1880                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
1881                LiteralValue::Array(arr) => {
1882                    for row in arr {
1883                        for cell in row {
1884                            if let Ok(n) = coerce_literal_num(&cell) {
1885                                values.push(n);
1886                            }
1887                        }
1888                    }
1889                }
1890                other => values.push(coerce_literal_num(&other)?),
1891            },
1892            CalcValue::Range(range) => {
1893                let (rows, cols) = range.dims();
1894                for r in 0..rows {
1895                    for c in 0..cols {
1896                        let cell = range.get_cell(r, c);
1897                        if let Ok(n) = coerce_literal_num(&cell) {
1898                            values.push(n);
1899                        }
1900                    }
1901                }
1902            }
1903            CalcValue::Callable(_) => {
1904                return Ok(CalcValue::Scalar(LiteralValue::Error(
1905                    ExcelError::new(ExcelErrorKind::Calc)
1906                        .with_message("LAMBDA value must be invoked"),
1907                )));
1908            }
1909        }
1910
1911        // Collect dates
1912        let mut dates = Vec::new();
1913        let date_val = args[1].value()?;
1914        match date_val {
1915            CalcValue::Scalar(lit) => match lit {
1916                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
1917                LiteralValue::Array(arr) => {
1918                    for row in arr {
1919                        for cell in row {
1920                            if let Ok(n) = coerce_literal_num(&cell) {
1921                                dates.push(n);
1922                            }
1923                        }
1924                    }
1925                }
1926                other => dates.push(coerce_literal_num(&other)?),
1927            },
1928            CalcValue::Range(range) => {
1929                let (rows, cols) = range.dims();
1930                for r in 0..rows {
1931                    for c in 0..cols {
1932                        let cell = range.get_cell(r, c);
1933                        if let Ok(n) = coerce_literal_num(&cell) {
1934                            dates.push(n);
1935                        }
1936                    }
1937                }
1938            }
1939            CalcValue::Callable(_) => {
1940                return Ok(CalcValue::Scalar(LiteralValue::Error(
1941                    ExcelError::new(ExcelErrorKind::Calc)
1942                        .with_message("LAMBDA value must be invoked"),
1943                )));
1944            }
1945        }
1946
1947        // Validate
1948        if values.len() != dates.len() || values.len() < 2 {
1949            return Ok(CalcValue::Scalar(
1950                LiteralValue::Error(ExcelError::new_num()),
1951            ));
1952        }
1953
1954        // Check that we have at least one positive and one negative cash flow
1955        let has_positive = values.iter().any(|&v| v > 0.0);
1956        let has_negative = values.iter().any(|&v| v < 0.0);
1957        if !has_positive || !has_negative {
1958            return Ok(CalcValue::Scalar(
1959                LiteralValue::Error(ExcelError::new_num()),
1960            ));
1961        }
1962
1963        // Initial guess
1964        let guess = if args.len() > 2 {
1965            coerce_num(&args[2])?
1966        } else {
1967            0.1
1968        };
1969
1970        // Newton-Raphson iteration to find XIRR
1971        let mut rate = guess;
1972        const MAX_ITER: i32 = 100;
1973        const EPSILON: f64 = 1e-10;
1974
1975        for _ in 0..MAX_ITER {
1976            let xnpv = calculate_xnpv(rate, &values, &dates);
1977            let d_xnpv = calculate_xnpv_derivative(rate, &values, &dates);
1978
1979            if d_xnpv.abs() < EPSILON {
1980                return Ok(CalcValue::Scalar(
1981                    LiteralValue::Error(ExcelError::new_num()),
1982                ));
1983            }
1984
1985            let new_rate = rate - xnpv / d_xnpv;
1986
1987            if (new_rate - rate).abs() < EPSILON {
1988                return Ok(CalcValue::Scalar(LiteralValue::Number(new_rate)));
1989            }
1990
1991            rate = new_rate;
1992
1993            // Prevent rate from going too negative (would make (1+rate) negative)
1994            if rate <= -1.0 {
1995                rate = -0.99;
1996            }
1997        }
1998
1999        Ok(CalcValue::Scalar(
2000            LiteralValue::Error(ExcelError::new_num()),
2001        ))
2002    }
2003}
2004
2005/// Converts fractional-dollar notation into a decimal dollar value.
2006///
2007/// This is commonly used for security price formats such as thirty-seconds (`fraction = 32`).
2008///
2009/// # Remarks
2010/// - `fraction` is truncated to an integer denominator and must be `>= 1`.
2011/// - Sign convention: sign is preserved (`-x` maps to `-result`).
2012/// - No periodic rate is involved in this conversion.
2013/// - Returns `#NUM!` when `fraction < 1` after truncation.
2014/// - Fractional parsing uses denominator digit width (`ceil(log10(fraction))`).
2015///
2016/// # Examples
2017/// ```yaml,sandbox
2018/// formula: =DOLLARDE(1.02, 16)
2019/// result: 1.125
2020/// ```
2021/// ```yaml,sandbox
2022/// formula: =DOLLARDE(-3.15, 32)
2023/// result: -3.46875
2024/// ```
2025/// ```yaml,docs
2026/// related:
2027///   - DOLLARFR
2028/// faq:
2029///   - q: "Why does `DOLLARDE` truncate `fraction`?"
2030///     a: "The denominator is treated as an integer quote base; values below `1` after truncation return `#NUM!`."
2031/// ```
2032#[derive(Debug)]
2033pub struct DollardeFn;
2034/// [formualizer-docgen:schema:start]
2035/// Name: DOLLARDE
2036/// Type: DollardeFn
2037/// Min args: 2
2038/// Max args: 2
2039/// Variadic: false
2040/// Signature: DOLLARDE(arg1: number@scalar, arg2: number@scalar)
2041/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2042/// Caps: PURE
2043/// [formualizer-docgen:schema:end]
2044impl Function for DollardeFn {
2045    func_caps!(PURE);
2046    fn name(&self) -> &'static str {
2047        "DOLLARDE"
2048    }
2049    fn min_args(&self) -> usize {
2050        2
2051    }
2052    fn arg_schema(&self) -> &'static [ArgSchema] {
2053        use std::sync::LazyLock;
2054        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2055            vec![
2056                ArgSchema::number_lenient_scalar(), // fractional_dollar
2057                ArgSchema::number_lenient_scalar(), // fraction
2058            ]
2059        });
2060        &SCHEMA[..]
2061    }
2062    fn eval<'a, 'b, 'c>(
2063        &self,
2064        args: &'c [ArgumentHandle<'a, 'b>],
2065        _ctx: &dyn FunctionContext<'b>,
2066    ) -> Result<CalcValue<'b>, ExcelError> {
2067        let fractional_dollar = coerce_num(&args[0])?;
2068        let fraction = coerce_num(&args[1])?.trunc() as i32;
2069
2070        // Validate fraction
2071        if fraction < 1 {
2072            return Ok(CalcValue::Scalar(
2073                LiteralValue::Error(ExcelError::new_num()),
2074            ));
2075        }
2076
2077        // Determine how many decimal places are in the fractional part
2078        // The fractional part represents numerator / fraction
2079        let sign = if fractional_dollar < 0.0 { -1.0 } else { 1.0 };
2080        let abs_value = fractional_dollar.abs();
2081        let integer_part = abs_value.trunc();
2082        let fractional_part = abs_value - integer_part;
2083
2084        // Calculate the number of digits needed to represent the fraction denominator
2085        let digits = (fraction as f64).log10().ceil() as i32;
2086        let multiplier = 10_f64.powi(digits);
2087
2088        // The fractional part is scaled by the multiplier, then divided by the fraction
2089        let numerator = (fractional_part * multiplier).round();
2090        let decimal_fraction = numerator / fraction as f64;
2091
2092        let result = sign * (integer_part + decimal_fraction);
2093        Ok(CalcValue::Scalar(LiteralValue::Number(result)))
2094    }
2095}
2096
2097/// Converts a decimal dollar value into fractional-dollar notation.
2098///
2099/// This is the inverse-style formatting helper used for quoted fractional price conventions.
2100///
2101/// # Remarks
2102/// - `fraction` is truncated to an integer denominator and must be `>= 1`.
2103/// - Sign convention: sign is preserved (`-x` maps to `-result`).
2104/// - No periodic rate is involved in this conversion.
2105/// - Returns `#NUM!` when `fraction < 1` after truncation.
2106/// - Fraction output is encoded by denominator digit width (`ceil(log10(fraction))`).
2107///
2108/// # Examples
2109/// ```yaml,sandbox
2110/// formula: =DOLLARFR(1.125, 16)
2111/// result: 1.02
2112/// ```
2113/// ```yaml,sandbox
2114/// formula: =DOLLARFR(-3.46875, 32)
2115/// result: -3.15
2116/// ```
2117/// ```yaml,docs
2118/// related:
2119///   - DOLLARDE
2120/// faq:
2121///   - q: "How does `DOLLARFR` encode the fractional part?"
2122///     a: "It scales the numerator into decimal digits based on `ceil(log10(fraction))`, preserving the input sign."
2123/// ```
2124#[derive(Debug)]
2125pub struct DollarfrFn;
2126/// [formualizer-docgen:schema:start]
2127/// Name: DOLLARFR
2128/// Type: DollarfrFn
2129/// Min args: 2
2130/// Max args: 2
2131/// Variadic: false
2132/// Signature: DOLLARFR(arg1: number@scalar, arg2: number@scalar)
2133/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberLenientText,max=None,repeating=None,default=false}
2134/// Caps: PURE
2135/// [formualizer-docgen:schema:end]
2136impl Function for DollarfrFn {
2137    func_caps!(PURE);
2138    fn name(&self) -> &'static str {
2139        "DOLLARFR"
2140    }
2141    fn min_args(&self) -> usize {
2142        2
2143    }
2144    fn arg_schema(&self) -> &'static [ArgSchema] {
2145        use std::sync::LazyLock;
2146        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2147            vec![
2148                ArgSchema::number_lenient_scalar(), // decimal_dollar
2149                ArgSchema::number_lenient_scalar(), // fraction
2150            ]
2151        });
2152        &SCHEMA[..]
2153    }
2154    fn eval<'a, 'b, 'c>(
2155        &self,
2156        args: &'c [ArgumentHandle<'a, 'b>],
2157        _ctx: &dyn FunctionContext<'b>,
2158    ) -> Result<CalcValue<'b>, ExcelError> {
2159        let decimal_dollar = coerce_num(&args[0])?;
2160        let fraction = coerce_num(&args[1])?.trunc() as i32;
2161
2162        // Validate fraction
2163        if fraction < 1 {
2164            return Ok(CalcValue::Scalar(
2165                LiteralValue::Error(ExcelError::new_num()),
2166            ));
2167        }
2168
2169        let sign = if decimal_dollar < 0.0 { -1.0 } else { 1.0 };
2170        let abs_value = decimal_dollar.abs();
2171        let integer_part = abs_value.trunc();
2172        let decimal_part = abs_value - integer_part;
2173
2174        // Convert decimal fraction to fractional representation
2175        // numerator = decimal_part * fraction
2176        let numerator = decimal_part * fraction as f64;
2177
2178        // Calculate the number of digits needed to represent the fraction denominator
2179        let digits = (fraction as f64).log10().ceil() as i32;
2180        let divisor = 10_f64.powi(digits);
2181
2182        // The fractional dollar format puts the numerator after the decimal point
2183        let result = sign * (integer_part + numerator / divisor);
2184        Ok(CalcValue::Scalar(LiteralValue::Number(result)))
2185    }
2186}
2187
2188pub fn register_builtins() {
2189    use std::sync::Arc;
2190    crate::function_registry::register_function(Arc::new(PmtFn));
2191    crate::function_registry::register_function(Arc::new(PvFn));
2192    crate::function_registry::register_function(Arc::new(FvFn));
2193    crate::function_registry::register_function(Arc::new(NpvFn));
2194    crate::function_registry::register_function(Arc::new(NperFn));
2195    crate::function_registry::register_function(Arc::new(RateFn));
2196    crate::function_registry::register_function(Arc::new(IpmtFn));
2197    crate::function_registry::register_function(Arc::new(PpmtFn));
2198    crate::function_registry::register_function(Arc::new(EffectFn));
2199    crate::function_registry::register_function(Arc::new(NominalFn));
2200    crate::function_registry::register_function(Arc::new(IrrFn));
2201    crate::function_registry::register_function(Arc::new(MirrFn));
2202    crate::function_registry::register_function(Arc::new(CumipmtFn));
2203    crate::function_registry::register_function(Arc::new(CumprincFn));
2204    crate::function_registry::register_function(Arc::new(XnpvFn));
2205    crate::function_registry::register_function(Arc::new(XirrFn));
2206    crate::function_registry::register_function(Arc::new(DollardeFn));
2207    crate::function_registry::register_function(Arc::new(DollarfrFn));
2208}