Repository: amzn/ion-go
Pull request #92 (2020-07-03 18:16:46)

Using go-cmp for equality check on timestamp, floats and decimal
To get a more accurate and custom way of checking equality between Ion values, we are going to use ```go-cmp``` package.

The implementation is limited to Decimal, Float and Timestamp, as we need custom equivalency. Other types are using ```reflect.DeepEqual()```.

-----
There are some discussions in [this draft pull request](https://github.com/Armanbqt/ion-go/pull/9) on my fork; adding it here to keep the history.



Patch:
diff --git a/go.mod b/go.mod
index c52f576..8e11425 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,5 @@
 module github.com/amzn/ion-go
 
 go 1.13
+
+require github.com/google/go-cmp v0.5.0
diff --git a/go.sum b/go.sum
index 426612c..f104aca 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,22 @@
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114 h1:Pm6R878vxWWWR+Sa3ppsLce/Zq+JNTs6aVvRu13jv9A=
 github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=


Test patch:
diff --git a/ion/cmp_test.go b/ion/cmp_test.go
new file mode 100644
index 0000000..55e242d
--- /dev/null
+++ b/ion/cmp_test.go
@@ -0,0 +1,205 @@
+package ion
+
+import (
+	"reflect"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+)
+
+type ionEqual interface {
+	eq(other ionEqual) bool
+}
+
+type ionFloat struct{ float64 }
+type ionDecimal struct{ *Decimal }
+type ionTimestamp struct{ time.Time }
+
+func (thisFloat ionFloat) eq(other ionEqual) bool {
+	return cmp.Equal(thisFloat.float64, other.(ionFloat).float64, cmpopts.EquateNaNs())
+}
+
+func (thisDecimal ionDecimal) eq(other ionEqual) bool {
+	if val, ok := other.(ionDecimal); ok {
+		if thisDecimal.scale != val.scale {
+			return false
+		}
+		return thisDecimal.Decimal.Equal(val.Decimal)
+	}
+	return false
+}
+
+func (thisTimestamp ionTimestamp) eq(other ionEqual) bool {
+	if val, ok := other.(ionTimestamp); ok {
+		return thisTimestamp.Time.Equal(val.Time)
+	}
+	return false
+}
+
+func cmpAnnotations(thisAnnotations, otherAnnotations []string) bool {
+	return reflect.DeepEqual(thisAnnotations, otherAnnotations)
+}
+
+func cmpFloats(thisValue, otherValue interface{}) bool {
+	if !haveSameTypes(thisValue, otherValue) {
+		return false
+	}
+
+	switch val := thisValue.(type) {
+	case string: // null.float
+		return strNullTypeCmp(val, otherValue)
+	case float64:
+		thisFloat := ionFloat{val}
+		return thisFloat.eq(ionFloat{otherValue.(float64)})
+	default:
+		return false
+	}
+}
+
+func cmpDecimals(thisValue, otherValue interface{}) bool {
+	if !haveSameTypes(thisValue, otherValue) {
+		return false
+	}
+
+	switch val := thisValue.(type) {
+	case string: // null.decimal
+		return strNullTypeCmp(val, otherValue)
+	case *Decimal:
+		thisDecimal := ionDecimal{val}
+		return thisDecimal.eq(ionDecimal{otherValue.(*Decimal)})
+	default:
+		return false
+	}
+}
+
+func cmpTimestamps(thisValue, otherValue interface{}) bool {
+	if !haveSameTypes(thisValue, otherValue) {
+		return false
+	}
+
+	switch val := thisValue.(type) {
+	case string: // null.timestamp
+		return strNullTypeCmp(val, otherValue)
+	case time.Time:
+		thisTimestamp := ionTimestamp{val}
+		return thisTimestamp.eq(ionTimestamp{otherValue.(time.Time)})
+	default:
+		return false
+	}
+}
+
+func cmpValueSlices(thisValues, otherValues []interface{}) bool {
+	if len(thisValues) == 0 && len(otherValues) == 0 {
+		return true
+	}
+
+	if len(thisValues) != len(otherValues) {
+		return false
+	}
+
+	res := false
+	for idx, this := range thisValues {
+		other := otherValues[idx]
+
+		if !haveSameTypes(this, other) {
+			return false
+		}
+
+		thisItem := getContainersType(this)
+		otherItem := getContainersType(other)
+		res = containersEquality(thisItem, otherItem)
+
+		if !res {
+			return false
+		}
+	}
+	return res
+}
+
+func cmpStruct(thisValues, otherValues []interface{}) bool {
+	if len(thisValues) == 0 && len(otherValues) == 0 {
+		return true
+	}
+
+	if len(thisValues) != len(otherValues) {
+		return false
+	}
+
+	var res bool
+	var checked []int
+	for _, this := range thisValues {
+		res = false
+		var thisItem = getContainersType(this)
+		for i := 0; i < len(otherValues); i++ {
+			if contains(checked, i) {
+				continue
+			}
+			if !haveSameTypes(this, otherValues[i]) {
+				continue
+			} else {
+				otherItem := getContainersType(otherValues[i])
+				res = containersEquality(thisItem, otherItem)
+				if res {
+					if !contains(checked, i) {
+						checked = append(checked, i)
+					}
+					break
+				}
+			}
+		}
+	}
+	if len(otherValues) != len(checked) {
+		return false
+	}
+
+	return res
+}
+
+func strNullTypeCmp(this, other interface{}) bool {
+	thisStr, thisOk := this.(string)
+	otherStr, otherOk := other.(string)
+	if thisOk && otherOk {
+		return cmp.Equal(thisStr, otherStr)
+	}
+	return false
+}
+
+func haveSameTypes(this, other interface{}) bool {
+	return reflect.TypeOf(this) == reflect.TypeOf(other)
+}
+
+func getContainersType(in interface{}) interface{} {
+	switch in.(type) {
+	case string:
+		return in.(string)
+	default:
+		return in.(ionItem)
+	}
+}
+
+func contains(list []int, idx int) bool {
+	for _, num := range list {
+		if num == idx {
+			return true
+		}
+	}
+	return false
+}
+
+// non-null containers have ionItems inside them
+func containersEquality(this, other interface{}) bool {
+	switch this.(type) {
+	case string: // null.list, null.sexp, null.struct
+		if strNullTypeCmp(this, other) {
+			return true
+		}
+	default:
+		otherItem := other.(ionItem)
+		thisItem := this.(ionItem)
+		if thisItem.fieldName == otherItem.fieldName && thisItem.equal(otherItem) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/ion/integration_test.go b/ion/integration_test.go
index fabbdc9..4bdc9b8 100644
--- a/ion/integration_test.go
+++ b/ion/integration_test.go
@@ -41,10 +41,27 @@ type ionItem struct {
 }
 
 func (i *ionItem) equal(o ionItem) bool {
-	return reflect.DeepEqual(i.value, o.value) &&
-		reflect.DeepEqual(i.annotations, o.annotations) &&
-		reflect.DeepEqual(i.ionType, o.ionType) &&
-		reflect.DeepEqual(i.fieldName, o.fieldName)
+	if i.ionType != o.ionType {
+		return false
+	}
+	if !cmpAnnotations(i.annotations, o.annotations) {
+		return false
+	}
+
+	switch i.ionType {
+	case FloatType:
+		return cmpFloats(i.value[0], o.value[0])
+	case DecimalType:
+		return cmpDecimals(i.value[0], o.value[0])
+	case TimestampType:
+		return cmpTimestamps(i.value[0], o.value[0])
+	case ListType, SexpType:
+		return cmpValueSlices(i.value, o.value)
+	case StructType:
+		return cmpStruct(i.value, o.value)
+	default:
+		return reflect.DeepEqual(i.value, o.value)
+	}
 }
 
 var readGoodFilesSkipList = []string{
@@ -191,8 +208,6 @@ var equivsSkipList = []string{
 	"nonIVMNoOps.ion",
 	"sexps.ion",
 	"stringUtf8.ion", // fails on utf-16 surrogate https://github.com/amzn/ion-go/issues/75
-	"structsFieldsDiffOrder.ion",
-	"structsFieldsRepeatedNames.ion",
 	"systemSymbols.ion",
 	"systemSymbolsAsAnnotations.ion",
 	"timestampSuperfluousOffset.10n",
@@ -206,7 +221,6 @@ var nonEquivsSkipList = []string{
 	"floats.ion",
 	"floatsVsDecimals.ion",
 	"localSymbolTableWithAnnotations.ion",
-	"structs.ion",
 	"symbolTables.ion",
 	"symbolTablesUnknownText.ion",
 	"symbols.ion",