Skip to content

Commit

Permalink
support context-controlled parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
anzdaddy committed Aug 14, 2024
1 parent a2aa86e commit 969cf25
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 29 deletions.
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.PHONY: all
all: test test-debug

.PHONY: test
test:
go test

.PHONY: test-debug
test-debug:
go test -tags=decimal_debug
14 changes: 14 additions & 0 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
160 changes: 152 additions & 8 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 Down Expand Up @@ -66,14 +214,10 @@ func TestDecimal64Int64(t *testing.T) {

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

require.EqualValues(int64(math.MaxInt64), Infinity64.Int64())
require.EqualValues(int64(math.MinInt64), NegInfinity64.Int64())

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

long := MustParse64("91234567890123456789e20")
require.EqualValues(int64(math.MaxInt64), long.Int64())
require.EqualValues(math.MaxInt64, Infinity64.Int64())

Check failure on line 217 in decimal64_test.go

View workflow job for this annotation

GitHub Actions / Build

cannot use math.MaxInt64 (untyped int constant 9223372036854775807) as int value in argument to require.EqualValues (overflows)
require.EqualValues(math.MinInt64, NegInfinity64.Int64())

Check failure on line 218 in decimal64_test.go

View workflow job for this annotation

GitHub Actions / Build

cannot use math.MinInt64 (untyped int constant -9223372036854775808) as int value in argument to require.EqualValues (overflows)
require.EqualValues(math.MaxInt64, MustParse64("1e100").Int64())

Check failure on line 219 in decimal64_test.go

View workflow job for this annotation

GitHub Actions / Build

cannot use math.MaxInt64 (untyped int constant 9223372036854775807) as int value in argument to require.EqualValues (overflows)
require.EqualValues(math.MaxInt64, MustParse64("91234567890123456789e20").Int64())

Check failure on line 220 in decimal64_test.go

View workflow job for this annotation

GitHub Actions / Build

cannot use math.MaxInt64 (untyped int constant 9223372036854775807) as int value in argument to require.EqualValues (overflows)
}

func TestDecimal64IsInf(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion decimal64fmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ func (ctx Context64) format(d Decimal64, s fmt.State, format rune) {
case 'v':
format = 'g'
default:
fmt.Fprintf(s, "%%!%c(*decimal.Decimal64=%s)", format, d.String())
fmt.Fprintf(s, "%%!%c(decimal.Decimal64=%s)", format, d.String())
return
}

Expand All @@ -229,6 +229,9 @@ func (ctx Context64) text(d Decimal64, format rune, width, prec int) string {
return string(ctx.append(d, make([]byte, 0, 16), format, width, prec))
}

// Contextual binds a [Decimal64] to a [Context64] for greater control of formatting.
// It implements [fmt.Stringer] and [fmt.Formatter] on behalf of the number,
// using the context to control formatting.
type Contextual struct {
ctx Context64
d Decimal64
Expand Down
12 changes: 10 additions & 2 deletions decimal64fmt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,21 @@ func TestDecimal64FormatPrec(t *testing.T) {
t.Parallel()
pi := MustParse64("3.1415926535897932384626433")

test := func(expected string, prec int, input Decimal64) {
test := func(expected string, prec int, n Decimal64) {
t.Helper()
buf := string(DefaultFormatContext64.append(input, nil, 'f', -1, prec))
buf := string(DefaultFormatContext64.append(n, nil, 'f', -1, prec))
assert.Equal(t, expected, string(buf))
assert.Equal(t, expected, fmt.Sprintf("%.*f", prec, n))
assert.Equal(t, expected, n.Text('f', prec))
}

assert.Equal(t, "3.141592653589793", pi.String())
assert.Equal(t, "3.141592653589793", Context64{Rounding: HalfEven}.With(pi).String())
assert.Equal(t, "3.141592653589793", Context64{Rounding: HalfUp}.With(pi).String())
assert.Equal(t, "3.141592653589793", fmt.Sprintf("%v", pi))
assert.Equal(t, "3.141593", fmt.Sprintf("%f", pi))
assert.Equal(t, "%!q(decimal.Decimal64=3.141592653589793)", fmt.Sprintf("%q", pi))

test("3", 0, pi)
test("3.1", 1, pi)
test("3.14", 2, pi)
Expand Down Expand Up @@ -202,6 +209,7 @@ func TestDecimal64FormatPrecEdgeCasesHalfUp(t *testing.T) {
n, err := Parse64(input)
require.NoError(t, err)
assert.Equal(t, expected, ctx.With(n).Text('f', -1, 3))
assert.Equal(t, expected, fmt.Sprintf("%.3f", ctx.With(n)))
}

test("0.063", "0.0625")
Expand Down
4 changes: 1 addition & 3 deletions decimal64math.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,9 +584,7 @@ func (ctx Context64) roundRefRaw(dp, ep *decParts) Decimal64 {
exp := dexp
if grew {
s /= 10
if exp++; exp > expMax {
return infinitiesRaw[dp.sign]
}
exp++ // Cannot max out because final digit never rounds up.
}
exp, s = resubnormal(exp, s)
return newFromPartsRaw(dp.sign, exp, s)
Expand Down
15 changes: 15 additions & 0 deletions decimal64math_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,21 @@ func TestRoundHDown(t *testing.T) {
assert.Equal(t, uint64(3000000000000000), rnd(ctx, 3000000000000000, 100000000000000))
}

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

ctx := Context64{Rounding: HalfUp}
assert.Equal(t, "0", ctx.ToIntegral(MustParse64("0")).String())
assert.Equal(t, "0", ctx.ToIntegral(MustParse64("0.499999999999999")).String())
assert.Equal(t, "1", ctx.ToIntegral(MustParse64("1")).String())
assert.Equal(t, "1", ctx.ToIntegral(MustParse64("1.49999999999999")).String())
assert.Equal(t, "2", ctx.ToIntegral(MustParse64("1.5")).String())
assert.Equal(t, "9", ctx.ToIntegral(MustParse64("9.49999999999999")).String())
assert.Equal(t, "10", ctx.ToIntegral(MustParse64("9.5")).String())
assert.Equal(t, "99", ctx.ToIntegral(MustParse64("99.499999999999")).String())
assert.Equal(t, "100", ctx.ToIntegral(MustParse64("99.5")).String())
}

func benchmarkDecimal64Data() []Decimal64 {
return []Decimal64{
One64,
Expand Down
Loading

0 comments on commit 969cf25

Please sign in to comment.