diff --git a/pkg/expression/builtin_cast.go b/pkg/expression/builtin_cast.go index b623a525cfc5c..70121f37c0862 100644 --- a/pkg/expression/builtin_cast.go +++ b/pkg/expression/builtin_cast.go @@ -23,6 +23,7 @@ package expression import ( + "bytes" "fmt" "math" "strconv" @@ -1033,6 +1034,39 @@ func (b *builtinCastRealAsStringSig) Clone() builtinFunc { return newSig } +func formatFloat(f float64, bitSize int) string { + // MySQL makes float to string max 6 significant digits + // MySQL makes double to string max 17 significant digits + + // Unified to display behavior of TiDB. TiKV should also follow this rule. + const ( + expFormatBig = 1e15 + expFormatSmall = 1e-15 + ) + + absVal := math.Abs(f) + isEFormat := false + + if bitSize == 32 { + isEFormat = float32(absVal) >= float32(expFormatBig) || (float32(absVal) != 0 && float32(absVal) < float32(expFormatSmall)) + } else { + isEFormat = absVal >= expFormatBig || (absVal != 0 && absVal < expFormatSmall) + } + + dst := make([]byte, 0, 24) + if isEFormat { + dst = strconv.AppendFloat(dst, f, 'e', -1, bitSize) + if idx := bytes.IndexByte(dst, '+'); idx != -1 { + copy(dst[idx:], dst[idx+1:]) + dst = dst[:len(dst)-1] + } + } else { + dst = strconv.AppendFloat(dst, f, 'f', -1, bitSize) + } + + return string(dst) +} + func (b *builtinCastRealAsStringSig) evalString(ctx EvalContext, row chunk.Row) (res string, isNull bool, err error) { val, isNull, err := b.args[0].EvalReal(ctx, row) if isNull || err != nil { @@ -1046,7 +1080,7 @@ func (b *builtinCastRealAsStringSig) evalString(ctx EvalContext, row chunk.Row) // If we strconv.FormatFloat the value with 64bits, the result is incorrect! bits = 32 } - res, err = types.ProduceStrWithSpecifiedTp(strconv.FormatFloat(val, 'f', -1, bits), b.tp, typeCtx(ctx), false) + res, err = types.ProduceStrWithSpecifiedTp(formatFloat(val, bits), b.tp, typeCtx(ctx), false) if err != nil { return res, false, err } diff --git a/pkg/expression/builtin_cast_test.go b/pkg/expression/builtin_cast_test.go index 606151c46f737..2bb01914459aa 100644 --- a/pkg/expression/builtin_cast_test.go +++ b/pkg/expression/builtin_cast_test.go @@ -1116,6 +1116,61 @@ func TestCastFuncSig(t *testing.T) { require.False(t, iRes.IsNull()) require.Equal(t, types.KindInt64, iRes.Kind()) require.Equal(t, int64(0), iRes.GetInt64()) + + // test cast real to string + zero := float32(0.00) + negZero := -zero + castFloat32ToStringCases := []struct { + v float32 + expect string + }{ + {negZero, "-0"}, + {00000.0, "0"}, + {4474.78125, "4474.7812"}, + {1e15, "1e15"}, + {-1e15, "-1e15"}, + {9.99999e14, "999999000000000"}, + {-9.99999e14, "-999999000000000"}, + {1e15 - 1.0, "1e15"}, + {-3.4028235e38, "-3.4028235e38"}, + {3.4028235e38, "3.4028235e38"}, + {1.1754944e-38, "1.1754944e-38"}, + {1.000, "1"}, + {-123456789123000.0, "-123456790000000"}, + {1e-15, "0.000000000000001"}, + {9.9999e-16, "9.9999e-16"}, + {1.23456789123000e-9, "0.0000000012345679"}, + } + + for _, d := range castFloat32ToStringCases { + require.Equal(t, formatFloat(float64(d.v), 32), d.expect) + } + + castFloat64ToStringCases := []struct { + v float64 + expect string + }{ + {float64(negZero), "-0"}, + {00000.0, "0"}, + {4474.78125, "4474.78125"}, + {1e15, "1e15"}, + {-1e15, "-1e15"}, + {9.99999e14, "999999000000000"}, + {-9.99999e14, "-999999000000000"}, + {1e15 - 1.0, "999999999999999"}, + {-1.7976931348623157e308, "-1.7976931348623157e308"}, + {1.7976931348623157e308, "1.7976931348623157e308"}, + {2.2250738585072014e-308, "2.2250738585072014e-308"}, + {1.000, "1"}, + {-123456789123000.0, "-123456789123000"}, + {1e-15, "0.000000000000001"}, + {9.9999e-16, "9.9999e-16"}, + {1.23456789123000e-9, "0.00000000123456789123"}, + } + + for _, d := range castFloat64ToStringCases { + require.Equal(t, formatFloat(d.v, 64), d.expect) + } } func TestCastJSONAsDecimalSig(t *testing.T) { diff --git a/pkg/expression/builtin_cast_vec.go b/pkg/expression/builtin_cast_vec.go index bce2383171338..0ffe672b7f0e1 100644 --- a/pkg/expression/builtin_cast_vec.go +++ b/pkg/expression/builtin_cast_vec.go @@ -221,7 +221,7 @@ func (b *builtinCastRealAsStringSig) vecEvalString(ctx EvalContext, input *chunk result.AppendNull() continue } - res, err = types.ProduceStrWithSpecifiedTp(strconv.FormatFloat(v, 'f', -1, bits), b.tp, tc, false) + res, err = types.ProduceStrWithSpecifiedTp(formatFloat(v, bits), b.tp, tc, false) if err != nil { return err }