Repository: amzn/ion-go
Pull request #156 (2020-09-11 20:26:22)

Handle Negative Zero Decimal
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.


Patch:
diff --git a/ion/binarywriter.go b/ion/binarywriter.go
index 94f9930..900e767 100644
--- a/ion/binarywriter.go
+++ b/ion/binarywriter.go
@@ -198,9 +198,9 @@ func (w *binaryWriter) WriteFloat(val float64) error {
 func (w *binaryWriter) WriteDecimal(val *Decimal) error {
 	coef, exp := val.CoEx()
 
-	// If the value is 0. (aka 0d0) then L is zero, there are no length or
+	// If the value is positive 0. (aka 0d0) then L is zero, there are no length or
 	// representation fields, and the entire value is encoded as the single byte 0x50.
-	if coef.Sign() == 0 && int64(exp) == 0 {
+	if coef.Sign() == 0 && int64(exp) == 0 && !val.isNegZero {
 		buf := make([]byte, 0, 0)
 		buf = appendTag(buf, 0x50, 0)
 
@@ -209,14 +209,24 @@ func (w *binaryWriter) WriteDecimal(val *Decimal) error {
 
 	// Otherwise, length or representation fields are present and must be considered.
 	vlength := varIntLen(int64(exp))
-	vlength += bigIntLen(coef)
+
+	if val.isNegZero {
+		vlength++
+	} else {
+		vlength += bigIntLen(coef)
+	}
 
 	bufLength := vlength + tagLen(vlength)
 	buf := make([]byte, 0, bufLength)
 
 	buf = appendTag(buf, 0x50, vlength)
 	buf = appendVarInt(buf, int64(exp))
-	buf = appendBigInt(buf, coef)
+
+	if val.isNegZero {
+		buf = append(buf, 0x80)
+	} else {
+		buf = appendBigInt(buf, coef)
+	}
 
 	return w.writeValue("Writer.WriteDecimal", buf)
 }
diff --git a/ion/bitstream.go b/ion/bitstream.go
index 337ddb5..4408f6a 100644
--- a/ion/bitstream.go
+++ b/ion/bitstream.go
@@ -725,6 +725,7 @@ func (b *bitstream) readNsecs(length uint64) (int, bool, error) {
 func (b *bitstream) readDecimal(length uint64) (*Decimal, error) {
 	exp := int64(0)
 	coef := new(big.Int)
+	negZero := false
 
 	if length > 0 {
 		val, _, vlength, err := b.readVarIntLen(length)
@@ -745,9 +746,11 @@ func (b *bitstream) readDecimal(length uint64) (*Decimal, error) {
 		if err := b.readBigInt(length, coef); err != nil {
 			return nil, err
 		}
+
+		negZero = coef.Sign() == 0
 	}
 
-	return NewDecimal(coef, int32(exp)), nil
+	return NewDecimal(coef, int32(exp), negZero), nil
 }
 
 // ReadSymbolID reads a symbol value.
diff --git a/ion/decimal.go b/ion/decimal.go
index a7bd917..703774a 100644
--- a/ion/decimal.go
+++ b/ion/decimal.go
@@ -38,21 +38,23 @@ func (e *ParseError) Error() string {
 
 // Decimal is an arbitrary-precision decimal value.
 type Decimal struct {
-	n     *big.Int
-	scale int32
+	n         *big.Int
+	scale     int32
+	isNegZero bool
 }
 
 // NewDecimal creates a new decimal whose value is equal to n * 10^exp.
-func NewDecimal(n *big.Int, exp int32) *Decimal {
+func NewDecimal(n *big.Int, exp int32, negZero bool) *Decimal {
 	return &Decimal{
-		n:     n,
-		scale: -exp,
+		n:         n,
+		scale:     -exp,
+		isNegZero: negZero,
 	}
 }
 
 // NewDecimalInt creates a new decimal whose value is equal to n.
 func NewDecimalInt(n int64) *Decimal {
-	return NewDecimal(big.NewInt(n), 0)
+	return NewDecimal(big.NewInt(n), 0, false)
 }
 
 // MustParseDecimal parses the given string into a decimal object,
@@ -107,7 +109,9 @@ func ParseDecimal(in string) (*Decimal, error) {
 		return nil, &ParseError{in, "cannot parse coefficient"}
 	}
 
-	return NewDecimal(n, exponent), nil
+	isNegZero := n.Sign() == 0 && len(in) > 0 && in[0] == '-'
+
+	return NewDecimal(n, exponent, isNegZero), nil
 }
 
 // CoEx returns this decimal's coefficient and exponent.
@@ -340,19 +344,31 @@ func (d *Decimal) String() string {
 	switch {
 	case d.scale == 0:
 		// Value is an unscaled integer. Just mark it as a decimal.
+		if d.isNegZero {
+			return "-0."
+		}
 		return d.n.String() + "."
 
 	case d.scale < 0:
 		// Value is a upscaled integer, nn'd'ss
+		if d.isNegZero {
+			return "-0d" + fmt.Sprintf("%d", -d.scale)
+		}
 		return d.n.String() + "d" + fmt.Sprintf("%d", -d.scale)
 
 	default:
 		// Value is a downscaled integer nn.nn('d'-ss)?
-		str := d.n.String()
+		var str string
+		if d.isNegZero {
+			str = "-0"
+		} else {
+			str = d.n.String()
+		}
+
 		idx := len(str) - int(d.scale)
 
 		prefix := 1
-		if d.n.Sign() < 0 {
+		if len(str) > 0 && str[0] == '-' {
 			// Account for leading '-'.
 			prefix++
 		}


Test patch:
diff --git a/ion/cmp_test.go b/ion/cmp_test.go
index 3934c4b..1f7375b 100644
--- a/ion/cmp_test.go
+++ b/ion/cmp_test.go
@@ -36,7 +36,7 @@ func (thisFloat ionFloat) eq(other ionEqual) bool {
 
 func (thisDecimal ionDecimal) eq(other ionEqual) bool {
 	if val, ok := other.(ionDecimal); ok {
-		if thisDecimal.scale != val.scale {
+		if thisDecimal.scale != val.scale || thisDecimal.isNegZero != val.isNegZero {
 			return false
 		}
 		return thisDecimal.Decimal.Equal(val.Decimal)
diff --git a/ion/integration_test.go b/ion/integration_test.go
index 3f10d73..2c42188 100644
--- a/ion/integration_test.go
+++ b/ion/integration_test.go
@@ -125,7 +125,6 @@ var equivsSkipList = []string{
 }
 
 var nonEquivsSkipList = []string{
-	"decimals.ion",
 	"floats.ion",
 	"floatsVsDecimals.ion",
 }