@@ -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
307308func 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