This commit is contained in:
2026-02-20 10:31:50 -08:00
parent 8ecb79a0b4
commit 8a55fbffd9
5 changed files with 146 additions and 0 deletions

1
.rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
hard_tabs=true

9
Cargo.lock generated
View File

@@ -1407,6 +1407,13 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde"
[[package]]
name = "ratio_from_float"
version = "0.1.0"
dependencies = [
"strafesnet_common",
]
[[package]]
name = "ratio_ops"
version = "0.1.1"
@@ -1687,6 +1694,7 @@ version = "0.1.0"
dependencies = [
"glam",
"pollster",
"ratio_from_float",
"strafesnet_common",
"strafesnet_graphics",
"strafesnet_roblox_bot_file",
@@ -1700,6 +1708,7 @@ dependencies = [
name = "strafesnet_roblox_bot_player_wasm_module"
version = "0.1.0"
dependencies = [
"ratio_from_float",
"strafesnet_common",
"strafesnet_graphics",
"strafesnet_roblox_bot_file",

View File

@@ -2,6 +2,7 @@
members = [
"lib",
"native-player",
"ratio_from_float",
"wasm-module"
]
resolver = "3"

View File

@@ -0,0 +1,7 @@
[package]
name = "ratio_from_float"
version = "0.1.0"
edition = "2024"
[dependencies]
strafesnet_common.workspace = true

128
ratio_from_float/src/lib.rs Normal file
View File

@@ -0,0 +1,128 @@
use strafesnet_common::integer::Ratio64;
/// Convert an `f64` to a `Ratio64`.
///
/// Returns `None` for NaN, infinities, or when the exact fraction would overflow `i64`/`u64`.
/// The result is always reduced to lowest terms.
pub fn ratio_from_f64(x: f64) -> Option<Ratio64> {
// Handle special values first
match x.classify() {
core::num::FpCategory::Nan | core::num::FpCategory::Infinite => return None,
core::num::FpCategory::Zero => return Ratio64::new(0, 1),
core::num::FpCategory::Subnormal | core::num::FpCategory::Normal => {
if x < i64::MIN as f64 {
return None;
}
if (i64::MAX as f64) < x {
return None;
}
}
}
// 2⃣ Pull out the raw bits
let bits: u64 = x.to_bits();
let sign: i64 = if (bits >> 63) != 0 { -1 } else { 1 };
let exp_raw: u32 = ((bits >> 52) & 0x7FF) as u32;
let mant: u64 = bits & 0xFFFFFFFFFFFFF; // 52 bits
// 3⃣ Normalise exponent and mantissa
let (exp, mant) = if exp_raw == 0 {
// subnormal
(1 - 1023, mant) // unbiased exponent = -1022
} else {
// normal
((exp_raw as i32) - 1023, mant | (1 << 52)) // implicit leading 1
};
// value = sign * mant * 2^(exp-52)
let shift = exp - 52; // may be negative
// 4⃣ Build numerator / denominator as 64bit values
// ────────────────────────────────────────
// If shift is positive → numerator = mant << shift
// If shift is negative → denominator = 1 << (-shift)
// We use the checked arithmetic helpers to catch overflow.
let (mut num, den) = if shift >= 0 {
// shift <= 63 because 53bit mantissa * 2^shift must fit in i64
let s = shift as u32;
let n = (mant as i64).checked_shl(s)?;
(n, 1)
} else {
// shift is negative
let s = (-shift) as u32;
if s > 63 {
// 2^s would not fit in a u64 → underflow
return Ratio64::new(0, 1);
}
(mant as i64, 1u64 << s)
};
// 5⃣ Apply the sign
num *= sign;
Ratio64::new(num, den)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic() {
let r = ratio_from_f64(1.5).unwrap();
assert_eq!(r.num(), 3);
assert_eq!(r.den(), 2);
let r = ratio_from_f64(0.1).unwrap();
// 0.1 = 3602879701896397 / 36028797018963968
assert_eq!(r.num(), 3602879701896397);
assert_eq!(r.den(), 36028797018963968);
let r = ratio_from_f64(-3.141592653589793).unwrap();
assert_eq!(r.num(), -884279719003555);
assert_eq!(r.den(), 281474976710656);
// NaN / Infinity → None
assert!(ratio_from_f64(f64::NAN).is_none());
assert!(ratio_from_f64(f64::INFINITY).is_none());
}
#[test]
fn overflow() {
// value that would need > 64bit numerator
let f = (i64::MAX as f64) * 2.0; // just above i64::MAX
assert!(ratio_from_f64(f).is_none());
// subnormal: denominator would need 2^1074 > u64::MAX
let sub = f64::MIN_POSITIVE / 2.0; // 2.22507e308 / 2 = 1.1125e308
assert_eq!(ratio_from_f64(sub).unwrap().num(), 0);
}
#[test]
fn test() {
let numbers = [
0.0,
-0.0,
1.0,
-1.0,
3.141592653589793,
1.5,
0.1,
2.225073858507201e-308, // subnormal
1.7976931348623157e308, // max normal
];
for f in numbers {
match ratio_from_f64(f) {
Some(r) => println!(
"{:>15}{:>15} / {:>15} (≈ {:.20})",
f,
r.num(),
r.den(),
f
),
None => println!("{:>15} → overflow / NaN / infinite", f),
}
}
}
}