diff --git a/circle_test.go b/circle_test.go index 4d1cc0ff..51a16a5c 100644 --- a/circle_test.go +++ b/circle_test.go @@ -7,7 +7,8 @@ import ( "github.com/go-spatial/geom/cmp" ) -const tolerance = geom.TOLERANCE +const tolerance = geom.Tolerance +const bitTolerance = geom.BitTolerance func TestCircleFromPoints(t *testing.T) { type tcase struct { @@ -28,17 +29,17 @@ func TestCircleFromPoints(t *testing.T) { return } - if !cmp.Float64(circle.Radius, tc.circle.Radius, tolerance) { + if !cmp.Float64(circle.Radius, tc.circle.Radius, tolerance, bitTolerance) { t.Errorf("circle radius, expected %v got %v", tc.circle, circle) return } - if !cmp.Float64(circle.Center[0], tc.circle.Center[0], tolerance) { + if !cmp.Float64(circle.Center[0], tc.circle.Center[0], tolerance, bitTolerance) { t.Errorf("circle x, expected %v got %v", tc.circle, circle) return } - if !cmp.Float64(circle.Center[1], tc.circle.Center[1], tolerance) { + if !cmp.Float64(circle.Center[1], tc.circle.Center[1], tolerance, bitTolerance) { t.Errorf("circle y, expected %v got %v", tc.circle, circle) return } diff --git a/cmp/cmp.go b/cmp/cmp.go index 13af12e1..ab2f72fb 100644 --- a/cmp/cmp.go +++ b/cmp/cmp.go @@ -7,8 +7,12 @@ import ( "github.com/go-spatial/geom" ) -// TOLERANCE is the epsilon value used in comparing floats. -const TOLERANCE = 0.000001 +// Tolerance is the epsilon value used in comparing floats with zero +const Tolerance = 0.000001 + +// BitTolerance is the epsilon value for comaparing float bit-patterns. +// It was calculated as math.Float64bits(1.000001) - math.Float64bits(1.0) +const BitTolerance = 4503599627 var ( NilPoint = (*geom.Point)(nil) @@ -21,10 +25,10 @@ var ( ) // FloatSlice compare two sets of float slices. -func FloatSlice(f1, f2 []float64) bool { return Float64Slice(f1, f2, TOLERANCE) } +func FloatSlice(f1, f2 []float64) bool { return Float64Slice(f1, f2, Tolerance, BitTolerance) } // Float64Slice compares two sets of float64 slices within the given tolerance. -func Float64Slice(f1, f2 []float64, tolerance float64) bool { +func Float64Slice(f1, f2 []float64, tolerance float64, bitTolerance int64) bool { if len(f1) != len(f2) { return false } @@ -38,7 +42,7 @@ func Float64Slice(f1, f2 []float64, tolerance float64) bool { sort.Float64s(f1s) sort.Float64s(f2s) for i := range f1s { - if !Float64(f1s[i], f2s[i], tolerance) { + if !Float64(f1s[i], f2s[i], tolerance, bitTolerance) { return false } } @@ -46,24 +50,31 @@ func Float64Slice(f1, f2 []float64, tolerance float64) bool { } // Float64 compares two floats to see if they are within the given tolerance. -func Float64(f1, f2, tolerance float64) bool { - if math.IsInf(f1, 1) { - return math.IsInf(f2, 1) - } - if math.IsInf(f2, 1) { - return math.IsInf(f1, 1) +func Float64(f1, f2, tolerance float64, bitTolerance int64) bool { + + // handle infinity + if math.IsInf(f1, 0) || math.IsInf(f2, 0) { + return math.IsInf(f1, -1) == math.IsInf(f2, -1) && + math.IsInf(f1, 1) == math.IsInf(f2, 1) } - if math.IsInf(f1, -1) { - return math.IsInf(f2, -1) + + // -0.0 exist but -0.0 == 0.0 is true + if f1 == 0 || f2 == 0 { + return math.Abs(f2 - f1) < tolerance } - if math.IsInf(f2, -1) { - return math.IsInf(f1, -1) + + i1 := int64(math.Float64bits(f1)) + i2 := int64(math.Float64bits(f2)) + d := i2 - i1 + + if d < 0 { + return d > -bitTolerance } - return math.Abs(f1-f2) < tolerance + return d < bitTolerance } // Float compares two floats to see if they are within 0.00001 from each other. This is the best way to compare floats. -func Float(f1, f2 float64) bool { return Float64(f1, f2, TOLERANCE) } +func Float(f1, f2 float64) bool { return Float64(f1, f2, Tolerance, BitTolerance) } // Extent will check to see if the Extents's are the same. func Extent(extent1, extent2 [4]float64) bool { diff --git a/cmp/cmp_test.go b/cmp/cmp_test.go index de7fe7e1..459b07ca 100644 --- a/cmp/cmp_test.go +++ b/cmp/cmp_test.go @@ -5,7 +5,8 @@ import ( "math" "testing" - geom "github.com/go-spatial/geom" + "github.com/go-spatial/geom" + gtesting "github.com/go-spatial/geom/testing" ) /* @@ -917,27 +918,65 @@ func TestFloat64(t *testing.T) { type tcase struct { f1, f2 float64 t float64 + b int64 e bool } + + // bit tolerence for 2 significant digits + bitTolerance2 := int64(math.Float64bits(1.1) - math.Float64bits(1)) + // float representation of negative zero + negativeZero := math.Float64frombits(1 << 63) + fn := func(t *testing.T, tc tcase) { - g := Float64(tc.f1, tc.f2, tc.t) + g := Float64(tc.f1, tc.f2, tc.t, tc.b) if g != tc.e { t.Errorf(" Float64, expected %v, got %v", tc.e, g) } } + tests := map[string]tcase{ - "t simple .01 ": { + "t simple .01": { f1: 0.11, f2: 0.111, t: 0.01, + b: bitTolerance2, e: true, }, "f simple .01": { f1: 0.11, f2: 0.121, t: 0.01, + b: bitTolerance2, e: false, }, + "t 0 .01": { + f1: 0.0, + f2: 0.001, + t: 0.01, + b: bitTolerance2, + e: true, + }, + "f 0 .01": { + f1: 0.0, + f2: 0.02, + t: 0.01, + b: bitTolerance2, + e: false, + }, + "t 0 0": { + f1: 0.0, + f2: 0.0, + t: 0.01, + b: bitTolerance2, + e: true, + }, + "t 0 -0": { + f1: 0.0, + f2: negativeZero, + t: 0.01, + b: bitTolerance2, + e: true, + }, "t inf 1 0": { f1: math.Inf(1), f2: math.Inf(1), @@ -984,3 +1023,23 @@ func TestFloat64(t *testing.T) { t.Run(name, func(t *testing.T) { fn(t, tc) }) } } + +func BenchmarkCmpFloat(b *testing.B) { + + sin := gtesting.SinLineString(10, 0, 100, 1000) + var result bool + + for i := 0; i < b.N; i++ { + for _, v := range sin { + for _, vv := range sin { + // xor + result = result != Float(v[0], vv[0]) + } + } + } + + // use the result + if result { + print("") + } +} diff --git a/geom.go b/geom.go index 2793485a..f67e61ce 100644 --- a/geom.go +++ b/geom.go @@ -1,7 +1,8 @@ // Package geom describes geometry interfaces. package geom -const TOLERANCE = 0.000001 +const Tolerance = 0.000001 +const BitTolerance = 4503599627 // Geometry is an object with a spatial reference. // if a method accepts a Geometry type it's only expected to support the geom types in this package diff --git a/slippy/tile_test.go b/slippy/tile_test.go index 1dbedf5c..df51bf6a 100644 --- a/slippy/tile_test.go +++ b/slippy/tile_test.go @@ -1,6 +1,7 @@ package slippy_test import ( + "math" "strconv" "testing" @@ -41,8 +42,9 @@ func TestNewTile(t *testing.T) { } { bounds := tile.Extent4326() + bitTolerence2 := int64(math.Float64bits(1.01) - math.Float64bits(1.00)) for i := 0; i < 4; i++ { - if !cmp.Float64(bounds[i], tc.eBounds[i], 0.01) { + if !cmp.Float64(bounds[i], tc.eBounds[i], 0.01, bitTolerence2) { t.Errorf("bounds[%v] , expected %v got %v", i, tc.eBounds[i], bounds[i]) }