#[inline]
pub(crate) fn positive_finite(x: f64) -> bool {
x.is_finite() && x > 0.0
}
#[inline]
pub fn floor_pow10(x: f64) -> f64 {
let mut e = x.log10().floor() as i32;
let mut p = 10f64.powi(e);
if p > x {
e -= 1;
p = 10f64.powi(e);
} else if p * 10.0 <= x {
e += 1;
p = 10f64.powi(e);
}
p
}
#[inline]
pub(crate) fn floor_pow2(x: f64) -> f64 {
let mut e = x.log2().floor() as i32;
let mut p = 2f64.powi(e);
if p > x {
e -= 1;
p = 2f64.powi(e);
} else if p * 2.0 <= x {
e += 1;
p = 2f64.powi(e);
}
p
}
#[inline]
pub(crate) fn pick_closest(x: f64, lo: f64, hi: f64) -> f64 {
if (hi - x).abs() < (x - lo).abs() {
hi
} else {
lo
}
}
pub(crate) fn floor_fibonacci_val(x: f64) -> f64 {
if x < 1.0 {
return 0.0;
}
let (mut a, mut b) = (1.0f64, 2.0f64);
while b <= x {
let next = a + b;
a = b;
b = next;
if !b.is_finite() {
return a;
}
}
a
}
pub(crate) fn ceiling_fibonacci_val(x: f64) -> f64 {
let (mut a, mut b) = (1.0f64, 2.0f64);
if x <= a {
return a;
}
loop {
if b >= x {
return b;
}
let next = a + b;
a = b;
b = next;
if !b.is_finite() {
return b;
}
}
}
#[crate::polydat_node(category = Math)]
pub(crate) fn floor_base10(x: f64) -> f64 {
if !positive_finite(x) {
return 0.0;
}
floor_pow10(x)
}
#[crate::polydat_node(category = Math)]
pub(crate) fn ceiling_base10(x: f64) -> f64 {
if !positive_finite(x) {
return 0.0;
}
let lo = floor_pow10(x);
if lo == x { lo } else { lo * 10.0 }
}
#[crate::polydat_node(category = Math)]
pub(crate) fn closest_base10(x: f64) -> f64 {
if !positive_finite(x) {
return 0.0;
}
let lo = floor_pow10(x);
let hi = if lo == x { lo } else { lo * 10.0 };
pick_closest(x, lo, hi)
}
#[crate::polydat_node(category = Math)]
pub(crate) fn floor_decade(x: f64) -> f64 {
if !positive_finite(x) {
return 0.0;
}
let base = floor_pow10(x);
(x / base).floor() * base
}
#[crate::polydat_node(category = Math)]
pub(crate) fn ceiling_decade(x: f64) -> f64 {
if !positive_finite(x) {
return 0.0;
}
let base = floor_pow10(x);
(x / base).ceil() * base
}
#[crate::polydat_node(category = Math)]
pub(crate) fn closest_decade(x: f64) -> f64 {
if !positive_finite(x) {
return 0.0;
}
let base = floor_pow10(x);
(x / base).round() * base
}
#[crate::polydat_node(category = Math)]
pub(crate) fn floor_fibonacci(x: f64) -> f64 {
if !positive_finite(x) {
return 0.0;
}
floor_fibonacci_val(x)
}
#[crate::polydat_node(category = Math)]
pub(crate) fn ceiling_fibonacci(x: f64) -> f64 {
if !positive_finite(x) {
return 0.0;
}
ceiling_fibonacci_val(x)
}
#[crate::polydat_node(category = Math)]
pub(crate) fn closest_fibonacci(x: f64) -> f64 {
if !positive_finite(x) {
return 0.0;
}
pick_closest(x, floor_fibonacci_val(x), ceiling_fibonacci_val(x))
}
#[crate::polydat_node(category = Math)]
pub(crate) fn floor_binomial(x: f64) -> f64 {
if !positive_finite(x) {
return 0.0;
}
floor_pow2(x)
}
#[crate::polydat_node(category = Math)]
pub(crate) fn ceiling_binomial(x: f64) -> f64 {
if !positive_finite(x) {
return 0.0;
}
let lo = floor_pow2(x);
if lo == x { lo } else { lo * 2.0 }
}
#[crate::polydat_node(category = Math)]
pub(crate) fn closest_binomial(x: f64) -> f64 {
if !positive_finite(x) {
return 0.0;
}
let lo = floor_pow2(x);
let hi = if lo == x { lo } else { lo * 2.0 };
pick_closest(x, lo, hi)
}
#[crate::polydat_node(category = Math)]
pub(crate) fn round_floor(x: f64, interval: f64) -> f64 {
if !(interval.is_finite() && interval > 0.0) {
return x;
}
(x / interval).floor() * interval
}
#[crate::polydat_node(category = Math)]
pub(crate) fn round_ceiling(x: f64, interval: f64) -> f64 {
if !(interval.is_finite() && interval > 0.0) {
return x;
}
(x / interval).ceil() * interval
}
#[crate::polydat_node(category = Math)]
pub(crate) fn round_nearest(x: f64, interval: f64) -> f64 {
if !(interval.is_finite() && interval > 0.0) {
return x;
}
(x / interval).round() * interval
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{PolydatNode, Value};
fn run1(node: &dyn PolydatNode, x: f64) -> f64 {
let mut out = [Value::None];
node.eval(&[Value::F64(x)], &mut out);
out[0].as_f64()
}
fn run2(node: &dyn PolydatNode, x: f64, interval: f64) -> f64 {
let mut out = [Value::None];
node.eval(&[Value::F64(x), Value::F64(interval)], &mut out);
out[0].as_f64()
}
#[test]
fn base10_vectors() {
assert_eq!(run1(&FloorBase10::new(), 1732.234), 1000.0);
assert_eq!(run1(&CeilingBase10::new(), 1732.0), 10000.0);
assert_eq!(run1(&ClosestBase10::new(), 1732.0), 1000.0);
assert_eq!(run1(&ClosestBase10::new(), 6000.0), 10000.0);
}
#[test]
fn base10_exact_power_is_stable() {
assert_eq!(run1(&FloorBase10::new(), 1000.0), 1000.0);
assert_eq!(run1(&CeilingBase10::new(), 1000.0), 1000.0);
}
#[test]
fn base10_fractional_below_one() {
assert!((run1(&FloorBase10::new(), 0.5) - 0.1).abs() < 1e-12);
}
#[test]
fn decade_vectors() {
assert_eq!(run1(&FloorDecade::new(), 2734.0), 2000.0);
assert_eq!(run1(&CeilingDecade::new(), 2734.0), 3000.0);
assert_eq!(run1(&ClosestDecade::new(), 2734.0), 3000.0);
assert_eq!(run1(&FloorDecade::new(), 1732.0), 1000.0);
assert_eq!(run1(&ClosestDecade::new(), 1732.0), 2000.0);
assert_eq!(run1(&CeilingDecade::new(), 1732.0), 2000.0);
}
#[test]
fn fibonacci_vectors() {
assert_eq!(run1(&FloorFibonacci::new(), 1732.0), 1597.0);
assert_eq!(run1(&CeilingFibonacci::new(), 1732.0), 2584.0);
assert_eq!(run1(&ClosestFibonacci::new(), 1732.0), 1597.0);
}
#[test]
fn fibonacci_floor_below_one_is_zero() {
assert_eq!(run1(&FloorFibonacci::new(), 0.5), 0.0);
}
#[test]
fn fibonacci_exact_member_is_stable() {
assert_eq!(run1(&FloorFibonacci::new(), 1597.0), 1597.0);
assert_eq!(run1(&CeilingFibonacci::new(), 1597.0), 1597.0);
}
#[test]
fn binomial_vectors() {
assert_eq!(run1(&FloorBinomial::new(), 1732.0), 1024.0);
assert_eq!(run1(&CeilingBinomial::new(), 1732.0), 2048.0);
assert_eq!(run1(&ClosestBinomial::new(), 1732.0), 2048.0);
}
#[test]
fn binomial_exact_power_is_stable() {
assert_eq!(run1(&FloorBinomial::new(), 1024.0), 1024.0);
assert_eq!(run1(&CeilingBinomial::new(), 1024.0), 1024.0);
}
#[test]
fn non_positive_inputs_are_zero() {
assert_eq!(run1(&FloorBase10::new(), 0.0), 0.0);
assert_eq!(run1(&FloorBase10::new(), -5.0), 0.0);
assert_eq!(run1(&CeilingBinomial::new(), -1.0), 0.0);
assert_eq!(run1(&ClosestFibonacci::new(), 0.0), 0.0);
assert_eq!(run1(&ClosestDecade::new(), -1000.0), 0.0);
assert_eq!(run1(&FloorBase10::new(), f64::INFINITY), 0.0);
assert_eq!(run1(&FloorBinomial::new(), f64::NAN), 0.0);
}
#[test]
fn round_interval_vectors() {
assert_eq!(run2(&RoundFloor::new(), 1732.0, 500.0), 1500.0);
assert_eq!(run2(&RoundCeiling::new(), 1732.0, 500.0), 2000.0);
assert_eq!(run2(&RoundNearest::new(), 1732.0, 500.0), 1500.0);
assert_eq!(run2(&RoundNearest::new(), 1700.0, 500.0), 1500.0);
assert_eq!(run2(&RoundFloor::new(), 1732.0, 0.0), 1732.0);
}
#[test]
fn round_interval_identity_on_bad_interval() {
assert_eq!(run2(&RoundNearest::new(), 1732.0, -5.0), 1732.0);
assert_eq!(run2(&RoundCeiling::new(), 1732.0, f64::INFINITY), 1732.0);
assert_eq!(run2(&RoundFloor::new(), 1732.0, f64::NAN), 1732.0);
}
#[test]
fn all_fifteen_registered_under_math() {
for name in [
"floor_base10", "ceiling_base10", "closest_base10",
"floor_decade", "ceiling_decade", "closest_decade",
"floor_fibonacci", "ceiling_fibonacci", "closest_fibonacci",
"floor_binomial", "ceiling_binomial", "closest_binomial",
"round_floor", "round_ceiling", "round_nearest",
] {
let sig = crate::dsl::registry::lookup(name)
.unwrap_or_else(|| panic!("node '{name}' not registered"));
assert_eq!(
sig.category,
crate::dsl::registry::FuncCategory::Math,
"node '{name}' registered under wrong category",
);
}
}
}