diff --git a/decomposer.go b/decomposer.go new file mode 100644 index 0000000..d2b1d48 --- /dev/null +++ b/decomposer.go @@ -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 +} diff --git a/decomposer_test.go b/decomposer_test.go new file mode 100644 index 0000000..99e6e8a --- /dev/null +++ b/decomposer_test.go @@ -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 ") + } + err = set2.Compose(f, n, c, e) + if err == nil && item.E { + t.Fatal("expected error, got ") + } + 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 ") + } + return + } + if s := fmt.Sprintf("%f", d); s != item.S { + t.Fatalf("unexpected value, got %q want %q", s, item.S) + } + }) + } +}