Repository: amzn/ion-go
Pull request #101 (2020-07-23 00:17:04)

Float32 Lossless Support
According to the Ion specification: http://amzn.github.io/ion-docs/docs/float.html, it's always valid to store a float as an float64, but if it fits in an float32 then that's an option too. This change checks to see if a float can be losslessly represented as a float32 and does so.

Originally this was part of the Marshal tests, but this fix can be its own PR.




Patch:
diff --git a/ion/binarywriter.go b/ion/binarywriter.go
index 2f40d88..e0eea59 100644
--- a/ion/binarywriter.go
+++ b/ion/binarywriter.go
@@ -159,6 +159,17 @@ func (w *binaryWriter) WriteFloat(val float64) error {
 		return w.writeValue("Writer.WriteFloat", []byte{0x40})
 	}
 
+	// Can this be losslessly represented as a float32?
+	if val == float64(float32(val)) {
+		bs := make([]byte, 5)
+		bs[0] = 0x44
+
+		bits := math.Float32bits(float32(val))
+		binary.BigEndian.PutUint32(bs[1:], bits)
+
+		return w.writeValue("Writer.WriteFloat", bs)
+	}
+
 	bs := make([]byte, 9)
 	bs[0] = 0x48
 


Test patch:
diff --git a/ion/binarywriter_test.go b/ion/binarywriter_test.go
index c36c293..656fe7d 100644
--- a/ion/binarywriter_test.go
+++ b/ion/binarywriter_test.go
@@ -200,10 +200,11 @@ func TestWriteBinaryFloats(t *testing.T) {
 		0x40,                                                 // 0
 		0x48, 0x7F, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // MaxFloat64
 		0x48, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // -MaxFloat64
-		0x48, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +inf
-		0x48, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // -inf
+		0x44, 0x7F, 0x80, 0x00, 0x00, // +inf (float32)
+		0x44, 0xFF, 0x80, 0x00, 0x00, // -inf (float32)
 		0x48, 0x7F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // NaN
 	}
+
 	testBinaryWriter(t, eval, func(w Writer) {
 		w.WriteFloat(0)
 		w.WriteFloat(math.MaxFloat64)
diff --git a/ion/marshal_test.go b/ion/marshal_test.go
index 5e64c19..89fcfd6 100644
--- a/ion/marshal_test.go
+++ b/ion/marshal_test.go
@@ -80,14 +80,31 @@ func TestMarshalBinary(t *testing.T) {
 		})
 	}
 
-	test(nil, "null", []byte{0xE0, 0x01, 0x00, 0xEA, 0x0F})
-	test(struct{ A, B int }{42, 0}, "{A:42,B:0}", []byte{
-		0xE0, 0x01, 0x00, 0xEA,
+	test(nil, "null", prefixIVM([]byte{0x0F}))
+
+	// Float32 valid type. Go treats floats as float64 by default, unless specified.
+	// Explicitly cast number to be of float32 and ensure type is handled. This should not be an unknown type.
+	test(float32(math.MaxFloat32), "float32 valid type", prefixIVM([]byte{0x44, 0x7F, 0x7F, 0xFF, 0xFF})) // 3.40282346638528859811704183484516925440e+38
+
+	// Float32. Ensure number can be represented losslessly as a float32 by testing that byte length is 5.
+	// This should not be represented as a float64.
+	test(math.MaxFloat32, "float32", prefixIVM([]byte{0x44, 0x7F, 0x7F, 0xFF, 0xFF})) // 3.40282346638528859811704183484516925440e+38
+
+	// Float 64.
+	test(math.MaxFloat64, "float64", prefixIVM([]byte{0x48, 0x7F, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})) // 1.797693134862315708145274237317043567981e+308
+
+	// Struct.
+	test(struct{ A, B int }{42, 0}, "{A:42,B:0}", prefixIVM([]byte{
 		0xE9, 0x81, 0x83, 0xD6, 0x87, 0xB4, 0x81, 'A', 0x81, 'B',
 		0xD5,
 		0x8A, 0x21, 0x2A,
 		0x8B, 0x20,
-	})
+	}))
+}
+
+func prefixIVM(data []byte) []byte {
+	prefix := []byte{0xE0, 0x01, 0x00, 0xEA} // $ion_1_0
+	return append(prefix, data...)
 }
 
 func TestMarshalBinaryLST(t *testing.T) {