use std::cmp::Ordering;
pub(crate) fn minmax(values: &[f64]) -> (f64, f64) {
let mut min_value = f64::INFINITY;
let mut max_value = f64::NEG_INFINITY;
for value in values {
min_value = min_value.min(*value);
max_value = max_value.max(*value);
}
(min_value, max_value)
}
pub(crate) fn same_value(left: f64, right: f64) -> bool {
left.total_cmp(&right) == Ordering::Equal
}
pub(crate) fn roundable(value: f64) -> bool {
value.is_finite()
&& value.fract().total_cmp(&0.0) == Ordering::Equal
&& format!("{value:.0}").parse::<i64>().is_ok()
}
pub(crate) fn format_axis_value(value: f64) -> String {
if roundable(value) {
format!("{:.0}", value.round())
} else {
value.to_string()
}
}
pub(crate) fn round_away_from_zero(value: f64, digits: i32) -> f64 {
let factor = 10f64.powi(digits);
(value * factor).ceil() / factor
}
pub(crate) fn round_toward_zero(value: f64, digits: i32) -> f64 {
let factor = 10f64.powi(digits);
(value * factor).floor() / factor
}
pub(crate) fn ceil_neg_log10(value: f64) -> i32 {
debug_assert!(
value > 0.0,
"ceil_neg_log10 requires a positive value, got {value}"
);
let mut scaled = value;
let mut digits = 0i32;
while scaled < 1.0 {
scaled *= 10.0;
digits = digits.saturating_add(1);
}
while scaled >= 10.0 {
scaled /= 10.0;
digits = digits.saturating_sub(1);
}
digits
}
pub(crate) fn plotting_range_narrow(min_value: f64, max_value: f64) -> (f64, f64) {
let diff = max_value - min_value;
(
round_down_subtick(min_value, diff),
round_up_subtick(max_value, diff),
)
}
pub(crate) fn round_up_subtick(value: f64, magnitude: f64) -> f64 {
if same_value(value, 0.0) {
return 0.0;
}
let digits = ceil_neg_log10(magnitude) + 1;
if value > 0.0 {
round_away_from_zero(value, digits)
} else {
-round_toward_zero(-value, digits)
}
}
pub(crate) fn round_down_subtick(value: f64, magnitude: f64) -> f64 {
if same_value(value, 0.0) {
return 0.0;
}
let digits = ceil_neg_log10(magnitude) + 1;
if value > 0.0 {
round_toward_zero(value, digits)
} else {
-round_away_from_zero(-value, digits)
}
}
pub(crate) fn extend_limits(values: &[f64], limits: (f64, f64)) -> (f64, f64) {
let mut min_value = limits.0.min(limits.1);
let mut max_value = limits.0.max(limits.1);
if same_value(min_value, 0.0) && same_value(max_value, 0.0) {
let (data_min, data_max) = minmax(values);
min_value = data_min;
max_value = data_max;
}
if same_value(min_value, max_value) {
min_value -= 1.0;
max_value += 1.0;
}
if same_value(limits.0, 0.0) && same_value(limits.1, 0.0) {
plotting_range_narrow(min_value, max_value)
} else {
(min_value, max_value)
}
}
pub(crate) fn usize_to_f64(value: usize) -> f64 {
u32::try_from(value)
.map(f64::from)
.unwrap_or(f64::from(u32::MAX))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minmax_basic() {
assert_eq!(minmax(&[3.0, 1.0, 4.0, 1.5, 9.0]), (1.0, 9.0));
assert_eq!(minmax(&[-5.0, 0.0, 5.0]), (-5.0, 5.0));
}
#[test]
fn minmax_empty() {
let (min, max) = minmax(&[]);
assert!(min.is_infinite() && min.is_sign_positive());
assert!(max.is_infinite() && max.is_sign_negative());
}
#[test]
fn same_value_positive_and_negative_zero() {
assert!(same_value(1.0, 1.0));
assert!(!same_value(0.0, -0.0));
assert!(!same_value(1.0, 1.0 + f64::EPSILON));
}
#[test]
fn roundable_integers_and_non_integers() {
assert!(roundable(5.0));
assert!(roundable(-3.0));
assert!(roundable(0.0));
assert!(!roundable(1.5));
assert!(!roundable(f64::NAN));
assert!(!roundable(f64::INFINITY));
}
#[test]
fn format_axis_value_integer_and_float() {
assert_eq!(format_axis_value(5.0), "5");
assert_eq!(format_axis_value(-3.0), "-3");
assert_eq!(format_axis_value(1.5), "1.5");
}
#[test]
fn round_away_and_toward_zero() {
assert!(same_value(round_away_from_zero(1.23, 1), 1.3));
assert!(same_value(round_toward_zero(1.23, 1), 1.2));
assert!(same_value(round_away_from_zero(1.0, 0), 1.0));
assert!(same_value(round_toward_zero(1.0, 0), 1.0));
}
#[test]
fn ceil_neg_log10_powers_of_ten() {
assert_eq!(ceil_neg_log10(0.001), 3);
assert_eq!(ceil_neg_log10(0.01), 2);
assert_eq!(ceil_neg_log10(0.1), 1);
assert_eq!(ceil_neg_log10(1.0), 0);
assert_eq!(ceil_neg_log10(10.0), -1);
assert_eq!(ceil_neg_log10(100.0), -2);
}
#[test]
fn ceil_neg_log10_non_powers() {
assert_eq!(ceil_neg_log10(0.5), 1);
assert_eq!(ceil_neg_log10(3.0), 0);
assert_eq!(ceil_neg_log10(7.0), 0);
assert_eq!(ceil_neg_log10(50.0), -1);
}
#[test]
fn extend_limits_auto_and_explicit() {
let (lo, hi) = extend_limits(&[1.0, 5.0], (0.0, 0.0));
assert!(lo <= 1.0);
assert!(hi >= 5.0);
let (lo, hi) = extend_limits(&[1.0, 5.0], (2.0, 8.0));
assert!(same_value(lo, 2.0));
assert!(same_value(hi, 8.0));
}
#[test]
fn extend_limits_single_value_expands() {
let (lo, hi) = extend_limits(&[3.0], (0.0, 0.0));
assert!(lo < 3.0);
assert!(hi > 3.0);
}
#[test]
fn round_up_subtick_positive_negative_and_zero() {
assert!(same_value(round_up_subtick(0.0, 1.0), 0.0));
let result = round_up_subtick(2985.0, 8000.0);
assert!(
result >= 2985.0,
"round_up_subtick should round upward: {result}"
);
let result = round_up_subtick(-5015.0, 8000.0);
assert!(
result >= -5015.0,
"round_up_subtick on negative should round toward zero: {result}"
);
}
#[test]
fn round_down_subtick_positive_negative_and_zero() {
assert!(same_value(round_down_subtick(0.0, 1.0), 0.0));
let result = round_down_subtick(2985.0, 8000.0);
assert!(
result <= 2985.0,
"round_down_subtick should round downward: {result}"
);
let result = round_down_subtick(-5015.0, 8000.0);
assert!(
result <= -5015.0,
"round_down_subtick on negative should round away from zero: {result}"
);
}
#[test]
fn plotting_range_narrow_expands_outward() {
let (lo, hi) = plotting_range_narrow(1.0, 5.0);
assert!(lo <= 1.0, "narrow lower bound should be <= min: {lo}");
assert!(hi >= 5.0, "narrow upper bound should be >= max: {hi}");
let (lo, hi) = plotting_range_narrow(-10.0, -5.0);
assert!(lo <= -10.0, "narrow lower bound for negative range: {lo}");
assert!(hi >= -5.0, "narrow upper bound for negative range: {hi}");
let (lo, hi) = plotting_range_narrow(-3.0, 7.0);
assert!(lo <= -3.0, "narrow lower bound for mixed range: {lo}");
assert!(hi >= 7.0, "narrow upper bound for mixed range: {hi}");
}
#[test]
fn usize_to_f64_small_and_large() {
assert!(same_value(usize_to_f64(0), 0.0));
assert!(same_value(usize_to_f64(42), 42.0));
assert!(same_value(usize_to_f64(usize::MAX), f64::from(u32::MAX)));
}
}