mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-06-04 07:26:47 +00:00
Types: Switch convert.StringToFloat64 to types.Number (#1415)
* Types: Add Number type * Types: Switch StringToFloat64 for Number This change mostly just renames the type. convert package and StringToFloat64 represent actions, not types, and make it misleading to use outside of the API context, especially when using it for a Float64ToString operation. * Common: Remove StringToFloat64 Replaced by types.Number * fixup! Types: Switch StringToFloat64 for Number Second pass at Okx * Spellcheck: Fix whitespace handling for okx line
This commit is contained in:
73
types/number.go
Normal file
73
types/number.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
var errInvalidNumberValue = errors.New("invalid value for Number type")
|
||||
|
||||
// Number represents a floating point number, and implements json.Unmarshaller and json.Marshaller
|
||||
type Number float64
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler
|
||||
func (f *Number) UnmarshalJSON(data []byte) error {
|
||||
switch c := data[0]; c { // From json.decode literalInterface
|
||||
case 'n', 't', 'f': // null, true, false
|
||||
return fmt.Errorf("%w: %s", errInvalidNumberValue, data)
|
||||
case '"': // string
|
||||
if len(data) < 2 || data[len(data)-1] != '"' {
|
||||
return fmt.Errorf("%w: %s", errInvalidNumberValue, data)
|
||||
}
|
||||
data = data[1 : len(data)-1] // Naive Unquote
|
||||
default: // Should be a number
|
||||
if c != '-' && (c < '0' || c > '9') { // Invalid json syntax
|
||||
return fmt.Errorf("%w: %s", errInvalidNumberValue, data)
|
||||
}
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
*f = Number(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
val, err := strconv.ParseFloat(string(data), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", errInvalidNumberValue, data) // We don't use err; We know it's not valid and errInvalidNumberValue is clearer
|
||||
}
|
||||
|
||||
*f = Number(val)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler by formatting to a json string
|
||||
// 1337.37 will marshal to "1337.37"
|
||||
// 0 will marshal to an empty string: ""
|
||||
func (f Number) MarshalJSON() ([]byte, error) {
|
||||
if f == 0 {
|
||||
return []byte(`""`), nil
|
||||
}
|
||||
val := strconv.FormatFloat(float64(f), 'f', -1, 64)
|
||||
return []byte(`"` + val + `"`), nil
|
||||
}
|
||||
|
||||
// Float64 returns the underlying float64
|
||||
func (f Number) Float64() float64 {
|
||||
return float64(f)
|
||||
}
|
||||
|
||||
// Int64 returns the truncated integer component of the number
|
||||
func (f Number) Int64() int64 {
|
||||
// It's likely this is sufficient, since Numbers probably have not had floating point math performed on them
|
||||
// However if issues arise then we can switch to math.Round
|
||||
return int64(f)
|
||||
}
|
||||
|
||||
// Decimal returns a decimal.Decimal
|
||||
func (f Number) Decimal() decimal.Decimal {
|
||||
return decimal.NewFromFloat(float64(f))
|
||||
}
|
||||
83
types/number_test.go
Normal file
83
types/number_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestNumberUnmarshalJSON asserts the following behaviour:
|
||||
// * Literal numbers and quoted are valid
|
||||
// * Anything else returns errInvalidNumberValue
|
||||
func TestNumberUnmarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
var n Number
|
||||
|
||||
err := n.UnmarshalJSON([]byte(`"0.00000001"`))
|
||||
assert.NoError(t, err, "Unmarshal should not error")
|
||||
assert.Equal(t, 1e-8, n.Float64(), "Float64() should return the correct value")
|
||||
|
||||
err = n.UnmarshalJSON([]byte(`""`))
|
||||
assert.NoError(t, err, "Unmarshal should not error")
|
||||
assert.Zero(t, n.Float64(), "UnmarshalJSON should parse empty as 0")
|
||||
|
||||
err = n.UnmarshalJSON([]byte(`1337.37`))
|
||||
assert.NoError(t, err, "Unmarshal should not error on number types")
|
||||
assert.Equal(t, 1337.37, n.Float64(), "UnmarshalJSON should handle raw numerics")
|
||||
|
||||
// Invalid value checking
|
||||
for _, i := range []string{`"MEOW"`, `null`, `false`, `true`, `"1337.37`} {
|
||||
err = n.UnmarshalJSON([]byte(i))
|
||||
assert.ErrorIsf(t, err, errInvalidNumberValue, "UnmarshalJSON should error with invalid Value for `%s`", i)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNumberMarshalJSON asserts the following behaviour:
|
||||
// 0 marshalls to quoted empty string
|
||||
// Anything else marshalls as a quoted number
|
||||
func TestNumberMarshalJSON(t *testing.T) {
|
||||
data, err := new(Number).MarshalJSON()
|
||||
assert.NoError(t, err, "MarshalJSON should not error")
|
||||
assert.Equal(t, `""`, string(data), "MarshalJSON should return the correct value")
|
||||
|
||||
data, err = Number(1337.1337).MarshalJSON()
|
||||
assert.NoError(t, err, "MarshalJSON should not error")
|
||||
assert.Equal(t, `"1337.1337"`, string(data), "MarshalJSON should return the correct value")
|
||||
}
|
||||
|
||||
// TestNumberFloat64 asserts Float64() returns a valid float64
|
||||
func TestNumberFloat64(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, 0.04200064, Number(0.04200064).Float64(), "Float64() should return the correct value")
|
||||
}
|
||||
|
||||
// TestNumberDecimal asserts Decimal() returns a valid decimal.Decimal
|
||||
func TestNumberDecimal(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, decimal.NewFromFloat(0.04200064), Number(0.04200064).Decimal(), "Decimal() should return the correct value")
|
||||
}
|
||||
|
||||
// TestNumberInt64 asserts Int64() returns a valid truncated int64
|
||||
func TestNumberInt64(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, int64(42), Number(42.00000064).Int64(), "Int64() should return the correct truncated value")
|
||||
assert.Equal(t, int64(43), Number(43.99999964).Int64(), "Int64() should not round the number")
|
||||
}
|
||||
|
||||
// BenchmarkNumberUnmarshalJSON provides a barebones benchmark of Unmarshaling a string value
|
||||
// Ballpark: 42.78 ns/op 16 B/op 1 allocs/op
|
||||
func BenchmarkNumberUnmarshalJSON(b *testing.B) {
|
||||
var n Number
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = n.UnmarshalJSON([]byte(`"0.04200074"`))
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkNumberMarshalJSON provides a barebones benchmark of Marshaling a string value
|
||||
// Ballpark: 118.2 ns/op 56 B/op 3 allocs/op
|
||||
func BenchmarkNumberMarshalJSON(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = Number(1337.1337).MarshalJSON()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user