Skip to content

Commit 10733f3

Browse files
committed
Add NPER and FV functions
1 parent 0bf49a9 commit 10733f3

File tree

4 files changed

+309
-9
lines changed

4 files changed

+309
-9
lines changed

TinyExprChanges.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ The following are changes from the original TinyExpr C library:
5151
- `even`: returns a value rounded up to the nearest even integer.
5252
- `fact`: alias for `fac()`, like the *Excel* function.
5353
- `false`: returns `false` (i.e., `0`) in a boolean expression.
54+
- `fv`: returns the future value of an investment.
5455
- `iserr`: returns true if an expression evaluates to NaN.
5556
- `iserror`: alias for `iserr`.
5657
- `iseven`: returns true if a number is even, false if odd.
@@ -66,6 +67,7 @@ The following are changes from the original TinyExpr C library:
6667
- `na`: returns `NaN` (i.e., Not-a-Number) in a boolean expression.
6768
- `nan`: alias for `na`.
6869
- `nominal`: returns the nominal annual interest rate, provided the effective rate and the number of compounding periods per year.
70+
- `nper`: returns the number of periods for an investment.
6971
- `odd`: returns a value rounded up to the nearest odd integer.
7072
- `or`: returns true (i.e., non-zero) if any condition is true (accepts 1-24 arguments).
7173
- `not`: returns logical negation of value.

docs/manual/functions.qmd

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Table: Statistical Functions\index{functions!statistical}
110110
| AND(Value1, Value2, ...) | Returns true if all conditions are true. |
111111
| IF(Condition, ValueIfTrue, ValueIfFalse) | If *Condition* is true (non-zero), then *ValueIfTrue* is returned; otherwise, *ValueIfFalse* is returned.<br>\linebreak Note that multiple `IF` commands can be nested to create a "case" statement\index{case statements}. |
112112
| IFS(Condition1, Value1, Condition2, Value2, ...) | Checks up to twelve conditions, returning the value corresponding to the first met condition.<br>\linebreak This is shorthand for multiple nested `IF` commands, providing better readability.<br>\linebreak Will accept 1–12 condition/value pairs.<br>\linebreak NaN will be returned if all conditions are false. |
113-
| NOT(Value) | Returns the logical negation of *Value.* |
113+
| NOT(Value) | Returns the logical negation of *Value*. |
114114
| OR(Value1, Value2, ...) | Returns true if any condition is true. |
115115

116116
Table: Logic Functions\index{functions!logical}
@@ -126,7 +126,9 @@ Any subsequent arguments that evaluate to NaN will be ignored.
126126
| :-- | :-- |
127127
| DB(Cost, Salvage, Lifetime, Period, Month) | Returns the depreciation of an asset for a specified period using the fixed-declining balance method. |
128128
| EFFECT(NominalRate, Periods) | Returns the effective annual interest rate, provided the nominal annual interest rate and the number of compounding periods per year.<br>\linebreak NaN will be returned if *Periods* is < 1 or if *NominalRate* <= 0. |
129+
| FV(Rate, Periods, Payment, [PresentValue], [Type]) | Returns the future value of an investment based on a constant interest rate, a fixed number of periods, and periodic payments.<br>\linebreak Note that *PresentValue* and *Type* are optional and default to 0.<br>\linebreak Also, *Type* specifies when payments are due: 0 = end of period, 1 = beginning of period.<br>\linebreak NaN will be returned if *Periods* <= 0 or if any required argument is not finite. |
129130
| NOMINAL(EffectiveRate, Periods) | Returns the nominal annual interest rate, provided the effective rate and the number of compounding periods per year.<br>\linebreak NaN will be returned if *Periods* is < 1 or if *EffectiveRate* <= 0. |
131+
| NPER(Rate, Payment, PresentValue, [FutureValue], [Type]) | Returns the number of periods for an investment or loan based on a constant interest rate, periodic payments, and present and future values.<br>\linebreak Note that *FutureValue* and *Type* are optional and default to 0.<br>\linebreak Also, *Type* specifies when payments are due: 0 = end of period, 1 = beginning of period.<br>\linebreak NaN will be returned if any required argument is not finite or if the calculation is not defined. |
130132
| PMT(Rate, Periods, PresentValue, [FutureValue], [Type]) | Returns the periodic payment for an investment or loan based on a constant interest rate, a fixed number of periods, and a present value.<br>\linebreak Note that *FutureValue* and *Type* are optional and default to 0.<br>\linebreak Also, *Type* specifies when payments are due: 0 = end of period, 1 = beginning of period.<br>\linebreak NaN will be returned if *Periods* <= 0 or if any required argument is not finite. |
131133
| PV(Rate, Periods, Payment, [FutureValue], [Type]) | Returns the present value of an investment based on a constant interest rate, a fixed number of periods, and periodic payments.<br>\linebreak Note that *FutureValue* and *Type* are optional and default to 0.<br>\linebreak Also, *Type* specifies when payments are due: 0 = end of period, 1 = beginning of period.<br>\linebreak NaN will be returned if *Periods* <= 0 or if any required argument is not finite. |
132134

tests/tetests.cpp

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4386,6 +4386,209 @@ TEST_CASE("PMT", "[finance]")
43864386
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PMT(0.05, 10, NaN)"))));
43874387
}
43884388

4389+
// --------------------------------------------------
4390+
// NPER
4391+
// --------------------------------------------------
4392+
TEST_CASE("NPER", "[finance]")
4393+
{
4394+
te_parser tep;
4395+
4396+
// Excel: =NPER(0.05/12, -188.71, 10000)
4397+
// Loan paid off in 60 months
4398+
CHECK_THAT(
4399+
WITHIN_TYPE_CAST(tep.evaluate("NPER(0.05/12, -188.71, 10000)")),
4400+
Catch::Matchers::WithinRel(
4401+
WITHIN_TYPE_CAST(60),
4402+
WITHIN_TYPE_CAST(0.0001)));
4403+
4404+
// Excel: =NPER(0.05/12, -200, 10000)
4405+
// Higher payment -> fewer periods
4406+
CHECK_THAT(
4407+
WITHIN_TYPE_CAST(tep.evaluate("NPER(0.05/12, -200, 10000)")),
4408+
Catch::Matchers::WithinRel(
4409+
WITHIN_TYPE_CAST(56.18429076),
4410+
WITHIN_TYPE_CAST(0.0001)));
4411+
4412+
// Excel: =NPER(0, -200, 12000)
4413+
// Zero interest
4414+
CHECK_THAT(
4415+
WITHIN_TYPE_CAST(tep.evaluate("NPER(0, -200, 12000)")),
4416+
Catch::Matchers::WithinRel(
4417+
WITHIN_TYPE_CAST(60)));
4418+
4419+
// --------------------------------------------------
4420+
// Excel example (provided)
4421+
// --------------------------------------------------
4422+
4423+
// Excel data:
4424+
// Annual interest rate: 12%
4425+
// Payment each period: -100
4426+
// Present value: -1000
4427+
// Future value: 10000
4428+
// Payments at beginning of period
4429+
4430+
// Excel: =NPER(A2/12, A3, A4, A5, 1)
4431+
// Excel result: 59.67386567
4432+
4433+
CHECK_THAT(
4434+
WITHIN_TYPE_CAST(tep.evaluate("NPER(0.12/12, -100, -1000, 10000, 1)")),
4435+
Catch::Matchers::WithinRel(
4436+
WITHIN_TYPE_CAST(59.67386567),
4437+
WITHIN_TYPE_CAST(0.0001)));
4438+
4439+
// Excel: =NPER(A2/12, A3, A4, A5)
4440+
// Payments at end of period
4441+
// Excel result: 60.08212285
4442+
CHECK_THAT(
4443+
WITHIN_TYPE_CAST(tep.evaluate("NPER(0.12/12, -100, -1000, 10000)")),
4444+
Catch::Matchers::WithinRel(
4445+
WITHIN_TYPE_CAST(60.08212285),
4446+
WITHIN_TYPE_CAST(0.0001)));
4447+
4448+
// Excel: =NPER(A2/12, A3, A4)
4449+
// Future value defaults to 0
4450+
// Excel result: -9.7859404
4451+
CHECK_THAT(
4452+
WITHIN_TYPE_CAST(tep.evaluate("NPER(0.12/12, -100, -1000)")),
4453+
Catch::Matchers::WithinRel(
4454+
WITHIN_TYPE_CAST(-9.57859404),
4455+
WITHIN_TYPE_CAST(0.0001)));
4456+
4457+
// --------------------------------------------------
4458+
// Invalid inputs
4459+
// --------------------------------------------------
4460+
4461+
// Excel: rate <= -1 -> #NUM!
4462+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("NPER(-1, -100, 1000)"))));
4463+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("NPER(-1.2, -100, 1000)"))));
4464+
4465+
// Excel: pmt == 0 with rate == 0 -> #NUM!
4466+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("NPER(0, 0, 1000)"))));
4467+
4468+
// Non-finite args -> NaN
4469+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("NPER(NaN, -100, 1000)"))));
4470+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("NPER(0.05, NaN, 1000)"))));
4471+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("NPER(0.05, -100, NaN)"))));
4472+
}
4473+
4474+
TEST_CASE("FV", "[finance]")
4475+
{
4476+
te_parser tep;
4477+
4478+
// Excel: =FV(0.05/12, 60, -200)
4479+
// Savings $200/month at 5% for 5 years
4480+
// Excel result ~ $13,601.22
4481+
CHECK_THAT(
4482+
WITHIN_TYPE_CAST(tep.evaluate("FV(0.05/12, 60, -200)")),
4483+
Catch::Matchers::WithinRel(
4484+
WITHIN_TYPE_CAST(13601.22),
4485+
WITHIN_TYPE_CAST(0.00005)));
4486+
4487+
// Excel: =FV(0.05/12, 60, -200, 0, 1)
4488+
// Payments at beginning of period
4489+
// Excel result ~ $13,657.89
4490+
CHECK_THAT(
4491+
WITHIN_TYPE_CAST(tep.evaluate("FV(0.05/12, 60, -200, 0, 1)")),
4492+
Catch::Matchers::WithinRel(
4493+
WITHIN_TYPE_CAST(13657.89),
4494+
WITHIN_TYPE_CAST(0.00005)));
4495+
4496+
// Excel: =FV(0, 60, -200)
4497+
// Zero interest
4498+
// Excel result = 12,000
4499+
CHECK_THAT(
4500+
WITHIN_TYPE_CAST(tep.evaluate("FV(0, 60, -200)")),
4501+
Catch::Matchers::WithinRel(
4502+
WITHIN_TYPE_CAST(12000)));
4503+
4504+
// Excel: =FV(0.08/12, 360, -1000)
4505+
// Long-horizon compounding
4506+
// Excel result ~ $1,490,359.45
4507+
CHECK_THAT(
4508+
WITHIN_TYPE_CAST(tep.evaluate("FV(0.08/12, 360, -1000)")),
4509+
Catch::Matchers::WithinRel(
4510+
WITHIN_TYPE_CAST(1490359.45),
4511+
WITHIN_TYPE_CAST(0.0001)));
4512+
4513+
// --------------------------------------------------
4514+
// FV – Excel examples
4515+
// --------------------------------------------------
4516+
4517+
// Example 1
4518+
// Excel data:
4519+
// Annual interest rate: 6%
4520+
// Number of payments: 10
4521+
// Payment: -200
4522+
// Present value: -500
4523+
// Payments at beginning of period
4524+
//
4525+
// Excel: =FV(A2/12, A3, A4, A5, A6)
4526+
// Excel result: $2,581.40
4527+
CHECK_THAT(
4528+
WITHIN_TYPE_CAST(tep.evaluate("FV(0.06/12, 10, -200, -500, 1)")),
4529+
Catch::Matchers::WithinRel(
4530+
WITHIN_TYPE_CAST(2581.40),
4531+
WITHIN_TYPE_CAST(0.00005)));
4532+
4533+
// Example 2
4534+
// Excel data:
4535+
// Annual interest rate: 12%
4536+
// Number of payments: 12
4537+
// Payment: -1000
4538+
//
4539+
// Excel: =FV(A2/12, A3, A4)
4540+
// Excel result: $12,682.50
4541+
CHECK_THAT(
4542+
WITHIN_TYPE_CAST(tep.evaluate("FV(0.12/12, 12, -1000)")),
4543+
Catch::Matchers::WithinRel(
4544+
WITHIN_TYPE_CAST(12682.50),
4545+
WITHIN_TYPE_CAST(0.00005)));
4546+
4547+
// Example 3
4548+
// Excel data:
4549+
// Annual interest rate: 11%
4550+
// Number of payments: 35
4551+
// Payment: -2000
4552+
// Payments at beginning of period
4553+
//
4554+
// Excel: =FV(A2/12, A3, A4, , A5)
4555+
// Excel result: $82,846.25
4556+
CHECK_THAT(
4557+
WITHIN_TYPE_CAST(tep.evaluate("FV(0.11/12, 35, -2000, 0, 1)")),
4558+
Catch::Matchers::WithinRel(
4559+
WITHIN_TYPE_CAST(82846.25),
4560+
WITHIN_TYPE_CAST(0.00005)));
4561+
4562+
// Example 4
4563+
// Excel data:
4564+
// Annual interest rate: 6%
4565+
// Number of payments: 12
4566+
// Payment: -100
4567+
// Present value: -1000
4568+
// Payments at beginning of period
4569+
//
4570+
// Excel: =FV(A2/12, A3, A4, A5, A6)
4571+
// Excel result: $2,301.40
4572+
CHECK_THAT(
4573+
WITHIN_TYPE_CAST(tep.evaluate("FV(0.06/12, 12, -100, -1000, 1)")),
4574+
Catch::Matchers::WithinRel(
4575+
WITHIN_TYPE_CAST(2301.40),
4576+
WITHIN_TYPE_CAST(0.00005)));
4577+
4578+
// Excel: rate <= -1 -> #NUM!
4579+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("FV(-1, 10, -100)"))));
4580+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("FV(-1.5, 10, -100)"))));
4581+
4582+
// Excel: nper <= 0 -> #NUM!
4583+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("FV(0.05, 0, -100)"))));
4584+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("FV(0.05, -10, -100)"))));
4585+
4586+
// Non-finite args -> NaN
4587+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("FV(NaN, 10, -100)"))));
4588+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("FV(0.05, NaN, -100)"))));
4589+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("FV(0.05, 10, NaN)"))));
4590+
}
4591+
43894592
TEST_CASE("Nominal", "[finance]")
43904593
{
43914594
te_parser tep;

0 commit comments

Comments
 (0)