├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── release-drafter.yml │ ├── stale.yml │ ├── reviewdog.yml │ ├── test.yml │ └── auto-go-mod-tidy.yml ├── dependabot.yml └── release-drafter.yml ├── go.mod ├── doc.go ├── .gitignore ├── CONTRIBUTING.md ├── bps ├── doc.go ├── bps.go ├── example_test.go ├── serial.go ├── bps_test.go ├── conv.go ├── calc.go ├── construct.go ├── serial_test.go ├── construct_test.go ├── conv_test.go └── calc_test.go ├── Makefile ├── LICENSE ├── .golangci.yml └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @iwata 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.mercari.io/go-bps 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What version of `go-bps` are you using? 2 | 3 | ### What did you expect to see? 4 | 5 | ### What did you see instead? 6 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Merpay, Inc. All rights reserved. 2 | 3 | // Package bps provides the basis points 4 | package bps // import "go.mercari.io/go-bps" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | coverage.* 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please read the CLA carefully before submitting your contribution to Mercari. 4 | Under any circumstances, by submitting your contribution, you are deemed to accept and agree to be bound by the terms and conditions of the CLA. 5 | 6 | https://www.mercari.com/cla/ 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /bps/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Merpay, Inc. All rights reserved. 2 | 3 | // Package bps provides the basis points 4 | 5 | /* 6 | Handling floating point numbers in programming causes rounding errors. 7 | To avoid this, all numerical calculations are done using basis points (integer only) in this package. 8 | */ 9 | package bps // import "go.mercari.io/go-bps/bps" 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | go test -v ./... -count 1 -race 4 | 5 | .PHONY: cover 6 | cover: 7 | go test -v ./... -coverpkg=./bps -covermode=count -coverprofile=coverage.txt 8 | 9 | .PHONY: view-cover 10 | view-cover: cover 11 | go tool cover -html coverage.txt 12 | 13 | setup: 14 | go mod tidy 15 | 16 | setup/tools: 17 | GO111MODULE=off go get -u \ 18 | github.com/golangci/golangci-lint/cmd/golangci-lint 19 | 20 | lint: 21 | golangci-lint run ./... 22 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release Drafter 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - main 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v6 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Close stale issues" 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v9 12 | with: 13 | repo-token: ${{ secrets.GITHUB_TOKEN }} 14 | stale-issue-message: 'Message to comment on stale issues. If none provided, will not mark issues stale' 15 | stale-pr-message: 'Message to comment on stale PRs. If none provided, will not mark PRs stale' 16 | -------------------------------------------------------------------------------- /.github/workflows/reviewdog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: reviewdog 3 | on: 4 | pull_request: 5 | jobs: 6 | golangci-lint: 7 | name: golangci-lint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out code into the Go module directory 11 | uses: actions/checkout@v4 12 | - name: Set up Go 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: "1.17" 16 | - name: golangci-lint 17 | uses: reviewdog/action-golangci-lint@v2 18 | with: 19 | filter_mode: nofilter 20 | fail_on_error: true 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: gomod 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | time: "09:00" 9 | timezone: "Asia/Tokyo" 10 | labels: 11 | - dependencies 12 | - dependabot 13 | reviewers: 14 | - iwata 15 | - package-ecosystem: github-actions 16 | directory: / # For GitHub Actions, set the `directory` to `/ `to check for workflow files in .github/workflows. 17 | schedule: 18 | interval: weekly 19 | day: "monday" 20 | time: "09:00" 21 | timezone: "Asia/Tokyo" 22 | commit-message: 23 | prefix: GitHub actions 24 | include: scope 25 | labels: 26 | - dependencies 27 | - dependabot 28 | - github-actions 29 | reviewers: 30 | - iwata 31 | -------------------------------------------------------------------------------- /bps/bps.go: -------------------------------------------------------------------------------- 1 | package bps 2 | 3 | import "math/big" 4 | 5 | // unit is the list of allowed values to set BaseUnit. 6 | type unit int 7 | 8 | // List of values that `unit` can take. 9 | const ( 10 | PPB unit = iota + 1 11 | PPM 12 | DeciBasisPoint 13 | HalfBasisPoint 14 | BasisPoint 15 | Percentage 16 | ) 17 | 18 | // BaseUnit is unit to display *BPS as string via String method. 19 | // Default is DeciBasisPoint unit, you can update this. 20 | // But it should be used consistent value in your application. 21 | var BaseUnit = DeciBasisPoint 22 | 23 | type BPS struct { 24 | value *big.Int 25 | } 26 | 27 | // String returns the string representation of BaseUnit as generated by *big.Int.String(). 28 | // That means the effective digits is modifiable by BaseUnit. 29 | func (b *BPS) String() string { 30 | return b.BaseUnitAmounts().String() 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test and coverage 3 | on: [push, pull_request] 4 | jobs: 5 | go-versions: 6 | runs-on: ubuntu-latest 7 | outputs: 8 | versions: ${{ steps.versions.outputs.value }} 9 | steps: 10 | - id: versions 11 | run: | 12 | versions=$(curl -s 'https://go.dev/dl/?mode=json' | jq -c 'map(.version[2:])') 13 | echo "::set-output name=value::${versions}" 14 | test: 15 | runs-on: ubuntu-latest 16 | needs: go-versions 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | go-version: ${{fromJson(needs.go-versions.outputs.versions)}} 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | - name: Install Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | - name: Run tests 29 | run: make test 30 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name-template: 'v$RESOLVED_VERSION 🌈' 3 | tag-template: 'v$RESOLVED_VERSION' 4 | categories: 5 | - title: '🚀 Features' 6 | labels: 7 | - 'feature' 8 | - 'enhancement' 9 | - title: '🐛 Bug Fixes' 10 | labels: 11 | - 'fix' 12 | - 'bugfix' 13 | - 'bug' 14 | - title: '🧰 Maintenance' 15 | label: 'chore' 16 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 17 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 18 | exclude-labels: 19 | - 'skip-changelog' 20 | version-resolver: 21 | major: 22 | labels: 23 | - 'major' 24 | minor: 25 | labels: 26 | - 'minor' 27 | patch: 28 | labels: 29 | - 'patch' 30 | default: patch 31 | template: | 32 | ## Changes 33 | 34 | $CHANGES 35 | replacers: 36 | - search: '/CVE-(\d{4})-(\d+)/g' 37 | replace: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-$1-$2' 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2020 Mercari, Inc. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /.github/workflows/auto-go-mod-tidy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: auto go-mod-tidy 3 | on: 4 | push: 5 | paths: ["go.mod"] 6 | 7 | jobs: 8 | tidy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: debug 12 | uses: hmarr/debug-action@v3.0.0 13 | - uses: actions/checkout@v4 14 | - name: Install Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: "1.17" 18 | - id: log 19 | run: echo "::set-output name=message::$(git log --no-merges -1 --oneline)" 20 | - name: Run go mod tidy 21 | run: | 22 | rm go.sum 23 | go mod tidy && go mod edit -fmt go.mod 24 | - id: git_diff 25 | run: echo "::set-output name=diff::$(git status --porcelain)" 26 | - name: configure github user 27 | run: | 28 | git config --local user.email "121048+iwata@users.noreply.github.com" 29 | git config --local user.name "Motonori IWATA" 30 | - name: Print next steps condition values 31 | run: | 32 | echo "git diff: ${{ steps.git_diff.outputs.diff }}" 33 | echo "commit message: ${{ steps.log.outputs.message }}" 34 | - name: commit and push 35 | if: "!contains(steps.log.outputs.message, 'ci skip') && steps.git_diff.outputs.diff != ''" 36 | run: | 37 | echo steps.git 38 | git add -A 39 | git commit -m "run go mod tidy" 40 | git push -u origin HEAD:${GITHUB_REF} 41 | -------------------------------------------------------------------------------- /bps/example_test.go: -------------------------------------------------------------------------------- 1 | package bps_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.mercari.io/go-bps/bps" 7 | ) 8 | 9 | type Bill struct { 10 | Principal int64 11 | Rate *bps.BPS 12 | } 13 | 14 | func NewBill(principal int64, rate *bps.BPS) *Bill { 15 | return &Bill{Principal: principal, Rate: rate} 16 | } 17 | 18 | // CalcInterestFee calculates an interest fee for `b` 19 | func (b *Bill) CalcInterestFee() *bps.BPS { 20 | return b.Rate.Mul(b.Principal) 21 | } 22 | 23 | type Bills []*Bill 24 | 25 | func (bl Bills) CalcInterestFee() *bps.BPS { 26 | var fees []*bps.BPS 27 | for _, b := range bl { 28 | fees = append(fees, b.CalcInterestFee()) 29 | } 30 | return bps.Sum(bps.NewFromAmount(0), fees...) 31 | } 32 | 33 | func Example() { 34 | // principal amount is 14999 35 | var principal int64 = 14999 36 | // interest rate is 8.0% 37 | rate1 := bps.NewFromPercentage(8) 38 | // interest rate is 2.645% = 264.5 basis points = 2645 deci basis points 39 | rate2 := bps.NewFromDeciBasisPoint(2645) 40 | // interest rate is 4.5% 41 | rate3 := bps.MustFromString(".045") 42 | 43 | b1 := NewBill(principal, rate1) 44 | b2 := NewBill(principal, rate2) 45 | b3 := NewBill(principal, rate3) 46 | bills := Bills{b1, b2, b3} 47 | 48 | // interest fee: 14999 * 8% = 1199.92 49 | // Amounts() returns fee amount as integer that's rounded off decimal floating point 50 | fmt.Println(b1.CalcInterestFee().FloatString(2)) 51 | // interest fee: 14999 * 2.645 = 396.72355 52 | fmt.Println(b2.CalcInterestFee().FloatString(2)) 53 | // interest fee: 14999 * 4.5% = 674.955 54 | fmt.Println(b3.CalcInterestFee().FloatString(2)) 55 | // sum interest fees: 1199.92 + 396.72355 + 674.955 = 2271.59855 56 | // not equal 1199 + 396 + 674 = 2269 57 | fmt.Println(bills.CalcInterestFee().FloatString(0)) 58 | // Output: 59 | // 1199.92 60 | // 396.72 61 | // 674.96 62 | // 2272 63 | } 64 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # ref. https://github.com/golangci/golangci-lint/blob/master/.golangci.yml 3 | linters-settings: 4 | govet: 5 | check-shadowing: true 6 | golint: 7 | min-confidence: 0 8 | gocyclo: 9 | min-complexity: 15 10 | maligned: 11 | suggest-new: true 12 | dupl: 13 | threshold: 150 14 | goconst: 15 | min-len: 2 16 | min-occurrences: 2 17 | misspell: 18 | locale: US 19 | lll: 20 | line-length: 140 21 | gocritic: 22 | enabled-tags: 23 | - diagnostic 24 | - experimental 25 | - opinionated 26 | - performance 27 | - style 28 | disabled-checks: 29 | - wrapperFunc 30 | - dupImport # https://github.com/go-critic/go-critic/issues/845 31 | - ifElseChain 32 | - octalLiteral 33 | funlen: 34 | lines: 100 35 | statements: 50 36 | 37 | linters: 38 | disable-all: true 39 | enable: 40 | - bodyclose 41 | - deadcode 42 | - depguard 43 | - dogsled 44 | - dupl 45 | - errcheck 46 | - funlen 47 | - gochecknoinits 48 | - goconst 49 | - gocritic 50 | - gocyclo 51 | - gofmt 52 | - goimports 53 | - golint 54 | - gosec 55 | - gosimple 56 | - govet 57 | - ineffassign 58 | - interfacer 59 | - lll 60 | - misspell 61 | - nakedret 62 | - scopelint 63 | - staticcheck 64 | - structcheck 65 | - stylecheck 66 | - typecheck 67 | - unconvert 68 | - unparam 69 | - unused 70 | - varcheck 71 | - whitespace 72 | 73 | # run: 74 | # skip-dirs: 75 | # - test/testdata_etc 76 | # skip-files: 77 | # - internal/cache/.*_test.go 78 | 79 | issues: 80 | exclude-rules: 81 | - path: ".*_test.go" 82 | linters: 83 | - dupl 84 | 85 | # golangci.com configuration 86 | # https://github.com/golangci/golangci/wiki/Configuration 87 | service: 88 | golangci-lint-version: 1.21.x 89 | -------------------------------------------------------------------------------- /bps/serial.go: -------------------------------------------------------------------------------- 1 | package bps 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "encoding" 7 | "errors" 8 | ) 9 | 10 | // make sure that the *BPS implements some interfaces. 11 | var _ interface { 12 | sql.Scanner 13 | driver.Valuer 14 | encoding.TextUnmarshaler 15 | } = (*BPS)(nil) 16 | 17 | // Scan implements the sql.Scanner interface for database deserialization. 18 | func (b *BPS) Scan(value interface{}) error { 19 | if b == nil { 20 | return errors.New("BPS.Scan: nil receiver") 21 | } 22 | 23 | switch v := value.(type) { 24 | case uint: 25 | s := NewFromBaseUnit(int64(v)) 26 | b.value = s.value 27 | return nil 28 | case uint32: 29 | s := NewFromBaseUnit(int64(v)) 30 | b.value = s.value 31 | return nil 32 | case uint64: 33 | s := NewFromBaseUnit(int64(v)) 34 | b.value = s.value 35 | return nil 36 | case int: 37 | s := NewFromBaseUnit(int64(v)) 38 | b.value = s.value 39 | return nil 40 | case int32: 41 | s := NewFromBaseUnit(int64(v)) 42 | b.value = s.value 43 | return nil 44 | case int64: 45 | s := NewFromBaseUnit(v) 46 | b.value = s.value 47 | return nil 48 | case string: 49 | s, err := NewFromString(v) 50 | if err != nil { 51 | return err 52 | } 53 | b.value = s.value 54 | return nil 55 | } 56 | 57 | return errors.New("BPS.Scan: invalid type, supporting only integer or string") 58 | } 59 | 60 | // Value implements the driver.Valuer interface for database serialization. 61 | func (b *BPS) Value() (driver.Value, error) { 62 | return b.String(), nil 63 | } 64 | 65 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 66 | func (b *BPS) UnmarshalText(text []byte) error { 67 | v := string(text) 68 | if v == "" { 69 | return errors.New("BPS.UnmarshalText: no data") 70 | } 71 | 72 | n, err := NewFromString(v) 73 | if err != nil { 74 | return err 75 | } 76 | b.value = n.value 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /bps/bps_test.go: -------------------------------------------------------------------------------- 1 | package bps_test 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "testing" 7 | 8 | "go.mercari.io/go-bps/bps" 9 | ) 10 | 11 | func TestBPS_String_Default_BaseUnit(t *testing.T) { 12 | t.Log("The default BaseUnit is DeciBasisPoint") 13 | 14 | tests := map[string]struct { 15 | b *bps.BPS 16 | want string 17 | }{ 18 | "1 ppb presents `0` as string": { 19 | bps.NewFromPPB(big.NewInt(1)), 20 | "0", 21 | }, 22 | "1 ppm presents `0` as string": { 23 | bps.NewFromPPM(big.NewInt(1)), 24 | "0", 25 | }, 26 | "1 deci basis point presents `1` as string": { 27 | bps.NewFromDeciBasisPoint(1), 28 | "1", 29 | }, 30 | "1 basis point presents `10` as string": { 31 | bps.NewFromBasisPoint(1), 32 | "10", 33 | }, 34 | "1 percentage presents `1000` as string": { 35 | bps.NewFromPercentage(1), 36 | "1000", 37 | }, 38 | "1 amount presents `100000` as string": { 39 | bps.NewFromAmount(1), 40 | "100000", 41 | }, 42 | "nil presents `0` as string": { 43 | &bps.BPS{}, 44 | "0", 45 | }, 46 | } 47 | for name, tt := range tests { 48 | tt := tt 49 | t.Run(name, func(t *testing.T) { 50 | t.Parallel() 51 | if got := tt.b.String(); got != tt.want { 52 | t.Errorf("BPS.String() = %v, want %v", got, tt.want) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | func ExampleString() { 59 | // backup 60 | u := bps.BaseUnit 61 | // 15% 62 | b := bps.NewFromPercentage(15) 63 | 64 | // The default BaseUnit is DeciBasisPoint, so output as deci basis points 65 | fmt.Println(b) 66 | 67 | // Update BaseUnit to output as basis points 68 | bps.BaseUnit = bps.BasisPoint 69 | fmt.Println(b) 70 | 71 | // Update BaseUnit to output as percentages 72 | bps.BaseUnit = bps.Percentage 73 | fmt.Println(b) 74 | 75 | // Update BaseUnit to output as ppms 76 | bps.BaseUnit = bps.PPM 77 | fmt.Println(b) 78 | 79 | // Update BaseUnit to output as ppms 80 | bps.BaseUnit = bps.PPB 81 | fmt.Println(b) 82 | 83 | // teardown 84 | bps.BaseUnit = u 85 | // Output: 86 | // 15000 87 | // 1500 88 | // 15 89 | // 150000 90 | // 150000000 91 | } 92 | -------------------------------------------------------------------------------- /bps/conv.go: -------------------------------------------------------------------------------- 1 | package bps 2 | 3 | import "math/big" 4 | 5 | // rawValue returns the row value as new big.Int instance 6 | func (b *BPS) rawValue() *big.Int { 7 | s := nilSafe(b) 8 | return new(big.Int).Set(s.value) 9 | } 10 | 11 | // PPBs returns the row value that means PPB. 12 | func (b *BPS) PPBs() *big.Int { 13 | return b.rawValue() 14 | } 15 | 16 | // PPMs returns the row value that means PPM. 17 | func (b *BPS) PPMs() *big.Int { 18 | return b.Div(DenomPPM).rawValue() 19 | } 20 | 21 | // Amounts returns the basis point as an integer amount. 22 | func (b *BPS) Amounts() int64 { 23 | return b.Div(DenomAmount).rawValue().Int64() 24 | } 25 | 26 | // Percentages returns the basis point as an integer percentage count. 27 | func (b *BPS) Percentages() *big.Int { 28 | return b.Div(DenomPercentage).rawValue() 29 | } 30 | 31 | // BasisPoints returns the basis point as an integer basis point count. 32 | func (b *BPS) BasisPoints() *big.Int { 33 | return b.Div(DenomBasisPoint).rawValue() 34 | } 35 | 36 | // HalfBasisPoints returns the basis point as an integer half basis point count. 37 | func (b *BPS) HalfBasisPoints() *big.Int { 38 | return b.Div(DenomHalfBasisPoint).rawValue() 39 | } 40 | 41 | // DeciBasisPoints returns the basis point as an integer half basis point count. 42 | func (b *BPS) DeciBasisPoints() *big.Int { 43 | return b.Div(DenomDeciBasisPoint).rawValue() 44 | } 45 | 46 | // Rat returns a rational number representation of `b`. 47 | func (b *BPS) Rat() *big.Rat { 48 | mul := big.NewInt(DenomAmount) 49 | num := nilSafe(b).value 50 | return new(big.Rat).SetFrac(num, mul) 51 | } 52 | 53 | // Float64 returns the nearest float64 value for `b` and a bool indicating whether f represents `b` exactly. 54 | // If the magnitude of `b` is too large to be represented by a float64, f is an infinity and exact is false. 55 | // The sign of f always matches the sign of `b`, even if f == 0. 56 | func (b *BPS) Float64() (f float64, exact bool) { 57 | return b.Rat().Float64() 58 | } 59 | 60 | // BaseUnitAmounts returns amount representation of BaseUnit as generated. 61 | // That means the effective digits is modifiable by BaseUnit. 62 | func (b *BPS) BaseUnitAmounts() *big.Int { 63 | switch BaseUnit { 64 | case DeciBasisPoint: 65 | return b.DeciBasisPoints() 66 | case HalfBasisPoint: 67 | return b.HalfBasisPoints() 68 | case BasisPoint: 69 | return b.BasisPoints() 70 | case Percentage: 71 | return b.Percentages() 72 | case PPM: 73 | return b.PPMs() 74 | } 75 | // default is PPB 76 | return b.rawValue() 77 | } 78 | 79 | // nilSafe returns zero value when b is nil or b.value is nil to avoid nil error. 80 | func nilSafe(b *BPS) *BPS { 81 | if b == nil { 82 | return zero 83 | } 84 | if b.value == nil { 85 | return zero 86 | } 87 | return b 88 | } 89 | -------------------------------------------------------------------------------- /bps/calc.go: -------------------------------------------------------------------------------- 1 | package bps 2 | 3 | import "math/big" 4 | 5 | // Abs returns the absolute value of the decimal. 6 | func (b *BPS) Abs() *BPS { 7 | s := nilSafe(b) 8 | abs := new(big.Int).Abs(s.value) 9 | return newBPS(abs) 10 | } 11 | 12 | // Neg returns -b. 13 | func (b *BPS) Neg() *BPS { 14 | s := nilSafe(b) 15 | neg := new(big.Int).Neg(s.value) 16 | return newBPS(neg) 17 | } 18 | 19 | // Add returns b + b2. 20 | func (b *BPS) Add(b2 *BPS) *BPS { 21 | added := new(big.Int).Add(b.rawValue(), b2.rawValue()) 22 | return newBPS(added) 23 | } 24 | 25 | // Sub returns b - b2. 26 | func (b *BPS) Sub(b2 *BPS) *BPS { 27 | subbed := new(big.Int).Sub(b.rawValue(), b2.rawValue()) 28 | return newBPS(subbed) 29 | } 30 | 31 | // Mul returns b * i. 32 | func (b *BPS) Mul(i int64) *BPS { 33 | muled := new(big.Int).Mul(b.rawValue(), big.NewInt(i)) 34 | return newBPS(muled) 35 | } 36 | 37 | // Div returns b / i, rounded down to ppm. 38 | func (b *BPS) Div(i int64) *BPS { 39 | dived := new(big.Int).Div(b.rawValue(), big.NewInt(i)) 40 | return newBPS(dived) 41 | } 42 | 43 | func (b *BPS) Cmp(b2 *BPS) int { 44 | return b.rawValue().Cmp(b2.rawValue()) 45 | } 46 | 47 | func (b *BPS) Equal(b2 *BPS) bool { 48 | return b.Cmp(b2) == 0 49 | } 50 | 51 | var zero = &BPS{ 52 | value: big.NewInt(0), 53 | } 54 | 55 | func (b *BPS) IsZero() bool { 56 | return b.Equal(zero) 57 | } 58 | 59 | // FloatString returns the string representation of the amount as generated by *big.Rat.FloatString(prec) 60 | func (b *BPS) FloatString(prec int) string { 61 | return b.Rat().FloatString(prec) 62 | } 63 | 64 | // Avg returns the average value of the provided first and rest BPS 65 | func Avg(first *BPS, rest ...*BPS) *BPS { 66 | count := int64(len(rest) + 1) 67 | sum := Sum(first, rest...) 68 | return sum.Div(count) 69 | } 70 | 71 | // Sum returns the combined total of the provided first and rest BPS 72 | func Sum(first *BPS, rest ...*BPS) *BPS { 73 | total := first 74 | for _, b := range rest { 75 | total = total.Add(b) 76 | } 77 | return total 78 | } 79 | 80 | // Max returns the largest BPS that was passed in the arguments. 81 | // 82 | // To call this function with an array, you must do: 83 | // 84 | // Max(arr[0], arr[1:]...) 85 | // 86 | // This makes it harder to accidentally call Max with 0 arguments. 87 | func Max(first *BPS, rest ...*BPS) *BPS { 88 | max := first 89 | for _, b := range rest { 90 | if b.Cmp(max) > 0 { 91 | max = b 92 | } 93 | } 94 | return max 95 | } 96 | 97 | // Min returns the smallest BPS that was passed in the arguments. 98 | // 99 | // To call this function with an array, you must do: 100 | // 101 | // Min(arr[0], arr[1:]...) 102 | // 103 | // This makes it harder to accidentally call Min with 0 arguments. 104 | func Min(fisrt *BPS, rest ...*BPS) *BPS { 105 | min := fisrt 106 | for _, b := range rest { 107 | if b.Cmp(min) < 0 { 108 | min = b 109 | } 110 | } 111 | return min 112 | } 113 | -------------------------------------------------------------------------------- /bps/construct.go: -------------------------------------------------------------------------------- 1 | package bps 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/big" 7 | "strings" 8 | ) 9 | 10 | // Denominators for each parts 11 | const ( 12 | DenomPPM int64 = 1000 13 | DenomDeciBasisPoint = DenomPPM * 10 14 | DenomHalfBasisPoint = DenomDeciBasisPoint * 5 15 | DenomBasisPoint = DenomHalfBasisPoint * 2 16 | DenomPercentage = DenomBasisPoint * 100 17 | DenomAmount = DenomPercentage * 100 18 | ) 19 | 20 | // NewFromString returns a new BPS from a string representation. 21 | func NewFromString(value string) (*BPS, error) { 22 | var intString string 23 | var mul int64 = 1 24 | 25 | parts := strings.Split(value, ".") 26 | if len(parts) == 1 { 27 | // There is no decimal point, the original string can be just parsed with an int 28 | intString = value 29 | } else if len(parts) == 2 { 30 | // strip the insignificant digits for more accurate comparisons. 31 | decimalPart := strings.TrimRight(parts[1], "0") 32 | intString = parts[0] + decimalPart 33 | if intString == "" && parts[1] != "" { 34 | intString = "0" 35 | } 36 | expInt := len(decimalPart) 37 | mul = int64(math.Pow10(expInt)) 38 | } else { 39 | return nil, fmt.Errorf("can't convert %s to BPS: too many .s", value) 40 | } 41 | 42 | parsed, ok := new(big.Int).SetString(intString, 10) 43 | if !ok { 44 | return nil, fmt.Errorf("can't convert %s to BPS", value) 45 | } 46 | 47 | return newBPS(parsed).Mul(DenomAmount).Div(mul), nil 48 | } 49 | 50 | // MustFromString returns a new BPS from a string representation or panics if NewFromString would have returned an error. 51 | func MustFromString(value string) *BPS { 52 | b, err := NewFromString(value) 53 | if err != nil { 54 | panic(err) 55 | } 56 | return b 57 | } 58 | 59 | // NewFromPPB makes new BPS instance from part per billion(ppb) 60 | func NewFromPPB(ppb *big.Int) *BPS { 61 | return newBPS(ppb) 62 | } 63 | 64 | // NewFromPPM makes new BPS instance from part per million(ppm) 65 | func NewFromPPM(ppm *big.Int) *BPS { 66 | return newBPS(ppm).Mul(DenomPPM) 67 | } 68 | 69 | // NewFromDeciBasisPoint makes new BPS instance from deci basis point 70 | func NewFromDeciBasisPoint(deci int64) *BPS { 71 | return newBPS(big.NewInt(deci)).Mul(DenomDeciBasisPoint) 72 | } 73 | 74 | // NewFromHalfBasisPoint makes new BPS instance from half basis point 75 | func NewFromHalfBasisPoint(bp int64) *BPS { 76 | return newBPS(big.NewInt(bp)).Mul(DenomHalfBasisPoint) 77 | } 78 | 79 | // NewFromBasisPoint makes new BPS instance from basis point 80 | func NewFromBasisPoint(bp int64) *BPS { 81 | return newBPS(big.NewInt(bp)).Mul(DenomBasisPoint) 82 | } 83 | 84 | // NewFromPercentage makes new BPS instance from percentage 85 | func NewFromPercentage(per int64) *BPS { 86 | return newBPS(big.NewInt(per)).Mul(DenomPercentage) 87 | } 88 | 89 | // NewFromAmount makes new BPS instance from real amount 90 | func NewFromAmount(amt int64) *BPS { 91 | return newBPS(big.NewInt(amt)).Mul(DenomAmount) 92 | } 93 | 94 | // NewFromBaseUnit makes new BPS instance from BaseUnit value. 95 | // That means the effective digits is modifiable by BaseUnit. 96 | func NewFromBaseUnit(v int64) *BPS { 97 | switch BaseUnit { 98 | case DeciBasisPoint: 99 | return NewFromDeciBasisPoint(v) 100 | case HalfBasisPoint: 101 | return NewFromHalfBasisPoint(v) 102 | case BasisPoint: 103 | return NewFromBasisPoint(v) 104 | case Percentage: 105 | return NewFromPercentage(v) 106 | case PPM: 107 | return NewFromPPM(big.NewInt(v)) 108 | } 109 | // The default unit is PPB 110 | return NewFromPPB(big.NewInt(v)) 111 | } 112 | 113 | func newBPS(value *big.Int) *BPS { 114 | if value == nil { 115 | value = big.NewInt(0) 116 | } 117 | return &BPS{ 118 | value: new(big.Int).Set(value), 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-bps 2 | 3 | [![pkg.go.dev][pkg.go.dev-badge]][pkg.go.dev] 4 | [![Test][test-badge]][test] 5 | [![reviewdog][reviewdog-badge]][reviewdog] 6 | [![Releases][release-badge]][release] 7 | 8 | `go-bps`is a Go package to operate the basis point. 9 | Handling floating point numbers in programming causes rounding errors. 10 | To avoid this, all numerical calculations are done using basis points (integer only) in this package. 11 | 12 | ## What's Basis Point 13 | 14 | > A per ten thousand sign or basis point (often denoted as bp, often pronounced as "bip" or "beep") is (a difference of) one hundredth of a percent or equivalently one ten thousandth. The related concept of a permyriad is literally one part per ten thousand. Figures are commonly quoted in basis points in finance, especially in fixed income markets. 15 | 16 | [from Wikipedia](https://en.wikipedia.org/wiki/Basis_point) 17 | 18 | One part per million(ppm) is used as the minimum unit for basis points on this package. 19 | 20 | ``` 21 | 1 ppm = 0.01 basis points = 0.0001 % 22 | ``` 23 | 24 | ## Example 25 | 26 | ```go 27 | package main 28 | 29 | import ( 30 | "fmt" 31 | 32 | "go.mercari.io/go-bps/bps" 33 | ) 34 | 35 | type Bill struct { 36 | Principal int64 37 | Rate *bps.BPS 38 | } 39 | 40 | func NewBill(principal int64, rate *bps.BPS) *Bill { 41 | return &Bill{Principal: principal, Rate: rate} 42 | } 43 | 44 | // CalcInterestFee calculates an interest fee for `b` 45 | func (b *Bill) CalcInterestFee() *bps.BPS { 46 | return b.Rate.Mul(b.Principal) 47 | } 48 | 49 | type Bills []*Bill 50 | 51 | func (bl Bills) CalcInterestFee() *bps.BPS { 52 | var fees []*bps.BPS 53 | for _, b := range bl { 54 | fees = append(fees, b.CalcInterestFee()) 55 | } 56 | return bps.Sum(bps.NewFromAmount(0), fees...) 57 | } 58 | 59 | func main() { 60 | // principal amount is 14999 61 | var principal int64 = 14999 62 | // interest rate is 8.0% 63 | rate1 := bps.NewFromPercentage(8) 64 | // interest rate is 2.645% = 264.5 basis points = 2645 deci basis points 65 | rate2 := bps.NewFromDeciBasisPoint(2645) 66 | // interest rate is 0.045 = 4.5% 67 | rate3 := bps.MustFromString(".045") 68 | 69 | b1 := NewBill(principal, rate1) 70 | b2 := NewBill(principal, rate2) 71 | b3 := NewBill(principal, rate3) 72 | bills := Bills{b1, b2, b3} 73 | 74 | // interest fee: 14999 * 8% = 1199.92 75 | // Amounts() returns fee amount as integer that's rounded off decimal floating point 76 | fmt.Println(b1.CalcInterestFee().Amounts()) 77 | // interest fee: 14999 * 2.645 = 396.72355 78 | fmt.Println(b2.CalcInterestFee().Amounts()) 79 | // interest fee: 14999 * 4.5% = 674.955 80 | fmt.Println(b3.CalcInterestFee().Amounts()) 81 | // sum interest fees: 1199.92 + 396.72355 + 674.955 = 2271.59855 82 | // not equal 1199 + 396 + 674 = 2269 83 | fmt.Println(bills.CalcInterestFee().Amounts()) 84 | // Output: 85 | // 1198 86 | // 396 87 | // 674 88 | // 2271 89 | } 90 | ``` 91 | 92 | ## References 93 | 94 | - [Basis point \- Wikipedia](https://en.wikipedia.org/wiki/Basis_point) 95 | - [Parts\-per notation \- Wikipedia](https://en.wikipedia.org/wiki/Parts-per_notation) 96 | 97 | 98 | ## Commiters 99 | 100 | - Motonori IWATA([@iwata](https://github.com/iwata)) 101 | 102 | ## Contribution 103 | 104 | Please read the CLA below carefully before submitting your contribution. 105 | 106 | https://www.mercari.com/cla/ 107 | 108 | ## License 109 | 110 | Copyright 2020 Mercari, Inc. 111 | 112 | Licensed under the MIT License. 113 | 114 | 115 | [test]: https://github.com/mercari/go-bps/actions?query=workflow%3A%22test+and+coverage%22 116 | [reviewdog]: https://github.com/mercari/go-bps/actions?query=workflow%3Areviewdog 117 | [pkg.go.dev]: https://pkg.go.dev/go.mercari.io/go-bps 118 | [release]: https://github.com/mercari/go-bps/releases/latest 119 | 120 | [test-badge]: https://github.com/mercari/go-bps/workflows/test%20and%20coverage/badge.svg 121 | [reviewdog-badge]: https://github.com/mercari/go-bps/workflows/reviewdog/badge.svg 122 | [pkg.go.dev-badge]: https://pkg.go.dev/badge/go.mercari.io/go-bps 123 | [release-badge]: https://img.shields.io/github/release/mercari/go-bps.svg?style=flat&logo=github 124 | -------------------------------------------------------------------------------- /bps/serial_test.go: -------------------------------------------------------------------------------- 1 | package bps_test 2 | 3 | import ( 4 | "database/sql/driver" 5 | "reflect" 6 | "testing" 7 | 8 | "go.mercari.io/go-bps/bps" 9 | ) 10 | 11 | func TestBPS_Value(t *testing.T) { 12 | tests := map[string]struct { 13 | b *bps.BPS 14 | want driver.Value 15 | }{ 16 | "zero": { 17 | bps.NewFromAmount(0), 18 | "0", 19 | }, 20 | "1 amount": { 21 | bps.NewFromAmount(1), 22 | "100000", 23 | }, 24 | } 25 | for name, tt := range tests { 26 | tt := tt 27 | t.Run(name, func(t *testing.T) { 28 | got, err := tt.b.Value() 29 | if err != nil { 30 | t.Errorf("BPS.Value() error = %v", err) 31 | return 32 | } 33 | if !reflect.DeepEqual(got, tt.want) { 34 | t.Errorf("BPS.Value() = %v, want %v", got, tt.want) 35 | } 36 | }) 37 | } 38 | } 39 | 40 | func TestBPS_Scan(t *testing.T) { 41 | tests := map[string]struct { 42 | b *bps.BPS 43 | value interface{} 44 | want *bps.BPS 45 | wantErr bool 46 | }{ 47 | "If b is nil, it should return an error": { 48 | nil, 49 | "fake", 50 | nil, 51 | true, 52 | }, 53 | "If value is uint, it should set value as DeciBasisPoint": { 54 | &bps.BPS{}, 55 | uint(5), 56 | bps.NewFromDeciBasisPoint(5), 57 | false, 58 | }, 59 | "If value is uint32, it should set value as DeciBasisPoint": { 60 | &bps.BPS{}, 61 | uint32(6), 62 | bps.NewFromDeciBasisPoint(6), 63 | false, 64 | }, 65 | "If value is uint64, it should set value as DeciBasisPoint": { 66 | &bps.BPS{}, 67 | uint64(7), 68 | bps.NewFromDeciBasisPoint(7), 69 | false, 70 | }, 71 | "If value is int, it should set value as DeciBasisPoint": { 72 | &bps.BPS{}, 73 | int(6), 74 | bps.NewFromDeciBasisPoint(6), 75 | false, 76 | }, 77 | "If value is int32, it should set value as DeciBasisPoint": { 78 | &bps.BPS{}, 79 | int32(7), 80 | bps.NewFromDeciBasisPoint(7), 81 | false, 82 | }, 83 | "If value is int64, it should set value as DeciBasisPoint": { 84 | &bps.BPS{}, 85 | int64(8), 86 | bps.NewFromDeciBasisPoint(8), 87 | false, 88 | }, 89 | "If value is valid string, it should set value via NewFromString": { 90 | &bps.BPS{}, 91 | ".15", 92 | bps.NewFromPercentage(15), 93 | false, 94 | }, 95 | "If value is invalid string, it should return an error": { 96 | &bps.BPS{}, 97 | "a15", 98 | &bps.BPS{}, 99 | true, 100 | }, 101 | "If value is float, it should return an error": { 102 | &bps.BPS{}, 103 | .5, 104 | &bps.BPS{}, 105 | true, 106 | }, 107 | } 108 | for name, tt := range tests { 109 | tt := tt 110 | t.Run(name, func(t *testing.T) { 111 | t.Parallel() 112 | if err := tt.b.Scan(tt.value); (err != nil) != tt.wantErr { 113 | t.Errorf("BPS.Scan() error = %v, wantErr %v", err, tt.wantErr) 114 | } 115 | if !reflect.DeepEqual(tt.b, tt.want) { 116 | t.Errorf("BPS.Value() = %v, want %v", tt.b, tt.want) 117 | } 118 | }) 119 | } 120 | } 121 | 122 | func TestBPS_UnmarshalText(t *testing.T) { 123 | t.Parallel() 124 | tests := map[string]struct { 125 | value string 126 | want *bps.BPS 127 | wantErr bool 128 | }{ 129 | "If value is valid string with the integer part abbreviating, it should set value via NewFromString": { 130 | ".15", 131 | bps.NewFromPercentage(15), 132 | false, 133 | }, 134 | "If value is valid string, it should set value via NewFromString": { 135 | "1.15", 136 | bps.NewFromPercentage(115), 137 | false, 138 | }, 139 | "If value is negative, it should set value via NewFromString": { 140 | "-123.456", 141 | bps.NewFromBasisPoint(-1234560), 142 | false, 143 | }, 144 | "If value is zero, it should set value via NewFromString": { 145 | "0", 146 | bps.NewFromAmount(0), 147 | false, 148 | }, 149 | "If value is invalid string, it should return an error": { 150 | "a15", 151 | &bps.BPS{}, 152 | true, 153 | }, 154 | "If value is empty, it should return an error": { 155 | "", 156 | &bps.BPS{}, 157 | true, 158 | }, 159 | } 160 | for name, tt := range tests { 161 | tt := tt 162 | t.Run(name, func(t *testing.T) { 163 | t.Parallel() 164 | b := &bps.BPS{} 165 | if err := b.UnmarshalText([]byte(tt.value)); (err != nil) != tt.wantErr { 166 | t.Errorf("BPS.UnmarshalText() error = %v, wantErr %v", err, tt.wantErr) 167 | } 168 | if !reflect.DeepEqual(b, tt.want) { 169 | t.Errorf("BPS.Value() = %v, want %v", b, tt.want) 170 | } 171 | }) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /bps/construct_test.go: -------------------------------------------------------------------------------- 1 | package bps_test 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "reflect" 7 | "testing" 8 | 9 | "go.mercari.io/go-bps/bps" 10 | ) 11 | 12 | func TestOneAmountEquality(t *testing.T) { 13 | t.Parallel() 14 | 15 | oneAmt := bps.NewFromAmount(1) 16 | 17 | per := bps.NewFromPercentage(100) 18 | if !oneAmt.Equal(per) { 19 | t.Error("1 amount = 100%") 20 | } 21 | 22 | bp := bps.NewFromBasisPoint(10000) 23 | if !oneAmt.Equal(bp) { 24 | t.Error("1 amount = 10,000 basis points") 25 | } 26 | 27 | dbp := bps.NewFromDeciBasisPoint(100000) 28 | if !oneAmt.Equal(dbp) { 29 | t.Error("1 amount = 100,000 deci basis points") 30 | } 31 | 32 | ppm := bps.NewFromPPM(big.NewInt(1000000)) 33 | if !oneAmt.Equal(ppm) { 34 | t.Error("1 amount = 1000,000 ppm") 35 | } 36 | 37 | ppb := bps.NewFromPPB(big.NewInt(1000000000)) 38 | if !oneAmt.Equal(ppb) { 39 | t.Error("1 amount = 1000,000,000 ppb") 40 | } 41 | } 42 | 43 | func TestNewFromString(t *testing.T) { 44 | tests := map[string]struct { 45 | arg string 46 | want *bps.BPS 47 | wantErr bool 48 | }{ 49 | "int part and decimal part": { 50 | "123.456", 51 | bps.NewFromBasisPoint(1234560), 52 | false, 53 | }, 54 | "only int part": { 55 | "123", 56 | bps.NewFromBasisPoint(1230000), 57 | false, 58 | }, 59 | "only decimal part": { 60 | ".1234", 61 | bps.NewFromBasisPoint(1234), 62 | false, 63 | }, 64 | "negative value": { 65 | "-123.456", 66 | bps.NewFromBasisPoint(-1234560), 67 | false, 68 | }, 69 | "zero": { 70 | "0.0", 71 | bps.NewFromAmount(0), 72 | false, 73 | }, 74 | "short zero": { 75 | ".0", 76 | bps.NewFromAmount(0), 77 | false, 78 | }, 79 | "If include multi dots, it should return an error": { 80 | "123.45.6", 81 | nil, 82 | true, 83 | }, 84 | "If base 2 format, it should return an error": { 85 | "0b11", 86 | nil, 87 | true, 88 | }, 89 | "If base 8 format, it should return an error": { 90 | "0o75", 91 | nil, 92 | true, 93 | }, 94 | "If base 16 format, it should return an error": { 95 | "0xF5", 96 | nil, 97 | true, 98 | }, 99 | } 100 | for name, tt := range tests { 101 | tt := tt 102 | t.Run(name, func(t *testing.T) { 103 | t.Parallel() 104 | got, err := bps.NewFromString(tt.arg) 105 | if (err != nil) != tt.wantErr { 106 | t.Errorf("NewFromString() error = %v, wantErr %v", err, tt.wantErr) 107 | return 108 | } 109 | if !reflect.DeepEqual(got, tt.want) { 110 | t.Errorf("NewFromString() = %v, want %v", got, tt.want) 111 | } 112 | }) 113 | } 114 | } 115 | 116 | func TestMustFromString(t *testing.T) { 117 | tests := map[string]struct { 118 | arg string 119 | want *bps.BPS 120 | wantPanic bool 121 | }{ 122 | "int part and decimal part": { 123 | "123.456", 124 | bps.NewFromBasisPoint(1234560), 125 | false, 126 | }, 127 | "only int part": { 128 | "123", 129 | bps.NewFromBasisPoint(1230000), 130 | false, 131 | }, 132 | "only decimal part": { 133 | ".1234", 134 | bps.NewFromBasisPoint(1234), 135 | false, 136 | }, 137 | "negative value": { 138 | "-123.456", 139 | bps.NewFromBasisPoint(-1234560), 140 | false, 141 | }, 142 | "zero": { 143 | "0.0", 144 | bps.NewFromAmount(0), 145 | false, 146 | }, 147 | "short zero": { 148 | ".0", 149 | bps.NewFromAmount(0), 150 | false, 151 | }, 152 | "If include multi dots, it should return an error": { 153 | "123.45.6", 154 | nil, 155 | true, 156 | }, 157 | "If base 2 format, it should return an error": { 158 | "0b11", 159 | nil, 160 | true, 161 | }, 162 | "If base 8 format, it should return an error": { 163 | "0o75", 164 | nil, 165 | true, 166 | }, 167 | "If base 16 format, it should return an error": { 168 | "0xF5", 169 | nil, 170 | true, 171 | }, 172 | } 173 | for name, tt := range tests { 174 | tt := tt 175 | t.Run(name, func(t *testing.T) { 176 | t.Parallel() 177 | if tt.wantPanic { 178 | //nolint:gocritic 179 | defer func() { 180 | err := recover() 181 | if err == nil { 182 | t.Error("MustFromString() should occure a panic") 183 | } 184 | }() 185 | } 186 | got := bps.MustFromString(tt.arg) 187 | if !reflect.DeepEqual(got, tt.want) { 188 | t.Errorf("MustFromString() = %v, want %v", got, tt.want) 189 | } 190 | }) 191 | } 192 | } 193 | 194 | func ExampleNewFromString() { 195 | // 15% 196 | b1, _ := bps.NewFromString("0.15") 197 | fmt.Println(b1.Percentages()) 198 | // 2.645% = 264.5 basis points 199 | b2, _ := bps.NewFromString("0.02645") 200 | fmt.Println(b2.DeciBasisPoints()) 201 | // Output: 202 | // 15 203 | // 2645 204 | } 205 | 206 | func ExampleMustFromString() { 207 | // 15% 208 | b1 := bps.MustFromString("0.15") 209 | fmt.Println(b1.Percentages()) 210 | // 2.645% = 264.5 basis points 211 | b2 := bps.MustFromString("0.02645") 212 | fmt.Println(b2.DeciBasisPoints()) 213 | 214 | a := bps.NewFromAmount(1e12) 215 | b, _ := bps.NewFromString(".000001") // 1 / 1e6 216 | // Set PPM as BaseUnit to show value as ppm 217 | bps.BaseUnit = bps.PPM 218 | fmt.Println(a.Add(b), "ppm") 219 | // teardown 220 | bps.BaseUnit = bps.DeciBasisPoint 221 | 222 | n := bps.NewFromAmount(0) 223 | for i := 0; i < 1000; i++ { 224 | n = n.Add(bps.MustFromString(".01")) 225 | } 226 | fmt.Println(n.Amounts()) 227 | // Output: 228 | // 15 229 | // 2645 230 | // 1000000000000000001 ppm 231 | // 10 232 | } 233 | 234 | func ExampleNewFromBaseUnit() { 235 | // backup 236 | u := bps.BaseUnit 237 | var arg int64 = 15 238 | 239 | // The default BaseUnit is DeciBasisPoint 240 | deci := bps.NewFromBaseUnit(arg) 241 | fmt.Println(deci.PPMs()) 242 | 243 | // BaseUnit is updated by PPB 244 | bps.BaseUnit = bps.PPB 245 | ppb := bps.NewFromBaseUnit(arg) 246 | fmt.Println(ppb.PPBs()) 247 | 248 | // BaseUnit is updated by PPM 249 | bps.BaseUnit = bps.PPM 250 | ppm := bps.NewFromBaseUnit(arg) 251 | fmt.Println(ppm.PPBs()) 252 | 253 | // BaseUnit is updated by HalfBasisPoint 254 | bps.BaseUnit = bps.HalfBasisPoint 255 | hbp := bps.NewFromBaseUnit(arg) 256 | fmt.Println(hbp.PPBs()) 257 | 258 | // BaseUnit is updated by BasisPoint 259 | bps.BaseUnit = bps.BasisPoint 260 | bp := bps.NewFromBaseUnit(arg) 261 | fmt.Println(bp.PPBs()) 262 | 263 | // BaseUnit is updated by Percentage 264 | bps.BaseUnit = bps.Percentage 265 | p := bps.NewFromBaseUnit(arg) 266 | fmt.Println(p.PPBs()) 267 | 268 | // teardown 269 | bps.BaseUnit = u 270 | // Output: 271 | // 150 272 | // 15 273 | // 15000 274 | // 750000 275 | // 1500000 276 | // 150000000 277 | } 278 | -------------------------------------------------------------------------------- /bps/conv_test.go: -------------------------------------------------------------------------------- 1 | package bps_test 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "reflect" 7 | "testing" 8 | 9 | "go.mercari.io/go-bps/bps" 10 | ) 11 | 12 | func TestBPS_Amounts(t *testing.T) { 13 | tests := map[string]struct { 14 | ppb int64 15 | want int64 16 | }{ 17 | "1,000,000,000 ppbs equals 1 amount": { 18 | 1000000000, 19 | 1, 20 | }, 21 | "1,999,999,999 ppbs equals 1 amount, round off fractions less than 100,000,000 ppbs": { 22 | 1999999999, 23 | 1, 24 | }, 25 | "2,000,000,000 ppbs equals 2 amounts": { 26 | 2000000000, 27 | 2, 28 | }, 29 | "999,999,999 ppbs equals zero amounts": { 30 | 999999999, 31 | 0, 32 | }, 33 | } 34 | for name, tt := range tests { 35 | tt := tt 36 | t.Run(name, func(t *testing.T) { 37 | t.Parallel() 38 | b := bps.NewFromPPB(big.NewInt(tt.ppb)) 39 | if got := b.Amounts(); got != tt.want { 40 | t.Errorf("BPS.Amounts() = %v, want %v", got, tt.want) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func TestBPS_Percentages(t *testing.T) { 47 | tests := map[string]struct { 48 | ppb *big.Int 49 | want *big.Int 50 | }{ 51 | "1,000,000,000 ppbs equals 100 percentages": { 52 | big.NewInt(1000000000), 53 | big.NewInt(100), 54 | }, 55 | "1,009,999,999 ppbs equals 100 percentages, round off fractions less than 10,000,000 ppbs": { 56 | big.NewInt(1009999999), 57 | big.NewInt(100), 58 | }, 59 | "1,010,000,000 ppbs equals 101 percentages": { 60 | big.NewInt(1010000000), 61 | big.NewInt(101), 62 | }, 63 | "10,000,000 ppbs equals 1 percentage": { 64 | big.NewInt(10000000), 65 | big.NewInt(1), 66 | }, 67 | "9,999,999 ppbs equals zero percentage": { 68 | big.NewInt(999999), 69 | big.NewInt(0), 70 | }, 71 | } 72 | for name, tt := range tests { 73 | tt := tt 74 | t.Run(name, func(t *testing.T) { 75 | t.Parallel() 76 | b := bps.NewFromPPB(tt.ppb) 77 | if got := b.Percentages(); !reflect.DeepEqual(got, tt.want) { 78 | t.Errorf("BPS.Percentages() = %v, want %v", got, tt.want) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestBPS_BasisPoints(t *testing.T) { 85 | tests := map[string]struct { 86 | ppb *big.Int 87 | want *big.Int 88 | }{ 89 | "1,000,000,000 ppbs equals 10,000 basis points": { 90 | big.NewInt(1000000000), 91 | big.NewInt(10000), 92 | }, 93 | "1,000,099,999 ppbs equals 10,000 basis points, round off fractions less than 1,000,000 ppbs": { 94 | big.NewInt(1000099999), 95 | big.NewInt(10000), 96 | }, 97 | "1,001,000,000 ppbs equals 10,001 basis points": { 98 | big.NewInt(1000100000), 99 | big.NewInt(10001), 100 | }, 101 | "99,999 ppbs equals zero basis points": { 102 | big.NewInt(99999), 103 | big.NewInt(0), 104 | }, 105 | } 106 | for name, tt := range tests { 107 | tt := tt 108 | t.Run(name, func(t *testing.T) { 109 | t.Parallel() 110 | b := bps.NewFromPPB(tt.ppb) 111 | if got := b.BasisPoints(); !reflect.DeepEqual(got, tt.want) { 112 | t.Errorf("BPS.BasisPoints() = %v, want %v", got, tt.want) 113 | } 114 | }) 115 | } 116 | } 117 | 118 | func TestBPS_HalfBasisPoints(t *testing.T) { 119 | tests := map[string]struct { 120 | ppb *big.Int 121 | want *big.Int 122 | }{ 123 | "1,000,000,000 ppbs equals 20,000 half basis points": { 124 | big.NewInt(1000000000), 125 | big.NewInt(20000), 126 | }, 127 | "1,000,049,999 ppbs equals 20,000 half basis points, round off fractions less than 50,000 ppbs": { 128 | big.NewInt(1000049999), 129 | big.NewInt(20000), 130 | }, 131 | "1,000,050,000 ppbs equals 20,001 half basis points": { 132 | big.NewInt(1000050000), 133 | big.NewInt(20001), 134 | }, 135 | "49,999 ppbs equals zero half basis points": { 136 | big.NewInt(49999), 137 | big.NewInt(0), 138 | }, 139 | } 140 | for name, tt := range tests { 141 | tt := tt 142 | t.Run(name, func(t *testing.T) { 143 | t.Parallel() 144 | b := bps.NewFromPPB(tt.ppb) 145 | if got := b.HalfBasisPoints(); !reflect.DeepEqual(got, tt.want) { 146 | t.Errorf("BPS.HalfBasisPoints() = %v, want %v", got, tt.want) 147 | } 148 | }) 149 | } 150 | } 151 | 152 | func TestBPS_DeciBasisPoints(t *testing.T) { 153 | tests := map[string]struct { 154 | ppb *big.Int 155 | want *big.Int 156 | }{ 157 | "1,000,000,000 ppbs equals 100,000 deci basis points": { 158 | big.NewInt(1000000000), 159 | big.NewInt(100000), 160 | }, 161 | "1,000,009,999 ppbs equals 100,000 deci basis points, round off fractions less than 10,000 ppbs": { 162 | big.NewInt(1000009999), 163 | big.NewInt(100000), 164 | }, 165 | "1,000,010,000 ppbs equals 100,001 deci basis points": { 166 | big.NewInt(1000010000), 167 | big.NewInt(100001), 168 | }, 169 | "9,999 ppbs equals zero deci basis points": { 170 | big.NewInt(9999), 171 | big.NewInt(0), 172 | }, 173 | } 174 | for name, tt := range tests { 175 | tt := tt 176 | t.Run(name, func(t *testing.T) { 177 | t.Parallel() 178 | b := bps.NewFromPPB(tt.ppb) 179 | if got := b.DeciBasisPoints(); !reflect.DeepEqual(got, tt.want) { 180 | t.Errorf("BPS.DeciBasisPoints() = %v, want %v", got, tt.want) 181 | } 182 | }) 183 | } 184 | } 185 | 186 | func TestBPS_PPMs(t *testing.T) { 187 | tests := map[string]struct { 188 | ppb *big.Int 189 | want *big.Int 190 | }{ 191 | "1000,000 ppbs equals 1,000 ppms": { 192 | big.NewInt(1000000), 193 | big.NewInt(1000), 194 | }, 195 | "1000 ppbs equals 1 ppms": { 196 | big.NewInt(1000), 197 | big.NewInt(1), 198 | }, 199 | "1,999 ppbs equals 1 ppms, round off fractions less than 1,000 ppbs": { 200 | big.NewInt(1999), 201 | big.NewInt(1), 202 | }, 203 | "2,001 ppbs equals 2 ppms": { 204 | big.NewInt(2001), 205 | big.NewInt(2), 206 | }, 207 | "999 ppbs equals 0 ppms": { 208 | big.NewInt(999), 209 | big.NewInt(0), 210 | }, 211 | } 212 | for name, tt := range tests { 213 | tt := tt 214 | t.Run(name, func(t *testing.T) { 215 | t.Parallel() 216 | b := bps.NewFromPPB(tt.ppb) 217 | if got := b.PPMs(); !reflect.DeepEqual(got, tt.want) { 218 | t.Errorf("BPS.PPMs() = %v, want %v", got, tt.want) 219 | } 220 | }) 221 | } 222 | } 223 | 224 | func TestBPS_PPBs(t *testing.T) { 225 | tests := map[string]struct { 226 | ppb *big.Int 227 | want *big.Int 228 | }{ 229 | "1,000 ppbs": { 230 | big.NewInt(1000), 231 | big.NewInt(1000), 232 | }, 233 | "1 ppbs": { 234 | big.NewInt(1), 235 | big.NewInt(1), 236 | }, 237 | "5 ppbs": { 238 | big.NewInt(5), 239 | big.NewInt(5), 240 | }, 241 | "nil equal 0 ppbs": { 242 | nil, 243 | big.NewInt(0), 244 | }, 245 | } 246 | for name, tt := range tests { 247 | tt := tt 248 | t.Run(name, func(t *testing.T) { 249 | t.Parallel() 250 | b := bps.NewFromPPB(tt.ppb) 251 | if got := b.PPBs(); !reflect.DeepEqual(got, tt.want) { 252 | t.Errorf("BPS.PPMs() = %v, want %v", got, tt.want) 253 | } 254 | }) 255 | } 256 | } 257 | 258 | func TestBPS_Rat(t *testing.T) { 259 | tests := map[string]struct { 260 | b *bps.BPS 261 | want *big.Rat 262 | }{ 263 | "10 ppbs = 1 / 100,000,000": { 264 | bps.NewFromPPB(big.NewInt(10)), 265 | big.NewRat(1, 100000000), 266 | }, 267 | "10 ppms = 1 / 100,000": { 268 | bps.NewFromPPM(big.NewInt(10)), 269 | big.NewRat(1, 100000), 270 | }, 271 | "8 deci basis points = 8 / 100,000": { 272 | bps.NewFromDeciBasisPoint(8), 273 | big.NewRat(8, 100000), 274 | }, 275 | "5 basis points = 5 / 10,000": { 276 | bps.NewFromBasisPoint(5), 277 | big.NewRat(5, 10000), 278 | }, 279 | "5 basis points = 1 / 2,000": { 280 | bps.NewFromBasisPoint(5), 281 | big.NewRat(1, 2000), 282 | }, 283 | "20 percentages = 1 / 5": { 284 | bps.NewFromPercentage(20), 285 | big.NewRat(1, 5), 286 | }, 287 | "3 amounts = 3 / 1": { 288 | bps.NewFromAmount(3), 289 | big.NewRat(3, 1), 290 | }, 291 | "nil = 0": { 292 | &bps.BPS{}, 293 | big.NewRat(0, 1), 294 | }, 295 | } 296 | for name, tt := range tests { 297 | tt := tt 298 | t.Run(name, func(t *testing.T) { 299 | t.Parallel() 300 | if got := tt.b.Rat(); got.Cmp(tt.want) != 0 { 301 | t.Errorf("BPS.Rat() = %v, want %v", got, tt.want) 302 | } 303 | }) 304 | } 305 | } 306 | 307 | func TestBPS_Float64(t *testing.T) { 308 | tests := map[string]struct { 309 | b *bps.BPS 310 | wantF float64 311 | wantExact bool 312 | }{ 313 | "1 / 4 can represent as float value exactly": { 314 | bps.NewFromAmount(1).Div(4), 315 | .25, 316 | true, 317 | }, 318 | "1 / 3 cannot represent as float value exactly": { 319 | bps.NewFromAmount(1).Div(3), 320 | .333333333, 321 | false, 322 | }, 323 | } 324 | for name, tt := range tests { 325 | tt := tt 326 | t.Run(name, func(t *testing.T) { 327 | t.Parallel() 328 | gotF, gotExact := tt.b.Float64() 329 | if gotF != tt.wantF { 330 | t.Errorf("BPS.Float64() gotF = %v, want %v", gotF, tt.wantF) 331 | } 332 | if gotExact != tt.wantExact { 333 | t.Errorf("BPS.Float64() gotExact = %v, want %v", gotExact, tt.wantExact) 334 | } 335 | }) 336 | } 337 | } 338 | 339 | func ExampleBPS_BaseUnitAmounts() { 340 | // backup 341 | u := bps.BaseUnit 342 | // 15% 343 | b := bps.NewFromPercentage(15) 344 | 345 | // The default BaseUnit is DeciBasisPoint 346 | fmt.Println(b.BaseUnitAmounts()) 347 | 348 | // BaseUnit is updated by PPB 349 | bps.BaseUnit = bps.PPB 350 | fmt.Println(b.BaseUnitAmounts()) 351 | 352 | // BaseUnit is updated by PPM 353 | bps.BaseUnit = bps.PPM 354 | fmt.Println(b.BaseUnitAmounts()) 355 | 356 | // BaseUnit is updated by HalfBasisPoint 357 | bps.BaseUnit = bps.HalfBasisPoint 358 | fmt.Println(b.BaseUnitAmounts()) 359 | 360 | // BaseUnit is updated by BasisPoint 361 | bps.BaseUnit = bps.BasisPoint 362 | fmt.Println(b.BaseUnitAmounts()) 363 | 364 | // BaseUnit is updated by Percentage 365 | bps.BaseUnit = bps.Percentage 366 | fmt.Println(b.BaseUnitAmounts()) 367 | 368 | // teardown 369 | bps.BaseUnit = u 370 | // Output: 371 | // 15000 372 | // 150000000 373 | // 150000 374 | // 3000 375 | // 1500 376 | // 15 377 | } 378 | -------------------------------------------------------------------------------- /bps/calc_test.go: -------------------------------------------------------------------------------- 1 | package bps_test 2 | 3 | import ( 4 | "math/big" 5 | "reflect" 6 | "testing" 7 | 8 | "go.mercari.io/go-bps/bps" 9 | ) 10 | 11 | func TestBPS_Add(t *testing.T) { 12 | tests := map[string]struct { 13 | b *bps.BPS 14 | b2 *bps.BPS 15 | want *bps.BPS 16 | }{ 17 | "1 basis point + 1 percentage = 10,100 ppms": { 18 | bps.NewFromBasisPoint(1), 19 | bps.NewFromPercentage(1), 20 | bps.NewFromPPM(big.NewInt(10100)), 21 | }, 22 | "50 ppms + 1 deci basis point = 60 ppms": { 23 | bps.NewFromPPM(big.NewInt(50)), 24 | bps.NewFromDeciBasisPoint(1), 25 | bps.NewFromPPM(big.NewInt(60)), 26 | }, 27 | "1 deci basis point + (-1) basis point = -90 ppms": { 28 | bps.NewFromDeciBasisPoint(1), 29 | bps.NewFromBasisPoint(-1), 30 | bps.NewFromPPM(big.NewInt(-90)), 31 | }, 32 | "nil + 1 ppm = 1 ppms": { 33 | &bps.BPS{}, 34 | bps.NewFromPPM(big.NewInt(1)), 35 | bps.NewFromPPM(big.NewInt(1)), 36 | }, 37 | "1 ppm + nil = 1 ppms": { 38 | bps.NewFromPPM(big.NewInt(1)), 39 | &bps.BPS{}, 40 | bps.NewFromPPM(big.NewInt(1)), 41 | }, 42 | } 43 | for name, tt := range tests { 44 | tt := tt 45 | t.Run(name, func(t *testing.T) { 46 | t.Parallel() 47 | got := tt.b.Add(tt.b2) 48 | if !reflect.DeepEqual(got, tt.want) { 49 | t.Errorf("BPS.Add() = %v, want %v", got, tt.want) 50 | } 51 | assertImmutableOperation(t, "BPS.Add()", got, tt.b, tt.b2) 52 | }) 53 | } 54 | } 55 | 56 | func assertImmutableOperation(t *testing.T, msg string, got, receiver *bps.BPS, args ...*bps.BPS) { 57 | t.Helper() 58 | 59 | if &got == &receiver { 60 | t.Errorf("%s has never mutated the receiver: got=%v, receiver=%v", msg, got, receiver) 61 | } 62 | for i, arg := range args { 63 | if &got == &receiver { 64 | t.Errorf("%s has never mutated any arguments: got=%v, i=%d, arg=%v", msg, got, i, arg) 65 | } 66 | } 67 | } 68 | 69 | func TestBPS_Sub(t *testing.T) { 70 | tests := map[string]struct { 71 | b *bps.BPS 72 | b2 *bps.BPS 73 | want *bps.BPS 74 | }{ 75 | "1 amount - 10 percentages = 900,000 ppms": { 76 | bps.NewFromAmount(1), 77 | bps.NewFromPercentage(10), 78 | bps.NewFromPPM(big.NewInt(900000)), 79 | }, 80 | "1 amount - (-10) percentages = 1100,000 ppms": { 81 | bps.NewFromAmount(1), 82 | bps.NewFromPercentage(-10), 83 | bps.NewFromPPM(big.NewInt(1100000)), 84 | }, 85 | "1 basis point - 10 deci basis point = 0": { 86 | bps.NewFromBasisPoint(1), 87 | bps.NewFromDeciBasisPoint(10), 88 | bps.NewFromAmount(0), 89 | }, 90 | "nil - 1 ppm = -1 ppm": { 91 | &bps.BPS{}, 92 | bps.NewFromPPM(big.NewInt(1)), 93 | bps.NewFromPPM(big.NewInt(-1)), 94 | }, 95 | "1 ppm - nil = 1 ppm": { 96 | bps.NewFromPPM(big.NewInt(1)), 97 | &bps.BPS{}, 98 | bps.NewFromPPM(big.NewInt(1)), 99 | }, 100 | } 101 | for name, tt := range tests { 102 | tt := tt 103 | t.Run(name, func(t *testing.T) { 104 | t.Parallel() 105 | got := tt.b.Sub(tt.b2) 106 | if !reflect.DeepEqual(got, tt.want) { 107 | t.Errorf("BPS.Sub() = %v, want %v", got, tt.want) 108 | } 109 | assertImmutableOperation(t, "BPS.Sub()", got, tt.b, tt.b2) 110 | }) 111 | } 112 | } 113 | 114 | func TestBPS_Mul(t *testing.T) { 115 | tests := map[string]struct { 116 | b *bps.BPS 117 | arg int64 118 | want *bps.BPS 119 | }{ 120 | "1 basis point * 5 = 500 ppms": { 121 | bps.NewFromBasisPoint(1), 122 | 5, 123 | bps.NewFromPPM(big.NewInt(500)), 124 | }, 125 | "1 deci basis point * (-10) = -100 ppms": { 126 | bps.NewFromDeciBasisPoint(1), 127 | -10, 128 | bps.NewFromPPM(big.NewInt(-100)), 129 | }, 130 | "-1 percentage * 2 = -20,000 ppms": { 131 | bps.NewFromPercentage(-1), 132 | 2, 133 | bps.NewFromPPM(big.NewInt(-20000)), 134 | }, 135 | "nil * 1 = 0": { 136 | &bps.BPS{}, 137 | 1, 138 | bps.NewFromAmount(0), 139 | }, 140 | } 141 | for name, tt := range tests { 142 | tt := tt 143 | t.Run(name, func(t *testing.T) { 144 | t.Parallel() 145 | got := tt.b.Mul(tt.arg) 146 | if !reflect.DeepEqual(got, tt.want) { 147 | t.Errorf("BPS.Mul() = %v, want %v", got, tt.want) 148 | } 149 | assertImmutableOperation(t, "BPS.Mul()", got, tt.b) 150 | }) 151 | } 152 | } 153 | 154 | func TestBPS_Div(t *testing.T) { 155 | tests := map[string]struct { 156 | b *bps.BPS 157 | arg int64 158 | want *bps.BPS 159 | }{ 160 | "100 ppms / 4 = 25 ppms": { 161 | bps.NewFromPPM(big.NewInt(100)), 162 | 4, 163 | bps.NewFromPPM(big.NewInt(25)), 164 | }, 165 | "100 ppms / 3 = 33.333 ppms = 33,333 ppbs, truncates after the decimal point": { 166 | bps.NewFromPPM(big.NewInt(100)), 167 | 3, 168 | bps.NewFromPPB(big.NewInt(33333)), 169 | }, 170 | "100 ppms / -5 = -20 ppms": { 171 | bps.NewFromPPM(big.NewInt(100)), 172 | -5, 173 | bps.NewFromPPM(big.NewInt(-20)), 174 | }, 175 | "nil / 5 = 0 ppms": { 176 | &bps.BPS{}, 177 | 5, 178 | bps.NewFromPPM(big.NewInt(0)), 179 | }, 180 | } 181 | for name, tt := range tests { 182 | tt := tt 183 | t.Run(name, func(t *testing.T) { 184 | t.Parallel() 185 | got := tt.b.Div(tt.arg) 186 | if !reflect.DeepEqual(got, tt.want) { 187 | t.Errorf("BPS.Div() = %v, want %v", got, tt.want) 188 | } 189 | assertImmutableOperation(t, "BPS.Div()", got, tt.b) 190 | }) 191 | } 192 | 193 | t.Run("If Div() by zero, it should occure division-by-zero run-time panic", func(t *testing.T) { 194 | b := bps.NewFromAmount(1) 195 | defer func() { 196 | err := recover() 197 | if err != "division by zero" { 198 | t.Errorf("got %s, want %s", err, "division by zero") 199 | } 200 | }() 201 | b.Div(0) 202 | }) 203 | } 204 | 205 | func TestBPS_Compare(t *testing.T) { 206 | t.Run("zero and zero", func(t *testing.T) { 207 | t.Parallel() 208 | 209 | b := bps.NewFromAmount(0) 210 | b2 := bps.NewFromAmount(0) 211 | 212 | if got := b.Cmp(b2); got != 0 { 213 | t.Errorf("BPS.Cmp() = %d, want 0", got) 214 | } 215 | if !b.Equal(b2) { 216 | t.Error("BPS.Equal() = true") 217 | } 218 | if !b.IsZero() { 219 | t.Error("BPS.IsZero() = true") 220 | } 221 | }) 222 | 223 | t.Run("About two differrent values", func(t *testing.T) { 224 | t.Parallel() 225 | 226 | b := bps.NewFromAmount(1) 227 | b2 := bps.NewFromAmount(2) 228 | 229 | if got := b.Cmp(b2); got != -1 { 230 | t.Errorf("BPS.Comp() = %d, want -1", got) 231 | } 232 | if b.Equal(b2) { 233 | t.Error("BPS.Equal() = false") 234 | } 235 | if b.IsZero() { 236 | t.Error("BPS.IsZero() = false") 237 | } 238 | }) 239 | 240 | t.Run("nil and nil", func(t *testing.T) { 241 | t.Parallel() 242 | 243 | b := &bps.BPS{} 244 | b2 := &bps.BPS{} 245 | 246 | if got := b.Cmp(b2); got != 0 { 247 | t.Errorf("BPS.Cmp() = %d, want 0", got) 248 | } 249 | if !b.Equal(b2) { 250 | t.Error("BPS.Equal() = true") 251 | } 252 | if !b.IsZero() { 253 | t.Error("BPS.IsZero() = true") 254 | } 255 | }) 256 | } 257 | 258 | func TestSum(t *testing.T) { 259 | tests := map[string]struct { 260 | first *bps.BPS 261 | rest []*bps.BPS 262 | want *bps.BPS 263 | }{ 264 | "1 amount + 1 percentage + 1 basis point + 1 deci basis point + 1 ppm + empty + nil = 1010,111 ppms": { 265 | bps.NewFromAmount(1), 266 | []*bps.BPS{ 267 | bps.NewFromPercentage(1), 268 | bps.NewFromBasisPoint(1), 269 | bps.NewFromDeciBasisPoint(1), 270 | bps.NewFromPPM(big.NewInt(1)), 271 | {}, 272 | nil, 273 | }, 274 | bps.NewFromPPM(big.NewInt(1010111)), 275 | }, 276 | "1 amount + (-1) percentage + (-1) basis point + (-1) deci basis point + (-1) ppm = 989,889 ppms": { 277 | bps.NewFromAmount(1), 278 | []*bps.BPS{ 279 | bps.NewFromPercentage(-1), 280 | bps.NewFromBasisPoint(-1), 281 | bps.NewFromDeciBasisPoint(-1), 282 | bps.NewFromPPM(big.NewInt(-1)), 283 | }, 284 | bps.NewFromPPM(big.NewInt(989889)), 285 | }, 286 | } 287 | for name, tt := range tests { 288 | tt := tt 289 | t.Run(name, func(t *testing.T) { 290 | t.Parallel() 291 | if got := bps.Sum(tt.first, tt.rest...); !reflect.DeepEqual(got, tt.want) { 292 | t.Errorf("Sum() = %v, want %v", got, tt.want) 293 | } 294 | }) 295 | } 296 | } 297 | 298 | func TestBPS_Abs(t *testing.T) { 299 | tests := map[string]struct { 300 | b *bps.BPS 301 | want *bps.BPS 302 | }{ 303 | "If plus value, it should return the same value": { 304 | bps.NewFromAmount(1), 305 | bps.NewFromAmount(1), 306 | }, 307 | "If minus value, it should return the plus value": { 308 | bps.NewFromAmount(-1), 309 | bps.NewFromAmount(1), 310 | }, 311 | "If nil, it should retrun zero": { 312 | &bps.BPS{}, 313 | bps.NewFromAmount(0), 314 | }, 315 | } 316 | for name, tt := range tests { 317 | tt := tt 318 | t.Run(name, func(t *testing.T) { 319 | t.Parallel() 320 | got := tt.b.Abs() 321 | if !reflect.DeepEqual(got, tt.want) { 322 | t.Errorf("BPS.Abs() = %v, want %v", got, tt.want) 323 | } 324 | assertImmutableOperation(t, "BPS.Abs()", got, tt.b) 325 | }) 326 | } 327 | } 328 | 329 | func TestBPS_Neg(t *testing.T) { 330 | tests := map[string]struct { 331 | b *bps.BPS 332 | want *bps.BPS 333 | }{ 334 | "If plus value, it should return the minus value": { 335 | bps.NewFromAmount(1), 336 | bps.NewFromAmount(-1), 337 | }, 338 | "If minus value, it should return the plus value": { 339 | bps.NewFromAmount(-1), 340 | bps.NewFromAmount(1), 341 | }, 342 | "If nil, it should return zero": { 343 | &bps.BPS{}, 344 | bps.NewFromAmount(0), 345 | }, 346 | } 347 | for name, tt := range tests { 348 | tt := tt 349 | t.Run(name, func(t *testing.T) { 350 | t.Parallel() 351 | got := tt.b.Neg() 352 | if !reflect.DeepEqual(got, tt.want) { 353 | t.Errorf("BPS.Neg() = %v, want %v", got, tt.want) 354 | } 355 | assertImmutableOperation(t, "BPS.Neg()", got, tt.b) 356 | }) 357 | } 358 | } 359 | 360 | func TestAvg(t *testing.T) { 361 | tests := map[string]struct { 362 | first *bps.BPS 363 | rest []*bps.BPS 364 | want *bps.BPS 365 | }{ 366 | "the average of 50 basis points, 125 basis points, and 345 basis points is 17,333,333 ppbs rounded off": { 367 | bps.NewFromBasisPoint(50), 368 | []*bps.BPS{ 369 | bps.NewFromBasisPoint(125), 370 | bps.NewFromBasisPoint(345), 371 | }, 372 | bps.NewFromPPB(big.NewInt(17333333)), 373 | }, 374 | "the average of 3 percentages, 2 amounts, and 50 basis points is 6783,333,333 ppbs rounded off": { 375 | bps.NewFromPercentage(3), 376 | []*bps.BPS{ 377 | bps.NewFromAmount(2), 378 | bps.NewFromBasisPoint(50), 379 | }, 380 | bps.NewFromPPB(big.NewInt(678333333)), 381 | }, 382 | "the average of 3 deci basis points, 15 percentages, and -360 basis points is 3801 deci basis points": { 383 | bps.NewFromDeciBasisPoint(3), 384 | []*bps.BPS{ 385 | bps.NewFromPercentage(15), 386 | bps.NewFromBasisPoint(-360), 387 | }, 388 | bps.NewFromDeciBasisPoint(3801), 389 | }, 390 | "the average of 50 basis points, 125 basis points, and nil is 5,833,333 ppbs rounded off": { 391 | bps.NewFromBasisPoint(50), 392 | []*bps.BPS{ 393 | bps.NewFromBasisPoint(125), 394 | {}, 395 | }, 396 | bps.NewFromPPB(big.NewInt(5833333)), 397 | }, 398 | } 399 | for name, tt := range tests { 400 | tt := tt 401 | t.Run(name, func(t *testing.T) { 402 | t.Parallel() 403 | if got := bps.Avg(tt.first, tt.rest...); !reflect.DeepEqual(got, tt.want) { 404 | t.Errorf("Avg() = %v, want %v", got, tt.want) 405 | } 406 | }) 407 | } 408 | } 409 | 410 | func TestMax(t *testing.T) { 411 | tests := map[string]struct { 412 | first *bps.BPS 413 | rest []*bps.BPS 414 | want *bps.BPS 415 | }{ 416 | "the maximum value of 10 basis points, 99 deci basis points, nil, and 1001 ppms is 1001 ppms": { 417 | bps.NewFromBasisPoint(10), 418 | []*bps.BPS{ 419 | bps.NewFromDeciBasisPoint(99), 420 | {}, 421 | bps.NewFromPPM(big.NewInt(1001)), 422 | }, 423 | bps.NewFromPPM(big.NewInt(1001)), 424 | }, 425 | "the maximum value of -10 basis points, -99 deci basis points, and -1001 ppms is -99 deci basis points": { 426 | bps.NewFromBasisPoint(-10), 427 | []*bps.BPS{ 428 | bps.NewFromDeciBasisPoint(-99), 429 | bps.NewFromPPM(big.NewInt(-1001)), 430 | }, 431 | bps.NewFromDeciBasisPoint(-99), 432 | }, 433 | } 434 | for name, tt := range tests { 435 | tt := tt 436 | t.Run(name, func(t *testing.T) { 437 | t.Parallel() 438 | if got := bps.Max(tt.first, tt.rest...); !got.Equal(tt.want) { 439 | t.Errorf("Max() = %v, want %v", got, tt.want) 440 | } 441 | }) 442 | } 443 | } 444 | 445 | func TestMin(t *testing.T) { 446 | tests := map[string]struct { 447 | fisrt *bps.BPS 448 | rest []*bps.BPS 449 | want *bps.BPS 450 | }{ 451 | "the minimum value of 10 basis points, 99 deci basis points, nil, and 1001 ppms is 0 ppms": { 452 | bps.NewFromBasisPoint(10), 453 | []*bps.BPS{ 454 | bps.NewFromDeciBasisPoint(99), 455 | {}, 456 | bps.NewFromPPM(big.NewInt(1001)), 457 | }, 458 | bps.NewFromAmount(0), 459 | }, 460 | "the minimum value of -10 basis points, -99 deci basis points, and -1001 ppms is -1001 deci basis points": { 461 | bps.NewFromBasisPoint(-10), 462 | []*bps.BPS{ 463 | bps.NewFromDeciBasisPoint(-99), 464 | bps.NewFromPPM(big.NewInt(-1001)), 465 | }, 466 | bps.NewFromPPM(big.NewInt(-1001)), 467 | }, 468 | } 469 | for name, tt := range tests { 470 | tt := tt 471 | t.Run(name, func(t *testing.T) { 472 | t.Parallel() 473 | if got := bps.Min(tt.fisrt, tt.rest...); !got.Equal(tt.want) { 474 | t.Errorf("Min() = %v, want %v", got, tt.want) 475 | } 476 | }) 477 | } 478 | } 479 | 480 | func TestBPS_FloatString(t *testing.T) { 481 | tests := map[string]struct { 482 | b *bps.BPS 483 | prec int 484 | want string 485 | }{ 486 | "1 ppm presents `0` as string": { 487 | bps.NewFromPPM(big.NewInt(1)), 488 | 0, 489 | "0", 490 | }, 491 | "1 ppm presents `0.000001` as string": { 492 | bps.NewFromPPM(big.NewInt(1)), 493 | 6, 494 | "0.000001", 495 | }, 496 | "1 deci basis point presents `0.00001` as string": { 497 | bps.NewFromDeciBasisPoint(1), 498 | 5, 499 | "0.00001", 500 | }, 501 | "1 deci basis point presents `0.000010` as string": { 502 | bps.NewFromDeciBasisPoint(1), 503 | 6, 504 | "0.000010", 505 | }, 506 | "1 basis point presents `0.0001` as string": { 507 | bps.NewFromBasisPoint(1), 508 | 4, 509 | "0.0001", 510 | }, 511 | "1 percentage presents `0.01` as string": { 512 | bps.NewFromPercentage(1), 513 | 2, 514 | "0.01", 515 | }, 516 | "5 percentage presents `0.1` as string, rounded to nearest": { 517 | bps.NewFromPercentage(5), 518 | 1, 519 | "0.1", 520 | }, 521 | "1 amount presents `1` as string": { 522 | bps.NewFromAmount(1), 523 | 0, 524 | "1", 525 | }, 526 | "1 amount presents `1.0` as string": { 527 | bps.NewFromAmount(1), 528 | 1, 529 | "1.0", 530 | }, 531 | } 532 | for name, tt := range tests { 533 | tt := tt 534 | t.Run(name, func(t *testing.T) { 535 | t.Parallel() 536 | if got := tt.b.FloatString(tt.prec); got != tt.want { 537 | t.Errorf("BPS.FloatString() = %v, want %v", got, tt.want) 538 | } 539 | }) 540 | } 541 | } 542 | --------------------------------------------------------------------------------