1// SPDX-License-Identifier: MIT
2
3package expr
4
5import (
6 "math"
7 "strconv"
8 "strings"
9)
10
11var (
12 nan = math.NaN()
13 posInf = math.Inf(1)
14)
15
16// numberToCoercedString is the string form of a number: .NET's
17// Double.ToString("G15", InvariantCulture), which is what the C# runner uses
18// for every number-to-string conversion (ExpressionConstants.NumberFormat,
19// EvaluationResult.ConvertToString, hence format(), join(), toJSON() and the
20// comparison with strings). The JavaScript reference computes
21// (+x.toFixed(15)).toString() instead; they agree on ordinary values and
22// differ on large, small and very precise ones. See PORTING.md.
23//
24// G15 rounds to 15 significant digits (ties to even on the exact binary
25// value), then prints positionally when the decimal exponent X of the rounded
26// value satisfies -5 < X < 15, and as d.dddE+XX otherwise, with the trailing
27// zeros of the mantissa removed and at least two exponent digits. NaN is
28// "NaN" and the infinities are "Infinity" and "-Infinity". Negative zero is
29// "-0", as .NET Core prints it and as the legacy engine of the runner
30// (DTExpressions2/EvaluationResult.ConvertToString, the one that runs today)
31// leaves it; the newer engine (Expressions/EvaluationResult.cs) maps it to "0".
32// See PORTING.md.
33func numberToCoercedString(x float64) string {
34 switch {
35 case math.IsNaN(x):
36 return "NaN"
37 case math.IsInf(x, 1):
38 return "Infinity"
39 case math.IsInf(x, -1):
40 return "-Infinity"
41 case x == 0:
42 if math.Signbit(x) {
43 return "-0"
44 }
45 return "0"
46 }
47 sign := ""
48 if x < 0 {
49 sign = "-"
50 x = -x
51 }
52 s := strconv.FormatFloat(x, 'e', 14, 64) // d.dddddddddddddde±xx, correctly rounded
53 mant, expStr, _ := strings.Cut(s, "e")
54 exp, _ := strconv.Atoi(expStr)
55 digits := strings.TrimRight(strings.Replace(mant, ".", "", 1), "0")
56 if digits == "" {
57 digits = "0"
58 }
59 switch {
60 case exp >= 15 || exp <= -5:
61 out := digits[:1]
62 if len(digits) > 1 {
63 out += "." + digits[1:]
64 }
65 esign := "+"
66 if exp < 0 {
67 esign = "-"
68 exp = -exp
69 }
70 e := strconv.Itoa(exp)
71 if len(e) < 2 {
72 e = "0" + e
73 }
74 return sign + out + "E" + esign + e
75 case exp < 0:
76 return sign + "0." + strings.Repeat("0", -exp-1) + digits
77 case len(digits) <= exp+1:
78 return sign + digits + strings.Repeat("0", exp+1-len(digits))
79 }
80 return sign + digits[:exp+1] + "." + digits[exp+1:]
81}
82
83// isNetSpace reports whether r is white space for .NET's Char.IsWhiteSpace,
84// which String.Trim() removes: U+0009..U+000D, U+0020, U+0085, U+00A0,
85// U+1680, U+2000..U+200A, U+2028, U+2029, U+202F, U+205F and U+3000. (The
86// JavaScript trim also removes U+FEFF and not U+0085.)
87func isNetSpace(r rune) bool {
88 switch {
89 case r >= 0x09 && r <= 0x0D, r == 0x20, r == 0x85, r == 0xA0, r == 0x1680,
90 r >= 0x2000 && r <= 0x200A, r == 0x2028, r == 0x2029, r == 0x202F, r == 0x205F, r == 0x3000:
91 return true
92 }
93 return false
94}
95
96// parseNumber converts a string to a number the way the runner does,
97// ExpressionUtility.ParseNumber (src/Sdk/DTExpressions2/Expressions2/Sdk/
98// ExpressionUtility.cs), which is used for number literals in expressions and
99// for every implicit string-to-number conversion:
100//
101// - the string is trimmed (see isNetSpace); empty means 0;
102// - Double.TryParse with AllowLeadingSign | AllowDecimalPoint |
103// AllowExponent in the invariant culture: [sign] digits [. digits]
104// [e [sign] digits], at least one digit around the point, ASCII digits
105// only, an out-of-range exponent giving an infinity, "Infinity" and
106// "-Infinity" in any letter case, NaN, and any number of trailing NUL
107// characters, which .NET ignores;
108// - "0x" followed by hexadecimal digits and "0o" followed by octal digits
109// (lower-case prefix, no sign), read as a 32-bit two's-complement
110// integer: 0xffffffff is -1, and a value that does not fit in 32 bits is
111// NaN. There is no binary form;
112// - everything else is NaN.
113//
114// The JavaScript reference uses Number(), which differs: 0X, 0O and 0B
115// prefixes, 64-bit exactness, no 32-bit wrap, and U+FEFF as white space.
116func parseNumber(s string) float64 {
117 s = strings.TrimFunc(s, isNetSpace)
118 if s == "" {
119 return 0
120 }
121 t := strings.TrimRight(s, "\x00")
122 if body, neg := strings.CutPrefix(t, "-"); neg {
123 if v, ok := parseDecimal(body); ok {
124 return -v
125 }
126 } else {
127 body, _ = strings.CutPrefix(t, "+")
128 if v, ok := parseDecimal(body); ok {
129 return v
130 }
131 }
132 if len(s) > 2 && s[0] == '0' {
133 switch s[1] {
134 case 'x':
135 if v, ok := parseRadix32(s[2:], 16); ok {
136 return v
137 }
138 case 'o':
139 if v, ok := parseRadix32(s[2:], 8); ok {
140 return v
141 }
142 }
143 }
144 return nan
145}
146
147// parseDecimal parses an unsigned decimal literal or "Infinity".
148func parseDecimal(body string) (float64, bool) {
149 if len(body) == 8 && strings.EqualFold(body, "infinity") && isASCII(body) {
150 return posInf, true
151 }
152 if !isDecimalLiteral(body) {
153 return 0, false
154 }
155 v, _ := strconv.ParseFloat(body, 64) // out of range yields an infinity, as in .NET
156 return v, true
157}
158
159// parseRadix32 reads digits in base 16 or 8 as an unsigned 32-bit value and
160// returns it as a signed one (Int32.TryParse with AllowHexSpecifier,
161// Convert.ToInt32(s, 8)). Leading zeros are free; digits beyond 32 bits fail.
162func parseRadix32(digits string, base int) (float64, bool) {
163 if digits == "" {
164 return 0, false
165 }
166 var v uint64
167 for i := 0; i < len(digits); i++ {
168 c := digits[i]
169 var d int
170 switch {
171 case c >= '0' && c <= '9':
172 d = int(c - '0')
173 case c >= 'a' && c <= 'f':
174 d = int(c-'a') + 10
175 case c >= 'A' && c <= 'F':
176 d = int(c-'A') + 10
177 default:
178 return 0, false
179 }
180 if d >= base {
181 return 0, false
182 }
183 v = v*uint64(base) + uint64(d)
184 if v > math.MaxUint32 {
185 return 0, false
186 }
187 }
188 return float64(int32(uint32(v))), true
189}
190
191// isDecimalLiteral reports whether s matches
192// (digits [. digits?] | . digits) ([eE] [+-]? digits)?.
193func isDecimalLiteral(s string) bool {
194 i := 0
195 digits := func() int {
196 start := i
197 for i < len(s) && s[i] >= '0' && s[i] <= '9' {
198 i++
199 }
200 return i - start
201 }
202 intDigits := digits()
203 fracDigits := 0
204 if i < len(s) && s[i] == '.' {
205 i++
206 fracDigits = digits()
207 }
208 if intDigits == 0 && fracDigits == 0 {
209 return false
210 }
211 if i < len(s) && (s[i] == 'e' || s[i] == 'E') {
212 i++
213 if i < len(s) && (s[i] == '+' || s[i] == '-') {
214 i++
215 }
216 if digits() == 0 {
217 return false
218 }
219 }
220 return i == len(s)
221}