├── .github
├── dependabot.yml
└── workflows
│ ├── reviewdog.yml
│ └── tests.yml
├── go.mod
├── .editorconfig
├── Makefile
├── enums
├── paymentperiod
│ └── payment_period.go
├── interesttype
│ └── interest_type.go
└── frequency
│ └── frequency.go
├── .gitignore
├── financial.go
├── .reviewdog.yml
├── error_codes.go
├── LICENSE
├── reducing.go
├── flat.go
├── CHANGELOG.md
├── CONTRIBUTING.md
├── go.sum
├── .cursorignore
├── example_amortization_test.go
├── config.go
├── amortization.go
├── example_reducing_utils_test.go
├── reducing_utils_test.go
├── reducing_utils.go
├── config_test.go
├── README.md
└── amortization_test.go
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: gomod
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "04:00"
8 | timezone: Asia/Calcutta
9 | open-pull-requests-limit: 99
10 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/razorpay/go-financial
2 |
3 | go 1.15
4 |
5 | require (
6 | github.com/go-echarts/go-echarts/v2 v2.2.4
7 | github.com/shopspring/decimal v1.3.1
8 | github.com/smartystreets/assertions v1.2.1
9 | )
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; http://editorconfig.org/
2 |
3 | root = true
4 |
5 | [*]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 |
11 | [*.go]
12 | indent_style = tab
13 | indent_size = 4
14 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | install-lint:
2 | go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.34.1
3 | lint-check:
4 | @echo "Performing lint check"
5 | @golangci-lint --out-format line-number -D gosimple -E gofumpt,goimports,gosec run ./...
6 |
7 | test-unit:
8 | @echo "Run unit tests"
9 | go test -v ./...
10 |
--------------------------------------------------------------------------------
/enums/paymentperiod/payment_period.go:
--------------------------------------------------------------------------------
1 | package paymentperiod
2 |
3 | type Type uint8
4 |
5 | const (
6 | BEGINNING Type = iota + 1
7 | ENDING
8 | )
9 |
10 | var value = map[Type]int64{
11 | BEGINNING: 1,
12 | ENDING: 0,
13 | }
14 |
15 | func (t Type) Value() int64 {
16 | return value[t]
17 | }
18 |
--------------------------------------------------------------------------------
/enums/interesttype/interest_type.go:
--------------------------------------------------------------------------------
1 | package interesttype
2 |
3 | type Type uint8
4 |
5 | const (
6 | FLAT Type = iota + 1
7 | REDUCING
8 | )
9 |
10 | var toString = map[Type]string{
11 | FLAT: "flat",
12 | REDUCING: "reducing",
13 | }
14 |
15 | func (t Type) String() string {
16 | return toString[t]
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories
15 | vendor/
16 |
17 | # goland configs
18 | .idea
19 |
20 |
21 |
--------------------------------------------------------------------------------
/financial.go:
--------------------------------------------------------------------------------
1 | package gofinancial
2 |
3 | import "github.com/shopspring/decimal"
4 |
5 | // Financial interface defines the methods to be over ridden for different financial use cases.
6 | type Financial interface {
7 | GetPrincipal(config Config, period int64) decimal.Decimal
8 | GetInterest(config Config, period int64) decimal.Decimal
9 | GetPayment(config Config) decimal.Decimal
10 | }
11 |
--------------------------------------------------------------------------------
/enums/frequency/frequency.go:
--------------------------------------------------------------------------------
1 | package frequency
2 |
3 | type Type uint8
4 |
5 | const (
6 | DAILY Type = iota + 1
7 | WEEKLY
8 | MONTHLY
9 | ANNUALLY
10 | )
11 |
12 | // TODO: check if this assumption is ok
13 | var toValue = map[Type]int{
14 | DAILY: 365,
15 | WEEKLY: 52,
16 | MONTHLY: 12,
17 | ANNUALLY: 1,
18 | }
19 |
20 | func (t *Type) Value() int {
21 | return toValue[*t]
22 | }
23 |
--------------------------------------------------------------------------------
/.reviewdog.yml:
--------------------------------------------------------------------------------
1 | # reviewdog.yml
2 |
3 | runner:
4 | golint:
5 | cmd: golint $(go list ./... | grep -v /vendor/)
6 | errorformat:
7 | - golint
8 | level: info
9 | golangci:
10 | cmd: golangci-lint --out-format line-number -D gosimple -E gofumpt,goimports,gosec run
11 | errorformat:
12 | - '%E%f:%l:%c: %m'
13 | - '%E%f:%l: %m'
14 | - '%C%.%#'
15 | level: info
16 |
--------------------------------------------------------------------------------
/error_codes.go:
--------------------------------------------------------------------------------
1 | package gofinancial
2 |
3 | import "errors"
4 |
5 | var (
6 | ErrPayment = errors.New("payment not matching interest plus principal")
7 | ErrUnevenEndDate = errors.New("uneven end date")
8 | ErrInvalidFrequency = errors.New("invalid frequency")
9 | ErrNotEqual = errors.New("input values are not equal")
10 | ErrOutOfBounds = errors.New("error in representing data as it is out of bounds")
11 | ErrTolerence = errors.New("nan error as tolerence level exceeded")
12 | )
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Razorpay
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/reducing.go:
--------------------------------------------------------------------------------
1 | package gofinancial
2 |
3 | import "github.com/shopspring/decimal"
4 |
5 | // Reducing implements financial methods for facilitating a loan use case, following a reducing rate of interest.
6 | type Reducing struct{}
7 |
8 | // GetPrincipal returns principal amount contribution in a given period towards a loan, depending on config.
9 | func (r *Reducing) GetPrincipal(config Config, period int64) decimal.Decimal {
10 | return PPmt(config.getInterestRatePerPeriodInDecimal(), period, config.periods, config.AmountBorrowed, decimal.Zero, config.PaymentPeriod)
11 | }
12 |
13 | // GetInterest returns interest amount contribution in a given period towards a loan, depending on config.
14 | func (r *Reducing) GetInterest(config Config, period int64) decimal.Decimal {
15 | return IPmt(config.getInterestRatePerPeriodInDecimal(), period, config.periods, config.AmountBorrowed, decimal.Zero, config.PaymentPeriod)
16 | }
17 |
18 | // GetPayment returns the periodic payment to be done for a loan depending on config.
19 | func (r *Reducing) GetPayment(config Config) decimal.Decimal {
20 | return Pmt(config.getInterestRatePerPeriodInDecimal(), config.periods, config.AmountBorrowed, decimal.Zero, config.PaymentPeriod)
21 | }
22 |
--------------------------------------------------------------------------------
/flat.go:
--------------------------------------------------------------------------------
1 | package gofinancial
2 |
3 | import "github.com/shopspring/decimal"
4 |
5 | // Flat implements financial methods for facilitating a loan use case, following a flat rate of interest.
6 | type Flat struct{}
7 |
8 | // GetPrincipal returns principal amount contribution in a given period towards a loan, depending on config.
9 | func (f *Flat) GetPrincipal(config Config, _ int64) decimal.Decimal {
10 | dPeriod := decimal.NewFromInt(config.periods)
11 | minusOne := decimal.NewFromInt(-1)
12 | return config.AmountBorrowed.Div(dPeriod).Mul(minusOne)
13 | }
14 |
15 | // GetInterest returns interest amount contribution in a given period towards a loan, depending on config.
16 | func (f *Flat) GetInterest(config Config, period int64) decimal.Decimal {
17 | minusOne := decimal.NewFromInt(-1)
18 | return config.getInterestRatePerPeriodInDecimal().Mul(config.AmountBorrowed).Mul(minusOne)
19 | }
20 |
21 | // GetPayment returns the periodic payment to be done for a loan depending on config.
22 | func (f *Flat) GetPayment(config Config) decimal.Decimal {
23 | dPeriod := decimal.NewFromInt(config.periods)
24 | minusOne := decimal.NewFromInt(-1)
25 | totalInterest := config.getInterestRatePerPeriodInDecimal().Mul(dPeriod).Mul(config.AmountBorrowed)
26 | Payment := totalInterest.Add(config.AmountBorrowed).Mul(minusOne).Div(dPeriod)
27 | return Payment
28 | }
29 |
--------------------------------------------------------------------------------
/.github/workflows/reviewdog.yml:
--------------------------------------------------------------------------------
1 | name: reviewdog
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 | jobs:
8 | reviewdog:
9 | name: Run reviewdog
10 | runs-on: ubuntu-latest
11 | env:
12 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
13 | HOME: "/home/runner"
14 | GOCACHE: "/home/runner/.cache/go-build"
15 | GOENV: "/home/runner/.config/go/env"
16 | GOMODCACHE: "/home/runner/go/pkg/mod"
17 | GOOS: "linux"
18 | GOPATH: "/home/runner/go"
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 |
23 | - name: Setup go
24 | uses: actions/setup-go@v2
25 | with:
26 | go-version: ~1.15
27 |
28 | - name: Download Go modules
29 | run: go mod download
30 |
31 | - name: Install golangci-lint
32 | run: wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh
33 |
34 | - name: Move golangci-lint
35 | run: sudo cp ./bin/golangci-lint /usr/bin/
36 |
37 | - name: Run reviewdog action setup
38 | uses: reviewdog/action-setup@v1
39 |
40 | - name: Run reviewdog
41 | env:
42 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43 | run: |
44 | reviewdog -diff='git diff master' -conf=.reviewdog.yml -reporter=github-pr-review -runners=golangci -fail-on-error -level=info
45 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
4 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5 |
6 | ## [UNRELEASED][unreleased]
7 |
8 | ## [1.1.0][1.1.0]
9 |
10 | ### Added
11 | * Rate function
12 | * Dependabot support
13 |
14 | ### Fixed
15 | * Func definition in readme
16 |
17 | ## [1.0.0][1.0.0]
18 |
19 | ### Added
20 | * support for nper with examples
21 |
22 | ### Changed
23 | * Replace floats with decimal
24 |
25 | ### Fixed
26 | * Fix PMT, 0 rate case
27 | * Fix IPMT
28 |
29 | ## [0.2.0][0.2.0]
30 |
31 | ### Added
32 | * support for pv & npv functions with examples & test cases.
33 | * test cases for PlotRows
34 | * updated readme.
35 |
36 | ### Changed
37 | * refactored PlotRows
38 |
39 | ## [0.1.0][0.1.0]
40 |
41 | ### Added
42 | * support for fv, ipmt, pmt, ppmt functions.
43 | * support for amortisation table generation.
44 |
45 | [unreleased]: https://github.com/razorpay/go-financial/compare/v1.0.0...master
46 | [0.1.0]: https://github.com/razorpay/go-financial/releases/tag/v0.1.0
47 | [0.2.0]: https://github.com/razorpay/go-financial/releases/tag/v0.2.0
48 | [1.0.0]: https://github.com/razorpay/go-financial/releases/tag/v1.0.0
49 | [1.1.0]: https://github.com/razorpay/go-financial/releases/tag/v1.1.0
50 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | Thank you for your interest in contribution to go-financial.
4 | Here's the recommended process of contribution.
5 |
6 |
7 |
8 | If you've changed APIs, update the documentation.
9 | Make sure your code lints.
10 | Issue that pull request!
11 | 1. Fork the repo and create your branch from master.
12 | 1. hack, hack, hack.
13 | 1. If you've added code that should be tested, add tests.
14 | 1. If the functions are exported, make sure they are documented with examples.
15 | 1. Update README if required.
16 | 1. Make sure that unit tests are passing `make test-unit`.
17 | 1. Make sure that lint-checks are also passing `make lint-check`.
18 | 1. Make sure that your change follows best practices in Go
19 | - [Effective Go](https://golang.org/doc/effective_go.html)
20 | - [Go Code Review Comments](https://golang.org/wiki/CodeReviewComments)
21 | 1. Open a pull request in GitHub in this repo.
22 |
23 | When you work on a larger contribution, it is also recommended that you get in touch
24 | with us through the issue tracker.
25 |
26 | ## Code reviews
27 |
28 | All submissions, including submissions by project members, require review.
29 |
30 | ## Releases
31 |
32 | Releases are partially automated. To draft a new release, follow these steps:
33 |
34 | 1. Decide on a release version.
35 | 1. Create a new release/{version} branch and edit the `CHANGELOG.md`.
36 | 1. Make sure that the tests are passing.
37 | 1. Get the PR merged.
38 | 1. Tag the merge commit (`vX.Y.Z`) and create a release on GitHub for the tag. (repo maintainers)
39 | 1. (Required) Sit back and pat yourself on the back for a job well done :clap:.
40 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: unit-tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | cancel:
7 | runs-on: ubuntu-latest
8 | name: Cancel Previous Runs
9 | if: always()
10 | steps:
11 | - uses: styfle/cancel-workflow-action@d57d93c3a8110b00c3a2c0b64b8516013c9fd4c9
12 | if: github.ref != 'refs/heads/master'
13 | name: cancel old workflows
14 | id: cancel
15 | with:
16 | access_token: ${{ github.token }}
17 | - if: github.ref == 'refs/heads/master'
18 | name: Don't cancel old workflows
19 | id: dont_cancel
20 | run: |
21 | echo "Don't cancel old workflow"
22 | tests:
23 | name: unit-tests
24 | runs-on: ubuntu-latest
25 | steps:
26 | - name: Install Go
27 | uses: actions/setup-go@v2
28 | - name: Checkout code
29 | uses: actions/checkout@v2
30 | - name: Unit test
31 | run: go test -v ./... -coverprofile=coverage.txt
32 | - name: Archive code coverage results
33 | if: contains(github.ref, 'master')
34 | uses: actions/upload-artifact@v2
35 | with:
36 | name: code-coverage-report
37 | path: ./coverage.txt
38 | code-coverage:
39 | name: Upload code coverage
40 | runs-on: ubuntu-latest
41 | needs: tests
42 | if: contains(github.ref, 'master')
43 | steps:
44 | - uses: actions/checkout@master
45 | - name: Download coverage report
46 | uses: actions/download-artifact@v2
47 | with:
48 | name: code-coverage-report
49 | - uses: codecov/codecov-action@v1
50 | with:
51 | file: ./coverage.txt
52 | flags: unittests
53 | name: go-financial
54 | fail_ci_if_error: true
55 | verbose: true
56 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/go-echarts/go-echarts/v2 v2.2.4 h1:SKJpdyNIyD65XjbUZjzg6SwccTNXEgmh+PlaO23g2H0=
5 | github.com/go-echarts/go-echarts/v2 v2.2.4/go.mod h1:6TOomEztzGDVDkOSCFBq3ed7xOYfbOqhaBzD0YV771A=
6 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
7 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
8 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
9 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
10 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
14 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
15 | github.com/smartystreets/assertions v1.2.1 h1:bKNHfEv7tSIjZ8JbKaFjzFINljxG4lzZvmHUnElzOIg=
16 | github.com/smartystreets/assertions v1.2.1/go.mod h1:wDmR7qL282YbGsPy6H/yAsesrxfxaaSlJazyFLYVFx8=
17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
18 | github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
19 | github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
21 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
22 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
23 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
24 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
25 |
--------------------------------------------------------------------------------
/.cursorignore:
--------------------------------------------------------------------------------
1 | # Distribution and Environment
2 | dist/*
3 | build/*
4 | venv/*
5 | env/*
6 | *.env
7 | .env.*
8 | virtualenv/*
9 | .python-version
10 | .ruby-version
11 | .node-version
12 |
13 | # Logs and Temporary Files
14 | *.log
15 | *.tsv
16 | *.csv
17 | *.txt
18 | tmp/*
19 | temp/*
20 | .tmp/*
21 | *.temp
22 | *.cache
23 | .cache/*
24 | logs/*
25 |
26 | # Sensitive Data
27 | *.sqlite
28 | *.sqlite3
29 | *.dbsql
30 | secrets.*
31 | .npmrc
32 | .yarnrc
33 | .aws/*
34 | .config/*
35 |
36 | # Credentials and Keys
37 | *.pem
38 | *.ppk
39 | *.key
40 | *.pub
41 | *.p12
42 | *.pfx
43 | *.htpasswd
44 | *.keystore
45 | *.jks
46 | *.truststore
47 | *.cer
48 | id_rsa*
49 | known_hosts
50 | authorized_keys
51 | .ssh/*
52 | .gnupg/*
53 | .pgpass
54 |
55 | # Config Files
56 | *.conf
57 | *.toml
58 | *.ini
59 | .env.local
60 | .env.development
61 | .env.test
62 | .env.production
63 | config/*
64 |
65 | # Database Files
66 | *.sql
67 | *.db
68 | *.dmp
69 | *.dump
70 | *.backup
71 | *.restore
72 | *.mdb
73 | *.accdb
74 | *.realm*
75 |
76 | # Backup and Archive Files
77 | *.bak
78 | *.backup
79 | *.swp
80 | *.swo
81 | *.swn
82 | *~
83 | *.old
84 | *.orig
85 | *.archive
86 | *.gz
87 | *.zip
88 | *.tar
89 | *.rar
90 | *.7z
91 |
92 | # Compiled and Binary Files
93 | *.pyc
94 | *.pyo
95 | **/__pycache__/**
96 | *.class
97 | *.jar
98 | *.war
99 | *.ear
100 | *.dll
101 | *.exe
102 | *.so
103 | *.dylib
104 | *.bin
105 | *.obj
106 |
107 | # IDE and Editor Files
108 | .idea/*
109 | *.iml
110 | .vscode/*
111 | .project
112 | .classpath
113 | .settings/*
114 | *.sublime-*
115 | .atom/*
116 | .eclipse/*
117 | *.code-workspace
118 | .history/*
119 |
120 | # Build and Dependency Directories
121 | node_modules/*
122 | bower_components/*
123 | vendor/*
124 | packages/*
125 | jspm_packages/*
126 | .gradle/*
127 | target/*
128 | out/*
129 |
130 | # Testing and Coverage Files
131 | coverage/*
132 | .coverage
133 | htmlcov/*
134 | .pytest_cache/*
135 | .tox/*
136 | junit.xml
137 | test-results/*
138 |
139 | # Mobile Development
140 | *.apk
141 | *.aab
142 | *.ipa
143 | *.xcarchive
144 | *.provisionprofile
145 | google-services.json
146 | GoogleService-Info.plist
147 |
148 | # Certificate and Security Files
149 | *.crt
150 | *.csr
151 | *.ovpn
152 | *.p7b
153 | *.p7s
154 | *.pfx
155 | *.spc
156 | *.stl
157 | *.pem.crt
158 | ssl/*
159 |
160 | # Container and Infrastructure
161 | *.tfstate
162 | *.tfstate.backup
163 | .terraform/*
164 | .vagrant/*
165 | docker-compose.override.yml
166 | kubernetes/*
167 |
168 | # Design and Media Files (often large and binary)
169 | *.psd
170 | *.ai
171 | *.sketch
172 | *.fig
173 | *.xd
174 | assets/raw/*
175 |
--------------------------------------------------------------------------------
/example_amortization_test.go:
--------------------------------------------------------------------------------
1 | package gofinancial_test
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/shopspring/decimal"
7 |
8 | gofinancial "github.com/razorpay/go-financial"
9 | "github.com/razorpay/go-financial/enums/frequency"
10 | "github.com/razorpay/go-financial/enums/interesttype"
11 | "github.com/razorpay/go-financial/enums/paymentperiod"
12 | )
13 |
14 | // This example generates amortization table for a loan of 20 lakhs over 15years at 12% per annum.
15 | func ExampleAmortization_GenerateTable() {
16 | loc, err := time.LoadLocation("Asia/Kolkata")
17 | if err != nil {
18 | panic("location loading error")
19 | }
20 | currentDate := time.Date(2009, 11, 11, 4, 30, 0, 0, loc)
21 | config := gofinancial.Config{
22 |
23 | // start date is inclusive
24 | StartDate: currentDate,
25 |
26 | // end date is inclusive.
27 | EndDate: currentDate.AddDate(15, 0, 0).AddDate(0, 0, -1),
28 | Frequency: frequency.ANNUALLY,
29 |
30 | // AmountBorrowed is in paisa
31 | AmountBorrowed: decimal.NewFromInt(200000000),
32 |
33 | // InterestType can be flat or reducing
34 | InterestType: interesttype.REDUCING,
35 |
36 | // interest is in basis points
37 | Interest: decimal.NewFromInt(1200),
38 |
39 | // amount is paid at the end of the period
40 | PaymentPeriod: paymentperiod.ENDING,
41 |
42 | // all values will be rounded
43 | EnableRounding: true,
44 |
45 | // it will be rounded to nearest int
46 | RoundingPlaces: 0,
47 |
48 | // no error is tolerated
49 | RoundingErrorTolerance: decimal.Zero,
50 | }
51 | amortization, err := gofinancial.NewAmortization(&config)
52 | if err != nil {
53 | panic(err)
54 | }
55 |
56 | rows, err := amortization.GenerateTable()
57 | if err != nil {
58 | panic(err)
59 | }
60 | gofinancial.PrintRows(rows)
61 | // Output:
62 | // [
63 | // {
64 | // "Period": 1,
65 | // "StartDate": "2009-11-11T04:30:00+05:30",
66 | // "EndDate": "2010-11-10T23:59:59+05:30",
67 | // "Payment": "-29364848",
68 | // "Interest": "-24000000",
69 | // "Principal": "-5364848"
70 | // },
71 | // {
72 | // "Period": 2,
73 | // "StartDate": "2010-11-11T00:00:00+05:30",
74 | // "EndDate": "2011-11-10T23:59:59+05:30",
75 | // "Payment": "-29364848",
76 | // "Interest": "-23356218",
77 | // "Principal": "-6008630"
78 | // },
79 | // {
80 | // "Period": 3,
81 | // "StartDate": "2011-11-11T00:00:00+05:30",
82 | // "EndDate": "2012-11-10T23:59:59+05:30",
83 | // "Payment": "-29364848",
84 | // "Interest": "-22635183",
85 | // "Principal": "-6729665"
86 | // },
87 | // {
88 | // "Period": 4,
89 | // "StartDate": "2012-11-11T00:00:00+05:30",
90 | // "EndDate": "2013-11-10T23:59:59+05:30",
91 | // "Payment": "-29364848",
92 | // "Interest": "-21827623",
93 | // "Principal": "-7537225"
94 | // },
95 | // {
96 | // "Period": 5,
97 | // "StartDate": "2013-11-11T00:00:00+05:30",
98 | // "EndDate": "2014-11-10T23:59:59+05:30",
99 | // "Payment": "-29364848",
100 | // "Interest": "-20923156",
101 | // "Principal": "-8441692"
102 | // },
103 | // {
104 | // "Period": 6,
105 | // "StartDate": "2014-11-11T00:00:00+05:30",
106 | // "EndDate": "2015-11-10T23:59:59+05:30",
107 | // "Payment": "-29364848",
108 | // "Interest": "-19910153",
109 | // "Principal": "-9454695"
110 | // },
111 | // {
112 | // "Period": 7,
113 | // "StartDate": "2015-11-11T00:00:00+05:30",
114 | // "EndDate": "2016-11-10T23:59:59+05:30",
115 | // "Payment": "-29364848",
116 | // "Interest": "-18775589",
117 | // "Principal": "-10589259"
118 | // },
119 | // {
120 | // "Period": 8,
121 | // "StartDate": "2016-11-11T00:00:00+05:30",
122 | // "EndDate": "2017-11-10T23:59:59+05:30",
123 | // "Payment": "-29364848",
124 | // "Interest": "-17504878",
125 | // "Principal": "-11859970"
126 | // },
127 | // {
128 | // "Period": 9,
129 | // "StartDate": "2017-11-11T00:00:00+05:30",
130 | // "EndDate": "2018-11-10T23:59:59+05:30",
131 | // "Payment": "-29364848",
132 | // "Interest": "-16081682",
133 | // "Principal": "-13283166"
134 | // },
135 | // {
136 | // "Period": 10,
137 | // "StartDate": "2018-11-11T00:00:00+05:30",
138 | // "EndDate": "2019-11-10T23:59:59+05:30",
139 | // "Payment": "-29364848",
140 | // "Interest": "-14487702",
141 | // "Principal": "-14877146"
142 | // },
143 | // {
144 | // "Period": 11,
145 | // "StartDate": "2019-11-11T00:00:00+05:30",
146 | // "EndDate": "2020-11-10T23:59:59+05:30",
147 | // "Payment": "-29364848",
148 | // "Interest": "-12702445",
149 | // "Principal": "-16662403"
150 | // },
151 | // {
152 | // "Period": 12,
153 | // "StartDate": "2020-11-11T00:00:00+05:30",
154 | // "EndDate": "2021-11-10T23:59:59+05:30",
155 | // "Payment": "-29364848",
156 | // "Interest": "-10702956",
157 | // "Principal": "-18661892"
158 | // },
159 | // {
160 | // "Period": 13,
161 | // "StartDate": "2021-11-11T00:00:00+05:30",
162 | // "EndDate": "2022-11-10T23:59:59+05:30",
163 | // "Payment": "-29364848",
164 | // "Interest": "-8463529",
165 | // "Principal": "-20901319"
166 | // },
167 | // {
168 | // "Period": 14,
169 | // "StartDate": "2022-11-11T00:00:00+05:30",
170 | // "EndDate": "2023-11-10T23:59:59+05:30",
171 | // "Payment": "-29364848",
172 | // "Interest": "-5955371",
173 | // "Principal": "-23409477"
174 | // },
175 | // {
176 | // "Period": 15,
177 | // "StartDate": "2023-11-11T00:00:00+05:30",
178 | // "EndDate": "2024-11-10T23:59:59+05:30",
179 | // "Payment": "-29364847",
180 | // "Interest": "-3146234",
181 | // "Principal": "-26218613"
182 | // }
183 | // ]
184 | }
185 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package gofinancial
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/shopspring/decimal"
7 |
8 | "github.com/razorpay/go-financial/enums/paymentperiod"
9 |
10 | "github.com/razorpay/go-financial/enums/interesttype"
11 |
12 | "github.com/razorpay/go-financial/enums/frequency"
13 | )
14 |
15 | // Config is used to store details used in generation of amortization table.
16 | type Config struct {
17 | StartDate time.Time // Starting day of the amortization schedule(inclusive)
18 | EndDate time.Time // Ending day of the amortization schedule(inclusive)
19 | Frequency frequency.Type // Frequency enum with DAILY, WEEKLY, MONTHLY or ANNUALLY
20 | AmountBorrowed decimal.Decimal // Amount Borrowed
21 | InterestType interesttype.Type // InterestType enum with FLAT or REDUCING value.
22 | Interest decimal.Decimal // Interest in basis points
23 | PaymentPeriod paymentperiod.Type // Payment period enum to know whether payment made at the BEGINNING or ENDING of a period
24 | EnableRounding bool // If enabled, the final values in amortization schedule are rounded
25 | RoundingPlaces int32 // If specified, the final values in amortization schedule are rounded to these many places
26 | RoundingErrorTolerance decimal.Decimal // Any difference in [payment-(principal+interest)] will be adjusted in interest component, upto the RoundingErrorTolerance value specified
27 | periods int64 // derived
28 | startDates []time.Time // derived
29 | endDates []time.Time // derived
30 | }
31 |
32 | func (c *Config) setPeriodsAndDates() error {
33 | sy, sm, sd := c.StartDate.Date()
34 | startDate := time.Date(sy, sm, sd, 0, 0, 0, 0, c.StartDate.Location())
35 |
36 | ey, em, ed := c.EndDate.Date()
37 | endDate := time.Date(ey, em, ed, 0, 0, 0, 0, c.EndDate.Location())
38 |
39 | period, err := GetPeriodDifference(startDate, endDate, c.Frequency)
40 | if err != nil {
41 | return err
42 | }
43 | c.periods = int64(period)
44 | for i := 0; i < period; i++ {
45 | date, err := getStartDate(startDate, c.Frequency, i)
46 | if err != nil {
47 | return err
48 | }
49 | if i == 0 {
50 | c.startDates = append(c.startDates, c.StartDate)
51 | } else {
52 | c.startDates = append(c.startDates, date)
53 | }
54 | if endDate, err := getEndDates(date, c.Frequency); err != nil {
55 | return err
56 | } else {
57 | c.endDates = append(c.endDates, endDate)
58 | }
59 | }
60 | return nil
61 | }
62 |
63 | func GetPeriodDifference(from time.Time, to time.Time, freq frequency.Type) (int, error) {
64 | var periods int
65 | switch freq {
66 | case frequency.DAILY:
67 | periods = int(to.Sub(from).Hours()/24) + 1
68 | case frequency.WEEKLY:
69 | days := int(to.Sub(from).Hours()/24) + 1
70 | if days%7 != 0 {
71 | return -1, ErrUnevenEndDate
72 | }
73 | periods = days / 7
74 | case frequency.MONTHLY:
75 | months, err := getMonthsBetweenDates(from, to)
76 | if err != nil {
77 | return -1, err
78 | }
79 | periods = *months
80 | case frequency.ANNUALLY:
81 | years, err := getYearsBetweenDates(from, to)
82 | if err != nil {
83 | return -1, err
84 | }
85 | periods = *years
86 | default:
87 | return -1, ErrInvalidFrequency
88 | }
89 | return periods, nil
90 | }
91 |
92 | func getStartDate(date time.Time, freq frequency.Type, index int) (time.Time, error) {
93 | var startDate time.Time
94 | switch freq {
95 | case frequency.DAILY:
96 | startDate = date.AddDate(0, 0, index)
97 | case frequency.WEEKLY:
98 | startDate = date.AddDate(0, 0, 7*index)
99 | case frequency.MONTHLY:
100 | startDate = date.AddDate(0, index, 0)
101 | case frequency.ANNUALLY:
102 | startDate = date.AddDate(index, 0, 0)
103 | default:
104 | return time.Time{}, ErrInvalidFrequency
105 | }
106 | return startDate, nil
107 | }
108 |
109 | func getMonthsBetweenDates(start time.Time, end time.Time) (*int, error) {
110 | count := 0
111 | for start.Before(end) {
112 | start = start.AddDate(0, 1, 0)
113 | count++
114 | }
115 | finalDate := start.AddDate(0, 0, -1)
116 | if !finalDate.Equal(end) {
117 | return nil, ErrUnevenEndDate
118 | }
119 | return &count, nil
120 | }
121 |
122 | func getYearsBetweenDates(start time.Time, end time.Time) (*int, error) {
123 | count := 0
124 | for start.Before(end) {
125 | start = start.AddDate(1, 0, 0)
126 | count++
127 | }
128 | finalDate := start.AddDate(0, 0, -1)
129 | if !finalDate.Equal(end) {
130 | return nil, ErrUnevenEndDate
131 | }
132 | return &count, nil
133 | }
134 |
135 | func getEndDates(date time.Time, freq frequency.Type) (time.Time, error) {
136 | var nextDate time.Time
137 | switch freq {
138 | case frequency.DAILY:
139 | nextDate = time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 0, date.Location())
140 | case frequency.WEEKLY:
141 | date = date.AddDate(0, 0, 6)
142 | nextDate = time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 0, date.Location())
143 | case frequency.MONTHLY:
144 | date = date.AddDate(0, 1, 0).AddDate(0, 0, -1)
145 | nextDate = time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 0, date.Location())
146 | case frequency.ANNUALLY:
147 | date = date.AddDate(1, 0, 0).AddDate(0, 0, -1)
148 | nextDate = time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 0, date.Location())
149 | default:
150 | return time.Time{}, ErrInvalidFrequency
151 | }
152 | return nextDate, nil
153 | }
154 |
155 | func (c *Config) getInterestRatePerPeriodInDecimal() decimal.Decimal {
156 | hundred := decimal.NewFromInt(100)
157 | freq := decimal.NewFromInt(int64(c.Frequency.Value()))
158 | interestInPercent := c.Interest.Div(hundred)
159 | InterestInDecimal := interestInPercent.Div(hundred)
160 | InterestPerPeriod := InterestInDecimal.Div(freq)
161 | return InterestPerPeriod
162 | }
163 |
--------------------------------------------------------------------------------
/amortization.go:
--------------------------------------------------------------------------------
1 | package gofinancial
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io"
7 | "os"
8 | "path"
9 | "time"
10 |
11 | "github.com/shopspring/decimal"
12 |
13 | "github.com/go-echarts/go-echarts/v2/charts"
14 | "github.com/go-echarts/go-echarts/v2/opts"
15 | "github.com/razorpay/go-financial/enums/interesttype"
16 | )
17 |
18 | // Amortization struct holds the configuration and financial details.
19 | type Amortization struct {
20 | Config *Config
21 | Financial Financial
22 | }
23 |
24 | // NewAmortization return a new amortisation object with config and financial fields initialised.
25 | func NewAmortization(c *Config) (*Amortization, error) {
26 | a := Amortization{Config: c}
27 | if err := a.Config.setPeriodsAndDates(); err != nil {
28 | return nil, err
29 | }
30 | switch a.Config.InterestType {
31 | case interesttype.REDUCING:
32 | a.Financial = &Reducing{}
33 | case interesttype.FLAT:
34 | a.Financial = &Flat{}
35 | }
36 | return &a, nil
37 | }
38 |
39 | // Row represents a single row in an amortization schedule.
40 | type Row struct {
41 | Period int64
42 | StartDate time.Time
43 | EndDate time.Time
44 | Payment decimal.Decimal
45 | Interest decimal.Decimal
46 | Principal decimal.Decimal
47 | }
48 |
49 | // GenerateTable constructs the amortization table based on the configuration.
50 | func (a Amortization) GenerateTable() ([]Row, error) {
51 | var result []Row
52 | for i := int64(1); i <= a.Config.periods; i++ {
53 | var row Row
54 | row.Period = i
55 | row.StartDate = a.Config.startDates[i-1]
56 | row.EndDate = a.Config.endDates[i-1]
57 |
58 | payment := a.Financial.GetPayment(*a.Config)
59 | principalPayment := a.Financial.GetPrincipal(*a.Config, i)
60 | interestPayment := a.Financial.GetInterest(*a.Config, i)
61 | if a.Config.EnableRounding {
62 | row.Payment = payment.Round(a.Config.RoundingPlaces)
63 | row.Principal = principalPayment.Round(a.Config.RoundingPlaces)
64 | // to avoid rounding errors.
65 | row.Interest = row.Payment.Sub(row.Principal)
66 | } else {
67 | row.Payment = payment
68 | row.Principal = principalPayment
69 | row.Interest = interestPayment
70 | }
71 | if i == a.Config.periods {
72 | DoPrincipalAdjustmentDueToRounding(&row, result, a.Config.AmountBorrowed, a.Config.EnableRounding, a.Config.RoundingPlaces)
73 | }
74 | if err := sanityCheckUpdate(&row, a.Config.RoundingErrorTolerance); err != nil {
75 | return nil, err
76 | }
77 | result = append(result, row)
78 | }
79 | return result, nil
80 | }
81 |
82 | // DoPrincipalAdjustmentDueToRounding takes care of errors in total principal to be collected and adjusts it against the
83 | // the final principal and payment amount.
84 | func DoPrincipalAdjustmentDueToRounding(finalRow *Row, rows []Row, principal decimal.Decimal, round bool, places int32) {
85 | principalCollected := finalRow.Principal
86 | for _, row := range rows {
87 | principalCollected = principalCollected.Add(row.Principal)
88 | }
89 | diff := principal.Abs().Sub(principalCollected.Abs())
90 | if round {
91 | // subtracting diff coz payment, principal and interest are -ve.
92 | finalRow.Payment = finalRow.Payment.Sub(diff).Round(places)
93 | finalRow.Principal = finalRow.Principal.Sub(diff).Round(places)
94 | } else {
95 | finalRow.Payment = finalRow.Payment.Sub(diff)
96 | finalRow.Principal = finalRow.Principal.Sub(diff)
97 | }
98 | }
99 |
100 | // sanityCheckUpdate verifies the equation,
101 | // payment = principal + interest for every row.
102 | // If there is a mismatch due to rounding error and it is withing the tolerance,
103 | // the difference is adjusted against the interest.
104 | func sanityCheckUpdate(row *Row, tolerance decimal.Decimal) error {
105 | if !row.Payment.Equal(row.Principal.Add(row.Interest)) {
106 | diff := row.Payment.Abs().Sub(row.Principal.Add(row.Interest).Abs())
107 | if diff.LessThanOrEqual(tolerance) {
108 | row.Interest = row.Interest.Sub(diff)
109 | } else {
110 | return ErrPayment
111 | }
112 | }
113 | return nil
114 | }
115 |
116 | // PrintRows outputs a formatted json for given rows as input.
117 | func PrintRows(rows []Row) {
118 | bytes, _ := json.MarshalIndent(rows, "", "\t")
119 | fmt.Printf("%s", bytes)
120 | }
121 |
122 | // PlotRows uses the go-echarts package to generate an interactive plot from the Rows array.
123 | func PlotRows(rows []Row, fileName string) (err error) {
124 | bar := getStackedBarPlot(rows)
125 | completePath, err := os.Getwd()
126 | if err != nil {
127 | return err
128 | }
129 | filePath := path.Join(completePath, fileName)
130 | f, err := os.Create(fmt.Sprintf("%s.html", filePath))
131 | if err != nil {
132 | return err
133 | }
134 | defer func() {
135 | // setting named err
136 | ferr := f.Close()
137 | if err == nil {
138 | err = ferr
139 | }
140 | }()
141 | return renderer(bar, f)
142 | }
143 |
144 | // getStackedBarPlot returns an instance for stacked bar plot.
145 | func getStackedBarPlot(rows []Row) *charts.Bar {
146 | bar := charts.NewBar()
147 | bar.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
148 | Title: "Loan repayment schedule",
149 | },
150 | ),
151 | charts.WithInitializationOpts(opts.Initialization{
152 | Width: "1200px",
153 | Height: "600px",
154 | }),
155 | charts.WithToolboxOpts(opts.Toolbox{Show: true}),
156 | charts.WithLegendOpts(opts.Legend{Show: true}),
157 | charts.WithDataZoomOpts(opts.DataZoom{
158 | Type: "inside",
159 | Start: 0,
160 | End: 50,
161 | }),
162 | charts.WithDataZoomOpts(opts.DataZoom{
163 | Type: "slider",
164 | Start: 0,
165 | End: 50,
166 | }),
167 | )
168 | var xAxis []string
169 | var interestArr []opts.BarData
170 | var principalArr []opts.BarData
171 | var paymentArr []opts.BarData
172 | minusOne := decimal.NewFromInt(-1)
173 | for _, row := range rows {
174 | xAxis = append(xAxis, row.EndDate.Format("2006-01-02"))
175 | interestArr = append(interestArr, opts.BarData{Value: row.Interest.Mul(minusOne).String()})
176 | principalArr = append(principalArr, opts.BarData{Value: row.Principal.Mul(minusOne).String()})
177 | paymentArr = append(paymentArr, opts.BarData{Value: row.Payment.Mul(minusOne).String()})
178 | }
179 | // Put data into instance
180 | bar.SetXAxis(xAxis).
181 | AddSeries("Principal", principalArr).
182 | AddSeries("Interest", interestArr).
183 | AddSeries("Payment", paymentArr).SetSeriesOptions(
184 | charts.WithBarChartOpts(opts.BarChart{
185 | Stack: "stackA",
186 | }))
187 | return bar
188 | }
189 |
190 | // renderer renders the bar into the writer interface
191 | func renderer(bar *charts.Bar, writer io.Writer) error {
192 | return bar.Render(writer)
193 | }
194 |
--------------------------------------------------------------------------------
/example_reducing_utils_test.go:
--------------------------------------------------------------------------------
1 | package gofinancial_test
2 |
3 | import (
4 | "fmt"
5 |
6 | gofinancial "github.com/razorpay/go-financial"
7 | "github.com/razorpay/go-financial/enums/paymentperiod"
8 | "github.com/shopspring/decimal"
9 | )
10 |
11 | // If you have a loan of 1,00,000 to be paid after 2 years, with 18% p.a. compounded annually, how much total payment will you have to do each month?
12 | // This example generates the total monthly payment(principal plus interest) needed for a loan of 1,00,000 over 2 years with 18% rate of interest compounded monthly
13 | func ExamplePmt_loan() {
14 | rate := decimal.NewFromFloat(0.18 / 12)
15 | nper := int64(12 * 2)
16 | pv := decimal.NewFromInt(100000)
17 | fv := decimal.NewFromInt(0)
18 | when := paymentperiod.ENDING
19 | pmt := gofinancial.Pmt(rate, nper, pv, fv, when)
20 | fmt.Printf("payment:%v", pmt.Round(0))
21 | // Output:
22 | // payment:-4992
23 | }
24 |
25 | // If an investment gives 6% rate of return compounded annually, how much amount should you invest each month to get 10,00,000 amount after 10 years?
26 | func ExamplePmt_investment() {
27 | rate := decimal.NewFromFloat(0.06)
28 | nper := int64(10)
29 | pv := decimal.NewFromInt(0)
30 | fv := decimal.NewFromInt(1000000)
31 | when := paymentperiod.BEGINNING
32 | pmt := gofinancial.Pmt(rate, nper, pv, fv, when)
33 | fmt.Printf("payment each year:%v", pmt.Round(0))
34 | // Output:
35 | // payment each year:-71574
36 | }
37 |
38 | // If an investment has a 6% p.a. rate of return, compounded annually, and you are investing ₹ 10,000 at the end of each year with initial investment of ₹ 10,000, how
39 | // much amount will you get at the end of 10 years ?
40 | func ExampleFv() {
41 | rate := decimal.NewFromFloat(0.06)
42 | nper := int64(10)
43 | payment := decimal.NewFromInt(-10000)
44 | pv := decimal.NewFromInt(-10000)
45 | when := paymentperiod.ENDING
46 |
47 | fv := gofinancial.Fv(rate, nper, payment, pv, when)
48 | fmt.Printf("fv:%v", fv.Round(0))
49 | // Output:
50 | // fv:149716
51 | }
52 |
53 | // If you have a loan of 1,00,000 to be paid after 2 years, with 18% p.a. compounded annually, how much of the total payment done each month will be interest ?
54 | func ExampleIPmt_loan() {
55 | rate := decimal.NewFromFloat(0.18 / 12)
56 | nper := int64(12 * 2)
57 | pv := decimal.NewFromInt(100000)
58 | fv := decimal.NewFromInt(0)
59 | when := paymentperiod.ENDING
60 |
61 | for i := int64(0); i < nper; i++ {
62 | ipmt := gofinancial.IPmt(rate, i+1, nper, pv, fv, when)
63 | fmt.Printf("period:%d interest:%v\n", i+1, ipmt.Round(0))
64 | }
65 | // Output:
66 | // period:1 interest:-1500
67 | // period:2 interest:-1448
68 | // period:3 interest:-1394
69 | // period:4 interest:-1340
70 | // period:5 interest:-1286
71 | // period:6 interest:-1230
72 | // period:7 interest:-1174
73 | // period:8 interest:-1116
74 | // period:9 interest:-1058
75 | // period:10 interest:-999
76 | // period:11 interest:-939
77 | // period:12 interest:-879
78 | // period:13 interest:-817
79 | // period:14 interest:-754
80 | // period:15 interest:-691
81 | // period:16 interest:-626
82 | // period:17 interest:-561
83 | // period:18 interest:-494
84 | // period:19 interest:-427
85 | // period:20 interest:-358
86 | // period:21 interest:-289
87 | // period:22 interest:-218
88 | // period:23 interest:-146
89 | // period:24 interest:-74
90 | }
91 |
92 | // If an investment gives 6% rate of return compounded annually, how much interest will you earn each year against your
93 | // yearly payments(71574) to get 10,00,000 amount after 10 years
94 | func ExampleIPmt_investment() {
95 | rate := decimal.NewFromFloat(0.06)
96 | nper := int64(10)
97 | pv := decimal.NewFromInt(0)
98 | fv := decimal.NewFromInt(1000000)
99 | when := paymentperiod.BEGINNING
100 |
101 | for i := int64(1); i < nper+1; i++ {
102 | ipmt := gofinancial.IPmt(rate, i+1, nper, pv, fv, when)
103 | fmt.Printf("period:%d interest earned:%v\n", i, ipmt.Round(0))
104 | }
105 | // Output:
106 | // period:1 interest earned:4294
107 | // period:2 interest earned:8846
108 | // period:3 interest earned:13672
109 | // period:4 interest earned:18786
110 | // period:5 interest earned:24208
111 | // period:6 interest earned:29955
112 | // period:7 interest earned:36047
113 | // period:8 interest earned:42504
114 | // period:9 interest earned:49348
115 | // period:10 interest earned:56604
116 | }
117 |
118 | // If you have a loan of 1,00,000 to be paid after 2 years, with 18% p.a. compounded annually, how much total payment done each month will be principal ?
119 | func ExamplePPmt_loan() {
120 | rate := decimal.NewFromFloat(0.18 / 12)
121 | nper := int64(12 * 2)
122 | pv := decimal.NewFromInt(100000)
123 | fv := decimal.NewFromInt(0)
124 | when := paymentperiod.ENDING
125 |
126 | for i := int64(0); i < nper; i++ {
127 | ppmt := gofinancial.PPmt(rate, i+1, nper, pv, fv, when)
128 | fmt.Printf("period:%d principal:%v\n", i+1, ppmt.Round(0))
129 | }
130 | // Output:
131 | // period:1 principal:-3492
132 | // period:2 principal:-3545
133 | // period:3 principal:-3598
134 | // period:4 principal:-3652
135 | // period:5 principal:-3707
136 | // period:6 principal:-3762
137 | // period:7 principal:-3819
138 | // period:8 principal:-3876
139 | // period:9 principal:-3934
140 | // period:10 principal:-3993
141 | // period:11 principal:-4053
142 | // period:12 principal:-4114
143 | // period:13 principal:-4176
144 | // period:14 principal:-4238
145 | // period:15 principal:-4302
146 | // period:16 principal:-4366
147 | // period:17 principal:-4432
148 | // period:18 principal:-4498
149 | // period:19 principal:-4566
150 | // period:20 principal:-4634
151 | // period:21 principal:-4704
152 | // period:22 principal:-4774
153 | // period:23 principal:-4846
154 | // period:24 principal:-4919
155 | }
156 |
157 | // If an investment has a 6% p.a. rate of return, compounded annually, and you wish to possess ₹ 1,49,716 at the end of 10 peroids while providing ₹ 10,000 per period,
158 | // how much should you put as your initial deposit ?
159 | func ExamplePv() {
160 | rate := decimal.NewFromFloat(0.06)
161 | nper := int64(10)
162 | payment := decimal.NewFromInt(-10000)
163 | fv := decimal.NewFromInt(149716)
164 | when := paymentperiod.ENDING
165 |
166 | pv := gofinancial.Pv(rate, nper, payment, fv, when)
167 | fmt.Printf("pv:%v", pv.Round(0))
168 | // Output:
169 | // pv:-10000
170 | }
171 |
172 | // Given a discount rate of 8% per period and initial deposit of 40000 followed by withdrawls of 5000, 8000, 12000 and 30000.
173 | // What is the net present value of the cash flow ?
174 | func ExampleNpv() {
175 | rate := decimal.NewFromFloat(0.08)
176 | values := []decimal.Decimal{decimal.NewFromInt(-40000), decimal.NewFromInt(5000), decimal.NewFromInt(8000), decimal.NewFromInt(12000), decimal.NewFromInt(30000)}
177 | npv := gofinancial.Npv(rate, values)
178 | fmt.Printf("npv:%v", npv.Round(0))
179 | // Output:
180 | // npv:3065
181 | }
182 |
183 | // If a loan has a 6% annual interest, compounded monthly, and you only have $200/month to pay towards the loan,
184 | // how long would it take to pay-off the loan of $5,000?
185 | func ExampleNper() {
186 | rate := decimal.NewFromFloat(0.06 / 12)
187 | fv := decimal.Zero
188 | payment := decimal.NewFromInt(-200)
189 | pv := decimal.NewFromInt(5000)
190 | when := paymentperiod.ENDING
191 |
192 | nper, _ := gofinancial.Nper(rate, payment, pv, fv, when)
193 | fmt.Printf("nper:%v", nper.Ceil())
194 | // Output:
195 | // nper:27
196 | }
197 |
--------------------------------------------------------------------------------
/reducing_utils_test.go:
--------------------------------------------------------------------------------
1 | package gofinancial
2 |
3 | import (
4 | "errors"
5 | "testing"
6 |
7 | "github.com/shopspring/decimal"
8 |
9 | "github.com/razorpay/go-financial/enums/paymentperiod"
10 | )
11 |
12 | func Test_Pmt(t *testing.T) {
13 | type args struct {
14 | rate decimal.Decimal
15 | nper int64
16 | pv decimal.Decimal
17 | fv decimal.Decimal
18 | when paymentperiod.Type
19 | }
20 | tests := []struct {
21 | name string
22 | args args
23 | want decimal.Decimal
24 | }{
25 | {
26 | "7.5% p.a., monthly basis, 15 yrs", args{decimal.NewFromFloat(0.075 / 12), 12 * 15, decimal.NewFromInt(200000), decimal.NewFromInt(0), paymentperiod.ENDING}, decimal.NewFromFloat(-1854.0247200054675),
27 | },
28 | {
29 | "bigDecimal case. would give nan if it were float64.", args{decimal.NewFromFloat(12), 400, decimal.NewFromInt(10000), decimal.NewFromInt(5000), paymentperiod.BEGINNING}, decimal.NewFromFloat(-9230.7692307692307692),
30 | },
31 | {
32 | "24% p.a., monthly basis, 2 yrs", args{decimal.NewFromFloat(0.24 / 12), 12 * 2, decimal.NewFromInt(1000000), decimal.NewFromInt(0), 0}, decimal.NewFromFloat(-52871.097253249915),
33 | },
34 | {
35 | "8% p.a., monthly basis, 5 yrs", args{decimal.NewFromFloat(0.08 / 12), 12 * 5, decimal.NewFromInt(15000), decimal.NewFromInt(0), 0}, decimal.NewFromFloat(-304.1459143262052370338701494),
36 | },
37 | {
38 | "0%p.a. , monthly basis, 15 yrs", args{decimal.Zero, 15 * 12, decimal.NewFromInt(200000), decimal.Zero, paymentperiod.ENDING}, decimal.NewFromFloat(-1111.111111111111),
39 | },
40 | }
41 | for _, tt := range tests {
42 | t.Run(tt.name, func(t *testing.T) {
43 | got := Pmt(tt.args.rate, tt.args.nper, tt.args.pv, tt.args.fv, tt.args.when)
44 | if err := isAlmostEqual(got, tt.want, decimal.NewFromFloat(precision)); err != nil {
45 | t.Errorf("error: %v, pmt() = %v, want %v", err.Error(), got.BigInt().String(), tt.want)
46 | }
47 | })
48 | }
49 | }
50 |
51 | func Test_Fv(t *testing.T) {
52 | type args struct {
53 | rate decimal.Decimal
54 | nper int64
55 | pmt decimal.Decimal
56 | pv decimal.Decimal
57 | when paymentperiod.Type
58 | }
59 | tests := []struct {
60 | name string
61 | args args
62 | want decimal.Decimal
63 | }{
64 | {
65 | name: "success", args: args{
66 | rate: decimal.NewFromFloat(0.05 / 12),
67 | nper: 10 * 12,
68 | pmt: decimal.NewFromInt(-100),
69 | pv: decimal.NewFromInt(-100),
70 | when: paymentperiod.ENDING,
71 | },
72 | want: decimal.NewFromFloat(15692.928894335893),
73 | },
74 | }
75 | for _, tt := range tests {
76 | t.Run(tt.name, func(t *testing.T) {
77 | got := Fv(tt.args.rate, tt.args.nper, tt.args.pmt, tt.args.pv, tt.args.when)
78 | if err := isAlmostEqual(got, tt.want, decimal.NewFromFloat(precision)); err != nil {
79 | t.Errorf("error = %v, fv() = %v, want %v", err.Error(), got, tt.want)
80 | }
81 | })
82 | }
83 | }
84 |
85 | func Test_IPmt(t *testing.T) {
86 | type args struct {
87 | rate decimal.Decimal
88 | per int64
89 | nper int64
90 | pv decimal.Decimal
91 | fv decimal.Decimal
92 | when paymentperiod.Type
93 | }
94 | tests := []struct {
95 | name string
96 | args args
97 | want decimal.Decimal
98 | }{
99 | {
100 | name: "1period",
101 | args: args{
102 | rate: decimal.NewFromFloat(0.0824 / 12),
103 | per: 1,
104 | nper: 1 * 12,
105 | pv: decimal.NewFromInt(2500),
106 | fv: decimal.NewFromInt(0),
107 | when: paymentperiod.ENDING,
108 | },
109 | want: decimal.NewFromFloat(-17.166666666666668),
110 | },
111 | {
112 | name: "2period",
113 | args: args{
114 | rate: decimal.NewFromFloat(0.0824 / 12),
115 | per: 2,
116 | nper: 1 * 12,
117 | pv: decimal.NewFromInt(2500),
118 | fv: decimal.NewFromInt(0),
119 | when: paymentperiod.ENDING,
120 | },
121 | want: decimal.NewFromFloat(-15.7893374573507768960793587710732749),
122 | },
123 | {
124 | name: "3period",
125 | args: args{
126 | rate: decimal.NewFromFloat(0.0824 / 12),
127 | per: 3,
128 | nper: 1 * 12,
129 | pv: decimal.NewFromInt(2500),
130 | fv: decimal.NewFromInt(0),
131 | when: paymentperiod.ENDING,
132 | },
133 | want: decimal.NewFromFloat(-14.4025505874642504602108554951782324459586257024424875),
134 | },
135 | }
136 | for _, tt := range tests {
137 | t.Run(tt.name, func(t *testing.T) {
138 | got := IPmt(tt.args.rate, tt.args.per, tt.args.nper, tt.args.pv, tt.args.fv, tt.args.when)
139 | if err := isAlmostEqual(got, tt.want, decimal.NewFromFloat(precision)); err != nil {
140 | t.Errorf("error: %v, ipmt() = %v, want %v", err.Error(), got, tt.want)
141 | }
142 | })
143 | }
144 | }
145 |
146 | //
147 | func Test_PPmt(t *testing.T) {
148 | type args struct {
149 | rate decimal.Decimal
150 | per int64
151 | nper int64
152 | pv decimal.Decimal
153 | fv decimal.Decimal
154 | when paymentperiod.Type
155 | }
156 |
157 | tests := []struct {
158 | name string
159 | args args
160 | want decimal.Decimal
161 | }{
162 | {
163 | name: "period1",
164 | args: args{
165 | rate: decimal.NewFromFloat(0.0824 / 12),
166 | per: 1,
167 | nper: 1 * 12,
168 | pv: decimal.NewFromInt(2500),
169 | fv: decimal.NewFromInt(0),
170 | when: paymentperiod.ENDING,
171 | },
172 | want: decimal.NewFromFloat(-200.5819236867801753),
173 | },
174 | {
175 | name: "period2",
176 | args: args{
177 | rate: decimal.NewFromFloat(0.0824 / 12),
178 | per: 2,
179 | nper: 1 * 12,
180 | pv: decimal.NewFromInt(2500),
181 | fv: decimal.NewFromInt(0),
182 | when: paymentperiod.ENDING,
183 | },
184 | want: decimal.NewFromFloat(-201.9592528960960659039206412289267251),
185 | },
186 | {
187 | name: "period3",
188 | args: args{
189 | rate: decimal.NewFromFloat(0.0824 / 12),
190 | per: 3,
191 | nper: 1 * 12,
192 | pv: decimal.NewFromInt(2500),
193 | fv: decimal.NewFromInt(0),
194 | when: paymentperiod.ENDING,
195 | },
196 | want: decimal.NewFromFloat(-203.3460397659825923397891445048217675540413742975575125),
197 | },
198 | {
199 | name: "period4",
200 | args: args{
201 | rate: decimal.NewFromFloat(0.0824 / 12),
202 | per: 4,
203 | nper: 1 * 12,
204 | pv: decimal.NewFromInt(2500),
205 | fv: decimal.NewFromInt(0),
206 | when: paymentperiod.ENDING,
207 | },
208 | want: decimal.NewFromFloat(-204.7423492390423394738416027282628017795870286168635449048641975308641975),
209 | },
210 | {
211 | name: "period5",
212 | args: args{
213 | rate: decimal.NewFromFloat(0.0824 / 12),
214 | per: 5,
215 | nper: 1 * 12,
216 | pv: decimal.NewFromInt(2500),
217 | fv: decimal.NewFromInt(0),
218 | when: paymentperiod.ENDING,
219 | },
220 | want: decimal.NewFromFloat(-206.1482467038170969439558163159297469375176170621658196811415267489711932911213991769547325),
221 | },
222 | }
223 | for _, tt := range tests {
224 | t.Run(tt.name, func(t *testing.T) {
225 | got := PPmt(tt.args.rate, tt.args.per, tt.args.nper, tt.args.pv, tt.args.fv, tt.args.when)
226 | if err := isAlmostEqual(got, tt.want, decimal.NewFromFloat(precision)); err != nil {
227 | t.Errorf("error:%v, ppmt() = %v, want %v", err.Error(), got, tt.want)
228 | }
229 | })
230 | }
231 | }
232 |
233 | func Test_Pv(t *testing.T) {
234 | type args struct {
235 | rate decimal.Decimal
236 | nper int64
237 | pmt decimal.Decimal
238 | fv decimal.Decimal
239 | when paymentperiod.Type
240 | }
241 | tests := []struct {
242 | name string
243 | args args
244 | want decimal.Decimal
245 | }{
246 | {
247 | name: "success", args: args{
248 | rate: decimal.NewFromFloat(0.24 / 12),
249 | nper: 1 * 12,
250 | pmt: decimal.NewFromInt(-300),
251 | fv: decimal.NewFromInt(1000),
252 | when: paymentperiod.BEGINNING,
253 | },
254 | want: decimal.NewFromFloat(2447.561238019001),
255 | }, {
256 | name: "success", args: args{
257 | rate: decimal.NewFromFloat(0.24 / 12),
258 | nper: 1 * 12,
259 | pmt: decimal.NewFromInt(-300),
260 | fv: decimal.NewFromInt(1000),
261 | when: paymentperiod.ENDING,
262 | },
263 | want: decimal.NewFromFloat(2384.1091906934976),
264 | },
265 | }
266 | for _, tt := range tests {
267 | t.Run(tt.name, func(t *testing.T) {
268 | got := Pv(tt.args.rate, tt.args.nper, tt.args.pmt, tt.args.fv, tt.args.when)
269 | if err := isAlmostEqual(got, tt.want, decimal.NewFromFloat(precision)); err != nil {
270 | t.Errorf("error:%v, pv() = %v, want %v", err, got, tt.want)
271 | }
272 | })
273 | }
274 | }
275 |
276 | func Test_Npv(t *testing.T) {
277 | type args struct {
278 | rate decimal.Decimal
279 | values []decimal.Decimal
280 | }
281 | tests := []struct {
282 | name string
283 | args args
284 | want decimal.Decimal
285 | }{
286 | {
287 | name: "success", args: args{
288 | rate: decimal.NewFromFloat(0.2),
289 | values: []decimal.Decimal{decimal.NewFromInt(-1000), decimal.NewFromInt(100), decimal.NewFromInt(100), decimal.NewFromInt(100)},
290 | },
291 | want: decimal.NewFromFloat(-789.3518518518518),
292 | },
293 | }
294 | for _, tt := range tests {
295 | t.Run(tt.name, func(t *testing.T) {
296 | got := Npv(tt.args.rate, tt.args.values)
297 | if err := isAlmostEqual(got, tt.want, decimal.NewFromFloat(precision)); err != nil {
298 | t.Errorf("npv() = %v, want %v", got, tt.want)
299 | }
300 | })
301 | }
302 | }
303 |
304 | func Test_Nper(t *testing.T) {
305 | type args struct {
306 | rate decimal.Decimal
307 | fv decimal.Decimal
308 | pmt decimal.Decimal
309 | pv decimal.Decimal
310 | when paymentperiod.Type
311 | }
312 | tests := []struct {
313 | name string
314 | args args
315 | want decimal.Decimal
316 | wantErr bool
317 | err error
318 | }{
319 | {
320 | name: "success", args: args{
321 | rate: decimal.NewFromFloat(0.07 / 12),
322 | fv: decimal.NewFromInt(0),
323 | pmt: decimal.NewFromInt(-150),
324 | pv: decimal.NewFromInt(8000),
325 | when: paymentperiod.ENDING,
326 | },
327 | want: decimal.NewFromFloat(64.0733487706618586),
328 | wantErr: false,
329 | err: nil,
330 | },
331 | {
332 | name: "failure", args: args{
333 | rate: decimal.NewFromFloat(1e100),
334 | fv: decimal.NewFromInt(0),
335 | pmt: decimal.NewFromInt(-150),
336 | pv: decimal.NewFromInt(8000),
337 | when: paymentperiod.ENDING,
338 | },
339 | want: decimal.NewFromFloat(64.0733487706618586),
340 | wantErr: true,
341 | err: ErrOutOfBounds,
342 | },
343 | }
344 | for _, tt := range tests {
345 | t.Run(tt.name, func(t *testing.T) {
346 | if got, err := Nper(tt.args.rate, tt.args.pmt, tt.args.pv, tt.args.fv, tt.args.when); err != nil {
347 | if !tt.wantErr && errors.Is(err, tt.err) {
348 | t.Errorf("error is not equal, want=%v, got=%v", tt.err, err)
349 | }
350 | } else {
351 | if err := isAlmostEqual(got, tt.want, decimal.NewFromFloat(precision)); err != nil {
352 | t.Errorf("fv() = %v, want %v", got, tt.want)
353 | }
354 | }
355 | })
356 | }
357 | }
358 |
359 | func Test_Rate(t *testing.T) {
360 | type args struct {
361 | pv decimal.Decimal
362 | fv decimal.Decimal
363 | pmt decimal.Decimal
364 | nper int64
365 | when paymentperiod.Type
366 | maxIter int64
367 | tolerance decimal.Decimal
368 | initialGuess decimal.Decimal
369 | }
370 | tests := []struct {
371 | name string
372 | args args
373 | want decimal.Decimal
374 | anyErr error
375 | }{
376 | {
377 | name: "success", args: args{
378 | pv: decimal.NewFromInt(2000),
379 | fv: decimal.NewFromInt(-3000),
380 | pmt: decimal.NewFromInt(100),
381 | nper: 4,
382 | when: paymentperiod.BEGINNING,
383 | maxIter: 100,
384 | tolerance: decimal.NewFromFloat(1e-7),
385 | initialGuess: decimal.NewFromFloat(0.1),
386 | },
387 | want: decimal.NewFromFloat(0.06106257989825202),
388 | anyErr: nil,
389 | }, {
390 | name: "success", args: args{
391 | pv: decimal.NewFromInt(-3000),
392 | fv: decimal.NewFromInt(1000),
393 | pmt: decimal.NewFromInt(500),
394 | nper: 2,
395 | when: paymentperiod.BEGINNING,
396 | maxIter: 100,
397 | tolerance: decimal.NewFromFloat(1e-7),
398 | initialGuess: decimal.NewFromFloat(0.1),
399 | },
400 | want: decimal.NewFromFloat(-0.25968757625671507),
401 | anyErr: nil,
402 | }, {
403 | name: "failure", args: args{
404 | pv: decimal.NewFromInt(3000),
405 | fv: decimal.NewFromInt(1000),
406 | pmt: decimal.NewFromInt(100),
407 | nper: 2,
408 | when: paymentperiod.BEGINNING,
409 | maxIter: 100,
410 | tolerance: decimal.NewFromFloat(1e-7),
411 | initialGuess: decimal.NewFromFloat(0.1),
412 | },
413 | want: decimal.Zero,
414 | anyErr: ErrTolerence,
415 | },
416 | }
417 | for _, tt := range tests {
418 | t.Run(tt.name, func(t *testing.T) {
419 | if got, err := Rate(tt.args.pv, tt.args.fv, tt.args.pmt, tt.args.nper, tt.args.when, tt.args.maxIter, tt.args.tolerance, tt.args.initialGuess); err != tt.anyErr || isAlmostEqual(got, tt.want, decimal.NewFromFloat(precision)) != nil {
420 | t.Errorf("Rate returned (%v,%v), wanted (%v,%v)", got, err, tt.want, tt.anyErr)
421 | }
422 | })
423 | }
424 | }
425 |
--------------------------------------------------------------------------------
/reducing_utils.go:
--------------------------------------------------------------------------------
1 | /*
2 | This package is a go native port of the numpy-financial package with some additional helper
3 | functions.
4 |
5 | The functions in this package are a scalar version of their vectorised counterparts in
6 | the numpy-financial(https://github.com/numpy/numpy-financial) library.
7 |
8 | Currently, only some functions are ported, the remaining will be ported soon.
9 | */
10 | package gofinancial
11 |
12 | import (
13 | "fmt"
14 | "math"
15 |
16 | "github.com/razorpay/go-financial/enums/paymentperiod"
17 | "github.com/shopspring/decimal"
18 | )
19 |
20 | /*
21 | Pmt compute the fixed payment(principal + interest) against a loan amount ( fv = 0).
22 |
23 | It can also be used to calculate the recurring payments needed to achieve a certain future value
24 | given an initial deposit, a fixed periodically compounded interest rate, and the total number of periods.
25 |
26 | It is obtained by solving the following equation:
27 |
28 | fv + pv*(1 + rate)**nper + pmt*(1 + rate*when)/rate*((1 + rate)**nper - 1) == 0
29 |
30 | Params:
31 | rate : rate of interest compounded once per period
32 | nper : total number of periods to be compounded for
33 | pv : present value (e.g., an amount borrowed)
34 | fv : future value (e.g., 0)
35 | when : specification of whether payment is made
36 | at the beginning (when = 1) or the end
37 | (when = 0) of each period
38 |
39 | References:
40 | [WRW] Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May).
41 | Open Document Format for Office Applications (OpenDocument)v1.2,
42 | Part 2: Recalculated Formula (OpenFormula) Format - Annotated Version,
43 | Pre-Draft 12. Organization for the Advancement of Structured Information
44 | Standards (OASIS). Billerica, MA, USA. [ODT Document].
45 | Available:
46 | http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formula
47 | OpenDocument-formula-20090508.odt
48 | */
49 | func Pmt(rate decimal.Decimal, nper int64, pv decimal.Decimal, fv decimal.Decimal, when paymentperiod.Type) decimal.Decimal {
50 | one := decimal.NewFromInt(1)
51 | minusOne := decimal.NewFromInt(-1)
52 | dWhen := decimal.NewFromInt(when.Value())
53 | dNper := decimal.NewFromInt(nper)
54 | dRateWithWhen := rate.Mul(dWhen)
55 |
56 | factor := one.Add(rate).Pow(dNper)
57 | var secondFactor decimal.Decimal
58 | if rate.Equal(decimal.Zero) {
59 | secondFactor = dNper
60 | } else {
61 | secondFactor = factor.Sub(one).Mul(one.Add(dRateWithWhen)).Div(rate)
62 | }
63 | return pv.Mul(factor).Add(fv).Div(secondFactor).Mul(minusOne)
64 | }
65 |
66 | /*
67 | IPmt computes interest payment for a loan under a given period.
68 |
69 | Params:
70 |
71 | rate : rate of interest compounded once per period
72 | per : period under consideration
73 | nper : total number of periods to be compounded for
74 | pv : present value (e.g., an amount borrowed)
75 | fv : future value (e.g., 0)
76 | when : specification of whether payment is made
77 | at the beginning (when = 1) or the end
78 | (when = 0) of each period
79 |
80 | References:
81 | [WRW] Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May).
82 | Open Document Format for Office Applications (OpenDocument)v1.2,
83 | Part 2: Recalculated Formula (OpenFormula) Format - Annotated Version,
84 | Pre-Draft 12. Organization for the Advancement of Structured Information
85 | Standards (OASIS). Billerica, MA, USA. [ODT Document].
86 | Available:
87 | http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formula
88 | OpenDocument-formula-20090508.odt
89 | */
90 | func IPmt(rate decimal.Decimal, per int64, nper int64, pv decimal.Decimal, fv decimal.Decimal, when paymentperiod.Type) decimal.Decimal {
91 | totalPmt := Pmt(rate, nper, pv, fv, when)
92 | one := decimal.NewFromInt(1)
93 | ipmt := rbl(rate, per, totalPmt, pv, when).Mul(rate)
94 | if when == paymentperiod.BEGINNING {
95 | if per == 1 {
96 | return decimal.Zero
97 | } else {
98 | // paying at the beginning, so discount it.
99 | return ipmt.Div(one.Add(rate))
100 | }
101 | } else {
102 | return ipmt
103 | }
104 | }
105 |
106 | /*
107 | PPmt computes principal payment for a loan under a given period.
108 |
109 | Params:
110 |
111 | rate : rate of interest compounded once per period
112 | per : period under consideration
113 | nper : total number of periods to be compounded for
114 | pv : present value (e.g., an amount borrowed)
115 | fv : future value (e.g., 0)
116 | when : specification of whether payment is made
117 | at the beginning (when = 1) or the end
118 | (when = 0) of each period
119 |
120 | References:
121 | [WRW] Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May).
122 | Open Document Format for Office Applications (OpenDocument)v1.2,
123 | Part 2: Recalculated Formula (OpenFormula) Format - Annotated Version,
124 | Pre-Draft 12. Organization for the Advancement of Structured Information
125 | Standards (OASIS). Billerica, MA, USA. [ODT Document].
126 | Available:
127 | http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formula
128 | OpenDocument-formula-20090508.odt
129 | */
130 | func PPmt(rate decimal.Decimal, per int64, nper int64, pv decimal.Decimal, fv decimal.Decimal, when paymentperiod.Type) decimal.Decimal {
131 | total := Pmt(rate, nper, pv, fv, when)
132 | ipmt := IPmt(rate, per, nper, pv, fv, when)
133 | return total.Sub(ipmt)
134 | }
135 |
136 | // Rbl computes remaining balance
137 | func rbl(rate decimal.Decimal, per int64, pmt decimal.Decimal, pv decimal.Decimal, when paymentperiod.Type) decimal.Decimal {
138 | return Fv(rate, per-1, pmt, pv, when)
139 | }
140 |
141 | /*
142 | Nper computes the number of periodic payments by solving the equation:
143 |
144 | fv +
145 | pv*(1 + rate)**nper +
146 | pmt*(1 + rate*when)/rate*((1 + rate)**nper - 1) = 0
147 |
148 |
149 | Params:
150 |
151 | rate : an interest rate compounded once per period
152 | pmt : a (fixed) payment, paid either
153 | at the beginning (when = 1) or the end (when = 0) of each period
154 | pv : a present value
155 | when : specification of whether payment is made
156 | at the beginning (when = 1) or the end
157 | (when = 0) of each period
158 | fv: a future value
159 | when : specification of whether payment is made
160 | at the beginning (when = 1) or the end
161 | (when = 0) of each period
162 |
163 | */
164 | func Nper(rate decimal.Decimal, pmt decimal.Decimal, pv decimal.Decimal, fv decimal.Decimal, when paymentperiod.Type) (result decimal.Decimal, err error) {
165 | defer func() {
166 | if r := recover(); r != nil {
167 | result = decimal.Zero
168 | err = fmt.Errorf("%w: %v", ErrOutOfBounds, r)
169 | }
170 | }()
171 | one := decimal.NewFromInt(1)
172 | minusOne := decimal.NewFromInt(-1)
173 | dWhen := decimal.NewFromInt(when.Value())
174 | dRateWithWhen := rate.Mul(dWhen)
175 | z := pmt.Mul(one.Add(dRateWithWhen)).Div(rate)
176 | numerator := minusOne.Mul(fv).Add(z).Div(pv.Add(z))
177 | denominator := one.Add(rate)
178 | floatNumerator, _ := numerator.BigFloat().Float64()
179 | floatDenominator, _ := denominator.BigFloat().Float64()
180 | logNumerator := math.Log(floatNumerator)
181 | logDenominator := math.Log(floatDenominator)
182 | dlogDenominator := decimal.NewFromFloat(logDenominator)
183 | result = decimal.NewFromFloat(logNumerator).Div(dlogDenominator)
184 | return result, nil
185 | }
186 |
187 | /*
188 | Fv computes future value at the end of some periods(nper) by solving the following equation:
189 |
190 | fv +
191 | pv*(1+rate)**nper +
192 | pmt*(1 + rate*when)/rate*((1 + rate)**nper - 1) == 0
193 |
194 |
195 | Params:
196 |
197 | pv : a present value
198 | rate : an interest rate compounded once per period
199 | nper : total number of periods
200 | pmt : a (fixed) payment, paid either
201 | at the beginning (when = 1) or the end (when = 0) of each period
202 | when : specification of whether payment is made
203 | at the beginning (when = 1) or the end
204 | (when = 0) of each period
205 |
206 | References:
207 | [WRW] Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May).
208 | Open Document Format for Office Applications (OpenDocument)v1.2,
209 | Part 2: Recalculated Formula (OpenFormula) Format - Annotated Version,
210 | Pre-Draft 12. Organization for the Advancement of Structured Information
211 | Standards (OASIS). Billerica, MA, USA. [ODT Document].
212 | Available:
213 | http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formula
214 | OpenDocument-formula-20090508.odt
215 | */
216 | func Fv(rate decimal.Decimal, nper int64, pmt decimal.Decimal, pv decimal.Decimal, when paymentperiod.Type) decimal.Decimal {
217 | one := decimal.NewFromInt(1)
218 | minusOne := decimal.NewFromInt(-1)
219 | dWhen := decimal.NewFromInt(when.Value())
220 | dRateWithWhen := rate.Mul(dWhen)
221 | dNper := decimal.NewFromInt(nper)
222 |
223 | factor := one.Add(rate).Pow(dNper)
224 | secondFactor := factor.Sub(one).Mul(one.Add(dRateWithWhen)).Div(rate)
225 |
226 | return pv.Mul(factor).Add(pmt.Mul(secondFactor)).Mul(minusOne)
227 | }
228 |
229 | /*
230 | Pv computes present value by solving the following equation:
231 |
232 | fv +
233 | pv*(1+rate)**nper +
234 | pmt*(1 + rate*when)/rate*((1 + rate)**nper - 1) == 0
235 |
236 |
237 | Params:
238 |
239 | fv : a future value
240 | rate : an interest rate compounded once per period
241 | nper : total number of periods
242 | pmt : a (fixed) payment, paid either
243 | at the beginning (when = 1) or the end (when = 0) of each period
244 | when : specification of whether payment is made
245 | at the beginning (when = 1) or the end
246 | (when = 0) of each period
247 |
248 | References:
249 | [WRW] Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May).
250 | Open Document Format for Office Applications (OpenDocument)v1.2,
251 | Part 2: Recalculated Formula (OpenFormula) Format - Annotated Version,
252 | Pre-Draft 12. Organization for the Advancement of Structured Information
253 | Standards (OASIS). Billerica, MA, USA. [ODT Document].
254 | Available:
255 | http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formula
256 | OpenDocument-formula-20090508.odt
257 | */
258 | func Pv(rate decimal.Decimal, nper int64, pmt decimal.Decimal, fv decimal.Decimal, when paymentperiod.Type) decimal.Decimal {
259 | one := decimal.NewFromInt(1)
260 | minusOne := decimal.NewFromInt(-1)
261 | dWhen := decimal.NewFromInt(when.Value())
262 | dNper := decimal.NewFromInt(nper)
263 | dRateWithWhen := rate.Mul(dWhen)
264 |
265 | factor := one.Add(rate).Pow(dNper)
266 | secondFactor := factor.Sub(one).Mul(one.Add(dRateWithWhen)).Div(rate)
267 |
268 | return fv.Add(pmt.Mul(secondFactor)).Div(factor).Mul(minusOne)
269 | }
270 |
271 | /*
272 | Npv computes the Net Present Value of a cash flow series
273 |
274 | Params:
275 |
276 | rate : a discount rate applied once per period
277 | values : the value of the cash flow for that time period. Values provided here must be an array of float64
278 |
279 | References:
280 | L. J. Gitman, “Principles of Managerial Finance, Brief,” 3rd ed., Addison-Wesley, 2003, pg. 346.
281 |
282 | */
283 | func Npv(rate decimal.Decimal, values []decimal.Decimal) decimal.Decimal {
284 | internalNpv := decimal.NewFromFloat(0.0)
285 | currentRateT := decimal.NewFromFloat(1.0)
286 | one := decimal.NewFromInt(1)
287 | for _, currentVal := range values {
288 | internalNpv = internalNpv.Add(currentVal.Div(currentRateT))
289 | currentRateT = currentRateT.Mul(one.Add(rate))
290 | }
291 | return internalNpv
292 | }
293 |
294 | /*
295 | This function computes the ratio that is used to find a single value that sets the non-liner equation to zero
296 |
297 | Params:
298 |
299 | nper : number of compounding periods
300 | pmt : a (fixed) payment, paid either
301 | at the beginning (when = 1) or the end (when = 0) of each period
302 | pv : a present value
303 | fv : a future value
304 | when : specification of whether payment is made
305 | at the beginning (when = 1) or the end (when = 0) of each period
306 | curRate: the rate compounded once per period rate
307 | */
308 | func getRateRatio(pv, fv, pmt, curRate decimal.Decimal, nper int64, when paymentperiod.Type) decimal.Decimal {
309 | oneInDecimal := decimal.NewFromInt(1)
310 | whenInDecimal := decimal.NewFromInt(when.Value())
311 | nperInDecimal := decimal.NewFromInt(nper)
312 |
313 | f0 := curRate.Add(oneInDecimal).Pow(decimal.NewFromInt(nper)) // f0 := math.Pow((1 + curRate), float64(nper))
314 | f1 := f0.Div(curRate.Add(oneInDecimal)) // f1 := f0 / (1 + curRate)
315 |
316 | yP0 := pv.Mul(f0)
317 | yP1 := pmt.Mul(oneInDecimal.Add(curRate.Mul(whenInDecimal))).Mul(f0.Sub(oneInDecimal)).Div(curRate)
318 | y := fv.Add(yP0).Add(yP1) // y := fv + pv*f0 + pmt*(1.0+curRate*when.Value())*(f0-1)/curRate
319 |
320 | derivativeP0 := nperInDecimal.Mul(f1).Mul(pv)
321 | derivativeP1 := pmt.Mul(whenInDecimal).Mul(f0.Sub(oneInDecimal)).Div(curRate)
322 | derivativeP2s0 := oneInDecimal.Add(curRate.Mul(whenInDecimal))
323 | derivativeP2s1 := ((curRate.Mul((nperInDecimal)).Mul(f1)).Sub(f0).Add(oneInDecimal)).Div(curRate.Mul(curRate))
324 | derivativeP2 := derivativeP2s0.Mul(derivativeP2s1)
325 | derivative := derivativeP0.Add(derivativeP1).Add(derivativeP2)
326 | // derivative := (float64(nper) * f1 * pv) + (pmt * ((when.Value() * (f0 - 1) / curRate) + ((1.0 + curRate*when.Value()) * ((curRate*float64(nper)*f1 - f0 + 1) / (curRate * curRate)))))
327 |
328 | return y.Div(derivative)
329 | }
330 |
331 | /*
332 | Rate computes the Interest rate per period by running Newton Rapson to find an approximate value for:
333 | y = fv + pv*(1+rate)**nper + pmt*(1+rate*when)/rate*((1+rate)**nper-1)*(0 - y_previous) /(rate - rate_previous) = dy/drate {derivative of y w.r.t. rate}
334 |
335 | Params:
336 | nper : number of compounding periods
337 | pmt : a (fixed) payment, paid either
338 | at the beginning (when = 1) or the end (when = 0) of each period
339 | pv : a present value
340 | fv : a future value
341 | when : specification of whether payment is made
342 | at the beginning (when = 1) or the end (when = 0) of each period
343 | maxIter : total number of iterations to perform calculation
344 | tolerance : accept result only if the difference in iteration values is less than the tolerance provided
345 | initialGuess : an initial point to start approximating from
346 |
347 | References:
348 | [WRW] Wheeler, D. A., E. Rathke, and R. Weir (Eds.) (2009, May).
349 | Open Document Format for Office Applications (OpenDocument)v1.2,
350 | Part 2: Recalculated Formula (OpenFormula) Format - Annotated Version,
351 | Pre-Draft 12. Organization for the Advancement of Structured Information
352 | Standards (OASIS). Billerica, MA, USA. [ODT Document].
353 | Available:
354 | http://www.oasis-open.org/committees/documents.php?wg_abbrev=office-formula
355 | OpenDocument-formula-20090508.odt
356 | */
357 | func Rate(pv, fv, pmt decimal.Decimal, nper int64, when paymentperiod.Type, maxIter int64, tolerance, initialGuess decimal.Decimal) (decimal.Decimal, error) {
358 | var nextIterRate, currentIterRate decimal.Decimal = initialGuess, initialGuess
359 |
360 | for iter := int64(0); iter < maxIter; iter++ {
361 | currentIterRate = nextIterRate
362 | nextIterRate = currentIterRate.Sub(getRateRatio(pv, fv, pmt, currentIterRate, nper, when))
363 | // skip further loops if |nextIterRate-currentIterRate| < tolerance
364 | if nextIterRate.Sub(currentIterRate).Abs().LessThan(tolerance) {
365 | break
366 | }
367 | }
368 |
369 | if nextIterRate.Sub(currentIterRate).Abs().GreaterThanOrEqual(tolerance) {
370 | return decimal.Zero, ErrTolerence
371 | }
372 | return nextIterRate, nil
373 | }
374 |
--------------------------------------------------------------------------------
/config_test.go:
--------------------------------------------------------------------------------
1 | package gofinancial
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 |
8 | Frequency "github.com/razorpay/go-financial/enums/frequency"
9 | )
10 |
11 | type dateGroup struct {
12 | startDate time.Time
13 | endDate time.Time
14 | }
15 |
16 | func TestConfig_SetPeriodsAndDates(t *testing.T) {
17 | type fields struct {
18 | StartDate time.Time
19 | EndDate time.Time
20 | Frequency Frequency.Type
21 | }
22 | tests := []struct {
23 | name string
24 | fields fields
25 | wantErr bool
26 | wantPeriods int64
27 | wantDates []dateGroup
28 | }{
29 | {
30 | name: "daily same year", fields: fields{getDate(2020, 1, 1), getDate(2020, 1, 31), Frequency.DAILY}, wantErr: false,
31 | wantPeriods: 31, wantDates: []dateGroup{
32 | {timeParseUtil(t, "2020-01-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-01 23:59:59 +0000 UTC")},
33 | {timeParseUtil(t, "2020-01-02 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-02 23:59:59 +0000 UTC")},
34 | {timeParseUtil(t, "2020-01-03 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-03 23:59:59 +0000 UTC")},
35 | {timeParseUtil(t, "2020-01-04 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-04 23:59:59 +0000 UTC")},
36 | {timeParseUtil(t, "2020-01-05 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-05 23:59:59 +0000 UTC")},
37 | {timeParseUtil(t, "2020-01-06 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-06 23:59:59 +0000 UTC")},
38 | {timeParseUtil(t, "2020-01-07 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-07 23:59:59 +0000 UTC")},
39 | {timeParseUtil(t, "2020-01-08 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-08 23:59:59 +0000 UTC")},
40 | {timeParseUtil(t, "2020-01-09 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-09 23:59:59 +0000 UTC")},
41 | {timeParseUtil(t, "2020-01-10 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-10 23:59:59 +0000 UTC")},
42 | {timeParseUtil(t, "2020-01-11 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-11 23:59:59 +0000 UTC")},
43 | {timeParseUtil(t, "2020-01-12 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-12 23:59:59 +0000 UTC")},
44 | {timeParseUtil(t, "2020-01-13 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-13 23:59:59 +0000 UTC")},
45 | {timeParseUtil(t, "2020-01-14 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-14 23:59:59 +0000 UTC")},
46 | {timeParseUtil(t, "2020-01-15 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-15 23:59:59 +0000 UTC")},
47 | {timeParseUtil(t, "2020-01-16 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-16 23:59:59 +0000 UTC")},
48 | {timeParseUtil(t, "2020-01-17 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-17 23:59:59 +0000 UTC")},
49 | {timeParseUtil(t, "2020-01-18 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-18 23:59:59 +0000 UTC")},
50 | {timeParseUtil(t, "2020-01-19 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-19 23:59:59 +0000 UTC")},
51 | {timeParseUtil(t, "2020-01-20 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-20 23:59:59 +0000 UTC")},
52 | {timeParseUtil(t, "2020-01-21 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-21 23:59:59 +0000 UTC")},
53 | {timeParseUtil(t, "2020-01-22 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-22 23:59:59 +0000 UTC")},
54 | {timeParseUtil(t, "2020-01-23 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-23 23:59:59 +0000 UTC")},
55 | {timeParseUtil(t, "2020-01-24 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-24 23:59:59 +0000 UTC")},
56 | {timeParseUtil(t, "2020-01-25 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-25 23:59:59 +0000 UTC")},
57 | {timeParseUtil(t, "2020-01-26 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-26 23:59:59 +0000 UTC")},
58 | {timeParseUtil(t, "2020-01-27 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-27 23:59:59 +0000 UTC")},
59 | {timeParseUtil(t, "2020-01-28 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-28 23:59:59 +0000 UTC")},
60 | {timeParseUtil(t, "2020-01-29 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-29 23:59:59 +0000 UTC")},
61 | {timeParseUtil(t, "2020-01-30 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-30 23:59:59 +0000 UTC")},
62 | {timeParseUtil(t, "2020-01-31 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-31 23:59:59 +0000 UTC")},
63 | },
64 | },
65 | {
66 | name: "weekly same year", fields: fields{getDate(2020, 1, 1), getDate(2020, 4, 14), Frequency.WEEKLY}, wantErr: false,
67 | wantPeriods: 15, wantDates: []dateGroup{
68 | {timeParseUtil(t, "2020-01-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-07 23:59:59 +0000 UTC")},
69 | {timeParseUtil(t, "2020-01-08 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-14 23:59:59 +0000 UTC")},
70 | {timeParseUtil(t, "2020-01-15 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-21 23:59:59 +0000 UTC")},
71 | {timeParseUtil(t, "2020-01-22 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-28 23:59:59 +0000 UTC")},
72 | {timeParseUtil(t, "2020-01-29 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-02-04 23:59:59 +0000 UTC")},
73 | {timeParseUtil(t, "2020-02-05 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-02-11 23:59:59 +0000 UTC")},
74 | {timeParseUtil(t, "2020-02-12 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-02-18 23:59:59 +0000 UTC")},
75 | {timeParseUtil(t, "2020-02-19 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-02-25 23:59:59 +0000 UTC")},
76 | {timeParseUtil(t, "2020-02-26 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-03-03 23:59:59 +0000 UTC")},
77 | {timeParseUtil(t, "2020-03-04 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-03-10 23:59:59 +0000 UTC")},
78 | {timeParseUtil(t, "2020-03-11 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-03-17 23:59:59 +0000 UTC")},
79 | {timeParseUtil(t, "2020-03-18 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-03-24 23:59:59 +0000 UTC")},
80 | {timeParseUtil(t, "2020-03-25 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-03-31 23:59:59 +0000 UTC")},
81 | {timeParseUtil(t, "2020-04-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-04-07 23:59:59 +0000 UTC")},
82 | {timeParseUtil(t, "2020-04-08 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-04-14 23:59:59 +0000 UTC")},
83 | },
84 | },
85 | {
86 | name: "weekly different year", fields: fields{getDate(2020, 1, 1), getDate(2021, 2, 23), Frequency.WEEKLY}, wantErr: false,
87 | wantPeriods: 60, wantDates: []dateGroup{
88 | {timeParseUtil(t, "2020-01-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-07 23:59:59 +0000 UTC")},
89 | {timeParseUtil(t, "2020-01-08 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-14 23:59:59 +0000 UTC")},
90 | {timeParseUtil(t, "2020-01-15 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-21 23:59:59 +0000 UTC")},
91 | {timeParseUtil(t, "2020-01-22 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-28 23:59:59 +0000 UTC")},
92 | {timeParseUtil(t, "2020-01-29 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-02-04 23:59:59 +0000 UTC")},
93 | {timeParseUtil(t, "2020-02-05 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-02-11 23:59:59 +0000 UTC")},
94 | {timeParseUtil(t, "2020-02-12 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-02-18 23:59:59 +0000 UTC")},
95 | {timeParseUtil(t, "2020-02-19 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-02-25 23:59:59 +0000 UTC")},
96 | {timeParseUtil(t, "2020-02-26 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-03-03 23:59:59 +0000 UTC")},
97 | {timeParseUtil(t, "2020-03-04 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-03-10 23:59:59 +0000 UTC")},
98 | {timeParseUtil(t, "2020-03-11 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-03-17 23:59:59 +0000 UTC")},
99 | {timeParseUtil(t, "2020-03-18 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-03-24 23:59:59 +0000 UTC")},
100 | {timeParseUtil(t, "2020-03-25 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-03-31 23:59:59 +0000 UTC")},
101 | {timeParseUtil(t, "2020-04-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-04-07 23:59:59 +0000 UTC")},
102 | {timeParseUtil(t, "2020-04-08 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-04-14 23:59:59 +0000 UTC")},
103 | {timeParseUtil(t, "2020-04-15 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-04-21 23:59:59 +0000 UTC")},
104 | {timeParseUtil(t, "2020-04-22 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-04-28 23:59:59 +0000 UTC")},
105 | {timeParseUtil(t, "2020-04-29 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-05-05 23:59:59 +0000 UTC")},
106 | {timeParseUtil(t, "2020-05-06 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-05-12 23:59:59 +0000 UTC")},
107 | {timeParseUtil(t, "2020-05-13 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-05-19 23:59:59 +0000 UTC")},
108 | {timeParseUtil(t, "2020-05-20 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-05-26 23:59:59 +0000 UTC")},
109 | {timeParseUtil(t, "2020-05-27 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-06-02 23:59:59 +0000 UTC")},
110 | {timeParseUtil(t, "2020-06-03 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-06-09 23:59:59 +0000 UTC")},
111 | {timeParseUtil(t, "2020-06-10 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-06-16 23:59:59 +0000 UTC")},
112 | {timeParseUtil(t, "2020-06-17 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-06-23 23:59:59 +0000 UTC")},
113 | {timeParseUtil(t, "2020-06-24 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-06-30 23:59:59 +0000 UTC")},
114 | {timeParseUtil(t, "2020-07-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-07-07 23:59:59 +0000 UTC")},
115 | {timeParseUtil(t, "2020-07-08 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-07-14 23:59:59 +0000 UTC")},
116 | {timeParseUtil(t, "2020-07-15 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-07-21 23:59:59 +0000 UTC")},
117 | {timeParseUtil(t, "2020-07-22 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-07-28 23:59:59 +0000 UTC")},
118 | {timeParseUtil(t, "2020-07-29 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-08-04 23:59:59 +0000 UTC")},
119 | {timeParseUtil(t, "2020-08-05 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-08-11 23:59:59 +0000 UTC")},
120 | {timeParseUtil(t, "2020-08-12 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-08-18 23:59:59 +0000 UTC")},
121 | {timeParseUtil(t, "2020-08-19 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-08-25 23:59:59 +0000 UTC")},
122 | {timeParseUtil(t, "2020-08-26 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-09-01 23:59:59 +0000 UTC")},
123 | {timeParseUtil(t, "2020-09-02 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-09-08 23:59:59 +0000 UTC")},
124 | {timeParseUtil(t, "2020-09-09 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-09-15 23:59:59 +0000 UTC")},
125 | {timeParseUtil(t, "2020-09-16 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-09-22 23:59:59 +0000 UTC")},
126 | {timeParseUtil(t, "2020-09-23 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-09-29 23:59:59 +0000 UTC")},
127 | {timeParseUtil(t, "2020-09-30 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-10-06 23:59:59 +0000 UTC")},
128 | {timeParseUtil(t, "2020-10-07 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-10-13 23:59:59 +0000 UTC")},
129 | {timeParseUtil(t, "2020-10-14 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-10-20 23:59:59 +0000 UTC")},
130 | {timeParseUtil(t, "2020-10-21 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-10-27 23:59:59 +0000 UTC")},
131 | {timeParseUtil(t, "2020-10-28 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-11-03 23:59:59 +0000 UTC")},
132 | {timeParseUtil(t, "2020-11-04 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-11-10 23:59:59 +0000 UTC")},
133 | {timeParseUtil(t, "2020-11-11 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-11-17 23:59:59 +0000 UTC")},
134 | {timeParseUtil(t, "2020-11-18 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-11-24 23:59:59 +0000 UTC")},
135 | {timeParseUtil(t, "2020-11-25 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-12-01 23:59:59 +0000 UTC")},
136 | {timeParseUtil(t, "2020-12-02 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-12-08 23:59:59 +0000 UTC")},
137 | {timeParseUtil(t, "2020-12-09 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-12-15 23:59:59 +0000 UTC")},
138 | {timeParseUtil(t, "2020-12-16 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-12-22 23:59:59 +0000 UTC")},
139 | {timeParseUtil(t, "2020-12-23 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-12-29 23:59:59 +0000 UTC")},
140 | {timeParseUtil(t, "2020-12-30 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-01-05 23:59:59 +0000 UTC")},
141 | {timeParseUtil(t, "2021-01-06 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-01-12 23:59:59 +0000 UTC")},
142 | {timeParseUtil(t, "2021-01-13 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-01-19 23:59:59 +0000 UTC")},
143 | {timeParseUtil(t, "2021-01-20 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-01-26 23:59:59 +0000 UTC")},
144 | {timeParseUtil(t, "2021-01-27 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-02-02 23:59:59 +0000 UTC")},
145 | {timeParseUtil(t, "2021-02-03 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-02-09 23:59:59 +0000 UTC")},
146 | {timeParseUtil(t, "2021-02-10 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-02-16 23:59:59 +0000 UTC")},
147 | {timeParseUtil(t, "2021-02-17 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-02-23 23:59:59 +0000 UTC")},
148 | },
149 | },
150 |
151 | {
152 | name: "monthly same year", fields: fields{getDate(2020, 1, 1), getDate(2020, 9, 30), Frequency.MONTHLY}, wantErr: false,
153 | wantPeriods: 9, wantDates: []dateGroup{
154 | {timeParseUtil(t, "2020-01-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-31 23:59:59 +0000 UTC")},
155 | {timeParseUtil(t, "2020-02-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-02-29 23:59:59 +0000 UTC")},
156 | {timeParseUtil(t, "2020-03-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-03-31 23:59:59 +0000 UTC")},
157 | {timeParseUtil(t, "2020-04-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-04-30 23:59:59 +0000 UTC")},
158 | {timeParseUtil(t, "2020-05-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-05-31 23:59:59 +0000 UTC")},
159 | {timeParseUtil(t, "2020-06-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-06-30 23:59:59 +0000 UTC")},
160 | {timeParseUtil(t, "2020-07-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-07-31 23:59:59 +0000 UTC")},
161 | {timeParseUtil(t, "2020-08-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-08-31 23:59:59 +0000 UTC")},
162 | {timeParseUtil(t, "2020-09-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-09-30 23:59:59 +0000 UTC")},
163 | },
164 | },
165 | {
166 | name: "monthly different year", fields: fields{getDate(2020, 1, 1), getDate(2021, 10, 31), Frequency.MONTHLY}, wantErr: false,
167 | wantPeriods: 22, wantDates: []dateGroup{
168 | {timeParseUtil(t, "2020-01-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-01-31 23:59:59 +0000 UTC")},
169 | {timeParseUtil(t, "2020-02-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-02-29 23:59:59 +0000 UTC")},
170 | {timeParseUtil(t, "2020-03-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-03-31 23:59:59 +0000 UTC")},
171 | {timeParseUtil(t, "2020-04-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-04-30 23:59:59 +0000 UTC")},
172 | {timeParseUtil(t, "2020-05-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-05-31 23:59:59 +0000 UTC")},
173 | {timeParseUtil(t, "2020-06-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-06-30 23:59:59 +0000 UTC")},
174 | {timeParseUtil(t, "2020-07-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-07-31 23:59:59 +0000 UTC")},
175 | {timeParseUtil(t, "2020-08-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-08-31 23:59:59 +0000 UTC")},
176 | {timeParseUtil(t, "2020-09-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-09-30 23:59:59 +0000 UTC")},
177 | {timeParseUtil(t, "2020-10-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-10-31 23:59:59 +0000 UTC")},
178 | {timeParseUtil(t, "2020-11-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-11-30 23:59:59 +0000 UTC")},
179 | {timeParseUtil(t, "2020-12-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2020-12-31 23:59:59 +0000 UTC")},
180 | {timeParseUtil(t, "2021-01-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-01-31 23:59:59 +0000 UTC")},
181 | {timeParseUtil(t, "2021-02-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-02-28 23:59:59 +0000 UTC")},
182 | {timeParseUtil(t, "2021-03-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-03-31 23:59:59 +0000 UTC")},
183 | {timeParseUtil(t, "2021-04-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-04-30 23:59:59 +0000 UTC")},
184 | {timeParseUtil(t, "2021-05-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-05-31 23:59:59 +0000 UTC")},
185 | {timeParseUtil(t, "2021-06-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-06-30 23:59:59 +0000 UTC")},
186 | {timeParseUtil(t, "2021-07-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-07-31 23:59:59 +0000 UTC")},
187 | {timeParseUtil(t, "2021-08-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-08-31 23:59:59 +0000 UTC")},
188 | {timeParseUtil(t, "2021-09-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-09-30 23:59:59 +0000 UTC")},
189 | {timeParseUtil(t, "2021-10-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-10-31 23:59:59 +0000 UTC")},
190 | },
191 | },
192 | {
193 | name: "annually", fields: fields{getDate(2020, 5, 1), getDate(2022, 4, 30), Frequency.ANNUALLY}, wantErr: false,
194 | wantPeriods: 2, wantDates: []dateGroup{
195 | {timeParseUtil(t, "2020-05-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2021-04-30 23:59:59 +0000 UTC")},
196 | {timeParseUtil(t, "2021-05-01 00:00:00 +0000 UTC"), timeParseUtil(t, "2022-04-30 23:59:59 +0000 UTC")},
197 | },
198 | },
199 | }
200 | for _, tt := range tests {
201 | t.Run(tt.name, func(t *testing.T) {
202 | c := &Config{
203 | StartDate: tt.fields.StartDate,
204 | EndDate: tt.fields.EndDate,
205 | Frequency: tt.fields.Frequency,
206 | }
207 | if err := c.setPeriodsAndDates(); (err != nil) != tt.wantErr {
208 | t.Fatalf("SetPeriodsAndDates() error = %v, wantErr %v", err, tt.wantErr)
209 | }
210 | if c.periods != tt.wantPeriods {
211 | t.Fatalf("want periods: %v, got periods:%v", tt.wantPeriods, c.periods)
212 | }
213 | if err := areDatesEqual(c.startDates, c.endDates, tt.wantDates); err != nil {
214 | t.Fatalf("dates are not equal. error:%v", err)
215 | }
216 | })
217 | }
218 | }
219 |
220 | func areDatesEqual(actualStartDates []time.Time, actualEndDates []time.Time, expected []dateGroup) error {
221 | for idx := range expected {
222 | if !actualStartDates[idx].Equal(expected[idx].startDate) || !actualEndDates[idx].Equal(expected[idx].endDate) {
223 | return fmt.Errorf("expected startDate:%v, endDate:%v, got startDate:%v endDate:%v", expected[idx].startDate, expected[idx].endDate, actualStartDates[idx], actualEndDates[idx])
224 | }
225 | }
226 | return nil
227 | }
228 |
229 | func getDate(y int, m int, d int) time.Time {
230 | return time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.UTC)
231 | }
232 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # go-financial
3 |
4 | This package is a go native port of the numpy-financial package with some additional helper
5 | functions.
6 |
7 | The functions in this package are a scalar version of their vectorised counterparts in
8 | the [numpy-financial](https://github.com/numpy/numpy-financial) library.
9 |
10 | []("https://github.com/razorpay/go-financial/workflows/unit-tests/badge.svg?branch=master") [](https://goreportcard.com/report/github.com/razorpay/go-financial) [](https://codecov.io/gh/razorpay/go-financial) [](https://godoc.org/github.com/razorpay/go-financial) [](https://github.com/razorpay/go-financial/releases) [](http://makeapullrequest.com)
11 |
12 | Currently, only some functions are ported,
13 | which are as follows:
14 |
15 |
16 | | numpy-financial function | go native function ported? | info|
17 | |:------------------------: |:------------------: | :------------------|
18 | | fv | ✅ | Computes the future value|
19 | | ipmt | ✅ | Computes interest payment for a loan|
20 | | pmt | ✅ | Computes the fixed periodic payment(principal + interest) made against a loan amount|
21 | | ppmt | ✅ | Computes principal payment for a loan|
22 | | nper | ✅ | Computes the number of periodic payments|
23 | | pv | ✅ | Computes the present value of a payment|
24 | | rate | ✅ | Computes the rate of interest per period|
25 | | irr | | Computes the internal rate of return|
26 | | npv | ✅ | Computes the net present value of a series of cash flow|
27 | | mirr | | Computes the modified internal rate of return|
28 |
29 | # Index
30 | While the numpy-financial package contains a set of elementary financial functions, this pkg also contains some helper functions on top of it. Their usage and description can be found below:
31 |
32 | * [Amortisation(Generate Table)](#amortisation-generate-table-)
33 | + [Generated plot](#generated-plot)
34 | * [Fv(Future value)](#fv)
35 | + [Example(Fv)](#examplefv)
36 | * [Pv(Present value)](#pv)
37 | + [Example(Pv)](#examplepv)
38 | * [Npv(Net present value)](#npv)
39 | + [Example(Npv)](#examplenpv)
40 | * [Pmt(Payment)](#pmt)
41 | + [Example(Pmt-Loan)](#examplepmt-loan)
42 | + [Example(Pmt-Investment)](#examplepmt-investment)
43 | * [IPmt(Interest Payment)](#ipmt)
44 | + [Example(IPmt-Loan)](#exampleipmt-loan)
45 | + [Example(IPmt-Investment)](#exampleipmt-investment)
46 | * [PPmt(Principal Payment)](#ppmt)
47 | + [Example(PPmt-Loan)](#exampleppmt-loan)
48 | * [Nper(Number of payments)](#nper)
49 | + [Example(Nper-Loan)](#examplenper-loan)
50 | * [Rate(Interest Rate)](#rate)
51 | + [Example(Rate-Investment)](#examplerate-investment)
52 |
53 | Detailed documentation is available at [godoc](https://godoc.org/github.com/razorpay/go-financial).
54 | ## Amortisation(Generate Table)
55 |
56 |
57 | To generate the schedule for a loan of 20 lakhs over 15years at 12%p.a., you can do the following:
58 |
59 | ```go
60 | package main
61 |
62 | import (
63 | "time"
64 |
65 | "github.com/shopspring/decimal"
66 |
67 | financial "github.com/razorpay/go-financial"
68 | "github.com/razorpay/go-financial/enums/frequency"
69 | "github.com/razorpay/go-financial/enums/interesttype"
70 | "github.com/razorpay/go-financial/enums/paymentperiod"
71 | )
72 |
73 | func main() {
74 | loc, err := time.LoadLocation("Asia/Kolkata")
75 | if err != nil {
76 | panic("location loading error")
77 | }
78 | currentDate := time.Date(2009, 11, 11, 4, 30, 0, 0, loc)
79 | config := financial.Config{
80 |
81 | // start date is inclusive
82 | StartDate: currentDate,
83 |
84 | // end date is inclusive.
85 | EndDate: currentDate.AddDate(15, 0, 0).AddDate(0, 0, -1),
86 | Frequency: frequency.ANNUALLY,
87 |
88 | // AmountBorrowed is in paisa
89 | AmountBorrowed: decimal.NewFromInt(200000000),
90 |
91 | // InterestType can be flat or reducing
92 | InterestType: interesttype.REDUCING,
93 |
94 | // interest is in basis points
95 | Interest: decimal.NewFromInt(1200),
96 |
97 | // amount is paid at the end of the period
98 | PaymentPeriod: paymentperiod.ENDING,
99 |
100 | // all values will be rounded
101 | EnableRounding: true,
102 |
103 | // it will be rounded to nearest int
104 | RoundingPlaces: 0,
105 |
106 | // no error is tolerated
107 | RoundingErrorTolerance: decimal.Zero,
108 | }
109 | amortization, err := financial.NewAmortization(&config)
110 | if err != nil {
111 | panic(err)
112 | }
113 |
114 | rows, err := amortization.GenerateTable()
115 | if err != nil {
116 | panic(err)
117 | }
118 | // Generates json output of the data
119 | financial.PrintRows(rows)
120 | // Generates a html file with plots of the given data.
121 | financial.PlotRows(rows, "20lakh-loan-repayment-schedule")
122 | }
123 |
124 | ```
125 |
126 | ### Generated plot
127 |
128 |
129 | ## Fv
130 |
131 | ```go
132 | func Fv(rate decimal.Decimal, nper int64, pmt decimal.Decimal, pv decimal.Decimal, when paymentperiod.Type) decimal.Decimal
133 | ```
134 | Params:
135 | ```text
136 | pv : a present value
137 | rate : an interest rate compounded once per period
138 | nper : total number of periods
139 | pmt : a (fixed) payment, paid either at the beginning (when = 1)
140 | or the end (when = 0) of each period
141 | when : specification of whether payment is made at the beginning (when = 1)
142 | or the end (when = 0) of each period
143 | ```
144 |
145 | Fv computes future value at the end of some periods(nper).
146 |
147 | ### Example(Fv)
148 |
149 | If an investment has a 6% p.a. rate of return, compounded annually, and you are investing ₹ 10,000 at the end of each year with initial investment of ₹ 10,000, how much amount will you get at the end of 10 years ?
150 |
151 | ```go
152 | package main
153 |
154 | import (
155 | "fmt"
156 |
157 | gofinancial "github.com/razorpay/go-financial"
158 | "github.com/razorpay/go-financial/enums/paymentperiod"
159 | "github.com/shopspring/decimal"
160 | )
161 |
162 | func main() {
163 | rate := decimal.NewFromFloat(0.06)
164 | nper := int64(10)
165 | payment := decimal.NewFromInt(-10000)
166 | pv := decimal.NewFromInt(-10000)
167 | when := paymentperiod.ENDING
168 |
169 | fv := gofinancial.Fv(rate, nper, payment, pv, when)
170 | fmt.Printf("fv:%v", fv.Round(0))
171 | // Output:
172 | // fv:149716
173 | }
174 | ```
175 | [Run on go-playground](https://play.golang.org/p/B08Fs4TlYuF)
176 |
177 |
178 | ## Pv
179 |
180 | ```go
181 | func Pv(rate decimal.Decimal, nper int64, pmt decimal.Decimal, fv decimal.Decimal, when paymentperiod.Type) decimal.Decimal
182 | ```
183 | Params:
184 | ```text
185 | fv : a future value
186 | rate : an interest rate compounded once per period
187 | nper : total number of periods
188 | pmt : a (fixed) payment, paid either
189 | at the beginning (when = 1) or the end (when = 0) of each period
190 | when : specification of whether payment is made
191 | at the beginning (when = 1) or the end
192 | (when = 0) of each period
193 | ```
194 |
195 | Pv computes present value some periods(nper) before the future value.
196 |
197 | ### Example(Pv)
198 |
199 | If an investment has a 6% p.a. rate of return, compounded annually, and you wish to possess ₹ 1,49,716 at the end of 10 peroids while providing ₹ 10,000 per period, how much should you put as your initial deposit ?
200 |
201 | ```go
202 | package main
203 |
204 | import (
205 | "fmt"
206 |
207 | gofinancial "github.com/razorpay/go-financial"
208 | "github.com/razorpay/go-financial/enums/paymentperiod"
209 | "github.com/shopspring/decimal"
210 | )
211 |
212 | func main() {
213 | rate := decimal.NewFromFloat(0.06)
214 | nper := int64(10)
215 | payment := decimal.NewFromInt(-10000)
216 | fv := decimal.NewFromInt(149716)
217 | when := paymentperiod.ENDING
218 |
219 | pv := gofinancial.Pv(rate, nper, payment, fv, when)
220 | fmt.Printf("pv:%v", pv.Round(0))
221 | // Output:
222 | // pv:-10000
223 | }
224 | ```
225 | [Run on go-playground](https://play.golang.org/p/WW6cXQZa2_k)
226 |
227 |
228 | ## Npv
229 |
230 | ```go
231 | func Npv(rate decimal.Decimal, values []decimal.Decimal) decimal.Decimal
232 | ```
233 | Params:
234 | ```text
235 | rate : a discount rate compounded once per period
236 | values : the value of the cash flow for that time period. Values provided here must be an array of float64
237 | ```
238 |
239 | Npv computes net present value based on the discount rate and the values of cash flow over the course of the cash flow period
240 |
241 | ### Example(Npv)
242 |
243 | Given a rate of 0.281 per period and initial deposit of 100 followed by withdrawls of 39, 59, 55, 20. What is the net present value of the cash flow ?
244 |
245 | ```go
246 | package main
247 |
248 | import (
249 | "fmt"
250 | gofinancial "github.com/razorpay/go-financial"
251 | "github.com/shopspring/decimal"
252 | )
253 |
254 | func main() {
255 | rate := decimal.NewFromFloat(0.281)
256 | values := []decimal.Decimal{decimal.NewFromInt(-100), decimal.NewFromInt(39), decimal.NewFromInt(59), decimal.NewFromInt(55), decimal.NewFromInt(20)}
257 | npv := gofinancial.Npv(rate, values)
258 | fmt.Printf("npv:%v", npv)
259 | // Output:
260 | // npv: -0.008478591638426
261 | }
262 | ```
263 | [Run on go-playground](https://play.golang.org/p/4nzo1FOR3U0)
264 |
265 |
266 | ## Pmt
267 |
268 | ```go
269 | func Pmt(rate decimal.Decimal, nper int64, pv decimal.Decimal, fv decimal.Decimal, when paymentperiod.Type) decimal.Decimal
270 | ```
271 | Params:
272 | ```text
273 | rate : rate of interest compounded once per period
274 | nper : total number of periods to be compounded for
275 | pv : present value (e.g., an amount borrowed)
276 | fv : future value (e.g., 0)
277 | when : specification of whether payment is made at the
278 | beginning (when = 1) or the end (when = 0) of each period
279 | ```
280 |
281 | Pmt compute the fixed payment(principal + interest) against a loan amount ( fv = 0).
282 | It can also be used to calculate the recurring payments needed to achieve a certain future value given an initial deposit,
283 | a fixed periodically compounded interest rate, and the total number of periods.
284 |
285 | ### Example(Pmt-Loan)
286 | If you have a loan of 1,00,000 to be paid after 2 years, with 18% p.a. compounded annually, how much total payment will you have to do each month? This example generates the total monthly payment(principal plus interest) needed for a loan of 1,00,000 over 2 years with 18% rate of interest compounded monthly
287 |
288 | ```go
289 | package main
290 |
291 | import (
292 | "fmt"
293 | gofinancial "github.com/razorpay/go-financial"
294 | "github.com/razorpay/go-financial/enums/paymentperiod"
295 | "github.com/shopspring/decimal"
296 | )
297 |
298 | func main() {
299 | rate := decimal.NewFromFloat(0.18 / 12)
300 | nper := int64(12 * 2)
301 | pv := decimal.NewFromInt(100000)
302 | fv := decimal.NewFromInt(0)
303 | when := paymentperiod.ENDING
304 | pmt := gofinancial.Pmt(rate, nper, pv, fv, when)
305 | fmt.Printf("payment:%v", pmt.Round(0))
306 | // Output:
307 | // payment:-4992
308 | }
309 | ```
310 |
311 | [Run on go-playground](https://play.golang.org/p/kFGoL9g4VM4)
312 |
313 | ### Example(Pmt-Investment)
314 |
315 | If an investment gives 6% rate of return compounded annually, how much amount should you invest each month to get 10,00,000 amount after 10 years?
316 |
317 | ```go
318 | package main
319 |
320 | import (
321 | "fmt"
322 | gofinancial "github.com/razorpay/go-financial"
323 | "github.com/razorpay/go-financial/enums/paymentperiod"
324 | "github.com/shopspring/decimal"
325 | )
326 |
327 | func main() {
328 | rate := decimal.NewFromFloat(0.06)
329 | nper := int64(10)
330 | pv := decimal.NewFromInt(0)
331 | fv := decimal.NewFromInt(1000000)
332 | when := paymentperiod.BEGINNING
333 | pmt := gofinancial.Pmt(rate, nper, pv, fv, when)
334 | fmt.Printf("payment each year:%v", pmt.Round(0))
335 | // Output:
336 | // payment each year:-71574
337 | }
338 | ```
339 | [Run on go-playground](https://play.golang.org/p/aXC3vt-3UwS)
340 |
341 |
342 |
343 | ## IPmt
344 |
345 | ```go
346 | func IPmt(rate decimal.Decimal, per int64, nper int64, pv decimal.Decimal, fv decimal.Decimal, when paymentperiod.Type) decimal.Decimal
347 | ```
348 | IPmt computes interest payment for a loan under a given period.
349 |
350 | Params:
351 | ```text
352 | rate : rate of interest compounded once per period
353 | per : period under consideration
354 | nper : total number of periods to be compounded for
355 | pv : present value (e.g., an amount borrowed)
356 | fv : future value (e.g., 0)
357 | when : specification of whether payment is made at the
358 | beginning (when = 1) or the end (when = 0) of each period
359 | ```
360 |
361 | ### Example(IPmt-Loan)
362 | If you have a loan of 1,00,000 to be paid after 2 years, with 18% p.a. compounded annually, how much of the total payment done each month will be interest ?
363 |
364 | ```go
365 | package main
366 |
367 | import (
368 | "fmt"
369 | gofinancial "github.com/razorpay/go-financial"
370 | "github.com/razorpay/go-financial/enums/paymentperiod"
371 | "github.com/shopspring/decimal"
372 | )
373 |
374 | func main() {
375 | rate := decimal.NewFromFloat(0.18 / 12)
376 | nper := int64(12 * 2)
377 | pv := decimal.NewFromInt(100000)
378 | fv := decimal.NewFromInt(0)
379 | when := paymentperiod.ENDING
380 |
381 | for i := int64(0); i < nper; i++ {
382 | ipmt := gofinancial.IPmt(rate, i+1, nper, pv, fv, when)
383 | fmt.Printf("period:%d interest:%v\n", i+1, ipmt.Round(0))
384 | }
385 | // Output:
386 | // period:1 interest:-1500
387 | // period:2 interest:-1448
388 | // period:3 interest:-1394
389 | // period:4 interest:-1340
390 | // period:5 interest:-1286
391 | // period:6 interest:-1230
392 | // period:7 interest:-1174
393 | // period:8 interest:-1116
394 | // period:9 interest:-1058
395 | // period:10 interest:-999
396 | // period:11 interest:-939
397 | // period:12 interest:-879
398 | // period:13 interest:-817
399 | // period:14 interest:-754
400 | // period:15 interest:-691
401 | // period:16 interest:-626
402 | // period:17 interest:-561
403 | // period:18 interest:-494
404 | // period:19 interest:-427
405 | // period:20 interest:-358
406 | // period:21 interest:-289
407 | // period:22 interest:-218
408 | // period:23 interest:-146
409 | // period:24 interest:-74
410 | }
411 | ```
412 | [Run on go-playground](https://play.golang.org/p/um60s2QZL_8)
413 |
414 | ### Example(IPmt-Investment)
415 |
416 | If an investment gives 6% rate of return compounded annually, how much interest will you earn each year against your yearly payments(71574) to get 10,00,000 amount after 10 years
417 |
418 | ```go
419 | package main
420 |
421 | import (
422 | "fmt"
423 | gofinancial "github.com/razorpay/go-financial"
424 | "github.com/razorpay/go-financial/enums/paymentperiod"
425 | "github.com/shopspring/decimal"
426 | )
427 |
428 | func main() {
429 | rate := decimal.NewFromFloat(0.06)
430 | nper := int64(10)
431 | pv := decimal.NewFromInt(0)
432 | fv := decimal.NewFromInt(1000000)
433 | when := paymentperiod.BEGINNING
434 |
435 | for i := int64(1); i < nper+1; i++ {
436 | ipmt := gofinancial.IPmt(rate, i+1, nper, pv, fv, when)
437 | fmt.Printf("period:%d interest earned:%v\n", i, ipmt.Round(0))
438 | }
439 | // Output:
440 | // period:1 interest earned:4294
441 | // period:2 interest earned:8846
442 | // period:3 interest earned:13672
443 | // period:4 interest earned:18786
444 | // period:5 interest earned:24208
445 | // period:6 interest earned:29955
446 | // period:7 interest earned:36047
447 | // period:8 interest earned:42504
448 | // period:9 interest earned:49348
449 | // period:10 interest earned:56604
450 | }
451 | ```
452 | [Run on go-playground](https://play.golang.org/p/_NjNWuFulNp)
453 |
454 | ## PPmt
455 |
456 | ```go
457 | func PPmt(rate decimal.Decimal, per int64, nper int64, pv decimal.Decimal, fv decimal.Decimal, when paymentperiod.Type) decimal.Decimal
458 | ```
459 | PPmt computes principal payment for a loan under a given period.
460 |
461 | Params:
462 | ```text
463 | rate : rate of interest compounded once per period
464 | per : period under consideration
465 | nper : total number of periods to be compounded for
466 | pv : present value (e.g., an amount borrowed)
467 | fv : future value (e.g., 0)
468 | when : specification of whether payment is made at
469 | the beginning (when = 1) or the end (when = 0) of each period
470 | ```
471 |
472 | ### Example(PPmt-Loan)
473 | If you have a loan of 1,00,000 to be paid after 2 years, with 18% p.a. compounded annually, how much total payment done each month will be principal ?
474 | ```go
475 | package main
476 |
477 | import (
478 | "fmt"
479 | gofinancial "github.com/razorpay/go-financial"
480 | "github.com/razorpay/go-financial/enums/paymentperiod"
481 | "github.com/shopspring/decimal"
482 | )
483 |
484 | func main() {
485 | rate := decimal.NewFromFloat(0.18 / 12)
486 | nper := int64(12 * 2)
487 | pv := decimal.NewFromInt(100000)
488 | fv := decimal.NewFromInt(0)
489 | when := paymentperiod.ENDING
490 |
491 | for i := int64(0); i < nper; i++ {
492 | ppmt := gofinancial.PPmt(rate, i+1, nper, pv, fv, when)
493 | fmt.Printf("period:%d principal:%v\n", i+1, ppmt.Round(0))
494 | }
495 | // Output:
496 | // period:1 principal:-3492
497 | // period:2 principal:-3545
498 | // period:3 principal:-3598
499 | // period:4 principal:-3652
500 | // period:5 principal:-3707
501 | // period:6 principal:-3762
502 | // period:7 principal:-3819
503 | // period:8 principal:-3876
504 | // period:9 principal:-3934
505 | // period:10 principal:-3993
506 | // period:11 principal:-4053
507 | // period:12 principal:-4114
508 | // period:13 principal:-4176
509 | // period:14 principal:-4238
510 | // period:15 principal:-4302
511 | // period:16 principal:-4366
512 | // period:17 principal:-4432
513 | // period:18 principal:-4498
514 | // period:19 principal:-4566
515 | // period:20 principal:-4634
516 | // period:21 principal:-4704
517 | // period:22 principal:-4774
518 | // period:23 principal:-4846
519 | // period:24 principal:-4919
520 | }
521 | ```
522 | [Run on go-playground](https://play.golang.org/p/s5nkkIeEj3x)
523 |
524 |
525 |
526 |
527 | ## Nper
528 |
529 | ```go
530 | func Nper(rate decimal.Decimal, pmt decimal.Decimal, pv decimal.Decimal, fv decimal.Decimal, when paymentperiod.Type) (result decimal.Decimal, err error)
531 | ```
532 | Params:
533 | ```text
534 | rate : an interest rate compounded once per period
535 | pmt : a (fixed) payment, paid either at the beginning (when = 1)
536 | or the end (when = 0) of each period
537 | pv : a present value
538 | fv : a future value
539 | when : specification of whether payment is made at the beginning (when = 1)
540 | or the end (when = 0) of each period
541 | ```
542 |
543 | Nper computes the number of periodic payments.
544 |
545 | ### Example(Nper-Loan)
546 |
547 | If a loan has a 6% annual interest, compounded monthly, and you only have \$200/month to pay towards the loan, how long would it take to pay-off the loan of \$5,000?
548 |
549 | ```go
550 | package main
551 |
552 | import (
553 | "fmt"
554 |
555 | gofinancial "github.com/razorpay/go-financial"
556 | "github.com/razorpay/go-financial/enums/paymentperiod"
557 | "github.com/shopspring/decimal"
558 | )
559 |
560 |
561 | func main() {
562 | rate := decimal.NewFromFloat(0.06 / 12)
563 | fv := decimal.NewFromFloat(0)
564 | payment := decimal.NewFromFloat(-200)
565 | pv := decimal.NewFromFloat(5000)
566 | when := paymentperiod.ENDING
567 |
568 | nper,err := gofinancial.Nper(rate, payment, pv, fv, when)
569 | if err != nil{
570 | fmt.Printf("error:%v\n", err)
571 | }
572 | fmt.Printf("nper:%v",nper.Ceil())
573 | // Output:
574 | // nper:27
575 | }
576 | ```
577 | [Run on go-playground](https://play.golang.org/p/hm77MTPBGYg)
578 |
579 | ## Rate
580 |
581 | ```go
582 | func Rate(pv, fv, pmt decimal.Decimal, nper int64, when paymentperiod.Type, maxIter int64, tolerance, initialGuess decimal.Decimal) (decimal.Decimal, error)
583 | ```
584 | Params:
585 | ```text
586 | pv : a present value
587 | fv : a future value
588 | pmt : a (fixed) payment, paid either at the beginning (when = 1)
589 | or the end (when = 0) of each period
590 | nper : total number of periods to be compounded for
591 | when : specification of whether payment is made at the beginning (when = 1)
592 | or the end (when = 0) of each period
593 | maxIter : total number of iterations for which function should run
594 | tolerance : tolerance threshold for acceptable result
595 | initialGuess : an initial guess amount to start from
596 | ```
597 |
598 | Returns:
599 | ```text
600 | rate : a value for the corresponding rate
601 | error : returns nil if rate difference is less than the threshold (returns an error conversely)
602 | ```
603 |
604 | Rate computes the interest rate to ensure a balanced cashflow equation
605 |
606 | ### Example(Rate-Investment)
607 |
608 | If an investment of $2000 is done and an amount of $100 is added at the start of each period, for what periodic interest rate would the invester be able to withdraw $3000 after the end of 4 periods ? (assuming 100 iterations, 1e-6 threshold and 0.1 as initial guessing point)
609 |
610 | ```go
611 | package main
612 |
613 | import (
614 | "fmt"
615 | gofinancial "github.com/razorpay/go-financial"
616 | "github.com/razorpay/go-financial/enums/paymentperiod"
617 | "github.com/shopspring/decimal"
618 | )
619 |
620 | func main() {
621 | fv := decimal.NewFromFloat(-3000)
622 | pmt := decimal.NewFromFloat(100)
623 | pv := decimal.NewFromFloat(2000)
624 | when := paymentperiod.BEGINNING
625 | nper := decimal.NewFromInt(4)
626 | maxIter := 100
627 | tolerance := decimal.NewFromFloat(1e-6)
628 | initialGuess := decimal.NewFromFloat(0.1),
629 |
630 | rate, err := gofinancial.Rate(pv, fv, pmt, nper, when, maxIter, tolerance, initialGuess)
631 | if err != nil {
632 | fmt.Printf(err)
633 | } else {
634 | fmt.Printf("rate: %v ", rate)
635 | }
636 | // Output:
637 | // rate: 0.06106257989825202
638 | }
639 | ```
640 | [Run on go-playground](https://play.golang.org/p/H2uybe1dbRj)
641 |
--------------------------------------------------------------------------------
/amortization_test.go:
--------------------------------------------------------------------------------
1 | package gofinancial
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "strings"
9 | "testing"
10 | "time"
11 |
12 | "github.com/shopspring/decimal"
13 |
14 | "github.com/go-echarts/go-echarts/v2/charts"
15 |
16 | "github.com/razorpay/go-financial/enums/interesttype"
17 | "github.com/razorpay/go-financial/enums/paymentperiod"
18 | "github.com/smartystreets/assertions"
19 |
20 | "github.com/razorpay/go-financial/enums/frequency"
21 | )
22 |
23 | const (
24 | precision = 0.0000001
25 | )
26 |
27 | func Test_amortization_GenerateTable(t *testing.T) {
28 | type fields struct {
29 | Config *Config
30 | }
31 |
32 | tests := []struct {
33 | name string
34 | fields fields
35 | want []Row
36 | wantErr bool
37 | }{
38 | {
39 | name: "monthly table with rounding, reducing interest",
40 | fields: fields{Config: getConfigDto(frequency.MONTHLY, true, interesttype.REDUCING, decimal.NewFromInt(1000000), decimal.NewFromInt(2400), 0)},
41 | want: getRowsWithRounding(t),
42 | wantErr: false,
43 | },
44 | {
45 | name: "monthly table without rounding, reducing interest",
46 | fields: fields{Config: getConfigDto(frequency.MONTHLY, false, interesttype.REDUCING, decimal.NewFromInt(1000000), decimal.NewFromInt(2400), 0)},
47 | want: getRowsWithoutRounding(t),
48 | wantErr: false,
49 | },
50 | {
51 | name: "daily table, flat interest, with rounding",
52 | fields: fields{
53 | Config: &Config{
54 | StartDate: time.Date(2020, 4, 15, 0, 0, 0, 0, time.UTC),
55 | EndDate: time.Date(2020, 5, 14, 0, 0, 0, 0, time.UTC),
56 | Frequency: frequency.DAILY,
57 | AmountBorrowed: decimal.NewFromInt(1000000),
58 | InterestType: interesttype.FLAT,
59 | Interest: decimal.NewFromInt(7300),
60 | PaymentPeriod: paymentperiod.ENDING,
61 | EnableRounding: true,
62 | RoundingPlaces: 0,
63 | },
64 | },
65 | want: getRowsFlatWithRounding(t),
66 | wantErr: false,
67 | },
68 | }
69 | for _, tt := range tests {
70 | t.Run(tt.name, func(t *testing.T) {
71 | a, err := NewAmortization(tt.fields.Config)
72 | if err != nil {
73 | t.Errorf("NewAmortization() call failed. error = %v", err)
74 | }
75 | got, err := a.GenerateTable()
76 | if (err != nil) != tt.wantErr {
77 | t.Errorf("GenerateTable() error = %v, wantErr %v", err, tt.wantErr)
78 | return
79 | }
80 | if len(got) != len(tt.want) {
81 | t.Fatalf("length mismatch of rows generate, want=%v, got=%v", len(tt.want), len(got))
82 | }
83 | for idx := range got {
84 | if err := verifyRow(t, got[idx], tt.want[idx]); err != nil {
85 | t.Fatal(err)
86 | }
87 | }
88 | if err := principalCheck(t, got, tt.fields.Config.AmountBorrowed); err != nil {
89 | t.Fatal(err)
90 | }
91 | })
92 | }
93 | }
94 |
95 | func principalCheck(t *testing.T, rows []Row, actualPrincipal decimal.Decimal) error {
96 | expectedPrincipal := decimal.Zero
97 | dPrecision := decimal.NewFromFloat(precision)
98 | for _, row := range rows {
99 | expectedPrincipal = expectedPrincipal.Add(row.Principal)
100 | }
101 | if err := isAlmostEqual(expectedPrincipal, actualPrincipal, dPrecision); err != nil {
102 | return fmt.Errorf("error:%v, principalCheck failed. expected:%v, got:%v", err.Error(), expectedPrincipal, actualPrincipal)
103 | }
104 | return nil
105 | }
106 |
107 | func verifyRow(t *testing.T, actual Row, expected Row) error {
108 | dPrecision := decimal.NewFromFloat(precision)
109 | if err := isAlmostEqual(actual.Principal, expected.Principal, dPrecision); err != nil {
110 | return fmt.Errorf("error:%v, principal values are not almost equal. expected:%v, got:%v", err.Error(), expected.Principal, actual.Principal)
111 | }
112 | if err := isAlmostEqual(actual.Interest, expected.Interest, dPrecision); err != nil {
113 | return fmt.Errorf("error:%v, interest values are not almost equal. expected:%v, got:%v", err.Error(), expected.Interest, actual.Interest)
114 | }
115 | if err := isAlmostEqual(actual.Payment, expected.Payment, dPrecision); err != nil {
116 | return fmt.Errorf("error:%v, payment values are not equal. expected:%v, got:%v", err.Error(), expected.Payment, actual.Payment)
117 | }
118 | if err := isAlmostEqual(actual.Principal.Add(actual.Interest), actual.Payment, dPrecision); err != nil {
119 | return fmt.Errorf("error:%v, the calculation is not correct. %v(Interest) + %v(Principal) != %v(Payment)", err.Error(), actual.Interest, actual.Principal, actual.Payment)
120 | }
121 | if !actual.StartDate.Equal(expected.StartDate) {
122 | return fmt.Errorf("start date value mismatch. Expected startDate:%v, endDate:%v, got startDate:%v endDate:%v", expected.StartDate, expected.EndDate, actual.StartDate, actual.EndDate)
123 | }
124 | if !actual.EndDate.Equal(expected.EndDate) {
125 | return fmt.Errorf("end date value mismatch. Expected startDate:%v, endDate:%v, got startDate:%v endDate:%v", expected.StartDate, expected.EndDate, actual.StartDate, actual.EndDate)
126 | }
127 | return nil
128 | }
129 |
130 | func isAlmostEqual(first decimal.Decimal, second decimal.Decimal, tolerance decimal.Decimal) error {
131 | diff := first.Abs().Sub(second.Abs())
132 | if diff.Abs().LessThanOrEqual(tolerance) {
133 | return nil
134 | } else {
135 | return fmt.Errorf("%s is not equal to %s with %s tolerance", first.String(), second.String(), tolerance.String())
136 | }
137 | }
138 |
139 | func getRowsWithRounding(t *testing.T) []Row {
140 | return []Row{
141 | {Period: 1, StartDate: timeParseUtil(t, "2020-04-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-20000), Principal: decimal.NewFromInt(-32871)},
142 | {Period: 2, StartDate: timeParseUtil(t, "2020-05-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-06-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-19342), Principal: decimal.NewFromInt(-33529)},
143 | {Period: 3, StartDate: timeParseUtil(t, "2020-06-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-07-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-18672), Principal: decimal.NewFromInt(-34199)},
144 | {Period: 4, StartDate: timeParseUtil(t, "2020-07-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-08-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-17988), Principal: decimal.NewFromInt(-34883)},
145 | {Period: 5, StartDate: timeParseUtil(t, "2020-08-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-09-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-17290), Principal: decimal.NewFromInt(-35581)},
146 | {Period: 6, StartDate: timeParseUtil(t, "2020-09-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-10-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-16579), Principal: decimal.NewFromInt(-36292)},
147 | {Period: 7, StartDate: timeParseUtil(t, "2020-10-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-11-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-15853), Principal: decimal.NewFromInt(-37018)},
148 | {Period: 8, StartDate: timeParseUtil(t, "2020-11-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-12-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-15112), Principal: decimal.NewFromInt(-37759)},
149 | {Period: 9, StartDate: timeParseUtil(t, "2020-12-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-01-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-14357), Principal: decimal.NewFromInt(-38514)},
150 | {Period: 10, StartDate: timeParseUtil(t, "2021-01-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-02-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-13587), Principal: decimal.NewFromInt(-39284)},
151 | {Period: 11, StartDate: timeParseUtil(t, "2021-02-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-03-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-12801), Principal: decimal.NewFromInt(-40070)},
152 | {Period: 12, StartDate: timeParseUtil(t, "2021-03-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-04-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-12000), Principal: decimal.NewFromInt(-40871)},
153 | {Period: 13, StartDate: timeParseUtil(t, "2021-04-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-05-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-11183), Principal: decimal.NewFromInt(-41688)},
154 | {Period: 14, StartDate: timeParseUtil(t, "2021-05-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-06-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-10349), Principal: decimal.NewFromInt(-42522)},
155 | {Period: 15, StartDate: timeParseUtil(t, "2021-06-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-07-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-9498), Principal: decimal.NewFromInt(-43373)},
156 | {Period: 16, StartDate: timeParseUtil(t, "2021-07-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-08-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-8631), Principal: decimal.NewFromInt(-44240)},
157 | {Period: 17, StartDate: timeParseUtil(t, "2021-08-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-09-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-7746), Principal: decimal.NewFromInt(-45125)},
158 | {Period: 18, StartDate: timeParseUtil(t, "2021-09-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-10-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-6844), Principal: decimal.NewFromInt(-46027)},
159 | {Period: 19, StartDate: timeParseUtil(t, "2021-10-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-11-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-5923), Principal: decimal.NewFromInt(-46948)},
160 | {Period: 20, StartDate: timeParseUtil(t, "2021-11-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-12-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-4984), Principal: decimal.NewFromInt(-47887)},
161 | {Period: 21, StartDate: timeParseUtil(t, "2021-12-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2022-01-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-4026), Principal: decimal.NewFromInt(-48845)},
162 | {Period: 22, StartDate: timeParseUtil(t, "2022-01-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2022-02-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-3049), Principal: decimal.NewFromInt(-49822)},
163 | {Period: 23, StartDate: timeParseUtil(t, "2022-02-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2022-03-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-2053), Principal: decimal.NewFromInt(-50818)},
164 | {Period: 24, StartDate: timeParseUtil(t, "2022-03-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2022-04-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-52871), Interest: decimal.NewFromInt(-1037), Principal: decimal.NewFromInt(-51834)},
165 | }
166 | }
167 |
168 | func getRowsWithoutRounding(t *testing.T) []Row {
169 | return []Row{
170 | {Period: 1, StartDate: timeParseUtil(t, "2020-04-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-20000), Principal: decimal.NewFromFloat(-32871.0972532498902312)},
171 | {Period: 2, StartDate: timeParseUtil(t, "2020-05-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-06-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-19342.578054935002195376), Principal: decimal.NewFromFloat(-33528.519198314888035824)},
172 | {Period: 3, StartDate: timeParseUtil(t, "2020-06-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-07-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-18672.00767096870443465952), Principal: decimal.NewFromFloat(-34199.08958228118579654048)},
173 | {Period: 4, StartDate: timeParseUtil(t, "2020-07-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-08-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-17988.0258793230807187287104), Principal: decimal.NewFromFloat(-34883.0713739268095124712896)},
174 | {Period: 5, StartDate: timeParseUtil(t, "2020-08-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-09-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-17290.364451844544528479284608), Principal: decimal.NewFromFloat(-35580.732801405345702720715392)},
175 | {Period: 6, StartDate: timeParseUtil(t, "2020-09-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-10-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-16578.74979581643761442487030016), Principal: decimal.NewFromFloat(-36292.34745743345261677512969984)},
176 | {Period: 7, StartDate: timeParseUtil(t, "2020-10-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-11-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-15852.9028466677685620893677061632), Principal: decimal.NewFromFloat(-37018.1944065821216691106322938368)},
177 | {Period: 8, StartDate: timeParseUtil(t, "2020-11-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-12-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-15112.538958536126128707155060286464), Principal: decimal.NewFromFloat(-37758.558294713764102492844939713536)},
178 | {Period: 9, StartDate: timeParseUtil(t, "2020-12-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-01-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-14357.36779264185084665729816149219328), Principal: decimal.NewFromFloat(-38513.72946060803938454270183850780672)},
179 | {Period: 10, StartDate: timeParseUtil(t, "2021-01-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-02-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-13587.0932034296900589664441247220371456), Principal: decimal.NewFromFloat(-39284.0040498202001722335558752779628544)},
180 | {Period: 11, StartDate: timeParseUtil(t, "2021-02-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-03-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-12801.413122433286068210836347996451544), Principal: decimal.NewFromFloat(-40069.684130816604162989163652003548456)},
181 | {Period: 12, StartDate: timeParseUtil(t, "2021-03-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-04-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-12000.0194398169540166737114269063147136), Principal: decimal.NewFromFloat(-40871.0778134329362145262885730936852864)},
182 | {Period: 13, StartDate: timeParseUtil(t, "2021-04-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-05-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-11182.5978835482952627753711936245024784), Principal: decimal.NewFromFloat(-41688.4993697015949684246288063754975216)},
183 | {Period: 14, StartDate: timeParseUtil(t, "2021-05-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-06-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-10348.8278961542633824404736286669530112), Principal: decimal.NewFromFloat(-42522.2693570956268487595263713330469888)},
184 | {Period: 15, StartDate: timeParseUtil(t, "2021-06-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-07-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-9498.38250901235076510121527630045892), Principal: decimal.NewFromFloat(-43372.71474423753946609878472369954108)},
185 | {Period: 16, StartDate: timeParseUtil(t, "2021-07-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-08-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-8630.9282141276000286503368350763583296), Principal: decimal.NewFromFloat(-44240.1690391222902025496631649236416704)},
186 | {Period: 17, StartDate: timeParseUtil(t, "2021-08-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-09-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-7746.1248333451542161399680112579030592), Principal: decimal.NewFromFloat(-45124.9724199047360150600319887420969408)},
187 | {Period: 18, StartDate: timeParseUtil(t, "2021-09-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-10-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-6843.6253849470594789200162504430962464), Principal: decimal.NewFromFloat(-46027.4718683028307522799837495569037536)},
188 | {Period: 19, StartDate: timeParseUtil(t, "2021-10-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-11-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-5923.0759475810028934822310372718967008), Principal: decimal.NewFromFloat(-46948.0213056688873377177689627281032992)},
189 | {Period: 20, StartDate: timeParseUtil(t, "2021-11-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2021-12-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-4984.1155214676251636466267790572995088), Principal: decimal.NewFromFloat(-47886.9817317822650675533732209427004912)},
190 | {Period: 21, StartDate: timeParseUtil(t, "2021-12-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2022-01-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-4026.375886831979834802588742948502578752), Principal: decimal.NewFromFloat(-48844.721366417910396397411257051497421248)},
191 | {Period: 22, StartDate: timeParseUtil(t, "2022-01-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2022-02-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-3049.48145950362167128636221053738042453504), Principal: decimal.NewFromFloat(-49821.61579374626855991363778946261957546496)},
192 | {Period: 23, StartDate: timeParseUtil(t, "2022-02-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2022-03-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.0972532498902312), Interest: decimal.NewFromFloat(-2053.0491436286962239537094100682861000977408), Principal: decimal.NewFromFloat(-50818.0481096211940072462905899317138999022592)},
193 | {Period: 24, StartDate: timeParseUtil(t, "2022-03-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2022-04-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromFloat(-52871.097253249891181711353923018822531276476416), Interest: decimal.NewFromFloat(-1036.688181436272405139256412039524490291695616), Principal: decimal.NewFromFloat(-51834.4090718136187765720975109792980409847808)},
194 | }
195 | }
196 |
197 | func getRowsFlatWithRounding(t *testing.T) []Row {
198 | return []Row{
199 | {Period: 1, StartDate: timeParseUtil(t, "2020-04-15 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-15 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
200 | {Period: 2, StartDate: timeParseUtil(t, "2020-04-16 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-16 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
201 | {Period: 3, StartDate: timeParseUtil(t, "2020-04-17 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-17 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
202 | {Period: 4, StartDate: timeParseUtil(t, "2020-04-18 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-18 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
203 | {Period: 5, StartDate: timeParseUtil(t, "2020-04-19 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-19 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
204 | {Period: 6, StartDate: timeParseUtil(t, "2020-04-20 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-20 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
205 | {Period: 7, StartDate: timeParseUtil(t, "2020-04-21 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-21 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
206 | {Period: 8, StartDate: timeParseUtil(t, "2020-04-22 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-22 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
207 | {Period: 9, StartDate: timeParseUtil(t, "2020-04-23 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-23 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
208 | {Period: 10, StartDate: timeParseUtil(t, "2020-04-24 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-24 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
209 | {Period: 11, StartDate: timeParseUtil(t, "2020-04-25 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-25 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
210 | {Period: 12, StartDate: timeParseUtil(t, "2020-04-26 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-26 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
211 | {Period: 13, StartDate: timeParseUtil(t, "2020-04-27 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-27 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
212 | {Period: 14, StartDate: timeParseUtil(t, "2020-04-28 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-28 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
213 | {Period: 15, StartDate: timeParseUtil(t, "2020-04-29 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-29 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
214 | {Period: 16, StartDate: timeParseUtil(t, "2020-04-30 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-04-30 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
215 | {Period: 17, StartDate: timeParseUtil(t, "2020-05-01 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-01 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
216 | {Period: 18, StartDate: timeParseUtil(t, "2020-05-02 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-02 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
217 | {Period: 19, StartDate: timeParseUtil(t, "2020-05-03 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-03 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
218 | {Period: 20, StartDate: timeParseUtil(t, "2020-05-04 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-04 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
219 | {Period: 21, StartDate: timeParseUtil(t, "2020-05-05 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-05 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
220 | {Period: 22, StartDate: timeParseUtil(t, "2020-05-06 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-06 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
221 | {Period: 23, StartDate: timeParseUtil(t, "2020-05-07 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-07 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
222 | {Period: 24, StartDate: timeParseUtil(t, "2020-05-08 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-08 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
223 | {Period: 25, StartDate: timeParseUtil(t, "2020-05-09 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-09 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
224 | {Period: 26, StartDate: timeParseUtil(t, "2020-05-10 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-10 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
225 | {Period: 27, StartDate: timeParseUtil(t, "2020-05-11 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-11 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
226 | {Period: 28, StartDate: timeParseUtil(t, "2020-05-12 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-12 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
227 | {Period: 29, StartDate: timeParseUtil(t, "2020-05-13 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-13 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35333), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33333)},
228 | {Period: 30, StartDate: timeParseUtil(t, "2020-05-14 00:00:00 +0000 UTC"), EndDate: timeParseUtil(t, "2020-05-14 23:59:59 +0000 UTC"), Payment: decimal.NewFromInt(-35343), Interest: decimal.NewFromInt(-2000), Principal: decimal.NewFromInt(-33343)},
229 | }
230 | }
231 |
232 | func timeParseUtil(t *testing.T, input string) time.Time {
233 | resultTime, err := time.Parse("2006-01-02 15:04:05 -0700 MST", input)
234 | if err != nil {
235 | t.Fatalf("invalid time format, %v", err)
236 | }
237 | return resultTime
238 | }
239 |
240 | func getConfigDto(frequency frequency.Type, round bool, interestType interesttype.Type, amount decimal.Decimal, interest decimal.Decimal, places int32) *Config {
241 | return &Config{
242 | StartDate: time.Date(2020, 4, 15, 0, 0, 0, 0, time.UTC),
243 | EndDate: time.Date(2022, 4, 14, 0, 0, 0, 0, time.UTC),
244 | Frequency: frequency,
245 | AmountBorrowed: amount,
246 | InterestType: interestType,
247 | Interest: interest,
248 | EnableRounding: round,
249 | RoundingPlaces: places,
250 | }
251 | }
252 |
253 | func TestPlotRows(t *testing.T) {
254 | type args struct {
255 | rows []Row
256 | fileName string
257 | }
258 | tests := []struct {
259 | name string
260 | args args
261 | wantErr bool
262 | }{
263 | {
264 | "plot for loan schedule",
265 | args{
266 | rows: getRowsWithRounding(t),
267 | fileName: "loan-schedule",
268 | },
269 | false,
270 | },
271 | }
272 |
273 | for _, tt := range tests {
274 | t.Run(tt.name, func(t *testing.T) {
275 | var err error
276 | if err = PlotRows(tt.args.rows, tt.args.fileName); (err != nil) != tt.wantErr {
277 | t.Errorf("PlotRows() error = %v, wantErr %v", err, tt.wantErr)
278 | }
279 | })
280 | }
281 | }
282 |
283 | func TestRenderer(t *testing.T) {
284 | type args struct {
285 | bar *charts.Bar
286 | }
287 | rows := getRowsWithRounding(t)
288 | tests := []struct {
289 | name string
290 | args args
291 | writer *bytes.Buffer
292 | stringWanted string
293 | wantErr bool
294 | err error
295 | errWriter io.Writer
296 | }{
297 | {
298 | "success",
299 | args{
300 | bar: getStackedBarPlot(rows),
301 | },
302 | &bytes.Buffer{},
303 | getExpectedHtmlString(),
304 | false,
305 | nil,
306 | nil,
307 | },
308 | {
309 | "error while writing",
310 | args{
311 | bar: getStackedBarPlot(rows),
312 | },
313 | nil,
314 | getExpectedHtmlString(),
315 | true,
316 | errors.New("error writer"),
317 | &errorWriter{},
318 | },
319 | }
320 | for _, tt := range tests {
321 | t.Run(tt.name, func(t *testing.T) {
322 | var err error
323 | if tt.wantErr {
324 | err = renderer(tt.args.bar, tt.errWriter)
325 | } else {
326 | err = renderer(tt.args.bar, tt.writer)
327 | result := assertions.ShouldEqual(getHtmlWithoutUniqueId(tt.stringWanted), getHtmlWithoutUniqueId(tt.writer.String()))
328 | if result != "" {
329 | t.Errorf("Rendere() expected != actual. diff:%v", result)
330 | }
331 | }
332 | if err != nil && err.Error() != tt.err.Error() {
333 | t.Errorf("Renderer() error = %v, wantErr %v", err, tt.wantErr)
334 | return
335 | }
336 | })
337 | }
338 | }
339 |
340 | type errorWriter struct{}
341 |
342 | func (er *errorWriter) Write(p []byte) (n int, err error) {
343 | return 1, errors.New("error writer")
344 | }
345 |
346 | func getHtmlWithoutUniqueId(input string) string {
347 | lines := strings.Split(input, "\n")
348 | var result []string
349 | for i := range lines {
350 | // skipping the unique id lines
351 | if i >= 11 && i <= 18 {
352 | continue
353 | }
354 | result = append(result, lines[i])
355 | }
356 | return strings.Join(result, "\n")
357 | }
358 |
359 | func getExpectedHtmlString() string {
360 | return `
361 |
362 |
363 |