Skip to content

Commit ebc6246

Browse files
authored
Chore/linting issues (#132)
* improve equality comparison for floats Signed-off-by: Pavel Afanasev <afansvme@yandex.ru> * IsFloat64JSONInteger check * fixed short-circuit case * added benchmark and verified that the new version is about 14% faster * experimental code to compare rounding methods Signed-off-by: Frédéric BIDON <fredbi@yahoo.com>
1 parent 44bc4fa commit ebc6246

File tree

2 files changed

+309
-26
lines changed

2 files changed

+309
-26
lines changed

conv/convert.go

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,21 @@ func IsFloat64AJSONInteger(f float64) bool {
3232
if math.IsNaN(f) || math.IsInf(f, 0) || f < minJSONFloat || f > maxJSONFloat {
3333
return false
3434
}
35-
fa := math.Abs(f)
36-
g := float64(uint64(f))
37-
ga := math.Abs(g)
38-
39-
diff := math.Abs(f - g)
40-
41-
// more info: https://floating-point-gui.de/errors/comparison/#look-out-for-edge-cases
42-
switch {
43-
case f == g: // best case
35+
rounded := math.Round(f)
36+
if f == rounded {
4437
return true
45-
case f == float64(int64(f)) || f == float64(uint64(f)): // optimistic case
38+
}
39+
if rounded == 0 { // f = 0.0 exited above
40+
return false
41+
}
42+
43+
diff := math.Abs(f - rounded)
44+
if diff == 0 {
4645
return true
47-
case f == 0 || g == 0 || diff < math.SmallestNonzeroFloat64: // very close to 0 values
48-
return diff < (epsilon * math.SmallestNonzeroFloat64)
4946
}
50-
// check the relative error
51-
return diff/math.Min(fa+ga, math.MaxFloat64) < epsilon
47+
48+
// relative error Abs{f - Round(f)) / Round(f)} < ε ; Round(f)
49+
return diff < epsilon*math.Abs(rounded)
5250
}
5351

5452
// ConvertFloat turns a string into a float numerical value.

conv/convert_format_test.go

Lines changed: 297 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"io"
2020
"math"
2121
"math/big"
22+
"math/bits"
2223
"slices"
2324
"strconv"
2425
"strings"
@@ -305,18 +306,302 @@ func TestConvertUinteger(t *testing.T) {
305306
}
306307

307308
func TestIsFloat64AJSONInteger(t *testing.T) {
308-
assert.False(t, IsFloat64AJSONInteger(math.Inf(1)))
309-
assert.False(t, IsFloat64AJSONInteger(maxJSONFloat+1))
310-
assert.False(t, IsFloat64AJSONInteger(minJSONFloat-1))
311-
assert.False(t, IsFloat64AJSONInteger(math.SmallestNonzeroFloat64))
312-
313-
assert.True(t, IsFloat64AJSONInteger(1.0))
314-
assert.True(t, IsFloat64AJSONInteger(maxJSONFloat))
315-
assert.True(t, IsFloat64AJSONInteger(minJSONFloat))
316-
assert.True(t, IsFloat64AJSONInteger(1/0.01*67.15000001))
317-
assert.True(t, IsFloat64AJSONInteger(math.SmallestNonzeroFloat64/2))
318-
assert.True(t, IsFloat64AJSONInteger(math.SmallestNonzeroFloat64/3))
319-
assert.True(t, IsFloat64AJSONInteger(math.SmallestNonzeroFloat64/4))
309+
t.Run("should not be integers", testNotIntegers(IsFloat64AJSONInteger, false))
310+
t.Run("should be integers", testIntegers(IsFloat64AJSONInteger, false))
311+
}
312+
313+
func TestPreviousIsFloat64AJSONInteger(t *testing.T) {
314+
t.Run("should not be integers", testNotIntegers(previousIsFloat64JSONInteger, false))
315+
t.Run("should be integers", testIntegers(previousIsFloat64JSONInteger, true))
316+
}
317+
318+
func TestBitWiseIsFloat64AJSONInteger(t *testing.T) {
319+
t.Run("should not be integers", testNotIntegers(bitwiseIsFloat64JSONInteger, false))
320+
t.Run("should be integers", testIntegers(bitwiseIsFloat64JSONInteger, false))
321+
}
322+
323+
func TestBitWise2IsFloat64AJSONInteger(t *testing.T) {
324+
t.Run("should not be integers", testNotIntegers(bitwiseIsFloat64JSONInteger2, false))
325+
t.Run("should be integers", testIntegers(bitwiseIsFloat64JSONInteger2, false))
326+
}
327+
328+
func TestStdlib2IsFloat64AJSONInteger(t *testing.T) {
329+
t.Run("should not be integers", testNotIntegers(stdlibIsFloat64JSONInteger, true))
330+
t.Run("should be integers", testIntegers(stdlibIsFloat64JSONInteger, true))
331+
}
332+
333+
func testNotIntegers(fn func(float64) bool, skipKnownFailure bool) func(*testing.T) {
334+
_ = skipKnownFailure
335+
336+
return func(t *testing.T) {
337+
assert.False(t, fn(math.Inf(1)))
338+
assert.False(t, fn(maxJSONFloat+1))
339+
assert.False(t, fn(minJSONFloat-1))
340+
assert.False(t, fn(math.SmallestNonzeroFloat64))
341+
assert.False(t, fn(0.5))
342+
assert.False(t, fn(0.25))
343+
assert.False(t, fn(1.00/func() float64 { return 2.00 }()))
344+
assert.False(t, fn(1.00/func() float64 { return 4.00 }()))
345+
assert.False(t, fn(epsilon))
346+
}
347+
}
348+
349+
func testIntegers(fn func(float64) bool, skipKnownFailure bool) func(*testing.T) {
350+
// wrapping in a function forces non-constant evaluation to test float64 rounding behavior
351+
return func(t *testing.T) {
352+
assert.True(t, fn(0.0))
353+
assert.True(t, fn(1.0))
354+
assert.True(t, fn(maxJSONFloat))
355+
assert.True(t, fn(minJSONFloat))
356+
if !skipKnownFailure {
357+
assert.True(t, fn(1/0.01*67.15000001))
358+
}
359+
if !skipKnownFailure {
360+
assert.True(t, fn(1.00/func() float64 { return 0.01 }()*4643.4))
361+
}
362+
assert.True(t, fn(1.00/func() float64 { return 1.00 / 3.00 }()))
363+
assert.True(t, fn(math.SmallestNonzeroFloat64/2))
364+
assert.True(t, fn(math.SmallestNonzeroFloat64/3))
365+
assert.True(t, fn(math.SmallestNonzeroFloat64/4))
366+
}
367+
}
368+
369+
func BenchmarkIsFloat64JSONInteger(b *testing.B) {
370+
b.ResetTimer()
371+
b.ReportAllocs()
372+
b.SetBytes(0)
373+
374+
b.Run("new float vs integer comparison", benchmarkIsFloat64JSONInteger(IsFloat64AJSONInteger))
375+
b.Run("previous float vs integer comparison", benchmarkIsFloat64JSONInteger(previousIsFloat64JSONInteger))
376+
b.Run("bitwise float vs integer comparison", benchmarkIsFloat64JSONInteger(bitwiseIsFloat64JSONInteger))
377+
b.Run("bitwise float vs integer comparison (2)", benchmarkIsFloat64JSONInteger(bitwiseIsFloat64JSONInteger2))
378+
b.Run("stdlib float vs integer comparison (2)", benchmarkIsFloat64JSONInteger(stdlibIsFloat64JSONInteger))
379+
}
380+
381+
func BenchmarkBitwise(b *testing.B) {
382+
b.ResetTimer()
383+
b.ReportAllocs()
384+
b.SetBytes(0)
385+
386+
b.Run("bitwise float vs integer comparison (2)", benchmarkIsFloat64JSONInteger(bitwiseIsFloat64JSONInteger2))
387+
}
388+
389+
func previousIsFloat64JSONInteger(f float64) bool {
390+
if math.IsNaN(f) || math.IsInf(f, 0) || f < minJSONFloat || f > maxJSONFloat {
391+
return false
392+
}
393+
fa := math.Abs(f)
394+
g := float64(uint64(f))
395+
ga := math.Abs(g)
396+
397+
diff := math.Abs(f - g)
398+
399+
// more info: https://floating-point-gui.de/errors/comparison/#look-out-for-edge-cases
400+
switch {
401+
case f == g: // best case
402+
return true
403+
case f == float64(int64(f)) || f == float64(uint64(f)): // optimistic case
404+
return true
405+
case f == 0 || g == 0 || diff < math.SmallestNonzeroFloat64: // very close to 0 values
406+
return diff < (epsilon * math.SmallestNonzeroFloat64)
407+
}
408+
// check the relative error
409+
return diff/math.Min(fa+ga, math.MaxFloat64) < epsilon
410+
}
411+
412+
func stdlibIsFloat64JSONInteger(f float64) bool {
413+
if f < minJSONFloat || f > maxJSONFloat {
414+
return false
415+
}
416+
var bf big.Float
417+
bf.SetFloat64(f)
418+
419+
return bf.IsInt()
420+
}
421+
422+
func bitwiseIsFloat64JSONInteger(f float64) bool {
423+
if math.IsNaN(f) || math.IsInf(f, 0) || f < minJSONFloat || f > maxJSONFloat {
424+
return false
425+
}
426+
427+
mant, exp := math.Frexp(f) // get normalized mantissa
428+
if exp == 0 && mant == 0 {
429+
return true
430+
}
431+
if exp <= 0 {
432+
return false
433+
}
434+
435+
zeros := bits.TrailingZeros64(uint64(mant))
436+
437+
return bits.UintSize-zeros <= exp
438+
}
439+
440+
func bitwiseIsFloat64JSONInteger2(f float64) bool {
441+
if f == 0 {
442+
return true
443+
}
444+
445+
if f < minJSONFloat || f > maxJSONFloat || f != f || f < -math.MaxFloat64 || f > math.MaxFloat64 {
446+
return false
447+
}
448+
449+
// inlined
450+
var (
451+
mant uint64
452+
exp int
453+
)
454+
{
455+
const smallestNormal = 2.2250738585072014e-308 // 2**-1022
456+
457+
if math.Abs(f) < smallestNormal {
458+
f *= (1 << shift) // x 2^52
459+
exp = -shift
460+
}
461+
462+
x := math.Float64bits(f)
463+
exp += int((x>>shift)&mask) - bias + 1 //nolint:gosec // x>>12 & 0x7FF - 1022 : extract exp, recentered from bias
464+
465+
x &^= mask << shift // x= x &^ 0x7FF << 12 (clear 11 exp bits then shift 12)
466+
x |= (-1 + bias) << shift // x = x | 1022 << 12 ==> or with 1022 as exp location
467+
mant = uint64(math.Float64frombits(x))
468+
}
469+
/*
470+
{
471+
x := math.Float64bits(f)
472+
exp = int(x>>shift) & mask
473+
474+
if exp < bias {
475+
} else if exp < bias+shift { // 1023 + 12
476+
exp -= bias
477+
}
478+
}
479+
*/
480+
/*
481+
e := uint(bits>>shift) & mask
482+
if e < bias {
483+
// Round abs(x) < 1 including denormals.
484+
bits &= signMask // +-0
485+
if e == bias-1 {
486+
bits |= uvone // +-1
487+
}
488+
} else if e < bias+shift {
489+
// Round any abs(x) >= 1 containing a fractional component [0,1).
490+
//
491+
// Numbers with larger exponents are returned unchanged since they
492+
// must be either an integer, infinity, or NaN.
493+
const half = 1 << (shift - 1)
494+
e -= bias
495+
bits += half >> e
496+
bits &^= fracMask >> e
497+
}
498+
*/
499+
500+
// It returns frac and exp satisfying f == frac × 2**exp,
501+
// with the absolute value of frac in the interval [½, 1).
502+
if exp <= 0 {
503+
return false
504+
}
505+
506+
zeros := bits.TrailingZeros64(mant)
507+
508+
return bits.UintSize-zeros <= exp
509+
}
510+
511+
const (
512+
mask = 0x7FF
513+
shift = 64 - 11 - 1
514+
// uvinf = 0x7FF0000000000000
515+
// uvneginf = 0xFFF0000000000000
516+
bias = 1023
517+
fracMask = 1<<shift - 1
518+
)
519+
520+
/*
521+
func isNaN(x uint64) bool { // f != f
522+
return uint32(x>>shift)&mask == mask // && x != uvinf && x != uvneginf
523+
}
524+
525+
func isInf(x uint64) bool { // f < - math.MaxFloat || f > math.MaxFloat
526+
return x == uvinf || x == uvneginf
527+
}
528+
*/
529+
530+
/*
531+
func frexp(f float64) (frac uint64, exp int) {
532+
const smallestNormal = 2.2250738585072014e-308 // 2**-1022
533+
g := f
534+
535+
if math.Abs(f) < smallestNormal {
536+
g *= (1 << 52)
537+
exp = -52
538+
}
539+
540+
x := math.Float64bits(g)
541+
exp += int((x>>shift)&mask) - bias + 1
542+
x &^= mask << shift
543+
x |= (-1 + bias) << shift
544+
frac = uint64(math.Float64frombits(x))
545+
546+
return
547+
}
548+
*/
549+
550+
func benchmarkIsFloat64JSONInteger(fn func(float64) bool) func(*testing.B) {
551+
assertCode := func() {
552+
panic("unexpected result during benchmark")
553+
}
554+
555+
return func(b *testing.B) {
556+
testFunc := func() {
557+
if fn(math.Inf(1)) {
558+
assertCode()
559+
}
560+
if fn(maxJSONFloat + 1) {
561+
assertCode()
562+
}
563+
if fn(minJSONFloat - 1) {
564+
assertCode()
565+
}
566+
if fn(math.SmallestNonzeroFloat64) {
567+
assertCode()
568+
}
569+
if fn(0.5) {
570+
assertCode()
571+
}
572+
573+
if !fn(1.0) {
574+
assertCode()
575+
}
576+
if !fn(maxJSONFloat) {
577+
assertCode()
578+
}
579+
if !fn(minJSONFloat) {
580+
assertCode()
581+
}
582+
if !fn(1 / 0.01 * 67.15000001) {
583+
assertCode()
584+
}
585+
/* can't compare both versions on this test case
586+
if !fn(1 / func() float64 { return 0.01 }() * 4643.4) {
587+
assertCode()
588+
}
589+
*/
590+
if !fn(math.SmallestNonzeroFloat64 / 2) {
591+
assertCode()
592+
}
593+
if !fn(math.SmallestNonzeroFloat64 / 3) {
594+
assertCode()
595+
}
596+
if !fn(math.SmallestNonzeroFloat64 / 4) {
597+
assertCode()
598+
}
599+
}
600+
601+
for n := 0; n < b.N; n++ {
602+
testFunc()
603+
}
604+
}
320605
}
321606

322607
// test utilities

0 commit comments

Comments
 (0)