Skip to content

Commit

Permalink
hardcode decimal fractions (#27)
Browse files Browse the repository at this point in the history
* split

* master

* fix

* fix

* fix
  • Loading branch information
nikolaydubina authored Nov 29, 2024
1 parent 878c2f4 commit 98f7525
Show file tree
Hide file tree
Showing 6 changed files with 764 additions and 59 deletions.
12 changes: 9 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Test

on:
push:
branches: [main]
branches: [master]
pull_request:
branches: [main]
branches: [master]

permissions: read-all

Expand All @@ -15,7 +15,7 @@ jobs:
steps:
- name: Check outcode
uses: actions/checkout@v4

- name: Set up Go 1.x
uses: actions/setup-go@v5
with:
Expand All @@ -27,6 +27,12 @@ jobs:
go install github.com/jstemmer/go-junit-report/v2@latest
go test -coverprofile=coverage.out -covermode=atomic -cover -json -v ./... 2>&1 | go-junit-report -set-exit-code > tests.xml
- name: Fuzz
run: |
go test -list . | grep Fuzz | xargs -P 8 -I {} go test -fuzz {} -fuzztime 5s .
cd fp3; go test -list . | grep Fuzz | xargs -P 8 -I {} go test -fuzz {} -fuzztime 5s . ; cd ..
cd fp6; go test -list . | grep Fuzz | xargs -P 8 -I {} go test -fuzz {} -fuzztime 5s . ; cd ..
- name: Upload test results to Codecov
uses: codecov/test-results-action@v1
with:
Expand Down
38 changes: 18 additions & 20 deletions fpdecimal.go → fp3/fpdecimal.go
Original file line number Diff line number Diff line change
@@ -1,64 +1,62 @@
package fpdecimal
package fp3

// Decimal is a decimal with fixed number of fraction digits.
// By default, uses 3 fractional digits.
// For example, values with 3 fractional digits will fit in ~9 quadrillion.
import "github.com/nikolaydubina/fpdecimal"

// Decimal with 3 fractional digits.
// Fractions lower than that are discarded in operations.
// Max: +9223372036854775.807
// Min: -9223372036854775.808
type Decimal struct{ v int64 }

var Zero = Decimal{}

var multipliers = [...]int64{1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000}

type integer interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64
}

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

func FromInt[T integer](v T) Decimal { return Decimal{int64(v) * multipliers[FractionDigits]} }
func FromInt[T integer](v T) Decimal { return Decimal{int64(v) * multiplier} }

func FromFloat[T float32 | float64](v T) Decimal {
return Decimal{int64(float64(v) * float64(multipliers[FractionDigits]))}
return Decimal{int64(float64(v) * float64(multiplier))}
}

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

func FromString(s string) (Decimal, error) {
v, err := ParseFixedPointDecimal([]byte(s), FractionDigits)
v, err := fpdecimal.ParseFixedPointDecimal([]byte(s), fractionDigits)
return Decimal{v}, err
}

func (v *Decimal) UnmarshalJSON(b []byte) (err error) {
v.v, err = ParseFixedPointDecimal(b, FractionDigits)
v.v, err = fpdecimal.ParseFixedPointDecimal(b, fractionDigits)
return err
}

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

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

func (a Decimal) Float32() float32 { return float32(a.v) / float32(multipliers[FractionDigits]) }
func (a Decimal) Float32() float32 { return float32(a.v) / float32(multiplier) }

func (a Decimal) Float64() float64 { return float64(a.v) / float64(multipliers[FractionDigits]) }
func (a Decimal) Float64() float64 { return float64(a.v) / float64(multiplier) }

func (a Decimal) String() string { return FixedPointDecimalToString(a.v, FractionDigits) }
func (a Decimal) String() string { return fpdecimal.FixedPointDecimalToString(a.v, fractionDigits) }

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

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

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

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

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

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

Expand Down
43 changes: 9 additions & 34 deletions fpdecimal_test.go → fp3/fpdecimal_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package fpdecimal_test
package fp3_test

import (
"encoding/json"
Expand All @@ -9,11 +9,9 @@ import (
"testing"
"unsafe"

fp "github.com/nikolaydubina/fpdecimal"
fp "github.com/nikolaydubina/fpdecimal/fp3"
)

var multipliers = [...]int64{1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000}

func FuzzArithmetics(f *testing.F) {
tests := [][2]int64{
{1, 2},
Expand Down Expand Up @@ -108,6 +106,11 @@ func FuzzParse_StringSameAsFloat(f *testing.F) {
t.Skip()
}

// gaps start around these floats
if r > 100_000_000 || r < -100_000_000 {
t.Skip()
}

s := fmt.Sprintf("%.3f", r)
rs, _ := strconv.ParseFloat(s, 64)

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

if float32(v) != a.Float32() {
t.Error("a", a, "a.f32", a.Float32(), "f32.v", float32(v))
}

if v != a.Float64() {
t.Error("a", a, "a.f32", a.Float32(), "v", v)
if delta := math.Abs(v - a.Float64()); delta > 0.00100001 {
t.Error("a", a, "a.f64", a.Float64(), "v", v, "delta", delta)
}
})
}
Expand Down Expand Up @@ -608,27 +607,3 @@ func TestDecimal_Compare(t *testing.T) {
t.Error(a, "==", b)
}
}

func TestSetFractionDigits(t *testing.T) {
defer func() { fp.FractionDigits = 3 }()

t.Run("default 3", func(t *testing.T) {
if a, err := fp.FromString("1.123"); a.String() != "1.123" || err != nil {
t.Error("SetFractionDigits", a)
}
})

t.Run("5", func(t *testing.T) {
fp.FractionDigits = 5
if a, err := fp.FromString("1.123456"); a.String() != "1.12345" || err != nil {
t.Error("SetFractionDigits 5", a)
}
})

t.Run("10", func(t *testing.T) {
fp.FractionDigits = 10
if a, err := fp.FromString("1.12345678910"); a.String() != "1.1234567891" || err != nil {
t.Error("SetFractionDigits 10", a)
}
})
}
107 changes: 107 additions & 0 deletions fp6/fpdecimal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package fp6

import "github.com/nikolaydubina/fpdecimal"

// Decimal with 6 fractional digits.
// Fractions lower than that are discarded in operations.
// Max: +9223372036854.775807
// Min: -9223372036854.775808
type Decimal struct{ v int64 }

var Zero = Decimal{}

type integer interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64
}

const (
fractionDigits = 6
multiplier = 1_000_000
)

func FromInt[T integer](v T) Decimal { return Decimal{int64(v) * multiplier} }

func FromFloat[T float32 | float64](v T) Decimal {
return Decimal{int64(float64(v) * float64(multiplier))}
}

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

func FromString(s string) (Decimal, error) {
v, err := fpdecimal.ParseFixedPointDecimal([]byte(s), fractionDigits)
return Decimal{v}, err
}

func (v *Decimal) UnmarshalJSON(b []byte) (err error) {
v.v, err = fpdecimal.ParseFixedPointDecimal(b, fractionDigits)
return err
}

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

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

func (a Decimal) Float32() float32 { return float32(a.v) / float32(multiplier) }

func (a Decimal) Float64() float64 { return float64(a.v) / float64(multiplier) }

func (a Decimal) String() string { return fpdecimal.FixedPointDecimalToString(a.v, fractionDigits) }

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

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

func (a Decimal) Mul(b Decimal) Decimal { return Decimal{v: a.v * b.v / multiplier} }

func (a Decimal) Div(b Decimal) Decimal { return Decimal{v: a.v * multiplier / b.v} }

func (a Decimal) Mod(b Decimal) Decimal { return Decimal{v: a.v % (b.v / multiplier)} }

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

func (a Decimal) Equal(b Decimal) bool { return a.v == b.v }

func (a Decimal) GreaterThan(b Decimal) bool { return a.v > b.v }

func (a Decimal) LessThan(b Decimal) bool { return a.v < b.v }

func (a Decimal) GreaterThanOrEqual(b Decimal) bool { return a.v >= b.v }

func (a Decimal) LessThanOrEqual(b Decimal) bool { return a.v <= b.v }

func (a Decimal) Compare(b Decimal) int {
if a.LessThan(b) {
return -1
}
if a.GreaterThan(b) {
return 1
}
return 0
}

func Min(vs ...Decimal) Decimal {
if len(vs) == 0 {
panic("min of empty set is undefined")
}
var v Decimal = vs[0]
for _, q := range vs {
if q.LessThan(v) {
v = q
}
}
return v
}

func Max(vs ...Decimal) Decimal {
if len(vs) == 0 {
panic("max of empty set is undefined")
}
var v Decimal = vs[0]
for _, q := range vs {
if q.GreaterThan(v) {
v = q
}
}
return v
}
Loading

0 comments on commit 98f7525

Please sign in to comment.