Skip to content

Commit 0bf49a9

Browse files
committed
Add PMT function
1 parent 4106697 commit 0bf49a9

File tree

4 files changed

+158
-10
lines changed

4 files changed

+158
-10
lines changed

TinyExprChanges.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ The following are changes from the original TinyExpr C library:
33
- Compiles as C++17 code.
44
- `te_*` functions are now wrapped in a `te_parser` class.
55
- `te_interp()`, `te_compile()`, and `te_eval()` have been replaced with `te_parser::compile()`, `te_parser::evaluate()`, and `te_parser::set_variables_and_functions()`.
6-
`set_variables_and_functions()` sets your list of custom functions and variables. `compile()` compiles and optimizes an expression.
7-
Finally, `evaluate()` will use the already compiled expression and return its result.
8-
`evaluate()` also has an overload that compiles and evaluates an expression in one call.
9-
- Variable/function types (e.g., `TE_FUNCTION0`) have been removed; types are now deduced by the compiler. The available flags
10-
for variables and functions are now just combinations of `TE_DEFAULT`, `TE_PURE`, and `TE_VARIADIC`.
6+
`set_variables_and_functions()` sets your list of custom functions and variables. `compile()` compiles and optimizes an expression.
7+
Finally, `evaluate()` will use the already compiled expression and return its result.
8+
`evaluate()` also has an overload that compiles and evaluates an expression in one call.
9+
- Variable/function types (e.g., `TE_FUNCTION0`) have been removed; types are now deduced by the compiler.
10+
The available flags for variables and functions are now just combinations of `TE_DEFAULT`, `TE_PURE`, and `TE_VARIADIC`.
1111
- Formula parsing is now case insensitive.
1212
- Added support for variadic functions (can accept 1-24 arguments); enabled through the `TE_VARIADIC` flag.
1313
(Refer to the `AVERAGE()` function in `tinyexp.cpp` for an example.)
@@ -34,12 +34,12 @@ The following are changes from the original TinyExpr C library:
3434
- `and`: returns true (i.e., non-zero) if all conditions are true (accepts 1-24 arguments).
3535
- `average`: returns the mean for a range of values (accepts 1-24 arguments).
3636
- `bitand`: bitwise AND.
37-
- `bitlrotate`: bitwise left rotate. Versions of this are available for 8-, 16-, 32-, and 64-bit integers (if supported by the platform). (Only available if compiled as C++20.)
37+
- `bitlrotate`: bitwise left rotate. Versions of this are available for 8-, 16-, 32-, and 64-bit integers (if supported by the platform).
3838
- `bitlshift`: left shift.
3939
Negative shift amount arguments (similar to *Excel*) are supported.
4040
- `bitnot`: bitwise NOT. Versions of this are available for 8-, 16-, 32-, and 64-bit integers (if supported by the platform).
4141
- `bitor`: bitwise OR.
42-
- `bitrrotate`: bitwise right rotate. Versions of this are available for 8-, 16-, 32-, and 64-bit integers (if supported by the platform). (Only available if compiled as C++20.)
42+
- `bitrrotate`: bitwise right rotate. Versions of this are available for 8-, 16-, 32-, and 64-bit integers (if supported by the platform).
4343
- `bitrshift`: right shift.
4444
Negative shift amount arguments (similar to *Excel*) are supported.
4545
- `bitxor`: bitwise XOR.
@@ -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+
- `pmt`: returns the periodic payment for an investment or loan based on a constant interest rate, a fixed number of periods, and a present value (like the *Excel* function).
7475
- `pv`: returns the present value of an investment, like the *Excel* function.
7576
- `rand`: returns random number between `0` and `1`.
7677
Note that this implementation uses the Mersenne Twister (`mt19937`) to generate random numbers.
@@ -127,8 +128,7 @@ The following are changes from the original TinyExpr C library:
127128
- All data fields are now initialized.
128129
- Added [Doxygen](https://github.com/doxygen/doxygen) comments.
129130
- Removed `te_print()` debug function.
130-
- Added `list_available_functions_and_variables()` function to display all available built-in and custom
131-
functions and variables.
131+
- Added `list_available_functions_and_variables()` function to display all available built-in and custom functions and variables.
132132
- Added `get_expression()` function to get the last formula used.
133133
- Added `[[nodiscard]]` attributes to improve compile-time warnings.
134134
- Added `constexpr` and `noexcept` for C++ optimization.

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+
| 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. |
130131
| 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. |
131132

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

tests/tetests.cpp

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4287,6 +4287,105 @@ TEST_CASE("PV", "[finance]")
42874287
WITHIN_TYPE_CAST(0.0001)));
42884288
}
42894289

4290+
TEST_CASE("PMT", "[finance]")
4291+
{
4292+
te_parser tep;
4293+
4294+
// Excel: =PMT(0.05/12, 60, 10000)
4295+
// Monthly payment on a $10,000 loan at 5% for 5 years
4296+
// Excel result ~ -188.71
4297+
CHECK_THAT(
4298+
WITHIN_TYPE_CAST(tep.evaluate("PMT(0.05/12, 60, 10000)")),
4299+
Catch::Matchers::WithinRel(
4300+
WITHIN_TYPE_CAST(-188.71),
4301+
WITHIN_TYPE_CAST(0.00002)));
4302+
4303+
// Excel: =PMT(0.05/12, 60, 10000, 0, 1)
4304+
// Payments at beginning of period -> slightly smaller payment
4305+
// Excel result ~ -187.93
4306+
CHECK_THAT(
4307+
WITHIN_TYPE_CAST(tep.evaluate("PMT(0.05/12, 60, 10000, 0, 1)")),
4308+
Catch::Matchers::WithinRel(
4309+
WITHIN_TYPE_CAST(-187.93),
4310+
WITHIN_TYPE_CAST(0.00001)));
4311+
4312+
// Excel: =PMT(0, 60, 12000)
4313+
// Zero interest: straight-line repayment
4314+
// Excel result = -200
4315+
CHECK_THAT(
4316+
WITHIN_TYPE_CAST(tep.evaluate("PMT(0, 60, 12000)")),
4317+
Catch::Matchers::WithinRel(
4318+
WITHIN_TYPE_CAST(-200)));
4319+
4320+
// Excel: =PMT(0.1, 1, 100)
4321+
// One-period loan at 10%
4322+
// Excel result = -110
4323+
CHECK_THAT(
4324+
WITHIN_TYPE_CAST(tep.evaluate("PMT(0.1, 1, 100)")),
4325+
Catch::Matchers::WithinRel(
4326+
WITHIN_TYPE_CAST(-110),
4327+
WITHIN_TYPE_CAST(0.00001)));
4328+
4329+
// Excel: =PMT(0.05/12, 60, 10000, 1000)
4330+
// Loan with balloon payment
4331+
// Excel result ~ -203.42
4332+
CHECK_THAT(
4333+
WITHIN_TYPE_CAST(tep.evaluate("PMT(0.05/12, 60, 10000, 1000)")),
4334+
Catch::Matchers::WithinRel(
4335+
WITHIN_TYPE_CAST(-203.42),
4336+
WITHIN_TYPE_CAST(0.00002)));
4337+
4338+
// Excel data:
4339+
// Annual interest rate: 8%
4340+
// Number of months: 10
4341+
// Loan amount: $10,000
4342+
4343+
// Excel: =PMT(A2/12, A3, A4)
4344+
// Monthly payment
4345+
// Excel result: ($1,037.03)
4346+
CHECK_THAT(
4347+
WITHIN_TYPE_CAST(tep.evaluate("PMT(0.08/12, 10, 10000)")),
4348+
Catch::Matchers::WithinRel(
4349+
WITHIN_TYPE_CAST(-1037.03),
4350+
WITHIN_TYPE_CAST(0.00002)));
4351+
4352+
// Excel: =PMT(A2/12, A3, A4, , 1)
4353+
// Payments at beginning of period
4354+
// Excel result: ($1,030.16)
4355+
CHECK_THAT(
4356+
WITHIN_TYPE_CAST(tep.evaluate("PMT(0.08/12, 10, 10000, 0, 1)")),
4357+
Catch::Matchers::WithinRel(
4358+
WITHIN_TYPE_CAST(-1030.16),
4359+
WITHIN_TYPE_CAST(0.00002)));
4360+
4361+
// Excel data:
4362+
// Annual interest rate: 6%
4363+
// Term: 18 years
4364+
// Target future value: $50,000
4365+
4366+
// Excel: =PMT(A9/12, A10*12, 0, A11)
4367+
// Monthly savings required
4368+
// Excel result ~ ($129.08)
4369+
CHECK_THAT(
4370+
WITHIN_TYPE_CAST(tep.evaluate("PMT(0.06/12, 18*12, 0, 50000)")),
4371+
Catch::Matchers::WithinRel(
4372+
WITHIN_TYPE_CAST(-129.08),
4373+
WITHIN_TYPE_CAST(0.00005)));
4374+
4375+
// Excel: rate <= -1 -> #NUM!
4376+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PMT(-1, 10, 1000)"))));
4377+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PMT(-1.5, 10, 1000)"))));
4378+
4379+
// Excel: nper <= 0 -> #NUM!
4380+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PMT(0.05, 0, 1000)"))));
4381+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PMT(0.05, -10, 1000)"))));
4382+
4383+
// Non-finite args -> NaN
4384+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PMT(NaN, 10, 1000)"))));
4385+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PMT(0.05, NaN, 1000)"))));
4386+
CHECK(std::isnan(WITHIN_TYPE_CAST(tep.evaluate("PMT(0.05, 10, NaN)"))));
4387+
}
4388+
42904389
TEST_CASE("Nominal", "[finance]")
42914390
{
42924391
te_parser tep;

tinyexpr.cpp

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,51 @@ namespace te_builtins
255255
decimalPosition;
256256
}
257257

258+
[[nodiscard]]
259+
static te_type te_pmt(te_type rate, te_type nper, te_type pv, te_type fv, te_type type)
260+
{
261+
if (!std::isfinite(rate) || !std::isfinite(nper) || !std::isfinite(pv))
262+
{
263+
return te_parser::te_nan;
264+
}
265+
266+
if (!std::isfinite(fv))
267+
{
268+
fv = 0;
269+
}
270+
if (!std::isfinite(type))
271+
{
272+
type = 0;
273+
}
274+
275+
if (nper <= 0)
276+
{
277+
return te_parser::te_nan;
278+
}
279+
280+
if (rate <= -1.0)
281+
{
282+
return te_parser::te_nan;
283+
}
284+
285+
// coerce type to 0 or 1
286+
type = (type != 0) ? 1 : 0;
287+
288+
// zero-interest
289+
if (rate == 0.0)
290+
{
291+
return -(pv + fv) / nper;
292+
}
293+
294+
const te_type powVal = std::pow(1 + rate, nper);
295+
if (!std::isfinite(powVal) || powVal == 0.0)
296+
{
297+
return te_parser::te_nan;
298+
}
299+
300+
return -((pv * powVal) + fv) * rate / ((1 + (rate * type)) * (powVal - 1));
301+
}
302+
258303
[[nodiscard]]
259304
static te_type te_pv(te_type rate, te_type nper, te_type pmt, te_type futureValue, te_type type)
260305
{
@@ -298,7 +343,8 @@ namespace te_builtins
298343
return te_parser::te_nan;
299344
}
300345

301-
return -(futureValue + pmt * (1 + rate * type) * (powVal - 1) / rate) / powVal;
346+
const te_type annuity = (pmt * (1 + (rate * type)) * (powVal - 1)) / rate;
347+
return -(futureValue + annuity) / powVal;
302348
}
303349

304350
[[nodiscard]]
@@ -1545,6 +1591,8 @@ const std::set<te_variable> te_parser::m_functions = { // NOLINT
15451591
{ "pi", static_cast<te_fun0>(te_builtins::te_pi), TE_PURE },
15461592
{ "pow", static_cast<te_fun2>(te_builtins::te_pow), TE_PURE },
15471593
{ "power", /* Excel alias*/ static_cast<te_fun2>(te_builtins::te_pow), TE_PURE },
1594+
{ "pmt", static_cast<te_fun5>(te_builtins::te_pmt),
1595+
static_cast<te_variable_flags>(TE_PURE | TE_VARIADIC) },
15481596
{ "pv", static_cast<te_fun5>(te_builtins::te_pv),
15491597
static_cast<te_variable_flags>(TE_PURE | TE_VARIADIC) },
15501598
{ "rand", static_cast<te_fun0>(te_builtins::te_random), TE_PURE },

0 commit comments

Comments
 (0)