Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

decimal: add decomposer interface #124

Merged
merged 1 commit into from
Jul 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions decomposer.go
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
}
98 changes: 98 additions & 0 deletions decomposer_test.go
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)
}
})
}
}