342 lines
6.4 KiB
Go
342 lines
6.4 KiB
Go
package jsnum
|
|
|
|
import (
|
|
"errors"
|
|
"math"
|
|
"math/big"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
|
|
"github.com/go-json-experiment/json"
|
|
)
|
|
|
|
// https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-tostring
|
|
func (n Number) String() string {
|
|
switch {
|
|
case n.IsNaN():
|
|
return "NaN"
|
|
case n.IsInf():
|
|
if n < 0 {
|
|
return "-Infinity"
|
|
}
|
|
return "Infinity"
|
|
}
|
|
|
|
// Fast path: for safe integers, directly convert to string.
|
|
if MinSafeInteger <= n && n <= MaxSafeInteger {
|
|
if i := int64(n); float64(i) == float64(n) {
|
|
return strconv.FormatInt(i, 10)
|
|
}
|
|
}
|
|
|
|
// Otherwise, the Go json package handles this correctly.
|
|
b, _ := json.Marshal(float64(n))
|
|
return string(b)
|
|
}
|
|
|
|
// https://tc39.es/ecma262/2024/multipage/abstract-operations.html#sec-stringtonumber
|
|
func FromString(s string) Number {
|
|
// Implementing StringToNumber exactly as written in the spec involves
|
|
// writing a parser, along with the conversion of the parsed AST into the
|
|
// actual value.
|
|
//
|
|
// We've already implemented a number parser in the scanner, but we can't
|
|
// import it here. We also do not have the conversion implemented since we
|
|
// previously just wrote `+literal` and let the runtime handle it.
|
|
//
|
|
// The strategy below is to instead break the number apart and fix it up
|
|
// such that Go's own parsing functionality can handle it. This won't be
|
|
// the fastest method, but it saves us from writing the full parser and
|
|
// conversion logic.
|
|
|
|
s = strings.TrimFunc(s, isStrWhiteSpace)
|
|
|
|
switch s {
|
|
case "":
|
|
return 0
|
|
case "Infinity", "+Infinity":
|
|
return Inf(1)
|
|
case "-Infinity":
|
|
return Inf(-1)
|
|
}
|
|
|
|
for _, r := range s {
|
|
if !isNumberRune(r) {
|
|
return NaN()
|
|
}
|
|
}
|
|
|
|
if n, ok := tryParseInt(s); ok {
|
|
return n
|
|
}
|
|
|
|
// Cut this off first so we can ensure -0 is returned as -0.
|
|
s, negative := strings.CutPrefix(s, "-")
|
|
|
|
if !negative {
|
|
s, _ = strings.CutPrefix(s, "+")
|
|
}
|
|
|
|
if first, _ := utf8.DecodeRuneInString(s); !stringutil.IsDigit(first) && first != '.' {
|
|
return NaN()
|
|
}
|
|
|
|
f := parseFloatString(s)
|
|
if math.IsNaN(f) {
|
|
return NaN()
|
|
}
|
|
|
|
sign := 1.0
|
|
if negative {
|
|
sign = -1.0
|
|
}
|
|
return Number(math.Copysign(f, sign))
|
|
}
|
|
|
|
func isStrWhiteSpace(r rune) bool {
|
|
// This is different than stringutil.IsWhiteSpaceLike.
|
|
|
|
// https://tc39.es/ecma262/2024/multipage/ecmascript-language-lexical-grammar.html#prod-LineTerminator
|
|
// https://tc39.es/ecma262/2024/multipage/ecmascript-language-lexical-grammar.html#prod-WhiteSpace
|
|
|
|
switch r {
|
|
// LineTerminator
|
|
case '\n', '\r', 0x2028, 0x2029:
|
|
return true
|
|
// WhiteSpace
|
|
case '\t', '\v', '\f', 0xFEFF:
|
|
return true
|
|
}
|
|
|
|
// WhiteSpace
|
|
return unicode.Is(unicode.Zs, r)
|
|
}
|
|
|
|
var errUnknownPrefix = errors.New("unknown number prefix")
|
|
|
|
func tryParseInt(s string) (Number, bool) {
|
|
var i int64
|
|
var err error
|
|
var hasIntResult bool
|
|
|
|
if len(s) > 2 {
|
|
prefix, rest := s[:2], s[2:]
|
|
switch prefix {
|
|
case "0b", "0B":
|
|
if !isAllBinaryDigits(rest) {
|
|
return NaN(), true
|
|
}
|
|
i, err = strconv.ParseInt(rest, 2, 64)
|
|
hasIntResult = true
|
|
case "0o", "0O":
|
|
if !isAllOctalDigits(rest) {
|
|
return NaN(), true
|
|
}
|
|
i, err = strconv.ParseInt(rest, 8, 64)
|
|
hasIntResult = true
|
|
case "0x", "0X":
|
|
if !isAllHexDigits(rest) {
|
|
return NaN(), true
|
|
}
|
|
i, err = strconv.ParseInt(rest, 16, 64)
|
|
hasIntResult = true
|
|
}
|
|
}
|
|
|
|
if !hasIntResult {
|
|
// StringToNumber does not parse leading zeros as octal.
|
|
s = trimLeadingZeros(s)
|
|
if !isAllDigits(s) {
|
|
return 0, false
|
|
}
|
|
i, err = strconv.ParseInt(s, 10, 64)
|
|
hasIntResult = true
|
|
}
|
|
|
|
if hasIntResult && err == nil {
|
|
return Number(i), true
|
|
}
|
|
|
|
// Using this to parse large integers.
|
|
bi, ok := new(big.Int).SetString(s, 0)
|
|
if !ok {
|
|
return NaN(), true
|
|
}
|
|
|
|
f, _ := bi.Float64()
|
|
return Number(f), true
|
|
}
|
|
|
|
func parseFloatString(s string) float64 {
|
|
var hasDot, hasExp bool
|
|
|
|
// <a>
|
|
// <a>.<b>
|
|
// <a>.<b>e<c>
|
|
// <a>e<c>
|
|
var a, b, c, rest string
|
|
|
|
a, rest, hasDot = strings.Cut(s, ".")
|
|
if hasDot {
|
|
// <a>.<b>
|
|
// <a>.<b>e<c>
|
|
b, c, hasExp = cutAny(rest, "eE")
|
|
} else {
|
|
// <a>
|
|
// <a>e<c>
|
|
a, c, hasExp = cutAny(s, "eE")
|
|
}
|
|
|
|
var sb strings.Builder
|
|
sb.Grow(len(a) + len(b) + len(c) + 3)
|
|
|
|
if a == "" {
|
|
if hasDot && b == "" {
|
|
return math.NaN()
|
|
}
|
|
if hasExp && c == "" {
|
|
return math.NaN()
|
|
}
|
|
sb.WriteString("0")
|
|
} else {
|
|
a = trimLeadingZeros(a)
|
|
if !isAllDigits(a) {
|
|
return math.NaN()
|
|
}
|
|
sb.WriteString(a)
|
|
}
|
|
|
|
if hasDot {
|
|
sb.WriteString(".")
|
|
if b == "" {
|
|
sb.WriteString("0")
|
|
} else {
|
|
b = trimTrailingZeros(b)
|
|
if !isAllDigits(b) {
|
|
return math.NaN()
|
|
}
|
|
sb.WriteString(b)
|
|
}
|
|
}
|
|
|
|
if hasExp {
|
|
sb.WriteString("e")
|
|
|
|
c, negative := strings.CutPrefix(c, "-")
|
|
if negative {
|
|
sb.WriteString("-")
|
|
} else {
|
|
c, _ = strings.CutPrefix(c, "+")
|
|
}
|
|
c = trimLeadingZeros(c)
|
|
if !isAllDigits(c) {
|
|
return math.NaN()
|
|
}
|
|
sb.WriteString(c)
|
|
}
|
|
|
|
return stringToFloat64(sb.String())
|
|
}
|
|
|
|
func cutAny(s string, cutset string) (before, after string, found bool) {
|
|
if i := strings.IndexAny(s, cutset); i >= 0 {
|
|
before = s[:i]
|
|
afterAndFound := s[i:]
|
|
_, size := utf8.DecodeRuneInString(afterAndFound)
|
|
after = afterAndFound[size:]
|
|
return before, after, true
|
|
}
|
|
return s, "", false
|
|
}
|
|
|
|
func trimLeadingZeros(s string) string {
|
|
if strings.HasPrefix(s, "0") {
|
|
s = strings.TrimLeft(s, "0")
|
|
if s == "" {
|
|
return "0"
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func trimTrailingZeros(s string) string {
|
|
if strings.HasSuffix(s, "0") {
|
|
s = strings.TrimRight(s, "0")
|
|
if s == "" {
|
|
return "0"
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func stringToFloat64(s string) float64 {
|
|
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
|
return f
|
|
} else {
|
|
if errors.Is(err, strconv.ErrRange) {
|
|
return f
|
|
}
|
|
}
|
|
return math.NaN()
|
|
}
|
|
|
|
func isAllDigits(s string) bool {
|
|
for _, r := range s {
|
|
if !stringutil.IsDigit(r) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isAllBinaryDigits(s string) bool {
|
|
for _, r := range s {
|
|
if r != '0' && r != '1' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isAllOctalDigits(s string) bool {
|
|
for _, r := range s {
|
|
if !stringutil.IsOctalDigit(r) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isAllHexDigits(s string) bool {
|
|
for _, r := range s {
|
|
if !stringutil.IsHexDigit(r) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isNumberRune(r rune) bool {
|
|
if stringutil.IsDigit(r) {
|
|
return true
|
|
}
|
|
|
|
if 'a' <= r && r <= 'f' {
|
|
return true
|
|
}
|
|
|
|
if 'A' <= r && r <= 'F' {
|
|
return true
|
|
}
|
|
|
|
switch r {
|
|
case '.', '-', '+', 'x', 'X', 'o', 'O':
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|