├── .github ├── CODEOWNERS ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── scorecard.yml │ └── tests.yml ├── go.mod ├── SECURITY.md ├── CITATION.cff ├── parser_test.go ├── LICENSE ├── parser.go ├── printer.go ├── printer_test.go ├── fp3 ├── fpdecimal.go └── fpdecimal_test.go ├── fp6 ├── fpdecimal.go └── fpdecimal_test.go ├── std_bench_test.go └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @nikolaydubina 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: @nikolaydubina 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nikolaydubina/fpdecimal 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Contact [@nikolaydubina](https://github.com/nikolaydubina) over email or linkedin. 6 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | 2 | cff-version: 1.2.0 3 | message: If you reference this library in publication, please cite it as below. 4 | title: Fixed Point numerics in Go 5 | abstract: High-performance fixed point decimal numeric type in Go 6 | authors: 7 | - family-names: Dubina 8 | given-names: Nikolay 9 | version: 2.1 10 | date-released: 2022-06-21 11 | license: MIT 12 | repository-code: https://github.com/nikolaydubina/fpdecimal 13 | url: https://github.com/nikolaydubina/fpdecimal 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package fpdecimal_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nikolaydubina/fpdecimal" 7 | ) 8 | 9 | func FuzzParseFixedPointDecimal(f *testing.F) { 10 | tests := []string{ 11 | "123.456", 12 | "0.123", 13 | "0.1", 14 | "0.01", 15 | "0.001", 16 | "0.000", 17 | "0.123.2", 18 | "0..1", 19 | "0.1.2", 20 | "123.1o2", 21 | "--123", 22 | "00000.123", 23 | "-", 24 | "", 25 | "123456", 26 | } 27 | for _, tc := range tests { 28 | f.Add(tc) 29 | f.Add("-" + tc) 30 | } 31 | f.Fuzz(func(t *testing.T, s string) { 32 | v, err := fpdecimal.ParseFixedPointDecimal([]byte(s), 3) 33 | if err != nil { 34 | if v != 0 { 35 | t.Errorf("has to be 0 on error") 36 | } 37 | if err.Error() == "" { 38 | t.Error(err, err.Error()) 39 | } 40 | return 41 | } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nikolay Dubina 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 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | name: Scorecard supply-chain security 2 | on: 3 | branch_protection_rule: 4 | schedule: 5 | - cron: '20 0 * * 6' 6 | push: 7 | branches: [ "main" ] 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | analysis: 13 | name: Scorecard analysis 14 | runs-on: ubuntu-latest 15 | permissions: 16 | security-events: write 17 | id-token: write 18 | 19 | steps: 20 | - name: "Checkout code" 21 | uses: actions/checkout@v3.1.0 22 | with: 23 | persist-credentials: false 24 | 25 | - name: "Run analysis" 26 | uses: ossf/scorecard-action@v2.3.1 27 | with: 28 | results_file: results.sarif 29 | results_format: sarif 30 | publish_results: true 31 | 32 | - name: "Upload artifact" 33 | uses: actions/upload-artifact@v3.1.0 34 | with: 35 | name: SARIF file 36 | path: results.sarif 37 | retention-days: 5 38 | 39 | - name: "Upload to code-scanning" 40 | uses: github/codeql-action/upload-sarif@v2.2.4 41 | with: 42 | sarif_file: results.sarif 43 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package fpdecimal 2 | 3 | const sep = '.' 4 | 5 | type errorString struct{ v string } 6 | 7 | func (e *errorString) Error() string { return e.v } 8 | 9 | var ( 10 | errEmptyString = &errorString{"empty string"} 11 | errMissingDigitsAfterSign = &errorString{"missing digits after sign"} 12 | errBadDigit = &errorString{"bad digit"} 13 | errMultipleDots = &errorString{"multiple dots"} 14 | ) 15 | 16 | // ParseFixedPointDecimal parses fixed-point decimal of p fractions into int64. 17 | func ParseFixedPointDecimal(s []byte, p uint8) (int64, error) { 18 | if len(s) == 0 { 19 | return 0, errEmptyString 20 | } 21 | 22 | s0 := s 23 | if s[0] == '-' || s[0] == '+' { 24 | s = s[1:] 25 | if len(s) < 1 { 26 | return 0, errMissingDigitsAfterSign 27 | } 28 | } 29 | 30 | var pn = int8(p) 31 | var d int8 = -1 // current decimal position 32 | var n int64 // output 33 | for _, ch := range s { 34 | if d == pn { 35 | break 36 | } 37 | 38 | if ch == sep { 39 | if d != -1 { 40 | return 0, errMultipleDots 41 | } 42 | d = 0 43 | continue 44 | } 45 | 46 | ch -= '0' 47 | if ch > 9 { 48 | return 0, errBadDigit 49 | } 50 | n = n*10 + int64(ch) 51 | 52 | if d != -1 { 53 | d++ 54 | } 55 | } 56 | 57 | // fill rest of 0 58 | if d == -1 { 59 | d = 0 60 | } 61 | for i := d; i < pn; i++ { 62 | n = n * 10 63 | } 64 | 65 | if s0[0] == '-' { 66 | n = -n 67 | } 68 | 69 | return n, nil 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check outcode 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Go 1.x 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: ^1.16 23 | 24 | - name: Test 25 | run: | 26 | go get -v -t -d ./... 27 | go install github.com/jstemmer/go-junit-report/v2@latest 28 | go test -coverprofile=coverage.out -covermode=atomic -cover -json -v ./... 2>&1 | go-junit-report -set-exit-code > tests.xml 29 | 30 | - name: Fuzz 31 | run: | 32 | go test -list . | grep Fuzz | xargs -P 8 -I {} go test -fuzz {} -fuzztime 5s . 33 | cd fp3; go test -list . | grep Fuzz | xargs -P 8 -I {} go test -fuzz {} -fuzztime 5s . ; cd .. 34 | cd fp6; go test -list . | grep Fuzz | xargs -P 8 -I {} go test -fuzz {} -fuzztime 5s . ; cd .. 35 | 36 | - name: Upload test results to Codecov 37 | uses: codecov/test-results-action@v1 38 | with: 39 | token: ${{ secrets.CODECOV_TOKEN }} 40 | files: tests.xml 41 | 42 | - name: Upload coverage to Codecov 43 | uses: codecov/codecov-action@v4.1.1 44 | with: 45 | token: ${{ secrets.CODECOV_TOKEN }} 46 | files: coverage.out 47 | -------------------------------------------------------------------------------- /printer.go: -------------------------------------------------------------------------------- 1 | package fpdecimal 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | const zeroPrefix = "0.000000000000000000000000000000000000" 8 | 9 | // FixedPointDecimalToString formats fixed-point decimal to string 10 | func FixedPointDecimalToString(v int64, p uint8) string { 11 | // max int64: +9223372036854775.807 12 | // min int64: -9223372036854775.808 13 | // max bytes int64: 21 14 | b := make([]byte, 0, 21) 15 | b = AppendFixedPointDecimal(b, v, p) 16 | return string(b) 17 | } 18 | 19 | // AppendFixedPointDecimal appends formatted fixed point decimal to destination buffer. 20 | // Returns appended slice. 21 | // This is efficient for avoiding memory copy. 22 | func AppendFixedPointDecimal(b []byte, v int64, p uint8) []byte { 23 | if v == 0 { 24 | return append(b, '0') 25 | } 26 | 27 | if p == 0 { 28 | return strconv.AppendInt(b, v, 10) 29 | } 30 | 31 | if v < 0 { 32 | v = -v 33 | b = append(b, '-') 34 | } 35 | 36 | // strconv.AppendInt is very efficient. 37 | // Efficient converting int64 to ASCII is not as trivial. 38 | s := uint8(len(b)) 39 | b = strconv.AppendInt(b, v, 10) 40 | 41 | // has whole? 42 | if uint8(len(b))-s > p { 43 | // place decimal point 44 | i := uint8(len(b)) - p 45 | b = append(b, 0) 46 | copy(b[i+1:], b[i:]) 47 | b[i] = '.' 48 | } else { 49 | // append zeroes and decimal point 50 | i := 2 + p - (uint8(len(b)) - s) 51 | for j := uint8(0); j < i; j++ { 52 | b = append(b, 0) 53 | } 54 | copy(b[s+i:], b[s:]) 55 | copy(b[s:], []byte(zeroPrefix[:i])) 56 | } 57 | 58 | // remove trailing zeros 59 | n := 0 60 | for i, q := range b { 61 | if q != '0' { 62 | n = i 63 | } 64 | } 65 | if b[n] == '.' { 66 | n-- 67 | } 68 | b = b[:n+1] 69 | 70 | return b 71 | } 72 | -------------------------------------------------------------------------------- /printer_test.go: -------------------------------------------------------------------------------- 1 | package fpdecimal_test 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/nikolaydubina/fpdecimal" 11 | ) 12 | 13 | func FuzzFixedPointDecimalToString_Fractions(f *testing.F) { 14 | tests := []float64{ 15 | 0, 16 | 0.100, 17 | 0.101, 18 | 0.010, 19 | 0.001, 20 | 0.0001, 21 | 0.123, 22 | 0.103, 23 | 0.100001, 24 | 12.001, 25 | 12.010, 26 | 12.345, 27 | 1, 28 | 2, 29 | 10, 30 | 12345678, 31 | } 32 | for _, tc := range tests { 33 | f.Add(tc) 34 | f.Add(-tc) 35 | } 36 | f.Fuzz(func(t *testing.T, r float64) { 37 | if r > math.MaxInt64/1000 || r < math.MinInt64/1000 { 38 | t.Skip() 39 | } 40 | 41 | // gaps start around these floats 42 | if r > 100_000_000 || r < -100_000_000 { 43 | t.Skip() 44 | } 45 | 46 | s := fmt.Sprintf("%.3f", r) 47 | rs, _ := strconv.ParseFloat(s, 64) 48 | 49 | v, err := fpdecimal.ParseFixedPointDecimal([]byte(s), 3) 50 | if err != nil { 51 | t.Error(err.Error()) 52 | } 53 | 54 | if s == "-0.000" || s == "0.000" || rs == 0 || rs == -0 || (rs > -0.001 && rs < 0.001) { 55 | if q := fpdecimal.FixedPointDecimalToString(v, 3); q != "0" { 56 | t.Error(r, s, q) 57 | } 58 | return 59 | } 60 | 61 | if s, fs := strconv.FormatFloat(rs, 'f', -1, 64), fpdecimal.FixedPointDecimalToString(v, 3); s != fs { 62 | t.Error(s, fs, r, v) 63 | } 64 | }) 65 | } 66 | 67 | func FuzzFixedPointDecimalToString_NoFractions(f *testing.F) { 68 | tests := []int64{ 69 | 0, 70 | 1, 71 | 2, 72 | 10, 73 | 12345678, 74 | } 75 | for _, tc := range tests { 76 | f.Add(tc) 77 | f.Add(-tc) 78 | } 79 | f.Fuzz(func(t *testing.T, r int64) { 80 | if a, b := fpdecimal.FixedPointDecimalToString(r, 0), strconv.FormatInt(r, 10); a != b { 81 | t.Error(a, b) 82 | } 83 | }) 84 | } 85 | 86 | func BenchmarkFixedPointDecimalToString(b *testing.B) { 87 | var s string 88 | for _, tc := range testsFloats { 89 | tests := make([]int64, 0, len(tc.vals)) 90 | for range tc.vals { 91 | tests = append(tests, int64(rand.Int())) 92 | } 93 | 94 | b.ResetTimer() 95 | b.Run(tc.name, func(b *testing.B) { 96 | for n := 0; n < b.N; n++ { 97 | s = fpdecimal.FixedPointDecimalToString(tests[n%len(tests)], 3) 98 | if s == "" { 99 | b.Error("empty str") 100 | } 101 | } 102 | }) 103 | } 104 | } 105 | 106 | func BenchmarkAppendFixedPointDecimal(b *testing.B) { 107 | d := make([]byte, 0, 21) 108 | for _, tc := range testsFloats { 109 | tests := make([]int64, 0, len(tc.vals)) 110 | for range tc.vals { 111 | tests = append(tests, int64(rand.Int())) 112 | } 113 | 114 | b.ResetTimer() 115 | b.Run(tc.name, func(b *testing.B) { 116 | for n := 0; n < b.N; n++ { 117 | d = fpdecimal.AppendFixedPointDecimal(d, tests[n%len(tests)], 3) 118 | if len(d) == 0 { 119 | b.Error("empty str") 120 | } 121 | } 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /fp3/fpdecimal.go: -------------------------------------------------------------------------------- 1 | package fp3 2 | 3 | import "github.com/nikolaydubina/fpdecimal" 4 | 5 | // Decimal with 3 fractional digits. 6 | // Fractions lower than that are discarded in operations. 7 | // Max: +9223372036854775.807 8 | // Min: -9223372036854775.808 9 | type Decimal struct{ v int64 } 10 | 11 | var Zero = Decimal{} 12 | 13 | type integer interface { 14 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 15 | } 16 | 17 | const ( 18 | fractionDigits = 3 19 | multiplier = 1000 20 | ) 21 | 22 | func FromInt[T integer](v T) Decimal { return Decimal{int64(v) * multiplier} } 23 | 24 | func FromFloat[T float32 | float64](v T) Decimal { 25 | return Decimal{int64(float64(v) * float64(multiplier))} 26 | } 27 | 28 | // FromIntScaled expects value already scaled to minor units 29 | func FromIntScaled[T integer](v T) Decimal { return Decimal{int64(v)} } 30 | 31 | func FromString(s string) (Decimal, error) { 32 | v, err := fpdecimal.ParseFixedPointDecimal([]byte(s), fractionDigits) 33 | return Decimal{v}, err 34 | } 35 | 36 | func (v *Decimal) UnmarshalJSON(b []byte) (err error) { 37 | v.v, err = fpdecimal.ParseFixedPointDecimal(b, fractionDigits) 38 | return err 39 | } 40 | 41 | func (v Decimal) MarshalJSON() ([]byte, error) { return []byte(v.String()), nil } 42 | 43 | func (v *Decimal) UnmarshalText(b []byte) (err error) { 44 | v.v, err = fpdecimal.ParseFixedPointDecimal(b, fractionDigits) 45 | return err 46 | } 47 | 48 | func (v Decimal) MarshalText() ([]byte, error) { return []byte(v.String()), nil } 49 | 50 | func (a Decimal) Scaled() int64 { return a.v } 51 | 52 | func (a Decimal) Float32() float32 { return float32(a.v) / float32(multiplier) } 53 | 54 | func (a Decimal) Float64() float64 { return float64(a.v) / float64(multiplier) } 55 | 56 | func (a Decimal) String() string { return fpdecimal.FixedPointDecimalToString(a.v, fractionDigits) } 57 | 58 | func (a Decimal) Add(b Decimal) Decimal { return Decimal{v: a.v + b.v} } 59 | 60 | func (a Decimal) Sub(b Decimal) Decimal { return Decimal{v: a.v - b.v} } 61 | 62 | func (a Decimal) Mul(b Decimal) Decimal { return Decimal{v: a.v * b.v / multiplier} } 63 | 64 | func (a Decimal) Div(b Decimal) Decimal { return Decimal{v: a.v * multiplier / b.v} } 65 | 66 | func (a Decimal) Mod(b Decimal) Decimal { return Decimal{v: a.v % (b.v / multiplier)} } 67 | 68 | func (a Decimal) DivMod(b Decimal) (part, remainder Decimal) { return a.Div(b), a.Mod(b) } 69 | 70 | func (a Decimal) Equal(b Decimal) bool { return a.v == b.v } 71 | 72 | func (a Decimal) GreaterThan(b Decimal) bool { return a.v > b.v } 73 | 74 | func (a Decimal) LessThan(b Decimal) bool { return a.v < b.v } 75 | 76 | func (a Decimal) GreaterThanOrEqual(b Decimal) bool { return a.v >= b.v } 77 | 78 | func (a Decimal) LessThanOrEqual(b Decimal) bool { return a.v <= b.v } 79 | 80 | func (a Decimal) Compare(b Decimal) int { 81 | if a.LessThan(b) { 82 | return -1 83 | } 84 | if a.GreaterThan(b) { 85 | return 1 86 | } 87 | return 0 88 | } 89 | 90 | func Min(vs ...Decimal) Decimal { 91 | if len(vs) == 0 { 92 | panic("min of empty set is undefined") 93 | } 94 | var v Decimal = vs[0] 95 | for _, q := range vs { 96 | if q.LessThan(v) { 97 | v = q 98 | } 99 | } 100 | return v 101 | } 102 | 103 | func Max(vs ...Decimal) Decimal { 104 | if len(vs) == 0 { 105 | panic("max of empty set is undefined") 106 | } 107 | var v Decimal = vs[0] 108 | for _, q := range vs { 109 | if q.GreaterThan(v) { 110 | v = q 111 | } 112 | } 113 | return v 114 | } 115 | -------------------------------------------------------------------------------- /fp6/fpdecimal.go: -------------------------------------------------------------------------------- 1 | package fp6 2 | 3 | import "github.com/nikolaydubina/fpdecimal" 4 | 5 | // Decimal with 6 fractional digits. 6 | // Fractions lower than that are discarded in operations. 7 | // Max: +9223372036854.775807 8 | // Min: -9223372036854.775808 9 | type Decimal struct{ v int64 } 10 | 11 | var Zero = Decimal{} 12 | 13 | type integer interface { 14 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 15 | } 16 | 17 | const ( 18 | fractionDigits = 6 19 | multiplier = 1_000_000 20 | ) 21 | 22 | func FromInt[T integer](v T) Decimal { return Decimal{int64(v) * multiplier} } 23 | 24 | func FromFloat[T float32 | float64](v T) Decimal { 25 | return Decimal{int64(float64(v) * float64(multiplier))} 26 | } 27 | 28 | // FromIntScaled expects value already scaled to minor units 29 | func FromIntScaled[T integer](v T) Decimal { return Decimal{int64(v)} } 30 | 31 | func FromString(s string) (Decimal, error) { 32 | v, err := fpdecimal.ParseFixedPointDecimal([]byte(s), fractionDigits) 33 | return Decimal{v}, err 34 | } 35 | 36 | func (v *Decimal) UnmarshalJSON(b []byte) (err error) { 37 | v.v, err = fpdecimal.ParseFixedPointDecimal(b, fractionDigits) 38 | return err 39 | } 40 | 41 | func (v Decimal) MarshalJSON() ([]byte, error) { return []byte(v.String()), nil } 42 | 43 | func (v *Decimal) UnmarshalText(b []byte) (err error) { 44 | v.v, err = fpdecimal.ParseFixedPointDecimal(b, fractionDigits) 45 | return err 46 | } 47 | 48 | func (v Decimal) MarshalText() ([]byte, error) { return []byte(v.String()), nil } 49 | 50 | func (a Decimal) Scaled() int64 { return a.v } 51 | 52 | func (a Decimal) Float32() float32 { return float32(a.v) / float32(multiplier) } 53 | 54 | func (a Decimal) Float64() float64 { return float64(a.v) / float64(multiplier) } 55 | 56 | func (a Decimal) String() string { return fpdecimal.FixedPointDecimalToString(a.v, fractionDigits) } 57 | 58 | func (a Decimal) Add(b Decimal) Decimal { return Decimal{v: a.v + b.v} } 59 | 60 | func (a Decimal) Sub(b Decimal) Decimal { return Decimal{v: a.v - b.v} } 61 | 62 | func (a Decimal) Mul(b Decimal) Decimal { return Decimal{v: a.v * b.v / multiplier} } 63 | 64 | func (a Decimal) Div(b Decimal) Decimal { return Decimal{v: a.v * multiplier / b.v} } 65 | 66 | func (a Decimal) Mod(b Decimal) Decimal { return Decimal{v: a.v % (b.v / multiplier)} } 67 | 68 | func (a Decimal) DivMod(b Decimal) (part, remainder Decimal) { return a.Div(b), a.Mod(b) } 69 | 70 | func (a Decimal) Equal(b Decimal) bool { return a.v == b.v } 71 | 72 | func (a Decimal) GreaterThan(b Decimal) bool { return a.v > b.v } 73 | 74 | func (a Decimal) LessThan(b Decimal) bool { return a.v < b.v } 75 | 76 | func (a Decimal) GreaterThanOrEqual(b Decimal) bool { return a.v >= b.v } 77 | 78 | func (a Decimal) LessThanOrEqual(b Decimal) bool { return a.v <= b.v } 79 | 80 | func (a Decimal) Compare(b Decimal) int { 81 | if a.LessThan(b) { 82 | return -1 83 | } 84 | if a.GreaterThan(b) { 85 | return 1 86 | } 87 | return 0 88 | } 89 | 90 | func Min(vs ...Decimal) Decimal { 91 | if len(vs) == 0 { 92 | panic("min of empty set is undefined") 93 | } 94 | var v Decimal = vs[0] 95 | for _, q := range vs { 96 | if q.LessThan(v) { 97 | v = q 98 | } 99 | } 100 | return v 101 | } 102 | 103 | func Max(vs ...Decimal) Decimal { 104 | if len(vs) == 0 { 105 | panic("max of empty set is undefined") 106 | } 107 | var v Decimal = vs[0] 108 | for _, q := range vs { 109 | if q.GreaterThan(v) { 110 | v = q 111 | } 112 | } 113 | return v 114 | } 115 | -------------------------------------------------------------------------------- /std_bench_test.go: -------------------------------------------------------------------------------- 1 | package fpdecimal_test 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | var testsFloats = []struct { 10 | name string 11 | vals []string 12 | }{ 13 | { 14 | name: "small", 15 | vals: []string{ 16 | "123.456", 17 | "0.123", 18 | "0.012", 19 | "0.001", 20 | "0.982", 21 | "0.101", 22 | "10", 23 | "11", 24 | "1", 25 | }, 26 | }, 27 | { 28 | name: "large", 29 | vals: []string{ 30 | "123123123112312.1232", 31 | "5341320482340234.123", 32 | }, 33 | }, 34 | } 35 | 36 | var testsInts = []struct { 37 | name string 38 | vals []string 39 | }{ 40 | { 41 | name: "small", 42 | vals: []string{ 43 | "123456", 44 | "0123", 45 | "0012", 46 | "0001", 47 | "0982", 48 | "0101", 49 | "10", 50 | "11", 51 | "1", 52 | }, 53 | }, 54 | { 55 | name: "large", 56 | vals: []string{ 57 | "123123123112312", 58 | "5341320482340234", 59 | }, 60 | }, 61 | } 62 | 63 | func BenchmarkParse_int_strconv_Atoi(b *testing.B) { 64 | var s int 65 | var err error 66 | for _, tc := range testsInts { 67 | b.Run(tc.name, func(b *testing.B) { 68 | for n := 0; n < b.N; n++ { 69 | s, err = strconv.Atoi(tc.vals[n%len(tc.vals)]) 70 | if err != nil || s == 0 { 71 | b.Error(s, err) 72 | } 73 | } 74 | }) 75 | } 76 | } 77 | 78 | func BenchmarkPrint_int_strconv_Itoa(b *testing.B) { 79 | var s string 80 | for _, tc := range testsInts { 81 | tests := make([]int, 0, len(tc.vals)) 82 | for _, q := range tc.vals { 83 | v, err := strconv.Atoi(q) 84 | if err != nil { 85 | b.Error(err) 86 | } 87 | tests = append(tests, v) 88 | tests = append(tests, -v) 89 | } 90 | 91 | b.ResetTimer() 92 | b.Run(tc.name, func(b *testing.B) { 93 | for n := 0; n < b.N; n++ { 94 | s = strconv.Itoa(tests[n%len(tests)]) 95 | if s == "" { 96 | b.Error("empty str") 97 | } 98 | } 99 | }) 100 | } 101 | } 102 | 103 | func BenchmarkParse_int_strconv_ParseInt(b *testing.B) { 104 | var err error 105 | for _, tc := range testsInts { 106 | b.Run(tc.name, func(b *testing.B) { 107 | b.Run("int32", func(b *testing.B) { 108 | if tc.name == "large" { 109 | b.Skip() 110 | } 111 | var s int64 112 | for n := 0; n < b.N; n++ { 113 | s, err = strconv.ParseInt(tc.vals[n%len(tc.vals)], 10, 32) 114 | if err != nil || s == 0 { 115 | b.Error(s, err) 116 | } 117 | } 118 | }) 119 | 120 | b.Run("int64", func(b *testing.B) { 121 | var s int64 122 | for n := 0; n < b.N; n++ { 123 | s, err = strconv.ParseInt(tc.vals[n%len(tc.vals)], 10, 64) 124 | if err != nil || s == 0 { 125 | b.Error(s, err) 126 | } 127 | } 128 | }) 129 | }) 130 | } 131 | } 132 | 133 | func BenchmarkPrint_int_strconv_FormatInt(b *testing.B) { 134 | var s string 135 | for _, tc := range testsInts { 136 | b.Run(tc.name, func(b *testing.B) { 137 | if tc.name == "large" { 138 | b.Skip() 139 | } 140 | 141 | tests := make([]int64, 0, len(tc.vals)) 142 | for _, q := range tc.vals { 143 | v, err := strconv.ParseInt(q, 10, 32) 144 | if err != nil { 145 | b.Error(err) 146 | } 147 | tests = append(tests, v) 148 | tests = append(tests, -v) 149 | } 150 | 151 | b.ResetTimer() 152 | for n := 0; n < b.N; n++ { 153 | s = strconv.FormatInt(tests[n%len(tests)], 10) 154 | if s == "" { 155 | b.Error("empty str") 156 | } 157 | } 158 | }) 159 | } 160 | } 161 | 162 | func BenchmarkParse_float_strconv_ParseFloat(b *testing.B) { 163 | var s float64 164 | var err error 165 | for _, tc := range testsFloats { 166 | b.Run(tc.name, func(b *testing.B) { 167 | b.Run("float32", func(b *testing.B) { 168 | for n := 0; n < b.N; n++ { 169 | s, err = strconv.ParseFloat(tc.vals[n%len(tc.vals)], 32) 170 | if err != nil || s == 0 { 171 | b.Error(s, err) 172 | } 173 | } 174 | }) 175 | 176 | b.Run("float64", func(b *testing.B) { 177 | for n := 0; n < b.N; n++ { 178 | s, err = strconv.ParseFloat(tc.vals[n%len(tc.vals)], 64) 179 | if err != nil || s == 0 { 180 | b.Error(s, err) 181 | } 182 | } 183 | }) 184 | }) 185 | } 186 | } 187 | 188 | func BenchmarkPrint_float_strconv_FormatFloat(b *testing.B) { 189 | var s string 190 | for _, tc := range testsFloats { 191 | b.Run(tc.name, func(b *testing.B) { 192 | tests := make([]float64, 0, len(tc.vals)) 193 | for _, q := range tc.vals { 194 | v, err := strconv.ParseFloat(q, 32) 195 | if err != nil { 196 | b.Error(err) 197 | } 198 | tests = append(tests, v) 199 | } 200 | 201 | b.ResetTimer() 202 | b.Run("float32", func(b *testing.B) { 203 | for n := 0; n < b.N; n++ { 204 | s = strconv.FormatFloat(tests[n%len(tests)], 'f', 3, 32) 205 | if s == "" { 206 | b.Error("empty str") 207 | } 208 | } 209 | }) 210 | 211 | b.Run("float64", func(b *testing.B) { 212 | for n := 0; n < b.N; n++ { 213 | s = strconv.FormatFloat(tests[n%len(tests)], 'f', 3, 64) 214 | if s == "" { 215 | b.Error("empty str") 216 | } 217 | } 218 | }) 219 | }) 220 | } 221 | } 222 | 223 | func BenchmarkParse_float_fmt_Sscanf(b *testing.B) { 224 | var s float32 225 | var err error 226 | for _, tc := range testsFloats { 227 | b.Run(tc.name, func(b *testing.B) { 228 | for n := 0; n < b.N; n++ { 229 | _, err = fmt.Sscanf(tc.vals[n%len(tc.vals)], "%f", &s) 230 | if err != nil || s == 0 { 231 | b.Error(s, err) 232 | } 233 | } 234 | }) 235 | } 236 | } 237 | 238 | func BenchmarkPrint_float_fmt_Sprintf(b *testing.B) { 239 | var s string 240 | var err error 241 | for _, tc := range testsFloats { 242 | tests := make([]float32, 0, len(tc.vals)) 243 | for _, q := range tc.vals { 244 | var s float32 245 | _, err = fmt.Sscanf(q, "%f", &s) 246 | if err != nil { 247 | b.Error(err) 248 | } 249 | tests = append(tests, s) 250 | } 251 | 252 | b.ResetTimer() 253 | b.Run(tc.name, func(b *testing.B) { 254 | for n := 0; n < b.N; n++ { 255 | s = fmt.Sprintf("%.3f", tests[n%len(tests)]) 256 | if s == "" { 257 | b.Error("empty str") 258 | } 259 | } 260 | }) 261 | } 262 | } 263 | 264 | func BenchmarkArithmetic_int64(b *testing.B) { 265 | var x int64 = 251231 266 | var y int64 = 21231001 267 | 268 | var s int64 269 | 270 | b.Run("add", func(b *testing.B) { 271 | s = 0 272 | for n := 0; n < b.N; n++ { 273 | s = x + y 274 | } 275 | }) 276 | 277 | b.Run("div", func(b *testing.B) { 278 | s = 0 279 | for n := 0; n < b.N; n++ { 280 | s = x + y 281 | } 282 | }) 283 | 284 | b.Run("divmod", func(b *testing.B) { 285 | s = 0 286 | for n := 0; n < b.N; n++ { 287 | s += x / y 288 | s += x % y 289 | } 290 | }) 291 | 292 | b.Run("mod", func(b *testing.B) { 293 | s = 0 294 | for n := 0; n < b.N; n++ { 295 | s = x % y 296 | } 297 | }) 298 | 299 | if s == 0 { 300 | b.Error() 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /fp3/fpdecimal_test.go: -------------------------------------------------------------------------------- 1 | package fp3_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "math" 8 | "strconv" 9 | "testing" 10 | "unsafe" 11 | 12 | fp "github.com/nikolaydubina/fpdecimal/fp3" 13 | ) 14 | 15 | func FuzzArithmetics(f *testing.F) { 16 | tests := [][2]int64{ 17 | {1, 2}, 18 | {1, -5}, 19 | {1, 0}, 20 | {1100, -2}, 21 | } 22 | for _, tc := range tests { 23 | f.Add(tc[0], tc[1]) 24 | } 25 | f.Fuzz(func(t *testing.T, a, b int64) { 26 | fa := fp.FromIntScaled(a) 27 | fb := fp.FromIntScaled(b) 28 | 29 | v := []bool{ 30 | // sum commutativity 31 | fa.Add(fb) == fb.Add(fa), 32 | 33 | // sum associativity 34 | fp.Zero.Add(fa).Add(fb).Add(fa) == fp.Zero.Add(fb).Add(fa).Add(fa), 35 | 36 | // sum zero 37 | fa == fa.Add(fb).Sub(fb), 38 | fa == fa.Sub(fb).Add(fb), 39 | fp.Zero == fp.Zero.Add(fa).Sub(fa), 40 | 41 | // product identity 42 | fa == fa.Mul(fp.FromInt(1)), 43 | 44 | // product zero 45 | fp.Zero == fa.Mul(fp.FromInt(0)), 46 | 47 | // match number 48 | (a == b) == (fa == fb), 49 | (a == b) == fa.Equal(fb), 50 | a < b == fa.LessThan(fb), 51 | a > b == fa.GreaterThan(fb), 52 | a <= b == fa.LessThanOrEqual(fb), 53 | a >= b == fa.GreaterThanOrEqual(fb), 54 | 55 | // match number convert 56 | fp.FromIntScaled(a+b) == fa.Add(fb), 57 | fp.FromIntScaled(a-b) == fa.Sub(fb), 58 | } 59 | for i, q := range v { 60 | if !q { 61 | t.Error(i, a, b, fa, fb) 62 | } 63 | } 64 | 65 | if b != 0 { 66 | pdiv := fa.Div(fp.FromInt(b)) 67 | p, r := fa.DivMod(fp.FromInt(b)) 68 | if p != pdiv { 69 | t.Error(p, pdiv) 70 | } 71 | if p != fp.FromIntScaled(a/b) { 72 | t.Error(a, b, p, a/b) 73 | } 74 | if fr := fp.FromIntScaled(a % b); r != fr { 75 | t.Error("Mod", "a", a, "b", b, "got", fr, "want", r) 76 | } 77 | } 78 | }) 79 | } 80 | 81 | func FuzzParse_StringSameAsFloat(f *testing.F) { 82 | tests := []float64{ 83 | 0, 84 | 0.100, 85 | 0.101, 86 | 0.010, 87 | 0.001, 88 | 0.0001, 89 | 0.123, 90 | 0.103, 91 | 0.100001, 92 | 12.001, 93 | 12.010, 94 | 12.345, 95 | 1, 96 | 2, 97 | 10, 98 | 12345678, 99 | } 100 | for _, tc := range tests { 101 | f.Add(tc) 102 | f.Add(-tc) 103 | } 104 | f.Fuzz(func(t *testing.T, r float64) { 105 | if r > math.MaxInt64/1000 || r < math.MinInt64/1000 { 106 | t.Skip() 107 | } 108 | 109 | // gaps start around these floats 110 | if r > 100_000_000 || r < -100_000_000 { 111 | t.Skip() 112 | } 113 | 114 | s := fmt.Sprintf("%.3f", r) 115 | rs, _ := strconv.ParseFloat(s, 64) 116 | 117 | v, err := fp.FromString(s) 118 | if err != nil { 119 | t.Error(err) 120 | } 121 | 122 | if s == "-0.000" || s == "0.000" || rs == 0 || rs == -0 || (rs > -0.001 && rs < 0.001) { 123 | if v.String() != "0" { 124 | t.Errorf("s('0') != Decimal.String(%#v) of fp3(%#v) float32(%#v) .3f-float32(%#v)", v.String(), v, r, s) 125 | } 126 | return 127 | } 128 | 129 | if s, fs := strconv.FormatFloat(rs, 'f', -1, 64), v.String(); s != fs { 130 | t.Error(s, fs, r, v) 131 | } 132 | }) 133 | } 134 | 135 | func FuzzParse_StringRaw(f *testing.F) { 136 | tests := []string{ 137 | "123.456", 138 | "0.123", 139 | "0.1", 140 | "0.01", 141 | "0.001", 142 | "0.000", 143 | "0.123.2", 144 | "0..1", 145 | "0.1.2", 146 | "123.1o2", 147 | "--123", 148 | "00000.123", 149 | "-", 150 | "", 151 | "123456", 152 | } 153 | for _, tc := range tests { 154 | f.Add(tc) 155 | f.Add("-" + tc) 156 | } 157 | f.Fuzz(func(t *testing.T, s string) { 158 | v, err := fp.FromString(s) 159 | if err != nil { 160 | if v != fp.Zero { 161 | t.Errorf("has to be 0 on error") 162 | } 163 | return 164 | } 165 | }) 166 | } 167 | 168 | func FuzzToFloat(f *testing.F) { 169 | tests := []float64{ 170 | 0, 171 | 0.001, 172 | 1, 173 | 123.456, 174 | } 175 | for _, tc := range tests { 176 | f.Add(tc) 177 | f.Add(-tc) 178 | } 179 | f.Fuzz(func(t *testing.T, v float64) { 180 | a := fp.FromFloat(v) 181 | 182 | if delta := math.Abs(v - a.Float64()); delta > 0.00100001 { 183 | t.Error("a", a, "a.f64", a.Float64(), "v", v, "delta", delta) 184 | } 185 | }) 186 | } 187 | 188 | func FuzzScaled(f *testing.F) { 189 | tests := []float64{ 190 | 0, 191 | 0.001, 192 | 1, 193 | 123.456, 194 | } 195 | for _, tc := range tests { 196 | f.Add(tc) 197 | f.Add(-tc) 198 | } 199 | f.Fuzz(func(t *testing.T, v float64) { 200 | a := fp.FromFloat(v) 201 | 202 | if int64(v*1000) != a.Scaled() { 203 | t.Error(a, a.Scaled(), int64(v*1000)) 204 | } 205 | }) 206 | } 207 | 208 | var floatsForTests = []struct { 209 | name string 210 | vals []string 211 | }{ 212 | { 213 | name: "small", 214 | vals: []string{ 215 | "123.456", 216 | "0.123", 217 | "0.012", 218 | "0.001", 219 | "0.982", 220 | "0.101", 221 | "10", 222 | "11", 223 | "1", 224 | }, 225 | }, 226 | { 227 | name: "large", 228 | vals: []string{ 229 | "123123123112312.1232", 230 | "5341320482340234.123", 231 | }, 232 | }, 233 | } 234 | 235 | func BenchmarkParse(b *testing.B) { 236 | var s fp.Decimal 237 | var err error 238 | 239 | b.Run("fromString", func(b *testing.B) { 240 | for _, tc := range floatsForTests { 241 | b.ResetTimer() 242 | b.Run(tc.name, func(b *testing.B) { 243 | for n := 0; n < b.N; n++ { 244 | s, err = fp.FromString(tc.vals[n%len(tc.vals)]) 245 | if err != nil || s == fp.Zero { 246 | b.Error(s, err) 247 | } 248 | } 249 | }) 250 | } 251 | }) 252 | 253 | b.Run("UnmarshalJSON", func(b *testing.B) { 254 | for _, tc := range floatsForTests { 255 | var vals [][]byte 256 | for i := range tc.vals { 257 | vals = append(vals, []byte(tc.vals[i])) 258 | } 259 | 260 | b.ResetTimer() 261 | b.Run(tc.name, func(b *testing.B) { 262 | for n := 0; n < b.N; n++ { 263 | if err = s.UnmarshalJSON(vals[n%len(vals)]); err != nil || s == fp.Zero { 264 | b.Error(s, err) 265 | } 266 | } 267 | }) 268 | } 269 | }) 270 | } 271 | 272 | func BenchmarkPrint(b *testing.B) { 273 | var s string 274 | for _, tc := range floatsForTests { 275 | tests := make([]fp.Decimal, 0, len(tc.vals)) 276 | for _, q := range tc.vals { 277 | v, err := fp.FromString(q) 278 | if err != nil { 279 | b.Error(err) 280 | } 281 | tests = append(tests, v) 282 | tests = append(tests, fp.Zero.Sub(v)) 283 | } 284 | 285 | b.Run("String", func(b *testing.B) { 286 | b.ResetTimer() 287 | b.Run(tc.name, func(b *testing.B) { 288 | for n := 0; n < b.N; n++ { 289 | s = tests[n%len(tc.vals)].String() 290 | if s == "" { 291 | b.Error("empty str") 292 | } 293 | } 294 | }) 295 | }) 296 | 297 | b.Run("Marshal", func(b *testing.B) { 298 | b.ResetTimer() 299 | b.Run(tc.name, func(b *testing.B) { 300 | for n := 0; n < b.N; n++ { 301 | s = tests[n%len(tc.vals)].String() 302 | if s == "" { 303 | b.Error("empty str") 304 | } 305 | } 306 | }) 307 | }) 308 | } 309 | } 310 | 311 | func TestUnmarshalJSON(t *testing.T) { 312 | type MyType struct { 313 | TeslaStockPrice fp.Decimal `json:"tesla-stock-price"` 314 | } 315 | 316 | tests := []struct { 317 | json string 318 | v fp.Decimal 319 | s string 320 | }{ 321 | { 322 | json: `{"tesla-stock-price": 9000.001}`, 323 | v: fp.FromFloat(9000.001), 324 | s: `9000.001`, 325 | }, 326 | } 327 | for _, tc := range tests { 328 | t.Run(tc.json, func(t *testing.T) { 329 | var v MyType 330 | err := json.Unmarshal([]byte(tc.json), &v) 331 | if err != nil { 332 | t.Error(err) 333 | } 334 | if s := v.TeslaStockPrice.String(); s != tc.s { 335 | t.Errorf("s(%#v) != tc.s(%#v)", s, tc.s) 336 | } 337 | }) 338 | } 339 | } 340 | 341 | func TestUMarshalJSON(t *testing.T) { 342 | type MyType struct { 343 | TeslaStockPrice fp.Decimal `json:"tesla-stock-price"` 344 | } 345 | 346 | t.Run("when nil struct, then error", func(t *testing.T) { 347 | var v *MyType 348 | err := json.Unmarshal([]byte(`{"tesla-stock-price": 9000.001}`), v) 349 | if err == nil { 350 | t.Error("expected error") 351 | } 352 | }) 353 | 354 | t.Run("when nil value, then error", func(t *testing.T) { 355 | var v *fp.Decimal 356 | err := json.Unmarshal([]byte(`{"tesla-stock-price": 9000.001}`), &v) 357 | if err == nil { 358 | t.Error("expected error") 359 | } 360 | }) 361 | 362 | t.Run("when nil const of type, then error", func(t *testing.T) { 363 | err := json.Unmarshal([]byte(`{"tesla-stock-price": 9000.001}`), (*fp.Decimal)(nil)) 364 | if err == nil { 365 | t.Error("expected error") 366 | } 367 | }) 368 | 369 | t.Run("ok", func(t *testing.T) { 370 | var v MyType 371 | err := json.Unmarshal([]byte(`{"tesla-stock-price": 9000.001}`), &v) 372 | if err != nil { 373 | t.Error(err) 374 | } 375 | e := MyType{fp.FromIntScaled(9000001)} 376 | if v != e { 377 | t.Error(v) 378 | } 379 | }) 380 | } 381 | 382 | func FuzzJSON(f *testing.F) { 383 | type MyType struct { 384 | A fp.Decimal `json:"a"` 385 | } 386 | 387 | tests := []float32{ 388 | 123456, 389 | 0, 390 | 1.1, 391 | 0.123, 392 | 0.0123, 393 | } 394 | for _, tc := range tests { 395 | f.Add(tc) 396 | f.Add(-tc) 397 | } 398 | f.Fuzz(func(t *testing.T, v float32) { 399 | if v > math.MaxInt64/1000 || v < math.MinInt64/1000 { 400 | t.Skip() 401 | } 402 | 403 | b := fmt.Sprintf("%.3f", v) 404 | rs, _ := strconv.ParseFloat(b, 64) 405 | s := `{"a":` + strconv.FormatFloat(rs, 'f', -1, 64) + `}` 406 | 407 | var x MyType 408 | err := json.Unmarshal([]byte(s), &x) 409 | if err != nil { 410 | t.Error(err, s) 411 | } 412 | 413 | if b == "-0.000" || b == "0.000" || rs == 0 || rs == -0 || (rs > 0.001 && rs < 0.001) { 414 | if x.A.String() != "0" { 415 | t.Error(b, x) 416 | } 417 | return 418 | } 419 | 420 | if a := x.A.String(); a != strconv.FormatFloat(rs, 'f', -1, 64) { 421 | t.Error(a, b) 422 | } 423 | 424 | ms, err := json.Marshal(x) 425 | if err != nil { 426 | t.Error(err) 427 | } 428 | if string(ms) != s { 429 | t.Error(s, string(ms), x) 430 | } 431 | }) 432 | } 433 | 434 | func ExampleDecimal() { 435 | var BuySP500Price = fp.FromInt(9000) 436 | 437 | input := []byte(`{"sp500": 9000.023}`) 438 | 439 | type Stocks struct { 440 | SP500 fp.Decimal `json:"sp500"` 441 | } 442 | var v Stocks 443 | if err := json.Unmarshal(input, &v); err != nil { 444 | log.Fatal(err) 445 | } 446 | 447 | var amountToBuy fp.Decimal 448 | if v.SP500.GreaterThan(BuySP500Price) { 449 | amountToBuy = amountToBuy.Add(v.SP500.Mul(fp.FromInt(2))) 450 | } 451 | 452 | fmt.Println(amountToBuy) 453 | // Output: 18000.046 454 | } 455 | 456 | func ExampleDecimal_skip_whole_fraction() { 457 | v, _ := fp.FromString("1013.0000") 458 | fmt.Println(v) 459 | // Output: 1013 460 | } 461 | 462 | func ExampleDecimal_skip_trailing_zeros() { 463 | v, _ := fp.FromString("102.0020") 464 | fmt.Println(v) 465 | // Output: 102.002 466 | } 467 | 468 | func ExampleDecimal_Div() { 469 | x, _ := fp.FromString("1.000") 470 | p := x.Div(fp.FromInt(3)) 471 | fmt.Print(p) 472 | // Output: 0.333 473 | } 474 | 475 | func ExampleDecimal_Div_whole() { 476 | x, _ := fp.FromString("1.000") 477 | p := x.Div(fp.FromInt(5)) 478 | fmt.Print(p) 479 | // Output: 0.2 480 | } 481 | 482 | func ExampleDecimal_Mod() { 483 | x, _ := fp.FromString("1.000") 484 | m := x.Mod(fp.FromInt(3)) 485 | fmt.Print(m) 486 | // Output: 0.001 487 | } 488 | 489 | func ExampleDecimal_DivMod() { 490 | x, _ := fp.FromString("1.000") 491 | p, m := x.DivMod(fp.FromInt(3)) 492 | fmt.Print(p, m) 493 | // Output: 0.333 0.001 494 | } 495 | 496 | func ExampleDecimal_DivMod_whole() { 497 | x, _ := fp.FromString("1.000") 498 | p, m := x.DivMod(fp.FromInt(5)) 499 | fmt.Print(p, m) 500 | // Output: 0.2 0 501 | } 502 | 503 | func ExampleFromInt_uint8() { 504 | var x uint8 = 100 505 | v := fp.FromInt(x) 506 | fmt.Print(v) 507 | // Output: 100 508 | } 509 | 510 | func ExampleFromInt_int8() { 511 | var x int8 = -100 512 | v := fp.FromInt(x) 513 | fmt.Print(v) 514 | // Output: -100 515 | } 516 | 517 | func ExampleFromInt_int() { 518 | var x int = -100 519 | v := fp.FromInt(x) 520 | fmt.Print(v) 521 | // Output: -100 522 | } 523 | 524 | func ExampleFromInt_uint() { 525 | var x uint = 100 526 | v := fp.FromInt(x) 527 | fmt.Print(v) 528 | // Output: 100 529 | } 530 | 531 | func ExampleMin() { 532 | min := fp.Min(fp.FromInt(100), fp.FromFloat(0.999), fp.FromFloat(100.001)) 533 | fmt.Print(min) 534 | // Output: 0.999 535 | } 536 | 537 | func ExampleMin_empty() { 538 | defer func() { fmt.Print(recover()) }() 539 | fp.Min() 540 | // Output: min of empty set is undefined 541 | } 542 | 543 | func ExampleMax() { 544 | max := fp.Max(fp.FromInt(100), fp.FromFloat(0.999), fp.FromFloat(100.001)) 545 | fmt.Print(max) 546 | // Output: 100.001 547 | } 548 | 549 | func ExampleMax_empty() { 550 | defer func() { fmt.Print(recover()) }() 551 | fp.Max() 552 | // Output: max of empty set is undefined 553 | } 554 | 555 | func BenchmarkArithmetic(b *testing.B) { 556 | x, _ := fp.FromString("251.231") 557 | y, _ := fp.FromString("21231.001") 558 | 559 | var s, u fp.Decimal 560 | 561 | u = fp.FromInt(5) 562 | 563 | b.Run("add", func(b *testing.B) { 564 | s = fp.Zero 565 | for n := 0; n < b.N; n++ { 566 | s = x.Add(y) 567 | } 568 | }) 569 | 570 | b.Run("div", func(b *testing.B) { 571 | s = fp.Zero 572 | for n := 0; n < b.N; n++ { 573 | s = x.Div(y) 574 | } 575 | }) 576 | 577 | b.Run("divmod", func(b *testing.B) { 578 | s = fp.Zero 579 | u = fp.Zero 580 | for n := 0; n < b.N; n++ { 581 | s, u = x.DivMod(y) 582 | } 583 | }) 584 | 585 | if s == fp.Zero || u == fp.Zero { 586 | b.Error() 587 | } 588 | } 589 | 590 | func TestDecimalMemoryLayout(t *testing.T) { 591 | a, _ := fp.FromString("-1000.123") 592 | if v := unsafe.Sizeof(a); v != 8 { 593 | t.Error(a, v) 594 | } 595 | } 596 | 597 | func TestDecimal_Compare(t *testing.T) { 598 | a, _ := fp.FromString("1.123") 599 | 600 | if b, _ := fp.FromString("1.122"); a.Compare(b) != 1 { 601 | t.Error(a, ">", b) 602 | } 603 | if b, _ := fp.FromString("1.124"); a.Compare(b) != -1 { 604 | t.Error(a, "<", b) 605 | } 606 | if b, _ := fp.FromString("1.123"); a.Compare(b) != 0 { 607 | t.Error(a, "==", b) 608 | } 609 | } 610 | -------------------------------------------------------------------------------- /fp6/fpdecimal_test.go: -------------------------------------------------------------------------------- 1 | package fp6_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "math" 8 | "strconv" 9 | "testing" 10 | "unsafe" 11 | 12 | fp "github.com/nikolaydubina/fpdecimal/fp6" 13 | ) 14 | 15 | const multiplier = 1_000_000 16 | 17 | func FuzzArithmetics(f *testing.F) { 18 | tests := [][2]int64{ 19 | {1, 2}, 20 | {1, -5}, 21 | {1, 0}, 22 | {1100, -2}, 23 | } 24 | for _, tc := range tests { 25 | f.Add(tc[0], tc[1]) 26 | } 27 | f.Fuzz(func(t *testing.T, a, b int64) { 28 | fa := fp.FromIntScaled(a) 29 | fb := fp.FromIntScaled(b) 30 | 31 | v := []bool{ 32 | // sum commutativity 33 | fa.Add(fb) == fb.Add(fa), 34 | 35 | // sum associativity 36 | fp.Zero.Add(fa).Add(fb).Add(fa) == fp.Zero.Add(fb).Add(fa).Add(fa), 37 | 38 | // sum zero 39 | fa == fa.Add(fb).Sub(fb), 40 | fa == fa.Sub(fb).Add(fb), 41 | fp.Zero == fp.Zero.Add(fa).Sub(fa), 42 | 43 | // product identity 44 | fa == fa.Mul(fp.FromInt(1)), 45 | 46 | // product zero 47 | fp.Zero == fa.Mul(fp.FromInt(0)), 48 | 49 | // match number 50 | (a == b) == (fa == fb), 51 | (a == b) == fa.Equal(fb), 52 | a < b == fa.LessThan(fb), 53 | a > b == fa.GreaterThan(fb), 54 | a <= b == fa.LessThanOrEqual(fb), 55 | a >= b == fa.GreaterThanOrEqual(fb), 56 | 57 | // match number convert 58 | fp.FromIntScaled(a+b) == fa.Add(fb), 59 | fp.FromIntScaled(a-b) == fa.Sub(fb), 60 | } 61 | for i, q := range v { 62 | if !q { 63 | t.Error(i, a, b, fa, fb) 64 | } 65 | } 66 | 67 | if b != 0 { 68 | pdiv := fa.Div(fp.FromInt(b)) 69 | p, r := fa.DivMod(fp.FromInt(b)) 70 | if p != pdiv { 71 | t.Error(p, pdiv) 72 | } 73 | if p != fp.FromIntScaled(a/b) { 74 | t.Error(a, b, p, a/b) 75 | } 76 | if fr := fp.FromIntScaled(a % b); r != fr { 77 | t.Error("Mod", "a", a, "b", b, "got", fr, "want", r) 78 | } 79 | } 80 | }) 81 | } 82 | 83 | func FuzzParse_StringSameAsFloat(f *testing.F) { 84 | tests := []float64{ 85 | 0, 86 | 0.100, 87 | 0.101, 88 | 0.010, 89 | 0.001, 90 | 0.0001, 91 | 0.123, 92 | 0.103, 93 | 0.100001, 94 | 12.001, 95 | 12.010, 96 | 12.345, 97 | 1, 98 | 2, 99 | 10, 100 | 12345678, 101 | } 102 | for _, tc := range tests { 103 | f.Add(tc) 104 | f.Add(-tc) 105 | } 106 | f.Fuzz(func(t *testing.T, r float64) { 107 | if r > math.MaxInt64/multiplier || r < math.MinInt64/multiplier { 108 | t.Skip() 109 | } 110 | 111 | // gaps start around these floats 112 | if r > 100_000_000 || r < -100_000_000 { 113 | t.Skip() 114 | } 115 | s := fmt.Sprintf("%.6f", r) 116 | rs, _ := strconv.ParseFloat(s, 64) 117 | 118 | v, err := fp.FromString(s) 119 | if err != nil { 120 | t.Error(err) 121 | } 122 | 123 | if s == "-0.000000" || s == "0.000000" || rs == 0 || rs == -0 || (rs > -0.000001 && rs < 0.000001) { 124 | if v.String() != "0" { 125 | t.Errorf("s('0') != Decimal.String(%#v) of fp3(%#v) float32(%#v) .3f-float32(%#v)", v.String(), v, r, s) 126 | } 127 | return 128 | } 129 | 130 | if s, fs := strconv.FormatFloat(rs, 'f', -1, 64), v.String(); s != fs { 131 | t.Error(s, fs, r, v) 132 | } 133 | }) 134 | } 135 | 136 | func FuzzParse_StringRaw(f *testing.F) { 137 | tests := []string{ 138 | "123.456", 139 | "0.123", 140 | "0.1", 141 | "0.01", 142 | "0.001", 143 | "0.000", 144 | "0.123.2", 145 | "0..1", 146 | "0.1.2", 147 | "123.1o2", 148 | "--123", 149 | "00000.123", 150 | "-", 151 | "", 152 | "123456", 153 | } 154 | for _, tc := range tests { 155 | f.Add(tc) 156 | f.Add("-" + tc) 157 | } 158 | f.Fuzz(func(t *testing.T, s string) { 159 | v, err := fp.FromString(s) 160 | if err != nil { 161 | if v != fp.Zero { 162 | t.Errorf("has to be 0 on error") 163 | } 164 | return 165 | } 166 | }) 167 | } 168 | 169 | func FuzzToFloat(f *testing.F) { 170 | tests := []float64{ 171 | 0, 172 | 0.001, 173 | 0.000001, 174 | 1, 175 | 123.000456, 176 | } 177 | for _, tc := range tests { 178 | f.Add(tc) 179 | f.Add(-tc) 180 | } 181 | f.Fuzz(func(t *testing.T, v float64) { 182 | a := fp.FromFloat(v) 183 | 184 | if delta := math.Abs(v - a.Float64()); delta > 0.00100001 { 185 | t.Error("a", a, "a.f64", a.Float64(), "v", v, "delta", delta) 186 | } 187 | }) 188 | } 189 | 190 | func FuzzScaled(f *testing.F) { 191 | tests := []float64{ 192 | 0, 193 | 0.001, 194 | 0.000001, 195 | 1, 196 | 123.000456, 197 | } 198 | for _, tc := range tests { 199 | f.Add(tc) 200 | f.Add(-tc) 201 | } 202 | f.Fuzz(func(t *testing.T, v float64) { 203 | a := fp.FromFloat(v) 204 | 205 | if int64(v*multiplier) != a.Scaled() { 206 | t.Error(a, a.Scaled(), int64(v*multiplier)) 207 | } 208 | }) 209 | } 210 | 211 | var floatsForTests = []struct { 212 | name string 213 | vals []string 214 | }{ 215 | { 216 | name: "small", 217 | vals: []string{ 218 | "123.456", 219 | "123.000456", 220 | "0.123", 221 | "0.012", 222 | "0.001", 223 | "0.982", 224 | "0.101", 225 | "10", 226 | "11", 227 | "1", 228 | }, 229 | }, 230 | { 231 | name: "large", 232 | vals: []string{ 233 | "123123123112312.001232", 234 | "5341320482340234.000123", 235 | }, 236 | }, 237 | } 238 | 239 | func BenchmarkParse(b *testing.B) { 240 | var s fp.Decimal 241 | var err error 242 | 243 | b.Run("fromString", func(b *testing.B) { 244 | for _, tc := range floatsForTests { 245 | b.ResetTimer() 246 | b.Run(tc.name, func(b *testing.B) { 247 | for n := 0; n < b.N; n++ { 248 | s, err = fp.FromString(tc.vals[n%len(tc.vals)]) 249 | if err != nil || s == fp.Zero { 250 | b.Error(s, err) 251 | } 252 | } 253 | }) 254 | } 255 | }) 256 | 257 | b.Run("UnmarshalJSON", func(b *testing.B) { 258 | for _, tc := range floatsForTests { 259 | var vals [][]byte 260 | for i := range tc.vals { 261 | vals = append(vals, []byte(tc.vals[i])) 262 | } 263 | 264 | b.ResetTimer() 265 | b.Run(tc.name, func(b *testing.B) { 266 | for n := 0; n < b.N; n++ { 267 | if err = s.UnmarshalJSON(vals[n%len(vals)]); err != nil || s == fp.Zero { 268 | b.Error(s, err) 269 | } 270 | } 271 | }) 272 | } 273 | }) 274 | } 275 | 276 | func BenchmarkPrint(b *testing.B) { 277 | var s string 278 | for _, tc := range floatsForTests { 279 | tests := make([]fp.Decimal, 0, len(tc.vals)) 280 | for _, q := range tc.vals { 281 | v, err := fp.FromString(q) 282 | if err != nil { 283 | b.Error(err) 284 | } 285 | tests = append(tests, v) 286 | tests = append(tests, fp.Zero.Sub(v)) 287 | } 288 | 289 | b.Run("String", func(b *testing.B) { 290 | b.ResetTimer() 291 | b.Run(tc.name, func(b *testing.B) { 292 | for n := 0; n < b.N; n++ { 293 | s = tests[n%len(tc.vals)].String() 294 | if s == "" { 295 | b.Error("empty str") 296 | } 297 | } 298 | }) 299 | }) 300 | 301 | b.Run("Marshal", func(b *testing.B) { 302 | b.ResetTimer() 303 | b.Run(tc.name, func(b *testing.B) { 304 | for n := 0; n < b.N; n++ { 305 | s = tests[n%len(tc.vals)].String() 306 | if s == "" { 307 | b.Error("empty str") 308 | } 309 | } 310 | }) 311 | }) 312 | } 313 | } 314 | 315 | func TestUnmarshalJSON(t *testing.T) { 316 | type MyType struct { 317 | TeslaStockPrice fp.Decimal `json:"tesla-stock-price"` 318 | } 319 | 320 | tests := []struct { 321 | json string 322 | v fp.Decimal 323 | s string 324 | }{ 325 | { 326 | json: `{"tesla-stock-price": 9000.000001}`, 327 | v: fp.FromFloat(9000.000001), 328 | s: `9000.000001`, 329 | }, 330 | } 331 | for _, tc := range tests { 332 | t.Run(tc.json, func(t *testing.T) { 333 | var v MyType 334 | err := json.Unmarshal([]byte(tc.json), &v) 335 | if err != nil { 336 | t.Error(err) 337 | } 338 | if s := v.TeslaStockPrice.String(); s != tc.s { 339 | t.Errorf("s(%#v) != tc.s(%#v)", s, tc.s) 340 | } 341 | }) 342 | } 343 | } 344 | 345 | func TestUMarshalJSON(t *testing.T) { 346 | type MyType struct { 347 | TeslaStockPrice fp.Decimal `json:"tesla-stock-price"` 348 | } 349 | 350 | t.Run("when nil struct, then error", func(t *testing.T) { 351 | var v *MyType 352 | err := json.Unmarshal([]byte(`{"tesla-stock-price": 9000.000001}`), v) 353 | if err == nil { 354 | t.Error("expected error") 355 | } 356 | }) 357 | 358 | t.Run("when nil value, then error", func(t *testing.T) { 359 | var v *fp.Decimal 360 | err := json.Unmarshal([]byte(`{"tesla-stock-price": 9000.000001}`), &v) 361 | if err == nil { 362 | t.Error("expected error") 363 | } 364 | }) 365 | 366 | t.Run("when nil const of type, then error", func(t *testing.T) { 367 | err := json.Unmarshal([]byte(`{"tesla-stock-price": 9000.000001}`), (*fp.Decimal)(nil)) 368 | if err == nil { 369 | t.Error("expected error") 370 | } 371 | }) 372 | 373 | t.Run("ok", func(t *testing.T) { 374 | var v MyType 375 | err := json.Unmarshal([]byte(`{"tesla-stock-price": 9000.000001}`), &v) 376 | if err != nil { 377 | t.Error(err) 378 | } 379 | e := MyType{fp.FromIntScaled(9000000001)} 380 | if v != e { 381 | t.Error(v) 382 | } 383 | }) 384 | } 385 | 386 | func FuzzJSON(f *testing.F) { 387 | type MyType struct { 388 | A fp.Decimal `json:"a"` 389 | } 390 | 391 | tests := []float32{ 392 | 123456, 393 | 0, 394 | 1.1, 395 | 0.123, 396 | 0.0123, 397 | 0.000123, 398 | } 399 | for _, tc := range tests { 400 | f.Add(tc) 401 | f.Add(-tc) 402 | } 403 | f.Fuzz(func(t *testing.T, v float32) { 404 | if v > math.MaxInt64/multiplier || v < math.MinInt64/multiplier { 405 | t.Skip() 406 | } 407 | 408 | b := fmt.Sprintf("%.3f", v) 409 | rs, _ := strconv.ParseFloat(b, 64) 410 | s := `{"a":` + strconv.FormatFloat(rs, 'f', -1, 64) + `}` 411 | 412 | var x MyType 413 | err := json.Unmarshal([]byte(s), &x) 414 | if err != nil { 415 | t.Error(err, s) 416 | } 417 | 418 | if b == "-0.000000" || b == "0.000000" || rs == 0 || rs == -0 || (rs > 0.000001 && rs < 0.000001) { 419 | if x.A.String() != "0" { 420 | t.Error(b, x) 421 | } 422 | return 423 | } 424 | 425 | if a := x.A.String(); a != strconv.FormatFloat(rs, 'f', -1, 64) { 426 | t.Error(a, b) 427 | } 428 | 429 | ms, err := json.Marshal(x) 430 | if err != nil { 431 | t.Error(err) 432 | } 433 | if string(ms) != s { 434 | t.Error(s, string(ms), x) 435 | } 436 | }) 437 | } 438 | 439 | func ExampleDecimal() { 440 | var BuySP500Price = fp.FromInt(9000) 441 | 442 | input := []byte(`{"sp500": 9000.000023}`) 443 | 444 | type Stocks struct { 445 | SP500 fp.Decimal `json:"sp500"` 446 | } 447 | var v Stocks 448 | if err := json.Unmarshal(input, &v); err != nil { 449 | log.Fatal(err) 450 | } 451 | 452 | var amountToBuy fp.Decimal 453 | if v.SP500.GreaterThan(BuySP500Price) { 454 | amountToBuy = amountToBuy.Add(v.SP500.Mul(fp.FromInt(2))) 455 | } 456 | 457 | fmt.Println(amountToBuy) 458 | // Output: 18000.000046 459 | } 460 | 461 | func ExampleDecimal_skip_whole_fraction() { 462 | v, _ := fp.FromString("1013.0000000") 463 | fmt.Println(v) 464 | // Output: 1013 465 | } 466 | 467 | func ExampleDecimal_skip_trailing_zeros() { 468 | v, _ := fp.FromString("102.0000020") 469 | fmt.Println(v) 470 | // Output: 102.000002 471 | } 472 | 473 | func ExampleDecimal_Div() { 474 | x, _ := fp.FromString("1.000000") 475 | p := x.Div(fp.FromInt(3)) 476 | fmt.Print(p) 477 | // Output: 0.333333 478 | } 479 | 480 | func ExampleDecimal_Div_whole() { 481 | x, _ := fp.FromString("1.000000") 482 | p := x.Div(fp.FromInt(5)) 483 | fmt.Print(p) 484 | // Output: 0.2 485 | } 486 | 487 | func ExampleDecimal_Mod() { 488 | x, _ := fp.FromString("1.000000") 489 | m := x.Mod(fp.FromInt(3)) 490 | fmt.Print(m) 491 | // Output: 0.000001 492 | } 493 | 494 | func ExampleDecimal_DivMod() { 495 | x, _ := fp.FromString("1.000000") 496 | p, m := x.DivMod(fp.FromInt(3)) 497 | fmt.Print(p, m) 498 | // Output: 0.333333 0.000001 499 | } 500 | 501 | func ExampleDecimal_DivMod_whole() { 502 | x, _ := fp.FromString("1.000000") 503 | p, m := x.DivMod(fp.FromInt(5)) 504 | fmt.Print(p, m) 505 | // Output: 0.2 0 506 | } 507 | 508 | func ExampleFromInt_uint8() { 509 | var x uint8 = 100 510 | v := fp.FromInt(x) 511 | fmt.Print(v) 512 | // Output: 100 513 | } 514 | 515 | func ExampleFromInt_int8() { 516 | var x int8 = -100 517 | v := fp.FromInt(x) 518 | fmt.Print(v) 519 | // Output: -100 520 | } 521 | 522 | func ExampleFromInt_int() { 523 | var x int = -100 524 | v := fp.FromInt(x) 525 | fmt.Print(v) 526 | // Output: -100 527 | } 528 | 529 | func ExampleFromInt_uint() { 530 | var x uint = 100 531 | v := fp.FromInt(x) 532 | fmt.Print(v) 533 | // Output: 100 534 | } 535 | 536 | func ExampleMin() { 537 | min := fp.Min(fp.FromInt(100), fp.FromFloat(0.999999), fp.FromFloat(100.000001)) 538 | fmt.Print(min) 539 | // Output: 0.999999 540 | } 541 | 542 | func ExampleMin_empty() { 543 | defer func() { fmt.Print(recover()) }() 544 | fp.Min() 545 | // Output: min of empty set is undefined 546 | } 547 | 548 | func ExampleMax() { 549 | max := fp.Max(fp.FromInt(100), fp.FromFloat(0.999999), fp.FromFloat(100.000001)) 550 | fmt.Print(max) 551 | // Output: 100.000001 552 | } 553 | 554 | func ExampleMax_empty() { 555 | defer func() { fmt.Print(recover()) }() 556 | fp.Max() 557 | // Output: max of empty set is undefined 558 | } 559 | 560 | func BenchmarkArithmetic(b *testing.B) { 561 | x, _ := fp.FromString("251.231") 562 | y, _ := fp.FromString("21231.001") 563 | 564 | var s, u fp.Decimal 565 | 566 | u = fp.FromInt(5) 567 | 568 | b.Run("add", func(b *testing.B) { 569 | s = fp.Zero 570 | for n := 0; n < b.N; n++ { 571 | s = x.Add(y) 572 | } 573 | }) 574 | 575 | b.Run("div", func(b *testing.B) { 576 | s = fp.Zero 577 | for n := 0; n < b.N; n++ { 578 | s = x.Div(y) 579 | } 580 | }) 581 | 582 | b.Run("divmod", func(b *testing.B) { 583 | s = fp.Zero 584 | u = fp.Zero 585 | for n := 0; n < b.N; n++ { 586 | s, u = x.DivMod(y) 587 | } 588 | }) 589 | 590 | if s == fp.Zero || u == fp.Zero { 591 | b.Error() 592 | } 593 | } 594 | 595 | func TestDecimalMemoryLayout(t *testing.T) { 596 | a, _ := fp.FromString("-1000.123") 597 | if v := unsafe.Sizeof(a); v != 8 { 598 | t.Error(a, v) 599 | } 600 | } 601 | 602 | func TestDecimal_Compare(t *testing.T) { 603 | a, _ := fp.FromString("1.000123") 604 | 605 | if b, _ := fp.FromString("1.000122"); a.Compare(b) != 1 { 606 | t.Error(a, ">", b) 607 | } 608 | if b, _ := fp.FromString("1.000124"); a.Compare(b) != -1 { 609 | t.Error(a, "<", b) 610 | } 611 | if b, _ := fp.FromString("1.000123"); a.Compare(b) != 0 { 612 | t.Error(a, "==", b) 613 | } 614 | } 615 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fixed-Point Decimals 2 | 3 | > To use in money, look at [github.com/nikolaydubina/fpmoney](https://github.com/nikolaydubina/fpmoney) 4 | 5 | > _Be Precise. Using floats to represent currency is almost criminal. — Robert.C.Martin, "Clean Code" p.301_ 6 | 7 | [![codecov](https://codecov.io/gh/nikolaydubina/fpdecimal/branch/main/graph/badge.svg?token=0pf0P5qloX)](https://codecov.io/gh/nikolaydubina/fpdecimal) 8 | [![Go Reference](https://pkg.go.dev/badge/github.com/nikolaydubina/fpdecimal.svg)](https://pkg.go.dev/github.com/nikolaydubina/fpdecimal) 9 | [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/avelino/awesome-go#financial) 10 | [![Go Report Card](https://goreportcard.com/badge/github.com/nikolaydubina/fpdecimal)](https://goreportcard.com/report/github.com/nikolaydubina/fpdecimal) 11 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/nikolaydubina/fpdecimal/badge)](https://securityscorecards.dev/viewer/?uri=github.com/nikolaydubina/fpdecimal) 12 | 13 | * `int64` inside 14 | * does not use `float` neither in parsing nor printing 15 | * as fast as `int64` in parsing, printing, arithmetics — 3x faser `float`, 20x faster [shopspring/decimal](https://github.com/shopspring/decimal), 30x faster `fmt` 16 | * zero-overhead 17 | * preventing error-prone fixed-point arithmetics 18 | * Fuzz tests, Benchmarks 19 | * JSON 20 | * 200LOC 21 | 22 | ```go 23 | import fp "github.com/nikolaydubina/fpdecimal" 24 | 25 | var BuySP500Price = fp.FromInt(9000) 26 | 27 | input := []byte(`{"sp500": 9000.023}`) 28 | 29 | type Stocks struct { 30 | SP500 fp.Decimal `json:"sp500"` 31 | } 32 | var v Stocks 33 | if err := json.Unmarshal(input, &v); err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | var amountToBuy fp.Decimal 38 | if v.SP500.GreaterThan(BuySP500Price) { 39 | amountToBuy = amountToBuy.Add(v.SP500.Mul(fp.FromInt(2))) 40 | } 41 | 42 | fmt.Println(amountToBuy) 43 | // Output: 18000.046 44 | ``` 45 | 46 | ### Implementation 47 | 48 | Parsing and Printing is expensive operation and requires a lot of code. 49 | However, if you know that your numbers are always small and simple and you do not care or do not permit lots of fractions like `-1234.567`, then parsing and printing can be greatly simplified. 50 | Code is heavily influenced by hot-path from Go core `strconv` package. 51 | 52 | It is wrapped into struct to prevent bugs: 53 | - block multiplication by `fpdecimal` type, which leads to increase in decimal fractions and loose of precision 54 | - block additions of untyped constants, which leads to errors if you forget to scale by factor 55 | 56 | ### Benchmarks 57 | 58 | Parse 59 | ``` 60 | $ go test -bench=BenchmarkParse -benchtime=5s -benchmem . 61 | goos: darwin 62 | goarch: arm64 63 | pkg: github.com/nikolaydubina/fpdecimal 64 | BenchmarkParse/fromString/small-10 534307098 11.36 ns/op 0 B/op 0 allocs/op 65 | BenchmarkParse/fromString/large-10 254741558 23.42 ns/op 0 B/op 0 allocs/op 66 | BenchmarkParse/UnmarshalJSON/small-10 816873427 7.32 ns/op 0 B/op 0 allocs/op 67 | BenchmarkParse/UnmarshalJSON/large-10 272173255 22.16 ns/op 0 B/op 0 allocs/op 68 | BenchmarkParse_int_strconv_Atoi/small-10 1000000000 4.87 ns/op 0 B/op 0 allocs/op 69 | BenchmarkParse_int_strconv_Atoi/large-10 420536834 14.31 ns/op 0 B/op 0 allocs/op 70 | BenchmarkParse_int_strconv_ParseInt/small/int32-10 561137575 10.67 ns/op 0 B/op 0 allocs/op 71 | BenchmarkParse_int_strconv_ParseInt/small/int64-10 564200026 10.64 ns/op 0 B/op 0 allocs/op 72 | BenchmarkParse_int_strconv_ParseInt/large/int64-10 219626983 27.17 ns/op 0 B/op 0 allocs/op 73 | BenchmarkParse_float_strconv_ParseFloat/small/float32-10 345666214 17.36 ns/op 0 B/op 0 allocs/op 74 | BenchmarkParse_float_strconv_ParseFloat/small/float64-10 339620222 17.68 ns/op 0 B/op 0 allocs/op 75 | BenchmarkParse_float_strconv_ParseFloat/large/float32-10 128824344 46.68 ns/op 0 B/op 0 allocs/op 76 | BenchmarkParse_float_strconv_ParseFloat/large/float64-10 128140617 46.89 ns/op 0 B/op 0 allocs/op 77 | BenchmarkParse_float_fmt_Sscanf/small-10 21202892 281.6 ns/op 69 B/op 2 allocs/op 78 | BenchmarkParse_float_fmt_Sscanf/large-10 10074237 599.2 ns/op 88 B/op 3 allocs/op 79 | PASS 80 | ok github.com/nikolaydubina/fpdecimal 116.249s 81 | ``` 82 | 83 | Print 84 | ``` 85 | $ go test -bench=BenchmarkPrint -benchtime=5s -benchmem . 86 | goos: darwin 87 | goarch: arm64 88 | pkg: github.com/nikolaydubina/fpdecimal 89 | BenchmarkPrint/small-10 191982066 31.24 ns/op 8 B/op 1 allocs/op 90 | BenchmarkPrint/large-10 150874335 39.89 ns/op 24 B/op 1 allocs/op 91 | BenchmarkPrint_int_strconv_Itoa/small-10 446302868 13.39 ns/op 3 B/op 0 allocs/op 92 | BenchmarkPrint_int_strconv_Itoa/large-10 237484774 25.20 ns/op 18 B/op 1 allocs/op 93 | BenchmarkPrint_int_strconv_FormatInt/small-10 444861666 13.70 ns/op 3 B/op 0 allocs/op 94 | BenchmarkPrint_float_strconv_FormatFloat/small/float32-10 55003357 104.2 ns/op 31 B/op 2 allocs/op 95 | BenchmarkPrint_float_strconv_FormatFloat/small/float64-10 43565430 137.4 ns/op 31 B/op 2 allocs/op 96 | BenchmarkPrint_float_strconv_FormatFloat/large/float32-10 64069650 92.07 ns/op 48 B/op 2 allocs/op 97 | BenchmarkPrint_float_strconv_FormatFloat/large/float64-10 68441746 87.36 ns/op 48 B/op 2 allocs/op 98 | BenchmarkPrint_float_fmt_Sprintf/small-10 46503666 127.7 ns/op 16 B/op 2 allocs/op 99 | BenchmarkPrint_float_fmt_Sprintf/large-10 51764224 115.8 ns/op 28 B/op 2 allocs/op 100 | PASS 101 | ok github.com/nikolaydubina/fpdecimal 79.192s 102 | ``` 103 | 104 | Arithmetics 105 | ``` 106 | $ go test -bench=BenchmarkArithmetic -benchtime=5s -benchmem . 107 | goos: darwin 108 | goarch: arm64 109 | pkg: github.com/nikolaydubina/fpdecimal 110 | BenchmarkArithmetic/add-10 1000000000 0.316 ns/op 0 B/op 0 allocs/op 111 | BenchmarkArithmetic/div-10 1000000000 0.950 ns/op 0 B/op 0 allocs/op 112 | BenchmarkArithmetic/divmod-10 1000000000 1.890 ns/op 0 B/op 0 allocs/op 113 | BenchmarkArithmetic_int64/add-10 1000000000 0.314 ns/op 0 B/op 0 allocs/op 114 | BenchmarkArithmetic_int64/div-10 1000000000 0.316 ns/op 0 B/op 0 allocs/op 115 | BenchmarkArithmetic_int64/divmod-10 1000000000 1.261 ns/op 0 B/op 0 allocs/op 116 | BenchmarkArithmetic_int64/mod-10 1000000000 0.628 ns/op 0 B/op 0 allocs/op 117 | PASS 118 | ok github.com/nikolaydubina/fpdecimal 6.721s 119 | ``` 120 | 121 | ## References 122 | 123 | - [Fixed-Point Arithmetic Wiki](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) 124 | - [shopspring/decimal](https://github.com/shopspring/decimal) 125 | 126 | ## Appendix A: Comparison to other libraries 127 | 128 | - https://github.com/shopspring/decimal solves arbitrary precision, fpdecimal solves only simple small decimals 129 | - https://github.com/Rhymond/go-money solves typed number (currency), decodes through `interface{}` and float64, no precision in decoding, expects encoding to be in cents 130 | 131 | ## Appendix B: Benchmarking [shopspring/decimal](https://github.com/shopspring/decimal) 132 | 133 | `2022-05-28` 134 | ``` 135 | $ go test -bench=. -benchtime=5s -benchmem ./... 136 | goos: darwin 137 | goarch: arm64 138 | pkg: github.com/shopspring/decimal 139 | BenchmarkNewFromFloatWithExponent-10 59701516 97.7 ns/op 106 B/op 4 allocs/op 140 | BenchmarkNewFromFloat-10 14771503 410.3 ns/op 67 B/op 2 allocs/op 141 | BenchmarkNewFromStringFloat-10 16246342 375.2 ns/op 175 B/op 5 allocs/op 142 | Benchmark_FloorFast-10 1000000000 2.1 ns/op 0 B/op 0 allocs/op 143 | Benchmark_FloorRegular-10 53857244 106.3 ns/op 112 B/op 6 allocs/op 144 | Benchmark_DivideOriginal-10 7 715322768 ns/op 737406446 B/op 30652495 allocs/op 145 | Benchmark_DivideNew-10 22 262893689 ns/op 308046721 B/op 12054905 allocs/op 146 | BenchmarkDecimal_RoundCash_Five-10 9311530 636.5 ns/op 616 B/op 28 allocs/op 147 | Benchmark_Cmp-10 44 133191579 ns/op 24 B/op 1 allocs/op 148 | Benchmark_decimal_Decimal_Add_different_precision-10 31561636 176.6 ns/op 280 B/op 9 allocs/op 149 | Benchmark_decimal_Decimal_Sub_different_precision-10 36892767 164.4 ns/op 240 B/op 9 allocs/op 150 | Benchmark_decimal_Decimal_Add_same_precision-10 134831919 44.9 ns/op 80 B/op 2 allocs/op 151 | Benchmark_decimal_Decimal_Sub_same_precision-10 134902627 43.1 ns/op 80 B/op 2 allocs/op 152 | BenchmarkDecimal_IsInteger-10 92543083 66.1 ns/op 8 B/op 1 allocs/op 153 | BenchmarkDecimal_NewFromString-10 827455 7382 ns/op 3525 B/op 216 allocs/op 154 | BenchmarkDecimal_NewFromString_large_number-10 212538 28836 ns/op 16820 B/op 360 allocs/op 155 | BenchmarkDecimal_ExpHullAbraham-10 10000 572091 ns/op 486628 B/op 568 allocs/op 156 | BenchmarkDecimal_ExpTaylor-10 26343 222915 ns/op 431226 B/op 3172 allocs/op 157 | PASS 158 | ok github.com/shopspring/decimal 123.541sa 159 | ``` 160 | 161 | ## Appendix C: Why this is good fit for money? 162 | 163 | There are only ~200 currencies in the world. 164 | All currencies have at most 3 decimal digits, thus it is sufficient to handle 3 decimal fractions. 165 | Next, currencies without decimal digits are typically 1000x larger than dollar, but even then maximum number that fits into `int64` (without 3 decimal fractions) is `9 223 372 036 854 775.807` which is ~9 quadrillion. This should be enough for most operations with money. 166 | 167 | ## Appendix D: Is it safe to use arithmetic operators in Go? 168 | 169 | Sort of... 170 | 171 | In one of iterations, I did Type Alias, but it required some effort to use it carefully. 172 | 173 | Operations with defined types (variables) will fail. 174 | ```go 175 | var a int64 176 | var b fpdecimal.FromInt(1000) 177 | 178 | // does not compile 179 | a + b 180 | ``` 181 | 182 | However, untyped constants will be resolved to underlying type `int64` and will be allowed. 183 | ```go 184 | const a 10000 185 | var b fpdecimal.FromInt(1000) 186 | 187 | // compiles 188 | a + b 189 | 190 | // also compiles 191 | b - 42 192 | 193 | // this one too 194 | b *= 23 195 | ``` 196 | 197 | Is this a problem? 198 | * For multiplication and division - yes, it can be. You have to be careful not to multiply two `fpdecimal` numbers, since scaling factor will quadruple. Multiplying by constants is ok tho. 199 | * For addition substraction - yes, it can be. You have to be careful and remind yourself that constants would be reduced 1000x. 200 | 201 | Both of this can be addressed at compile time by providing linter. 202 | This can be also addressed by wrapping into a struct and defining methods. 203 | Formed is hard to achieve in Go, due to lack of operator overload and lots of work required to write AST parser. 204 | Later has been implemented in this pacakge, and, as benchmarks show, without any extra memory or calls overhead as compared to `int64`. 205 | 206 | ## Appendix E: Print into destination 207 | 208 | To avoid mallocs, it is advantageous to print formatted value to pre-allocated destination. 209 | Similarly, to `strconv.AppendInt`, we provide `AppendFixedPointDecimal`. 210 | This is utilized in `github.com/nikolaydubina/fpmoney` package. 211 | 212 | ``` 213 | BenchmarkFixedPointDecimalToString/small-10 28522474 35.43 ns/op 24 B/op 1 allocs/op 214 | BenchmarkFixedPointDecimalToString/large-10 36883687 32.32 ns/op 24 B/op 1 allocs/op 215 | BenchmarkAppendFixedPointDecimal/small-10 38105520 30.51 ns/op 117 B/op 0 allocs/op 216 | BenchmarkAppendFixedPointDecimal/large-10 55147478 29.52 ns/op 119 B/op 0 allocs/op 217 | ``` 218 | 219 | ## Appendix F: DivMod notation 220 | 221 | In early versions, `Div` and `Mul` operated on `int` and `Div` returned remainder. 222 | As recommended by @vanodevium and more in line with other common libraries, notation is changed. 223 | Bellow is survey as of 2023-05-18. 224 | 225 | Go, https://pkg.go.dev/math/big 226 | ```go 227 | func (z *Int) Div(x, y *Int) *Int 228 | func (z *Int) DivMod(x, y, m *Int) (*Int, *Int) 229 | func (z *Int) Mod(x, y *Int) *Int 230 | ``` 231 | 232 | Go, github.com/shopspring/decimal 233 | ```go 234 | func (d Decimal) Div(d2 Decimal) Decimal 235 | // X no DivMod 236 | func (d Decimal) Mod(d2 Decimal) Decimal 237 | func (d Decimal) DivRound(d2 Decimal, precision int32) Decimal 238 | ``` 239 | 240 | Python, https://docs.python.org/3/library/decimal.html 241 | ```python 242 | divide(x, y) number 243 | divide_int(x, y) number // truncates 244 | divmod(x, y) number 245 | remainder(x, y) number 246 | ``` 247 | 248 | Pytorch, https://pytorch.org/docs/stable/generated/torch.div.html 249 | ```python 250 | torch.div(input, other, *, rounding_mode=None, out=None) → [Tensor] // discards remainder 251 | torch.remainder(input, other, *, out=None) → [Tensor] // remainder 252 | ``` 253 | 254 | numpy, https://numpy.org/doc/stable/reference/generated/numpy.divmod.html 255 | ```python 256 | np.divmod(x, y) (number, number) // is equivalent to (x // y, x % y 257 | np.mod(x, y) number 258 | np.remainder(x, y) number 259 | np.divide(x, y) number 260 | np.true_divide(x, y) number // same as divide 261 | np.floor_divide(x, y) number // rounding down 262 | ``` 263 | 264 | ## Appendix G: generics switch for decimal counting 265 | 266 | Go does not support numerics in templates. However, defining multiple types each associated with specific number of decimals and passing them to functions and defining constraint as union of these types — is an attractive option. 267 | This does not work well since Go does not support switch case (casting generic) back to integer well. 268 | 269 | ## Appendix H: `string` vs `[]byte` in interface 270 | 271 | The typical usage of parsing number is through some JSON or other mechanism. Those APIs are dealing with `[]byte`. 272 | Now, conversion from `[]byte` to `string` requires to copy data, since `string` is immutable. 273 | To improve performance, we are using `[]byte` in signatures. 274 | 275 | Using `string` 276 | ``` 277 | BenchmarkParse/fromString/small-10 831217767 7.07 ns/op 0 B/op 0 allocs/op 278 | BenchmarkParse/fromString/large-10 275009497 21.79 ns/op 0 B/op 0 allocs/op 279 | BenchmarkParse/UnmarshalJSON/small-10 553035127 10.98 ns/op 0 B/op 0 allocs/op 280 | BenchmarkParse/UnmarshalJSON/large-10 248815030 24.14 ns/op 0 B/op 0 allocs/op 281 | ``` 282 | 283 | Using `[]byte` 284 | ``` 285 | BenchmarkParse/fromString/small-10 523937236 11.32 ns/op 0 B/op 0 allocs/op 286 | BenchmarkParse/fromString/large-10 257542226 23.23 ns/op 0 B/op 0 allocs/op 287 | BenchmarkParse/UnmarshalJSON/small-10 809793006 7.31 ns/op 0 B/op 0 allocs/op 288 | BenchmarkParse/UnmarshalJSON/large-10 272087984 22.04 ns/op 0 B/op 0 allocs/op 289 | ``` 290 | 291 | ## Appendix F: dynamic pkg level fraction digits 292 | 293 | This is very bug prone. In fact, this was observd in production, [issue](https://github.com/nikolaydubina/fpdecimal/issues/26). 294 | 295 | Consider: 296 | 297 | 1. package A. init map with .FromInt 298 | 2. package B imports A and sets in init() num fraction digits 299 | 3. package B sees values in package A initialized with different fraction digits 300 | 4. 💥 301 | 302 | Therefore, we are inlining fraction digits into most common fractions. 303 | 304 | - 3 is enough to represent all currencies 305 | - 3 is enough for SI units conversion ladder 306 | - 6 is enough for 10cm x 10cm of (lat,long) 307 | --------------------------------------------------------------------------------