├── .travis.yml ├── CHANGELOG.md ├── go.mod ├── .gitignore ├── README.md ├── Makefile ├── .github └── workflows │ └── test.yaml ├── go.sum ├── scripts └── release.sh ├── LICENSE ├── decimal.go └── decimal_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.2 5 | - 1.3 6 | - 1.4 7 | - tip 8 | 9 | install: 10 | - go build . 11 | 12 | script: 13 | - go test -v 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Big Release notes 2 | 3 | ## 0.7.0 4 | * Add support for NaN values 5 | 6 | ## 0.6.0 7 | * Add MaxSlice and MinSlice methods 8 | 9 | ## 0.5.0 10 | * Add NewFromInt method 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sdcoffey/big 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/pmezard/go-difflib v1.0.0 // indirect 8 | github.com/stretchr/testify v1.1.4 9 | ) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | vendor/* 16 | .idea/* 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BIG 2 | 3 | Big is a simple, immuatable wrapper around golang's built-in `*big.Float` type desinged to offer a more user-friendly API and immutability guarantees at the cost of some runtime performance. 4 | 5 | ### Example 6 | 7 | Usage is dead simple: 8 | ```go 9 | dec := big.NewDecimal(1.24) 10 | addend := big.NewDecimal(3.14) 11 | 12 | dec.Add(addend).String() // prints "4.38" 13 | ``` 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | files := $(shell find . -name "*.go" | grep -v vendor) 2 | 3 | bootstrap: 4 | go install -v golang.org/x/lint/golint@latest 5 | go install -v golang.org/x/tools/...@latest 6 | go install -v honnef.co/go/tools/cmd/staticcheck@latest 7 | 8 | lint: 9 | golint -set_exit_status 10 | staticcheck github.com/sdcoffey/big 11 | 12 | clean: 13 | goimports -w $(files) 14 | 15 | test: clean 16 | go test -v 17 | 18 | release: clean test 19 | ./scripts/release.sh -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Install Go 8 | uses: actions/setup-go@v2 9 | with: 10 | go-version: 1.16 11 | - name: Checkout Code 12 | uses: actions/checkout@v2 13 | - name: Bootstrap environment 14 | run: make bootstrap 15 | - name: Lint 16 | run: make lint 17 | - name: Run Tests 18 | run: make test 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.1.4 h1:ToftOQTytwshuOSj6bDSolVUa3GINfJP/fg3OkkOzQQ= 6 | github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 7 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euf -o pipefail 4 | 5 | echo -n "Version: " 6 | read newversion 7 | 8 | tag_count=`git tag --list | grep "$newversion" | wc -l | tr -d '[:space:]' || true` 9 | 10 | if [[ $tag_count -gt 0 ]]; then 11 | echo "Tag: $newversion already exists" 12 | exit 1 13 | else 14 | echo "Releasing $newversion" 15 | fi 16 | 17 | echo "Update CHANGELOG.md and press enter" 18 | read 19 | 20 | git add CHANGELOG.md 21 | 22 | added_count=`git status --porcelain | grep "CHANGELOG.md" | wc -l | tr -d '[:space:]' || true` 23 | if [[ $added_count -gt 0 ]]; then 24 | git commit -m"Release version $newversion" 25 | fi 26 | 27 | git tag "v$newversion" -m "v$newversion" 28 | 29 | git push origin main 30 | git push --tags 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Steve Coffey 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 | -------------------------------------------------------------------------------- /decimal.go: -------------------------------------------------------------------------------- 1 | package big 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "math" 9 | "math/big" 10 | ) 11 | 12 | var ( 13 | flZero = *big.NewFloat(0) 14 | 15 | // NaN == Not a Number 16 | NaN = NewDecimal(math.NaN()) 17 | 18 | // ZERO == 0 19 | ZERO = NewFromString("0") 20 | 21 | // ONE == 1 22 | ONE = NewFromString("1") 23 | 24 | // TEN == 10 25 | TEN = NewFromString("10") 26 | 27 | // MarshalQuoted - can toggle this to true to marshal values as strings 28 | MarshalQuoted = false 29 | ) 30 | 31 | // Decimal is the main exported type. It is a simple, immutable wrapper around a *big.Float 32 | type Decimal struct { 33 | fl *big.Float 34 | } 35 | 36 | // NewDecimal creates a new Decimal type from a float value. 37 | func NewDecimal(val float64) Decimal { 38 | var fl *big.Float 39 | 40 | defer func() { 41 | if r := recover(); r != nil { 42 | switch r.(type) { 43 | case big.ErrNaN: 44 | fl = nil 45 | } 46 | } 47 | }() 48 | 49 | fl = big.NewFloat(val) 50 | 51 | return Decimal{ 52 | fl: fl, 53 | } 54 | } 55 | 56 | // NewFromString creates a new Decimal type from a string value. 57 | func NewFromString(str string) Decimal { 58 | bfl := big.NewFloat(0) 59 | 60 | if _, _, err := bfl.Parse(str, 10); err != nil { 61 | return NaN 62 | } 63 | 64 | return Decimal{bfl} 65 | } 66 | 67 | // NewFromInt creates a new Decimal type from an int value 68 | func NewFromInt(dec int) Decimal { 69 | return Decimal{big.NewFloat(float64(dec))} 70 | } 71 | 72 | // MaxSlice returns the max of a slice of decimals 73 | func MaxSlice(decimals ...Decimal) Decimal { 74 | if anyNan(decimals...) { 75 | return NaN 76 | } else if len(decimals) == 0 { 77 | return ZERO 78 | } 79 | 80 | initial := NewFromString("-Inf") 81 | 82 | for _, decimal := range decimals { 83 | if decimal.GT(initial) { 84 | initial = decimal 85 | } 86 | } 87 | 88 | return initial 89 | } 90 | 91 | // MinSlice returns the min of a slice of decimals 92 | func MinSlice(decimals ...Decimal) Decimal { 93 | if anyNan(decimals...) { 94 | return NaN 95 | } else if len(decimals) == 0 { 96 | return ZERO 97 | } 98 | 99 | initial := NewFromString("Inf") 100 | for _, decimal := range decimals { 101 | if decimal.LT(initial) { 102 | initial = decimal 103 | } 104 | } 105 | 106 | return initial 107 | } 108 | 109 | // Add adds a decimal instance to another Decimal instance. 110 | func (d Decimal) Add(addend Decimal) Decimal { 111 | return nanGuard(func() Decimal { 112 | return Decimal{d.cpy().Add(d.fl, addend.fl)} 113 | }, d, addend) 114 | } 115 | 116 | // Sub subtracts another decimal instance from this Decimal instance. 117 | func (d Decimal) Sub(subtrahend Decimal) Decimal { 118 | return nanGuard(func() Decimal { 119 | return Decimal{d.cpy().Sub(d.fl, subtrahend.fl)} 120 | }, d, subtrahend) 121 | } 122 | 123 | // Mul multiplies another decimal instance with this Decimal instance. 124 | func (d Decimal) Mul(factor Decimal) Decimal { 125 | return nanGuard(func() Decimal { 126 | return Decimal{d.cpy().Mul(d.fl, factor.fl)} 127 | }, d, factor) 128 | } 129 | 130 | // Div divides this Decimal by the denominator passed. 131 | func (d Decimal) Div(denominator Decimal) Decimal { 132 | return nanGuard(func() Decimal { 133 | return Decimal{d.cpy().Quo(d.fl, denominator.fl)} 134 | }, d, denominator) 135 | } 136 | 137 | // Frac returns another Decimal instance representing this Decimal multiplied by the 138 | // provided float. 139 | func (d Decimal) Frac(f float64) Decimal { 140 | fractionFactor := NewDecimal(f) 141 | 142 | return nanGuard(func() Decimal { 143 | return d.Mul(NewDecimal(f)) 144 | }, d, fractionFactor) 145 | } 146 | 147 | // Neg returns this Decimal multiplied by -1. 148 | func (d Decimal) Neg() Decimal { 149 | return nanGuard(func() Decimal { 150 | return d.Mul(NewDecimal(-1)) 151 | }, d) 152 | } 153 | 154 | // Abs returns the absolute value of this Decimal 155 | func (d Decimal) Abs() Decimal { 156 | if d.LT(ZERO) { 157 | return d.Mul(ONE.Neg()) 158 | } 159 | 160 | return d 161 | } 162 | 163 | // Pow returns the decimal to the inputted power 164 | func (d Decimal) Pow(exp int) Decimal { 165 | return nanGuard(func() Decimal { 166 | if exp == 0 { 167 | return ONE 168 | } 169 | 170 | x := Decimal{d.cpy()} 171 | 172 | for i := 1; i < exp; i++ { 173 | x = x.Mul(d) 174 | } 175 | 176 | return x 177 | }, d) 178 | } 179 | 180 | // Sqrt returns the decimal's square root 181 | func (d Decimal) Sqrt() Decimal { 182 | return nanGuard(func() Decimal { 183 | return Decimal{d.cpy().Sqrt(d.cpy())} 184 | }, d) 185 | } 186 | 187 | // EQ returns true if this Decimal exactly equals the provided decimal. 188 | func (d Decimal) EQ(other Decimal) bool { 189 | if anyNan(d, other) { 190 | return false 191 | } 192 | 193 | return d.Cmp(other) == 0 194 | } 195 | 196 | // LT returns true if this decimal is less than the provided decimal. 197 | func (d Decimal) LT(other Decimal) bool { 198 | if anyNan(d, other) { 199 | return false 200 | } 201 | 202 | return d.Cmp(other) < 0 203 | } 204 | 205 | // LTE returns true if this decimal is less or equal to the provided decimal. 206 | func (d Decimal) LTE(other Decimal) bool { 207 | if anyNan(d, other) { 208 | return false 209 | } 210 | 211 | return d.Cmp(other) <= 0 212 | } 213 | 214 | // GT returns true if this decimal is greater than the provided decimal. 215 | func (d Decimal) GT(other Decimal) bool { 216 | if anyNan(d, other) { 217 | return false 218 | } 219 | 220 | return d.Cmp(other) > 0 221 | } 222 | 223 | // GTE returns true if this decimal is greater than or equal to the provided decimal. 224 | func (d Decimal) GTE(other Decimal) bool { 225 | if anyNan(d, other) { 226 | return false 227 | } 228 | 229 | return d.Cmp(other) >= 0 230 | } 231 | 232 | // Cmp will return 1 if this decimal is greater than the provided, 0 if they are the same, and -1 if it is less. 233 | func (d Decimal) Cmp(other Decimal) int { 234 | if anyNan(d, other) { 235 | return 0 236 | } 237 | 238 | return d.fl.Cmp(other.fl) 239 | } 240 | 241 | // Float will return this Decimal as a float value. 242 | // Note that there may be some loss of precision in this operation. 243 | func (d Decimal) Float() float64 { 244 | if d.NaN() { 245 | return math.NaN() 246 | } 247 | 248 | f, _ := d.fl.Float64() 249 | return f 250 | } 251 | 252 | // Zero will return true if this Decimal is equal to 0. 253 | // Deprecated: Use IsZero instead 254 | func (d Decimal) Zero() bool { 255 | return d.IsZero() 256 | } 257 | 258 | // NaN returns true if the underlying is not a valid number 259 | func (d Decimal) NaN() bool { 260 | return d.fl == nil 261 | } 262 | 263 | // IsZero will return true if this Decimal is equal to 0. 264 | func (d Decimal) IsZero() bool { 265 | if d.NaN() { 266 | return false 267 | } 268 | 269 | return d.fl == nil || d.fl.Cmp(&flZero) == 0 270 | } 271 | 272 | func (d Decimal) String() string { 273 | if d.NaN() { 274 | return "NaN" 275 | } 276 | 277 | if d.fl == nil { 278 | d.fl = new(big.Float) 279 | } 280 | 281 | return d.fl.String() 282 | } 283 | 284 | // FormattedString returns the string value of the number to the requested precision 285 | func (d Decimal) FormattedString(places int) string { 286 | if d.NaN() { 287 | return d.String() 288 | } 289 | 290 | format := "%." + fmt.Sprint(places) + "f" 291 | fl := d.Float() 292 | return fmt.Sprintf(format, fl) 293 | } 294 | 295 | // MarshalJSON implements the json.Marshaler interface 296 | func (d Decimal) MarshalJSON() ([]byte, error) { 297 | if MarshalQuoted { 298 | return []byte("\"" + d.String() + "\""), nil 299 | } 300 | 301 | return d.fl.MarshalText() 302 | } 303 | 304 | // UnmarshalJSON implements the json.Unmarshaler interface 305 | func (d *Decimal) UnmarshalJSON(b []byte) error { 306 | if d.fl == nil { 307 | d.fl = big.NewFloat(0) 308 | } 309 | 310 | if isQuoted(b) { 311 | b = b[1 : len(b)-1] 312 | } 313 | 314 | return d.fl.UnmarshalText(b) 315 | } 316 | 317 | func isQuoted(b []byte) bool { 318 | quoteByte := byte('"') 319 | return len(b) > 0 && b[0] == quoteByte && b[len(b)-1] == quoteByte 320 | } 321 | 322 | // Value implements the sql.Valuer interface 323 | func (d Decimal) Value() (driver.Value, error) { 324 | return d.String(), nil 325 | } 326 | 327 | // Scan implements the sql.Scanner interface 328 | func (d *Decimal) Scan(src interface{}) error { 329 | switch src := src.(type) { 330 | case string: 331 | return json.Unmarshal([]byte(src), d) 332 | case []byte: 333 | return json.Unmarshal(src, d) 334 | default: 335 | return errors.New(fmt.Sprint("Passed value ", src, " should be a string")) 336 | } 337 | } 338 | 339 | func (d Decimal) cpy() *big.Float { 340 | cpy := new(big.Float) 341 | return cpy.Copy(d.fl) 342 | } 343 | 344 | func anyNan(decimals ...Decimal) bool { 345 | for _, decimal := range decimals { 346 | if decimal.NaN() { 347 | return true 348 | } 349 | } 350 | 351 | return false 352 | } 353 | 354 | func nanGuard(yeildFunc func() Decimal, decimals ...Decimal) Decimal { 355 | if anyNan(decimals...) { 356 | return NaN 357 | } 358 | 359 | return yeildFunc() 360 | } 361 | -------------------------------------------------------------------------------- /decimal_test.go: -------------------------------------------------------------------------------- 1 | package big 2 | 3 | import ( 4 | "encoding/json" 5 | "math" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type equalExample struct { 12 | value Decimal 13 | expected string 14 | } 15 | 16 | type booleanExample struct { 17 | value bool 18 | expected bool 19 | } 20 | 21 | func validateEqExamples(t *testing.T, examples ...equalExample) { 22 | for _, ex := range examples { 23 | assert.EqualValues(t, ex.expected, ex.value.String()) 24 | } 25 | } 26 | 27 | func validateBoolExamples(t *testing.T, examples ...booleanExample) { 28 | for _, ex := range examples { 29 | if ex.expected { 30 | assert.True(t, ex.value) 31 | } else { 32 | assert.False(t, ex.value) 33 | } 34 | } 35 | } 36 | 37 | func TestNewDecimal(t *testing.T) { 38 | t.Run("valid", func(t *testing.T) { 39 | d := NewDecimal(math.Pi) 40 | 41 | assert.EqualValues(t, math.Pi, d.Float()) 42 | }) 43 | 44 | t.Run("NaN", func(t *testing.T) { 45 | d := NewDecimal(math.NaN()) 46 | 47 | assert.Nil(t, d.fl) 48 | }) 49 | } 50 | 51 | func TestNewFromString(t *testing.T) { 52 | validateEqExamples(t, 53 | equalExample{ 54 | value: NewFromString("1.87"), 55 | expected: "1.87", 56 | }, 57 | equalExample{ 58 | value: NewFromString("NaN"), 59 | expected: "NaN", 60 | }, 61 | ) 62 | } 63 | 64 | func TestNewFromInt(t *testing.T) { 65 | d := NewFromInt(1) 66 | 67 | assert.EqualValues(t, "1", d.String()) 68 | } 69 | 70 | func TestMaxSlice(t *testing.T) { 71 | validateEqExamples(t, 72 | equalExample{ 73 | value: MaxSlice(NewDecimal(-100), 74 | NewDecimal(100), 75 | NewDecimal(0)), 76 | expected: "100", 77 | }, 78 | equalExample{ 79 | value: MaxSlice(NewDecimal(100), NaN), 80 | expected: "NaN", 81 | }, 82 | equalExample{ 83 | value: MaxSlice(), 84 | expected: "0", 85 | }, 86 | ) 87 | } 88 | 89 | func TestMinSlice(t *testing.T) { 90 | validateEqExamples(t, 91 | equalExample{ 92 | value: MinSlice(NewDecimal(-100), 93 | NewDecimal(100), 94 | NewDecimal(0)), 95 | expected: "-100", 96 | }, 97 | equalExample{ 98 | value: MinSlice(NewDecimal(100), NaN), 99 | expected: "NaN", 100 | }, 101 | equalExample{ 102 | value: MinSlice(), 103 | expected: "0", 104 | }, 105 | ) 106 | } 107 | 108 | func TestDecimal_Add(t *testing.T) { 109 | validateEqExamples(t, 110 | equalExample{ 111 | value: NewDecimal(3.14).Add(NewDecimal(2)), 112 | expected: "5.14", 113 | }, 114 | equalExample{ 115 | value: NaN.Add(NewDecimal(2)), 116 | expected: "NaN", 117 | }, 118 | equalExample{ 119 | value: NewDecimal(1).Add(NaN), 120 | expected: "NaN", 121 | }, 122 | ) 123 | } 124 | 125 | func TestDecimal_Sub(t *testing.T) { 126 | validateEqExamples(t, 127 | equalExample{ 128 | value: NewDecimal(3.14).Sub(NewDecimal(2)), 129 | expected: "1.14", 130 | }, 131 | equalExample{ 132 | value: NaN.Sub(NewDecimal(1)), 133 | expected: "NaN", 134 | }, 135 | equalExample{ 136 | value: ONE.Sub(NaN), 137 | expected: "NaN", 138 | }, 139 | ) 140 | } 141 | 142 | func TestDecimal_Mul(t *testing.T) { 143 | validateEqExamples(t, 144 | equalExample{ 145 | value: NewDecimal(3.14).Mul(TEN), 146 | expected: "31.4", 147 | }, 148 | equalExample{ 149 | value: NaN.Mul(TEN), 150 | expected: "NaN", 151 | }, 152 | equalExample{ 153 | value: TEN.Mul(NaN), 154 | expected: "NaN", 155 | }, 156 | ) 157 | } 158 | 159 | func TestDecimal_Div(t *testing.T) { 160 | validateEqExamples(t, 161 | equalExample{ 162 | value: NewDecimal(3.14).Div(TEN), 163 | expected: "0.314", 164 | }, 165 | equalExample{ 166 | value: TEN.Div(NaN), 167 | expected: "NaN", 168 | }, 169 | equalExample{ 170 | value: NaN.Div(TEN), 171 | expected: "NaN", 172 | }, 173 | ) 174 | } 175 | 176 | func TestDecimal_Neg(t *testing.T) { 177 | validateEqExamples(t, 178 | equalExample{ 179 | value: TEN.Neg(), 180 | expected: "-10", 181 | }, 182 | equalExample{ 183 | value: NaN.Neg(), 184 | expected: "NaN", 185 | }, 186 | ) 187 | } 188 | 189 | func TestDecimal_Abs(t *testing.T) { 190 | validateEqExamples(t, 191 | equalExample{ 192 | value: TEN.Abs(), 193 | expected: "10", 194 | }, 195 | equalExample{ 196 | value: NewFromString("-10").Abs(), 197 | expected: "10", 198 | }, 199 | equalExample{ 200 | value: NaN.Abs(), 201 | expected: "NaN", 202 | }, 203 | ) 204 | } 205 | 206 | func TestDecimal_Frac(t *testing.T) { 207 | validateEqExamples(t, 208 | equalExample{ 209 | value: TEN.Frac(0.5), 210 | expected: "5", 211 | }, 212 | equalExample{ 213 | value: TEN.Frac(1.5), 214 | expected: "15", 215 | }, 216 | equalExample{ 217 | value: NaN.Frac(0.5), 218 | expected: "NaN", 219 | }, 220 | ) 221 | } 222 | 223 | func TestDecimal_EQ(t *testing.T) { 224 | validateBoolExamples(t, 225 | booleanExample{ 226 | value: NewDecimal(182.1921).EQ(NewDecimal(182.1921)), 227 | expected: true, 228 | }, 229 | booleanExample{ 230 | value: NaN.EQ(NaN), 231 | expected: false, 232 | }, 233 | ) 234 | } 235 | 236 | func TestDecimal_GT(t *testing.T) { 237 | validateBoolExamples(t, 238 | booleanExample{ 239 | value: NewDecimal(182.1921).GT(NewDecimal(182.1921)), 240 | expected: false, 241 | }, 242 | booleanExample{ 243 | value: NewDecimal(182.1920).GT(NewDecimal(182.1921)), 244 | expected: false, 245 | }, 246 | booleanExample{ 247 | value: NewDecimal(182.1921).GT(NewDecimal(182.1920)), 248 | expected: true, 249 | }, 250 | booleanExample{ 251 | value: NaN.GT(NaN), 252 | expected: false, 253 | }, 254 | ) 255 | } 256 | 257 | func TestDecimal_GTE(t *testing.T) { 258 | validateBoolExamples(t, 259 | booleanExample{ 260 | value: NewDecimal(182.1921).GTE(NewDecimal(182.1921)), 261 | expected: true, 262 | }, 263 | booleanExample{ 264 | value: NewDecimal(182.1920).GTE(NewDecimal(182.1921)), 265 | expected: false, 266 | }, 267 | booleanExample{ 268 | value: NewDecimal(182.1921).GTE(NewDecimal(182.1920)), 269 | expected: true, 270 | }, 271 | booleanExample{ 272 | value: NaN.GTE(NaN), 273 | expected: false, 274 | }, 275 | ) 276 | } 277 | 278 | func TestDecimal_LT(t *testing.T) { 279 | validateBoolExamples(t, 280 | booleanExample{ 281 | value: NewDecimal(182.1921).LT(NewDecimal(182.1921)), 282 | expected: false, 283 | }, 284 | booleanExample{ 285 | value: NewDecimal(182.1920).LT(NewDecimal(182.1921)), 286 | expected: true, 287 | }, 288 | booleanExample{ 289 | value: NewDecimal(182.1921).LT(NewDecimal(182.1920)), 290 | expected: false, 291 | }, 292 | booleanExample{ 293 | value: NaN.LT(NaN), 294 | expected: false, 295 | }, 296 | ) 297 | } 298 | 299 | func TestDecimal_LTE(t *testing.T) { 300 | validateBoolExamples(t, 301 | booleanExample{ 302 | value: NewDecimal(182.1921).LTE(NewDecimal(182.1921)), 303 | expected: true, 304 | }, 305 | booleanExample{ 306 | value: NewDecimal(182.1920).LTE(NewDecimal(182.1921)), 307 | expected: true, 308 | }, 309 | booleanExample{ 310 | value: NewDecimal(182.1921).LTE(NewDecimal(182.1920)), 311 | expected: false, 312 | }, 313 | booleanExample{ 314 | value: NaN.LTE(NaN), 315 | expected: false, 316 | }, 317 | ) 318 | } 319 | 320 | func TestDecimal_Cmp(t *testing.T) { 321 | assert.EqualValues(t, 0, ONE.Cmp(ONE)) 322 | assert.EqualValues(t, 1, TEN.Cmp(ONE)) 323 | assert.EqualValues(t, -1, ONE.Cmp(TEN)) 324 | assert.EqualValues(t, 0, NaN.Cmp(NaN)) 325 | } 326 | 327 | func TestDecimal_Float(t *testing.T) { 328 | assert.EqualValues(t, 1.13, NewDecimal(1.13).Float()) 329 | assert.True(t, math.IsNaN(NaN.Float())) 330 | } 331 | 332 | func TestDecimal_Pow(t *testing.T) { 333 | validateEqExamples(t, 334 | equalExample{ 335 | value: NewDecimal(8).Pow(2), 336 | expected: "64", 337 | }, 338 | equalExample{ 339 | value: TEN.Pow(0), 340 | expected: "1", 341 | }, 342 | equalExample{ 343 | value: NaN.Pow(2), 344 | expected: "NaN", 345 | }, 346 | ) 347 | } 348 | 349 | func TestDecimal_Sqrt(t *testing.T) { 350 | validateEqExamples(t, 351 | equalExample{ 352 | value: NewDecimal(64).Sqrt(), 353 | expected: "8", 354 | }, 355 | equalExample{ 356 | value: NaN.Sqrt(), 357 | expected: "NaN", 358 | }, 359 | ) 360 | } 361 | 362 | func TestDecimal_String(t *testing.T) { 363 | validateEqExamples(t, 364 | equalExample{ 365 | value: NewDecimal(1.13), 366 | expected: "1.13", 367 | }, 368 | equalExample{ 369 | value: NaN, 370 | expected: "NaN", 371 | }, 372 | ) 373 | } 374 | 375 | func TestDecimal_FormattedString(t *testing.T) { 376 | assert.EqualValues(t, "3.1416", NewDecimal(math.Pi).FormattedString(4)) 377 | assert.EqualValues(t, "NaN", NaN.FormattedString(4)) 378 | } 379 | 380 | func TestDecimal_IsZero(t *testing.T) { 381 | validateBoolExamples(t, 382 | booleanExample{ 383 | value: ZERO.IsZero(), 384 | expected: true, 385 | }, 386 | booleanExample{ 387 | value: ONE.IsZero(), 388 | expected: false, 389 | }, 390 | booleanExample{ 391 | value: NaN.IsZero(), 392 | expected: false, 393 | }, 394 | ) 395 | } 396 | func TestDecimal_Json(t *testing.T) { 397 | type jsonType struct { 398 | Decimal Decimal `json:"decimal"` 399 | } 400 | 401 | t.Run("MarshalJSON - quoted", func(t *testing.T) { 402 | MarshalQuoted = true 403 | tmpStruct := jsonType{ 404 | Decimal: NewFromString("3.1419"), 405 | } 406 | marshaled, err := json.Marshal(tmpStruct) 407 | 408 | assert.NoError(t, err) 409 | assert.Equal(t, `{"decimal":"3.1419"}`, string(marshaled)) 410 | MarshalQuoted = false 411 | }) 412 | 413 | t.Run("MarshalJSON - unquoted", func(t *testing.T) { 414 | tmpStruct := jsonType{ 415 | Decimal: NewFromString("3.1419"), 416 | } 417 | marshaled, err := json.Marshal(tmpStruct) 418 | 419 | assert.NoError(t, err) 420 | assert.Equal(t, `{"decimal":3.1419}`, string(marshaled)) 421 | }) 422 | 423 | t.Run("UnmarshalJSON - unquoted", func(t *testing.T) { 424 | var ts jsonType 425 | 426 | d := `{"decimal":3.1419}` 427 | err := json.Unmarshal([]byte(d), &ts) 428 | 429 | assert.NoError(t, err) 430 | assert.Equal(t, "3.1419", ts.Decimal.String()) 431 | }) 432 | 433 | t.Run("UnmarshalJSON - quoted", func(t *testing.T) { 434 | var ts jsonType 435 | 436 | d := `{"decimal":"3.1419"}` 437 | err := json.Unmarshal([]byte(d), &ts) 438 | 439 | assert.NoError(t, err) 440 | assert.Equal(t, "3.1419", ts.Decimal.String()) 441 | }) 442 | } 443 | 444 | func TestDecimal_Sql(t *testing.T) { 445 | t.Run("Value", func(t *testing.T) { 446 | d := ONE 447 | value, err := d.Value() 448 | 449 | assert.NoError(t, err) 450 | assert.Equal(t, `1`, value) 451 | }) 452 | 453 | t.Run("Scan", func(t *testing.T) { 454 | var d Decimal 455 | 456 | data := `1.23` 457 | err := d.Scan(data) 458 | 459 | assert.NoError(t, err) 460 | assert.Equal(t, "1.23", d.String()) 461 | }) 462 | 463 | t.Run("Scan []byte", func(t *testing.T) { 464 | var d Decimal 465 | 466 | data := `1.23` 467 | err := d.Scan([]byte(data)) 468 | 469 | assert.NoError(t, err) 470 | assert.Equal(t, "1.23", d.String()) 471 | }) 472 | 473 | t.Run("Scan returns error when src is not string", func(t *testing.T) { 474 | var d Decimal 475 | 476 | data := 1.23 477 | err := d.Scan(data) 478 | 479 | assert.NotNil(t, err) 480 | assert.EqualValues(t, "Passed value 1.23 should be a string", err.Error()) 481 | }) 482 | } 483 | --------------------------------------------------------------------------------