fix(12-05): cast floats to strings with PHP's 14-digit precision in request validation

PHP 8's (string) cast of a float formats with the precision ini (14
significant digits, %.14G), not the shortest round-trip form: 1/3 is
0.33333333333333, 1e14 is 1.0E+14 and 5e-324 is 4.9406564584125E-324.
phpFloatString, used for the string form of JSON floats in size, in, regex
and integer checks, printed up to 17 digits and switched to the exponent
form only from 1e15. TestPHPFloatStringMatchesPHPCast pins 27 values to
php -r output.
This commit is contained in:
Jakub Zych
2026-10-02 15:46:24 +02:00
parent 36983bc87e
commit 3ac1d64ab4
2 changed files with 75 additions and 12 deletions

View File

@@ -618,8 +618,11 @@ func phpScalarString(value any) (string, bool) {
return "", false
}
// phpFloatString formats a float as PHP 8 casts it to string: the shortest
// round-trip digits, in exponent form (1.0E+15) from 1e15 up and below 1e-4.
// phpFloatString formats a float as PHP 8 casts it to string: %.14G with
// the default precision ini of 14 (zend_gcvt). The value is correctly
// rounded to 14 significant digits and trailing zeros are dropped; it is
// written in exponent form (1.0E+15, 1.5E-5) when the decimal point would
// sit more than 14 digits right or more than 3 zeros left of the digits.
func phpFloatString(f float64) string {
switch {
case math.IsNaN(f):
@@ -634,21 +637,38 @@ func phpFloatString(f float64) string {
}
return "0"
}
e := strconv.FormatFloat(f, 'e', -1, 64)
const precision = 14
e := strconv.FormatFloat(math.Abs(f), 'e', precision-1, 64)
mant, expStr, _ := strings.Cut(e, "e")
exp, _ := strconv.Atoi(expStr)
if exp >= -4 && exp < 15 {
return strconv.FormatFloat(f, 'f', -1, 64)
digits := strings.TrimRight(strings.Replace(mant, ".", "", 1), "0")
if digits == "" {
digits = "0"
}
if !strings.Contains(mant, ".") {
mant += ".0"
decpt := exp + 1
var out string
switch {
case decpt < -3 || decpt > precision:
m := digits[:1] + ".0"
if len(digits) > 1 {
m = digits[:1] + "." + digits[1:]
}
sign := "+"
if exp < 0 {
sign, exp = "-", -exp
}
out = m + "E" + sign + strconv.Itoa(exp)
case decpt <= 0:
out = "0." + strings.Repeat("0", -decpt) + digits
case decpt >= len(digits):
out = digits + strings.Repeat("0", decpt-len(digits))
default:
out = digits[:decpt] + "." + digits[decpt:]
}
sign := "+"
if exp < 0 {
sign = "-"
exp = -exp
if f < 0 {
out = "-" + out
}
return mant + "E" + sign + strconv.Itoa(exp)
return out
}
// phpTrim trims the characters PHP's trim() does.