-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
decimal: add decomposer interface (#124)
For golang/go#30870
- Loading branch information
1 parent
d0a4357
commit 50723dc
Showing
2 changed files
with
194 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package decimal | ||
|
||
import ( | ||
"encoding/binary" | ||
"fmt" | ||
"math" | ||
"math/big" | ||
) | ||
|
||
// decomposer composes or decomposes a decimal value to and from individual parts. | ||
// There are four separate parts: a boolean negative flag, a form byte with three possible states | ||
// (finite=0, infinite=1, NaN=2), a base-2 big-endian integer | ||
// coefficient (also known as a significand) as a []byte, and an int32 exponent. | ||
// These are composed into a final value as "decimal = (neg) (form=finite) coefficient * 10 ^ exponent". | ||
// A zero length coefficient is a zero value. | ||
// If the form is not finite the coefficient and scale should be ignored. | ||
// The negative parameter may be set to true for any form, although implementations are not required | ||
// to respect the negative parameter in the non-finite form. | ||
// | ||
// Implementations may choose to signal a negative zero or negative NaN, but implementations | ||
// that do not support these may also ignore the negative zero or negative NaN without error. | ||
// If an implementation does not support Infinity it may be converted into a NaN without error. | ||
// If a value is set that is larger then what is supported by an implementation is attempted to | ||
// be set, an error must be returned. | ||
// Implementations must return an error if a NaN or Infinity is attempted to be set while neither | ||
// are supported. | ||
type decomposer interface { | ||
// Decompose returns the internal decimal state into parts. | ||
// If the provided buf has sufficient capacity, buf may be returned as the coefficient with | ||
// the value set and length set as appropriate. | ||
Decompose(buf []byte) (form byte, negative bool, coefficient []byte, exponent int32) | ||
|
||
// Compose sets the internal decimal value from parts. If the value cannot be | ||
// represented then an error should be returned. | ||
// The coefficent should not be modified. Successive calls to compose with | ||
// the same arguments should result in the same decimal value. | ||
Compose(form byte, negative bool, coefficient []byte, exponent int32) error | ||
} | ||
|
||
// Decompose returns the internal decimal state into parts. | ||
// If the provided buf has sufficient capacity, buf may be returned as the coefficient with | ||
// the value set and length set as appropriate. | ||
func (z *Big) Decompose(buf []byte) (form byte, negative bool, coefficient []byte, exponent int32) { | ||
negative = z.Sign() < 0 | ||
switch { | ||
case z.IsInf(0): | ||
form = 1 | ||
return | ||
case z.IsNaN(0): | ||
form = 2 | ||
return | ||
} | ||
if !z.IsFinite() { | ||
panic("expected number to be finite") | ||
} | ||
if z.exp > math.MaxInt32 { | ||
panic("exponent exceeds max size") | ||
} | ||
exponent = int32(z.exp) | ||
|
||
if z.isCompact() { | ||
if cap(buf) >= 8 { | ||
coefficient = buf[:8] | ||
} else { | ||
coefficient = make([]byte, 8) | ||
} | ||
binary.BigEndian.PutUint64(coefficient, z.compact) | ||
} else { | ||
coefficient = z.unscaled.Bytes() // This returns a big-endian slice. | ||
} | ||
return | ||
} | ||
|
||
// Compose sets the internal decimal value from parts. If the value cannot be | ||
// represented then an error should be returned. | ||
func (z *Big) Compose(form byte, negative bool, coefficient []byte, exponent int32) error { | ||
switch form { | ||
default: | ||
return fmt.Errorf("unknown form: %v", form) | ||
case 0: | ||
// Finite form below. | ||
case 1: | ||
z.SetInf(negative) | ||
return nil | ||
case 2: | ||
z.SetNaN(false) | ||
return nil | ||
} | ||
bigc := &big.Int{} | ||
bigc.SetBytes(coefficient) | ||
z.SetBigMantScale(bigc, -int(exponent)) | ||
if negative { | ||
z.Neg(z) | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package decimal | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
) | ||
|
||
func TestDecomposerRoundTrip(t *testing.T) { | ||
list := []struct { | ||
N string // Name. | ||
S string // String value. | ||
E bool // Expect an error. | ||
}{ | ||
{N: "Normal-1", S: "123.456"}, | ||
{N: "Normal-2", S: "-123.456"}, | ||
{N: "NaN-1", S: "NaN"}, | ||
{N: "NaN-2", S: "-NaN"}, | ||
{N: "Infinity-1", S: "Infinity"}, | ||
{N: "Infinity-2", S: "-Infinity"}, | ||
} | ||
for _, item := range list { | ||
t.Run(item.N, func(t *testing.T) { | ||
d := &Big{} | ||
d, ok := d.SetString(item.S) | ||
if !ok { | ||
t.Fatal("unable to set value") | ||
} | ||
set, set2 := &Big{}, &Big{} | ||
f, n, c, e := d.Decompose(nil) | ||
err := set.Compose(f, n, c, e) | ||
if err == nil && item.E { | ||
t.Fatal("expected error, got <nil>") | ||
} | ||
err = set2.Compose(f, n, c, e) | ||
if err == nil && item.E { | ||
t.Fatal("expected error, got <nil>") | ||
} | ||
if set.Cmp(set2) != 0 { | ||
t.Fatalf("composing the same value twice resulted in different values. set=%v set2=%v", set, set2) | ||
} | ||
|
||
if err != nil && !item.E { | ||
t.Fatalf("unexpected error: %v", err) | ||
} | ||
if set.Cmp(d) != 0 { | ||
t.Fatalf("values incorrect, got %v want %v (%s)", set, d, item.S) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestDecomposerCompose(t *testing.T) { | ||
list := []struct { | ||
N string // Name. | ||
S string // String value. | ||
|
||
Form byte // Form | ||
Neg bool | ||
Coef []byte // Coefficent | ||
Exp int32 | ||
|
||
Err bool // Expect an error. | ||
}{ | ||
{N: "Zero", S: "0", Coef: nil, Exp: 0}, | ||
{N: "Normal-1", S: "123.456", Coef: []byte{0x01, 0xE2, 0x40}, Exp: -3}, | ||
{N: "Neg-1", S: "-123.456", Neg: true, Coef: []byte{0x01, 0xE2, 0x40}, Exp: -3}, | ||
{N: "PosExp-1", S: "123456000", Coef: []byte{0x01, 0xE2, 0x40}, Exp: 3}, | ||
{N: "PosExp-2", S: "-123456000", Neg: true, Coef: []byte{0x01, 0xE2, 0x40}, Exp: 3}, | ||
{N: "AllDec-1", S: "0.123456", Coef: []byte{0x01, 0xE2, 0x40}, Exp: -6}, | ||
{N: "AllDec-2", S: "-0.123456", Neg: true, Coef: []byte{0x01, 0xE2, 0x40}, Exp: -6}, | ||
{N: "NaN-1", S: "NaN", Form: 2}, | ||
{N: "Infinity-1", S: "Infinity", Form: 1}, | ||
{N: "Infinity-2", S: "-Infinity", Form: 1, Neg: true}, | ||
} | ||
|
||
for _, item := range list { | ||
t.Run(item.N, func(t *testing.T) { | ||
d := &Big{} | ||
d, ok := d.SetString(item.S) | ||
if !ok { | ||
t.Fatal("unable to set value") | ||
} | ||
err := d.Compose(item.Form, item.Neg, item.Coef, item.Exp) | ||
if err != nil && !item.Err { | ||
t.Fatalf("unexpected error, got %v", err) | ||
} | ||
if item.Err { | ||
if err == nil { | ||
t.Fatal("expected error, got <nil>") | ||
} | ||
return | ||
} | ||
if s := fmt.Sprintf("%f", d); s != item.S { | ||
t.Fatalf("unexpected value, got %q want %q", s, item.S) | ||
} | ||
}) | ||
} | ||
} |