1fn repr_of(value: f64) -> String {
20 format!("{value:?}")
21}
22
23fn parse_decimal(s: &str) -> Option<(i128, u32)> {
27 let s = s.trim();
28 if s.is_empty() || s.eq_ignore_ascii_case("nan") || s.contains("inf") {
29 return None;
30 }
31 let (neg, body) = match s.strip_prefix('-') {
32 Some(rest) => (true, rest),
33 None => (false, s.strip_prefix('+').unwrap_or(s)),
34 };
35 let (mantissa_str, exp) = match body.split_once(['e', 'E']) {
36 Some((m, e)) => (m, e.parse::<i32>().ok()?),
37 None => (body, 0),
38 };
39 let (int_str, frac_str) = match mantissa_str.split_once('.') {
40 Some((i, f)) => (i, f),
41 None => (mantissa_str, ""),
42 };
43 if int_str.is_empty() && frac_str.is_empty() {
44 return None;
45 }
46 if !int_str.bytes().all(|b| b.is_ascii_digit()) || !frac_str.bytes().all(|b| b.is_ascii_digit())
47 {
48 return None;
49 }
50 let digits: String = format!("{int_str}{frac_str}");
51 let mut mant = digits.parse::<i128>().ok()?;
52 if neg {
53 mant = -mant;
54 }
55 let mut scale = frac_str.len() as i32 - exp;
56 if scale < 0 {
57 mant = mant.checked_mul(10i128.checked_pow((-scale) as u32)?)?;
58 scale = 0;
59 }
60 Some((mant, scale as u32))
61}
62
63type DecParts = (i128, u32);
65
66fn ratio_parts(value: DecParts, target: DecParts) -> Option<(i128, i128)> {
68 let (mut num, sv) = value;
69 let (mut den, st) = target;
70 debug_assert!(den != 0);
71 if den < 0 {
72 num = num.checked_neg()?;
73 den = den.checked_neg()?;
74 }
75 let p = st as i32 - sv as i32;
77 if p >= 0 {
78 num = num.checked_mul(10i128.checked_pow(p as u32)?)?;
79 } else {
80 den = den.checked_mul(10i128.checked_pow((-p) as u32)?)?;
81 }
82 Some((num, den))
83}
84
85fn div_round_half_even(num: i128, den: i128) -> i128 {
87 let q = num.div_euclid(den);
88 let r = num.rem_euclid(den);
89 match (r * 2).cmp(&den) {
90 std::cmp::Ordering::Greater => q + 1,
91 std::cmp::Ordering::Less => q,
92 std::cmp::Ordering::Equal => {
93 if q % 2 == 0 {
94 q
95 } else {
96 q + 1
97 }
98 }
99 }
100}
101
102fn div_floor(num: i128, den: i128) -> i128 {
104 num.div_euclid(den)
105}
106
107fn div_ceil(num: i128, den: i128) -> i128 {
109 -((-num).div_euclid(den))
110}
111
112fn combine(q: i128, target: DecParts) -> Option<f64> {
114 let (mt, st) = target;
115 let prod = q.checked_mul(mt)?;
116 Some(prod as f64 / 10f64.powi(st as i32))
117}
118
119fn target_parts(target: f64) -> Option<DecParts> {
120 let parts = parse_decimal(&repr_of(target))?;
121 (parts.0 != 0).then_some(parts)
122}
123
124pub fn round_to(value: f64, target: f64) -> f64 {
128 try_round_to(value, target).unwrap_or(f64::NAN)
129}
130
131pub fn try_round_to(value: f64, target: f64) -> Option<f64> {
133 let t = target_parts(target)?;
134 let v = parse_decimal(&repr_of(value))?;
135 let (num, den) = ratio_parts(v, t)?;
136 combine(div_round_half_even(num, den), t)
137}
138
139pub fn floor_to(value: f64, target: f64) -> f64 {
141 try_floor_to(value, target).unwrap_or(f64::NAN)
142}
143
144pub fn try_floor_to(value: f64, target: f64) -> Option<f64> {
146 let t = target_parts(target)?;
147 let v = parse_decimal(&repr_of(value))?;
148 let (num, den) = ratio_parts(v, t)?;
149 combine(div_floor(num, den), t)
150}
151
152pub fn ceil_to(value: f64, target: f64) -> f64 {
154 try_ceil_to(value, target).unwrap_or(f64::NAN)
155}
156
157pub fn try_ceil_to(value: f64, target: f64) -> Option<f64> {
159 let t = target_parts(target)?;
160 let v = parse_decimal(&repr_of(value))?;
161 let (num, den) = ratio_parts(v, t)?;
162 combine(div_ceil(num, den), t)
163}
164
165pub fn get_digits(value: f64) -> i32 {
170 let s = repr_of(value);
171 if let Some((_, buf)) = s.split_once("e-") {
172 buf.parse().unwrap_or(0)
173 } else if let Some((_, buf)) = s.split_once('.') {
174 buf.len() as i32
175 } else {
176 0
177 }
178}