Repository: googleapis/google-cloud-go
Pull request #2618 (2020-07-20 20:57:10)

bigqueryNullFloat64 add +Inf, -Inf and NaN support
This is a new PR for closed PR https://github.com/googleapis/google-cloud-go/pull/2574 because you can only allow edits for maintainers on user-forks and NOT organization forks.


Patch:
diff --git a/bigquery/nulls.go b/bigquery/nulls.go
index bf3cb9d650..6ef53d13fc 100644
--- a/bigquery/nulls.go
+++ b/bigquery/nulls.go
@@ -18,6 +18,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"math"
 	"reflect"
 	"strconv"
 	"time"
@@ -25,6 +26,16 @@ import (
 	"cloud.google.com/go/civil"
 )
 
+var (
+	jsonNull      = []byte("null")
+	posInf        = []byte(`"+Inf"`)
+	inf           = []byte(`"Inf"`)
+	minusInf      = []byte(`"-Inf"`)
+	infinity      = []byte(`"Infinity"`)
+	minusInfinity = []byte(`"-Infinity"`)
+	nan           = []byte(`"NaN"`)
+)
+
 // NullInt64 represents a BigQuery INT64 that may be NULL.
 type NullInt64 struct {
 	Int64 int64
@@ -111,7 +122,21 @@ func (n NullDateTime) String() string {
 func (n NullInt64) MarshalJSON() ([]byte, error) { return nulljson(n.Valid, n.Int64) }
 
 // MarshalJSON converts the NullFloat64 to JSON.
-func (n NullFloat64) MarshalJSON() ([]byte, error) { return nulljson(n.Valid, n.Float64) }
+func (n NullFloat64) MarshalJSON() (b []byte, err error) {
+	if n.Valid {
+		switch {
+		case math.IsInf(n.Float64, 1):
+			return infinity, nil
+		case math.IsInf(n.Float64, -1):
+			return minusInfinity, nil
+		case math.IsNaN(n.Float64):
+			return nan, nil
+		default:
+			return json.Marshal(n.Float64)
+		}
+	}
+	return jsonNull, nil
+}
 
 // MarshalJSON converts the NullBool to JSON.
 func (n NullBool) MarshalJSON() ([]byte, error) { return nulljson(n.Valid, n.Bool) }
@@ -151,8 +176,6 @@ func nullstr(valid bool, v interface{}) string {
 	return fmt.Sprint(v)
 }
 
-var jsonNull = []byte("null")
-
 func nulljson(valid bool, v interface{}) ([]byte, error) {
 	if !valid {
 		return jsonNull, nil
@@ -181,8 +204,19 @@ func (n *NullFloat64) UnmarshalJSON(b []byte) error {
 	n.Float64 = 0
 	if bytes.Equal(b, jsonNull) {
 		return nil
+	} else if bytes.Equal(b, posInf) || bytes.Equal(b, inf) || bytes.Equal(b, infinity) {
+		n.Float64 = math.Inf(1)
+		n.Valid = true
+		return nil
+	} else if bytes.Equal(b, minusInf) || bytes.Equal(b, minusInfinity) {
+		n.Float64 = math.Inf(-1)
+		n.Valid = true
+		return nil
+	} else if bytes.Equal(b, nan) {
+		n.Float64 = math.NaN()
+		n.Valid = true
+		return nil
 	}
-
 	if err := json.Unmarshal(b, &n.Float64); err != nil {
 		return err
 	}


Test patch:
diff --git a/bigquery/nulls_test.go b/bigquery/nulls_test.go
index 92a0b9ce71..7e5c939a14 100644
--- a/bigquery/nulls_test.go
+++ b/bigquery/nulls_test.go
@@ -16,6 +16,7 @@ package bigquery
 
 import (
 	"encoding/json"
+	"math"
 	"reflect"
 	"testing"
 
@@ -52,6 +53,10 @@ func TestNullsJSON(t *testing.T) {
 		{&NullDate{}, `null`},
 		{&NullTime{}, `null`},
 		{&NullDateTime{}, `null`},
+
+		{&NullFloat64{Valid: true, Float64: math.Inf(1)}, `"Infinity"`},
+		{&NullFloat64{Valid: true, Float64: math.Inf(-1)}, `"-Infinity"`},
+		{&NullFloat64{Valid: true, Float64: math.NaN()}, `"NaN"`},
 	} {
 		bytes, err := json.Marshal(test.in)
 		if err != nil {
@@ -73,3 +78,83 @@ func TestNullsJSON(t *testing.T) {
 		}
 	}
 }
+
+func TestNullFloat64JSON(t *testing.T) {
+	for _, tc := range []struct {
+		name         string
+		in           string
+		unmarshalled NullFloat64
+		marshalled   string
+	}{
+		{
+			name:         "float value",
+			in:           "3.14",
+			unmarshalled: NullFloat64{Valid: true, Float64: 3.14},
+			marshalled:   "3.14",
+		},
+		{
+			name:         "null",
+			in:           "null",
+			unmarshalled: NullFloat64{},
+			marshalled:   "null",
+		},
+		{
+			name:         "long infinity",
+			in:           `"Infinity"`,
+			unmarshalled: NullFloat64{Valid: true, Float64: math.Inf(1)},
+			marshalled:   `"Infinity"`,
+		},
+		{
+			name:         "short infinity",
+			in:           `"Inf"`,
+			unmarshalled: NullFloat64{Valid: true, Float64: math.Inf(1)},
+			marshalled:   `"Infinity"`,
+		},
+		{
+			name:         "positive short infinity",
+			in:           `"+Inf"`,
+			unmarshalled: NullFloat64{Valid: true, Float64: math.Inf(1)},
+			marshalled:   `"Infinity"`,
+		},
+		{
+			name:         "minus infinity",
+			in:           `"-Infinity"`,
+			unmarshalled: NullFloat64{Valid: true, Float64: math.Inf(-1)},
+			marshalled:   `"-Infinity"`,
+		},
+		{
+			name:         "minus short infinity",
+			in:           `"-Inf"`,
+			unmarshalled: NullFloat64{Valid: true, Float64: math.Inf(-1)},
+			marshalled:   `"-Infinity"`,
+		},
+		{
+			name:         "NaN",
+			in:           `"NaN"`,
+			unmarshalled: NullFloat64{Valid: true, Float64: math.NaN()},
+			marshalled:   `"NaN"`,
+		},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			var f NullFloat64
+			err := json.Unmarshal([]byte(tc.in), &f)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if got, want := f, tc.unmarshalled; !testutil.Equal(got, want) {
+				t.Errorf("%#v: got %#v, want %#v", tc.in, got, want)
+			}
+
+			b, err := json.Marshal(f)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if got, want := string(b), tc.marshalled; got != want {
+				t.Errorf("%#v: got %s, want %s", tc.in, got, want)
+			}
+		})
+	}
+}