Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions jlib/number.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
27 changes: 23 additions & 4 deletions jparse/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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).
Expand Down
32 changes: 25 additions & 7 deletions jparse/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
7 changes: 7 additions & 0 deletions jsonata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down