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 return None;
48 }
49 let digits: String = format!("{int_str}{frac_str}");
50 let mut mant = digits.parse::<i128>().ok()?;
51 if neg {
52 mant = -mant;
53 }
54 let mut scale = frac_str.len() as i32 - exp;
55 if scale < 0 {
56 mant = mant.checked_mul(10i128.checked_pow((-scale) as u32)?)?;
57 scale = 0;
58 }
59 Some((mant, scale as u32))
60}
61
62type DecParts = (i128, u32);
64
65fn ratio_parts(value: DecParts, target: DecParts) -> Option<(i128, i128)> {
67 let (mut num, sv) = value;
68 let (mut den, st) = target;
69 debug_assert!(den != 0);
70 if den < 0 {
71 num = num.checked_neg()?;
72 den = den.checked_neg()?;
73 }
74 let p = st as i32 - sv as i32;
76 if p >= 0 {
77 num = num.checked_mul(10i128.checked_pow(p as u32)?)?;
78 } else {
79 den = den.checked_mul(10i128.checked_pow((-p) as u32)?)?;
80 }
81 Some((num, den))
82}
83
84fn div_round_half_even(num: i128, den: i128) -> i128 {
86 let q = num.div_euclid(den);
87 let r = num.rem_euclid(den);
88 match (r * 2).cmp(&den) {
89 std::cmp::Ordering::Greater => q + 1,
90 std::cmp::Ordering::Less => q,
91 std::cmp::Ordering::Equal => {
92 if q % 2 == 0 {
93 q
94 } else {
95 q + 1
96 }
97 }
98 }
99}
100
101fn div_floor(num: i128, den: i128) -> i128 {
103 num.div_euclid(den)
104}
105
106fn div_ceil(num: i128, den: i128) -> i128 {
108 -((-num).div_euclid(den))
109}
110
111fn combine(q: i128, target: DecParts) -> Option<f64> {
113 let (mt, st) = target;
114 let prod = q.checked_mul(mt)?;
115 Some(prod as f64 / 10f64.powi(st as i32))
116}
117
118fn target_parts(target: f64) -> Option<DecParts> {
119 let parts = parse_decimal(&repr_of(target))?;
120 (parts.0 != 0).then_some(parts)
121}
122
123pub fn round_to(value: f64, target: f64) -> f64 {
127 try_round_to(value, target).unwrap_or(f64::NAN)
128}
129
130pub fn try_round_to(value: f64, target: f64) -> Option<f64> {
132 let t = target_parts(target)?;
133 let v = parse_decimal(&repr_of(value))?;
134 let (num, den) = ratio_parts(v, t)?;
135 combine(div_round_half_even(num, den), t)
136}
137
138pub fn floor_to(value: f64, target: f64) -> f64 {
140 try_floor_to(value, target).unwrap_or(f64::NAN)
141}
142
143pub fn try_floor_to(value: f64, target: f64) -> Option<f64> {
145 let t = target_parts(target)?;
146 let v = parse_decimal(&repr_of(value))?;
147 let (num, den) = ratio_parts(v, t)?;
148 combine(div_floor(num, den), t)
149}
150
151pub fn ceil_to(value: f64, target: f64) -> f64 {
153 try_ceil_to(value, target).unwrap_or(f64::NAN)
154}
155
156pub fn try_ceil_to(value: f64, target: f64) -> Option<f64> {
158 let t = target_parts(target)?;
159 let v = parse_decimal(&repr_of(value))?;
160 let (num, den) = ratio_parts(v, t)?;
161 combine(div_ceil(num, den), t)
162}
163
164pub fn get_digits(value: f64) -> i32 {
169 let s = repr_of(value);
170 if let Some((_, buf)) = s.split_once("e-") {
171 buf.parse().unwrap_or(0)
172 } else if let Some((_, buf)) = s.split_once('.') {
173 buf.len() as i32
174 } else {
175 0
176 }
177}