@@ -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.
4458template <PrimitiveType P>
4559typename 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 }
0 commit comments