Files

165 lines
3.8 KiB
Go

package interpres
import (
"fmt"
"math"
"strconv"
"strings"
)
// decodeNumber parses a bare numeric token under strict TOML rules: no leading
// zeros, underscores only between digits, prefixed radixes without a sign, and
// floats with explicit fraction/exponent digits.
func decodeNumber(tok string) (any, error) {
switch tok {
case "inf", "+inf":
return math.Inf(1), nil
case "-inf":
return math.Inf(-1), nil
case "nan", "+nan", "-nan":
return math.NaN(), nil
}
if len(tok) >= 2 && tok[0] == '0' && (tok[1] == 'x' || tok[1] == 'o' || tok[1] == 'b') {
return decodeRadix(tok)
}
if strings.ContainsAny(tok, ".eE") {
return decodeFloat(tok)
}
return decodeDecimalInt(tok)
}
func decodeDecimalInt(tok string) (any, error) {
sign, body := splitSign(tok)
digits, err := joinDigits(body, isDecDigit)
if err != nil {
return nil, err
}
if err := checkNoLeadingZero(digits); err != nil {
return nil, err
}
i, err := strconv.ParseInt(sign+digits, 10, 64)
if err != nil {
return nil, fmt.Errorf("integer %q out of range", tok)
}
return i, nil
}
func decodeRadix(tok string) (any, error) {
var base int
var isDigit func(byte) bool
switch tok[1] {
case 'x':
base, isDigit = 16, isHexDigit
case 'o':
base, isDigit = 8, isOctDigit
case 'b':
base, isDigit = 2, isBinDigit
}
digits, err := joinDigits(tok[2:], isDigit)
if err != nil {
return nil, err
}
i, err := strconv.ParseInt(digits, base, 64)
if err != nil {
return nil, fmt.Errorf("integer %q out of range", tok)
}
return i, nil
}
func decodeFloat(tok string) (any, error) {
sign, s := splitSign(tok)
mantissa, exp := s, ""
if i := strings.IndexAny(s, "eE"); i >= 0 {
mantissa, exp = s[:i], s[i+1:]
}
intPart, frac, hasDot := mantissa, "", false
if i := strings.IndexByte(mantissa, '.'); i >= 0 {
intPart, frac, hasDot = mantissa[:i], mantissa[i+1:], true
}
if !hasDot && exp == "" {
return nil, fmt.Errorf("invalid float %q", tok)
}
ip, err := joinDigits(intPart, isDecDigit)
if err != nil {
return nil, err
}
if err := checkNoLeadingZero(ip); err != nil {
return nil, err
}
build := sign + ip
if hasDot {
fp, err := joinDigits(frac, isDecDigit)
if err != nil {
return nil, err
}
build += "." + fp
}
if exp != "" {
esign, edigits := splitSign(exp)
ed, err := joinDigits(edigits, isDecDigit)
if err != nil {
return nil, err
}
build += "e" + esign + ed
}
f, err := strconv.ParseFloat(build, 64)
if err != nil {
return nil, fmt.Errorf("invalid float %q", tok)
}
return f, nil
}
// joinDigits validates that every rune is a digit (per isDigit) and that each
// underscore sits between two digits, returning the digits with underscores
// removed.
func joinDigits(s string, isDigit func(byte) bool) (string, error) {
if s == "" {
return "", fmt.Errorf("number is missing digits")
}
var b strings.Builder
for i := 0; i < len(s); i++ {
c := s[i]
if c == '_' {
if i == 0 || i == len(s)-1 || !isDigit(s[i-1]) || !isDigit(s[i+1]) {
return "", fmt.Errorf("misplaced underscore in number %q", s)
}
continue
}
if !isDigit(c) {
return "", fmt.Errorf("invalid character %q in number", string(c))
}
b.WriteByte(c)
}
return b.String(), nil
}
func checkNoLeadingZero(digits string) error {
if len(digits) > 1 && digits[0] == '0' {
return fmt.Errorf("leading zeros are not allowed in numbers")
}
return nil
}
func splitSign(tok string) (sign, rest string) {
if tok != "" && (tok[0] == '+' || tok[0] == '-') {
if tok[0] == '-' {
return "-", tok[1:]
}
return "", tok[1:]
}
return "", tok
}
func isDecDigit(c byte) bool { return c >= '0' && c <= '9' }
func isOctDigit(c byte) bool { return c >= '0' && c <= '7' }
func isBinDigit(c byte) bool { return c == '0' || c == '1' }
func isHexDigit(c byte) bool {
return isDecDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}