Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Most efficient way to check if a byte slice is a number

Tags:

go

I'm looking for the most efficient way to tell whether a byte slice is a float.

This is to be done on huge datasets, so performance is key.

Tried approaches:

  • strconv.ParseFloat
  • regexp.Match
  • CheckNumber - home rolled function using IsNumber + looking at whether the byte slice contains a ..

    func CheckNumber(p []byte) bool {
        r := string(p)
        sep := 0
        for _, b := range r {
            if unicode.IsNumber(b) {
                continue
            }
            if b == rune('.') {
                if sep > 0 {
                    return false
                }
                sep++
                continue
            }
            return false
        }
        return true
    }
    

The benchmark code:

func BenchmarkFloatStrconv(b *testing.B) {
    p := []byte("15.34234234234")

    for i := 0; i < b.N; i++ {
        _, err := strconv.ParseFloat(string(p), 64)
        if err != nil {
            log.Fatalf("NaN")
        }
    }
}

func BenchmarkFloatRegex(b *testing.B) {
    p := []byte("15.34234234234")
    r := `[-+]?[0-9]*\.?[0-9]`
    c, _ := regexp.Compile(r)

    for i := 0; i < b.N; i++ {
        ok := c.Match(p)
        if !ok {
            log.Fatalf("NaN")
        }
    }
}

func BenchmarkCheckNumber(b *testing.B) {
    p := []byte("15.34234234234")

    for i := 0; i < b.N; i++ {
        ok := CheckNumber(p)
        if !ok {
            log.Fatalf("NaN")
        }
    }
}

Benchmark results:

BenchmarkFloatStrconv-8     20000000            85.8 ns/op        16 B/op          1 allocs/op
BenchmarkFloatRegex-8        5000000           252 ns/op           0 B/op          0 allocs/op
BenchmarkCheckNumber-8      20000000            64.3 ns/op         0 B/op          0 allocs/op
  • Am I doing the different solutions fairness?
  • Are there better solutions?

Edit: thanks to pointers from Adrian and icza, this avoids converting to strings/runes

func CheckNumberNoStringConvert(r []byte) bool {
    sep := 0

    for i := range r {
        if r[i] >= 48 && r[i] <= 57 {
            continue
        }
        if r[i] == 46 {
            if sep > 0 {
                return false
            }
            sep++
            continue
        }
        return false
    }

    return true
}

and performs quite well ;-)

BenchmarkCheckNumberNoStringConvert-8       200000000            8.55 ns/op        0 B/op          0 allocs/op
like image 906
salient Avatar asked Aug 18 '17 20:08

salient


2 Answers

For a simple real (floating-point) number (no scientific or engineering floating-point format, no group separators),

func IsReal(n []byte) bool {
    if len(n) > 0 && n[0] == '-' {
        n = n[1:]
    }
    if len(n) == 0 {
        return false
    }
    var point bool
    for _, c := range n {
        if '0' <= c && c <= '9' {
            continue
        }
        if c == '.' && len(n) > 1 && !point {
            point = true
            continue
        }
        return false
    }
    return true
}

Benchmark:

$ go test -run=! -bench=. -benchmem -cpu=1 real_test.go
goos: linux
goarch: amd64
BenchmarkIsReal         100000000       20.8 ns/op         0 B/op          0 allocs/op
BenchmarkFloatStrconv   20000000       101 ns/op          16 B/op          1 allocs/op
BenchmarkFloatRegex      5000000       284 ns/op           0 B/op          0 allocs/op
BenchmarkCheckNumber    20000000        73.0 ns/op         0 B/op          0 allocs/op
PASS
ok      command-line-arguments  7.380s

real_test.go:

package main

import (
    "log"
    "regexp"
    "strconv"
    "testing"
    "unicode"
)

func IsReal(n []byte) bool {
    if len(n) > 0 && n[0] == '-' {
        n = n[1:]
    }
    if len(n) == 0 {
        return false
    }
    var point bool
    for _, c := range n {
        if '0' <= c && c <= '9' {
            continue
        }
        if c == '.' && len(n) > 1 && !point {
            point = true
            continue
        }
        return false
    }
    return true
}

func BenchmarkIsReal(b *testing.B) {
    p := []byte("15.34234234234")
    for i := 0; i < b.N; i++ {
        ok := IsReal(p)
        if !ok {
            log.Fatalf("NaN")
        }
    }
}

func CheckNumber(p []byte) bool {
    r := string(p)

    sep := 0

    for _, b := range r {
        if unicode.IsNumber(b) {
            continue
        }
        if b == rune('.') {
            if sep > 0 {
                return false
            }
            sep++
            continue
        }
        return false
    }

    return true

}

func BenchmarkFloatStrconv(b *testing.B) {
    p := []byte("15.34234234234")

    for i := 0; i < b.N; i++ {
        _, err := strconv.ParseFloat(string(p), 64)
        if err != nil {
            log.Fatalf("NaN")
        }
    }
}

func BenchmarkFloatRegex(b *testing.B) {
    p := []byte("15.34234234234")
    r := `[-+]?[0-9]*\.?[0-9]`
    c, _ := regexp.Compile(r)

    for i := 0; i < b.N; i++ {
        ok := c.Match(p)
        if !ok {
            log.Fatalf("NaN")
        }
    }
}

func BenchmarkCheckNumber(b *testing.B) {
    p := []byte("15.34234234234")

    for i := 0; i < b.N; i++ {
        ok := CheckNumber(p)
        if !ok {
            log.Fatalf("NaN")
        }
    }
}
like image 60
peterSO Avatar answered Nov 14 '22 02:11

peterSO


I took upon it as a challenge for myself to rewrite this as some kind of state machine synthesizing the collective input from everyone here :)

func Validate(b []byte) bool {
    for i := range b {
        switch {
        case b[i] >= '0' && b[i] <= '9':
            continue
        case b[i] == '.':
            if len(b) == 1 {
                return false
            }
            if len(b) > i {
                return fractional(b[i+1:])
            }
            return true
        case i == 0 && b[i] == '-':
            if len(b) == 1 {
                return false
            }
            continue
        default:
            return false
        }
    }

    return true
}

func fractional(b []byte) bool {
    for i := range b {
        switch {
        case b[i] >= '0' && b[i] <= '9':
            continue
        case b[i] == 'e' || b[i] == 'E':
            if len(b[:i]) == 0 {
                return false
            }
            if len(b) > i+1 {
                return scientific(b[i+1:])
            }
            return false
        default:
            return false
        }
    }

    return true
}

func scientific(b []byte) bool {
    for i := range b {
        switch {
        case b[i] >= '0' && b[i] <= '9':
            continue
        case i == 0 && b[i] == '-':
            if len(b) == 1 {
                return false
            }
            continue
        default:
            return false
        }
    }

    return true
}

It seems to work on a few different number formats:

type v struct {
    Input    []byte
    Expected bool
}

func TestPermutations(t *testing.T) {
    b := []v{
        v{[]byte("123.456"), true},
        v{[]byte("123"), true},
        v{[]byte("123."), true},
        v{[]byte(".123"), true},
        v{[]byte("12.1e12"), true},
        v{[]byte("12.1e-12"), true},
        v{[]byte("-123.456"), true},
        v{[]byte("-123"), true},
        v{[]byte("-123."), true},
        v{[]byte("-.123"), true},
        v{[]byte("-12.1e12"), true},
        v{[]byte("-12.1e-12"), true},
        v{[]byte(".1e-12"), true},
        v{[]byte(".e-12"), false},
        v{[]byte(".e"), false},
        v{[]byte("e"), false},
        v{[]byte("abcdef"), false},
        v{[]byte("-"), false},
        v{[]byte("."), false},
    }

    for _, test := range b {
        ok := Validate(test.Input)
        if ok != test.Expected {
            t.Errorf("could not handle case %s", test.Input)
        }
    }

}

and perform quite well on the original benchmark:

BenchmarkValidate-8     100000000           13.0 ns/op         0 B/op          0 allocs/op

Benchmark code:

func BenchmarkValidate(b *testing.B) {
    p := []byte("15.1234567890")

    for i := 0; i < b.N; i++ {
        ok := Validate(p)
        if !ok {
            log.Fatalf("problem")
        }
    }
}
like image 32
salient Avatar answered Nov 14 '22 02:11

salient