Skip to main content

drizzle_core/
conv.rs

1//! Runtime value-conversion helpers shared across dialects.
2//!
3//! Helpers here are intentionally small, dialect-agnostic primitives used by
4//! per-dialect `FromXValue` implementations.
5
6use crate::error::DrizzleError;
7use crate::prelude::format;
8
9/// Convert an `f64` database value to an integer type `T`, rejecting
10/// non-finite, non-integral, and out-of-range inputs with a descriptive error.
11///
12/// The `type_name` is embedded in the error for context (e.g. `"i64"`, `"i32"`).
13///
14/// # Errors
15///
16/// Returns [`DrizzleError::ConversionError`] when:
17/// - `value` is not finite (NaN, ±∞).
18/// - `value` has a non-zero fractional part.
19/// - `value` is outside the representable range of `T` (or of `i128`).
20pub fn checked_float_to_int<T>(value: f64, type_name: &str) -> Result<T, DrizzleError>
21where
22    T: TryFrom<i128>,
23    <T as TryFrom<i128>>::Error: core::fmt::Display,
24{
25    if !value.is_finite() {
26        return Err(DrizzleError::ConversionError(
27            format!("cannot convert non-finite float {value} to {type_name}").into(),
28        ));
29    }
30
31    if value % 1.0 != 0.0 {
32        return Err(DrizzleError::ConversionError(
33            format!("cannot convert non-integer float {value} to {type_name}").into(),
34        ));
35    }
36
37    // `2^127` is exactly representable in f64 and equals `i128::MAX + 1`
38    // (and `-2^127` equals `i128::MIN`). Building the bounds directly in f64
39    // space avoids a precision-losing `i128 as f64` cast. The bit pattern
40    // encodes sign=0, biased_exp=127+1023=1150, mantissa=0.
41    const TWO_POW_127: f64 = f64::from_bits(0x47E0_0000_0000_0000);
42    let two_pow_127 = TWO_POW_127;
43    if value < -two_pow_127 || value >= two_pow_127 {
44        return Err(DrizzleError::ConversionError(
45            format!("float {value} out of range for {type_name}").into(),
46        ));
47    }
48
49    // Safe: `value` is finite, integer-valued, and strictly within `[-2^127, 2^127)`.
50    // Round-trip through a decimal string to avoid an f64-as-i128 cast warning.
51    let int_value: i128 = format!("{value:.0}").parse().map_err(|e| {
52        DrizzleError::ConversionError(
53            format!("float {value} could not be parsed as integer for {type_name}: {e}").into(),
54        )
55    })?;
56    int_value.try_into().map_err(|e| {
57        DrizzleError::ConversionError(
58            format!("float {value} out of range for {type_name}: {e}").into(),
59        )
60    })
61}