Skip to content

Commit 3a6548d

Browse files
authored
refactor decimal package to decimal/d64. (#86)
1. Refactor the `decimal` package to `decimal/d64`. 2. Tag `decimal` package as deprecated. 3. Update `README.md`.
1 parent f4e138b commit 3a6548d

33 files changed

+4905
-66
lines changed

Makefile

+26-21
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ test-all: test test-32
99

1010
.PHONY: test
1111
test: test-release
12-
go test $(GOTESTFLAGS) -tags=decimal_debug
12+
go test $(GOTESTFLAGS) -tags=decimal_debug ./d64
1313

1414
.PHONY: test-release
1515
test-release:
16-
go test $(GOTESTFLAGS)
16+
go test $(GOTESTFLAGS) ./d64
1717

1818
.PHONY: test-32
1919
test-32:
2020
if [ "$(shell go env GOOS)" = "linux" ]; then \
21-
GOARCH=386 go test $(subst -race,,$(GOTESTFLAGS)); \
21+
GOARCH=386 go test $(subst -race,,$(GOTESTFLAGS)) ./d64; \
2222
else \
23-
$(DOCKERRUN) -e GOARCH=arm golang:1.23.0 go test $(GOTESTFLAGS); \
23+
$(DOCKERRUN) -e GOARCH=arm golang:1.23.0 go test $(GOTESTFLAGS) ./d64; \
2424
fi
2525

2626
.PHONY: build-linux
@@ -32,19 +32,23 @@ build-linux:
3232
build: build-64-bit build-32-bit
3333

3434
.PHONY: build-32-bit build-64-bit
35-
build-32-bit: decimal.32.release.test decimal.32.debug.test
36-
build-64-bit: decimal.64.release.test decimal.64.debug.test
35+
build-32-bit: d64/decimal.32.release.test d64/decimal.32.debug.test
36+
build-64-bit: d64/decimal.64.release.test d64/decimal.64.debug.test
3737

3838
GOARCH.32=arm
3939
GOARCH.64=
4040

41-
.INTERMEDIATE: decimal.32.release.test decimal.64.release.test
42-
decimal.%.release.test:
43-
GOARCH=$(GOARCH.$*) go test -c -o $@ $(GOTESTFLAGS) .
41+
.INTERMEDIATE: d64/decimal.32.debug.test d64/decimal.64.debug.test
42+
d64/decimal.%.debug.test:
43+
GOARCH=$(GOARCH.$*) go test -c -o $@ -tags=decimal_debug $(GOTESTFLAGS) ./d64
4444

45-
.INTERMEDIATE: decimal.32.debug.test decimal.64.debug.test
46-
decimal.%.debug.test:
47-
GOARCH=$(GOARCH.$*) go test -c -o $@ -tags=decimal_debug $(GOTESTFLAGS) .
45+
.INTERMEDIATE: d64/decimal.32.release.test d64/decimal.64.release.test
46+
d64/decimal.%.release.test:
47+
GOARCH=$(GOARCH.$*) go test -c -o $@ $(GOTESTFLAGS) ./d64
48+
49+
.PHONY: clean
50+
clean:
51+
rm -f decimal.*.release.test decimal.*.debug.test
4852

4953
DOCKERRUN = docker run --rm \
5054
-w /app \
@@ -55,7 +59,7 @@ DOCKERRUN = docker run --rm \
5559
# Dependency on build-linux primes Go caches.
5660
.PHONY: lint
5761
lint: build-linux
58-
$(DOCKERRUN) golangci/golangci-lint:v1.60.1-alpine golangci-lint run
62+
$(DOCKERRUN) golangci/golangci-lint:v1.64.5-alpine golangci-lint run
5963

6064
%.pprof: %.prof
6165
go tool pprof -http=:8080 $<
@@ -65,18 +69,19 @@ lint: build-linux
6569
go test -$*profile $@ $(GOPROFILEFLAGS)
6670

6771
.PHONY: bench
68-
bench: bench.txt
72+
bench: d64/bench.txt
6973
cat $<
7074

71-
bench-stat: bench.stat
75+
.PHONY: bench-stat
76+
bench-stat: d64/bench.stat
7277
cat $<
7378

74-
bench.stat: bench.txt
75-
[ -f bench.old ] || git show @:$< > bench.old || (rm -f $@; false)
76-
benchstat bench.old $< > $@ || (rm -f $@; false)
79+
d64/bench.stat: d64/bench.txt
80+
[ -f d64/bench.old ] || git show @:$< > d64/bench.old || (rm -f $@; false)
81+
benchstat d64/bench.old $< > $@ || (rm -f $@; false)
7782

78-
bench.txt: test
79-
go test -run=^$$ -bench=. -benchmem $(GOBENCHFLAGS) | tee $@ || (rm -f $@; false)
83+
d64/bench.txt: test
84+
go test -run=^$$ -bench=. -benchmem $(GOBENCHFLAGS) ./d64 | tee $@ || (rm -f $@; false)
8085

8186
NOALLOC = \
8287
BenchmarkIODecimal64String2 \
@@ -92,7 +97,7 @@ NOALLOC = \
9297

9398
no-allocs:
9499
allocs=$$( \
95-
go test -run=^$$ -bench="^($$(echo $(NOALLOC) | sed 's/ /|/g'))$$" -benchmem $(GOBENCHFLAGS) | \
100+
go test -run=^$$ -bench="^($$(echo $(NOALLOC) | sed 's/ /|/g'))$$" -benchmem $(GOBENCHFLAGS) ./d64 | \
96101
awk '/^Benchmark/ {if ($$7 != "0") print}' \
97102
); \
98103
if [ -n "$$allocs" ]; then \

README.md

+36-45
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,100 @@
11
# decimal
22

3-
This library implements fixed-precision decimal numbers based on the IEEE 754-2019 standard;
4-
<https://ieeexplore.ieee.org/document/8766229>.
5-
More info can be found at:
6-
<http://speleotrove.com/decimal/>
3+
This library implements fixed-precision decimal numbers based on the [IEEE 754-2019 standard](https://ieeexplore.ieee.org/document/8766229).
4+
More info can be found at <http://speleotrove.com/decimal/>.
75

86
## Features
97

108
- Decimal64, partial implementation of the ieee-754R standard
11-
- Half up and half even rounding
9+
- Rounding modes: half up, half even, down (towards zero)
1210
- Up to 3 times faster than arbitrary precision decimal libraries in Go
1311

1412
## Goals
1513

16-
- Implement 128 bit decimal
14+
- Implement 128 bit decimal.
1715
- Implement as much of <https://speleotrove.com/decimal/decarith.pdf> as possible.
1816

1917
## Installation and use
2018

21-
Run `go get github.com/anz-bank/decimal`
19+
Run `go get github.com/anz-bank/decimal/d64`
2220

2321
```go
2422
package main
2523

2624
import (
2725
"fmt"
2826

29-
"github.com/anz-bank/decimal"
27+
"github.com/anz-bank/decimal/d64"
3028
)
3129

3230
func main() {
33-
var a decimal.Decimal64
34-
b := decimal.MustParse64("0.1")
35-
c := decimal.MustParse64("0.3")
36-
d := decimal.New64FromInt64(123456)
31+
var a d64.Decimal
32+
b := d64.MustParse("0.1")
33+
c := d64.MustParse("0.3")
34+
d := d64.NewFromInt64(123456)
3735

3836
fmt.Println(a, b, c, d)
3937
}
4038
```
4139

4240
## Usage notes
4341

42+
The d128 package doesn't exist yet, so d64 is assumed below.
43+
4444
### Formatting
4545

46-
`Decimal64` provides numerous ways to present numbers for human and machine
47-
consumption.
46+
`Decimal` provides numerous ways to present numbers for human and machine consumption.
4847

49-
`Decimal64` implements the following conventional interfaces:
48+
`Decimal` implements the following conventional interfaces:
5049

5150
- `fmt`: `Formatter`, `Scanner` and `Stringer`
52-
- It currently supports specifying a precision argument, e.g., `%.10f` for the
53-
`f` and `F` verbs, while support for `g` and `G` is [planned](https://github.com/anz-bank/decimal/issues/72), as is [support for width specifiers](https://github.com/anz-bank/decimal/issues/72).
51+
- It currently supports specifying a precision argument, e.g., `%.10f` for the `f` and `F` verbs, while support for `g` and `G` is [planned](https://github.com/anz-bank/decimal/issues/72), as is [support for width specifiers](https://github.com/anz-bank/decimal/issues/72).
5452
- `json`: `Marshaller` and `Unmarshaller`
5553
- `encoding`: `BinaryMarshaler`, `BinaryUnmarshaler`, `TextMarshaler` and `TextUnmarshaler`
56-
- `encoging/gob`: `GobEncoder` and `GobDecoder`
54+
- `encoding/gob`: `GobEncoder` and `GobDecoder`
5755

58-
The following methods provide more direct access to the internal methods used to
59-
implement `fmt.Formatter`. (For maximum control, use `fmt.Printf` &co or invoke
60-
the `fmt.Formatter` interface directly.)
56+
The following methods provide more direct access to the internal methods used to implement `fmt.Formatter`.
57+
For maximum control, use `fmt.Printf` &co or invoke the `fmt.Formatter` interface directly.
6158

62-
- `Decimal64.Append` formats straight into a `[]byte` buffer.
63-
- `Decimal64.Text` formats in the same way, but returns a `string`.
59+
- `Decimal.Append` formats straight into a `[]byte` buffer.
60+
- `Decimal.Text` formats in the same way, but returns a `string`.
6461

6562
### Debugging
6663

67-
tl;dr: Use the `decimal_debug` compiler tag during debugging to greatly ease
68-
runtime inspection of `Decimal64` values.
64+
tl;dr: Use the `decimal_debug` compiler tag during debugging to greatly ease runtime inspection of `Decimal` values.
6965

70-
Debugging with the `decimal` package can be challeging because a `Decimal64`
71-
number is encoded in a `uint64` and the values it holds are inscrutable even to
72-
the trained eye. For example, the number one `decimal.One64` is represented
73-
internally as the number `3450757314565799936` (`2fe38d7ea4c68000` in
74-
hexadecimal).
66+
Debugging with the `decimal` package can be challenging because a `Decimal` number is encoded in a `uint64` and the values it holds are inscrutable even to the trained eye.
67+
For example, the number one `One` is represented internally as the number `3450757314565799936` (`2fe38d7ea4c68000` in hexadecimal).
7568

76-
To ease debugging, `Decimal64` holds an optional `debugInfo` structure that
77-
contains a string representation and unpacked components of the `uint64`
78-
representation for every `Decimal64` value.
69+
To ease debugging, `Decimal` holds an optional `debugInfo` structure that contains a string representation and unpacked components of the `uint64` representation for every `Decimal` value.
7970

80-
This feature is enabled through the `decimal_debug` compiler tag. This is done
81-
at compile time instead of through runtime flags because having the structure
82-
there even if not used would double the size of each number and greatly increase
83-
the cost of using it. The size and runtime cost of this feature is zero when the
84-
compiler tag is not present.
71+
This feature is enabled through the `decimal_debug` compiler tag.
72+
This is done at compile time instead of through runtime flags because having the structure there, even if not used, would double the size of each number and greatly increase the cost of using it.
73+
The size and runtime cost of this feature is zero when the compiler tag is not present.
8574

8675
## Docs
8776

8877
<https://godoc.org/github.com/anz-bank/decimal>
8978

9079
## Why decimal?
9180

92-
Binary floating point numbers are fundamentally flawed when it comes to representing exact numbers in a decimal world. Just like 1/3 can't be represented in base 10 (it evaluates to 0.3333333333 repeating), 1/10 can't be represented in binary.
81+
Binary floating point numbers are fundamentally flawed when it comes to representing exact numbers in a decimal world.
82+
Just like 1/3 can't be represented in base 10 (it evaluates to 0.3333333333 repeating), 1/10 can't be represented in binary.
9383
The solution is to use a decimal floating point number.
9484
Binary floating point numbers (often just called floating point numbers) are usually in the form `Sign * Significand * 2 ^ exp` and decimal floating point numbers change this to `Sign * Significand * 10 ^ exp`.
9585
The use of base 10 eliminates the decimal fraction problem.
9686

9787
## Why fixed precision?
9888

99-
Most implementations of a decimal floating point datatype implement an *arbitrary precision* type, which often uses an underlying big int. This gives flexibility in that as the number grows, the number of bits assigned to the number grows ( hence the term "arbitrary precision").
100-
This library is different. It uses a 64-bit decimal datatype as specified in the IEEE-754R standard. This sacrifices the ability to represent arbitrarily large numbers, but is much faster than arbitrary precision libraries.
89+
Most implementations of a decimal floating point datatype implement an *arbitrary precision* type, which often uses an underlying big int.
90+
This gives flexibility in that as the number grows, the number of bits assigned to the number grows (hence the term "arbitrary precision").
91+
This library is different.
92+
It uses a 64-bit decimal datatype as specified in the IEEE-754R standard.
93+
This sacrifices the ability to represent arbitrarily large numbers, but is much faster than arbitrary precision libraries.
10194
There are two main reasons for this:
10295

103-
1. The fixed-size data type is a `uint64` under the hood and never requires heap
104-
allocation.
105-
2. All the algorithms can hard-code assumptions about the number of bits to work
106-
with.
96+
1. The fixed-size data type is a `uint64` under the hood and never requires heap allocation.
97+
2. All the algorithms can hard-code assumptions about the number of bits to work with.
10798
In fact, many of the operations work on the entire number as a single unit
10899
using 64-bit integer arithmetic and, on the occasions it needs to use more,
109100
128 bits always suffices.

d64/bench.txt

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
goos: darwin
2+
goarch: arm64
3+
pkg: github.com/anz-bank/decimal/d64
4+
cpu: Apple M4 Max
5+
BenchmarkIODecimalString-16 45000848 26.82 ns/op 16 B/op 1 allocs/op
6+
BenchmarkIODecimalString2-16 50688696 22.96 ns/op 13 B/op 0 allocs/op
7+
BenchmarkIODecimalFormat-16 16520335 72.77 ns/op 56 B/op 3 allocs/op
8+
BenchmarkIODecimalAppend-16 65414551 18.39 ns/op 0 B/op 0 allocs/op
9+
BenchmarkDecimalAbs-16 1000000000 0.5281 ns/op 0 B/op 0 allocs/op
10+
BenchmarkDecimalAdd-16 169439442 7.296 ns/op 0 B/op 0 allocs/op
11+
BenchmarkDecimalCmp-16 121484217 9.711 ns/op 0 B/op 0 allocs/op
12+
BenchmarkDecimalMul-16 155150511 7.752 ns/op 0 B/op 0 allocs/op
13+
BenchmarkFloat64Mul-16 1000000000 0.8068 ns/op 0 B/op 0 allocs/op
14+
BenchmarkDecimalQuo-16 217405482 5.582 ns/op 0 B/op 0 allocs/op
15+
BenchmarkDecimalSqrt-16 216664428 5.596 ns/op 0 B/op 0 allocs/op
16+
BenchmarkDecimalSub-16 164511207 7.313 ns/op 0 B/op 0 allocs/op
17+
BenchmarkIOParse-16 6746329 181.2 ns/op 64 B/op 2 allocs/op
18+
BenchmarkIODecimalScan-16 9673527 123.8 ns/op 80 B/op 2 allocs/op
19+
PASS
20+
ok github.com/anz-bank/decimal/d64 21.807s

d64/const.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package d64
2+
3+
// Zero is 0 represented as a [Decimal].
4+
var Zero = newFromParts(0, 0, 0)
5+
6+
// NegZero is -0 represented as a [Decimal].
7+
// Note that [Zero] != NegZero, but [Zero].Equal(NegZero) returns true.
8+
var NegZero = newFromParts(1, 0, 0)
9+
10+
// One is 1 represented as a [Decimal].
11+
var One = newFromParts(0, -15, decimalBase)
12+
13+
// NegOne is -1 represented as a [Decimal].
14+
var NegOne = newFromParts(1, -15, decimalBase)
15+
16+
// Inf is ∞ represented as a [Decimal].
17+
var Inf = newDec(inf)
18+
19+
// NegInf is -∞ represented as a [Decimal].
20+
var NegInf = newDec(neg | inf)
21+
22+
// QNaN is a quiet NaN represented as a [Decimal].
23+
var QNaN = newDec(0x7c << 56)
24+
25+
// SNaN is a signalling NaN represented as a [Decimal].
26+
// Note that the decimal never signals on NaNs but some operations treat sNaN
27+
// differently to NaN.
28+
var SNaN = newDec(0x7e << 56)
29+
30+
// Pi represents the transcendental number π.
31+
var Pi = newFromParts(0, -15, 3_141592653589793)
32+
33+
// E represents the transcendental number e (lim[n→∞](1+1/n)ⁿ).
34+
var E = newFromParts(0, -15, 2_718281828459045)
35+
36+
const neg uint64 = 0x80 << 56
37+
const inf uint64 = 0x78 << 56
38+
39+
const decimalBase uint64 = 1_000_000_000_000_000 // 1E15
40+
const decimalDigits = 16
41+
42+
// maxSig is the maximum significand possible that fits in 16 decimal places.
43+
const maxSig = 10*decimalBase - 1
44+
45+
const expOffset = 398
46+
const expMax = 369
47+
48+
// Max is the highest finite number representable as a [Decimal].
49+
// It has the value 9.999999999999999E+384.
50+
var Max = newFromParts(0, expMax, maxSig)
51+
52+
// NegMax is the lowest finite number representable as a [Decimal].
53+
// It has the value -9.999999999999999E+384.
54+
var NegMax = newFromParts(1, expMax, maxSig)
55+
56+
// Min is the closest positive number to zero.
57+
// It has the value 1E-398.
58+
var Min = newFromParts(0, -398, 1)
59+
60+
// NegMin is the closest negative number to zero.
61+
// It has the value -1E-398.
62+
var NegMin = newFromParts(1, -398, 1)
63+
64+
var zeroes = [2]Decimal{Zero, NegZero}
65+
var infinities = [2]Decimal{Inf, NegInf}
66+
67+
// DefaultContext is the context that arithmetic functions will use in order to
68+
// do calculations.
69+
// Setting this context to a different value will globally affect all
70+
// [Decimal] methods whose behavior depends on context.
71+
// Note that all such methods are also available as direct methods of [Context].
72+
// It uses [HalfUp] rounding.
73+
var DefaultContext = Context{Rounding: HalfUp}

d64/const_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package d64
2+
3+
import "testing"
4+
5+
func TestPi(t *testing.T) {
6+
t.Parallel()
7+
8+
equal(t, "3.141592653589793", Pi.String())
9+
}
10+
11+
func TestE(t *testing.T) {
12+
t.Parallel()
13+
14+
equal(t, "2.718281828459045", E.String())
15+
}

0 commit comments

Comments
 (0)