use rust_decimal::prelude::ToPrimitive;
use selene_core::Value;
use crate::{
SourceSpan,
runtime::{DataExceptionSubclass, ExecutorError},
};
use super::{
invalid_character, non_iso_combination, non_iso_static_source_for_target,
numeric_text::classify_signed_numeric_text,
};
#[derive(Clone, Copy)]
pub(super) enum FloatTarget {
F32,
F64,
}
impl FloatTarget {
const fn name(self) -> &'static str {
match self {
Self::F32 => "FLOAT32",
Self::F64 => "FLOAT",
}
}
fn value(self, value: f64, span: SourceSpan) -> Result<Value, ExecutorError> {
match self {
Self::F64 => Ok(Value::Float(value)),
Self::F32 => {
#[allow(clippy::cast_possible_truncation)]
let narrowed = value as f32;
if value.is_finite() && !narrowed.is_finite() {
return Err(float32_out_of_range(span));
}
Ok(Value::Float32(narrowed))
}
}
}
}
pub(super) fn cast_to_float(
value: Value,
target: FloatTarget,
span: SourceSpan,
) -> Result<Value, ExecutorError> {
match value {
Value::Float(value) => target.value(value, span),
#[allow(clippy::cast_precision_loss)]
Value::Int(value) => target.value(value as f64, span),
#[allow(clippy::cast_precision_loss)]
Value::Uint(value) => target.value(value as f64, span),
#[allow(clippy::cast_precision_loss)]
Value::Int128(value) => target.value(value as f64, span),
#[allow(clippy::cast_precision_loss)]
Value::Uint128(value) => target.value(value as f64, span),
Value::Float32(value) => target.value(f64::from(value), span),
Value::Decimal(value) => decimal_to_float(value, target, span),
Value::String(value) => string_to_float(value.as_str(), target, span),
Value::Bool(_) => Err(non_iso_combination(
"CAST from BOOLEAN to a numeric type is not a valid type combination",
span,
)),
other => Err(
non_iso_static_source_for_target(&other, target.name(), span).unwrap_or(
ExecutorError::FeatureNotSupportedYet {
feature: "CAST source not supported for FLOAT target",
span,
},
),
),
}
}
fn decimal_to_float(
value: rust_decimal::Decimal,
target: FloatTarget,
span: SourceSpan,
) -> Result<Value, ExecutorError> {
value
.to_f64()
.ok_or_else(|| float_out_of_range(span))
.and_then(|value| target.value(value, span))
}
fn string_to_float(
text: &str,
target: FloatTarget,
span: SourceSpan,
) -> Result<Value, ExecutorError> {
let numeric = classify_signed_numeric_text(text, target.name(), span)?;
let image = numeric.image();
if is_non_numeric_float_token(image) {
return Err(invalid_character(text, target.name(), span));
}
let value = image
.parse::<f64>()
.map_err(|_| invalid_character(text, target.name(), span))?;
if value.is_nan() {
return Err(invalid_character(text, target.name(), span));
}
if value.is_infinite() {
return Err(string_float_out_of_range(target, span));
}
target.value(value, span)
}
fn is_non_numeric_float_token(image: &str) -> bool {
let unsigned = match image.as_bytes().first().copied() {
Some(b'+' | b'-') => &image[1..],
_ => image,
};
unsigned.eq_ignore_ascii_case("nan")
|| unsigned.eq_ignore_ascii_case("inf")
|| unsigned.eq_ignore_ascii_case("infinity")
}
fn float_out_of_range(span: SourceSpan) -> ExecutorError {
ExecutorError::data_exception(
DataExceptionSubclass::NumericValueOutOfRange,
"DECIMAL value has no FLOAT image during CAST",
span,
)
}
fn string_float_out_of_range(target: FloatTarget, span: SourceSpan) -> ExecutorError {
ExecutorError::data_exception(
DataExceptionSubclass::NumericValueOutOfRange,
format!("STRING value exceeds {} range during CAST", target.name()),
span,
)
}
fn float32_out_of_range(span: SourceSpan) -> ExecutorError {
ExecutorError::data_exception(
DataExceptionSubclass::NumericValueOutOfRange,
"numeric value exceeds FLOAT32 range during CAST",
span,
)
}