Skip to content

Commit d916040

Browse files
committed
decimal: add decomposer interface
For golang/go#30870
1 parent c787d12 commit d916040

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

decomposer.go

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package decimal
2+
3+
import (
4+
"encoding/binary"
5+
"fmt"
6+
"math"
7+
"math/big"
8+
)
9+
10+
// decomposer composes or decomposes a decimal value to and from individual parts.
11+
// There are four separate parts: a boolean negative flag, a form byte with three possible states
12+
// (finite=0, infinite=1, NaN=2), a base-2 little-endian integer
13+
// coefficient (also known as a significand) as a []byte, and an int32 exponent.
14+
// These are composed into a final value as "decimal = (neg) (form=finite) coefficient * 10 ^ exponent".
15+
// A zero length coefficient is a zero value.
16+
// If the form is not finite, the coefficient and scale should be zero and should be ignored.
17+
// The negative parameter may be set to true for any form, although implementations are not required
18+
// to respect the negative parameter in the non-finite form.
19+
//
20+
// Implementations may choose to signal a negative zero or negative NaN, but implementations
21+
// that do not support these may also ignore the negative zero or negative NaN without error.
22+
// If an implementation does not support Infinity it may be converted into a NaN without error.
23+
// If a value is set that is larger then what is supported by an implementation is attempted to
24+
// be set, an error must be returned.
25+
// Implementations must return an error if a NaN or Infinity is attempted to be set while neither
26+
// are supported.
27+
type decomposer interface {
28+
// Decompose returns the internal decimal state into parts.
29+
// If the provided buf has sufficient capacity, buf may be returned as the coefficient with
30+
// the value set and length set as appropriate.
31+
Decompose(buf []byte) (form byte, negative bool, coefficient []byte, exponent int32)
32+
33+
// Compose sets the internal decimal value from parts. If the value cannot be
34+
// represented then an error should be returned.
35+
Compose(form byte, negative bool, coefficient []byte, exponent int32) error
36+
}
37+
38+
// Decompose returns the internal decimal state into parts.
39+
// If the provided buf has sufficient capacity, buf may be returned as the coefficient with
40+
// the value set and length set as appropriate.
41+
func (z *Big) Decompose(buf []byte) (form byte, negative bool, coefficient []byte, exponent int32) {
42+
negative = z.Sign() < 0
43+
switch {
44+
case z.IsInf(0):
45+
form = 1
46+
return
47+
case z.IsNaN(0):
48+
form = 2
49+
return
50+
}
51+
if !z.IsFinite() {
52+
panic("expected number to be finite")
53+
}
54+
if z.exp > math.MaxInt32 {
55+
panic("exponent exceeds max size")
56+
}
57+
exponent = int32(z.exp)
58+
59+
if z.isCompact() {
60+
if cap(buf) >= 16 {
61+
coefficient = buf[:16]
62+
} else {
63+
coefficient = make([]byte, 16)
64+
}
65+
binary.LittleEndian.PutUint64(coefficient, z.compact)
66+
} else {
67+
coefficient = z.unscaled.Bytes() // This returns a big-endian slice.
68+
// Swap bytes for little endian.
69+
70+
i := 0
71+
j := len(coefficient) - 1
72+
for i < j {
73+
coefficient[i], coefficient[j] = coefficient[j], coefficient[i]
74+
i++
75+
j--
76+
}
77+
}
78+
return
79+
}
80+
81+
// Compose sets the internal decimal value from parts. If the value cannot be
82+
// represented then an error should be returned.
83+
func (z *Big) Compose(form byte, negative bool, coefficient []byte, exponent int32) error {
84+
switch form {
85+
default:
86+
return fmt.Errorf("unknown form: %v", form)
87+
case 0:
88+
// Finite form below.
89+
case 1:
90+
z.SetInf(negative)
91+
return nil
92+
case 2:
93+
z.SetNaN(false)
94+
return nil
95+
}
96+
// Finite form.
97+
98+
// bigint expect bytes in big-endian form.
99+
i := 0
100+
j := len(coefficient) - 1
101+
for i < j {
102+
coefficient[i], coefficient[j] = coefficient[j], coefficient[i]
103+
i++
104+
j--
105+
}
106+
c := &big.Int{}
107+
c.SetBytes(coefficient)
108+
z.SetBigMantScale(c, -int(exponent))
109+
if negative {
110+
z.Neg(z)
111+
}
112+
return nil
113+
}

decomposer_test.go

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package decimal
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestDecomposerRoundTrip(t *testing.T) {
8+
list := []struct {
9+
N string // Name.
10+
S string // String value.
11+
E bool // Expect an error.
12+
}{
13+
{N: "Normal-1", S: "123.456"},
14+
{N: "Normal-2", S: "-123.456"},
15+
{N: "NaN-1", S: "NaN"},
16+
{N: "NaN-2", S: "-NaN"},
17+
{N: "Infinity-1", S: "Infinity"},
18+
{N: "Infinity-2", S: "-Infinity"},
19+
}
20+
for _, item := range list {
21+
t.Run(item.N, func(t *testing.T) {
22+
d := &Big{}
23+
d, ok := d.SetString(item.S)
24+
if !ok {
25+
t.Fatal("unable to set value")
26+
}
27+
set := &Big{}
28+
err := set.Compose(d.Decompose(nil))
29+
if err == nil && item.E {
30+
t.Fatal("expected error, got <nil>")
31+
}
32+
if err != nil && !item.E {
33+
t.Fatalf("unexpected error: %v", err)
34+
}
35+
if set.Cmp(d) != 0 {
36+
t.Fatalf("values incorrect, got %v want %v (%s)", set, d, item.S)
37+
}
38+
})
39+
}
40+
}
41+
42+
func TestCompose(t *testing.T) {
43+
list := []struct {
44+
N string // Name.
45+
S string // String value.
46+
47+
Form byte // Form
48+
Neg bool
49+
Coef []byte // Coefficent
50+
Exp int32
51+
52+
Err bool // Expect an error.
53+
}{
54+
{N: "Normal-1", S: "123.456", Coef: []byte{0x40, 0xE2, 0x01}, Exp: -3},
55+
{N: "Normal-2", S: "-123.456", Neg: true, Coef: []byte{0x40, 0xE2, 0x01}, Exp: -3},
56+
{N: "NaN-1", S: "NaN", Form: 2},
57+
{N: "Infinity-1", S: "Infinity", Form: 1},
58+
{N: "Infinity-2", S: "-Infinity", Form: 1, Neg: true},
59+
}
60+
61+
for _, item := range list {
62+
t.Run(item.N, func(t *testing.T) {
63+
d := &Big{}
64+
d, ok := d.SetString(item.S)
65+
if !ok {
66+
t.Fatal("unable to set value")
67+
}
68+
err := d.Compose(item.Form, item.Neg, item.Coef, item.Exp)
69+
if err != nil && !item.Err {
70+
t.Fatalf("unexpected error, got %v", err)
71+
}
72+
if item.Err {
73+
if err == nil {
74+
t.Fatal("expected error, got <nil>")
75+
}
76+
return
77+
}
78+
if s := d.String(); s != item.S {
79+
t.Fatalf("unexpected value, got %q want %q", s, item.S)
80+
}
81+
})
82+
}
83+
}

0 commit comments

Comments
 (0)