diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b864289..6ff073a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,8 +19,6 @@ jobs: - armv7 - aarch64 go: - - '1.13' - - '1.14' - '1.15' - '1.16' - '1.17' diff --git a/decomposer.go b/decomposer.go index aa67961..ca8724a 100644 --- a/decomposer.go +++ b/decomposer.go @@ -34,9 +34,11 @@ import "fmt" // 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 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. Note that it does not act like + // Append-like functions and does not fill necessarily from the beginning of the + // buffer. Decompose(buf []byte) (form byte, negative bool, coefficient []byte, exponent int32) // Compose sets the internal decimal value from parts. If the value cannot be @@ -48,9 +50,11 @@ type decomposer interface { var _ decomposer = &Decimal{} -// 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 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. Note that it does not act like +// Append-like functions and does not fill necessarily from the beginning of the +// buffer. func (d *Decimal) Decompose(buf []byte) (form byte, negative bool, coefficient []byte, exponent int32) { switch d.Form { default: @@ -69,7 +73,19 @@ func (d *Decimal) Decompose(buf []byte) (form byte, negative bool, coefficient [ // Finite form. negative = d.Negative exponent = d.Exponent - coefficient = d.Coeff.Bytes() + + sizeInBytes := (d.Coeff.BitLen() + 8 - 1) / 8 // math.Ceil(d.Coeff.BitLen()/8.0) + if cap(buf) >= sizeInBytes { + // It extends the buffer as the filling of bytes expects an already + // allocated slice. + buf = buf[:sizeInBytes] + + // We can fit the coefficient in the given buffer which prevents an + // allocation. + coefficient = d.Coeff.FillBytes(buf) + } else { + coefficient = d.Coeff.Bytes() + } return } diff --git a/decomposer_test.go b/decomposer_test.go index 1f52f83..14283aa 100644 --- a/decomposer_test.go +++ b/decomposer_test.go @@ -15,7 +15,11 @@ package apd import ( + "bytes" + "encoding/hex" "fmt" + "math" + "strconv" "testing" ) @@ -109,3 +113,136 @@ func TestDecomposerCompose(t *testing.T) { }) } } + +func TestDecomposerDecompose_usesTheBufferForCoefficientWithSameSize(t *testing.T) { + tests := []struct { + value string + }{ + {"0"}, + {"1"}, + {strconv.FormatUint(math.MaxUint32, 10)}, + {strconv.FormatUint(math.MaxUint64, 10)}, + {"18446744073709551616"}, // math.MaxUint64 + 1 + {"36893488147419103230"}, // math.MaxUint64 * 2 + } + + for _, test := range tests { + t.Run(test.value, func(t *testing.T) { + value, _, err := NewFromString(test.value) + if err != nil { + t.Fatal("unexpected error", err) + } + + buffer := make([]byte, 0, (value.Coeff.BitLen()+8-1)/8) + + _, _, coef, _ := value.Decompose(buffer) + if !bytes.Equal(coef, value.Coeff.Bytes()) { + t.Fatalf("unexpected different coefficients: %s != %s", hex.EncodeToString(coef), hex.EncodeToString(value.Coeff.Bytes())) + } + + var res BigInt + res.SetBytes(coef) + if res != value.Coeff { + t.Fatal("unexpected different results") + } + }) + } +} + +func TestDecomposerDecompose_usesTheBufferForCoefficientWithBiggerSize(t *testing.T) { + tests := []struct { + value string + }{ + {"0"}, + {"1"}, + {strconv.FormatUint(math.MaxUint32, 10)}, + {strconv.FormatUint(math.MaxUint64, 10)}, + {"18446744073709551616"}, // math.MaxUint64 + 1 + {"36893488147419103230"}, // math.MaxUint64 * 2 + } + + for _, test := range tests { + t.Run(test.value, func(t *testing.T) { + value, _, err := NewFromString(test.value) + if err != nil { + t.Fatal("unexpected error", err) + } + + buffer := make([]byte, 0, 64) + + _, _, coef, _ := value.Decompose(buffer) + if !bytes.Equal(coef, value.Coeff.Bytes()) { + t.Fatalf("unexpected different coefficients: %s != %s", hex.EncodeToString(coef), hex.EncodeToString(value.Coeff.Bytes())) + } + + var res BigInt + res.SetBytes(coef) + if res != value.Coeff { + t.Fatal("unexpected different results") + } + }) + } +} + +func TestDecomposerDecompose_ignoresBufferIfItDoesNotFit(t *testing.T) { + value := New(42, 0) + buffer := make([]byte, 0) + + _, _, coef, _ := value.Decompose(buffer) + if !bytes.Equal([]byte{42}, coef) { + t.Fatal("unexpected different buffers", coef) + } + + _, _, coef, _ = value.Decompose(nil) + if !bytes.Equal([]byte{42}, coef) { + t.Fatal("unexpected different buffers with ", coef) + } +} + +func TestDecomposerDecompose_usesBufferWithNonZeroLength(t *testing.T) { + value := New(42, 0) + buffer := make([]byte, 4) + + _, _, coef, _ := value.Decompose(buffer) + if !bytes.Equal([]byte{42}, coef) { + t.Fatal("unexpected different buffers", coef) + } +} + +func TestDecomposerDecompose_usesBufferWithNonZeroCapacity(t *testing.T) { + value := New(42, 0) + buffer := make([]byte, 0, 4) + + _, _, coef, _ := value.Decompose(buffer) + if !bytes.Equal([]byte{42}, coef) { + t.Fatal("unexpected different buffers", coef) + } +} + +func TestDecomposerDecompose_extendsBufferWithNonZeroLength(t *testing.T) { + value := New(math.MaxInt64, 0) + buffer := make([]byte, 2) + + _, _, coef, _ := value.Decompose(buffer) + if !bytes.Equal([]byte{127, 255, 255, 255, 255, 255, 255, 255}, coef) { + t.Fatal("unexpected different buffers", coef) + } +} + +func BenchmarkDecomposerDecompose(b *testing.B) { + b.Run("no allocation", func(b *testing.B) { + buf := make([]byte, 0, 8) + value := New(42, -1) + + for i := 0; i < b.N; i++ { + _, _, _, _ = value.Decompose(buf) + } + }) + b.Run("one allocation", func(b *testing.B) { + value := New(42, -1) + + for i := 0; i < b.N; i++ { + _, _, _, _ = value.Decompose(nil) + } + }) +}