Skip to content

Commit ad2828f

Browse files
committed
[fix](decimal) Fix scientific string cast to decimal and rounding for very small scientific-notation values
Problems: 1. String-to-decimal casting counted exponent characters as significand digits, so values such as "1.4E+2" could miss the exponent scale and return 14 instead of 140. 2. String-to-decimal parsing rounded scientific-notation values up even when implicit zeros placed the significant digit beyond the first discarded decimal scale position. Also add comments to help understand the code.
1 parent 9ba7e04 commit ad2828f

3 files changed

Lines changed: 77 additions & 8 deletions

File tree

be/src/util/string_parser.cpp

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ namespace doris {
4141
// <exponent> ::= <e_marker> <sign>? <digits>
4242
//
4343
// <e_marker> ::= "e" | "E"
44+
//
45+
// Parsing algorithm:
46+
// 1. Trim spaces and the sign, then normalize the significand by skipping leading zeros and an
47+
// optional leading dot. During this scan, count digits that belong to the original integral
48+
// part (`int_part_count`) and remember where the significand ends (`end_digit_index`).
49+
// 2. Parse the optional exponent. Scientific notation is handled by moving the decimal point:
50+
// `result_int_part_digit_count = int_part_count + exponent`. For example, "12.34e-1" has
51+
// int_part_count=2 and exponent=-1, so the result has one integral digit: "1.234".
52+
// 3. Build the result in scaled-integer form: first collect the integral digits up to the shifted
53+
// decimal point, then collect up to `type_scale` fractional digits, padding with zeros when the
54+
// input has fewer fractional digits than the target scale.
55+
// 4. If there are extra fractional digits, round half up using the first discarded digit. Finally,
56+
// check the integral digit count against `type_precision - type_scale` and return the signed
57+
// scaled integer value.
4458
template <PrimitiveType P>
4559
typename PrimitiveTypeTraits<P>::CppType::NativeType StringParser::string_to_decimal(
4660
const char* __restrict s, size_t len, int type_precision, int type_scale,
@@ -50,6 +64,16 @@ typename PrimitiveTypeTraits<P>::CppType::NativeType StringParser::string_to_dec
5064
std::is_same_v<T, __int128> || std::is_same_v<T, wide::Int256>,
5165
"Cast string to decimal only support target type int32_t, int64_t, __int128 or "
5266
"wide::Int256.");
67+
68+
// Parse in two logical coordinate systems:
69+
// 1. `s[0, end_digit_index)` is the normalized significand after trimming spaces, sign and
70+
// leading zeros. If the original value starts with '.', the dot is also skipped so
71+
// ".14E+3" is parsed as significand "14" with exponent 3.
72+
// 2. `result_int_part_digit_count = int_part_count + exponent` is the decimal point position
73+
// after applying scientific notation. For example, "1.4E+2" has int_part_count=1,
74+
// exponent=2, result_int_part_digit_count=3, so "14" becomes integer 140.
75+
// `digit_index` always indexes the normalized significand string, which may still contain a
76+
// dot for inputs like "1.4E+2"; loops that build numbers skip that dot explicitly.
5377
// Ignore leading and trailing spaces.
5478
s = skip_ascii_whitespaces(s, len);
5579

@@ -102,7 +126,9 @@ typename PrimitiveTypeTraits<P>::CppType::NativeType StringParser::string_to_dec
102126
*result = StringParser::PARSE_FAILURE;
103127
return 0;
104128
}
105-
// parse exponent if any
129+
// Parse exponent if any. Keep `end_digit_index` before consuming 'e/E' so later digit counts
130+
// ignore exponent syntax. For "1.4E+2", end_digit_index points just after "1.4", not after
131+
// "E+2".
106132
int64_t exponent = 0;
107133
auto end_digit_index = i;
108134
if (i != len) {
@@ -149,8 +175,6 @@ typename PrimitiveTypeTraits<P>::CppType::NativeType StringParser::string_to_dec
149175
return 0;
150176
}
151177
}
152-
T int_part_number = 0;
153-
T frac_part_number = 0;
154178
// TODO: check limit values of exponent and add UT
155179
// max string len is config::string_type_length_soft_limit_bytes,
156180
// whose max value is std::numeric_limits<int32_t>::max() - 4,
@@ -163,9 +187,15 @@ typename PrimitiveTypeTraits<P>::CppType::NativeType StringParser::string_to_dec
163187
return 0;
164188
}
165189
int result_int_part_digit_count = tmp_result_int_part_digit_count;
190+
T int_part_number = 0;
191+
T frac_part_number = 0;
166192
int actual_frac_part_count = 0;
167193
int digit_index = 0;
168194
if (result_int_part_digit_count >= 0) {
195+
// `max_index` is the raw significand index where integer-part digits stop. Add one extra
196+
// raw character only when crossing an in-buffer dot, e.g. "1.4E+2" must scan "1.4" to
197+
// collect three integer digits after the exponent shift. It is capped by end_digit_index
198+
// because missing digits are appended later by multiplying with powers of 10.
169199
int max_index = std::min(found_dot ? (result_int_part_digit_count +
170200
((int_part_count > 0 && exponent > 0) ? 1 : 0))
171201
: result_int_part_digit_count,
@@ -188,7 +218,11 @@ typename PrimitiveTypeTraits<P>::CppType::NativeType StringParser::string_to_dec
188218
}
189219
int_part_number = int_part_number * 10 + (s[digit_index] - '0');
190220
}
191-
auto total_significant_digit_count = i - ((found_dot && int_part_count > 0) ? 1 : 0);
221+
// Count only significand digits, not exponent syntax. If the exponent moves the decimal
222+
// point past all available significant digits, append zeros by scaling the integer part:
223+
// "1.4E+2" scans integer 14, total_significant_digit_count=2, then multiplies by 10.
224+
auto total_significant_digit_count =
225+
end_digit_index - ((found_dot && int_part_count > 0) ? 1 : 0);
192226
if (result_int_part_digit_count > total_significant_digit_count) {
193227
int_part_number *= get_scale_multiplier<T>(result_int_part_digit_count -
194228
total_significant_digit_count);
@@ -206,8 +240,11 @@ typename PrimitiveTypeTraits<P>::CppType::NativeType StringParser::string_to_dec
206240
++actual_frac_part_count;
207241
}
208242
auto type_scale_multiplier = get_scale_multiplier<T>(type_scale);
209-
// there are still extra fraction digits left, check rounding
210-
if (digit_index != end_digit_index) {
243+
// Round only when the next parsed significand digit is exactly the first discarded fractional
244+
// digit. If `actual_frac_part_count` is already greater than type_scale, the missing positions
245+
// are implicit zeros from a negative exponent, so "5e-17" to scale 15 must stay 0 instead of
246+
// rounding up.
247+
if (actual_frac_part_count == type_scale && digit_index != end_digit_index) {
211248
if (UNLIKELY(s[digit_index] == '.')) {
212249
++digit_index;
213250
}

be/test/core/data_type/decimal_test.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ TEST(DecimalTest, string_parser) {
157157
EXPECT_EQ(result, StringParser::PARSE_SUCCESS);
158158
EXPECT_EQ(value, dec_max.value);
159159
}
160+
160161
TEST(DecimalTest, crc32) {
161162
PrimitiveType type = PrimitiveType::TYPE_DECIMAL256;
162163
DataTypeDecimal256 data_type(76, 10);
@@ -312,4 +313,4 @@ TEST(DecimalTest, to_string) {
312313
"0.9999999999999999999999999999999999999999999999999999999999999999999999999999");
313314
}
314315
}
315-
} // namespace doris
316+
} // namespace doris

be/test/exprs/function/cast/cast_to_decimal.cpp

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,37 @@ TEST_F(FunctionCastToDecimalTest, test_from_string_invalid_input) {
8888
int table_index = 0;
8989
from_string_invalid_input_test_func<Decimal32>(9, 3, table_index++);
9090
}
91+
92+
TEST_F(FunctionCastToDecimalTest, test_from_string_scientific_notation) {
93+
InputTypeSet input_types = {PrimitiveType::TYPE_VARCHAR};
94+
DataSet data_set = {
95+
{{std::string("1.4E+2")}, DECIMAL128V3(140, 0, 15)},
96+
{{std::string(".14E+3")}, DECIMAL128V3(140, 0, 15)},
97+
{{std::string("0.001E+5")}, DECIMAL128V3(100, 0, 15)},
98+
{{std::string("1.E+2")}, DECIMAL128V3(100, 0, 15)},
99+
{{std::string("1.4E+0")}, DECIMAL128V3(1, 400000000000000, 15)},
100+
{{std::string("1.4E-2")}, DECIMAL128V3(0, 14000000000000, 15)},
101+
};
102+
check_function_for_cast<DataTypeDecimal<Decimal128V3::PType>>(input_types, data_set, 15, 38);
103+
}
104+
105+
TEST(FunctionCastToDecimalTest, string_parser_scientific_rounding) {
106+
auto parse_decimal128 = [](std::string_view value) {
107+
StringParser::ParseResult result = StringParser::PARSE_SUCCESS;
108+
auto parsed = StringParser::string_to_decimal<TYPE_DECIMAL128I>(value.data(), value.size(),
109+
38, 15, &result);
110+
EXPECT_EQ(result, StringParser::PARSE_SUCCESS);
111+
return parsed;
112+
};
113+
114+
EXPECT_EQ(parse_decimal128("5e-16"), 1);
115+
EXPECT_EQ(parse_decimal128("5e-17"), 0);
116+
EXPECT_EQ(parse_decimal128("9e-17"), 0);
117+
EXPECT_EQ(parse_decimal128("-5e-17"), 0);
118+
EXPECT_EQ(parse_decimal128("0.0000000000000005"), 1);
119+
EXPECT_EQ(parse_decimal128("0.00000000000000005"), 0);
120+
}
121+
91122
TEST_F(FunctionCastToDecimalTest, test_from_bool) {
92123
from_bool_test_func<Decimal32>(9, 0);
93124
from_bool_test_func<Decimal32>(9, 1);
@@ -122,4 +153,4 @@ TEST_F(FunctionCastToDecimalTest, test_from_bool_overflow) {
122153
from_bool_overflow_test_func<Decimal128V3>();
123154
from_bool_overflow_test_func<Decimal256>();
124155
}
125-
} // namespace doris
156+
} // namespace doris

0 commit comments

Comments
 (0)