From 545b7c967e90e8d498936e7c0a9e36ca805b7c98 Mon Sep 17 00:00:00 2001 From: Daniel Theophanes Date: Fri, 5 Apr 2019 22:01:13 -0700 Subject: [PATCH] apd: add decomposer interface methods This implements the interface as worked on in the issue https://golang.org/issue/30870 . --- decomposer.go | 121 +++++++++++++++++++++++++++++++++++++++++++++ decomposer_test.go | 111 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 decomposer.go create mode 100644 decomposer_test.go diff --git a/decomposer.go b/decomposer.go new file mode 100644 index 0000000..e245e23 --- /dev/null +++ b/decomposer.go @@ -0,0 +1,121 @@ +// Copyright 2016 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package apd + +import ( + "fmt" +) + +// 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. + // 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 (d *Decimal) Decompose(buf []byte) (form byte, negative bool, coefficient []byte, exponent int32) { + switch d.Form { + default: + panic(fmt.Errorf("unknown Form: %v", d.Form)) + case Finite: + // Nothing, continue on. + case Infinite: + negative = d.Negative + form = 1 + return + case NaNSignaling, NaN: + negative = d.Negative + form = 2 + return + } + // Finite form. + negative = d.Negative + exponent = d.Exponent + // Return big-endian, but need little-endian. + // Returned []byte is a new and can be safely modified in place. + coefficient = d.Coeff.Bytes() + + 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 (d *Decimal) Compose(form byte, negative bool, coefficient []byte, exponent int32) error { + switch form { + default: + return fmt.Errorf("unknown form: %v", form) + case 0: + d.Form = Finite + // Set rest of finite form below. + case 1: + d.Form = Infinite + d.Negative = negative + return nil + case 2: + d.Form = NaN + d.Negative = negative + return nil + } + // Finite form. + d.Negative = negative + + // coefficient is a little-endian, swap for big-endian. + c2 := make([]byte, len(coefficient)) + copy(c2, coefficient) + coefficient = c2 + i := 0 + j := len(coefficient) - 1 + for i < j { + coefficient[i], coefficient[j] = coefficient[j], coefficient[i] + i++ + j-- + } + d.Coeff.SetBytes(coefficient) + d.Exponent = exponent + return nil +} diff --git a/decomposer_test.go b/decomposer_test.go new file mode 100644 index 0000000..91b7317 --- /dev/null +++ b/decomposer_test.go @@ -0,0 +1,111 @@ +// Copyright 2016 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package apd + +import ( + "fmt" + "testing" +) + +func TestDecomposerRoundTrip(t *testing.T) { + list := []struct { + N string // Name. + S string // String value. + E bool // Expect an error. + }{ + {N: "Zero", S: "0"}, + {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, _, err := NewFromString(item.S) + if err != nil { + t.Fatal(err) + } + set, set2 := &Decimal{}, &Decimal{} + 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{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: "NaN-2", S: "-NaN", Form: 2, Neg: true}, + {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, _, err := NewFromString(item.S) + if err != nil { + t.Fatal(err) + } + 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) + } + }) + } +}