diff --git a/jlib/number.go b/jlib/number.go index 8cb0e46..35f6afc 100644 --- a/jlib/number.go +++ b/jlib/number.go @@ -17,6 +17,7 @@ import ( ) var reNumber = regexp.MustCompile(`^-?(([0-9]+))(\.[0-9]+)?([Ee][-+]?[0-9]+)?$`) +var reHexNumber = regexp.MustCompile(`^-?0[xX][0-9a-fA-F]+$`) // Number converts values to numbers. Numeric values are returned // unchanged. Strings in legal JSON number format are converted @@ -36,7 +37,27 @@ func Number(value StringNumberBool) (float64, error) { } s, ok := jtypes.AsString(v) - if ok && reNumber.MatchString(s) { + if !ok { + return 0, fmt.Errorf("unable to cast %q to a number", s) + } + + // Check for hexadecimal numbers first + if reHexNumber.MatchString(s) { + // Remove the 0x or 0X prefix and parse as base 16 + hexStr := s + if strings.HasPrefix(s, "-0x") || strings.HasPrefix(s, "-0X") { + hexStr = "-" + s[3:] + } else if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { + hexStr = s[2:] + } + + if n, err := strconv.ParseInt(hexStr, 16, 64); err == nil { + return float64(n), nil + } + } + + // Check for regular decimal numbers + if reNumber.MatchString(s) { if n, err := strconv.ParseFloat(s, 64); err == nil { return n, nil } @@ -116,7 +137,7 @@ func Random() float64 { // It does this by converting back and forth to strings to // avoid floating point rounding errors, e.g. // -// 4.525 * math.Pow10(2) returns 452.50000000000006 +// 4.525 * math.Pow10(2) returns 452.50000000000006 func multByPow10(x float64, n int) float64 { if n == 0 || math.IsNaN(x) || math.IsInf(x, 0) { return x diff --git a/jparse/lexer.go b/jparse/lexer.go index bff6df4..bf49600 100644 --- a/jparse/lexer.go +++ b/jparse/lexer.go @@ -326,13 +326,26 @@ Loop: // and returns a number token. func (l *lexer) scanNumber() token { - // JSON does not support leading zeroes. The integer part of - // a number will either be a single zero, or a non-zero digit - // followed by zero or more digits. - if !l.acceptRune('0') { + // Check for hexadecimal numbers first (0x or 0X prefix) + if l.acceptRune('0') { + if l.acceptRunes2('x', 'X') { + // This is a hexadecimal number + if !l.acceptAll(isHexDigit) { + // If there are no hex digits after 0x, this is invalid + return l.error(ErrInvalidNumber, l.input[l.start:l.current]) + } + return l.newToken(typeNumber) + } + // Single zero - valid decimal number + } else { + // JSON does not support leading zeroes. The integer part of + // a number will either be a single zero, or a non-zero digit + // followed by zero or more digits. l.accept(isNonZeroDigit) l.acceptAll(isDigit) } + + // Handle decimal point for decimal numbers if l.acceptRune('.') { if !l.acceptAll(isDigit) { // If there are no digits after the decimal point, @@ -342,6 +355,8 @@ func (l *lexer) scanNumber() token { return l.newToken(typeNumber) } } + + // Handle scientific notation for decimal numbers if l.acceptRunes2('e', 'E') { l.acceptRunes2('+', '-') l.acceptAll(isDigit) @@ -522,6 +537,10 @@ func isNonZeroDigit(r rune) bool { return r >= '1' && r <= '9' } +func isHexDigit(r rune) bool { + return (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') +} + // symbolsAndKeywords maps operator token types back to their // string representations. It's only used by tokenType.String // (and one test). diff --git a/jparse/node.go b/jparse/node.go index 6d2bbe4..7cc436c 100644 --- a/jparse/node.go +++ b/jparse/node.go @@ -57,14 +57,32 @@ type NumberNode struct { func parseNumber(p *parser, t token) (Node, error) { - // Number literals are promoted to type float64. - n, err := strconv.ParseFloat(t.Value, 64) - if err != nil { - typ := ErrInvalidNumber - if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { - typ = ErrNumberRange + var n float64 + var err error + + // Check if this is a hexadecimal number + if strings.HasPrefix(t.Value, "0x") || strings.HasPrefix(t.Value, "0X") { + // Parse as hexadecimal + hexStr := t.Value[2:] // Remove 0x prefix + if hexInt, parseErr := strconv.ParseInt(hexStr, 16, 64); parseErr == nil { + n = float64(hexInt) + } else { + typ := ErrInvalidNumber + if e, ok := parseErr.(*strconv.NumError); ok && e.Err == strconv.ErrRange { + typ = ErrNumberRange + } + return nil, newError(typ, t) + } + } else { + // Parse as decimal number + n, err = strconv.ParseFloat(t.Value, 64) + if err != nil { + typ := ErrInvalidNumber + if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { + typ = ErrNumberRange + } + return nil, newError(typ, t) } - return nil, newError(typ, t) } return &NumberNode{ diff --git a/jsonata_test.go b/jsonata_test.go index 3267918..aa30ff0 100644 --- a/jsonata_test.go +++ b/jsonata_test.go @@ -5991,6 +5991,13 @@ func TestFuncNumber(t *testing.T) { }, Output: float64(0), }, + { + Expression: []string{ + "$number(0x0023)", + `$number("0x0023")`, + }, + Output: float64(35), + }, { Expression: []string{ "$number(10)",