@@ -104,69 +104,83 @@ Literal LiteralCaster::AboveMaxLiteral(std::shared_ptr<PrimitiveType> type) {
104104 return Literal (Literal::AboveMax{}, std::move (type));
105105}
106106
107+ namespace {
108+
109+ // Rescale `unscaled` (interpreted at `from_scale`) to `to_scale` using HALF_UP rounding
110+ // (round half away from zero), matching Java's BigDecimal.setScale(scale, HALF_UP).
111+ // Unlike Decimal::Rescale, which only truncates and rejects any dropped remainder, this
112+ // rounds, and it supports the full negative..positive scale range Iceberg decimals allow.
113+ Result<Decimal> RescaleHalfUp (const Decimal& unscaled, int32_t from_scale,
114+ int32_t to_scale, bool negative) {
115+ const int32_t delta = to_scale - from_scale;
116+ if (delta == 0 ) {
117+ return unscaled;
118+ }
119+ if (delta > 0 ) {
120+ // Growing the scale multiplies by 10^delta and is exact; Rescale rejects overflow.
121+ if (delta > Decimal::kMaxScale ) {
122+ return InvalidArgument (" scale change {} exceeds the maximum {}" , delta,
123+ Decimal::kMaxScale );
124+ }
125+ return unscaled.Rescale (from_scale, to_scale);
126+ }
127+ // Shrinking the scale drops `drop` digits with HALF_UP rounding. A drop larger than the
128+ // digits any decimal can hold rounds everything away, so the result is zero (e.g.
129+ // 1e-100 to decimal(9, 2)); this also keeps the divisor within the powers-of-ten table.
130+ const int32_t drop = -delta;
131+ if (drop > Decimal::kMaxScale ) {
132+ return Decimal (0 );
133+ }
134+ ICEBERG_ASSIGN_OR_RAISE (auto divisor, Decimal (1 ).Rescale (0 , drop));
135+ ICEBERG_ASSIGN_OR_RAISE (auto divmod, unscaled.Divide (divisor));
136+ Decimal quotient = divmod.first ;
137+ Decimal remainder = Decimal::Abs (divmod.second );
138+ if (remainder * Decimal (2 ) >= divisor) {
139+ quotient += negative ? Decimal (-1 ) : Decimal (1 );
140+ }
141+ return quotient;
142+ }
143+
144+ } // namespace
145+
107146Result<Literal> LiteralCaster::CastIntegerToDecimal (
108147 int64_t value, const std::shared_ptr<PrimitiveType>& target_type) {
109148 const auto & decimal_type = internal::checked_cast<const DecimalType&>(*target_type);
110- const int32_t scale = decimal_type.scale ();
111- // DecimalType does not bound its scale, but Rescale indexes a powers-of-ten table sized
112- // for [0, kMaxScale]; reject an out-of-range scale here rather than reading past it.
113- if (scale < 0 || scale > Decimal::kMaxScale ) {
114- return InvalidArgument (" Cannot cast {} as a {} value" , value,
115- target_type->ToString ());
116- }
117- // An integer has scale 0, so scaling it to the target scale multiplies the unscaled
118- // value by 10^scale; Rescale rejects a target scale that would overflow the value.
119- ICEBERG_ASSIGN_OR_RAISE (auto decimal, Decimal (value).Rescale (0 , scale));
120- if (!decimal.FitsInPrecision (decimal_type.precision ())) {
149+ // An integer has scale 0; rescale it to the target scale, rounding HALF_UP when the
150+ // target scale is negative (matching Java's numeric-to-decimal default handling).
151+ ICEBERG_ASSIGN_OR_RAISE (auto unscaled, RescaleHalfUp (Decimal (value), /* from_scale=*/ 0 ,
152+ decimal_type.scale (), value < 0 ));
153+ if (!unscaled.FitsInPrecision (decimal_type.precision ())) {
121154 return InvalidArgument (" Cannot cast {} as a {} value" , value,
122155 target_type->ToString ());
123156 }
124- return Literal::Decimal (decimal .value (), decimal_type.precision (),
157+ return Literal::Decimal (unscaled .value (), decimal_type.precision (),
125158 decimal_type.scale ());
126159}
127160
128161Result<Literal> LiteralCaster::CastRealToDecimal (
129162 double value, const std::shared_ptr<PrimitiveType>& target_type) {
130163 const auto & decimal_type = internal::checked_cast<const DecimalType&>(*target_type);
131- const int32_t target_scale = decimal_type.scale ();
132- if (target_scale < 0 || target_scale > Decimal::kMaxScale ) {
133- return InvalidArgument (" Cannot cast {} as a {} value" , value,
134- target_type->ToString ());
135- }
136164 if (!std::isfinite (value)) {
137165 return InvalidArgument (" Cannot cast {} as a {} value" , value,
138166 target_type->ToString ());
139167 }
140168
141169 // Parse the shortest round-tripping decimal representation of the value (matching
142170 // Java's BigDecimal.valueOf(double), which goes through Double.toString) into a
143- // full-precision decimal, then round to the target scale below .
171+ // full-precision decimal, then round to the target scale.
144172 int32_t parsed_scale = 0 ;
145173 ICEBERG_ASSIGN_OR_RAISE (
146174 auto parsed, Decimal::FromString (std::format (" {}" , value), nullptr , &parsed_scale));
147175
148- Decimal unscaled = parsed;
149- if (parsed_scale > target_scale) {
150- // Drop excess fractional digits with HALF_UP rounding (round half away from zero, as
151- // Java does), since Rescale itself only truncates and rejects any dropped remainder.
152- const int32_t drop = parsed_scale - target_scale;
153- ICEBERG_ASSIGN_OR_RAISE (auto divisor, Decimal (1 ).Rescale (0 , drop));
154- ICEBERG_ASSIGN_OR_RAISE (auto divmod, parsed.Divide (divisor));
155- Decimal quotient = divmod.first ;
156- Decimal remainder = Decimal::Abs (divmod.second );
157- if (remainder * Decimal (2 ) >= divisor) {
158- quotient += (value < 0 ) ? Decimal (-1 ) : Decimal (1 );
159- }
160- unscaled = quotient;
161- } else if (parsed_scale < target_scale) {
162- ICEBERG_ASSIGN_OR_RAISE (unscaled, parsed.Rescale (parsed_scale, target_scale));
163- }
164-
176+ ICEBERG_ASSIGN_OR_RAISE (auto unscaled, RescaleHalfUp (parsed, parsed_scale,
177+ decimal_type.scale (), value < 0 ));
165178 if (!unscaled.FitsInPrecision (decimal_type.precision ())) {
166179 return InvalidArgument (" Cannot cast {} as a {} value" , value,
167180 target_type->ToString ());
168181 }
169- return Literal::Decimal (unscaled.value (), decimal_type.precision (), target_scale);
182+ return Literal::Decimal (unscaled.value (), decimal_type.precision (),
183+ decimal_type.scale ());
170184}
171185
172186Result<Literal> LiteralCaster::CastFromInt (
0 commit comments