#![forbid(unsafe_code)]
use crate::geometry::traits::coordinate::{
CoordinateConversionError, CoordinateConversionValue, F64_MANTISSA_DIGITS,
InvalidCoordinateValue,
};
use num_traits::ToPrimitive;
#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum ValueConversionFailureReason {
#[error("target type rejected value")]
TargetTypeRejected,
}
#[derive(Clone, Debug, thiserror::Error, PartialEq)]
#[non_exhaustive]
pub enum ValueConversionError {
#[error("Cannot convert {value} from {from_type} to {to_type}: {reason}")]
ConversionFailed {
value: CoordinateConversionValue,
from_type: &'static str,
to_type: &'static str,
reason: ValueConversionFailureReason,
},
#[error("Cannot convert {value} from {from_type} to {to_type}: {source}")]
CoordinateConversion {
value: CoordinateConversionValue,
from_type: &'static str,
to_type: &'static str,
#[source]
source: Box<CoordinateConversionError>,
},
}
pub(super) fn safe_cast_to_f64(
value: f64,
coordinate_index: usize,
) -> Result<f64, CoordinateConversionError> {
if !value.is_finite() {
return Err(CoordinateConversionError::NonFiniteValue {
coordinate_index,
coordinate_value: InvalidCoordinateValue::from_debug(&value),
});
}
Ok(value)
}
pub(super) fn safe_cast_from_f64(
value: f64,
coordinate_index: usize,
) -> Result<f64, CoordinateConversionError> {
if !value.is_finite() {
return Err(CoordinateConversionError::NonFiniteValue {
coordinate_index,
coordinate_value: InvalidCoordinateValue::from_debug(&value),
});
}
Ok(value)
}
pub fn safe_coords_to_f64<const D: usize>(
coords: &[f64; D],
) -> Result<[f64; D], CoordinateConversionError> {
let mut result = [0.0_f64; D];
for (i, &coord) in coords.iter().enumerate() {
result[i] = safe_cast_to_f64(coord, i)?;
}
Ok(result)
}
pub fn safe_coords_from_f64<const D: usize>(
coords: &[f64; D],
) -> Result<[f64; D], CoordinateConversionError> {
let mut result = [0.0_f64; D];
for (i, &coord) in coords.iter().enumerate() {
result[i] = safe_cast_from_f64(coord, i)?;
}
Ok(result)
}
pub fn safe_scalar_to_f64(value: f64) -> Result<f64, CoordinateConversionError> {
safe_cast_to_f64(value, 0)
}
pub fn safe_scalar_from_f64(value: f64) -> Result<f64, CoordinateConversionError> {
safe_cast_from_f64(value, 0)
}
pub fn safe_usize_to_scalar(value: usize) -> Result<f64, CoordinateConversionError> {
let max_precise_u128: u128 = 1u128 << F64_MANTISSA_DIGITS;
let value_u64 =
u64::try_from(value).map_err(|_| CoordinateConversionError::ConversionFailed {
coordinate_index: 0,
coordinate_value: CoordinateConversionValue::from_usize(value),
from_type: "usize",
to_type: "f64",
})?;
if u128::from(value_u64) > max_precise_u128 {
return Err(CoordinateConversionError::ConversionFailed {
coordinate_index: 0,
coordinate_value: CoordinateConversionValue::from_usize(value),
from_type: "usize",
to_type: "f64",
});
}
value
.to_f64()
.ok_or_else(|| CoordinateConversionError::ConversionFailed {
coordinate_index: 0,
coordinate_value: CoordinateConversionValue::from_usize(value),
from_type: "usize",
to_type: "f64",
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::traits::coordinate::CoordinateConversionError;
use approx::assert_relative_eq;
use num_traits::cast;
use std::assert_matches;
#[test]
fn test_safe_usize_to_scalar_basic_success() {
let small_value = 42_usize;
let result: Result<f64, _> = safe_usize_to_scalar(small_value);
assert!(result.is_ok());
assert_relative_eq!(result.unwrap(), 42.0f64, epsilon = 1e-15);
}
#[test]
fn test_safe_usize_to_scalar_within_f64_precision() {
let safe_large_values = [
usize::try_from(1_u64 << 50).unwrap_or(usize::MAX), usize::try_from(1_u64 << 51).unwrap_or(usize::MAX), usize::try_from(1_u64 << 53).unwrap_or(usize::MAX), ];
for &value in &safe_large_values {
let result: Result<f64, _> = safe_usize_to_scalar(value);
assert!(result.is_ok(), "Failed to convert safe large value {value}");
let converted = result.unwrap();
let back_converted: usize =
cast(converted).expect("f64 should convert back to usize exactly");
assert_eq!(
back_converted, value,
"Conversion was not exact for {value}"
);
}
}
#[test]
fn test_safe_usize_to_scalar_precision_boundary() {
const MAX_PRECISE_USIZE_IN_F64: u64 = 1_u64 << 53;
if usize::try_from(MAX_PRECISE_USIZE_IN_F64).is_ok() {
let boundary_value = usize::try_from(MAX_PRECISE_USIZE_IN_F64).unwrap();
let result: Result<f64, _> = safe_usize_to_scalar(boundary_value);
assert!(result.is_ok(), "Boundary value 2^53 should be convertible");
let converted = result.unwrap();
let back_converted: usize =
cast(converted).expect("Boundary f64 should convert back to usize exactly");
assert_eq!(
back_converted, boundary_value,
"Boundary conversion should be exact"
);
}
}
#[test]
fn test_safe_usize_to_scalar_precision_loss_detection() {
const MAX_PRECISE_USIZE_IN_F64: u64 = 1_u64 << 53;
if std::mem::size_of::<usize>() >= 8 {
let precision_loss_values = [
usize::try_from(MAX_PRECISE_USIZE_IN_F64 + 1).unwrap_or(usize::MAX), usize::try_from(MAX_PRECISE_USIZE_IN_F64 + 100).unwrap_or(usize::MAX), ];
for &value in &precision_loss_values {
let value_u64 = u64::try_from(value).unwrap_or(u64::MAX);
if value_u64 > u64::try_from(usize::MAX).unwrap_or(u64::MAX) {
continue;
}
let result: Result<f64, _> = safe_usize_to_scalar(value);
assert!(
result.is_err(),
"Value {value} should fail conversion due to precision loss"
);
assert_matches!(
result,
Err(CoordinateConversionError::ConversionFailed {
coordinate_index: 0,
coordinate_value,
from_type: "usize",
to_type: "f64",
}) if coordinate_value == CoordinateConversionValue::from_usize(value)
);
}
}
}
#[test]
fn test_safe_usize_to_scalar_error_message_format() {
const MAX_PRECISE_USIZE_IN_F64: u64 = 1_u64 << 53;
if std::mem::size_of::<usize>() >= 8 {
let large_value = usize::try_from(MAX_PRECISE_USIZE_IN_F64 + 1).unwrap_or(usize::MAX);
if u64::try_from(large_value).unwrap_or(u64::MAX)
<= u64::try_from(usize::MAX).unwrap_or(u64::MAX)
{
let result: Result<f64, _> = safe_usize_to_scalar(large_value);
assert!(result.is_err());
let error_message = format!("{}", result.unwrap_err());
assert!(error_message.contains("Failed to convert"));
assert!(error_message.contains(&format!("{large_value}")));
assert!(error_message.contains("usize"));
assert!(error_message.contains("f64"));
}
}
}
#[test]
fn test_safe_usize_to_scalar_consistency_with_direct_cast() {
let safe_values = [0, 1, 42, 100, 1000, 10_000, 100_000];
for &value in &safe_values {
let safe_result: Result<f64, _> = safe_usize_to_scalar(value);
assert!(safe_result.is_ok());
let safe_converted = safe_result.unwrap();
let direct_cast: f64 = cast(value).expect("Small values should convert to f64 safely");
assert_relative_eq!(safe_converted, direct_cast, epsilon = 1e-15);
}
}
#[test]
fn test_safe_usize_to_scalar_platform_independence() {
tracing::debug!(
"Testing on platform with usize size: {} bytes",
std::mem::size_of::<usize>()
);
tracing::debug!("usize::MAX = {}", usize::MAX);
tracing::debug!("2^53 = {}", 1_u64 << 53);
let universal_safe_values = [0, 1, 100, 10000];
for &value in &universal_safe_values {
let result: Result<f64, _> = safe_usize_to_scalar(value);
assert!(
result.is_ok(),
"Universal safe value {value} should convert on any platform"
);
}
let _usize_max_u64 = u64::try_from(usize::MAX).unwrap_or(u64::MAX);
}
#[test]
fn test_safe_cast_to_f64_non_finite() {
let result = safe_cast_to_f64(f64::NAN, 0);
assert_matches!(
result,
Err(CoordinateConversionError::NonFiniteValue { .. })
);
let result = safe_cast_to_f64(f64::INFINITY, 1);
assert_matches!(
result,
Err(CoordinateConversionError::NonFiniteValue { .. })
);
let result = safe_cast_to_f64(f64::NEG_INFINITY, 2);
assert_matches!(
result,
Err(CoordinateConversionError::NonFiniteValue { .. })
);
let result = safe_cast_to_f64(42.5f64, 0);
assert!(result.is_ok());
assert_relative_eq!(result.unwrap(), 42.5);
}
#[test]
fn test_safe_cast_from_f64_non_finite() {
let result: Result<f64, _> = safe_cast_from_f64(f64::NAN, 0);
assert_matches!(
result,
Err(CoordinateConversionError::NonFiniteValue { .. })
);
let result: Result<f64, _> = safe_cast_from_f64(f64::INFINITY, 1);
assert_matches!(
result,
Err(CoordinateConversionError::NonFiniteValue { .. })
);
let result: Result<f64, _> = safe_cast_from_f64(f64::NEG_INFINITY, 2);
assert_matches!(
result,
Err(CoordinateConversionError::NonFiniteValue { .. })
);
let result: Result<f64, _> = safe_cast_from_f64(42.5, 0);
assert!(result.is_ok());
}
#[test]
fn test_safe_coords_conversion_with_non_finite() {
let coords_nan = [1.0, f64::NAN, 3.0];
let result = safe_coords_to_f64(&coords_nan);
assert_matches!(
result,
Err(CoordinateConversionError::NonFiniteValue { .. })
);
let coords_inf = [1.0, 2.0, f64::INFINITY];
let result = safe_coords_to_f64(&coords_inf);
assert_matches!(
result,
Err(CoordinateConversionError::NonFiniteValue { .. })
);
let coords_valid = [1.0, 2.0, 3.0];
let result = safe_coords_to_f64(&coords_valid);
assert!(result.is_ok());
assert_relative_eq!(
result.unwrap().as_slice(),
[1.0f64, 2.0f64, 3.0f64].as_slice()
);
}
#[test]
fn test_safe_coords_from_f64_with_non_finite() {
let coords_nan = [1.0f64, f64::NAN, 3.0f64];
let result: Result<[f64; 3], _> = safe_coords_from_f64(&coords_nan);
assert_matches!(
result,
Err(CoordinateConversionError::NonFiniteValue { .. })
);
let coords_inf = [1.0f64, 2.0f64, f64::INFINITY];
let result: Result<[f64; 3], _> = safe_coords_from_f64(&coords_inf);
assert_matches!(
result,
Err(CoordinateConversionError::NonFiniteValue { .. })
);
let coords_valid = [1.0f64, 2.0f64, 3.0f64];
let result: Result<[f64; 3], _> = safe_coords_from_f64(&coords_valid);
assert!(result.is_ok());
}
#[test]
fn test_safe_scalar_conversion_edge_cases() {
let result = safe_scalar_to_f64(f64::NAN);
assert_matches!(
result,
Err(CoordinateConversionError::NonFiniteValue { .. })
);
let result: Result<f64, _> = safe_scalar_from_f64(f64::NAN);
assert_matches!(
result,
Err(CoordinateConversionError::NonFiniteValue { .. })
);
let result = safe_scalar_to_f64(42.5);
assert!(result.is_ok());
assert_relative_eq!(result.unwrap(), 42.5f64);
let result: Result<f64, _> = safe_scalar_from_f64(42.5);
assert!(result.is_ok());
}
#[test]
fn test_safe_usize_to_scalar_precision_boundary_extended() {
const MAX_PRECISE: u64 = 1_u64 << 53;
let result: Result<f64, _> =
safe_usize_to_scalar(usize::try_from(MAX_PRECISE).unwrap_or(usize::MAX));
assert!(result.is_ok());
if std::mem::size_of::<usize>() >= 8 {
let large_value = usize::try_from(MAX_PRECISE + 1).unwrap_or(usize::MAX);
let result: Result<f64, _> = safe_usize_to_scalar(large_value);
assert_matches!(
result,
Err(CoordinateConversionError::ConversionFailed { .. })
);
}
let result: Result<f64, _> = safe_usize_to_scalar(42_usize);
assert!(result.is_ok());
assert_relative_eq!(result.unwrap(), 42.0);
let result: Result<f64, _> = safe_usize_to_scalar(0_usize);
assert!(result.is_ok());
assert_relative_eq!(result.unwrap(), 0.0);
}
#[test]
fn value_conversion_error_display_names_conversion() {
let value_error = ValueConversionError::ConversionFailed {
value: CoordinateConversionValue::from_usize(42),
from_type: "i32",
to_type: "u32",
reason: ValueConversionFailureReason::TargetTypeRejected,
};
let display = format!("{value_error}");
assert!(display.contains("Cannot convert 42 from i32 to u32"));
assert!(display.contains("target type rejected value"));
}
}