v0.1.0 28f9d6e

number.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close