use std::sync::Arc;
use arrow::datatypes::{Decimal32Type, Decimal64Type, Decimal128Type, DecimalType};
use super::{contains_nan, float_scalar_range, integral_range, libcudf_cast, round_half_up_impl, scalar_raw_range};
use crate::{
core::{
column::{Column, ColumnView, copy_view_in},
error::{Error, ErrorCode, Result},
scalar::{Scalar, ScalarValue},
types::{DataType, TypeId},
},
ops::{
binary::{self, BinaryOperator},
reduction,
},
runtime::execution_domain::ExecutionDomain,
};
const I128_EXCLUSIVE_BOUND_AS_F64: f64 = i128::MAX as f64;
struct DecimalTarget {
id: TypeId,
scale: i32,
precision: u8,
storage_precision: u8,
max_raw: u128,
}
#[derive(Clone, Copy)]
enum Source {
Integer,
Float,
Decimal {
scale: i32,
},
}
fn storage_precision(id: TypeId) -> Option<u8> {
match id {
TypeId::Decimal32 => Some(Decimal32Type::MAX_PRECISION),
TypeId::Decimal64 => Some(Decimal64Type::MAX_PRECISION),
TypeId::Decimal128 => Some(Decimal128Type::MAX_PRECISION),
_ => None,
}
}
fn pow10(exponent: u32) -> Option<i128> {
10i128.checked_pow(exponent)
}
fn divide_rounding_half_away(raw: i128, divisor: i128) -> i128 {
let quotient = raw / divisor;
let remainder = raw % divisor;
let half = divisor / 2;
if raw >= 0 && remainder >= half {
quotient + 1
} else if raw < 0 && remainder <= -half {
quotient - 1
} else {
quotient
}
}
impl DecimalTarget {
fn new(output_type: &DataType, precision: u8, operation: &'static str) -> Result<Self> {
let id = output_type.id();
let Some(storage_precision) = storage_precision(id) else {
return Err(Error::new_with(
ErrorCode::UnsupportedDtype,
operation,
"a decimal precision applies only to a decimal cast target",
)
.with_detail("reason", "decimal_precision_requires_decimal_target")
.with_detail("target_type_id", id.name()));
};
if precision == 0 || precision > storage_precision {
return Err(Error::new_with(
ErrorCode::UnsupportedDtype,
operation,
"decimal precision is outside the range of the target storage type",
)
.with_detail("reason", "invalid_decimal_precision")
.with_detail("target_type_id", id.name())
.with_detail("target_precision", precision.to_string())
.with_detail("storage_precision", storage_precision.to_string()));
}
Ok(Self {
id,
scale: output_type.scale().saturating_neg(),
precision,
storage_precision,
max_raw: 10u128.pow(u32::from(precision)) - 1,
})
}
fn contains(&self, raw: i128) -> bool {
raw.unsigned_abs() <= self.max_raw
}
fn scale_factor_fits(&self, source: Source) -> bool {
let storage_precision = i64::from(self.storage_precision);
match source {
Source::Integer => i64::from(self.scale).abs() <= storage_precision,
Source::Decimal { scale } => i64::from(self.scale) - i64::from(scale) <= storage_precision,
Source::Float => true,
}
}
fn rescaled(&self, source: Source, raw: i128) -> Option<i128> {
let source_scale = match source {
Source::Decimal { scale } => scale,
Source::Integer | Source::Float => 0,
};
let delta = i64::from(self.scale) - i64::from(source_scale);
let digits = u32::try_from(delta.unsigned_abs()).ok()?;
if delta >= 0 {
return raw.checked_mul(pow10(digits)?);
}
let Some(divisor) = pow10(digits) else {
return Some(0);
};
Some(match source {
Source::Integer => raw / divisor,
Source::Decimal { .. } | Source::Float => divide_rounding_half_away(raw, divisor),
})
}
fn accepts(&self, source: Source, raw: i128) -> bool {
self.rescaled(source, raw).is_some_and(|rescaled| self.contains(rescaled))
}
fn accepts_float(&self, value: f64) -> bool {
let scaled = (10_f64.powi(self.scale) * value).round();
scaled.abs() < I128_EXCLUSIVE_BOUND_AS_F64 && self.contains(scaled as i128)
}
}
impl Source {
fn of(input_type: &DataType) -> Option<Self> {
let id = input_type.id();
if id.is_float() {
Some(Self::Float)
} else if id.is_decimal() {
Some(Self::Decimal { scale: input_type.scale().saturating_neg() })
} else {
integral_range(id).map(|_| Self::Integer)
}
}
fn storage_raw_range(self, id: TypeId) -> Option<(i128, i128)> {
match self {
Self::Integer => integral_range(id),
Self::Decimal { .. } => {
let bound = pow10(u32::from(storage_precision(id)?))? - 1;
Some((-bound, bound))
},
Self::Float => None,
}
}
}
fn out_of_range_error(
operation: &'static str,
source_type: &DataType,
target: &DecimalTarget,
reason: &'static str,
) -> Error {
Error::new_with(
ErrorCode::ValueOutOfRange,
operation,
"cast input cannot be represented in the target decimal precision",
)
.with_detail("reason", reason)
.with_detail("source_type_id", source_type.id().name())
.with_detail("target_type_id", target.id.name())
.with_detail("target_precision", target.precision.to_string())
.with_detail("target_scale", target.scale.to_string())
}
fn validate_input(
input: &ColumnView<'_>,
input_type: &DataType,
source: Source,
target: &DecimalTarget,
output_domain: &Arc<ExecutionDomain>,
exec_options: &cudf_sys::cudf_exec_options_t,
operation: &'static str,
) -> Result<()> {
let out_of_range = |reason: &'static str| out_of_range_error(operation, input_type, target, reason);
if !target.scale_factor_fits(source) {
return Err(out_of_range("decimal_scale_overflow"));
}
if let Some((low, high)) = source.storage_raw_range(input_type.id())
&& target.accepts(source, low)
&& target.accepts(source, high)
{
return Ok(());
}
if matches!(source, Source::Float) && contains_nan(input, output_domain, exec_options, operation)? {
return Err(out_of_range("nan"));
}
let (min_scalar, max_scalar) = reduction::minmax_impl(input, output_domain, exec_options, operation)?;
if !min_scalar.is_valid()? {
return Ok(());
}
let fits = if matches!(source, Source::Float) {
let (min, max) = float_scalar_range(input_type.id(), &min_scalar, &max_scalar)?;
target.accepts_float(min) && target.accepts_float(max)
} else {
let (min, max) = scalar_raw_range(input_type.id(), &min_scalar, &max_scalar)?;
target.accepts(source, min) && target.accepts(source, max)
};
if fits { Ok(()) } else { Err(out_of_range("decimal_precision_overflow")) }
}
pub(crate) fn cast_to_decimal_impl(
input: &ColumnView<'_>,
output_type: &DataType,
precision: u8,
output_domain: &Arc<ExecutionDomain>,
exec_options: &cudf_sys::cudf_exec_options_t,
operation: &'static str,
) -> Result<Column> {
let target = DecimalTarget::new(output_type, precision, operation)?;
let input_type = input.data_type()?;
let Some(source) = Source::of(&input_type) else {
return libcudf_cast(input, &input_type, output_type, output_domain, exec_options, operation);
};
validate_input(input, &input_type, source, &target, output_domain, exec_options, operation)?;
match source {
Source::Float => {
float_to_decimal(input, &input_type, output_type, &target, output_domain, exec_options, operation)
},
Source::Decimal { scale } if scale > target.scale => {
let rounded = round_half_up_impl(input, target.scale, output_domain, exec_options, operation)?;
if input_type.id() == output_type.id() {
return Ok(rounded);
}
let rounded_type = DataType::fixed_point(input_type.id(), output_type.scale())?;
libcudf_cast(&rounded.view()?, &rounded_type, output_type, output_domain, exec_options, operation)
},
_ if input_type == *output_type => {
copy_view_in(input, output_domain, exec_options).map_err(|error| error.reanchor(operation))
},
_ => libcudf_cast(input, &input_type, output_type, output_domain, exec_options, operation),
}
}
fn float_to_decimal(
input: &ColumnView<'_>,
input_type: &DataType,
output_type: &DataType,
target: &DecimalTarget,
output_domain: &Arc<ExecutionDomain>,
exec_options: &cudf_sys::cudf_exec_options_t,
operation: &'static str,
) -> Result<Column> {
let float64 = DataType::new(TypeId::Float64);
let widened;
let widened_view;
let values = if input_type.id() == TypeId::Float64 {
input
} else {
widened = libcudf_cast(input, input_type, &float64, output_domain, exec_options, operation)?;
widened_view = widened.view()?;
&widened_view
};
let factor = Scalar::from_value_in_domain(10_f64.powi(target.scale), output_domain)?;
let scaled = binary::binary_operation_vs_impl(
values,
&factor,
BinaryOperator::Mul,
&float64,
output_domain,
exec_options,
operation,
)?;
let rounded = round_half_up_impl(&scaled.view()?, 0, output_domain, exec_options, operation)?;
let whole_type = DataType::fixed_point(TypeId::Decimal128, 0)?;
let whole = libcudf_cast(&rounded.view()?, &float64, &whole_type, output_domain, exec_options, operation)?;
let unit = ScalarValue::decimal(1, DataType::fixed_point(TypeId::Decimal128, target.scale)?)
.and_then(|unit| Scalar::from_scalar_value_in_domain(&unit, output_domain))?;
let wide_type = DataType::fixed_point(TypeId::Decimal128, output_type.scale())?;
let wide = binary::binary_operation_vs_impl(
&whole.view()?,
&unit,
BinaryOperator::Div,
&wide_type,
output_domain,
exec_options,
operation,
)?;
if output_type.id() == TypeId::Decimal128 {
return Ok(wide);
}
libcudf_cast(&wide.view()?, &wide_type, output_type, output_domain, exec_options, operation)
}