use rust_decimal::Decimal;
use rust_decimal::prelude::ToPrimitive;
use selene_core::{DecimalType, Value, round_decimal_to_type};
use crate::{
SourceSpan,
runtime::{DataExceptionSubclass, ExecutorError},
};
use super::{
non_iso_static_source_for_target,
numeric_text::{NumericText, classify_signed_numeric_text},
};
fn out_of_range(message: &'static str, span: SourceSpan) -> ExecutorError {
ExecutorError::data_exception(DataExceptionSubclass::NumericValueOutOfRange, message, span)
}
fn invalid_decimal_text(text: &str, span: SourceSpan) -> ExecutorError {
ExecutorError::data_exception(
DataExceptionSubclass::InvalidCharacterValueForCast,
format!("STRING value `{text}` is not a valid DECIMAL"),
span,
)
}
pub(super) fn numeric_to_decimal(value: Value, span: SourceSpan) -> Result<Value, ExecutorError> {
let dec = match value {
Value::Decimal(d) => d,
Value::Int(v) => Decimal::from(v),
Value::Uint(v) => Decimal::from(v),
Value::Int128(v) => Decimal::try_from_i128_with_scale(v, 0)
.map_err(|_| out_of_range("INT128 value exceeds DECIMAL range during CAST", span))?,
Value::Uint128(v) => {
let signed = i128::try_from(v).map_err(|_| {
out_of_range("UINT128 value exceeds DECIMAL range during CAST", span)
})?;
Decimal::try_from_i128_with_scale(signed, 0).map_err(|_| {
out_of_range("UINT128 value exceeds DECIMAL range during CAST", span)
})?
}
Value::Float(f) => float_to_decimal(f, span)?,
Value::Float32(f) => float_to_decimal(f64::from(f), span)?,
Value::String(s) => string_to_decimal(s.as_str(), span)?,
Value::Bool(_) => {
return Err(super::non_iso_combination(
"CAST from BOOLEAN to a numeric type is not a valid type combination",
span,
));
}
other => {
return Err(
non_iso_static_source_for_target(&other, "DECIMAL", span).unwrap_or(
ExecutorError::FeatureNotSupportedYet {
feature: "CAST source not supported for DECIMAL target",
span,
},
),
);
}
};
Ok(Value::Decimal(dec))
}
pub(super) fn numeric_to_decimal_exact(
value: Value,
decimal_type: DecimalType,
span: SourceSpan,
) -> Result<Value, ExecutorError> {
let Value::Decimal(decimal) = numeric_to_decimal(value, span)? else {
unreachable!("numeric_to_decimal returns Value::Decimal on success")
};
round_decimal_to_type(decimal, decimal_type)
.map(Value::Decimal)
.ok_or_else(|| {
out_of_range(
"DECIMAL value exceeds target precision after scale conversion",
span,
)
})
}
fn float_to_decimal(f: f64, span: SourceSpan) -> Result<Decimal, ExecutorError> {
if f.is_nan() {
return Err(ExecutorError::data_exception(
DataExceptionSubclass::InvalidCharacterValueForCast,
"CAST of NaN to DECIMAL has no representable image",
span,
));
}
Decimal::from_f64_retain(f)
.ok_or_else(|| out_of_range("FLOAT value exceeds DECIMAL range during CAST", span))
}
fn string_to_decimal(text: &str, span: SourceSpan) -> Result<Decimal, ExecutorError> {
match classify_signed_numeric_text(text, "DECIMAL", span)? {
NumericText::Integer(image) | NumericText::Decimal(image) => image
.parse::<Decimal>()
.map_err(|_| invalid_decimal_text(text, span)),
NumericText::Approximate(image) => image
.parse::<f64>()
.map_err(|_| invalid_decimal_text(text, span))
.and_then(|value| float_to_decimal(value, span)),
}
}
pub(super) fn decimal_to_int(dec: Decimal, span: SourceSpan) -> Result<Value, ExecutorError> {
dec.trunc()
.to_i64()
.map(Value::Int)
.ok_or_else(|| out_of_range("DECIMAL value exceeds INTEGER range during CAST", span))
}
pub(super) fn decimal_to_string(dec: &Decimal) -> String {
dec.to_string()
}
pub(super) fn u64_to_int(value: u64, span: SourceSpan) -> Result<Value, ExecutorError> {
i64::try_from(value)
.map(Value::Int)
.map_err(|_| out_of_range("UINT value exceeds INTEGER range during CAST", span))
}
pub(super) fn i128_to_int(value: i128, span: SourceSpan) -> Result<Value, ExecutorError> {
i64::try_from(value)
.map(Value::Int)
.map_err(|_| out_of_range("INT128 value exceeds INTEGER range during CAST", span))
}
pub(super) fn u128_to_int(value: u128, span: SourceSpan) -> Result<Value, ExecutorError> {
i64::try_from(value)
.map(Value::Int)
.map_err(|_| out_of_range("UINT128 value exceeds INTEGER range during CAST", span))
}
#[cfg(test)]
mod tests {
use super::*;
fn span() -> SourceSpan {
SourceSpan::default()
}
fn dec(literal: &str) -> Decimal {
literal.parse::<Decimal>().expect("valid decimal literal")
}
fn ok(value: Value) -> Value {
numeric_to_decimal(value, span()).expect("→ DECIMAL succeeds")
}
fn status(result: Result<Value, ExecutorError>) -> String {
result
.expect_err("rejected")
.gqlstatus()
.as_str()
.to_owned()
}
#[test]
fn int_to_decimal() {
assert_eq!(ok(Value::Int(42)), Value::Decimal(dec("42")));
}
#[test]
fn uint_to_decimal() {
assert_eq!(
ok(Value::Uint(u64::MAX)),
Value::Decimal(dec("18446744073709551615"))
);
}
#[test]
fn int128_to_decimal() {
assert_eq!(ok(Value::Int128(-123)), Value::Decimal(dec("-123")));
}
#[test]
fn uint128_to_decimal() {
assert_eq!(ok(Value::Uint128(123)), Value::Decimal(dec("123")));
}
#[test]
fn float_to_decimal() {
assert_eq!(ok(Value::Float(2.5)), Value::Decimal(dec("2.5")));
}
#[test]
fn float32_to_decimal() {
assert_eq!(ok(Value::Float32(2.5_f32)), Value::Decimal(dec("2.5")));
}
#[test]
fn string_to_decimal() {
assert_eq!(
ok(Value::String(selene_core::db_string("123.45").unwrap())),
Value::Decimal(dec("123.45"))
);
}
#[test]
fn int128_overflow_to_decimal_returns_22003() {
assert_eq!(
status(numeric_to_decimal(Value::Int128(i128::MAX), span())),
"22003"
);
}
#[test]
fn uint128_overflow_to_decimal_returns_22003() {
assert_eq!(
status(numeric_to_decimal(Value::Uint128(u128::MAX), span())),
"22003"
);
}
#[test]
fn float_infinity_to_decimal_returns_22003() {
assert_eq!(
status(numeric_to_decimal(Value::Float(f64::INFINITY), span())),
"22003"
);
}
#[test]
fn float_nan_to_decimal_returns_22018() {
assert_eq!(
status(numeric_to_decimal(Value::Float(f64::NAN), span())),
"22018"
);
}
#[test]
fn string_parse_fail_to_decimal_returns_22018() {
assert_eq!(
status(numeric_to_decimal(
Value::String(selene_core::db_string("abc").unwrap()),
span()
)),
"22018"
);
}
#[test]
fn decimal_to_integer_truncates_toward_zero() {
assert_eq!(decimal_to_int(dec("3.7"), span()).unwrap(), Value::Int(3));
assert_eq!(decimal_to_int(dec("-3.7"), span()).unwrap(), Value::Int(-3));
}
#[test]
fn decimal_overflow_to_integer_returns_22003() {
assert_eq!(
status(decimal_to_int(dec("100000000000000000000"), span())),
"22003"
);
}
#[test]
fn decimal_to_string_canonical() {
assert_eq!(decimal_to_string(&dec("123.450")), "123.450");
}
}