imx/numeric.rs
1//! Numeric type conversion utilities with safety guarantees.
2//!
3//! This module provides a set of safe numeric conversion functions that handle
4//! edge cases and prevent undefined behavior. The functions are designed for:
5//!
6//! - Safe conversion between floating-point and integer types
7//! - Handling of NaN, infinity, and out-of-range values
8//! - Clamping values to valid ranges
9//! - Proper rounding of floating-point numbers
10//!
11//! # Safety
12//!
13//! All functions in this module guarantee:
14//! - No undefined behavior
15//! - No panics
16//! - Deterministic results for all inputs
17//! - Proper handling of edge cases (NaN, infinity, etc.)
18//!
19//! # Examples
20//!
21//! ```rust
22//! use imx::numeric::{f32_to_i32, i32_to_u32, f32_to_u8};
23//!
24//! // Safe float to int conversion
25//! assert_eq!(f32_to_i32(3.7), 4); // Rounds to nearest
26//! assert_eq!(f32_to_i32(f32::NAN), 0); // NaN becomes 0
27//!
28//! // Safe signed to unsigned conversion
29//! assert_eq!(i32_to_u32(-5), 0); // Negative becomes 0
30//! assert_eq!(i32_to_u32(42), 42); // Positive passes through
31//!
32//! // Safe float to byte conversion
33//! assert_eq!(f32_to_u8(127.6), 128); // Rounds to nearest
34//! assert_eq!(f32_to_u8(300.0), 255); // Clamps to max
35//! ```
36
37#![warn(clippy::all, clippy::pedantic)]
38
39/// Constants for f32 range that can safely represent integers without precision loss.
40/// These values are derived from the fact that f32 has 24 bits of precision,
41/// meaning it can exactly represent integers up to 2^24 (16,777,216).
42pub(crate) const F32_MAX_SAFE_INT: f32 = 16_777_216.0; // 2^24
43pub(crate) const F32_MIN_SAFE_INT: f32 = -16_777_216.0;
44
45/// Safely converts an f32 to i32 with proper rounding and range clamping.
46///
47/// This function provides several safety guarantees:
48/// - NaN values are converted to 0
49/// - Values outside i32's range are clamped to `i32::MIN` or `i32::MAX`
50/// - Values are rounded to the nearest integer using banker's rounding
51///
52/// # Arguments
53///
54/// * `x` - The f32 value to convert
55///
56/// # Returns
57///
58/// Returns the converted i32 value, properly rounded and clamped
59///
60/// # Examples
61///
62/// ```rust
63/// use imx::numeric::f32_to_i32;
64///
65/// assert_eq!(f32_to_i32(3.7), 4); // Rounds up
66/// assert_eq!(f32_to_i32(3.2), 3); // Rounds down
67/// assert_eq!(f32_to_i32(f32::NAN), 0); // NaN becomes 0
68/// assert_eq!(f32_to_i32(f32::INFINITY), i32::MAX); // Clamps to max
69/// assert_eq!(f32_to_i32(f32::NEG_INFINITY), i32::MIN); // Clamps to min
70/// ```
71#[must_use]
72pub fn f32_to_i32(x: f32) -> i32 {
73 if x.is_nan() {
74 0
75 } else if x >= F32_MAX_SAFE_INT {
76 i32::MAX
77 } else if x <= F32_MIN_SAFE_INT {
78 i32::MIN
79 } else {
80 // Safe because we've bounded x within safe integer range
81 #[allow(clippy::cast_possible_truncation)]
82 let result = x.round() as i32;
83 result
84 }
85}
86
87/// Safely converts an i32 to u32, clamping negative values to 0.
88///
89/// This function guarantees that negative values become 0 while
90/// positive values are preserved. This is useful when working with
91/// unsigned quantities like array indices or dimensions.
92///
93/// # Arguments
94///
95/// * `x` - The i32 value to convert
96///
97/// # Returns
98///
99/// Returns the converted u32 value, with negative inputs clamped to 0
100///
101/// # Examples
102///
103/// ```rust
104/// use imx::numeric::i32_to_u32;
105///
106/// assert_eq!(i32_to_u32(42), 42); // Positive passes through
107/// assert_eq!(i32_to_u32(0), 0); // Zero remains zero
108/// assert_eq!(i32_to_u32(-5), 0); // Negative becomes zero
109/// ```
110#[must_use]
111pub fn i32_to_u32(x: i32) -> u32 {
112 // Safe because we're clamping negative values to 0
113 #[allow(clippy::cast_sign_loss)]
114 let result = x.max(0) as u32;
115 result
116}
117
118/// Safely converts a u32 to i32, clamping values above `i32::MAX`.
119///
120/// This function handles the case where a u32 value is too large
121/// to fit in an i32. Such values are clamped to `i32::MAX`.
122///
123/// # Arguments
124///
125/// * `x` - The u32 value to convert
126///
127/// # Returns
128///
129/// Returns the converted i32 value, clamped to `i32::MAX` if necessary
130///
131/// # Examples
132///
133/// ```rust
134/// use imx::numeric::u32_to_i32;
135///
136/// assert_eq!(u32_to_i32(42), 42); // Small values pass through
137/// assert_eq!(u32_to_i32(0), 0); // Zero remains zero
138/// assert_eq!(u32_to_i32(3_000_000_000), i32::MAX); // Large values clamp to max
139/// ```
140#[must_use]
141pub fn u32_to_i32(x: u32) -> i32 {
142 if x > i32::MAX as u32 {
143 i32::MAX
144 } else {
145 // Safe because we've checked the upper bound
146 #[allow(clippy::cast_possible_wrap)]
147 let result = x as i32;
148 result
149 }
150}
151
152/// Safely converts an f32 to u8, with rounding and range clamping.
153///
154/// This function is particularly useful for color channel conversions
155/// where values need to be constrained to the 0-255 range. It provides
156/// proper handling of floating point edge cases.
157///
158/// # Arguments
159///
160/// * `x` - The f32 value to convert
161///
162/// # Returns
163///
164/// Returns the converted u8 value, rounded and clamped to 0..=255
165///
166/// # Examples
167///
168/// ```rust
169/// use imx::numeric::f32_to_u8;
170///
171/// assert_eq!(f32_to_u8(127.6), 128); // Rounds to nearest
172/// assert_eq!(f32_to_u8(300.0), 255); // Clamps to max
173/// assert_eq!(f32_to_u8(-5.0), 0); // Clamps to min
174/// assert_eq!(f32_to_u8(f32::NAN), 0); // NaN becomes 0
175/// ```
176#[must_use]
177pub fn f32_to_u8(x: f32) -> u8 {
178 if x.is_nan() {
179 0
180 } else if x >= 255.0 {
181 255
182 } else if x <= 0.0 {
183 0
184 } else {
185 // Safe because we've bounded x within u8's range
186 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
187 let result = x.round() as u8;
188 result
189 }
190}
191
192/// Converts an i32 to f32 for text positioning purposes.
193///
194/// This function is specifically designed for converting screen coordinates
195/// to floating point values for text rendering. While it may lose precision
196/// for very large values, this is acceptable for text positioning where:
197/// - Values are typically small (screen coordinates)
198/// - Sub-pixel precision isn't critical
199/// - Exact representation isn't required
200///
201/// # Arguments
202///
203/// * `x` - The i32 screen coordinate to convert
204///
205/// # Returns
206///
207/// Returns the coordinate as an f32 value
208///
209/// # Examples
210///
211/// ```rust
212/// use imx::numeric::i32_to_f32_for_pos;
213///
214/// assert_eq!(i32_to_f32_for_pos(42), 42.0); // Exact for small values
215/// assert_eq!(i32_to_f32_for_pos(0), 0.0); // Zero remains exact
216/// ```
217///
218/// # Note
219///
220/// This function is marked as safe for text positioning specifically because:
221/// 1. Screen coordinates are typically well within f32's precise range
222/// 2. Sub-pixel precision isn't critical for text rendering
223/// 3. Any loss of precision won't affect visual quality
224#[must_use]
225pub fn i32_to_f32_for_pos(x: i32) -> f32 {
226 // Safe for text positioning where precision isn't critical
227 #[allow(clippy::cast_precision_loss)]
228 let result = x as f32;
229 result
230}
231
232/// Converts an f32 to a u32, handling NaN, infinity, and out-of-range values.
233///
234/// This function provides safe conversion from f32 to u32 with consistent handling
235/// of edge cases and proper rounding behavior. Note that due to f32's precision
236/// limitations, values very close to `u32::MAX` may be rounded to `u32::MAX`.
237///
238/// # Examples
239///
240/// ```
241/// use imx::numeric::f32_to_u32;
242///
243/// assert_eq!(f32_to_u32(0.0), 0);
244/// assert_eq!(f32_to_u32(1.4), 1);
245/// assert_eq!(f32_to_u32(1.6), 2);
246/// assert_eq!(f32_to_u32(-1.0), 0); // Negative values clamp to 0
247/// assert_eq!(f32_to_u32(f32::NAN), 0);
248/// assert_eq!(f32_to_u32(f32::INFINITY), u32::MAX);
249/// ```
250#[must_use]
251pub fn f32_to_u32(x: f32) -> u32 {
252 if x.is_nan() {
253 0
254 } else if x.is_infinite() {
255 if x.is_sign_positive() {
256 u32::MAX
257 } else {
258 0
259 }
260 } else if x <= 0.0 {
261 0
262 } else {
263 // For values near u32::MAX, we need to be extra careful
264 // We intentionally allow precision loss here as it's part of our strategy
265 // for handling values near u32::MAX consistently
266 #[allow(clippy::cast_precision_loss)]
267 let max_f32 = u32::MAX as f32;
268
269 // Round first to handle fractional values consistently
270 let rounded = x.round();
271
272 // If the value is very close to u32::MAX, return u32::MAX
273 if rounded >= max_f32 {
274 u32::MAX
275 } else {
276 // Safe because we've bounded x below u32::MAX
277 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
278 let result = rounded as u32;
279 result
280 }
281 }
282}