Skip to content

Commit

Permalink
decimal: add decomposer interface
Browse files Browse the repository at this point in the history
  • Loading branch information
kardianos committed Apr 9, 2019
1 parent b4c50b8 commit 9d983c3
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 0 deletions.
113 changes: 113 additions & 0 deletions decomposer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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 little-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.
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) >= 16 {
coefficient = buf[:16]
} else {
coefficient = make([]byte, 16)
}
binary.LittleEndian.PutUint64(coefficient, z.compact)
} else {
coefficient = z.unscaled.Bytes() // This returns a big-endian slice.
// Swap bytes for little endian.

i := 0
j := len(coefficient) - 1
for i < j {
coefficient[i], coefficient[j] = coefficient[j], coefficient[i]
i++
j--
}
}
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
}
// Finite form.

// bigint expect bytes in big-endian form.
i := 0
j := len(coefficient) - 1
for i < j {
coefficient[i], coefficient[j] = coefficient[j], coefficient[i]
i++
j--
}
c := &big.Int{}
c.SetBytes(coefficient)
z.SetBigMantScale(c, -int(exponent))
if negative {
z.Neg(z)
}
return nil
}
89 changes: 89 additions & 0 deletions decomposer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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 := &Big{}
err := set.Compose(d.Decompose(nil))
if err == nil && item.E {
t.Fatal("expected error, got <nil>")
}
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{0x40, 0xE2, 0x01}, Exp: -3},
{N: "Neg-1", S: "-123.456", Neg: true, Coef: []byte{0x40, 0xE2, 0x01}, Exp: -3},
{N: "PosExp-1", S: "123456000", Coef: []byte{0x40, 0xE2, 0x01}, Exp: 3},
{N: "PosExp-2", S: "-123456000", Neg: true, Coef: []byte{0x40, 0xE2, 0x01}, Exp: 3},
{N: "AllDec-1", S: "0.123456", Coef: []byte{0x40, 0xE2, 0x01}, Exp: -6},
{N: "AllDec-2", S: "-0.123456", Neg: true, Coef: []byte{0x40, 0xE2, 0x01}, 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)
}
})
}
}

0 comments on commit 9d983c3

Please sign in to comment.