diff --git a/CHANGELOG.md b/CHANGELOG.md index ef88dbe..54d92ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the input is validated UTF-8 up front, so the conversion pass and its four bytes per rune were pure overhead. Parsing a large array-of-tables document runs about a fifth faster and allocates about half the memory. +- Numeric tokens without underscores skip the normalising rebuild: digits are + validated in place in `joinDigits`, and a float whose token is already + clean goes to `strconv.ParseFloat` directly. One allocation per integer + atom and two per float atom disappear. ### Fixed diff --git a/number.go b/number.go index 20d1ee3..e2debd4 100644 --- a/number.go +++ b/number.go @@ -94,28 +94,43 @@ func decodeFloat(tok string) (any, error) { if err := checkNoLeadingZero(ip); err != nil { return nil, err } - build := sign + ip + fp := "" if hasDot { - fp, err := joinDigits(frac, isDecDigit) - if err != nil { + if fp, err = joinDigits(frac, isDecDigit); err != nil { return nil, err } - build += "." + fp } // The ABNF requires at least one digit after the exponent marker, so a // trailing e or E is an error even though strconv would accept it. The // digits are a zero-prefixable integer, so leading zeros are fine here // (the corpus holds valid cases such as 1e06 and 0e00). + esign, ed := "", "" if hasExp { - esign, edigits := splitSign(exp) - ed, err := joinDigits(edigits, isDecDigit) - if err != nil { + var digits string + esign, digits = splitSign(exp) + if ed, err = joinDigits(digits, isDecDigit); err != nil { return nil, err } - build += "e" + esign + ed } + // The checks above validated the token's shape, and every character a + // valid token may carry is one strconv.ParseFloat accepts in place, so + // only a token with underscores needs the stripped rebuild. + if !strings.ContainsRune(tok, '_') { + f, err := strconv.ParseFloat(tok, 64) + if err != nil { + return nil, fmt.Errorf("invalid float %q", tok) + } + return f, nil + } + build := sign + ip + if hasDot { + build += "." + fp + } + if hasExp { + build += "e" + esign + ed + } f, err := strconv.ParseFloat(build, 64) if err != nil { return nil, fmt.Errorf("invalid float %q", tok) @@ -125,11 +140,20 @@ func decodeFloat(tok string) (any, error) { // joinDigits validates that every rune is a digit (per isDigit) and that each // underscore sits between two digits, returning the digits with underscores -// removed. +// removed. A token without underscores, the common case, is validated in +// place and returned without a copy. func joinDigits(s string, isDigit func(byte) bool) (string, error) { if s == "" { return "", fmt.Errorf("number is missing digits") } + if !strings.ContainsRune(s, '_') { + for i := range len(s) { + if !isDigit(s[i]) { + return "", fmt.Errorf("invalid character %q in number", string(s[i])) + } + } + return s, nil + } var b strings.Builder for i := range len(s) { c := s[i]