Skip to content

Commit 5061516

Browse files
committed
Add PV (present value) function
1 parent 5dd050a commit 5061516

File tree

6 files changed

+132
-4
lines changed

6 files changed

+132
-4
lines changed

TinyExprChanges.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ The following are changes from the original TinyExpr C library:
7171
- `not`: returns logical negation of value.
7272
- `permut`: alias for `npr()`, like the *Excel* function.
7373
- `power`: alias for `pow()`, like the *Excel* function.
74+
- `pv`: returns the present value of an investment, like the *Excel* function.
7475
- `rand`: returns random number between `0` and `1`.
7576
Note that this implementation uses the Mersenne Twister (`mt19937`) to generate random numbers.
7677
- `round`: returns a number, rounded to a given decimal point.

docs/manual/functions.qmd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ Any subsequent arguments that evaluate to NaN will be ignored.
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. |
129129
| 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. |
130+
| 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 *FutureValue* and *Type* are optional and default to 0.<br>\linebreak *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. |
130131

131132
Table: Financial Functions\index{functions!financial}
132133
:::

docs/manual/unknown-symbol-resolution.qmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ tep.set_unknown_symbol_resolver(ResolveResolutionSymbols);
7373
7474
/* Will resolve to 288, and "RESOLUTION" will be added as a
7575
variable to the parser with a value of 96.
76-
Also, beccause TinyExpr++ is case insensitive,
76+
Also, because TinyExpr++ is case insensitive,
7777
"resolution" will also be seen as 96 once "RESOLUTION" was resolved.*/
7878
tep.evaluate("RESOLUTION * 3");
7979
```

tests/tetests.cpp

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
/*
2626
* TINYEXPR++ - Tiny recursive descent parser and evaluation engine in C++
27-
* Copyright (c) 2020-2023 Blake Madden
27+
* Copyright (c) 2020-2026 Blake Madden
2828
*
2929
* C++ version of the TinyExpr library.
3030
*
@@ -4193,6 +4193,84 @@ TEST_CASE("NaN Comparison", "[nan]")
41934193
}
41944194

41954195
// Financial functions
4196+
// --------------------------------------------------
4197+
// PV
4198+
// --------------------------------------------------
4199+
TEST_CASE("PV", "[finance]")
4200+
{
4201+
te_parser tep;
4202+
4203+
// Excel: =PV(0.05/12, 60, -200)
4204+
CHECK_THAT(WITHIN_TYPE_CAST(tep.evaluate("PV(0.05/12, 60, -200)")),
4205+
Catch::Matchers::WithinRel(WITHIN_TYPE_CAST(10598.14), WITHIN_TYPE_CAST(0.0001)));
4206+
4207+
// Excel: =PV(0.05/12, 60, -200, 0, 1)
4208+
CHECK_THAT(WITHIN_TYPE_CAST(tep.evaluate("PV(0.05/12, 60, -200, 0, 1)")),
4209+
Catch::Matchers::WithinRel(WITHIN_TYPE_CAST(10642.30), WITHIN_TYPE_CAST(0.0001)));
4210+
4211+
// Excel: =PV(0.1, 1, -100)
4212+
CHECK_THAT(WITHIN_TYPE_CAST(tep.evaluate("PV(0.1, 1, -100)")),
4213+
Catch::Matchers::WithinRel(WITHIN_TYPE_CAST(90.91), WITHIN_TYPE_CAST(0.0001)));
4214+
4215+
// Excel: =PV(0, 60, -200)
4216+
CHECK_THAT(WITHIN_TYPE_CAST(tep.evaluate("PV(0, 60, -200)")),
4217+
Catch::Matchers::WithinRel(WITHIN_TYPE_CAST(12000)));
4218+
4219+
// Excel: =PV(0, 10, 0, 1000)
4220+
CHECK_THAT(WITHIN_TYPE_CAST(tep.evaluate("PV(0, 10, 0, 1000)")),
4221+
Catch::Matchers::WithinRel(WITHIN_TYPE_CAST(-1000)));
4222+
4223+
// Excel: =PV(0.05, 10, -100)
4224+
CHECK_THAT(WITHIN_TYPE_CAST(tep.evaluate("PV(0.05, 10, -100)")),
4225+
Catch::Matchers::WithinRel(WITHIN_TYPE_CAST(772.17), WITHIN_TYPE_CAST(0.0001)));
4226+
4227+
// Excel: =PV(0.05, 10, -100, 0)
4228+
CHECK_THAT(WITHIN_TYPE_CAST(tep.evaluate("PV(0.05, 10, -100, 0)")),
4229+
Catch::Matchers::WithinRel(WITHIN_TYPE_CAST(772.17), WITHIN_TYPE_CAST(0.0001)));
4230+
4231+
// Excel: =PV(0.05, 10, -100, 0, 0)
4232+
CHECK_THAT(WITHIN_TYPE_CAST(tep.evaluate("PV(0.05, 10, -100, 0, 0)")),
4233+
Catch::Matchers::WithinRel(WITHIN_TYPE_CAST(772.17), WITHIN_TYPE_CAST(0.0001)));
4234+
4235+
// Excel: =PV(0.05, 10, -100, 0, 2)
4236+
CHECK_THAT(WITHIN_TYPE_CAST(tep.evaluate("PV(0.05, 10, -100, 0, 2)")),
4237+
Catch::Matchers::WithinRel(WITHIN_TYPE_CAST(810.78), WITHIN_TYPE_CAST(0.0001)));
4238+
4239+
// Excel: =PV(0.05, 10, -100, 0, -1)
4240+
CHECK_THAT(WITHIN_TYPE_CAST(tep.evaluate("PV(0.05, 10, -100, 0, -1)")),
4241+
Catch::Matchers::WithinRel(WITHIN_TYPE_CAST(810.78), WITHIN_TYPE_CAST(0.0001)));
4242+
4243+
// Excel: =PV(-0.5, 5, -100)
4244+
CHECK_THAT(WITHIN_TYPE_CAST(tep.evaluate("PV(-0.5, 5, -100)")),
4245+
Catch::Matchers::WithinRel(WITHIN_TYPE_CAST(6200), WITHIN_TYPE_CAST(0.0001)));
4246+
4247+
// Excel: =PV(-1, 10, -100) -> #NUM!
4248+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PV(-1, 10, -100)"))));
4249+
4250+
// Excel: =PV(0.05, 0, -100) -> #NUM!
4251+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PV(0.05, 0, -100)"))));
4252+
4253+
// Excel: =PV(0.05, -10, -100) -> #NUM!
4254+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PV(0.05, -10, -100)"))));
4255+
4256+
// Excel: =PV(NaN, 10, -100) -> #NUM!
4257+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PV(NaN, 10, -100)"))));
4258+
4259+
// Excel: =PV(0.05, NaN, -100) -> #NUM!
4260+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PV(0.05, NaN, -100)"))));
4261+
4262+
// Excel: =PV(0.05, 10, NaN) -> #NUM!
4263+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PV(0.05, 10, NaN)"))));
4264+
4265+
// Excel: =PV(0.08/12, 360, -1000)
4266+
CHECK_THAT(WITHIN_TYPE_CAST(tep.evaluate("PV(0.08/12, 360, -1000)")),
4267+
Catch::Matchers::WithinRel(WITHIN_TYPE_CAST(136283.35), WITHIN_TYPE_CAST(0.0001)));
4268+
4269+
// Excel: =PV(0.03, 1, 0, 1000)
4270+
CHECK_THAT(WITHIN_TYPE_CAST(tep.evaluate("PV(0.03, 1, 0, 1000)")),
4271+
Catch::Matchers::WithinRel(WITHIN_TYPE_CAST(-970.87), WITHIN_TYPE_CAST(0.0001)));
4272+
}
4273+
41964274
TEST_CASE("Nominal", "[finance]")
41974275
{
41984276
te_parser tep;

tinyexpr.cpp

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
/*
2727
* TINYEXPR++ - Tiny recursive descent parser and evaluation engine in C++
2828
*
29-
* Copyright (c) 2020-2025 Blake Madden
29+
* Copyright (c) 2020-2026 Blake Madden
3030
*
3131
* C++ version of the TinyExpr library.
3232
*
@@ -255,6 +255,52 @@ namespace te_builtins
255255
decimalPosition;
256256
}
257257

258+
[[nodiscard]]
259+
static te_type te_pv(te_type rate, te_type nper, te_type pmt, te_type fv, te_type type)
260+
{
261+
if (!std::isfinite(rate) || !std::isfinite(nper) || !std::isfinite(pmt))
262+
{
263+
return te_parser::te_nan;
264+
}
265+
266+
// optional args default like Excel
267+
if (!std::isfinite(fv))
268+
{
269+
fv = 0;
270+
}
271+
if (!std::isfinite(type))
272+
{
273+
type = 0;
274+
}
275+
276+
if (nper <= 0)
277+
{
278+
return te_parser::te_nan;
279+
}
280+
281+
// Excel: rate <= -1 -> #NUM!
282+
if (rate <= -1.0)
283+
{
284+
return te_parser::te_nan;
285+
}
286+
287+
// coerce type to 0 or 1
288+
type = (type != 0) ? 1 : 0;
289+
290+
if (rate == 0.0)
291+
{
292+
return -(fv + pmt * nper);
293+
}
294+
295+
const te_type powVal = std::pow(1 + rate, nper);
296+
if (!std::isfinite(powVal) || powVal == 0.0)
297+
{
298+
return te_parser::te_nan;
299+
}
300+
301+
return -(fv + pmt * (1 + rate * type) * (powVal - 1) / rate) / powVal;
302+
}
303+
258304
[[nodiscard]]
259305
static te_type te_nominal(te_type effectiveRate, te_type periods)
260306
{
@@ -1499,6 +1545,8 @@ const std::set<te_variable> te_parser::m_functions = { // NOLINT
14991545
{ "pi", static_cast<te_fun0>(te_builtins::te_pi), TE_PURE },
15001546
{ "pow", static_cast<te_fun2>(te_builtins::te_pow), TE_PURE },
15011547
{ "power", /* Excel alias*/ static_cast<te_fun2>(te_builtins::te_pow), TE_PURE },
1548+
{ "pv", static_cast<te_fun5>(te_builtins::te_pv),
1549+
static_cast<te_variable_flags>(TE_PURE | TE_VARIADIC) },
15021550
{ "rand", static_cast<te_fun0>(te_builtins::te_random), TE_PURE },
15031551
{ "round", static_cast<te_fun2>(te_builtins::te_round),
15041552
static_cast<te_variable_flags>(TE_PURE | TE_VARIADIC) },

tinyexpr.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
/*
2727
* TINYEXPR++ - Tiny recursive descent parser and evaluation engine in C++
2828
*
29-
* Copyright (c) 2020-2025 Blake Madden
29+
* Copyright (c) 2020-2026 Blake Madden
3030
*
3131
* C++ version of the TinyExpr library.
3232
*

0 commit comments

Comments
 (0)