Skip to main content

molframe_core/
numeric.rs

1//! The one place a numeric conversion that can lose information is written.
2//!
3//! Every function here is total and states what it does at the edge of the
4//! target type: integers become the nearest float, a float narrows to the
5//! nearest float of the smaller width, and a float becomes an integer by
6//! truncation toward zero, saturating at the type's bounds, with NaN as zero.
7//! Callers use these in place of `as`, so no conversion in the workspace
8//! needs a lint suppression, and each one is a thin wrapper that inlines away.
9
10use num_traits::ToPrimitive;
11
12/// A count as `f64`; exact up to 2^53, rounded to nearest beyond.
13#[must_use]
14#[inline]
15pub fn usize_to_f64(value: usize) -> f64 {
16    match value.to_f64() {
17        Some(converted) => converted,
18        None => f64::INFINITY,
19    }
20}
21
22/// A count as `f64`; exact up to 2^53, rounded to nearest beyond.
23#[must_use]
24#[inline]
25pub fn u64_to_f64(value: u64) -> f64 {
26    match value.to_f64() {
27        Some(converted) => converted,
28        None => f64::INFINITY,
29    }
30}
31
32/// A count as `f32`; exact up to 2^24, rounded to nearest beyond.
33#[must_use]
34#[inline]
35pub fn usize_to_f32(value: usize) -> f32 {
36    match value.to_f32() {
37        Some(converted) => converted,
38        None => f32::INFINITY,
39    }
40}
41
42/// `f64` narrowed to the nearest `f32`; out-of-range magnitudes become
43/// infinities and NaN stays NaN.
44#[must_use]
45#[inline]
46pub fn f64_to_f32(value: f64) -> f32 {
47    match value.to_f32() {
48        Some(converted) => converted,
49        None if value.is_sign_negative() => f32::NEG_INFINITY,
50        None => f32::INFINITY,
51    }
52}
53
54/// `f64` truncated toward zero into `i64`, saturating; NaN is zero.
55#[must_use]
56#[inline]
57pub fn f64_to_i64(value: f64) -> i64 {
58    match value.to_i64() {
59        Some(converted) => converted,
60        None if value.is_nan() => 0,
61        None if value.is_sign_negative() => i64::MIN,
62        None => i64::MAX,
63    }
64}
65
66/// `f32` truncated toward zero into `i64`, saturating; NaN is zero.
67#[must_use]
68#[inline]
69pub fn f32_to_i64(value: f32) -> i64 {
70    f64_to_i64(f64::from(value))
71}
72
73/// `f64` truncated toward zero into `usize`, saturating; NaN and negative
74/// values are zero.
75#[must_use]
76#[inline]
77pub fn f64_to_usize(value: f64) -> usize {
78    match value.to_usize() {
79        Some(converted) => converted,
80        None if value.is_nan() || value.is_sign_negative() => 0,
81        None => usize::MAX,
82    }
83}
84
85/// An index as `u32`, saturating at `u32::MAX`.
86#[must_use]
87#[inline]
88pub fn usize_to_u32(value: usize) -> u32 {
89    match u32::try_from(value) {
90        Ok(converted) => converted,
91        Err(_) => u32::MAX,
92    }
93}
94
95/// A `u64` as `u32`, saturating at `u32::MAX`.
96#[must_use]
97#[inline]
98pub fn u64_to_u32(value: u64) -> u32 {
99    match u32::try_from(value) {
100        Ok(converted) => converted,
101        Err(_) => u32::MAX,
102    }
103}
104
105/// `f64` truncated toward zero into `u32`, saturating; NaN and negative
106/// values are zero.
107#[must_use]
108#[inline]
109pub fn f64_to_u32(value: f64) -> u32 {
110    match value.to_u32() {
111        Some(converted) => converted,
112        None if value.is_nan() || value.is_sign_negative() => 0,
113        None => u32::MAX,
114    }
115}
116
117/// `f64` truncated toward zero into `u64`, saturating; NaN and negative
118/// values are zero.
119#[must_use]
120#[inline]
121pub fn f64_to_u64(value: f64) -> u64 {
122    match value.to_u64() {
123        Some(converted) => converted,
124        None if value.is_nan() || value.is_sign_negative() => 0,
125        None => u64::MAX,
126    }
127}
128
129/// The low eight bits of `value`; the rest are discarded on purpose.
130#[must_use]
131#[inline]
132pub const fn low_u8(value: u32) -> u8 {
133    value.to_le_bytes()[0]
134}
135
136/// The low sixteen bits of `value`; the rest are discarded on purpose.
137#[must_use]
138#[inline]
139pub const fn low_u16(value: u32) -> u16 {
140    let bytes = value.to_le_bytes();
141    u16::from_le_bytes([bytes[0], bytes[1]])
142}
143
144/// IEEE equality of two `f32` values: `-0.0` equals `0.0`, NaN equals nothing.
145#[must_use]
146#[inline]
147pub fn f32_eq(left: f32, right: f32) -> bool {
148    left.partial_cmp(&right) == Some(std::cmp::Ordering::Equal)
149}
150
151/// IEEE equality of two `f64` values: `-0.0` equals `0.0`, NaN equals nothing.
152#[must_use]
153#[inline]
154pub fn f64_eq(left: f64, right: f64) -> bool {
155    left.partial_cmp(&right) == Some(std::cmp::Ordering::Equal)
156}
157
158/// An `i64` as `f64`; exact up to 2^53 in magnitude, rounded to nearest beyond.
159#[must_use]
160#[inline]
161pub fn i64_to_f64(value: i64) -> f64 {
162    match value.to_f64() {
163        Some(converted) => converted,
164        None => f64::NAN,
165    }
166}
167
168/// A 24-bit sample `bits` scaled to `[0, 1)`; exact in `f32`.
169#[must_use]
170#[inline]
171pub fn u64_to_f32_unit(bits: u64) -> f32 {
172    f64_to_f32(u64_to_f64(bits & 0x00ff_ffff) / 16_777_216.0)
173}