├── .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 | [![unit-tests status](https://github.com/razorpay/go-financial/workflows/unit-tests/badge.svg?branch=master "unit-tests")]("https://github.com/razorpay/go-financial/workflows/unit-tests/badge.svg?branch=master") [![Go Report Card](https://goreportcard.com/badge/github.com/razorpay/go-financial)](https://goreportcard.com/report/github.com/razorpay/go-financial) [![codecov](https://codecov.io/gh/razorpay/go-financial/branch/master/graph/badge.svg)](https://codecov.io/gh/razorpay/go-financial) [![GoDoc](https://godoc.org/github.com/razorpay/go-financial?status.svg)](https://godoc.org/github.com/razorpay/go-financial) [![Release](https://img.shields.io/github/release/razorpay/go-financial.svg?style=flat-square)](https://github.com/razorpay/go-financial/releases) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](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 | 364 | 365 | Awesome go-echarts 366 | 367 | 368 | 369 | 370 |
371 |
372 |
373 | 374 | 380 | 381 | 385 | 386 | 387 | ` 388 | } 389 | --------------------------------------------------------------------------------