+69
-18
@@ -38,20 +38,62 @@ "use strict"; | ||
| /** | ||
| * @type {import("../types").GetNextInstalmentPartFunction} | ||
| */ | ||
| function getNextDiminishingInstalment(amount, installmentsNumber, capitalSum, interestRateMonth) { | ||
| var capital = rnd(amount / installmentsNumber); | ||
| var interest = rnd((amount - capitalSum) * interestRateMonth); | ||
| var installment = capital + interest; | ||
| return { | ||
| capital: capital, | ||
| interest: interest, | ||
| installment: installment | ||
| }; | ||
| } | ||
| /** | ||
| * @type {import("../types").GetNextInstalmentPartFunction} | ||
| */ | ||
| function getNextAnnuityInstalment(amount, installmentsNumber, capitalSum, interestRateMonth) { | ||
| var z = 1 / (1 + interestRateMonth); | ||
| var div = (1 - Math.pow(z, installmentsNumber)) * z; | ||
| var installment = rnd(amount * (1 - z) / div); | ||
| var interest = rnd((amount - capitalSum) * interestRateMonth); | ||
| var capital = installment - interest; | ||
| return { | ||
| capital: capital, | ||
| interest: interest, | ||
| installment: installment | ||
| }; | ||
| } | ||
| /** | ||
| * @type {import("../types").GetNextInstalmentPartFunction} | ||
| */ | ||
| function getNextAnnuityDueInstalment(amount, installmentsNumber, capitalSum, interestRateMonth) { | ||
| var z = 1 / (1 + interestRateMonth); | ||
| var div = 1 - Math.pow(z, installmentsNumber); | ||
| var installment = rnd(amount * (1 - z) / div); | ||
| var interest = rnd((amount - capitalSum) * interestRateMonth); | ||
| var capital = installment - interest; | ||
| return { | ||
| capital: capital, | ||
| interest: interest, | ||
| installment: installment | ||
| }; | ||
| } | ||
| var nextInstalmentFnMap = { | ||
| annuity: getNextAnnuityInstalment, | ||
| diminishing: getNextDiminishingInstalment, | ||
| annuityDue: getNextAnnuityDueInstalment | ||
| }; | ||
| /** | ||
| * @type {import("../types").GetNextInstalmentFunction} | ||
| */ | ||
| var getNextInstalment = function getNextInstalment(amount, installmentsNumber, interestRate, diminishing, capitalSum, interestSum) { | ||
| var capital; | ||
| var interest; | ||
| var installment; | ||
| var irmPow; | ||
| function getNextInstalment(amount, installmentsNumber, interestRate, loanType, capitalSum, interestSum) { | ||
| var interestRateMonth = interestRate / 1200; | ||
| if (diminishing) { | ||
| capital = rnd(amount / installmentsNumber); | ||
| interest = rnd((amount - capitalSum) * interestRateMonth); | ||
| installment = capital + interest; | ||
| } else { | ||
| irmPow = Math.pow(1 + interestRateMonth, installmentsNumber); | ||
| installment = rnd(amount * (interestRateMonth * irmPow / (irmPow - 1))); | ||
| interest = rnd((amount - capitalSum) * interestRateMonth); | ||
| capital = installment - interest; | ||
| } | ||
| var nextInstalmentFn = typeof loanType === 'function' ? loanType : nextInstalmentFnMap[loanType] || getNextAnnuityInstalment; | ||
| var _nextInstalmentFn = nextInstalmentFn(amount, installmentsNumber, capitalSum, interestRateMonth), | ||
| capital = _nextInstalmentFn.capital, | ||
| interest = _nextInstalmentFn.interest, | ||
| installment = _nextInstalmentFn.installment; | ||
| return { | ||
@@ -64,3 +106,3 @@ capital: capital, | ||
| }; | ||
| }; | ||
| } | ||
@@ -71,3 +113,3 @@ /** | ||
| function Loan(amount, installmentsNumber, interestRate) { | ||
| var diminishing = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; | ||
| var loanTypeWithBool = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'annuity'; | ||
| if (!amount || amount <= 0 || !installmentsNumber || installmentsNumber <= 0 || !interestRate || interestRate <= 0) { | ||
@@ -77,2 +119,3 @@ throw new Error("wrong parameters: ".concat(amount, " ").concat(installmentsNumber, " ").concat(interestRate)); | ||
| var installments = []; | ||
| var loanType = typeof loanTypeWithBool === 'boolean' ? loanTypeFromBool(loanTypeWithBool) : loanTypeWithBool; | ||
| var interestSum = 0; | ||
@@ -82,3 +125,3 @@ var capitalSum = 0; | ||
| for (var i = 0; i < installmentsNumber; i++) { | ||
| var inst = getNextInstalment(amount, installmentsNumber, interestRate, diminishing, capitalSum, interestSum); | ||
| var inst = getNextInstalment(amount, installmentsNumber, interestRate, loanType, capitalSum, interestSum); | ||
| sum += inst.installment; | ||
@@ -104,2 +147,10 @@ capitalSum += inst.capital; | ||
| /** | ||
| * @param {boolean} boolType | ||
| * @returns {import("../types").LoanType} | ||
| */ | ||
| function loanTypeFromBool(boolType) { | ||
| return boolType ? 'diminishing' : 'annuity'; | ||
| } | ||
| /* istanbul ignore next */ | ||
@@ -106,0 +157,0 @@ if (typeof window !== 'undefined') { |
+1
-1
@@ -1,1 +0,1 @@ | ||
| "use strict";function _typeof(n){return(_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(n){return typeof n}:function(n){return n&&"function"==typeof Symbol&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n})(n)}!function(n){"object"===("undefined"==typeof exports?"undefined":_typeof(exports))&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):("undefined"!=typeof globalThis?globalThis:self).loan=n()}(function(){function w(n){return Math.round(100*n)/100}var n;function t(n,t,e){var o=3<arguments.length&&void 0!==arguments[3]&&arguments[3];if(!n||n<=0||!t||t<=0||!e||e<=0)throw new Error("wrong parameters: ".concat(n," ").concat(t," ").concat(e));for(var r,i,a,f,u,l,p,c,m=[],s=0,d=0,y=0,S=0;S<t;S++){r=n,b=t,i=e,f=d,u=s,c=p=l=void 0,i/=1200,(a=o)?c=(l=w(r/b))+(p=w((r-f)*i)):l=(c=w(r*(i*(a=Math.pow(1+i,b))/(a-1))))-(p=w((r-f)*i));var b={capital:l,interest:p,installment:c,remain:r-f-l,interestSum:u+p};y+=b.installment,d+=b.capital,s+=b.interest,S===t-1&&(d+=b.remain,y+=b.remain,b.remain=0),m.push(b)}return{installments:m,amount:w(n),interestSum:w(s),capitalSum:w(d),sum:w(y)}}return"undefined"!=typeof window&&((n=window).LoanJS||(n.LoanJS={}),n.LoanJS.Loan=t),t}); | ||
| "use strict";function _typeof(n){return(_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(n){return typeof n}:function(n){return n&&"function"==typeof Symbol&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n})(n)}!function(n){"object"===("undefined"==typeof exports?"undefined":_typeof(exports))&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):("undefined"!=typeof globalThis?globalThis:self).loan=n()}(function(){function b(n){return Math.round(100*n)/100}function S(n,t,e,o){var i=1/(1+o),t=b(n*(1-i)/((1-Math.pow(i,t))*i)),i=b((n-e)*o);return{capital:t-i,interest:i,installment:t}}var n,h={annuity:S,diminishing:function(n,t,e,o){return t=b(n/t),n=b((n-e)*o),{capital:t,interest:n,installment:t+n}},annuityDue:function(n,t,e,o){var i=b(n*(1-(i=1/(1+o)))/(1-Math.pow(i,t))),t=b((n-e)*o);return{capital:i-t,interest:t,installment:i}}};function t(n,t,e){var o=3<arguments.length&&void 0!==arguments[3]?arguments[3]:"annuity";if(!n||n<=0||!t||t<=0||!e||e<=0)throw new Error("wrong parameters: ".concat(n," ").concat(t," ").concat(e));for(var i,a,r,u,f,l=[],s="boolean"==typeof o?o?"diminishing":"annuity":o,c=0,p=0,m=0,y=0;y<t;y++){i=n,a=t,r=e,u=p,f=c,d=("function"==typeof(d=s)?d:h[d]||S)(i,a,u,r/1200),a=d.capital,r=d.interest;var d={capital:a,interest:r,installment:d.installment,remain:i-u-a,interestSum:f+r};m+=d.installment,p+=d.capital,c+=d.interest,y===t-1&&(p+=d.remain,m+=d.remain,d.remain=0),l.push(d)}return{installments:l,amount:b(n),interestSum:b(c),capitalSum:b(p),sum:b(m)}}return"undefined"!=typeof window&&((n=window).LoanJS||(n.LoanJS={}),n.LoanJS.Loan=t),t}); |
+59
-20
@@ -13,29 +13,59 @@ /* | ||
| /** | ||
| * @type {import("../types").GetNextInstalmentPartFunction} | ||
| */ | ||
| function getNextDiminishingInstalment (amount, installmentsNumber, capitalSum, interestRateMonth) { | ||
| const capital = rnd(amount / installmentsNumber) | ||
| const interest = rnd((amount - capitalSum) * interestRateMonth) | ||
| const installment = capital + interest | ||
| return { capital, interest, installment } | ||
| } | ||
| /** | ||
| * @type {import("../types").GetNextInstalmentPartFunction} | ||
| */ | ||
| function getNextAnnuityInstalment (amount, installmentsNumber, capitalSum, interestRateMonth) { | ||
| const z = 1 / (1 + interestRateMonth) | ||
| const div = (1 - Math.pow(z, installmentsNumber)) * z | ||
| const installment = rnd(amount * (1 - z) / div) | ||
| const interest = rnd((amount - capitalSum) * interestRateMonth) | ||
| const capital = installment - interest | ||
| return { capital, interest, installment } | ||
| } | ||
| /** | ||
| * @type {import("../types").GetNextInstalmentPartFunction} | ||
| */ | ||
| function getNextAnnuityDueInstalment (amount, installmentsNumber, capitalSum, interestRateMonth) { | ||
| const z = 1 / (1 + interestRateMonth) | ||
| const div = (1 - Math.pow(z, installmentsNumber)) | ||
| const installment = rnd(amount * (1 - z) / div) | ||
| const interest = rnd((amount - capitalSum) * interestRateMonth) | ||
| const capital = installment - interest | ||
| return { capital, interest, installment } | ||
| } | ||
| const nextInstalmentFnMap = { | ||
| annuity: getNextAnnuityInstalment, | ||
| diminishing: getNextDiminishingInstalment, | ||
| annuityDue: getNextAnnuityDueInstalment | ||
| } | ||
| /** | ||
| * @type {import("../types").GetNextInstalmentFunction} | ||
| */ | ||
| const getNextInstalment = ( | ||
| function getNextInstalment ( | ||
| amount, | ||
| installmentsNumber, | ||
| interestRate, | ||
| diminishing, | ||
| loanType, | ||
| capitalSum, | ||
| interestSum | ||
| ) => { | ||
| let capital | ||
| let interest | ||
| let installment | ||
| let irmPow | ||
| ) { | ||
| const interestRateMonth = interestRate / 1200 | ||
| const nextInstalmentFn = typeof loanType === 'function' ? loanType : nextInstalmentFnMap[loanType] || getNextAnnuityInstalment | ||
| const { capital, interest, installment } = nextInstalmentFn(amount, installmentsNumber, capitalSum, interestRateMonth) | ||
| if (diminishing) { | ||
| capital = rnd(amount / installmentsNumber) | ||
| interest = rnd((amount - capitalSum) * interestRateMonth) | ||
| installment = capital + interest | ||
| } else { | ||
| irmPow = Math.pow(1 + interestRateMonth, installmentsNumber) | ||
| installment = rnd(amount * ((interestRateMonth * irmPow) / (irmPow - 1))) | ||
| interest = rnd((amount - capitalSum) * interestRateMonth) | ||
| capital = installment - interest | ||
| } | ||
| return { | ||
@@ -53,3 +83,3 @@ capital, | ||
| */ | ||
| function Loan (amount, installmentsNumber, interestRate, diminishing = false) { | ||
| function Loan (amount, installmentsNumber, interestRate, loanTypeWithBool = 'annuity') { | ||
| if ( | ||
@@ -69,2 +99,3 @@ !amount || | ||
| const installments = [] | ||
| const loanType = typeof loanTypeWithBool === 'boolean' ? loanTypeFromBool(loanTypeWithBool) : loanTypeWithBool | ||
| let interestSum = 0 | ||
@@ -79,3 +110,3 @@ let capitalSum = 0 | ||
| interestRate, | ||
| diminishing, | ||
| loanType, | ||
| capitalSum, | ||
@@ -107,2 +138,10 @@ interestSum | ||
| /** | ||
| * @param {boolean} boolType | ||
| * @returns {import("../types").LoanType} | ||
| */ | ||
| function loanTypeFromBool (boolType) { | ||
| return boolType ? 'diminishing' : 'annuity' | ||
| } | ||
| /* istanbul ignore next */ | ||
@@ -109,0 +148,0 @@ if (typeof window !== 'undefined') { |
+60
-1
@@ -9,3 +9,3 @@ /* eslint-env jest */ | ||
| test('loan js should count loan with equal instalment', () => { | ||
| test('loan js should count loan with equal instalment (annuity)', () => { | ||
| const loan = Loan(20000, 30 * 12, 5, false) | ||
@@ -25,2 +25,19 @@ | ||
| test('loan js should count loan with equal instalment (annuity) as default', () => { | ||
| /** @type any */ | ||
| const LoanToTest = Loan | ||
| const loan = LoanToTest(20000, 30 * 12, 5, 'wrong-type-as-annuity') | ||
| expect(loan).toBeDefined() | ||
| expect(loan.installments.length).toBe(30 * 12) | ||
| expect(loan.installments[0].installment).toBe(107.36) | ||
| expect(loan.installments[30 * 12 - 1].installment).toBe(107.36) | ||
| expect(loan.amount).toBe(20000) | ||
| expect(loan.capitalSum).toBe(20000) | ||
| expect(loan.interestSum).toBe(18653.27) | ||
| expect(loan.sum).toBe(38653.27) | ||
| expect(rnd(loan.capitalSum + loan.interestSum)).toBe(loan.sum) | ||
| }) | ||
| test('loan js should count loan with dismissing instalment', () => { | ||
@@ -42,2 +59,44 @@ const loan = Loan(20000, 30 * 12, 5, true) | ||
| test('loan js should count loan with any loanType function', () => { | ||
| /** | ||
| * @type {import("../types").GetNextInstalmentPartFunction} | ||
| */ | ||
| function getNext10Instalment (amount, installmentsNumber, capitalSum, interestRateMonth) { | ||
| const capital = rnd(amount / installmentsNumber) | ||
| const interest = 10 | ||
| const installment = capital + interest | ||
| return { capital, interest, installment } | ||
| } | ||
| const loan = Loan(20000, 30 * 12, 5, getNext10Instalment) | ||
| expect(loan).toBeDefined() | ||
| expect(loan.installments.length).toBe(30 * 12) | ||
| expect(loan.installments[0].installment).toBe(65.56) | ||
| expect(loan.installments[30 * 12 - 1].installment).toBe(65.56) | ||
| expect(loan.amount).toBe(20000) | ||
| expect(loan.capitalSum).toBe(20000) | ||
| expect(loan.interestSum).toBe(3600) | ||
| expect(loan.sum).toBe(23600) | ||
| expect(rnd(loan.capitalSum + loan.interestSum)).toBe(loan.sum) | ||
| }) | ||
| test('loan js should count loan with annuity due function', () => { | ||
| const loan = Loan(30000, 30 * 12, 5, 'annuityDue') | ||
| expect(loan).toBeDefined() | ||
| expect(loan.installments.length).toBe(360) | ||
| expect(loan.installments[0].installment).toBe(160.38) | ||
| expect(loan.installments[23].installment).toBe(160.38) | ||
| expect(loan.amount).toBe(30000) | ||
| expect(loan.capitalSum).toBe(30000) | ||
| expect(loan.interestSum).toBe(28291.49) | ||
| expect(loan.sum).toBe(58291.49) | ||
| expect(rnd(loan.capitalSum + loan.interestSum)).toBe(loan.sum) | ||
| }) | ||
| test('loan js should throw on wrong params', () => { | ||
@@ -44,0 +103,0 @@ /** @type any */ |
+1
-1
| { | ||
| "name": "loanjs", | ||
| "version": "1.0.12", | ||
| "version": "1.1.0", | ||
| "main": "LoanJS.js", | ||
@@ -5,0 +5,0 @@ "description": "Calculate loan in js (browser/node.js) for equal installments, installments decreasing, the sum of interest, etc.", |
+63
-25
@@ -7,9 +7,8 @@ # LoanJS | ||
| Super **small** (**~500B**) and **fast** module to calculate loan in js (browser/node.js) for **equal**/**decreasing** installments, the **sum of interest**, etc. | ||
| Super **small** (**~500B**) and **fast** module to calculate loan in js (browser/node.js) for **equal**/**decreasing**/**annuity**/**annuityDue** installments, the **sum of interest**, etc, with TypeScript support | ||
| Now with TypeScript support | ||
| ## Getting Started | ||
| Install the module with: | ||
| Install with: | ||
| ``` | ||
@@ -19,7 +18,2 @@ npm install loanjs | ||
| or Bower: | ||
| ``` | ||
| bower install loan-js --save | ||
| ``` | ||
| #### Calculating Loan: | ||
@@ -35,3 +29,3 @@ ```js | ||
| 5, // interest rate | ||
| true // diminishing | ||
| 'annuity' // loanType: 'annuity' | 'annuityDue' | 'diminishing' | GetNextInstalmentPartFunction | ||
| ); | ||
@@ -59,12 +53,27 @@ /** returns | ||
| ### Loan | ||
| LoanJS.Loan(amount, installmentsNumber, interestRate, diminishing) | ||
| LoanJS.Loan(amount, installmentsNumber, interestRate, loanType) | ||
| ### Arguments | ||
| | Argument | type | default | Description | ||
| | ------------------ | ------ | --------- | ------------------ | ||
| | amount | number | *required | full amount of Loan | ||
| | installmentsNumber | number | *required | how many installments will be (in months) | ||
| | interestRate | number | *required | interest rate in percent (ex. 3.5) | ||
| | diminishing | bool | false | if installments will be - true: diminishing; false: equal/annuity | ||
| | Argument | type | default | Description | ||
| | ------------------ | -------------- | --------- | ------------------ | ||
| | amount | number | *required | full amount of Loan | ||
| | installmentsNumber | number | *required | how many installments will be (in months) | ||
| | interestRate | number | *required | interest rate in percent (ex. 3.5) | ||
| | loanType | string or fn | 'annuity' | 'annuity' | 'annuityDue' | 'diminishing' | GetNextInstalmentPartFunction | ||
| ```ts | ||
| interface InstallmentPart { | ||
| capital: number; | ||
| interest: number; | ||
| installment: number; | ||
| } | ||
| type GetNextInstalmentPartFunction = ( | ||
| amount: number, | ||
| installmentsNumber: number, | ||
| interestRateMonth: number, | ||
| capitalSum: number | ||
| ) => InstallmentPart; | ||
| ``` | ||
| ### Returns | ||
@@ -90,15 +99,38 @@ ```js | ||
| nodejs / browserify example | ||
| ### typescript example | ||
| ```ts | ||
| import { Loan } from 'loanjs'; | ||
| const annuityLoan = new Loan(1000, 12, 5, 'annuity'); | ||
| const annuityDueLoan = new Loan(1000, 12, 5, 'annuityDue'); | ||
| const diminishingLoan = new Loan(1000, 12, 5, 'diminishing'); | ||
| const customInstalmentLoan = new Loan(1000, 12, 5, getNext10Instalment); | ||
| function getNext10Instalment (amount: number, installmentsNumber: number, capitalSum: number, interestRateMonth: number) { | ||
| const capital = rnd(amount / installmentsNumber); | ||
| const interest = 10; | ||
| const installment = capital + interest; | ||
| return { capital, interest, installment }; | ||
| } | ||
| ``` | ||
| ### nodejs example | ||
| ```js | ||
| import { Loan } from 'loanjs'; | ||
| // or | ||
| const { Loan } = require('loanjs'); | ||
| const loan_1 = new Loan(1000, 12, 5, true); | ||
| // loan on 1 000($) in 12 diminishing installments (ex. months) with 5% interest rate | ||
| const loan_1 = new Loan(1000, 12, 5, 'diminishing'); | ||
| // loan on 1 000($) in 12 loanType installments (ex. months) with 5% interest rate | ||
| const loan_2 = new Loan(500000, 360, 3.5); | ||
| const loan_2 = new Loan(500000, 360, 3.5, 'annuity'); | ||
| // loan on 500 000($) in 360 equal installments (30 years) with 3.5% interest rate | ||
| ``` | ||
| Browser example: | ||
| ### Browser example: | ||
| > You can also render loan as html table | ||
@@ -110,5 +142,5 @@ | ||
| <script> | ||
| var loan = new LoanJS.Loan(1000, 12, 5, true); | ||
| const loan = new LoanJS.Loan(1000, 12, 5, 'annuity'); | ||
| var div = document.createElement("div"); | ||
| const div = document.createElement("div"); | ||
| div.innerHTML = LoanJS.loanToHtmlTable(loan); // loan rendering as html table string | ||
@@ -131,2 +163,8 @@ document.body.appendChild(div); | ||
| #### 2023-06-23 v1.1.0 | ||
| * add `annuityDue` interest rate loan type | ||
| * changing the fourth argument `diminishing` to `loanType` (`annuity` | `diminishing` | `annuityDue`), with backward compatibility (false == 'annuity', true == 'diminishing') | ||
| * refactor getNextInstalment to be open for extensions | ||
| * add option to provide function to loanType to customize instalments counting | ||
| #### 2023-06-23 v1.0.11 | ||
@@ -133,0 +171,0 @@ * add TypeScript types |
+17
-2
@@ -21,3 +21,3 @@ export interface Installment { | ||
| interestRate: number, | ||
| diminishing: boolean, | ||
| loanType: LoanType, | ||
| capitalSum: number, | ||
@@ -27,2 +27,17 @@ interestSum: number | ||
| export interface InstallmentPart { | ||
| capital: number; | ||
| interest: number; | ||
| installment: number; | ||
| } | ||
| export type GetNextInstalmentPartFunction = ( | ||
| amount: number, | ||
| installmentsNumber: number, | ||
| interestRateMonth: number, | ||
| capitalSum: number | ||
| ) => InstallmentPart; | ||
| export type LoanType = 'annuity' | 'annuityDue' | 'diminishing' | GetNextInstalmentPartFunction | ||
| export type LoanFunction = ( | ||
@@ -32,3 +47,3 @@ amount: number, | ||
| interestRate: number, | ||
| diminishing = false | ||
| loanType: LoanType | false | true = 'annuity' | ||
| ) => LoanInstance; | ||
@@ -35,0 +50,0 @@ |
34773
27.65%680
25.46%192
24.68%