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/// Compute NPV at a given rate.
1092fn irr_npv(cashflows: &[f64], rate: f64) -> f64 {
1093    let mut npv = 0.0;
1094    for (i, &cf) in cashflows.iter().enumerate() {
1095        npv += cf / (1.0 + rate).powi(i as i32);
1096    }
1097    npv
1098}
1099
1100/// Compute NPV and its derivative w.r.t. rate.
1101fn irr_npv_deriv(cashflows: &[f64], rate: f64) -> (f64, f64) {
1102    let mut npv = 0.0;
1103    let mut d_npv = 0.0;
1104    for (i, &cf) in cashflows.iter().enumerate() {
1105        let factor = (1.0 + rate).powi(i as i32);
1106        npv += cf / factor;
1107        if i > 0 {
1108            d_npv -= (i as f64) * cf / (factor * (1.0 + rate));
1109        }
1110    }
1111    (npv, d_npv)
1112}
1113
1114/// Solve for IRR using Newton-Raphson with Brent's method fallback.
1115///
1116/// Strategy:
1117/// 1. Try Newton-Raphson from the user's guess (fast when it works).
1118/// 2. If Newton diverges, bracket the root by scanning probe points,
1119///    then use Brent's method (superlinear convergence with guaranteed
1120///    bracketing) to find the root precisely.
1121fn irr_solve(cashflows: &[f64], guess: f64) -> Option<f64> {
1122    const MAX_NR: usize = 100;
1123    const MAX_BRENT: usize = 200;
1124    const TOL: f64 = 1e-12;
1125    const MACH_EPS: f64 = f64::EPSILON;
1126
1127    // --- Phase 1: Newton-Raphson from the given guess ---
1128    let mut rate = guess;
1129    for _ in 0..MAX_NR {
1130        let (npv, d_npv) = irr_npv_deriv(cashflows, rate);
1131        if d_npv.abs() < TOL {
1132            break; // flat derivative, fall through to Brent
1133        }
1134        let new_rate = rate - npv / d_npv;
1135        // Accept if converged and rate > -1 (pole at -1)
1136        if (new_rate - rate).abs() < TOL && new_rate > -1.0 {
1137            return Some(new_rate);
1138        }
1139        // If Newton shoots below -1 or to NaN/Inf, bail out
1140        if new_rate <= -1.0 || !new_rate.is_finite() {
1141            break;
1142        }
1143        rate = new_rate;
1144    }
1145
1146    // --- Phase 2: Bracket the root, then apply Brent's method ---
1147    // Search for a sign change in NPV across a wide range of rates.
1148    let probes: &[f64] = &[
1149        -0.99, -0.9, -0.5, -0.1, -0.01, 0.0, 0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.15, 0.2, 0.3,
1150        0.5, 1.0, 2.0, 5.0, 10.0,
1151    ];
1152    let mut lo = f64::NAN;
1153    let mut hi = f64::NAN;
1154    let mut npv_lo = f64::NAN;
1155
1156    for &r in probes {
1157        let npv = irr_npv(cashflows, r);
1158        if !npv.is_finite() {
1159            continue;
1160        }
1161        if lo.is_nan() {
1162            lo = r;
1163            npv_lo = npv;
1164            continue;
1165        }
1166        if npv_lo * npv < 0.0 {
1167            hi = r;
1168            break;
1169        }
1170        lo = r;
1171        npv_lo = npv;
1172    }
1173
1174    if hi.is_nan() {
1175        return None; // no sign change found — no real IRR
1176    }
1177
1178    // Brent's method (following scipy's brentq / Brent's zeroin algorithm).
1179    // Combines inverse quadratic interpolation, secant, and bisection.
1180    // xpre/xcur maintain the bracket; xblk is the contrapoint.
1181    let mut xpre = lo;
1182    let mut xcur = hi;
1183    let mut fpre = irr_npv(cashflows, xpre);
1184    let mut fcur = irr_npv(cashflows, xcur);
1185
1186    if fpre == 0.0 {
1187        return Some(xpre);
1188    }
1189    if fcur == 0.0 {
1190        return Some(xcur);
1191    }
1192
1193    let mut xblk = 0.0;
1194    let mut fblk = 0.0;
1195    let mut spre = 0.0;
1196    let mut scur = 0.0;
1197
1198    for _ in 0..MAX_BRENT {
1199        // If xpre and xcur bracket the root, reset the contrapoint
1200        if fpre * fcur < 0.0 {
1201            xblk = xpre;
1202            fblk = fpre;
1203            spre = xcur - xpre;
1204            scur = spre;
1205        }
1206
1207        // Ensure xcur is the best approximation (|fcur| <= |fblk|)
1208        if fblk.abs() < fcur.abs() {
1209            xpre = xcur;
1210            xcur = xblk;
1211            xblk = xpre;
1212            fpre = fcur;
1213            fcur = fblk;
1214            fblk = fpre;
1215        }
1216
1217        let delta = (MACH_EPS * xcur.abs() + 0.5 * TOL).max(MACH_EPS);
1218        let sbis = (xblk - xcur) * 0.5;
1219
1220        if fcur == 0.0 || sbis.abs() < delta {
1221            return Some(xcur);
1222        }
1223
1224        if spre.abs() >= delta && fcur.abs() < fpre.abs() {
1225            // Try interpolation
1226            let stry = if (xpre - xblk).abs() < MACH_EPS {
1227                // Secant step
1228                -fcur * (xcur - xpre) / (fcur - fpre)
1229            } else {
1230                // Inverse quadratic interpolation
1231                let dpre = (fpre - fcur) / (xpre - xcur);
1232                let dblk = (fblk - fcur) / (xblk - xcur);
1233                -fcur * (fblk * dblk - fpre * dpre) / (dblk * dpre * (fblk - fpre))
1234            };
1235
1236            // Accept if step is small enough
1237            if 2.0 * stry.abs() < spre.abs().min(3.0 * sbis.abs() - delta) {
1238                spre = scur;
1239                scur = stry;
1240            } else {
1241                spre = sbis;
1242                scur = sbis;
1243            }
1244        } else {
1245            // Bisection
1246            spre = sbis;
1247            scur = sbis;
1248        }
1249
1250        xpre = xcur;
1251        fpre = fcur;
1252
1253        if scur.abs() > delta {
1254            xcur += scur;
1255        } else {
1256            xcur += if sbis > 0.0 { delta } else { -delta };
1257        }
1258        fcur = irr_npv(cashflows, xcur);
1259    }
1260    Some(xcur)
1261}
1262
1263/// Calculates periodic internal rate of return for regularly spaced cash flows.
1264///
1265/// The function iteratively finds the per-period rate where discounted cash flows sum to zero.
1266///
1267/// # Remarks
1268/// - Output is a rate per cash-flow period (not automatically annualized).
1269/// - Cash-flow sign convention: outflows are negative and inflows are positive.
1270/// - Non-numeric cells in arrays/ranges are ignored; direct scalar errors are propagated.
1271/// - A callable value input returns `#CALC!`.
1272/// - Returns `#NUM!` if fewer than two numeric cash flows are available, if derivative is near zero, or if iteration does not converge.
1273///
1274/// # Examples
1275/// ```yaml,sandbox
1276/// formula: =IRR({-10000,3000,4200,6800})
1277/// result: 0.16340560068898924
1278/// ```
1279/// ```yaml,sandbox
1280/// formula: =IRR({-5000,1200,1410,1875,1050}, 0.1)
1281/// result: 0.041848876015677466
1282/// ```
1283/// ```yaml,docs
1284/// related:
1285///   - MIRR
1286///   - NPV
1287///   - XIRR
1288/// faq:
1289///   - q: "Why can `IRR` return `#NUM!` even with numeric cash flows?"
1290///     a: "The Newton solve can fail if derivative terms become unstable or no convergent root is reached from the chosen guess."
1291/// ```
1292#[derive(Debug)]
1293pub struct IrrFn;
1294/// [formualizer-docgen:schema:start]
1295/// Name: IRR
1296/// Type: IrrFn
1297/// Min args: 1
1298/// Max args: variadic
1299/// Variadic: true
1300/// Signature: IRR(arg1: any@scalar, arg2...: number@scalar)
1301/// 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}
1302/// Caps: PURE
1303/// [formualizer-docgen:schema:end]
1304impl Function for IrrFn {
1305    func_caps!(PURE);
1306    fn name(&self) -> &'static str {
1307        "IRR"
1308    }
1309    fn min_args(&self) -> usize {
1310        1
1311    }
1312    fn variadic(&self) -> bool {
1313        true
1314    }
1315    fn arg_schema(&self) -> &'static [ArgSchema] {
1316        use std::sync::LazyLock;
1317        static SCHEMA: LazyLock<Vec<ArgSchema>> =
1318            LazyLock::new(|| vec![ArgSchema::any(), ArgSchema::number_lenient_scalar()]);
1319        &SCHEMA[..]
1320    }
1321    fn eval<'a, 'b, 'c>(
1322        &self,
1323        args: &'c [ArgumentHandle<'a, 'b>],
1324        _ctx: &dyn FunctionContext<'b>,
1325    ) -> Result<CalcValue<'b>, ExcelError> {
1326        // Collect cash flows
1327        let mut cashflows = Vec::new();
1328        let val = args[0].value()?;
1329        match val {
1330            CalcValue::Scalar(lit) => match lit {
1331                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
1332                LiteralValue::Array(arr) => {
1333                    for row in arr {
1334                        for cell in row {
1335                            if let Ok(n) = coerce_literal_num(&cell) {
1336                                cashflows.push(n);
1337                            }
1338                        }
1339                    }
1340                }
1341                other => cashflows.push(coerce_literal_num(&other)?),
1342            },
1343            CalcValue::Range(range) => {
1344                let (rows, cols) = range.dims();
1345                for r in 0..rows {
1346                    for c in 0..cols {
1347                        let cell = range.get_cell(r, c);
1348                        if let Ok(n) = coerce_literal_num(&cell) {
1349                            cashflows.push(n);
1350                        }
1351                    }
1352                }
1353            }
1354            CalcValue::Callable(_) => {
1355                return Ok(CalcValue::Scalar(LiteralValue::Error(
1356                    ExcelError::new(ExcelErrorKind::Calc)
1357                        .with_message("LAMBDA value must be invoked"),
1358                )));
1359            }
1360        }
1361
1362        if cashflows.len() < 2 {
1363            return Ok(CalcValue::Scalar(
1364                LiteralValue::Error(ExcelError::new_num()),
1365            ));
1366        }
1367
1368        // Initial guess
1369        let guess = if args.len() > 1 {
1370            coerce_num(&args[1])?
1371        } else {
1372            0.1
1373        };
1374
1375        match irr_solve(&cashflows, guess) {
1376            Some(rate) => Ok(CalcValue::Scalar(LiteralValue::Number(rate))),
1377            None => Ok(CalcValue::Scalar(
1378                LiteralValue::Error(ExcelError::new_num()),
1379            )),
1380        }
1381    }
1382}
1383
1384/// Calculates modified internal rate of return with separate finance and reinvest rates.
1385///
1386/// Negative cash flows are discounted at `finance_rate` and positive cash flows are compounded at
1387/// `reinvest_rate`, then combined into a single periodic return.
1388///
1389/// # Remarks
1390/// - `finance_rate` and `reinvest_rate` are both rates per cash-flow period.
1391/// - Cash-flow sign convention: at least one negative and one positive cash flow are required.
1392/// - Non-numeric cells in arrays/ranges are ignored; direct scalar errors are propagated.
1393/// - A callable value input returns `#CALC!`.
1394/// - Returns `#NUM!` for insufficient cash flows, and `#DIV/0!` when computed positive/negative legs are invalid.
1395///
1396/// # Examples
1397/// ```yaml,sandbox
1398/// formula: =MIRR({-10000,3000,4200,6800}, 0.1, 0.12)
1399/// result: 0.15147133664676304
1400/// ```
1401/// ```yaml,sandbox
1402/// formula: =MIRR({-120000,39000,30000,21000,37000,46000}, 0.1, 0.12)
1403/// result: 0.1260941303659051
1404/// ```
1405/// ```yaml,docs
1406/// related:
1407///   - IRR
1408///   - NPV
1409///   - XNPV
1410/// faq:
1411///   - q: "Why does `MIRR` return `#DIV/0!` for some cash-flow sets?"
1412///     a: "`MIRR` needs both a negative leg and a positive leg; if discounted negatives or compounded positives are invalid, it returns `#DIV/0!`."
1413/// ```
1414#[derive(Debug)]
1415pub struct MirrFn;
1416/// [formualizer-docgen:schema:start]
1417/// Name: MIRR
1418/// Type: MirrFn
1419/// Min args: 3
1420/// Max args: 3
1421/// Variadic: false
1422/// Signature: MIRR(arg1: any@scalar, arg2: number@scalar, arg3: number@scalar)
1423/// 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}
1424/// Caps: PURE
1425/// [formualizer-docgen:schema:end]
1426impl Function for MirrFn {
1427    func_caps!(PURE);
1428    fn name(&self) -> &'static str {
1429        "MIRR"
1430    }
1431    fn min_args(&self) -> usize {
1432        3
1433    }
1434    fn arg_schema(&self) -> &'static [ArgSchema] {
1435        use std::sync::LazyLock;
1436        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
1437            vec![
1438                ArgSchema::any(),
1439                ArgSchema::number_lenient_scalar(),
1440                ArgSchema::number_lenient_scalar(),
1441            ]
1442        });
1443        &SCHEMA[..]
1444    }
1445    fn eval<'a, 'b, 'c>(
1446        &self,
1447        args: &'c [ArgumentHandle<'a, 'b>],
1448        _ctx: &dyn FunctionContext<'b>,
1449    ) -> Result<CalcValue<'b>, ExcelError> {
1450        // Collect cash flows
1451        let mut cashflows = Vec::new();
1452        let val = args[0].value()?;
1453        match val {
1454            CalcValue::Scalar(lit) => match lit {
1455                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
1456                LiteralValue::Array(arr) => {
1457                    for row in arr {
1458                        for cell in row {
1459                            if let Ok(n) = coerce_literal_num(&cell) {
1460                                cashflows.push(n);
1461                            }
1462                        }
1463                    }
1464                }
1465                other => cashflows.push(coerce_literal_num(&other)?),
1466            },
1467            CalcValue::Range(range) => {
1468                let (rows, cols) = range.dims();
1469                for r in 0..rows {
1470                    for c in 0..cols {
1471                        let cell = range.get_cell(r, c);
1472                        if let Ok(n) = coerce_literal_num(&cell) {
1473                            cashflows.push(n);
1474                        }
1475                    }
1476                }
1477            }
1478            CalcValue::Callable(_) => {
1479                return Ok(CalcValue::Scalar(LiteralValue::Error(
1480                    ExcelError::new(ExcelErrorKind::Calc)
1481                        .with_message("LAMBDA value must be invoked"),
1482                )));
1483            }
1484        }
1485
1486        let finance_rate = coerce_num(&args[1])?;
1487        let reinvest_rate = coerce_num(&args[2])?;
1488
1489        if cashflows.len() < 2 {
1490            return Ok(CalcValue::Scalar(
1491                LiteralValue::Error(ExcelError::new_num()),
1492            ));
1493        }
1494
1495        let n = cashflows.len() as i32;
1496
1497        // Present value of negative cash flows (discounted at finance_rate)
1498        let mut pv_neg = 0.0;
1499        // Future value of positive cash flows (compounded at reinvest_rate)
1500        let mut fv_pos = 0.0;
1501
1502        for (i, &cf) in cashflows.iter().enumerate() {
1503            if cf < 0.0 {
1504                pv_neg += cf / (1.0 + finance_rate).powi(i as i32);
1505            } else {
1506                fv_pos += cf * (1.0 + reinvest_rate).powi(n - 1 - i as i32);
1507            }
1508        }
1509
1510        if pv_neg >= 0.0 || fv_pos <= 0.0 {
1511            return Ok(CalcValue::Scalar(
1512                LiteralValue::Error(ExcelError::new_div()),
1513            ));
1514        }
1515
1516        // MIRR = (FV_pos / -PV_neg)^(1/(n-1)) - 1
1517        let mirr = (-fv_pos / pv_neg).powf(1.0 / (n - 1) as f64) - 1.0;
1518        Ok(CalcValue::Scalar(LiteralValue::Number(mirr)))
1519    }
1520}
1521
1522/// Returns cumulative interest paid between two inclusive payment periods.
1523///
1524/// Use this to total the interest component over a slice of an amortization schedule.
1525///
1526/// # Remarks
1527/// - `rate` is the interest rate per payment period.
1528/// - `start_period` and `end_period` are 1-based, inclusive integer periods.
1529/// - `type` must be `0` (end-of-period) or `1` (beginning-of-period).
1530/// - Sign convention follows this implementation's balance model; with positive `pv`, cumulative interest is typically positive.
1531/// - Returns `#NUM!` for invalid domain values (non-positive rate, invalid ranges, invalid type, or non-positive `pv`).
1532///
1533/// # Examples
1534/// ```yaml,sandbox
1535/// formula: =CUMIPMT(0.06/12, 360, 300000, 1, 12, 0)
1536/// result: 16929.385083045923
1537/// ```
1538/// ```yaml,sandbox
1539/// formula: =CUMIPMT(0.06/12, 360, 300000, 13, 24, 0)
1540/// result: 14681.09233746059
1541/// ```
1542/// ```yaml,docs
1543/// related:
1544///   - IPMT
1545///   - PMT
1546///   - CUMPRINC
1547/// faq:
1548///   - q: "Are `start_period` and `end_period` inclusive in `CUMIPMT`?"
1549///     a: "Yes. Both bounds are inclusive and interpreted as 1-based periods after truncation to integers."
1550/// ```
1551#[derive(Debug)]
1552pub struct CumipmtFn;
1553/// [formualizer-docgen:schema:start]
1554/// Name: CUMIPMT
1555/// Type: CumipmtFn
1556/// Min args: 6
1557/// Max args: 6
1558/// Variadic: false
1559/// Signature: CUMIPMT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar, arg6: number@scalar)
1560/// 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}
1561/// Caps: PURE
1562/// [formualizer-docgen:schema:end]
1563impl Function for CumipmtFn {
1564    func_caps!(PURE);
1565    fn name(&self) -> &'static str {
1566        "CUMIPMT"
1567    }
1568    fn min_args(&self) -> usize {
1569        6
1570    }
1571    fn arg_schema(&self) -> &'static [ArgSchema] {
1572        use std::sync::LazyLock;
1573        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
1574            vec![
1575                ArgSchema::number_lenient_scalar(),
1576                ArgSchema::number_lenient_scalar(),
1577                ArgSchema::number_lenient_scalar(),
1578                ArgSchema::number_lenient_scalar(),
1579                ArgSchema::number_lenient_scalar(),
1580                ArgSchema::number_lenient_scalar(),
1581            ]
1582        });
1583        &SCHEMA[..]
1584    }
1585    fn eval<'a, 'b, 'c>(
1586        &self,
1587        args: &'c [ArgumentHandle<'a, 'b>],
1588        _ctx: &dyn FunctionContext<'b>,
1589    ) -> Result<CalcValue<'b>, ExcelError> {
1590        let rate = coerce_num(&args[0])?;
1591        let nper = coerce_num(&args[1])?.trunc() as i32;
1592        let pv = coerce_num(&args[2])?;
1593        let start = coerce_num(&args[3])?.trunc() as i32;
1594        let end = coerce_num(&args[4])?.trunc() as i32;
1595        let pay_type = coerce_num(&args[5])?.trunc() as i32;
1596
1597        // Validation
1598        if rate <= 0.0
1599            || nper <= 0
1600            || pv <= 0.0
1601            || start < 1
1602            || end < start
1603            || end > nper
1604            || (pay_type != 0 && pay_type != 1)
1605        {
1606            return Ok(CalcValue::Scalar(
1607                LiteralValue::Error(ExcelError::new_num()),
1608            ));
1609        }
1610
1611        // Calculate PMT
1612        let pmt = if rate == 0.0 {
1613            -pv / nper as f64
1614        } else {
1615            -pv * rate * (1.0 + rate).powi(nper) / ((1.0 + rate).powi(nper) - 1.0)
1616        };
1617
1618        // Sum interest payments from start to end
1619        let mut cum_int = 0.0;
1620        let mut balance = pv;
1621
1622        for period in 1..=end {
1623            let interest = if pay_type == 1 && period == 1 {
1624                0.0
1625            } else {
1626                balance * rate
1627            };
1628
1629            if period >= start {
1630                cum_int += interest;
1631            }
1632
1633            let principal = pmt - interest;
1634            balance += principal;
1635        }
1636
1637        Ok(CalcValue::Scalar(LiteralValue::Number(cum_int)))
1638    }
1639}
1640
1641/// Returns cumulative principal paid between two inclusive payment periods.
1642///
1643/// Use this to measure principal reduction over a selected amortization window.
1644///
1645/// # Remarks
1646/// - `rate` is the interest rate per payment period.
1647/// - `start_period` and `end_period` are 1-based, inclusive integer periods.
1648/// - `type` must be `0` (end-of-period) or `1` (beginning-of-period).
1649/// - Sign convention follows payment direction; with positive `pv`, cumulative principal is typically negative.
1650/// - Returns `#NUM!` for invalid domain values (non-positive rate, invalid ranges, invalid type, or non-positive `pv`).
1651///
1652/// # Examples
1653/// ```yaml,sandbox
1654/// formula: =CUMPRINC(0.06/12, 360, 300000, 1, 12, 0)
1655/// result: -38513.20398854517
1656/// ```
1657/// ```yaml,sandbox
1658/// formula: =CUMPRINC(0.06/12, 360, 300000, 13, 24, 0)
1659/// result: -36264.91124295984
1660/// ```
1661/// ```yaml,docs
1662/// related:
1663///   - PPMT
1664///   - PMT
1665///   - CUMIPMT
1666/// faq:
1667///   - q: "Why is `CUMPRINC` often negative for loans?"
1668///     a: "With positive `pv`, payment cash outflows are negative in this convention, so cumulative principal is typically negative."
1669/// ```
1670#[derive(Debug)]
1671pub struct CumprincFn;
1672/// [formualizer-docgen:schema:start]
1673/// Name: CUMPRINC
1674/// Type: CumprincFn
1675/// Min args: 6
1676/// Max args: 6
1677/// Variadic: false
1678/// Signature: CUMPRINC(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar, arg5: number@scalar, arg6: number@scalar)
1679/// 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}
1680/// Caps: PURE
1681/// [formualizer-docgen:schema:end]
1682impl Function for CumprincFn {
1683    func_caps!(PURE);
1684    fn name(&self) -> &'static str {
1685        "CUMPRINC"
1686    }
1687    fn min_args(&self) -> usize {
1688        6
1689    }
1690    fn arg_schema(&self) -> &'static [ArgSchema] {
1691        use std::sync::LazyLock;
1692        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
1693            vec![
1694                ArgSchema::number_lenient_scalar(),
1695                ArgSchema::number_lenient_scalar(),
1696                ArgSchema::number_lenient_scalar(),
1697                ArgSchema::number_lenient_scalar(),
1698                ArgSchema::number_lenient_scalar(),
1699                ArgSchema::number_lenient_scalar(),
1700            ]
1701        });
1702        &SCHEMA[..]
1703    }
1704    fn eval<'a, 'b, 'c>(
1705        &self,
1706        args: &'c [ArgumentHandle<'a, 'b>],
1707        _ctx: &dyn FunctionContext<'b>,
1708    ) -> Result<CalcValue<'b>, ExcelError> {
1709        let rate = coerce_num(&args[0])?;
1710        let nper = coerce_num(&args[1])?.trunc() as i32;
1711        let pv = coerce_num(&args[2])?;
1712        let start = coerce_num(&args[3])?.trunc() as i32;
1713        let end = coerce_num(&args[4])?.trunc() as i32;
1714        let pay_type = coerce_num(&args[5])?.trunc() as i32;
1715
1716        // Validation
1717        if rate <= 0.0
1718            || nper <= 0
1719            || pv <= 0.0
1720            || start < 1
1721            || end < start
1722            || end > nper
1723            || (pay_type != 0 && pay_type != 1)
1724        {
1725            return Ok(CalcValue::Scalar(
1726                LiteralValue::Error(ExcelError::new_num()),
1727            ));
1728        }
1729
1730        // Calculate PMT
1731        let pmt = if rate == 0.0 {
1732            -pv / nper as f64
1733        } else {
1734            -pv * rate * (1.0 + rate).powi(nper) / ((1.0 + rate).powi(nper) - 1.0)
1735        };
1736
1737        // Sum principal payments from start to end
1738        let mut cum_princ = 0.0;
1739        let mut balance = pv;
1740
1741        for period in 1..=end {
1742            let interest = if pay_type == 1 && period == 1 {
1743                0.0
1744            } else {
1745                balance * rate
1746            };
1747
1748            let principal = pmt - interest;
1749
1750            if period >= start {
1751                cum_princ += principal;
1752            }
1753
1754            balance += principal;
1755        }
1756
1757        Ok(CalcValue::Scalar(LiteralValue::Number(cum_princ)))
1758    }
1759}
1760
1761/// Calculates annualized net present value for irregularly dated cash flows.
1762///
1763/// Discounting uses an actual-day offset divided by 365 from the first provided date.
1764///
1765/// # Remarks
1766/// - `rate` is an annual discount rate.
1767/// - Cash-flow sign convention: outflows are negative and inflows are positive.
1768/// - `values` and `dates` are flattened to numeric entries; non-numeric entries are ignored.
1769/// - Scalar error inputs are propagated; callable inputs return `#CALC!`.
1770/// - Returns `#NUM!` when `values` and `dates` lengths differ or no numeric pair exists.
1771///
1772/// # Examples
1773/// ```yaml,sandbox
1774/// formula: =XNPV(0.10, {-10000,2750,4250,3250,2750}, {0,365,730,1095,1460})
1775/// result: 332.4567993989465
1776/// ```
1777/// ```yaml,sandbox
1778/// formula: =XNPV(0.08, {-5000,1200,1800,2400}, {0,180,365,730})
1779/// result: -120.41078799700836
1780/// ```
1781/// ```yaml,docs
1782/// related:
1783///   - NPV
1784///   - XIRR
1785///   - MIRR
1786/// faq:
1787///   - q: "How are dates interpreted in `XNPV`?"
1788///     a: "Each cash flow is discounted by `(date_i - first_date) / 365`, so dates must align one-to-one with values."
1789/// ```
1790#[derive(Debug)]
1791pub struct XnpvFn;
1792/// [formualizer-docgen:schema:start]
1793/// Name: XNPV
1794/// Type: XnpvFn
1795/// Min args: 3
1796/// Max args: 3
1797/// Variadic: false
1798/// Signature: XNPV(arg1: number@scalar, arg2: any@scalar, arg3: any@scalar)
1799/// 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}
1800/// Caps: PURE
1801/// [formualizer-docgen:schema:end]
1802impl Function for XnpvFn {
1803    func_caps!(PURE);
1804    fn name(&self) -> &'static str {
1805        "XNPV"
1806    }
1807    fn min_args(&self) -> usize {
1808        3
1809    }
1810    fn arg_schema(&self) -> &'static [ArgSchema] {
1811        use std::sync::LazyLock;
1812        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
1813            vec![
1814                ArgSchema::number_lenient_scalar(), // rate
1815                ArgSchema::any(),                   // values
1816                ArgSchema::any(),                   // dates
1817            ]
1818        });
1819        &SCHEMA[..]
1820    }
1821    fn eval<'a, 'b, 'c>(
1822        &self,
1823        args: &'c [ArgumentHandle<'a, 'b>],
1824        _ctx: &dyn FunctionContext<'b>,
1825    ) -> Result<CalcValue<'b>, ExcelError> {
1826        let rate = coerce_num(&args[0])?;
1827
1828        // Collect values
1829        let mut values = Vec::new();
1830        let val = args[1].value()?;
1831        match val {
1832            CalcValue::Scalar(lit) => match lit {
1833                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
1834                LiteralValue::Array(arr) => {
1835                    for row in arr {
1836                        for cell in row {
1837                            if let Ok(n) = coerce_literal_num(&cell) {
1838                                values.push(n);
1839                            }
1840                        }
1841                    }
1842                }
1843                other => values.push(coerce_literal_num(&other)?),
1844            },
1845            CalcValue::Range(range) => {
1846                let (rows, cols) = range.dims();
1847                for r in 0..rows {
1848                    for c in 0..cols {
1849                        let cell = range.get_cell(r, c);
1850                        if let Ok(n) = coerce_literal_num(&cell) {
1851                            values.push(n);
1852                        }
1853                    }
1854                }
1855            }
1856            CalcValue::Callable(_) => {
1857                return Ok(CalcValue::Scalar(LiteralValue::Error(
1858                    ExcelError::new(ExcelErrorKind::Calc)
1859                        .with_message("LAMBDA value must be invoked"),
1860                )));
1861            }
1862        }
1863
1864        // Collect dates
1865        let mut dates = Vec::new();
1866        let date_val = args[2].value()?;
1867        match date_val {
1868            CalcValue::Scalar(lit) => match lit {
1869                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
1870                LiteralValue::Array(arr) => {
1871                    for row in arr {
1872                        for cell in row {
1873                            if let Ok(n) = coerce_literal_num(&cell) {
1874                                dates.push(n);
1875                            }
1876                        }
1877                    }
1878                }
1879                other => dates.push(coerce_literal_num(&other)?),
1880            },
1881            CalcValue::Range(range) => {
1882                let (rows, cols) = range.dims();
1883                for r in 0..rows {
1884                    for c in 0..cols {
1885                        let cell = range.get_cell(r, c);
1886                        if let Ok(n) = coerce_literal_num(&cell) {
1887                            dates.push(n);
1888                        }
1889                    }
1890                }
1891            }
1892            CalcValue::Callable(_) => {
1893                return Ok(CalcValue::Scalar(LiteralValue::Error(
1894                    ExcelError::new(ExcelErrorKind::Calc)
1895                        .with_message("LAMBDA value must be invoked"),
1896                )));
1897            }
1898        }
1899
1900        // Validate that values and dates have the same length
1901        if values.len() != dates.len() || values.is_empty() {
1902            return Ok(CalcValue::Scalar(
1903                LiteralValue::Error(ExcelError::new_num()),
1904            ));
1905        }
1906
1907        // Calculate XNPV: Sum of values[i] / (1 + rate)^((dates[i] - dates[0]) / 365)
1908        let first_date = dates[0];
1909        let mut xnpv = 0.0;
1910
1911        for (i, &value) in values.iter().enumerate() {
1912            let days_from_start = dates[i] - first_date;
1913            let years = days_from_start / 365.0;
1914            xnpv += value / (1.0 + rate).powf(years);
1915        }
1916
1917        Ok(CalcValue::Scalar(LiteralValue::Number(xnpv)))
1918    }
1919}
1920
1921/// Helper function to calculate XNPV given rate, values, and dates
1922fn calculate_xnpv(rate: f64, values: &[f64], dates: &[f64]) -> f64 {
1923    if values.is_empty() || dates.is_empty() {
1924        return 0.0;
1925    }
1926    let first_date = dates[0];
1927    let mut xnpv = 0.0;
1928    for (i, &value) in values.iter().enumerate() {
1929        let days_from_start = dates[i] - first_date;
1930        let years = days_from_start / 365.0;
1931        xnpv += value / (1.0 + rate).powf(years);
1932    }
1933    xnpv
1934}
1935
1936/// Helper function to calculate the derivative of XNPV with respect to rate
1937fn calculate_xnpv_derivative(rate: f64, values: &[f64], dates: &[f64]) -> f64 {
1938    if values.is_empty() || dates.is_empty() {
1939        return 0.0;
1940    }
1941    let first_date = dates[0];
1942    let mut d_xnpv = 0.0;
1943    for (i, &value) in values.iter().enumerate() {
1944        let days_from_start = dates[i] - first_date;
1945        let years = days_from_start / 365.0;
1946        // d/dr [value / (1+r)^years] = -years * value / (1+r)^(years+1)
1947        d_xnpv -= years * value / (1.0 + rate).powf(years + 1.0);
1948    }
1949    d_xnpv
1950}
1951
1952/// Calculates annualized internal rate of return for irregularly dated cash flows.
1953///
1954/// The solver uses Newton-Raphson on `XNPV(rate, values, dates) = 0` with day-count basis 365.
1955///
1956/// # Remarks
1957/// - Output is an annualized rate.
1958/// - Cash-flow sign convention requires at least one negative and one positive value.
1959/// - `guess` defaults to `0.1` and can materially affect convergence.
1960/// - Non-numeric entries in value/date arrays are ignored; callable inputs return `#CALC!`.
1961/// - Returns `#NUM!` for mismatched lengths, insufficient valid points, missing sign change, derivative failure, or non-convergence.
1962///
1963/// # Examples
1964/// ```yaml,sandbox
1965/// formula: =XIRR({-10000,2750,4250,3250,2750}, {0,365,730,1095,1460})
1966/// result: 0.11541278310055854
1967/// ```
1968/// ```yaml,sandbox
1969/// formula: =XIRR({-5000,1200,1800,2400}, {0,180,365,730}, 0.1)
1970/// result: 0.06001829492127762
1971/// ```
1972/// ```yaml,docs
1973/// related:
1974///   - XNPV
1975///   - IRR
1976///   - NPV
1977/// faq:
1978///   - q: "What data shape does `XIRR` require?"
1979///     a: "`values` and `dates` must have equal numeric length with at least one positive and one negative cash flow, or `#NUM!` is returned."
1980/// ```
1981#[derive(Debug)]
1982pub struct XirrFn;
1983/// [formualizer-docgen:schema:start]
1984/// Name: XIRR
1985/// Type: XirrFn
1986/// Min args: 2
1987/// Max args: variadic
1988/// Variadic: true
1989/// Signature: XIRR(arg1: any@scalar, arg2: any@scalar, arg3...: number@scalar)
1990/// 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}
1991/// Caps: PURE
1992/// [formualizer-docgen:schema:end]
1993impl Function for XirrFn {
1994    func_caps!(PURE);
1995    fn name(&self) -> &'static str {
1996        "XIRR"
1997    }
1998    fn min_args(&self) -> usize {
1999        2
2000    }
2001    fn variadic(&self) -> bool {
2002        true
2003    }
2004    fn arg_schema(&self) -> &'static [ArgSchema] {
2005        use std::sync::LazyLock;
2006        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2007            vec![
2008                ArgSchema::any(),                   // values
2009                ArgSchema::any(),                   // dates
2010                ArgSchema::number_lenient_scalar(), // guess (optional)
2011            ]
2012        });
2013        &SCHEMA[..]
2014    }
2015    fn eval<'a, 'b, 'c>(
2016        &self,
2017        args: &'c [ArgumentHandle<'a, 'b>],
2018        _ctx: &dyn FunctionContext<'b>,
2019    ) -> Result<CalcValue<'b>, ExcelError> {
2020        // Collect values
2021        let mut values = Vec::new();
2022        let val = args[0].value()?;
2023        match val {
2024            CalcValue::Scalar(lit) => match lit {
2025                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
2026                LiteralValue::Array(arr) => {
2027                    for row in arr {
2028                        for cell in row {
2029                            if let Ok(n) = coerce_literal_num(&cell) {
2030                                values.push(n);
2031                            }
2032                        }
2033                    }
2034                }
2035                other => values.push(coerce_literal_num(&other)?),
2036            },
2037            CalcValue::Range(range) => {
2038                let (rows, cols) = range.dims();
2039                for r in 0..rows {
2040                    for c in 0..cols {
2041                        let cell = range.get_cell(r, c);
2042                        if let Ok(n) = coerce_literal_num(&cell) {
2043                            values.push(n);
2044                        }
2045                    }
2046                }
2047            }
2048            CalcValue::Callable(_) => {
2049                return Ok(CalcValue::Scalar(LiteralValue::Error(
2050                    ExcelError::new(ExcelErrorKind::Calc)
2051                        .with_message("LAMBDA value must be invoked"),
2052                )));
2053            }
2054        }
2055
2056        // Collect dates
2057        let mut dates = Vec::new();
2058        let date_val = args[1].value()?;
2059        match date_val {
2060            CalcValue::Scalar(lit) => match lit {
2061                LiteralValue::Error(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
2062                LiteralValue::Array(arr) => {
2063                    for row in arr {
2064                        for cell in row {
2065                            if let Ok(n) = coerce_literal_num(&cell) {
2066                                dates.push(n);
2067                            }
2068                        }
2069                    }
2070                }
2071                other => dates.push(coerce_literal_num(&other)?),
2072            },
2073            CalcValue::Range(range) => {
2074                let (rows, cols) = range.dims();
2075                for r in 0..rows {
2076                    for c in 0..cols {
2077                        let cell = range.get_cell(r, c);
2078                        if let Ok(n) = coerce_literal_num(&cell) {
2079                            dates.push(n);
2080                        }
2081                    }
2082                }
2083            }
2084            CalcValue::Callable(_) => {
2085                return Ok(CalcValue::Scalar(LiteralValue::Error(
2086                    ExcelError::new(ExcelErrorKind::Calc)
2087                        .with_message("LAMBDA value must be invoked"),
2088                )));
2089            }
2090        }
2091
2092        // Validate
2093        if values.len() != dates.len() || values.len() < 2 {
2094            return Ok(CalcValue::Scalar(
2095                LiteralValue::Error(ExcelError::new_num()),
2096            ));
2097        }
2098
2099        // Check that we have at least one positive and one negative cash flow
2100        let has_positive = values.iter().any(|&v| v > 0.0);
2101        let has_negative = values.iter().any(|&v| v < 0.0);
2102        if !has_positive || !has_negative {
2103            return Ok(CalcValue::Scalar(
2104                LiteralValue::Error(ExcelError::new_num()),
2105            ));
2106        }
2107
2108        // Initial guess
2109        let guess = if args.len() > 2 {
2110            coerce_num(&args[2])?
2111        } else {
2112            0.1
2113        };
2114
2115        // Newton-Raphson iteration to find XIRR
2116        let mut rate = guess;
2117        const MAX_ITER: i32 = 100;
2118        const EPSILON: f64 = 1e-10;
2119
2120        for _ in 0..MAX_ITER {
2121            let xnpv = calculate_xnpv(rate, &values, &dates);
2122            let d_xnpv = calculate_xnpv_derivative(rate, &values, &dates);
2123
2124            if d_xnpv.abs() < EPSILON {
2125                return Ok(CalcValue::Scalar(
2126                    LiteralValue::Error(ExcelError::new_num()),
2127                ));
2128            }
2129
2130            let new_rate = rate - xnpv / d_xnpv;
2131
2132            if (new_rate - rate).abs() < EPSILON {
2133                return Ok(CalcValue::Scalar(LiteralValue::Number(new_rate)));
2134            }
2135
2136            rate = new_rate;
2137
2138            // Prevent rate from going too negative (would make (1+rate) negative)
2139            if rate <= -1.0 {
2140                rate = -0.99;
2141            }
2142        }
2143
2144        Ok(CalcValue::Scalar(
2145            LiteralValue::Error(ExcelError::new_num()),
2146        ))
2147    }
2148}
2149
2150/// Converts fractional-dollar notation into a decimal dollar value.
2151///
2152/// This is commonly used for security price formats such as thirty-seconds (`fraction = 32`).
2153///
2154/// # Remarks
2155/// - `fraction` is truncated to an integer denominator and must be `>= 1`.
2156/// - Sign convention: sign is preserved (`-x` maps to `-result`).
2157/// - No periodic rate is involved in this conversion.
2158/// - Returns `#NUM!` when `fraction < 1` after truncation.
2159/// - Fractional parsing uses denominator digit width (`ceil(log10(fraction))`).
2160///
2161/// # Examples
2162/// ```yaml,sandbox
2163/// formula: =DOLLARDE(1.02, 16)
2164/// result: 1.125
2165/// ```
2166/// ```yaml,sandbox
2167/// formula: =DOLLARDE(-3.15, 32)
2168/// result: -3.46875
2169/// ```
2170/// ```yaml,docs
2171/// related:
2172///   - DOLLARFR
2173/// faq:
2174///   - q: "Why does `DOLLARDE` truncate `fraction`?"
2175///     a: "The denominator is treated as an integer quote base; values below `1` after truncation return `#NUM!`."
2176/// ```
2177#[derive(Debug)]
2178pub struct DollardeFn;
2179/// [formualizer-docgen:schema:start]
2180/// Name: DOLLARDE
2181/// Type: DollardeFn
2182/// Min args: 2
2183/// Max args: 2
2184/// Variadic: false
2185/// Signature: DOLLARDE(arg1: number@scalar, arg2: number@scalar)
2186/// 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}
2187/// Caps: PURE
2188/// [formualizer-docgen:schema:end]
2189impl Function for DollardeFn {
2190    func_caps!(PURE);
2191    fn name(&self) -> &'static str {
2192        "DOLLARDE"
2193    }
2194    fn min_args(&self) -> usize {
2195        2
2196    }
2197    fn arg_schema(&self) -> &'static [ArgSchema] {
2198        use std::sync::LazyLock;
2199        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2200            vec![
2201                ArgSchema::number_lenient_scalar(), // fractional_dollar
2202                ArgSchema::number_lenient_scalar(), // fraction
2203            ]
2204        });
2205        &SCHEMA[..]
2206    }
2207    fn eval<'a, 'b, 'c>(
2208        &self,
2209        args: &'c [ArgumentHandle<'a, 'b>],
2210        _ctx: &dyn FunctionContext<'b>,
2211    ) -> Result<CalcValue<'b>, ExcelError> {
2212        let fractional_dollar = coerce_num(&args[0])?;
2213        let fraction = coerce_num(&args[1])?.trunc() as i32;
2214
2215        // Validate fraction
2216        if fraction < 1 {
2217            return Ok(CalcValue::Scalar(
2218                LiteralValue::Error(ExcelError::new_num()),
2219            ));
2220        }
2221
2222        // Determine how many decimal places are in the fractional part
2223        // The fractional part represents numerator / fraction
2224        let sign = if fractional_dollar < 0.0 { -1.0 } else { 1.0 };
2225        let abs_value = fractional_dollar.abs();
2226        let integer_part = abs_value.trunc();
2227        let fractional_part = abs_value - integer_part;
2228
2229        // Calculate the number of digits needed to represent the fraction denominator
2230        let digits = (fraction as f64).log10().ceil() as i32;
2231        let multiplier = 10_f64.powi(digits);
2232
2233        // The fractional part is scaled by the multiplier, then divided by the fraction
2234        let numerator = (fractional_part * multiplier).round();
2235        let decimal_fraction = numerator / fraction as f64;
2236
2237        let result = sign * (integer_part + decimal_fraction);
2238        Ok(CalcValue::Scalar(LiteralValue::Number(result)))
2239    }
2240}
2241
2242/// Converts a decimal dollar value into fractional-dollar notation.
2243///
2244/// This is the inverse-style formatting helper used for quoted fractional price conventions.
2245///
2246/// # Remarks
2247/// - `fraction` is truncated to an integer denominator and must be `>= 1`.
2248/// - Sign convention: sign is preserved (`-x` maps to `-result`).
2249/// - No periodic rate is involved in this conversion.
2250/// - Returns `#NUM!` when `fraction < 1` after truncation.
2251/// - Fraction output is encoded by denominator digit width (`ceil(log10(fraction))`).
2252///
2253/// # Examples
2254/// ```yaml,sandbox
2255/// formula: =DOLLARFR(1.125, 16)
2256/// result: 1.02
2257/// ```
2258/// ```yaml,sandbox
2259/// formula: =DOLLARFR(-3.46875, 32)
2260/// result: -3.15
2261/// ```
2262/// ```yaml,docs
2263/// related:
2264///   - DOLLARDE
2265/// faq:
2266///   - q: "How does `DOLLARFR` encode the fractional part?"
2267///     a: "It scales the numerator into decimal digits based on `ceil(log10(fraction))`, preserving the input sign."
2268/// ```
2269#[derive(Debug)]
2270pub struct DollarfrFn;
2271/// [formualizer-docgen:schema:start]
2272/// Name: DOLLARFR
2273/// Type: DollarfrFn
2274/// Min args: 2
2275/// Max args: 2
2276/// Variadic: false
2277/// Signature: DOLLARFR(arg1: number@scalar, arg2: number@scalar)
2278/// 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}
2279/// Caps: PURE
2280/// [formualizer-docgen:schema:end]
2281impl Function for DollarfrFn {
2282    func_caps!(PURE);
2283    fn name(&self) -> &'static str {
2284        "DOLLARFR"
2285    }
2286    fn min_args(&self) -> usize {
2287        2
2288    }
2289    fn arg_schema(&self) -> &'static [ArgSchema] {
2290        use std::sync::LazyLock;
2291        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2292            vec![
2293                ArgSchema::number_lenient_scalar(), // decimal_dollar
2294                ArgSchema::number_lenient_scalar(), // fraction
2295            ]
2296        });
2297        &SCHEMA[..]
2298    }
2299    fn eval<'a, 'b, 'c>(
2300        &self,
2301        args: &'c [ArgumentHandle<'a, 'b>],
2302        _ctx: &dyn FunctionContext<'b>,
2303    ) -> Result<CalcValue<'b>, ExcelError> {
2304        let decimal_dollar = coerce_num(&args[0])?;
2305        let fraction = coerce_num(&args[1])?.trunc() as i32;
2306
2307        // Validate fraction
2308        if fraction < 1 {
2309            return Ok(CalcValue::Scalar(
2310                LiteralValue::Error(ExcelError::new_num()),
2311            ));
2312        }
2313
2314        let sign = if decimal_dollar < 0.0 { -1.0 } else { 1.0 };
2315        let abs_value = decimal_dollar.abs();
2316        let integer_part = abs_value.trunc();
2317        let decimal_part = abs_value - integer_part;
2318
2319        // Convert decimal fraction to fractional representation
2320        // numerator = decimal_part * fraction
2321        let numerator = decimal_part * fraction as f64;
2322
2323        // Calculate the number of digits needed to represent the fraction denominator
2324        let digits = (fraction as f64).log10().ceil() as i32;
2325        let divisor = 10_f64.powi(digits);
2326
2327        // The fractional dollar format puts the numerator after the decimal point
2328        let result = sign * (integer_part + numerator / divisor);
2329        Ok(CalcValue::Scalar(LiteralValue::Number(result)))
2330    }
2331}
2332
2333/// RRI(nper, pv, fv) — equivalent interest rate for growth of an investment.
2334/// Returns (fv/pv)^(1/nper) - 1  (i.e. CAGR).
2335#[derive(Debug)]
2336pub struct RriFn;
2337/// [formualizer-docgen:schema:start]
2338/// Name: RRI
2339/// Type: RriFn
2340/// Min args: 3
2341/// Max args: 3
2342/// Variadic: false
2343/// Signature: RRI(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
2344/// 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}
2345/// Caps: PURE
2346/// [formualizer-docgen:schema:end]
2347impl Function for RriFn {
2348    func_caps!(PURE);
2349    fn name(&self) -> &'static str {
2350        "RRI"
2351    }
2352    fn min_args(&self) -> usize {
2353        3
2354    }
2355    fn arg_schema(&self) -> &'static [ArgSchema] {
2356        use std::sync::LazyLock;
2357        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2358            vec![
2359                ArgSchema::number_lenient_scalar(), // nper
2360                ArgSchema::number_lenient_scalar(), // pv
2361                ArgSchema::number_lenient_scalar(), // fv
2362            ]
2363        });
2364        &SCHEMA[..]
2365    }
2366    fn eval<'a, 'b, 'c>(
2367        &self,
2368        args: &'c [ArgumentHandle<'a, 'b>],
2369        _ctx: &dyn FunctionContext<'b>,
2370    ) -> Result<CalcValue<'b>, ExcelError> {
2371        let nper = coerce_num(&args[0])?;
2372        let pv = coerce_num(&args[1])?;
2373        let fv = coerce_num(&args[2])?;
2374
2375        // nper must be > 0, pv must be non-zero
2376        if nper <= 0.0 || pv == 0.0 {
2377            return Ok(CalcValue::Scalar(
2378                LiteralValue::Error(ExcelError::new_num()),
2379            ));
2380        }
2381
2382        // If pv and fv have different signs, the ratio is negative and
2383        // fractional exponent would produce NaN → Excel returns #NUM!
2384        let ratio = fv / pv;
2385        if ratio < 0.0 {
2386            return Ok(CalcValue::Scalar(
2387                LiteralValue::Error(ExcelError::new_num()),
2388            ));
2389        }
2390
2391        let result = ratio.powf(1.0 / nper) - 1.0;
2392        Ok(CalcValue::Scalar(LiteralValue::Number(result)))
2393    }
2394}
2395
2396/// Returns the interest paid on the outstanding principal for a specific period
2397/// of an investment with even principal payments.
2398/// Calculates interest paid in a period for a loan repaid with equal-principal payments.
2399///
2400/// Unlike `IPMT`, `ISPMT` assumes the principal is repaid in equal installments
2401/// so the interest portion decreases linearly over the life of the loan.
2402///
2403/// # Remarks
2404/// - `rate` is the interest rate per period.
2405/// - `per` is 0-based period number (0 to `nper - 1`).
2406/// - `nper` is the total number of payment periods; must be non-zero.
2407/// - `pv` is the present value (principal).
2408/// - Formula: `pv * rate * (per / nper - 1)`.
2409/// - The result is typically negative for a positive loan principal, representing interest paid.
2410/// - Returns `#NUM!` when `nper` is zero.
2411///
2412/// # Examples
2413/// ```excel
2414/// =ISPMT(0.005, 1, 24, 100000)
2415/// ```
2416///
2417/// ```yaml,sandbox
2418/// title: "Interest in the first period"
2419/// formula: '=ISPMT(0.005, 1, 24, 100000)'
2420/// expected: -479.1666666666667
2421/// ```
2422///
2423/// ```yaml,docs
2424/// related:
2425///   - IPMT
2426///   - PPMT
2427///   - PMT
2428/// faq:
2429///   - q: "How is ISPMT different from IPMT?"
2430///     a: "ISPMT assumes equal principal repayment, so interest declines linearly instead of following an annuity schedule."
2431/// ```
2432#[derive(Debug)]
2433pub struct IspmtFn;
2434
2435/// [formualizer-docgen:schema:start]
2436/// Name: ISPMT
2437/// Type: IspmtFn
2438/// Min args: 4
2439/// Max args: 4
2440/// Variadic: false
2441/// Signature: ISPMT(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar, arg4: number@scalar)
2442/// 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}
2443/// Caps: PURE
2444/// [formualizer-docgen:schema:end]
2445impl Function for IspmtFn {
2446    func_caps!(PURE);
2447    fn name(&self) -> &'static str {
2448        "ISPMT"
2449    }
2450    fn min_args(&self) -> usize {
2451        4
2452    }
2453    fn arg_schema(&self) -> &'static [ArgSchema] {
2454        use std::sync::LazyLock;
2455        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2456            vec![
2457                ArgSchema::number_lenient_scalar(), // rate
2458                ArgSchema::number_lenient_scalar(), // per
2459                ArgSchema::number_lenient_scalar(), // nper
2460                ArgSchema::number_lenient_scalar(), // pv
2461            ]
2462        });
2463        &SCHEMA[..]
2464    }
2465    fn eval<'a, 'b, 'c>(
2466        &self,
2467        args: &'c [ArgumentHandle<'a, 'b>],
2468        _ctx: &dyn FunctionContext<'b>,
2469    ) -> Result<CalcValue<'b>, ExcelError> {
2470        let rate = coerce_num(&args[0])?;
2471        let per = coerce_num(&args[1])?;
2472        let nper = coerce_num(&args[2])?;
2473        let pv = coerce_num(&args[3])?;
2474
2475        if nper == 0.0 {
2476            return Ok(CalcValue::Scalar(
2477                LiteralValue::Error(ExcelError::new_num()),
2478            ));
2479        }
2480
2481        // ISPMT = pv * rate * (per / nper - 1)
2482        let result = pv * rate * (per / nper - 1.0);
2483        Ok(CalcValue::Scalar(LiteralValue::Number(result)))
2484    }
2485}
2486
2487/// Returns the number of periods required for an investment to reach a
2488/// specified future value at a constant interest rate.
2489///
2490/// # Remarks
2491/// - `rate` is the interest rate per compounding period; must be positive.
2492/// - `pv` and `fv` must be positive and `fv > pv` (growth scenario) or
2493///   `fv < pv` is valid as long as both are positive.
2494/// - Formula: `(ln(fv) - ln(pv)) / ln(1 + rate)`.
2495/// - Returns `#NUM!` when `rate <= 0`, or `pv` or `fv` are non-positive.
2496///
2497/// # Examples
2498/// ```excel
2499/// =PDURATION(0.10, 1000, 2000)
2500/// ```
2501///
2502/// ```yaml,sandbox
2503/// title: "Time to double at ten percent"
2504/// formula: '=PDURATION(0.10, 1000, 2000)'
2505/// expected: 7.272540897341713
2506/// ```
2507///
2508/// ```yaml,docs
2509/// related:
2510///   - NPER
2511///   - RRI
2512///   - FV
2513/// faq:
2514///   - q: "Does PDURATION require growth?"
2515///     a: "It requires positive present and future values plus a positive rate; the logarithmic formula then returns the compounding periods needed to move between them."
2516/// ```
2517#[derive(Debug)]
2518pub struct PdurationFn;
2519
2520/// [formualizer-docgen:schema:start]
2521/// Name: PDURATION
2522/// Type: PdurationFn
2523/// Min args: 3
2524/// Max args: 3
2525/// Variadic: false
2526/// Signature: PDURATION(arg1: number@scalar, arg2: number@scalar, arg3: number@scalar)
2527/// 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}
2528/// Caps: PURE
2529/// [formualizer-docgen:schema:end]
2530impl Function for PdurationFn {
2531    func_caps!(PURE);
2532    fn name(&self) -> &'static str {
2533        "PDURATION"
2534    }
2535    fn min_args(&self) -> usize {
2536        3
2537    }
2538    fn arg_schema(&self) -> &'static [ArgSchema] {
2539        use std::sync::LazyLock;
2540        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
2541            vec![
2542                ArgSchema::number_lenient_scalar(), // rate
2543                ArgSchema::number_lenient_scalar(), // pv
2544                ArgSchema::number_lenient_scalar(), // fv
2545            ]
2546        });
2547        &SCHEMA[..]
2548    }
2549    fn eval<'a, 'b, 'c>(
2550        &self,
2551        args: &'c [ArgumentHandle<'a, 'b>],
2552        _ctx: &dyn FunctionContext<'b>,
2553    ) -> Result<CalcValue<'b>, ExcelError> {
2554        let rate = coerce_num(&args[0])?;
2555        let pv = coerce_num(&args[1])?;
2556        let fv = coerce_num(&args[2])?;
2557
2558        if rate <= 0.0 || pv <= 0.0 || fv <= 0.0 {
2559            return Ok(CalcValue::Scalar(
2560                LiteralValue::Error(ExcelError::new_num()),
2561            ));
2562        }
2563
2564        let result = (fv.ln() - pv.ln()) / (1.0 + rate).ln();
2565        Ok(CalcValue::Scalar(LiteralValue::Number(result)))
2566    }
2567}
2568
2569pub fn register_builtins() {
2570    use std::sync::Arc;
2571    crate::function_registry::register_function(Arc::new(PmtFn));
2572    crate::function_registry::register_function(Arc::new(PvFn));
2573    crate::function_registry::register_function(Arc::new(FvFn));
2574    crate::function_registry::register_function(Arc::new(NpvFn));
2575    crate::function_registry::register_function(Arc::new(NperFn));
2576    crate::function_registry::register_function(Arc::new(RateFn));
2577    crate::function_registry::register_function(Arc::new(IpmtFn));
2578    crate::function_registry::register_function(Arc::new(PpmtFn));
2579    crate::function_registry::register_function(Arc::new(EffectFn));
2580    crate::function_registry::register_function(Arc::new(NominalFn));
2581    crate::function_registry::register_function(Arc::new(IrrFn));
2582    crate::function_registry::register_function(Arc::new(MirrFn));
2583    crate::function_registry::register_function(Arc::new(CumipmtFn));
2584    crate::function_registry::register_function(Arc::new(CumprincFn));
2585    crate::function_registry::register_function(Arc::new(XnpvFn));
2586    crate::function_registry::register_function(Arc::new(XirrFn));
2587    crate::function_registry::register_function(Arc::new(DollardeFn));
2588    crate::function_registry::register_function(Arc::new(DollarfrFn));
2589    crate::function_registry::register_function(Arc::new(RriFn));
2590    crate::function_registry::register_function(Arc::new(IspmtFn));
2591    crate::function_registry::register_function(Arc::new(PdurationFn));
2592}