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