Skip to content

Commit

Permalink
improve format rounding algo, support half-up rounding (#78)
Browse files Browse the repository at this point in the history
* improve rounding algo, support half-up rounding
* support context-controlled parsing
* add make lint
* fix formatting regression
  • Loading branch information
anzdaddy authored Aug 15, 2024
1 parent 2134fe4 commit 04a4ec5
Show file tree
Hide file tree
Showing 11 changed files with 543 additions and 185 deletions.
35 changes: 35 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.PHONY: all
all: test test-debug build-linux lint

.PHONY: test
test:
go test

.PHONY: test-debug
test-debug:
go test -tags=decimal_debug

.PHONY: build-linux
build-linux:
GOOS=linux $(MAKE) build build-32bit

.PHONY: build
build-64bit:
go test -c -o decimal.$@.test . && rm -f decimal.$@.test
go test -c -o decimal.$@.debug.test -tags=decimal_debug . && rm -f decimal.$@.debug.test

.PHONY: build-32bit
build-32bit:
GOARCH=arm go test -c -o decimal.$@.test . && rm -f decimal.$@.test
GOARCH=arm go test -c -o decimal.$@.debug.test -tags=decimal_debug . && rm -f decimal.$@.debug.test

# Dependency on build-linux primes Go caches.
.PHONY: lint
lint: build-linux
docker run --rm \
-w /app \
-v $(PWD):/app \
-v `go env GOCACHE`:/root/.cache/go-build \
-v `go env GOMODCACHE`:/go/pkg/mod \
golangci/golangci-lint:v1.60.1-alpine \
golangci-lint run
26 changes: 24 additions & 2 deletions decimal64.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package decimal

import (
"fmt"
"math"
"math/bits"
)
Expand Down Expand Up @@ -39,6 +40,19 @@ const (
Down
)

func (r Rounding) String() string {
switch r {
case HalfUp:
return "HalfUp"
case HalfEven:
return "HalfEven"
case Down:
return "Down"
default:
return fmt.Sprintf("Unknown rounding mode %d", r)
}
}

// Context64 may be used to tune the behaviour of arithmetic operations.
type Context64 struct {
// Rounding sets the rounding behaviour of arithmetic operations.
Expand Down Expand Up @@ -190,18 +204,26 @@ func countTrailingZeros(n uint64) int {
return zeros
}

func new64Raw(bits uint64) Decimal64 {
return Decimal64{bits: bits}
}

func newFromParts(sign int, exp int, significand uint64) Decimal64 {
return new64(newFromPartsRaw(sign, exp, significand).bits)
}

func newFromPartsRaw(sign int, exp int, significand uint64) Decimal64 {
s := uint64(sign) << 63

if significand < 0x8<<50 {
// s EEeeeeeeee (0)ttt tttttttttt tttttttttt tttttttttt tttttttttt tttttttttt
// EE ∈ {00, 01, 10}
return new64(s | uint64(exp+expOffset)<<(63-10) | significand)
return new64Raw(s | uint64(exp+expOffset)<<(63-10) | significand)
}
// s 11EEeeeeeeee (100)t tttttttttt tttttttttt tttttttttt tttttttttt tttttttttt
// EE ∈ {00, 01, 10}
significand &= 0x8<<50 - 1
return new64(s | uint64(0xc00|(exp+expOffset))<<(63-12) | significand)
return new64Raw(s | uint64(0xc00|(exp+expOffset))<<(63-12) | significand)
}

func (d Decimal64) parts() (fl flavor, sign int, exp int, significand uint64) {
Expand Down
3 changes: 1 addition & 2 deletions decimal64_debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ type Decimal64 struct {
// The verbose construction below makes it easy to audit accidental raw cosntruction.
// A search for (?<!\[\])Decimal64\{ must come up empty.
func new64(bits uint64) Decimal64 {
var d Decimal64
d.bits = bits
d := new64Raw(bits)

fl, sign, exp, significand := d.parts()

Expand Down
4 changes: 1 addition & 3 deletions decimal64_ndebug.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,5 @@ type Decimal64 struct {
// The verbose construction below makes it easy to audit accidental raw cosntruction.
// A search for (?<!\[\])Decimal64\{ must come up empty.
func new64(bits uint64) Decimal64 {
var d Decimal64
d.bits = bits
return d
return new64Raw(bits)
}
174 changes: 159 additions & 15 deletions decimal64_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package decimal

import (
"math"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -36,6 +38,152 @@ func TestNew64FromInt64Big(t *testing.T) {
}
}

func TestDecimal64Parse(t *testing.T) {
t.Parallel()

test := func(expected string, source string) {
t.Helper()
assert.Equal(t, strings.TrimSpace(expected), MustParse64(source).String())
}

test("0", "0")
test("1e-13", "0.0000000000001")
test("1e-13", "1e-13")
test("1", "1")
test("100000", "100000")
test("1e+6", "1000000")
}

func TestDecimal64ParseHalfEvenOdd(t *testing.T) {
t.Parallel()

ctx := Context64{Rounding: HalfEven}
test := func(expected string, source string) {
t.Helper()
assert.Equal(t, strings.TrimSpace(expected), ctx.MustParse(source).String())
}

test("1.000000000000007 ", "1.000000000000007")
test("1.000000000000007 ", "1.0000000000000074999999")
test("1.000000000000008 ", "1.0000000000000075000000")
test("1.000000000000008 ", "1.0000000000000075000001")

test("1.000000000000007e+11", "100000000000.0007")
test("1.000000000000007e+11", "100000000000.00074999999")
test("1.000000000000008e+11", "100000000000.00075000000")
test("1.000000000000008e+11", "100000000000.00075000001")
}

func TestDecimal64ParseHalfEvenEven(t *testing.T) {
t.Parallel()

ctx := Context64{Rounding: HalfEven}
test := func(expected string, source string) {
t.Helper()
assert.Equal(t, strings.TrimSpace(expected), ctx.MustParse(source).String())
}

test("1.000000000000008 ", "1.000000000000008")
test("1.000000000000008 ", "1.0000000000000084999999")
test("1.000000000000008 ", "1.0000000000000085000000")
test("1.000000000000009 ", "1.0000000000000085000001")

test("1.000000000000008e+11", "100000000000.0008")
test("1.000000000000008e+11", "100000000000.00084999999")
test("1.000000000000008e+11", "100000000000.00085000000")
test("1.000000000000009e+11", "100000000000.00085000001")
}

func TestDecimal64ParseHalfUp(t *testing.T) {
t.Parallel()

ctx := Context64{Rounding: HalfUp}
test := func(expected string, source string) {
t.Helper()
assert.Equal(t, strings.TrimSpace(expected), ctx.MustParse(source).String())
}

test("0", "0")
test("1e-13", "0.0000000000001")
test("1e-13", "1e-13")
test("1", "1")
test("100000", "100000")
test("1e+6", "1000000")

test("1.49999999999999 ", "1.49999999999999")
test("1.499999999999999", "1.499999999999999")
test("1.499999999999999", "1.4999999999999994999999")
test("1.5 ", "1.4999999999999995000000")
test("1.5 ", "1.4999999999999995000001")

test("1.99999999999949 ", "1.99999999999949")
test("1.999999999999499", "1.999999999999499")
test("1.999999999999499", "1.9999999999994994999999")
test("1.9999999999995 ", "1.9999999999994995000000")
test("1.9999999999995 ", "1.9999999999994995000001")

test("1.99999999999994 ", "1.99999999999994")
test("1.999999999999949", "1.999999999999949")
test("1.999999999999949", "1.9999999999999494999999")
test("1.99999999999995 ", "1.9999999999999495000000")
test("1.99999999999995 ", "1.9999999999999495000001")

test("10.4999999999999 ", "10.4999999999999")
test("10.49999999999999", "10.49999999999999")
test("10.49999999999999", "10.499999999999994999999")
test("10.5 ", "10.499999999999995000000")
test("10.5 ", "10.499999999999995000001")

test("1.00000000000499e+11 ", "100000000000.499")
test("1.000000000004999e+11", "100000000000.4999")
test("1.000000000005e+11 ", "100000000000.49999")
}

func TestDecimal64ParseDown(t *testing.T) {
t.Parallel()

ctx := Context64{Rounding: Down}
test := func(expected string, source string) {
t.Helper()
assert.Equal(t, strings.TrimSpace(expected), ctx.MustParse(source).String())
}

test("0", "0")
test("1e-13", "0.0000000000001")
test("1e-13", "1e-13")
test("1", "1")
test("100000", "100000")
test("1e+6", "1000000")

test("1.49999999999999 ", "1.49999999999999")
test("1.499999999999999", "1.499999999999999")
test("1.499999999999999", "1.4999999999999994999999")
test("1.499999999999999", "1.4999999999999995000000")
test("1.499999999999999", "1.4999999999999995000001")

test("1.99999999999949 ", "1.99999999999949")
test("1.999999999999499", "1.999999999999499")
test("1.999999999999499", "1.9999999999994994999999")
test("1.999999999999499", "1.9999999999994995000000")
test("1.999999999999499", "1.9999999999994995000001")

test("1.99999999999994 ", "1.99999999999994")
test("1.999999999999949", "1.999999999999949")
test("1.999999999999949", "1.9999999999999494999999")
test("1.999999999999949", "1.9999999999999495000000")
test("1.999999999999949", "1.9999999999999495000001")

test("10.4999999999999 ", "10.4999999999999")
test("10.49999999999999", "10.49999999999999")
test("10.49999999999999", "10.499999999999994999999")
test("10.49999999999999", "10.499999999999995000000")
test("10.49999999999999", "10.499999999999995000001")

test("1.00000000000499e+11 ", "100000000000.499")
test("1.000000000004999e+11", "100000000000.4999")
test("1.000000000004999e+11", "100000000000.49999")
}

func TestDecimal64Float64(t *testing.T) {
require := require.New(t)

Expand All @@ -56,24 +204,20 @@ func TestDecimal64Float64(t *testing.T) {
}

func TestDecimal64Int64(t *testing.T) {
require := require.New(t)

require.EqualValues(-1, NegOne64.Int64())
require.EqualValues(0, Zero64.Int64())
require.EqualValues(-0, NegZero64.Int64())
require.EqualValues(1, One64.Int64())
require.EqualValues(10, New64FromInt64(10).Int64())

require.EqualValues(0, QNaN64.Int64())
t.Parallel()

require.EqualValues(int64(math.MaxInt64), Infinity64.Int64())
require.EqualValues(int64(math.MinInt64), NegInfinity64.Int64())
assert.EqualValues(t, -1, NegOne64.Int64())
assert.EqualValues(t, 0, Zero64.Int64())
assert.EqualValues(t, -0, NegZero64.Int64())
assert.EqualValues(t, 1, One64.Int64())
assert.EqualValues(t, 10, New64FromInt64(10).Int64())

googol := MustParse64("1e100")
require.EqualValues(int64(math.MaxInt64), googol.Int64())
assert.EqualValues(t, 0, QNaN64.Int64())

long := MustParse64("91234567890123456789e20")
require.EqualValues(int64(math.MaxInt64), long.Int64())
assert.EqualValues(t, int64(math.MaxInt64), Infinity64.Int64())
assert.EqualValues(t, int64(math.MinInt64), NegInfinity64.Int64())
assert.EqualValues(t, int64(math.MaxInt64), MustParse64("1e100").Int64())
assert.EqualValues(t, int64(math.MaxInt64), MustParse64("91234567890123456789e20").Int64())
}

func TestDecimal64IsInf(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions decimal64const.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ var Min64 = newFromParts(0, -398, 1)
// It has the value -1E-398.
var NegMin64 = newFromParts(1, -398, 1)

var zeroes = []Decimal64{Zero64, NegZero64}
var infinities = []Decimal64{Infinity64, NegInfinity64}
var zeroes64 = []Decimal64{Zero64, NegZero64}
var infinities64 = []Decimal64{Infinity64, NegInfinity64}

// DefaultContext64 is the context that arithmetic functions will use in order to
// do calculations.
Expand Down
Loading

0 comments on commit 04a4ec5

Please sign in to comment.