feat(core): initial release of financial calculation package with decimal precision
Some checks failed
CI Pipeline (nottags) / security (push) Successful in 17s
CI Pipeline (tags) / security (push) Successful in 17s
CI Pipeline (nottags) / test (push) Failing after 52s
CI Pipeline (tags) / test (push) Failing after 50s
CI Pipeline (tags) / release (push) Has been skipped
CI Pipeline (tags) / metadata (push) Has been skipped
Some checks failed
CI Pipeline (nottags) / security (push) Successful in 17s
CI Pipeline (tags) / security (push) Successful in 17s
CI Pipeline (nottags) / test (push) Failing after 52s
CI Pipeline (tags) / test (push) Failing after 50s
CI Pipeline (tags) / release (push) Has been skipped
CI Pipeline (tags) / metadata (push) Has been skipped
This commit is contained in:
59
.gitea/workflows/default_nottags.yaml
Normal file
59
.gitea/workflows/default_nottags.yaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: "CI Pipeline (nottags)"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
tags-ignore:
|
||||||
|
- "**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
container:
|
||||||
|
image: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up npmci
|
||||||
|
run: npmci node install stable
|
||||||
|
|
||||||
|
- name: Prepare project
|
||||||
|
run: npmci npm prepare
|
||||||
|
|
||||||
|
- name: Audit production dependencies
|
||||||
|
run: npmci command npm audit --audit-level=high --only=prod
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Audit development dependencies
|
||||||
|
run: npmci command npm audit --audit-level=high --only=dev
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
test:
|
||||||
|
needs: security
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up npmci
|
||||||
|
run: npmci node install stable
|
||||||
|
|
||||||
|
- name: Prepare project
|
||||||
|
run: npmci npm prepare
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npmci npm install
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npmci npm test
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
run: npmci npm build
|
||||||
|
|
||||||
|
- name: Build documentation
|
||||||
|
run: npmci npm buildDocs
|
123
.gitea/workflows/default_tags.yaml
Normal file
123
.gitea/workflows/default_tags.yaml
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
name: "CI Pipeline (tags)"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
env:
|
||||||
|
NPMCI_TOKEN_NPM: ${{ secrets.NPMCI_TOKEN_NPM }}
|
||||||
|
NPMCI_TOKEN_NPM2: ${{ secrets.NPMCI_TOKEN_NPM2 }}
|
||||||
|
NPMCI_GIT_GITHUBTOKEN: ${{ secrets.NPMCI_GIT_GITHUBTOKEN }}
|
||||||
|
NPMCI_URL_CLOUDFLARE: ${{ secrets.NPMCI_URL_CLOUDFLARE }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
container:
|
||||||
|
image: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up npmci
|
||||||
|
run: npmci node install stable
|
||||||
|
|
||||||
|
- name: Prepare project
|
||||||
|
run: npmci npm prepare
|
||||||
|
|
||||||
|
- name: Audit production dependencies
|
||||||
|
run: npmci command npm audit --audit-level=high --only=prod
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Audit development dependencies
|
||||||
|
run: npmci command npm audit --audit-level=high --only=dev
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
test:
|
||||||
|
needs: security
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up npmci
|
||||||
|
run: npmci node install stable
|
||||||
|
|
||||||
|
- name: Prepare project
|
||||||
|
run: npmci npm prepare
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npmci npm install
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npmci npm test
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
run: npmci npm build
|
||||||
|
|
||||||
|
- name: Build documentation
|
||||||
|
run: npmci npm buildDocs
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: test
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up npmci
|
||||||
|
run: npmci node install stable
|
||||||
|
|
||||||
|
- name: Prepare project
|
||||||
|
run: npmci npm prepare
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npmci npm install
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
run: npmci npm build
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
run: npmci npm publish
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
needs: test
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up npmci
|
||||||
|
run: npmci node install stable
|
||||||
|
|
||||||
|
- name: Prepare project
|
||||||
|
run: npmci npm prepare
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npmci npm install
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
run: npmci npm build
|
||||||
|
|
||||||
|
- name: Generate documentation
|
||||||
|
run: npmci npm buildDocs
|
||||||
|
|
||||||
|
- name: Generate metadata
|
||||||
|
run: npmci command npm pack --dry-run --json
|
||||||
|
|
||||||
|
- name: Trigger further builds
|
||||||
|
run: npmci trigger
|
||||||
|
|
||||||
|
- name: Build and deploy docs
|
||||||
|
run: npmci node install stable && npmci npm prepare && npmci npm install && npmci npm buildDocs
|
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist_*/
|
||||||
|
dist/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Test outputs
|
||||||
|
.nogit/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
13
.vscode/settings.json
vendored
Normal file
13
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": "explicit"
|
||||||
|
},
|
||||||
|
"files.exclude": {
|
||||||
|
"node_modules": true,
|
||||||
|
"dist_*": true,
|
||||||
|
".nogit": true
|
||||||
|
}
|
||||||
|
}
|
14
changelog.md
Normal file
14
changelog.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [1.0.0] - 2025-07-29
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release of @fin.cx/calculation
|
||||||
|
- Calculator class with high-precision decimal arithmetic
|
||||||
|
- Financial class with time value of money calculations (PV, FV, NPV, IRR, XIRR)
|
||||||
|
- Interest class with simple, compound, and continuous interest calculations
|
||||||
|
- Amortization class with loan schedules and payment analysis
|
||||||
|
- Currency class with conversion, formatting, and monetary operations
|
||||||
|
- Comprehensive test suite
|
||||||
|
- Full TypeScript support
|
||||||
|
- CI/CD workflows for automated testing and publishing
|
21
license.md
Normal file
21
license.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 fin.cx
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
4
npmextra.json
Normal file
4
npmextra.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"npmGlobalTools": [],
|
||||||
|
"npmAccessLevel": "public"
|
||||||
|
}
|
47
package.json
Normal file
47
package.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "@fin.cx/calculation",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": false,
|
||||||
|
"description": "A unified, cross-platform financial calculation package with decimal precision for accurate financial computations",
|
||||||
|
"main": "dist_ts/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "(tstest test/ --verbose)",
|
||||||
|
"build": "(tsbuild tsfolders --allowimplicitany && tsbundle npm)",
|
||||||
|
"buildDocs": "tsdoc"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "code.foss.global/fin.cx/calculation.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"financial",
|
||||||
|
"calculation",
|
||||||
|
"finance",
|
||||||
|
"interest",
|
||||||
|
"amortization",
|
||||||
|
"npv",
|
||||||
|
"irr",
|
||||||
|
"decimal",
|
||||||
|
"precision"
|
||||||
|
],
|
||||||
|
"author": "fin.cx",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/fin-cx/calculation/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/fin-cx/calculation#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@git.zone/tsbuild": "^2.6.4",
|
||||||
|
"@git.zone/tsbundle": "^2.4.0",
|
||||||
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
|
"@git.zone/tstest": "^2.3.1",
|
||||||
|
"@push.rocks/tapbundle": "^6.0.3",
|
||||||
|
"@types/node": "^22.7.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
|
"decimal.js": "^10.4.3"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.11.0"
|
||||||
|
}
|
10045
pnpm-lock.yaml
generated
Normal file
10045
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
packages:
|
||||||
|
- '.'
|
36
readme.hints.md
Normal file
36
readme.hints.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Project Hints
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This project follows the @push.rocks/smartstate pattern with the following structure:
|
||||||
|
- TypeScript source files in `ts/` directory
|
||||||
|
- Test files in `test/` directory
|
||||||
|
- Built files output to `dist_ts/` directory
|
||||||
|
- ES modules configuration (`"type": "module"`)
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
1. **Decimal Precision**: Uses decimal.js library to avoid JavaScript floating-point errors
|
||||||
|
2. **Class Hierarchy**: Calculator is the base class, other classes extend it for specific domains
|
||||||
|
3. **Naming Convention**: Files follow pattern `calculation.classes.*.ts`
|
||||||
|
4. **Plugin Pattern**: Dependencies are imported through `calculation.plugins.ts`
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
1. Run tests: `pnpm test`
|
||||||
|
2. Build project: `pnpm build`
|
||||||
|
3. Type checking: `tsbuild check test/**/* --skiplibcheck`
|
||||||
|
4. Testing uses @git.zone/tstest with expect from @push.rocks/tapbundle
|
||||||
|
|
||||||
|
## Financial Calculations
|
||||||
|
|
||||||
|
- All monetary calculations use decimal arithmetic for accuracy
|
||||||
|
- Interest calculations support multiple compounding frequencies
|
||||||
|
- Financial functions follow standard financial formulas
|
||||||
|
- Currency class handles formatting based on locale conventions
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
- Cross-platform tests in `test.both.ts`
|
||||||
|
- Tests cover accuracy, edge cases, and error handling
|
||||||
|
- Precision tests ensure decimal accuracy is maintained
|
406
readme.md
Normal file
406
readme.md
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
# @fin.cx/calculation
|
||||||
|
|
||||||
|
> Professional financial calculations with decimal precision for JavaScript and TypeScript
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/@fin.cx/calculation)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
## 🎯 Why @fin.cx/calculation?
|
||||||
|
|
||||||
|
JavaScript's floating-point arithmetic can lead to precision errors in financial calculations:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// JavaScript floating point problem
|
||||||
|
0.1 + 0.2 // 0.30000000000000004 ❌
|
||||||
|
|
||||||
|
// With @fin.cx/calculation
|
||||||
|
calculator.add(0.1, 0.2) // 0.3 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
This package provides **accurate financial calculations** using decimal arithmetic, making it perfect for:
|
||||||
|
|
||||||
|
- 🏦 Banking applications
|
||||||
|
- 💰 Investment analysis tools
|
||||||
|
- 📊 Financial reporting systems
|
||||||
|
- 🏢 Accounting software
|
||||||
|
- 💳 Payment processing systems
|
||||||
|
- 📈 Trading platforms
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- **🔢 Decimal Precision**: Built on decimal.js for accurate calculations
|
||||||
|
- **📐 Comprehensive Functions**: Time value of money, interest calculations, loan amortization, and more
|
||||||
|
- **💱 Currency Support**: Built-in currency formatting and conversion
|
||||||
|
- **🌍 Cross-Platform**: Works in Node.js and browsers
|
||||||
|
- **📘 TypeScript First**: Full type safety and IntelliSense support
|
||||||
|
- **🧪 Battle-Tested**: Extensive test suite ensures reliability
|
||||||
|
- **🚀 Zero Dependencies**: Only depends on decimal.js for core functionality
|
||||||
|
|
||||||
|
## 📦 Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @fin.cx/calculation
|
||||||
|
# or
|
||||||
|
yarn add @fin.cx/calculation
|
||||||
|
# or
|
||||||
|
pnpm add @fin.cx/calculation
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Calculator, Financial, Currency } from '@fin.cx/calculation';
|
||||||
|
|
||||||
|
// Basic calculations with precision
|
||||||
|
const calc = new Calculator();
|
||||||
|
const result = calc.add(0.1, 0.2); // 0.3 (exact)
|
||||||
|
|
||||||
|
// Financial calculations
|
||||||
|
const financial = new Financial();
|
||||||
|
const payment = financial.payment(200000, 0.045/12, 360); // Monthly mortgage payment
|
||||||
|
|
||||||
|
// Currency operations
|
||||||
|
const currency = new Currency();
|
||||||
|
currency.setExchangeRate('USD', 'EUR', 0.85);
|
||||||
|
const euros = currency.convert(100, 'USD', 'EUR'); // Convert $100 to €85
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Core Classes
|
||||||
|
|
||||||
|
### Calculator - Precision Arithmetic
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const calc = new Calculator({ precision: 10 });
|
||||||
|
|
||||||
|
// Basic operations
|
||||||
|
calc.add(10.5, 20.3); // 30.8
|
||||||
|
calc.subtract(100, 45.5); // 54.5
|
||||||
|
calc.multiply(15, 3.5); // 52.5
|
||||||
|
calc.divide(100, 3); // 33.3333333333...
|
||||||
|
|
||||||
|
// Advanced operations
|
||||||
|
calc.power(2, 8); // 256
|
||||||
|
calc.sqrt(16); // 4
|
||||||
|
calc.ln(Math.E); // 1
|
||||||
|
calc.round(3.14159, 2); // 3.14
|
||||||
|
```
|
||||||
|
|
||||||
|
### Financial - Time Value of Money
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const financial = new Financial();
|
||||||
|
|
||||||
|
// Present Value - How much is $10,000 in 5 years worth today at 5% interest?
|
||||||
|
const pv = financial.presentValue(10000, 0.05, 5); // $7,835.26
|
||||||
|
|
||||||
|
// Future Value - How much will $5,000 be worth in 10 years at 7% interest?
|
||||||
|
const fv = financial.futureValue(5000, 0.07, 10); // $9,835.76
|
||||||
|
|
||||||
|
// Loan Payment - Monthly payment for $200,000 mortgage at 4.5% for 30 years
|
||||||
|
const pmt = financial.payment(200000, 0.045/12, 360); // $1,013.37
|
||||||
|
|
||||||
|
// Net Present Value - Is this investment worth it?
|
||||||
|
const cashFlows = [-50000, 15000, 15000, 15000, 15000, 20000];
|
||||||
|
const npv = financial.npv(0.1, cashFlows); // $7,946.11 (positive = good investment)
|
||||||
|
|
||||||
|
// Internal Rate of Return - What's the return rate?
|
||||||
|
const irr = financial.irr(cashFlows); // 16.89%
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interest - Various Interest Calculations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const interest = new Interest();
|
||||||
|
|
||||||
|
// Simple Interest - $1,000 at 5% for 2 years
|
||||||
|
const simple = interest.simple(1000, 0.05, 2); // $100
|
||||||
|
|
||||||
|
// Compound Interest - $1,000 at 5% for 2 years, compounded monthly
|
||||||
|
const compound = interest.compound(1000, 0.05, 2, 'monthly'); // $104.94
|
||||||
|
|
||||||
|
// Effective Annual Rate - What's the real rate with monthly compounding?
|
||||||
|
const ear = interest.effectiveAnnualRate(0.12, 'monthly'); // 12.68%
|
||||||
|
|
||||||
|
// Rule of 72 - How long to double your money at 8%?
|
||||||
|
const years = interest.ruleOf72(8); // ~9 years
|
||||||
|
```
|
||||||
|
|
||||||
|
### Amortization - Loan Schedules
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const amortization = new Amortization();
|
||||||
|
|
||||||
|
// Generate complete loan schedule
|
||||||
|
const schedule = amortization.schedule({
|
||||||
|
principal: 250000,
|
||||||
|
annualRate: 0.045,
|
||||||
|
termYears: 30,
|
||||||
|
extraPayment: 200 // Optional extra monthly payment
|
||||||
|
});
|
||||||
|
|
||||||
|
// Analyze the schedule
|
||||||
|
console.log(`Monthly Payment: $${schedule.monthlyPayment.toFixed(2)}`);
|
||||||
|
console.log(`Total Interest: $${schedule.totalInterest.toFixed(2)}`);
|
||||||
|
console.log(`Payoff in ${schedule.payments.length} months`);
|
||||||
|
|
||||||
|
// Each payment includes:
|
||||||
|
schedule.payments[0]; // {
|
||||||
|
// period: 1,
|
||||||
|
// payment: 1266.71,
|
||||||
|
// principal: 329.21,
|
||||||
|
// interest: 937.50,
|
||||||
|
// balance: 249670.79,
|
||||||
|
// cumulativePrincipal: 329.21,
|
||||||
|
// cumulativeInterest: 937.50
|
||||||
|
// }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Currency - Money Formatting & Conversion
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const currency = new Currency();
|
||||||
|
|
||||||
|
// Set exchange rates
|
||||||
|
currency.setExchangeRates([
|
||||||
|
{ from: 'USD', to: 'EUR', rate: 0.85 },
|
||||||
|
{ from: 'USD', to: 'GBP', rate: 0.73 },
|
||||||
|
{ from: 'USD', to: 'JPY', rate: 110 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Convert currencies
|
||||||
|
const euros = currency.convert(1000, 'USD', 'EUR'); // €850
|
||||||
|
|
||||||
|
// Format currency for display
|
||||||
|
currency.format(1234.56, 'USD'); // "$1,234.56"
|
||||||
|
currency.format(1234.56, 'EUR'); // "€1.234,56"
|
||||||
|
currency.format(1234.56, 'JPY'); // "¥1,235"
|
||||||
|
|
||||||
|
// Money operations (ensures same currency)
|
||||||
|
const total = currency.addMoney(
|
||||||
|
currency.money(100, 'USD'),
|
||||||
|
currency.money(50, 'USD')
|
||||||
|
); // $150
|
||||||
|
|
||||||
|
// Percentage calculations
|
||||||
|
currency.percentage(200, 15); // 30 (15% of 200)
|
||||||
|
currency.discount(100, 20); // 80 (20% off)
|
||||||
|
currency.withTax(100, 0.08); // 108 (8% tax)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Precision and Accuracy
|
||||||
|
|
||||||
|
All calculations use decimal.js internally to avoid floating-point precision errors common in JavaScript:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// JavaScript floating point error
|
||||||
|
console.log(0.1 + 0.2); // 0.30000000000000004
|
||||||
|
|
||||||
|
// @fin.cx/calculation precision
|
||||||
|
const calc = new Calculator();
|
||||||
|
const result = calc.add(0.1, 0.2);
|
||||||
|
console.log(calc.toString(result)); // "0.3"
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Calculator Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ICalculatorOptions {
|
||||||
|
precision?: number; // Decimal places (default: 10)
|
||||||
|
rounding?: number; // Rounding mode (default: ROUND_HALF_UP)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compounding Frequencies
|
||||||
|
|
||||||
|
- `'annually'` - Once per year
|
||||||
|
- `'semiannually'` - Twice per year
|
||||||
|
- `'quarterly'` - Four times per year
|
||||||
|
- `'monthly'` - Twelve times per year
|
||||||
|
- `'weekly'` - 52 times per year
|
||||||
|
- `'daily'` - 365 times per year
|
||||||
|
- `'continuous'` - Continuous compounding
|
||||||
|
|
||||||
|
### Currency Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ICurrencyOptions {
|
||||||
|
code: string;
|
||||||
|
symbol?: string;
|
||||||
|
decimals?: number;
|
||||||
|
thousandsSeparator?: string;
|
||||||
|
decimalSeparator?: string;
|
||||||
|
symbolPosition?: 'before' | 'after';
|
||||||
|
spaceBetweenSymbolAndValue?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Real-World Examples
|
||||||
|
|
||||||
|
### Mortgage Calculator
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Financial, Amortization } from '@fin.cx/calculation';
|
||||||
|
|
||||||
|
function calculateMortgage(homePrice: number, downPayment: number, rate: number, years: number) {
|
||||||
|
const financial = new Financial();
|
||||||
|
const amortization = new Amortization();
|
||||||
|
|
||||||
|
const loanAmount = homePrice - downPayment;
|
||||||
|
const monthlyRate = rate / 12;
|
||||||
|
const months = years * 12;
|
||||||
|
|
||||||
|
const monthlyPayment = financial.payment(loanAmount, monthlyRate, months);
|
||||||
|
const totalPaid = monthlyPayment.mul(months);
|
||||||
|
const totalInterest = totalPaid.sub(loanAmount);
|
||||||
|
|
||||||
|
const schedule = amortization.schedule({
|
||||||
|
principal: loanAmount,
|
||||||
|
annualRate: rate,
|
||||||
|
termYears: years
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
monthlyPayment,
|
||||||
|
totalInterest,
|
||||||
|
schedule
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Investment Analysis
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Financial } from '@fin.cx/calculation';
|
||||||
|
|
||||||
|
function analyzeInvestment(initialInvestment: number, cashFlows: number[], discountRate: number) {
|
||||||
|
const financial = new Financial();
|
||||||
|
|
||||||
|
// Add initial investment as negative cash flow
|
||||||
|
const allCashFlows = [-initialInvestment, ...cashFlows];
|
||||||
|
|
||||||
|
const npv = financial.npv(discountRate, allCashFlows);
|
||||||
|
const irr = financial.irr(allCashFlows);
|
||||||
|
const paybackPeriod = calculatePaybackPeriod(allCashFlows);
|
||||||
|
|
||||||
|
return {
|
||||||
|
npv: npv.toFixed(2),
|
||||||
|
irr: (irr.mul(100)).toFixed(2) + '%',
|
||||||
|
profitable: npv.greaterThan(0),
|
||||||
|
paybackPeriod
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Currency Invoice System
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Currency } from '@fin.cx/calculation';
|
||||||
|
|
||||||
|
class InvoiceSystem {
|
||||||
|
private currency = new Currency();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Set up exchange rates (in production, fetch from API)
|
||||||
|
this.currency.setExchangeRates([
|
||||||
|
{ from: 'USD', to: 'EUR', rate: 0.85 },
|
||||||
|
{ from: 'USD', to: 'GBP', rate: 0.73 }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateInvoice(items: Array<{price: number, quantity: number}>, currency: string, taxRate: number) {
|
||||||
|
const subtotal = items.reduce((sum, item) =>
|
||||||
|
this.currency.add(sum, this.currency.multiply(item.price, item.quantity)),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const tax = this.currency.tax(subtotal, taxRate);
|
||||||
|
const total = this.currency.add(subtotal, tax);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subtotal: this.currency.format(subtotal, currency),
|
||||||
|
tax: this.currency.format(tax, currency),
|
||||||
|
total: this.currency.format(total, currency),
|
||||||
|
// Provide total in other currencies
|
||||||
|
totalUSD: this.currency.format(this.currency.convert(total, currency, 'USD'), 'USD'),
|
||||||
|
totalEUR: this.currency.format(this.currency.convert(total, currency, 'EUR'), 'EUR')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Calculator Precision
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Default precision is 10 decimal places
|
||||||
|
const calc = new Calculator();
|
||||||
|
|
||||||
|
// Custom precision for specific use cases
|
||||||
|
const highPrecision = new Calculator({ precision: 20 });
|
||||||
|
const moneyCalc = new Calculator({ precision: 4, rounding: Decimal.ROUND_HALF_UP });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Currencies
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const currency = new Currency();
|
||||||
|
|
||||||
|
// Register a custom currency
|
||||||
|
currency.registerCurrency({
|
||||||
|
code: 'BTC',
|
||||||
|
symbol: '₿',
|
||||||
|
decimals: 8,
|
||||||
|
thousandsSeparator: ',',
|
||||||
|
decimalSeparator: '.',
|
||||||
|
symbolPosition: 'before'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use it like any other currency
|
||||||
|
currency.format(0.00042, 'BTC'); // "₿0.00042000"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
The package includes comprehensive tests for all calculations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
npm run test:coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](license.md) file for details.
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- Built on top of [decimal.js](https://github.com/MikeMcl/decimal.js/) for decimal arithmetic
|
||||||
|
- Inspired by financial calculation needs in modern web applications
|
||||||
|
- Thanks to all contributors who have helped shape this library
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
- 📧 Email: support@fin.cx
|
||||||
|
- 🐛 Issues: [GitHub Issues](https://github.com/fin-cx/calculation/issues)
|
||||||
|
- 💬 Discussions: [GitHub Discussions](https://github.com/fin-cx/calculation/discussions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Made with ❤️ by the fin.cx team
|
246
test/test.both.ts
Normal file
246
test/test.both.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
import * as calculation from '../ts/index.js';
|
||||||
|
|
||||||
|
tap.test('Calculator class should perform basic arithmetic with decimal precision', async () => {
|
||||||
|
const calc = new calculation.Calculator({ precision: 10 });
|
||||||
|
|
||||||
|
// Test addition
|
||||||
|
const sum = calc.add(0.1, 0.2);
|
||||||
|
expect(calc.toString(sum)).toEqual('0.3');
|
||||||
|
|
||||||
|
// Test subtraction
|
||||||
|
const diff = calc.subtract(1, 0.9);
|
||||||
|
expect(calc.toString(diff)).toEqual('0.1');
|
||||||
|
|
||||||
|
// Test multiplication
|
||||||
|
const product = calc.multiply(0.1, 0.2);
|
||||||
|
expect(calc.toString(product)).toEqual('0.02');
|
||||||
|
|
||||||
|
// Test division
|
||||||
|
const quotient = calc.divide(1, 3);
|
||||||
|
expect(calc.toString(quotient)).toEqual('0.3333333333');
|
||||||
|
|
||||||
|
// Test power
|
||||||
|
const power = calc.power(2, 3);
|
||||||
|
expect(calc.toNumber(power)).toEqual(8);
|
||||||
|
|
||||||
|
// Test square root
|
||||||
|
const sqrt = calc.sqrt(9);
|
||||||
|
expect(calc.toNumber(sqrt)).toEqual(3);
|
||||||
|
|
||||||
|
// Test rounding
|
||||||
|
const rounded = calc.round(3.14159, 2);
|
||||||
|
expect(calc.toString(rounded)).toEqual('3.14');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Financial class should calculate time value of money correctly', async () => {
|
||||||
|
const financial = new calculation.Financial();
|
||||||
|
|
||||||
|
// Test Present Value
|
||||||
|
const pv = financial.presentValue(1000, 0.05, 5);
|
||||||
|
expect(financial.round(pv, 2).toString()).toEqual('783.53');
|
||||||
|
|
||||||
|
// Test Future Value
|
||||||
|
const fv = financial.futureValue(1000, 0.05, 5);
|
||||||
|
expect(financial.round(fv, 2).toString()).toEqual('1276.28');
|
||||||
|
|
||||||
|
// Test Payment
|
||||||
|
const pmt = financial.payment(10000, 0.05/12, 60);
|
||||||
|
expect(financial.round(pmt, 2).toString()).toEqual('188.71');
|
||||||
|
|
||||||
|
// Test NPV
|
||||||
|
const cashFlows = [-1000, 300, 300, 300, 300, 300];
|
||||||
|
const npv = financial.npv(0.1, cashFlows);
|
||||||
|
expect(financial.round(npv, 2).toString()).toEqual('137.24');
|
||||||
|
|
||||||
|
// Test periods calculation
|
||||||
|
const periods = financial.periods(1000, 2000, 0.08);
|
||||||
|
expect(financial.round(periods, 2).toString()).toEqual('9.01');
|
||||||
|
|
||||||
|
// Test rate calculation
|
||||||
|
const rate = financial.rate(1000, 2000, 10);
|
||||||
|
expect(financial.round(rate, 4).toString()).toEqual('0.0718');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Financial class should calculate IRR correctly', async () => {
|
||||||
|
const financial = new calculation.Financial();
|
||||||
|
|
||||||
|
// Test basic IRR
|
||||||
|
const cashFlows = [-1000, 200, 300, 400, 500];
|
||||||
|
const irr = financial.irr(cashFlows);
|
||||||
|
expect(financial.round(irr, 4).toString()).toEqual('0.1283');
|
||||||
|
|
||||||
|
// Test MIRR
|
||||||
|
const mirr = financial.mirr(cashFlows, 0.1, 0.12);
|
||||||
|
expect(financial.round(mirr, 4).toString()).toEqual('0.1256');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Interest class should calculate different types of interest', async () => {
|
||||||
|
const interest = new calculation.Interest();
|
||||||
|
|
||||||
|
// Test simple interest
|
||||||
|
const simple = interest.simple(1000, 0.05, 2);
|
||||||
|
expect(interest.toString(simple)).toEqual('100');
|
||||||
|
|
||||||
|
// Test simple interest amount
|
||||||
|
const simpleAmount = interest.simpleAmount(1000, 0.05, 2);
|
||||||
|
expect(interest.toString(simpleAmount)).toEqual('1100');
|
||||||
|
|
||||||
|
// Test compound interest annually
|
||||||
|
const compound = interest.compound(1000, 0.05, 2, 'annually');
|
||||||
|
expect(interest.round(compound, 2).toString()).toEqual('102.5');
|
||||||
|
|
||||||
|
// Test compound interest monthly
|
||||||
|
const compoundMonthly = interest.compound(1000, 0.12, 1, 'monthly');
|
||||||
|
expect(interest.round(compoundMonthly, 2).toString()).toEqual('126.83');
|
||||||
|
|
||||||
|
// Test continuous compound interest
|
||||||
|
const continuous = interest.compound(1000, 0.05, 2, 'continuous');
|
||||||
|
expect(interest.round(continuous, 2).toString()).toEqual('105.17');
|
||||||
|
|
||||||
|
// Test effective annual rate
|
||||||
|
const ear = interest.effectiveAnnualRate(0.12, 'monthly');
|
||||||
|
expect(interest.round(ear, 4).toString()).toEqual('0.1268');
|
||||||
|
|
||||||
|
// Test real rate
|
||||||
|
const realRate = interest.realRate(0.08, 0.03);
|
||||||
|
expect(interest.round(realRate, 4).toString()).toEqual('0.0485');
|
||||||
|
|
||||||
|
// Test Rule of 72
|
||||||
|
const rule72 = interest.ruleOf72(8);
|
||||||
|
expect(interest.toString(rule72)).toEqual('9');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Amortization class should generate correct loan schedules', async () => {
|
||||||
|
const amortization = new calculation.Amortization();
|
||||||
|
|
||||||
|
// Test basic loan schedule
|
||||||
|
const schedule = amortization.schedule({
|
||||||
|
principal: 10000,
|
||||||
|
annualRate: 0.05,
|
||||||
|
termYears: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(schedule.payments).toHaveLength(24);
|
||||||
|
expect(amortization.round(schedule.monthlyPayment, 2).toString()).toEqual('438.71');
|
||||||
|
expect(amortization.round(schedule.totalInterest, 2).toString()).toEqual('529.13');
|
||||||
|
|
||||||
|
// Test remaining balance
|
||||||
|
const balance = amortization.remainingBalance(10000, 0.05, 24, 12);
|
||||||
|
expect(amortization.round(balance, 2).toString()).toEqual('5124.71');
|
||||||
|
|
||||||
|
// Test max loan amount
|
||||||
|
const maxLoan = amortization.maxLoanAmount(1000, 0.05, 360);
|
||||||
|
expect(amortization.round(maxLoan, 2).toString()).toEqual('186281.62');
|
||||||
|
|
||||||
|
// Test LTV ratio
|
||||||
|
const ltv = amortization.ltv(80000, 100000);
|
||||||
|
expect(amortization.toString(ltv)).toEqual('0.8');
|
||||||
|
|
||||||
|
// Test DTI ratio
|
||||||
|
const dti = amortization.dti(1500, 5000);
|
||||||
|
expect(amortization.toString(dti)).toEqual('0.3');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Currency class should handle currency operations correctly', async () => {
|
||||||
|
const currency = new calculation.Currency();
|
||||||
|
|
||||||
|
// Set up exchange rates
|
||||||
|
currency.setExchangeRate('USD', 'EUR', 0.85);
|
||||||
|
currency.setExchangeRate('USD', 'GBP', 0.73);
|
||||||
|
|
||||||
|
// Test currency conversion
|
||||||
|
const converted = currency.convert(100, 'USD', 'EUR');
|
||||||
|
expect(currency.toString(converted)).toEqual('85');
|
||||||
|
|
||||||
|
// Test inverse conversion
|
||||||
|
const inverse = currency.convert(85, 'EUR', 'USD');
|
||||||
|
expect(currency.round(inverse, 2).toString()).toEqual('100');
|
||||||
|
|
||||||
|
// Test currency formatting
|
||||||
|
const formatted = currency.format(1234.56, 'USD');
|
||||||
|
expect(formatted).toEqual('$1,234.56');
|
||||||
|
|
||||||
|
const formattedEur = currency.format(1234.56, 'EUR');
|
||||||
|
expect(formattedEur).toEqual('€1.234,56');
|
||||||
|
|
||||||
|
const formattedJpy = currency.format(1234.56, 'JPY');
|
||||||
|
expect(formattedJpy).toEqual('¥1,235');
|
||||||
|
|
||||||
|
// Test parsing
|
||||||
|
const parsed = currency.parse('$1,234.56', 'USD');
|
||||||
|
expect(currency.toString(parsed)).toEqual('1234.56');
|
||||||
|
|
||||||
|
// Test money operations
|
||||||
|
const money1 = currency.money(100, 'USD');
|
||||||
|
const money2 = currency.money(50, 'USD');
|
||||||
|
const sum = currency.addMoney(money1, money2);
|
||||||
|
expect(currency.toString(sum.amount)).toEqual('150');
|
||||||
|
|
||||||
|
// Test percentage calculations
|
||||||
|
const percentage = currency.percentage(100, 15);
|
||||||
|
expect(currency.toString(percentage)).toEqual('15');
|
||||||
|
|
||||||
|
// Test tax calculations
|
||||||
|
const withTax = currency.withTax(100, 0.08);
|
||||||
|
expect(currency.toString(withTax)).toEqual('108');
|
||||||
|
|
||||||
|
const extracted = currency.extractTax(108, 0.08);
|
||||||
|
expect(currency.round(extracted.baseAmount, 2).toString()).toEqual('100');
|
||||||
|
expect(currency.round(extracted.taxAmount, 2).toString()).toEqual('8');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Edge cases and error handling', async () => {
|
||||||
|
const calc = new calculation.Calculator();
|
||||||
|
const financial = new calculation.Financial();
|
||||||
|
const interest = new calculation.Interest();
|
||||||
|
const amortization = new calculation.Amortization();
|
||||||
|
const currency = new calculation.Currency();
|
||||||
|
|
||||||
|
// Test division by zero
|
||||||
|
expect(() => calc.divide(10, 0)).toThrow();
|
||||||
|
|
||||||
|
// Test zero interest rate in periods calculation
|
||||||
|
expect(() => financial.periods(1000, 2000, 0)).toThrow();
|
||||||
|
|
||||||
|
// Test zero periods in rate calculation
|
||||||
|
expect(() => financial.rate(1000, 2000, 0)).toThrow();
|
||||||
|
|
||||||
|
// Test doubling time with zero rate
|
||||||
|
expect(() => interest.doubleTime(0)).toThrow();
|
||||||
|
|
||||||
|
// Test Rule of 72 with zero rate
|
||||||
|
expect(() => interest.ruleOf72(0)).toThrow();
|
||||||
|
|
||||||
|
// Test LTV with zero property value
|
||||||
|
expect(() => amortization.ltv(100000, 0)).toThrow();
|
||||||
|
|
||||||
|
// Test DTI with zero income
|
||||||
|
expect(() => amortization.dti(1000, 0)).toThrow();
|
||||||
|
|
||||||
|
// Test currency conversion without exchange rate
|
||||||
|
expect(() => currency.convert(100, 'USD', 'JPY')).toThrow();
|
||||||
|
|
||||||
|
// Test adding money with different currencies
|
||||||
|
const usdMoney = currency.money(100, 'USD');
|
||||||
|
const eurMoney = currency.money(100, 'EUR');
|
||||||
|
expect(() => currency.addMoney(usdMoney, eurMoney)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Precision and accuracy tests', async () => {
|
||||||
|
const calc = new calculation.Calculator({ precision: 20 });
|
||||||
|
|
||||||
|
// Test high precision calculation
|
||||||
|
const result = calc.divide(1, 3);
|
||||||
|
expect(calc.toString(result)).toEqual('0.33333333333333333333');
|
||||||
|
|
||||||
|
// Test very small numbers
|
||||||
|
const small = calc.multiply('0.000000001', '0.000000001');
|
||||||
|
expect(calc.toString(small)).toEqual('1e-18');
|
||||||
|
|
||||||
|
// Test very large numbers
|
||||||
|
const large = calc.multiply('1000000000000', '1000000000000');
|
||||||
|
expect(calc.toString(large)).toEqual('1e+24');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* autocreated commitinfo by @push.rocks/commitinfo
|
||||||
|
*/
|
||||||
|
export const commitinfo = {
|
||||||
|
name: '@fin.cx/calculation',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'A unified, cross-platform financial calculation package with decimal precision for accurate financial computations'
|
||||||
|
};
|
313
ts/calculation.classes.amortization.ts
Normal file
313
ts/calculation.classes.amortization.ts
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
import * as plugins from './calculation.plugins.js';
|
||||||
|
import { Calculator } from './calculation.classes.calculator.js';
|
||||||
|
|
||||||
|
export interface IAmortizationPayment {
|
||||||
|
period: number;
|
||||||
|
payment: plugins.Decimal;
|
||||||
|
principal: plugins.Decimal;
|
||||||
|
interest: plugins.Decimal;
|
||||||
|
balance: plugins.Decimal;
|
||||||
|
cumulativePrincipal: plugins.Decimal;
|
||||||
|
cumulativeInterest: plugins.Decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAmortizationSchedule {
|
||||||
|
payments: IAmortizationPayment[];
|
||||||
|
totalPayment: plugins.Decimal;
|
||||||
|
totalPrincipal: plugins.Decimal;
|
||||||
|
totalInterest: plugins.Decimal;
|
||||||
|
monthlyPayment: plugins.Decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILoanOptions {
|
||||||
|
principal: plugins.Decimal.Value;
|
||||||
|
annualRate: plugins.Decimal.Value;
|
||||||
|
termYears?: plugins.Decimal.Value;
|
||||||
|
termMonths?: plugins.Decimal.Value;
|
||||||
|
paymentFrequency?: 'monthly' | 'biweekly' | 'weekly';
|
||||||
|
extraPayment?: plugins.Decimal.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amortization calculations class for loan schedules and payment analysis
|
||||||
|
*/
|
||||||
|
export class Amortization extends Calculator {
|
||||||
|
constructor() {
|
||||||
|
super({ precision: 15 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a complete amortization schedule
|
||||||
|
* @param options Loan options
|
||||||
|
*/
|
||||||
|
public schedule(options: ILoanOptions): IAmortizationSchedule {
|
||||||
|
const principal = this.decimal(options.principal);
|
||||||
|
const annualRate = this.decimal(options.annualRate);
|
||||||
|
const termMonths = this.calculateTermMonths(options);
|
||||||
|
const extraPayment = options.extraPayment ? this.decimal(options.extraPayment) : this.decimal(0);
|
||||||
|
|
||||||
|
const monthlyRate = this.divide(annualRate, 12);
|
||||||
|
const monthlyPayment = this.calculatePayment(principal, monthlyRate, termMonths);
|
||||||
|
|
||||||
|
const payments: IAmortizationPayment[] = [];
|
||||||
|
let balance = principal;
|
||||||
|
let cumulativePrincipal = this.decimal(0);
|
||||||
|
let cumulativeInterest = this.decimal(0);
|
||||||
|
|
||||||
|
for (let period = 1; period <= termMonths.toNumber() && balance.greaterThan(0); period++) {
|
||||||
|
const interestPayment = this.multiply(balance, monthlyRate);
|
||||||
|
const scheduledPrincipal = this.subtract(monthlyPayment, interestPayment);
|
||||||
|
|
||||||
|
// Apply extra payment to principal
|
||||||
|
const totalPrincipal = this.add(scheduledPrincipal, extraPayment);
|
||||||
|
|
||||||
|
// Ensure we don't pay more than the remaining balance
|
||||||
|
const actualPrincipal = this.min(totalPrincipal, balance);
|
||||||
|
const actualPayment = this.add(interestPayment, actualPrincipal);
|
||||||
|
|
||||||
|
balance = this.subtract(balance, actualPrincipal);
|
||||||
|
cumulativePrincipal = this.add(cumulativePrincipal, actualPrincipal);
|
||||||
|
cumulativeInterest = this.add(cumulativeInterest, interestPayment);
|
||||||
|
|
||||||
|
payments.push({
|
||||||
|
period,
|
||||||
|
payment: actualPayment,
|
||||||
|
principal: actualPrincipal,
|
||||||
|
interest: interestPayment,
|
||||||
|
balance: balance,
|
||||||
|
cumulativePrincipal: cumulativePrincipal,
|
||||||
|
cumulativeInterest: cumulativeInterest
|
||||||
|
});
|
||||||
|
|
||||||
|
// If balance is paid off early, stop
|
||||||
|
if (balance.isZero()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPayment = payments.reduce((sum, p) => this.add(sum, p.payment), this.decimal(0));
|
||||||
|
const totalPrincipal = cumulativePrincipal;
|
||||||
|
const totalInterest = cumulativeInterest;
|
||||||
|
|
||||||
|
return {
|
||||||
|
payments,
|
||||||
|
totalPayment,
|
||||||
|
totalPrincipal,
|
||||||
|
totalInterest,
|
||||||
|
monthlyPayment
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate monthly payment for a loan
|
||||||
|
*/
|
||||||
|
private calculatePayment(
|
||||||
|
principal: plugins.Decimal,
|
||||||
|
monthlyRate: plugins.Decimal,
|
||||||
|
termMonths: plugins.Decimal
|
||||||
|
): plugins.Decimal {
|
||||||
|
if (monthlyRate.isZero()) {
|
||||||
|
return this.divide(principal, termMonths);
|
||||||
|
}
|
||||||
|
|
||||||
|
const numerator = this.multiply(principal, monthlyRate);
|
||||||
|
const denominator = this.subtract(
|
||||||
|
1,
|
||||||
|
this.power(this.add(1, monthlyRate), this.multiply(-1, termMonths))
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.divide(numerator, denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total term in months
|
||||||
|
*/
|
||||||
|
private calculateTermMonths(options: ILoanOptions): plugins.Decimal {
|
||||||
|
if (options.termMonths) {
|
||||||
|
return this.decimal(options.termMonths);
|
||||||
|
} else if (options.termYears) {
|
||||||
|
return this.multiply(this.decimal(options.termYears), 12);
|
||||||
|
} else {
|
||||||
|
throw new Error('Either termYears or termMonths must be specified');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate remaining balance after a specific number of payments
|
||||||
|
* @param principal Original loan amount
|
||||||
|
* @param annualRate Annual interest rate
|
||||||
|
* @param termMonths Total term in months
|
||||||
|
* @param paymentsMade Number of payments made
|
||||||
|
*/
|
||||||
|
public remainingBalance(
|
||||||
|
principal: plugins.Decimal.Value,
|
||||||
|
annualRate: plugins.Decimal.Value,
|
||||||
|
termMonths: plugins.Decimal.Value,
|
||||||
|
paymentsMade: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const p = this.decimal(principal);
|
||||||
|
const r = this.divide(this.decimal(annualRate), 12);
|
||||||
|
const n = this.decimal(termMonths);
|
||||||
|
const m = this.decimal(paymentsMade);
|
||||||
|
|
||||||
|
if (r.isZero()) {
|
||||||
|
const paymentAmount = this.divide(p, n);
|
||||||
|
const paidAmount = this.multiply(paymentAmount, m);
|
||||||
|
return this.subtract(p, paidAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onePlusR = this.add(1, r);
|
||||||
|
const factor1 = this.power(onePlusR, n);
|
||||||
|
const factor2 = this.power(onePlusR, m);
|
||||||
|
|
||||||
|
const numerator = this.multiply(p, this.subtract(factor1, factor2));
|
||||||
|
const denominator = this.subtract(factor1, 1);
|
||||||
|
|
||||||
|
return this.divide(numerator, denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate interest paid over a specific period
|
||||||
|
* @param schedule The amortization schedule
|
||||||
|
* @param startPeriod Start period (inclusive)
|
||||||
|
* @param endPeriod End period (inclusive)
|
||||||
|
*/
|
||||||
|
public interestForPeriod(
|
||||||
|
schedule: IAmortizationSchedule,
|
||||||
|
startPeriod: number,
|
||||||
|
endPeriod: number
|
||||||
|
): plugins.Decimal {
|
||||||
|
return schedule.payments
|
||||||
|
.filter(p => p.period >= startPeriod && p.period <= endPeriod)
|
||||||
|
.reduce((sum, p) => this.add(sum, p.interest), this.decimal(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate principal paid over a specific period
|
||||||
|
* @param schedule The amortization schedule
|
||||||
|
* @param startPeriod Start period (inclusive)
|
||||||
|
* @param endPeriod End period (inclusive)
|
||||||
|
*/
|
||||||
|
public principalForPeriod(
|
||||||
|
schedule: IAmortizationSchedule,
|
||||||
|
startPeriod: number,
|
||||||
|
endPeriod: number
|
||||||
|
): plugins.Decimal {
|
||||||
|
return schedule.payments
|
||||||
|
.filter(p => p.period >= startPeriod && p.period <= endPeriod)
|
||||||
|
.reduce((sum, p) => this.add(sum, p.principal), this.decimal(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the payoff date with extra payments
|
||||||
|
* @param options Loan options with extra payment
|
||||||
|
*/
|
||||||
|
public payoffDate(options: ILoanOptions, startDate: Date = new Date()): Date {
|
||||||
|
const schedule = this.schedule(options);
|
||||||
|
const lastPayment = schedule.payments[schedule.payments.length - 1];
|
||||||
|
|
||||||
|
const monthsToPayoff = lastPayment.period;
|
||||||
|
const payoffDate = new Date(startDate);
|
||||||
|
payoffDate.setMonth(payoffDate.getMonth() + monthsToPayoff);
|
||||||
|
|
||||||
|
return payoffDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate interest savings with extra payments
|
||||||
|
* @param regularOptions Loan options without extra payment
|
||||||
|
* @param extraPaymentOptions Loan options with extra payment
|
||||||
|
*/
|
||||||
|
public interestSavings(
|
||||||
|
regularOptions: ILoanOptions,
|
||||||
|
extraPaymentOptions: ILoanOptions
|
||||||
|
): plugins.Decimal {
|
||||||
|
const regularSchedule = this.schedule(regularOptions);
|
||||||
|
const extraSchedule = this.schedule(extraPaymentOptions);
|
||||||
|
|
||||||
|
return this.subtract(regularSchedule.totalInterest, extraSchedule.totalInterest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate biweekly payment amount (26 payments per year)
|
||||||
|
* @param monthlyPayment The regular monthly payment
|
||||||
|
*/
|
||||||
|
public biweeklyPayment(monthlyPayment: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
return this.divide(this.decimal(monthlyPayment), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate weekly payment amount (52 payments per year)
|
||||||
|
* @param monthlyPayment The regular monthly payment
|
||||||
|
*/
|
||||||
|
public weeklyPayment(monthlyPayment: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
const monthly = this.decimal(monthlyPayment);
|
||||||
|
const yearlyPayment = this.multiply(monthly, 12);
|
||||||
|
return this.divide(yearlyPayment, 52);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the maximum loan amount based on payment capacity
|
||||||
|
* @param maxPayment Maximum monthly payment
|
||||||
|
* @param annualRate Annual interest rate
|
||||||
|
* @param termMonths Term in months
|
||||||
|
*/
|
||||||
|
public maxLoanAmount(
|
||||||
|
maxPayment: plugins.Decimal.Value,
|
||||||
|
annualRate: plugins.Decimal.Value,
|
||||||
|
termMonths: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const payment = this.decimal(maxPayment);
|
||||||
|
const r = this.divide(this.decimal(annualRate), 12);
|
||||||
|
const n = this.decimal(termMonths);
|
||||||
|
|
||||||
|
if (r.isZero()) {
|
||||||
|
return this.multiply(payment, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onePlusR = this.add(1, r);
|
||||||
|
const factor = this.power(onePlusR, this.multiply(-1, n));
|
||||||
|
const numerator = this.subtract(1, factor);
|
||||||
|
const denominator = this.divide(numerator, r);
|
||||||
|
|
||||||
|
return this.multiply(payment, denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate loan-to-value ratio (LTV)
|
||||||
|
* @param loanAmount The loan amount
|
||||||
|
* @param propertyValue The property value
|
||||||
|
*/
|
||||||
|
public ltv(
|
||||||
|
loanAmount: plugins.Decimal.Value,
|
||||||
|
propertyValue: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const loan = this.decimal(loanAmount);
|
||||||
|
const value = this.decimal(propertyValue);
|
||||||
|
|
||||||
|
if (value.isZero()) {
|
||||||
|
throw new Error('Property value cannot be zero');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.divide(loan, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate debt-to-income ratio (DTI)
|
||||||
|
* @param monthlyDebtPayments Total monthly debt payments
|
||||||
|
* @param monthlyIncome Monthly gross income
|
||||||
|
*/
|
||||||
|
public dti(
|
||||||
|
monthlyDebtPayments: plugins.Decimal.Value,
|
||||||
|
monthlyIncome: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const debt = this.decimal(monthlyDebtPayments);
|
||||||
|
const income = this.decimal(monthlyIncome);
|
||||||
|
|
||||||
|
if (income.isZero()) {
|
||||||
|
throw new Error('Monthly income cannot be zero');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.divide(debt, income);
|
||||||
|
}
|
||||||
|
}
|
178
ts/calculation.classes.calculator.ts
Normal file
178
ts/calculation.classes.calculator.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import * as plugins from './calculation.plugins.js';
|
||||||
|
|
||||||
|
export interface ICalculatorOptions {
|
||||||
|
precision?: number;
|
||||||
|
rounding?: plugins.Decimal.Rounding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base calculator class providing high-precision decimal arithmetic
|
||||||
|
* for financial calculations
|
||||||
|
*/
|
||||||
|
export class Calculator {
|
||||||
|
private precision: number;
|
||||||
|
private rounding: plugins.Decimal.Rounding;
|
||||||
|
|
||||||
|
constructor(options: ICalculatorOptions = {}) {
|
||||||
|
this.precision = options.precision || 10;
|
||||||
|
this.rounding = options.rounding || plugins.Decimal.ROUND_HALF_UP;
|
||||||
|
|
||||||
|
// Configure Decimal.js globally for this calculator instance
|
||||||
|
plugins.Decimal.set({
|
||||||
|
precision: this.precision,
|
||||||
|
rounding: this.rounding
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Decimal instance from a value
|
||||||
|
*/
|
||||||
|
public decimal(value: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
return new plugins.Decimal(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add two or more numbers with decimal precision
|
||||||
|
*/
|
||||||
|
public add(...values: plugins.Decimal.Value[]): plugins.Decimal {
|
||||||
|
return values.reduce<plugins.Decimal>((sum, value) => {
|
||||||
|
return sum.add(this.decimal(value));
|
||||||
|
}, new plugins.Decimal(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtract numbers with decimal precision
|
||||||
|
*/
|
||||||
|
public subtract(minuend: plugins.Decimal.Value, ...subtrahends: plugins.Decimal.Value[]): plugins.Decimal {
|
||||||
|
let result = new plugins.Decimal(minuend);
|
||||||
|
for (const subtrahend of subtrahends) {
|
||||||
|
result = result.sub(subtrahend);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiply numbers with decimal precision
|
||||||
|
*/
|
||||||
|
public multiply(...values: plugins.Decimal.Value[]): plugins.Decimal {
|
||||||
|
return values.reduce<plugins.Decimal>((product, value) => {
|
||||||
|
return product.mul(this.decimal(value));
|
||||||
|
}, new plugins.Decimal(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Divide numbers with decimal precision
|
||||||
|
*/
|
||||||
|
public divide(dividend: plugins.Decimal.Value, divisor: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
const divisorDecimal = new plugins.Decimal(divisor);
|
||||||
|
if (divisorDecimal.isZero()) {
|
||||||
|
throw new Error('Division by zero');
|
||||||
|
}
|
||||||
|
return new plugins.Decimal(dividend).div(divisor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate power with decimal precision
|
||||||
|
*/
|
||||||
|
public power(base: plugins.Decimal.Value, exponent: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
return new plugins.Decimal(base).pow(exponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate square root with decimal precision
|
||||||
|
*/
|
||||||
|
public sqrt(value: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
return new plugins.Decimal(value).sqrt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate natural logarithm with decimal precision
|
||||||
|
*/
|
||||||
|
public ln(value: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
return new plugins.Decimal(value).ln();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate logarithm base 10 with decimal precision
|
||||||
|
*/
|
||||||
|
public log10(value: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
return new plugins.Decimal(value).log();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate exponential (e^x) with decimal precision
|
||||||
|
*/
|
||||||
|
public exp(value: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
return new plugins.Decimal(value).exp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Round a number to specified decimal places
|
||||||
|
*/
|
||||||
|
public round(value: plugins.Decimal.Value, decimalPlaces: number = 0): plugins.Decimal {
|
||||||
|
return new plugins.Decimal(value).toDecimalPlaces(decimalPlaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a value equals another value
|
||||||
|
*/
|
||||||
|
public equals(value1: plugins.Decimal.Value, value2: plugins.Decimal.Value): boolean {
|
||||||
|
return new plugins.Decimal(value1).equals(value2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a value is greater than another value
|
||||||
|
*/
|
||||||
|
public greaterThan(value1: plugins.Decimal.Value, value2: plugins.Decimal.Value): boolean {
|
||||||
|
return new plugins.Decimal(value1).greaterThan(value2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a value is less than another value
|
||||||
|
*/
|
||||||
|
public lessThan(value1: plugins.Decimal.Value, value2: plugins.Decimal.Value): boolean {
|
||||||
|
return new plugins.Decimal(value1).lessThan(value2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the minimum value from an array of values
|
||||||
|
*/
|
||||||
|
public min(...values: plugins.Decimal.Value[]): plugins.Decimal {
|
||||||
|
return plugins.Decimal.min(...values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum value from an array of values
|
||||||
|
*/
|
||||||
|
public max(...values: plugins.Decimal.Value[]): plugins.Decimal {
|
||||||
|
return plugins.Decimal.max(...values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the absolute value
|
||||||
|
*/
|
||||||
|
public abs(value: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
return new plugins.Decimal(value).abs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a decimal to a regular number
|
||||||
|
*/
|
||||||
|
public toNumber(value: plugins.Decimal.Value): number {
|
||||||
|
return new plugins.Decimal(value).toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a decimal to a string
|
||||||
|
*/
|
||||||
|
public toString(value: plugins.Decimal.Value): string {
|
||||||
|
return new plugins.Decimal(value).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a decimal to a fixed-point string
|
||||||
|
*/
|
||||||
|
public toFixed(value: plugins.Decimal.Value, decimalPlaces: number = 2): string {
|
||||||
|
return new plugins.Decimal(value).toFixed(decimalPlaces);
|
||||||
|
}
|
||||||
|
}
|
328
ts/calculation.classes.currency.ts
Normal file
328
ts/calculation.classes.currency.ts
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
import * as plugins from './calculation.plugins.js';
|
||||||
|
import { Calculator } from './calculation.classes.calculator.js';
|
||||||
|
|
||||||
|
export interface ICurrencyOptions {
|
||||||
|
code: string;
|
||||||
|
symbol?: string;
|
||||||
|
decimals?: number;
|
||||||
|
thousandsSeparator?: string;
|
||||||
|
decimalSeparator?: string;
|
||||||
|
symbolPosition?: 'before' | 'after';
|
||||||
|
spaceBetweenSymbolAndValue?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExchangeRate {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
rate: plugins.Decimal.Value;
|
||||||
|
timestamp?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMoneyValue {
|
||||||
|
amount: plugins.Decimal;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currency calculations and formatting class
|
||||||
|
*/
|
||||||
|
export class Currency extends Calculator {
|
||||||
|
private currencies: Map<string, ICurrencyOptions> = new Map();
|
||||||
|
private exchangeRates: Map<string, plugins.Decimal> = new Map();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({ precision: 10 });
|
||||||
|
this.initializeCommonCurrencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize common currencies with their default options
|
||||||
|
*/
|
||||||
|
private initializeCommonCurrencies(): void {
|
||||||
|
const commonCurrencies: ICurrencyOptions[] = [
|
||||||
|
{ code: 'USD', symbol: '$', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
|
||||||
|
{ code: 'EUR', symbol: '€', decimals: 2, thousandsSeparator: '.', decimalSeparator: ',', symbolPosition: 'before' },
|
||||||
|
{ code: 'GBP', symbol: '£', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
|
||||||
|
{ code: 'JPY', symbol: '¥', decimals: 0, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
|
||||||
|
{ code: 'CHF', symbol: 'Fr.', decimals: 2, thousandsSeparator: "'", decimalSeparator: '.', symbolPosition: 'after', spaceBetweenSymbolAndValue: true },
|
||||||
|
{ code: 'CAD', symbol: 'C$', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
|
||||||
|
{ code: 'AUD', symbol: 'A$', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
|
||||||
|
{ code: 'CNY', symbol: '¥', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
|
||||||
|
{ code: 'INR', symbol: '₹', decimals: 2, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' },
|
||||||
|
{ code: 'BTC', symbol: '₿', decimals: 8, thousandsSeparator: ',', decimalSeparator: '.', symbolPosition: 'before' }
|
||||||
|
];
|
||||||
|
|
||||||
|
commonCurrencies.forEach(currency => {
|
||||||
|
this.currencies.set(currency.code, currency);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new currency or update existing one
|
||||||
|
*/
|
||||||
|
public registerCurrency(options: ICurrencyOptions): void {
|
||||||
|
this.currencies.set(options.code, {
|
||||||
|
...options,
|
||||||
|
decimals: options.decimals ?? 2,
|
||||||
|
thousandsSeparator: options.thousandsSeparator ?? ',',
|
||||||
|
decimalSeparator: options.decimalSeparator ?? '.',
|
||||||
|
symbolPosition: options.symbolPosition ?? 'before',
|
||||||
|
spaceBetweenSymbolAndValue: options.spaceBetweenSymbolAndValue ?? false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set exchange rate between two currencies
|
||||||
|
* @param from Source currency code
|
||||||
|
* @param to Target currency code
|
||||||
|
* @param rate Exchange rate (how many 'to' units per 'from' unit)
|
||||||
|
*/
|
||||||
|
public setExchangeRate(from: string, to: string, rate: plugins.Decimal.Value): void {
|
||||||
|
const key = `${from}_${to}`;
|
||||||
|
this.exchangeRates.set(key, this.decimal(rate));
|
||||||
|
|
||||||
|
// Also set the inverse rate
|
||||||
|
const inverseKey = `${to}_${from}`;
|
||||||
|
const inverseRate = this.divide(1, rate);
|
||||||
|
this.exchangeRates.set(inverseKey, inverseRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set multiple exchange rates at once
|
||||||
|
*/
|
||||||
|
public setExchangeRates(rates: IExchangeRate[]): void {
|
||||||
|
rates.forEach(rate => {
|
||||||
|
this.setExchangeRate(rate.from, rate.to, rate.rate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert amount from one currency to another
|
||||||
|
* @param amount The amount to convert
|
||||||
|
* @param from Source currency code
|
||||||
|
* @param to Target currency code
|
||||||
|
*/
|
||||||
|
public convert(
|
||||||
|
amount: plugins.Decimal.Value,
|
||||||
|
from: string,
|
||||||
|
to: string
|
||||||
|
): plugins.Decimal {
|
||||||
|
if (from === to) {
|
||||||
|
return this.decimal(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${from}_${to}`;
|
||||||
|
const rate = this.exchangeRates.get(key);
|
||||||
|
|
||||||
|
if (!rate) {
|
||||||
|
throw new Error(`Exchange rate not found for ${from} to ${to}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.multiply(amount, rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a money value object
|
||||||
|
*/
|
||||||
|
public money(amount: plugins.Decimal.Value, currency: string): IMoneyValue {
|
||||||
|
return {
|
||||||
|
amount: this.decimal(amount),
|
||||||
|
currency
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add two money values (must be same currency)
|
||||||
|
*/
|
||||||
|
public addMoney(money1: IMoneyValue, money2: IMoneyValue): IMoneyValue {
|
||||||
|
if (money1.currency !== money2.currency) {
|
||||||
|
throw new Error('Cannot add money values with different currencies');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
amount: this.add(money1.amount, money2.amount),
|
||||||
|
currency: money1.currency
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtract two money values (must be same currency)
|
||||||
|
*/
|
||||||
|
public subtractMoney(money1: IMoneyValue, money2: IMoneyValue): IMoneyValue {
|
||||||
|
if (money1.currency !== money2.currency) {
|
||||||
|
throw new Error('Cannot subtract money values with different currencies');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
amount: this.subtract(money1.amount, money2.amount),
|
||||||
|
currency: money1.currency
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format currency value for display
|
||||||
|
* @param amount The amount to format
|
||||||
|
* @param currencyCode The currency code
|
||||||
|
* @param options Override formatting options
|
||||||
|
*/
|
||||||
|
public format(
|
||||||
|
amount: plugins.Decimal.Value,
|
||||||
|
currencyCode: string,
|
||||||
|
options?: Partial<ICurrencyOptions>
|
||||||
|
): string {
|
||||||
|
const currency = this.currencies.get(currencyCode);
|
||||||
|
|
||||||
|
if (!currency) {
|
||||||
|
throw new Error(`Currency ${currencyCode} not registered`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatOptions = { ...currency, ...options };
|
||||||
|
const value = this.decimal(amount);
|
||||||
|
|
||||||
|
// Round to the correct number of decimal places
|
||||||
|
const rounded = this.round(value, formatOptions.decimals || 0);
|
||||||
|
|
||||||
|
// Convert to string with fixed decimals
|
||||||
|
let numberStr = this.toFixed(rounded, formatOptions.decimals || 0);
|
||||||
|
|
||||||
|
// Split into integer and decimal parts
|
||||||
|
const parts = numberStr.split('.');
|
||||||
|
|
||||||
|
// Apply thousands separator to integer part
|
||||||
|
if (formatOptions.thousandsSeparator) {
|
||||||
|
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, formatOptions.thousandsSeparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join parts with appropriate decimal separator
|
||||||
|
if (parts.length === 2) {
|
||||||
|
numberStr = parts.join(formatOptions.decimalSeparator || '.');
|
||||||
|
} else {
|
||||||
|
numberStr = parts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add currency symbol
|
||||||
|
if (formatOptions.symbol) {
|
||||||
|
const space = formatOptions.spaceBetweenSymbolAndValue ? ' ' : '';
|
||||||
|
|
||||||
|
if (formatOptions.symbolPosition === 'after') {
|
||||||
|
return `${numberStr}${space}${formatOptions.symbol}`;
|
||||||
|
} else {
|
||||||
|
return `${formatOptions.symbol}${space}${numberStr}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return numberStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a formatted currency string back to a decimal value
|
||||||
|
* @param formattedValue The formatted string
|
||||||
|
* @param currencyCode The currency code
|
||||||
|
*/
|
||||||
|
public parse(formattedValue: string, currencyCode: string): plugins.Decimal {
|
||||||
|
const currency = this.currencies.get(currencyCode);
|
||||||
|
|
||||||
|
if (!currency) {
|
||||||
|
throw new Error(`Currency ${currencyCode} not registered`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove currency symbol
|
||||||
|
let cleanValue = formattedValue;
|
||||||
|
if (currency.symbol) {
|
||||||
|
cleanValue = cleanValue.replace(currency.symbol, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove thousands separator
|
||||||
|
if (currency.thousandsSeparator) {
|
||||||
|
cleanValue = cleanValue.replace(new RegExp(`\\${currency.thousandsSeparator}`, 'g'), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace decimal separator with standard dot
|
||||||
|
if (currency.decimalSeparator && currency.decimalSeparator !== '.') {
|
||||||
|
cleanValue = cleanValue.replace(currency.decimalSeparator, '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any remaining spaces and parse
|
||||||
|
cleanValue = cleanValue.trim();
|
||||||
|
|
||||||
|
return this.decimal(cleanValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Round amount to currency's standard decimal places
|
||||||
|
*/
|
||||||
|
public roundToCurrency(amount: plugins.Decimal.Value, currencyCode: string): plugins.Decimal {
|
||||||
|
const currency = this.currencies.get(currencyCode);
|
||||||
|
|
||||||
|
if (!currency) {
|
||||||
|
throw new Error(`Currency ${currencyCode} not registered`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.round(amount, currency.decimals || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate percentage of amount
|
||||||
|
* @param amount The base amount
|
||||||
|
* @param percentage The percentage (e.g., 15 for 15%)
|
||||||
|
*/
|
||||||
|
public percentage(amount: plugins.Decimal.Value, percentage: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
const percent = this.divide(percentage, 100);
|
||||||
|
return this.multiply(amount, percent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate markup amount
|
||||||
|
* @param cost The base cost
|
||||||
|
* @param markupPercentage The markup percentage
|
||||||
|
*/
|
||||||
|
public markup(cost: plugins.Decimal.Value, markupPercentage: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
const markupAmount = this.percentage(cost, markupPercentage);
|
||||||
|
return this.add(cost, markupAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate discount amount
|
||||||
|
* @param price The original price
|
||||||
|
* @param discountPercentage The discount percentage
|
||||||
|
*/
|
||||||
|
public discount(price: plugins.Decimal.Value, discountPercentage: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
const discountAmount = this.percentage(price, discountPercentage);
|
||||||
|
return this.subtract(price, discountAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate tax amount
|
||||||
|
* @param amount The base amount
|
||||||
|
* @param taxRate The tax rate (e.g., 0.08 for 8%)
|
||||||
|
*/
|
||||||
|
public tax(amount: plugins.Decimal.Value, taxRate: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
return this.multiply(amount, taxRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total with tax
|
||||||
|
* @param amount The base amount
|
||||||
|
* @param taxRate The tax rate
|
||||||
|
*/
|
||||||
|
public withTax(amount: plugins.Decimal.Value, taxRate: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
const taxAmount = this.tax(amount, taxRate);
|
||||||
|
return this.add(amount, taxAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract base amount from a tax-inclusive price
|
||||||
|
* @param totalAmount The total amount including tax
|
||||||
|
* @param taxRate The tax rate
|
||||||
|
*/
|
||||||
|
public extractTax(totalAmount: plugins.Decimal.Value, taxRate: plugins.Decimal.Value): {
|
||||||
|
baseAmount: plugins.Decimal;
|
||||||
|
taxAmount: plugins.Decimal;
|
||||||
|
} {
|
||||||
|
const divisor = this.add(1, taxRate);
|
||||||
|
const baseAmount = this.divide(totalAmount, divisor);
|
||||||
|
const taxAmount = this.subtract(totalAmount, baseAmount);
|
||||||
|
|
||||||
|
return { baseAmount, taxAmount };
|
||||||
|
}
|
||||||
|
}
|
325
ts/calculation.classes.financial.ts
Normal file
325
ts/calculation.classes.financial.ts
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
import * as plugins from './calculation.plugins.js';
|
||||||
|
import { Calculator } from './calculation.classes.calculator.js';
|
||||||
|
|
||||||
|
export interface ICashFlow {
|
||||||
|
amount: plugins.Decimal.Value;
|
||||||
|
period?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Financial calculations class providing time value of money and investment analysis functions
|
||||||
|
*/
|
||||||
|
export class Financial extends Calculator {
|
||||||
|
constructor() {
|
||||||
|
super({ precision: 15 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate Present Value (PV)
|
||||||
|
* @param futureValue The future value
|
||||||
|
* @param rate The interest rate per period
|
||||||
|
* @param periods The number of periods
|
||||||
|
*/
|
||||||
|
public presentValue(
|
||||||
|
futureValue: plugins.Decimal.Value,
|
||||||
|
rate: plugins.Decimal.Value,
|
||||||
|
periods: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
const n = this.decimal(periods);
|
||||||
|
const fv = this.decimal(futureValue);
|
||||||
|
|
||||||
|
const denominator = this.power(this.add(1, r), n);
|
||||||
|
return this.divide(fv, denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate Future Value (FV)
|
||||||
|
* @param presentValue The present value
|
||||||
|
* @param rate The interest rate per period
|
||||||
|
* @param periods The number of periods
|
||||||
|
*/
|
||||||
|
public futureValue(
|
||||||
|
presentValue: plugins.Decimal.Value,
|
||||||
|
rate: plugins.Decimal.Value,
|
||||||
|
periods: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
const n = this.decimal(periods);
|
||||||
|
const pv = this.decimal(presentValue);
|
||||||
|
|
||||||
|
const multiplier = this.power(this.add(1, r), n);
|
||||||
|
return this.multiply(pv, multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate Payment (PMT) for a loan
|
||||||
|
* @param principal The loan principal
|
||||||
|
* @param rate The interest rate per period
|
||||||
|
* @param periods The number of periods
|
||||||
|
*/
|
||||||
|
public payment(
|
||||||
|
principal: plugins.Decimal.Value,
|
||||||
|
rate: plugins.Decimal.Value,
|
||||||
|
periods: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
const n = this.decimal(periods);
|
||||||
|
const p = this.decimal(principal);
|
||||||
|
|
||||||
|
if (r.isZero()) {
|
||||||
|
return this.divide(p, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
const numerator = this.multiply(p, r);
|
||||||
|
const denominator = this.subtract(1, this.power(this.add(1, r), this.multiply(-1, n)));
|
||||||
|
|
||||||
|
return this.divide(numerator, denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate Net Present Value (NPV)
|
||||||
|
* @param rate The discount rate
|
||||||
|
* @param cashFlows Array of cash flows (first element is initial investment, usually negative)
|
||||||
|
*/
|
||||||
|
public npv(rate: plugins.Decimal.Value, cashFlows: plugins.Decimal.Value[]): plugins.Decimal {
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
|
||||||
|
return cashFlows.reduce<plugins.Decimal>((npv, cashFlow, period) => {
|
||||||
|
const cf = this.decimal(cashFlow);
|
||||||
|
const discountFactor = this.power(this.add(1, r), period);
|
||||||
|
const presentValue = this.divide(cf, discountFactor);
|
||||||
|
return this.add(npv, presentValue);
|
||||||
|
}, this.decimal(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate Internal Rate of Return (IRR) using Newton-Raphson method
|
||||||
|
* @param cashFlows Array of cash flows
|
||||||
|
* @param guess Initial guess for IRR (default: 0.1)
|
||||||
|
* @param tolerance Convergence tolerance (default: 0.000001)
|
||||||
|
* @param maxIterations Maximum iterations (default: 1000)
|
||||||
|
*/
|
||||||
|
public irr(
|
||||||
|
cashFlows: plugins.Decimal.Value[],
|
||||||
|
guess: plugins.Decimal.Value = 0.1,
|
||||||
|
tolerance: plugins.Decimal.Value = 0.000001,
|
||||||
|
maxIterations: number = 1000
|
||||||
|
): plugins.Decimal {
|
||||||
|
let rate = this.decimal(guess);
|
||||||
|
const tol = this.decimal(tolerance);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxIterations; i++) {
|
||||||
|
const npvValue = this.npv(rate, cashFlows);
|
||||||
|
const npvDerivative = this.npvDerivative(rate, cashFlows);
|
||||||
|
|
||||||
|
if (npvDerivative.isZero()) {
|
||||||
|
throw new Error('IRR calculation failed: derivative is zero');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRate = this.subtract(rate, this.divide(npvValue, npvDerivative));
|
||||||
|
|
||||||
|
if (this.abs(this.subtract(newRate, rate)).lessThan(tol)) {
|
||||||
|
return newRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
rate = newRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('IRR calculation failed: maximum iterations reached');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the derivative of NPV with respect to rate (for IRR calculation)
|
||||||
|
*/
|
||||||
|
private npvDerivative(rate: plugins.Decimal.Value, cashFlows: plugins.Decimal.Value[]): plugins.Decimal {
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
|
||||||
|
return cashFlows.reduce<plugins.Decimal>((derivative, cashFlow, period) => {
|
||||||
|
if (period === 0) return derivative;
|
||||||
|
|
||||||
|
const cf = this.decimal(cashFlow);
|
||||||
|
const negPeriod = this.decimal(period).neg();
|
||||||
|
const onePlusR = this.add(1, r);
|
||||||
|
const power = this.add(negPeriod, -1);
|
||||||
|
const discountFactor = this.power(onePlusR, power);
|
||||||
|
const termDerivative = this.multiply(this.multiply(negPeriod, cf), discountFactor);
|
||||||
|
|
||||||
|
return this.add(derivative, termDerivative);
|
||||||
|
}, this.decimal(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate Modified Internal Rate of Return (MIRR)
|
||||||
|
* @param cashFlows Array of cash flows
|
||||||
|
* @param financeRate Rate for negative cash flows
|
||||||
|
* @param reinvestRate Rate for positive cash flows
|
||||||
|
*/
|
||||||
|
public mirr(
|
||||||
|
cashFlows: plugins.Decimal.Value[],
|
||||||
|
financeRate: plugins.Decimal.Value,
|
||||||
|
reinvestRate: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const fRate = this.decimal(financeRate);
|
||||||
|
const rRate = this.decimal(reinvestRate);
|
||||||
|
const n = cashFlows.length - 1;
|
||||||
|
|
||||||
|
let pvNegative = this.decimal(0);
|
||||||
|
let fvPositive = this.decimal(0);
|
||||||
|
|
||||||
|
cashFlows.forEach((cashFlow, period) => {
|
||||||
|
const cf = this.decimal(cashFlow);
|
||||||
|
if (cf.lessThan(0)) {
|
||||||
|
pvNegative = this.add(pvNegative, this.presentValue(cf, fRate, period));
|
||||||
|
} else {
|
||||||
|
fvPositive = this.add(fvPositive, this.futureValue(cf, rRate, n - period));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pvNegative.isZero() || fvPositive.isZero()) {
|
||||||
|
throw new Error('MIRR calculation failed: no negative or positive cash flows');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = this.divide(fvPositive, this.abs(pvNegative));
|
||||||
|
const result = this.subtract(this.power(ratio, this.divide(1, n)), 1);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the number of periods for an investment to reach a target value
|
||||||
|
* @param presentValue The present value
|
||||||
|
* @param futureValue The future value
|
||||||
|
* @param rate The interest rate per period
|
||||||
|
*/
|
||||||
|
public periods(
|
||||||
|
presentValue: plugins.Decimal.Value,
|
||||||
|
futureValue: plugins.Decimal.Value,
|
||||||
|
rate: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const pv = this.decimal(presentValue);
|
||||||
|
const fv = this.decimal(futureValue);
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
|
||||||
|
if (r.isZero()) {
|
||||||
|
throw new Error('Cannot calculate periods with zero interest rate');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = this.divide(fv, pv);
|
||||||
|
const numerator = this.ln(ratio);
|
||||||
|
const denominator = this.ln(this.add(1, r));
|
||||||
|
|
||||||
|
return this.divide(numerator, denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the interest rate given PV, FV, and periods
|
||||||
|
* @param presentValue The present value
|
||||||
|
* @param futureValue The future value
|
||||||
|
* @param periods The number of periods
|
||||||
|
*/
|
||||||
|
public rate(
|
||||||
|
presentValue: plugins.Decimal.Value,
|
||||||
|
futureValue: plugins.Decimal.Value,
|
||||||
|
periods: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const pv = this.decimal(presentValue);
|
||||||
|
const fv = this.decimal(futureValue);
|
||||||
|
const n = this.decimal(periods);
|
||||||
|
|
||||||
|
if (n.isZero()) {
|
||||||
|
throw new Error('Cannot calculate rate with zero periods');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = this.divide(fv, pv);
|
||||||
|
const exponent = this.divide(1, n);
|
||||||
|
const result = this.subtract(this.power(ratio, exponent), 1);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate Extended Internal Rate of Return (XIRR) for irregular cash flows
|
||||||
|
* @param cashFlows Array of cash flow objects with amounts and dates
|
||||||
|
* @param dates Array of dates corresponding to cash flows
|
||||||
|
* @param guess Initial guess for XIRR (default: 0.1)
|
||||||
|
*/
|
||||||
|
public xirr(
|
||||||
|
cashFlows: plugins.Decimal.Value[],
|
||||||
|
dates: Date[],
|
||||||
|
guess: plugins.Decimal.Value = 0.1
|
||||||
|
): plugins.Decimal {
|
||||||
|
if (cashFlows.length !== dates.length) {
|
||||||
|
throw new Error('Cash flows and dates arrays must have the same length');
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDate = dates[0];
|
||||||
|
const yearFractions = dates.map(date => {
|
||||||
|
const daysDiff = (date.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24);
|
||||||
|
return this.divide(daysDiff, 365);
|
||||||
|
});
|
||||||
|
|
||||||
|
let rate = this.decimal(guess);
|
||||||
|
const tolerance = this.decimal(0.000001);
|
||||||
|
const maxIterations = 1000;
|
||||||
|
|
||||||
|
for (let i = 0; i < maxIterations; i++) {
|
||||||
|
const xnpv = this.xnpv(rate, cashFlows, yearFractions);
|
||||||
|
const xnpvDerivative = this.xnpvDerivative(rate, cashFlows, yearFractions);
|
||||||
|
|
||||||
|
if (xnpvDerivative.isZero()) {
|
||||||
|
throw new Error('XIRR calculation failed: derivative is zero');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRate = this.subtract(rate, this.divide(xnpv, xnpvDerivative));
|
||||||
|
|
||||||
|
if (this.abs(this.subtract(newRate, rate)).lessThan(tolerance)) {
|
||||||
|
return newRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
rate = newRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('XIRR calculation failed: maximum iterations reached');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate XNPV for irregular cash flows
|
||||||
|
*/
|
||||||
|
private xnpv(
|
||||||
|
rate: plugins.Decimal.Value,
|
||||||
|
cashFlows: plugins.Decimal.Value[],
|
||||||
|
yearFractions: plugins.Decimal[]
|
||||||
|
): plugins.Decimal {
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
|
||||||
|
return cashFlows.reduce<plugins.Decimal>((npv, cashFlow, index) => {
|
||||||
|
const cf = this.decimal(cashFlow);
|
||||||
|
const t = yearFractions[index];
|
||||||
|
const discountFactor = this.power(this.add(1, r), t);
|
||||||
|
const presentValue = this.divide(cf, discountFactor);
|
||||||
|
return this.add(npv, presentValue);
|
||||||
|
}, this.decimal(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the derivative of XNPV
|
||||||
|
*/
|
||||||
|
private xnpvDerivative(
|
||||||
|
rate: plugins.Decimal.Value,
|
||||||
|
cashFlows: plugins.Decimal.Value[],
|
||||||
|
yearFractions: plugins.Decimal[]
|
||||||
|
): plugins.Decimal {
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
|
||||||
|
return cashFlows.reduce<plugins.Decimal>((derivative, cashFlow, index) => {
|
||||||
|
const cf = this.decimal(cashFlow);
|
||||||
|
const t = yearFractions[index];
|
||||||
|
const negT = this.multiply(-1, t);
|
||||||
|
const discountFactor = this.power(this.add(1, r), this.subtract(negT, 1));
|
||||||
|
const termDerivative = this.multiply(this.multiply(negT, cf), discountFactor);
|
||||||
|
|
||||||
|
return this.add(derivative, termDerivative);
|
||||||
|
}, this.decimal(0));
|
||||||
|
}
|
||||||
|
}
|
297
ts/calculation.classes.interest.ts
Normal file
297
ts/calculation.classes.interest.ts
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
import * as plugins from './calculation.plugins.js';
|
||||||
|
import { Calculator } from './calculation.classes.calculator.js';
|
||||||
|
|
||||||
|
export type CompoundingFrequency = 'annually' | 'semiannually' | 'quarterly' | 'monthly' | 'weekly' | 'daily' | 'continuous';
|
||||||
|
|
||||||
|
export interface IInterestOptions {
|
||||||
|
principal: plugins.Decimal.Value;
|
||||||
|
rate: plugins.Decimal.Value;
|
||||||
|
time: plugins.Decimal.Value;
|
||||||
|
compoundingFrequency?: CompoundingFrequency;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interest calculations class providing various interest computation methods
|
||||||
|
*/
|
||||||
|
export class Interest extends Calculator {
|
||||||
|
private readonly frequencyMap: Record<CompoundingFrequency, number> = {
|
||||||
|
annually: 1,
|
||||||
|
semiannually: 2,
|
||||||
|
quarterly: 4,
|
||||||
|
monthly: 12,
|
||||||
|
weekly: 52,
|
||||||
|
daily: 365,
|
||||||
|
continuous: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({ precision: 15 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate simple interest
|
||||||
|
* @param principal The principal amount
|
||||||
|
* @param rate The annual interest rate (as decimal, e.g., 0.05 for 5%)
|
||||||
|
* @param time The time period in years
|
||||||
|
*/
|
||||||
|
public simple(
|
||||||
|
principal: plugins.Decimal.Value,
|
||||||
|
rate: plugins.Decimal.Value,
|
||||||
|
time: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const p = this.decimal(principal);
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
const t = this.decimal(time);
|
||||||
|
|
||||||
|
return this.multiply(p, r, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate simple interest amount (principal + interest)
|
||||||
|
*/
|
||||||
|
public simpleAmount(
|
||||||
|
principal: plugins.Decimal.Value,
|
||||||
|
rate: plugins.Decimal.Value,
|
||||||
|
time: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const p = this.decimal(principal);
|
||||||
|
const interest = this.simple(principal, rate, time);
|
||||||
|
|
||||||
|
return this.add(p, interest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate compound interest
|
||||||
|
* @param principal The principal amount
|
||||||
|
* @param rate The annual interest rate (as decimal)
|
||||||
|
* @param time The time period in years
|
||||||
|
* @param frequency The compounding frequency (default: annually)
|
||||||
|
*/
|
||||||
|
public compound(
|
||||||
|
principal: plugins.Decimal.Value,
|
||||||
|
rate: plugins.Decimal.Value,
|
||||||
|
time: plugins.Decimal.Value,
|
||||||
|
frequency: CompoundingFrequency = 'annually'
|
||||||
|
): plugins.Decimal {
|
||||||
|
const p = this.decimal(principal);
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
const t = this.decimal(time);
|
||||||
|
|
||||||
|
if (frequency === 'continuous') {
|
||||||
|
return this.continuousCompound(principal, rate, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = this.decimal(this.frequencyMap[frequency]);
|
||||||
|
const amount = this.compoundAmount(principal, rate, time, frequency);
|
||||||
|
|
||||||
|
return this.subtract(amount, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate compound interest amount (principal + interest)
|
||||||
|
*/
|
||||||
|
public compoundAmount(
|
||||||
|
principal: plugins.Decimal.Value,
|
||||||
|
rate: plugins.Decimal.Value,
|
||||||
|
time: plugins.Decimal.Value,
|
||||||
|
frequency: CompoundingFrequency = 'annually'
|
||||||
|
): plugins.Decimal {
|
||||||
|
const p = this.decimal(principal);
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
const t = this.decimal(time);
|
||||||
|
|
||||||
|
if (frequency === 'continuous') {
|
||||||
|
return this.continuousCompoundAmount(principal, rate, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = this.decimal(this.frequencyMap[frequency]);
|
||||||
|
const ratePerPeriod = this.divide(r, n);
|
||||||
|
const periods = this.multiply(n, t);
|
||||||
|
const multiplier = this.power(this.add(1, ratePerPeriod), periods);
|
||||||
|
|
||||||
|
return this.multiply(p, multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate continuous compound interest
|
||||||
|
*/
|
||||||
|
private continuousCompound(
|
||||||
|
principal: plugins.Decimal.Value,
|
||||||
|
rate: plugins.Decimal.Value,
|
||||||
|
time: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const p = this.decimal(principal);
|
||||||
|
const amount = this.continuousCompoundAmount(principal, rate, time);
|
||||||
|
|
||||||
|
return this.subtract(amount, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate continuous compound interest amount
|
||||||
|
*/
|
||||||
|
private continuousCompoundAmount(
|
||||||
|
principal: plugins.Decimal.Value,
|
||||||
|
rate: plugins.Decimal.Value,
|
||||||
|
time: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const p = this.decimal(principal);
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
const t = this.decimal(time);
|
||||||
|
|
||||||
|
const exponent = this.multiply(r, t);
|
||||||
|
const multiplier = this.exp(exponent);
|
||||||
|
|
||||||
|
return this.multiply(p, multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate effective annual rate (EAR) from nominal rate
|
||||||
|
* @param nominalRate The nominal annual interest rate
|
||||||
|
* @param frequency The compounding frequency
|
||||||
|
*/
|
||||||
|
public effectiveAnnualRate(
|
||||||
|
nominalRate: plugins.Decimal.Value,
|
||||||
|
frequency: CompoundingFrequency = 'annually'
|
||||||
|
): plugins.Decimal {
|
||||||
|
const r = this.decimal(nominalRate);
|
||||||
|
|
||||||
|
if (frequency === 'continuous') {
|
||||||
|
return this.subtract(this.exp(r), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = this.decimal(this.frequencyMap[frequency]);
|
||||||
|
const ratePerPeriod = this.divide(r, n);
|
||||||
|
const onePlusRate = this.add(1, ratePerPeriod);
|
||||||
|
const compounded = this.power(onePlusRate, n);
|
||||||
|
|
||||||
|
return this.subtract(compounded, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate nominal rate from effective annual rate
|
||||||
|
* @param effectiveRate The effective annual rate
|
||||||
|
* @param frequency The compounding frequency
|
||||||
|
*/
|
||||||
|
public nominalRate(
|
||||||
|
effectiveRate: plugins.Decimal.Value,
|
||||||
|
frequency: CompoundingFrequency = 'annually'
|
||||||
|
): plugins.Decimal {
|
||||||
|
const ear = this.decimal(effectiveRate);
|
||||||
|
|
||||||
|
if (frequency === 'continuous') {
|
||||||
|
return this.ln(this.add(1, ear));
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = this.decimal(this.frequencyMap[frequency]);
|
||||||
|
const onePlusEar = this.add(1, ear);
|
||||||
|
const exponent = this.divide(1, n);
|
||||||
|
const nthRoot = this.power(onePlusEar, exponent);
|
||||||
|
const ratePerPeriod = this.subtract(nthRoot, 1);
|
||||||
|
|
||||||
|
return this.multiply(ratePerPeriod, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the real interest rate adjusted for inflation
|
||||||
|
* @param nominalRate The nominal interest rate
|
||||||
|
* @param inflationRate The inflation rate
|
||||||
|
*/
|
||||||
|
public realRate(
|
||||||
|
nominalRate: plugins.Decimal.Value,
|
||||||
|
inflationRate: plugins.Decimal.Value
|
||||||
|
): plugins.Decimal {
|
||||||
|
const r = this.decimal(nominalRate);
|
||||||
|
const i = this.decimal(inflationRate);
|
||||||
|
|
||||||
|
const numerator = this.subtract(r, i);
|
||||||
|
const denominator = this.add(1, i);
|
||||||
|
|
||||||
|
return this.divide(numerator, denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the time required to double an investment
|
||||||
|
* @param rate The interest rate
|
||||||
|
* @param frequency The compounding frequency
|
||||||
|
*/
|
||||||
|
public doubleTime(
|
||||||
|
rate: plugins.Decimal.Value,
|
||||||
|
frequency: CompoundingFrequency = 'annually'
|
||||||
|
): plugins.Decimal {
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
|
||||||
|
if (r.isZero()) {
|
||||||
|
throw new Error('Cannot calculate doubling time with zero interest rate');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frequency === 'continuous') {
|
||||||
|
return this.divide(this.ln(2), r);
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = this.decimal(this.frequencyMap[frequency]);
|
||||||
|
const ratePerPeriod = this.divide(r, n);
|
||||||
|
const numerator = this.ln(2);
|
||||||
|
const denominator = this.multiply(n, this.ln(this.add(1, ratePerPeriod)));
|
||||||
|
|
||||||
|
return this.divide(numerator, denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the Rule of 72 approximation for doubling time
|
||||||
|
* @param rate The annual interest rate (as percentage, e.g., 5 for 5%)
|
||||||
|
*/
|
||||||
|
public ruleOf72(rate: plugins.Decimal.Value): plugins.Decimal {
|
||||||
|
const r = this.decimal(rate);
|
||||||
|
|
||||||
|
if (r.isZero()) {
|
||||||
|
throw new Error('Cannot calculate Rule of 72 with zero interest rate');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.divide(72, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate periodic interest rate from annual rate
|
||||||
|
* @param annualRate The annual interest rate
|
||||||
|
* @param frequency The compounding frequency
|
||||||
|
*/
|
||||||
|
public periodicRate(
|
||||||
|
annualRate: plugins.Decimal.Value,
|
||||||
|
frequency: CompoundingFrequency
|
||||||
|
): plugins.Decimal {
|
||||||
|
if (frequency === 'continuous') {
|
||||||
|
throw new Error('Periodic rate not applicable for continuous compounding');
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = this.decimal(annualRate);
|
||||||
|
const n = this.decimal(this.frequencyMap[frequency]);
|
||||||
|
|
||||||
|
return this.divide(r, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate annual percentage yield (APY)
|
||||||
|
* Same as effective annual rate but commonly used in banking
|
||||||
|
*/
|
||||||
|
public apy(
|
||||||
|
nominalRate: plugins.Decimal.Value,
|
||||||
|
frequency: CompoundingFrequency = 'annually'
|
||||||
|
): plugins.Decimal {
|
||||||
|
return this.effectiveAnnualRate(nominalRate, frequency);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate interest on interest (compound interest minus simple interest)
|
||||||
|
*/
|
||||||
|
public interestOnInterest(
|
||||||
|
principal: plugins.Decimal.Value,
|
||||||
|
rate: plugins.Decimal.Value,
|
||||||
|
time: plugins.Decimal.Value,
|
||||||
|
frequency: CompoundingFrequency = 'annually'
|
||||||
|
): plugins.Decimal {
|
||||||
|
const simpleInt = this.simple(principal, rate, time);
|
||||||
|
const compoundInt = this.compound(principal, rate, time, frequency);
|
||||||
|
|
||||||
|
return this.subtract(compoundInt, simpleInt);
|
||||||
|
}
|
||||||
|
}
|
7
ts/calculation.plugins.ts
Normal file
7
ts/calculation.plugins.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
|
import { Decimal } from 'decimal.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
smartpromise,
|
||||||
|
Decimal
|
||||||
|
};
|
5
ts/index.ts
Normal file
5
ts/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './calculation.classes.calculator.js';
|
||||||
|
export * from './calculation.classes.financial.js';
|
||||||
|
export * from './calculation.classes.interest.js';
|
||||||
|
export * from './calculation.classes.amortization.js';
|
||||||
|
export * from './calculation.classes.currency.js';
|
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"verbatimModuleSyntax": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist_*/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
Reference in New Issue
Block a user