Skip to content

Commit 98f7525

Browse files
hardcode decimal fractions (#27)
* split * master * fix * fix * fix
1 parent 878c2f4 commit 98f7525

6 files changed

Lines changed: 764 additions & 59 deletions

File tree

.github/workflows/tests.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: Test
22

33
on:
44
push:
5-
branches: [main]
5+
branches: [master]
66
pull_request:
7-
branches: [main]
7+
branches: [master]
88

99
permissions: read-all
1010

@@ -15,7 +15,7 @@ jobs:
1515
steps:
1616
- name: Check outcode
1717
uses: actions/checkout@v4
18-
18+
1919
- name: Set up Go 1.x
2020
uses: actions/setup-go@v5
2121
with:
@@ -27,6 +27,12 @@ jobs:
2727
go install github.com/jstemmer/go-junit-report/v2@latest
2828
go test -coverprofile=coverage.out -covermode=atomic -cover -json -v ./... 2>&1 | go-junit-report -set-exit-code > tests.xml
2929
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+
3036
- name: Upload test results to Codecov
3137
uses: codecov/test-results-action@v1
3238
with:

fpdecimal.go renamed to fp3/fpdecimal.go

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,62 @@
1-
package fpdecimal
1+
package fp3
22

3-
// Decimal is a decimal with fixed number of fraction digits.
4-
// By default, uses 3 fractional digits.
5-
// For example, values with 3 fractional digits will fit in ~9 quadrillion.
3+
import "github.com/nikolaydubina/fpdecimal"
4+
5+
// Decimal with 3 fractional digits.
66
// Fractions lower than that are discarded in operations.
77
// Max: +9223372036854775.807
88
// Min: -9223372036854775.808
99
type Decimal struct{ v int64 }
1010

1111
var Zero = Decimal{}
1212

13-
var multipliers = [...]int64{1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000}
14-
1513
type integer interface {
1614
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64
1715
}
1816

19-
// FractionDigits that operations will use.
20-
// Warning, after change, existing variables are not updated.
21-
// Likely you want to use this once per runtime and in `func init()`.
22-
var FractionDigits uint8 = 3
17+
const (
18+
fractionDigits = 3
19+
multiplier = 1000
20+
)
2321

24-
func FromInt[T integer](v T) Decimal { return Decimal{int64(v) * multipliers[FractionDigits]} }
22+
func FromInt[T integer](v T) Decimal { return Decimal{int64(v) * multiplier} }
2523

2624
func FromFloat[T float32 | float64](v T) Decimal {
27-
return Decimal{int64(float64(v) * float64(multipliers[FractionDigits]))}
25+
return Decimal{int64(float64(v) * float64(multiplier))}
2826
}
2927

3028
// FromIntScaled expects value already scaled to minor units
3129
func FromIntScaled[T integer](v T) Decimal { return Decimal{int64(v)} }
3230

3331
func FromString(s string) (Decimal, error) {
34-
v, err := ParseFixedPointDecimal([]byte(s), FractionDigits)
32+
v, err := fpdecimal.ParseFixedPointDecimal([]byte(s), fractionDigits)
3533
return Decimal{v}, err
3634
}
3735

3836
func (v *Decimal) UnmarshalJSON(b []byte) (err error) {
39-
v.v, err = ParseFixedPointDecimal(b, FractionDigits)
37+
v.v, err = fpdecimal.ParseFixedPointDecimal(b, fractionDigits)
4038
return err
4139
}
4240

4341
func (v Decimal) MarshalJSON() ([]byte, error) { return []byte(v.String()), nil }
4442

4543
func (a Decimal) Scaled() int64 { return a.v }
4644

47-
func (a Decimal) Float32() float32 { return float32(a.v) / float32(multipliers[FractionDigits]) }
45+
func (a Decimal) Float32() float32 { return float32(a.v) / float32(multiplier) }
4846

49-
func (a Decimal) Float64() float64 { return float64(a.v) / float64(multipliers[FractionDigits]) }
47+
func (a Decimal) Float64() float64 { return float64(a.v) / float64(multiplier) }
5048

51-
func (a Decimal) String() string { return FixedPointDecimalToString(a.v, FractionDigits) }
49+
func (a Decimal) String() string { return fpdecimal.FixedPointDecimalToString(a.v, fractionDigits) }
5250

5351
func (a Decimal) Add(b Decimal) Decimal { return Decimal{v: a.v + b.v} }
5452

5553
func (a Decimal) Sub(b Decimal) Decimal { return Decimal{v: a.v - b.v} }
5654

57-
func (a Decimal) Mul(b Decimal) Decimal { return Decimal{v: a.v * b.v / multipliers[FractionDigits]} }
55+
func (a Decimal) Mul(b Decimal) Decimal { return Decimal{v: a.v * b.v / multiplier} }
5856

59-
func (a Decimal) Div(b Decimal) Decimal { return Decimal{v: a.v * multipliers[FractionDigits] / b.v} }
57+
func (a Decimal) Div(b Decimal) Decimal { return Decimal{v: a.v * multiplier / b.v} }
6058

61-
func (a Decimal) Mod(b Decimal) Decimal { return Decimal{v: a.v % (b.v / multipliers[FractionDigits])} }
59+
func (a Decimal) Mod(b Decimal) Decimal { return Decimal{v: a.v % (b.v / multiplier)} }
6260

6361
func (a Decimal) DivMod(b Decimal) (part, remainder Decimal) { return a.Div(b), a.Mod(b) }
6462

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package fpdecimal_test
1+
package fp3_test
22

33
import (
44
"encoding/json"
@@ -9,11 +9,9 @@ import (
99
"testing"
1010
"unsafe"
1111

12-
fp "github.com/nikolaydubina/fpdecimal"
12+
fp "github.com/nikolaydubina/fpdecimal/fp3"
1313
)
1414

15-
var multipliers = [...]int64{1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000}
16-
1715
func FuzzArithmetics(f *testing.F) {
1816
tests := [][2]int64{
1917
{1, 2},
@@ -108,6 +106,11 @@ func FuzzParse_StringSameAsFloat(f *testing.F) {
108106
t.Skip()
109107
}
110108

109+
// gaps start around these floats
110+
if r > 100_000_000 || r < -100_000_000 {
111+
t.Skip()
112+
}
113+
111114
s := fmt.Sprintf("%.3f", r)
112115
rs, _ := strconv.ParseFloat(s, 64)
113116

@@ -176,12 +179,8 @@ func FuzzToFloat(f *testing.F) {
176179
f.Fuzz(func(t *testing.T, v float64) {
177180
a := fp.FromFloat(v)
178181

179-
if float32(v) != a.Float32() {
180-
t.Error("a", a, "a.f32", a.Float32(), "f32.v", float32(v))
181-
}
182-
183-
if v != a.Float64() {
184-
t.Error("a", a, "a.f32", a.Float32(), "v", v)
182+
if delta := math.Abs(v - a.Float64()); delta > 0.00100001 {
183+
t.Error("a", a, "a.f64", a.Float64(), "v", v, "delta", delta)
185184
}
186185
})
187186
}
@@ -608,27 +607,3 @@ func TestDecimal_Compare(t *testing.T) {
608607
t.Error(a, "==", b)
609608
}
610609
}
611-
612-
func TestSetFractionDigits(t *testing.T) {
613-
defer func() { fp.FractionDigits = 3 }()
614-
615-
t.Run("default 3", func(t *testing.T) {
616-
if a, err := fp.FromString("1.123"); a.String() != "1.123" || err != nil {
617-
t.Error("SetFractionDigits", a)
618-
}
619-
})
620-
621-
t.Run("5", func(t *testing.T) {
622-
fp.FractionDigits = 5
623-
if a, err := fp.FromString("1.123456"); a.String() != "1.12345" || err != nil {
624-
t.Error("SetFractionDigits 5", a)
625-
}
626-
})
627-
628-
t.Run("10", func(t *testing.T) {
629-
fp.FractionDigits = 10
630-
if a, err := fp.FromString("1.12345678910"); a.String() != "1.1234567891" || err != nil {
631-
t.Error("SetFractionDigits 10", a)
632-
}
633-
})
634-
}

fp6/fpdecimal.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 (a Decimal) Scaled() int64 { return a.v }
44+
45+
func (a Decimal) Float32() float32 { return float32(a.v) / float32(multiplier) }
46+
47+
func (a Decimal) Float64() float64 { return float64(a.v) / float64(multiplier) }
48+
49+
func (a Decimal) String() string { return fpdecimal.FixedPointDecimalToString(a.v, fractionDigits) }
50+
51+
func (a Decimal) Add(b Decimal) Decimal { return Decimal{v: a.v + b.v} }
52+
53+
func (a Decimal) Sub(b Decimal) Decimal { return Decimal{v: a.v - b.v} }
54+
55+
func (a Decimal) Mul(b Decimal) Decimal { return Decimal{v: a.v * b.v / multiplier} }
56+
57+
func (a Decimal) Div(b Decimal) Decimal { return Decimal{v: a.v * multiplier / b.v} }
58+
59+
func (a Decimal) Mod(b Decimal) Decimal { return Decimal{v: a.v % (b.v / multiplier)} }
60+
61+
func (a Decimal) DivMod(b Decimal) (part, remainder Decimal) { return a.Div(b), a.Mod(b) }
62+
63+
func (a Decimal) Equal(b Decimal) bool { return a.v == b.v }
64+
65+
func (a Decimal) GreaterThan(b Decimal) bool { return a.v > b.v }
66+
67+
func (a Decimal) LessThan(b Decimal) bool { return a.v < b.v }
68+
69+
func (a Decimal) GreaterThanOrEqual(b Decimal) bool { return a.v >= b.v }
70+
71+
func (a Decimal) LessThanOrEqual(b Decimal) bool { return a.v <= b.v }
72+
73+
func (a Decimal) Compare(b Decimal) int {
74+
if a.LessThan(b) {
75+
return -1
76+
}
77+
if a.GreaterThan(b) {
78+
return 1
79+
}
80+
return 0
81+
}
82+
83+
func Min(vs ...Decimal) Decimal {
84+
if len(vs) == 0 {
85+
panic("min of empty set is undefined")
86+
}
87+
var v Decimal = vs[0]
88+
for _, q := range vs {
89+
if q.LessThan(v) {
90+
v = q
91+
}
92+
}
93+
return v
94+
}
95+
96+
func Max(vs ...Decimal) Decimal {
97+
if len(vs) == 0 {
98+
panic("max of empty set is undefined")
99+
}
100+
var v Decimal = vs[0]
101+
for _, q := range vs {
102+
if q.GreaterThan(v) {
103+
v = q
104+
}
105+
}
106+
return v
107+
}

0 commit comments

Comments
 (0)