1pub use polydat::numeric::round_numbers::{
23 ceiling_fibonacci_val, floor_fibonacci_val, floor_pow2, floor_pow10, pick_closest,
24 positive_finite,
25};
26
27#[polydat::polydat_node(category = Math)]
33pub(crate) fn floor_base10(x: f64) -> f64 {
34 if !positive_finite(x) {
35 return 0.0;
36 }
37 floor_pow10(x)
38}
39
40#[polydat::polydat_node(category = Math)]
42pub(crate) fn ceiling_base10(x: f64) -> f64 {
43 if !positive_finite(x) {
44 return 0.0;
45 }
46 let lo = floor_pow10(x);
47 if lo == x { lo } else { lo * 10.0 }
48}
49
50#[polydat::polydat_node(category = Math)]
53pub(crate) fn closest_base10(x: f64) -> f64 {
54 if !positive_finite(x) {
55 return 0.0;
56 }
57 let lo = floor_pow10(x);
58 let hi = if lo == x { lo } else { lo * 10.0 };
59 pick_closest(x, lo, hi)
60}
61
62#[polydat::polydat_node(category = Math)]
69pub(crate) fn floor_decade(x: f64) -> f64 {
70 if !positive_finite(x) {
71 return 0.0;
72 }
73 let base = floor_pow10(x);
74 (x / base).floor() * base
75}
76
77#[polydat::polydat_node(category = Math)]
80pub(crate) fn ceiling_decade(x: f64) -> f64 {
81 if !positive_finite(x) {
82 return 0.0;
83 }
84 let base = floor_pow10(x);
85 (x / base).ceil() * base
86}
87
88#[polydat::polydat_node(category = Math)]
91pub(crate) fn closest_decade(x: f64) -> f64 {
92 if !positive_finite(x) {
93 return 0.0;
94 }
95 let base = floor_pow10(x);
96 (x / base).round() * base
97}
98
99#[polydat::polydat_node(category = Math)]
105pub(crate) fn floor_fibonacci(x: f64) -> f64 {
106 if !positive_finite(x) {
107 return 0.0;
108 }
109 floor_fibonacci_val(x)
110}
111
112#[polydat::polydat_node(category = Math)]
114pub(crate) fn ceiling_fibonacci(x: f64) -> f64 {
115 if !positive_finite(x) {
116 return 0.0;
117 }
118 ceiling_fibonacci_val(x)
119}
120
121#[polydat::polydat_node(category = Math)]
124pub(crate) fn closest_fibonacci(x: f64) -> f64 {
125 if !positive_finite(x) {
126 return 0.0;
127 }
128 pick_closest(x, floor_fibonacci_val(x), ceiling_fibonacci_val(x))
129}
130
131#[polydat::polydat_node(category = Math)]
137pub(crate) fn floor_binomial(x: f64) -> f64 {
138 if !positive_finite(x) {
139 return 0.0;
140 }
141 floor_pow2(x)
142}
143
144#[polydat::polydat_node(category = Math)]
146pub(crate) fn ceiling_binomial(x: f64) -> f64 {
147 if !positive_finite(x) {
148 return 0.0;
149 }
150 let lo = floor_pow2(x);
151 if lo == x { lo } else { lo * 2.0 }
152}
153
154#[polydat::polydat_node(category = Math)]
157pub(crate) fn closest_binomial(x: f64) -> f64 {
158 if !positive_finite(x) {
159 return 0.0;
160 }
161 let lo = floor_pow2(x);
162 let hi = if lo == x { lo } else { lo * 2.0 };
163 pick_closest(x, lo, hi)
164}
165
166#[polydat::polydat_node(category = Math)]
173pub(crate) fn round_floor(x: f64, interval: f64) -> f64 {
174 if !(interval.is_finite() && interval > 0.0) {
175 return x;
176 }
177 (x / interval).floor() * interval
178}
179
180#[polydat::polydat_node(category = Math)]
183pub(crate) fn round_ceiling(x: f64, interval: f64) -> f64 {
184 if !(interval.is_finite() && interval > 0.0) {
185 return x;
186 }
187 (x / interval).ceil() * interval
188}
189
190#[polydat::polydat_node(category = Math)]
193pub(crate) fn round_nearest(x: f64, interval: f64) -> f64 {
194 if !(interval.is_finite() && interval > 0.0) {
195 return x;
196 }
197 (x / interval).round() * interval
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use polydat::ast::{PolydatNode, Value};
204
205 fn run1(node: &dyn PolydatNode, x: f64) -> f64 {
206 let mut out = [Value::None];
207 node.eval(&[Value::F64(x)], &mut out);
208 out[0].as_f64()
209 }
210
211 fn run2(node: &dyn PolydatNode, x: f64, interval: f64) -> f64 {
212 let mut out = [Value::None];
213 node.eval(&[Value::F64(x), Value::F64(interval)], &mut out);
214 out[0].as_f64()
215 }
216
217 #[test]
219 fn base10_vectors() {
220 assert_eq!(run1(&FloorBase10::new(), 1732.234), 1000.0);
221 assert_eq!(run1(&CeilingBase10::new(), 1732.0), 10000.0);
222 assert_eq!(run1(&ClosestBase10::new(), 1732.0), 1000.0);
223 assert_eq!(run1(&ClosestBase10::new(), 6000.0), 10000.0);
224 }
225
226 #[test]
227 fn base10_exact_power_is_stable() {
228 assert_eq!(run1(&FloorBase10::new(), 1000.0), 1000.0);
230 assert_eq!(run1(&CeilingBase10::new(), 1000.0), 1000.0);
231 }
232
233 #[test]
234 fn base10_fractional_below_one() {
235 assert!((run1(&FloorBase10::new(), 0.5) - 0.1).abs() < 1e-12);
237 }
238
239 #[test]
241 fn decade_vectors() {
242 assert_eq!(run1(&FloorDecade::new(), 2734.0), 2000.0);
243 assert_eq!(run1(&CeilingDecade::new(), 2734.0), 3000.0);
244 assert_eq!(run1(&ClosestDecade::new(), 2734.0), 3000.0);
245 assert_eq!(run1(&FloorDecade::new(), 1732.0), 1000.0);
246 assert_eq!(run1(&ClosestDecade::new(), 1732.0), 2000.0);
247 assert_eq!(run1(&CeilingDecade::new(), 1732.0), 2000.0);
248 }
249
250 #[test]
252 fn fibonacci_vectors() {
253 assert_eq!(run1(&FloorFibonacci::new(), 1732.0), 1597.0);
254 assert_eq!(run1(&CeilingFibonacci::new(), 1732.0), 2584.0);
255 assert_eq!(run1(&ClosestFibonacci::new(), 1732.0), 1597.0);
256 }
257
258 #[test]
259 fn fibonacci_floor_below_one_is_zero() {
260 assert_eq!(run1(&FloorFibonacci::new(), 0.5), 0.0);
261 }
262
263 #[test]
264 fn fibonacci_exact_member_is_stable() {
265 assert_eq!(run1(&FloorFibonacci::new(), 1597.0), 1597.0);
266 assert_eq!(run1(&CeilingFibonacci::new(), 1597.0), 1597.0);
267 }
268
269 #[test]
271 fn binomial_vectors() {
272 assert_eq!(run1(&FloorBinomial::new(), 1732.0), 1024.0);
273 assert_eq!(run1(&CeilingBinomial::new(), 1732.0), 2048.0);
274 assert_eq!(run1(&ClosestBinomial::new(), 1732.0), 2048.0);
275 }
276
277 #[test]
278 fn binomial_exact_power_is_stable() {
279 assert_eq!(run1(&FloorBinomial::new(), 1024.0), 1024.0);
280 assert_eq!(run1(&CeilingBinomial::new(), 1024.0), 1024.0);
281 }
282
283 #[test]
285 fn non_positive_inputs_are_zero() {
286 assert_eq!(run1(&FloorBase10::new(), 0.0), 0.0);
287 assert_eq!(run1(&FloorBase10::new(), -5.0), 0.0);
288 assert_eq!(run1(&CeilingBinomial::new(), -1.0), 0.0);
289 assert_eq!(run1(&ClosestFibonacci::new(), 0.0), 0.0);
290 assert_eq!(run1(&ClosestDecade::new(), -1000.0), 0.0);
291 assert_eq!(run1(&FloorBase10::new(), f64::INFINITY), 0.0);
292 assert_eq!(run1(&FloorBinomial::new(), f64::NAN), 0.0);
293 }
294
295 #[test]
297 fn round_interval_vectors() {
298 assert_eq!(run2(&RoundFloor::new(), 1732.0, 500.0), 1500.0);
299 assert_eq!(run2(&RoundCeiling::new(), 1732.0, 500.0), 2000.0);
300 assert_eq!(run2(&RoundNearest::new(), 1732.0, 500.0), 1500.0);
302 assert_eq!(run2(&RoundNearest::new(), 1700.0, 500.0), 1500.0);
303 assert_eq!(run2(&RoundFloor::new(), 1732.0, 0.0), 1732.0);
305 }
306
307 #[test]
308 fn round_interval_identity_on_bad_interval() {
309 assert_eq!(run2(&RoundNearest::new(), 1732.0, -5.0), 1732.0);
310 assert_eq!(run2(&RoundCeiling::new(), 1732.0, f64::INFINITY), 1732.0);
311 assert_eq!(run2(&RoundFloor::new(), 1732.0, f64::NAN), 1732.0);
312 }
313
314 #[test]
316 fn all_fifteen_registered_under_math() {
317 for name in [
318 "floor_base10",
319 "ceiling_base10",
320 "closest_base10",
321 "floor_decade",
322 "ceiling_decade",
323 "closest_decade",
324 "floor_fibonacci",
325 "ceiling_fibonacci",
326 "closest_fibonacci",
327 "floor_binomial",
328 "ceiling_binomial",
329 "closest_binomial",
330 "round_floor",
331 "round_ceiling",
332 "round_nearest",
333 ] {
334 let sig = polydat::dsl::registry::lookup(name)
335 .unwrap_or_else(|| panic!("node '{name}' not registered"));
336 assert_eq!(
337 sig.category,
338 polydat::dsl::registry::FuncCategory::Math,
339 "node '{name}' registered under wrong category",
340 );
341 }
342 }
343}