pub const METERS_PER_FOOT: f64 = 0.3048;
pub const FEET_PER_METER: f64 = 1.0 / 0.3048;
pub const CUBIC_FEET_PER_ACRE_FOOT: f64 = 43_560.0;
pub const CUBIC_METERS_PER_CUBIC_FOOT: f64 = 0.028316846592;
pub const ACRE_FEET_PER_CUBIC_METER: f64 = 1.0 / (43_560.0 * 0.028316846592);
pub fn feet_to_meters(feet: f64) -> f64 {
feet * METERS_PER_FOOT
}
pub fn meters_to_feet(meters: f64) -> f64 {
meters * FEET_PER_METER
}
pub fn shift_datum_feet(elevation_ft: f64, offset_ft: f64) -> f64 {
elevation_ft + offset_ft
}
pub fn shift_datum_meters(elevation_m: f64, offset_m: f64) -> f64 {
elevation_m + offset_m
}
pub fn acre_feet_to_cubic_meters(acre_feet: f64) -> f64 {
acre_feet * CUBIC_FEET_PER_ACRE_FOOT * CUBIC_METERS_PER_CUBIC_FOOT
}
pub fn cubic_meters_to_acre_feet(cubic_meters: f64) -> f64 {
cubic_meters * ACRE_FEET_PER_CUBIC_METER
}
pub fn percent_of_pool(current: f64, min: f64, full: f64) -> f64 {
if full <= min {
return f64::NAN;
}
(current - min) / (full - min) * 100.0
}
pub fn storage_in_pool_acre_feet(
current: f64,
min: f64,
full: f64,
surface_area_acres: f64,
) -> f64 {
if surface_area_acres < 0.0 {
return f64::NAN;
}
if full <= min {
return f64::NAN;
}
let clamped = current.clamp(min, full);
surface_area_acres * (clamped - min)
}
pub fn conservation_pool_storage_acre_feet(
min: f64,
full: f64,
surface_area_acres: f64,
) -> f64 {
storage_in_pool_acre_feet(full, min, full, surface_area_acres)
}
pub fn feet_below_full(current: f64, full: f64) -> f64 {
full - current
}
pub fn is_in_flood_pool(current: f64, full: f64) -> bool {
current > full
}
pub fn is_below_min_pool(current: f64, min: f64) -> bool {
current < min
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_foot_is_exactly_0_3048_m() {
assert!((feet_to_meters(1.0) - 0.3048).abs() < 1e-12);
}
#[test]
fn one_meter_is_exactly_inverse() {
assert!((meters_to_feet(1.0) - (1.0 / 0.3048)).abs() < 1e-12);
}
#[test]
fn round_trip_feet_meters() {
for v in [0.0, 100.0, 1000.37, -5.5] {
assert!((meters_to_feet(feet_to_meters(v)) - v).abs() < 1e-9);
}
}
#[test]
fn datum_shift_feet_adds_offset() {
assert!((shift_datum_feet(1000.0, -2.7) - 997.3).abs() < 1e-9);
}
#[test]
fn datum_shift_meters_adds_offset() {
assert!((shift_datum_meters(300.0, 0.5) - 300.5).abs() < 1e-9);
}
#[test]
fn datum_shift_through_units_is_consistent() {
let shifted_ft = shift_datum_feet(1000.0, meters_to_feet(1.0));
let shifted_m = shift_datum_meters(feet_to_meters(1000.0), 1.0);
assert!((shifted_ft - meters_to_feet(shifted_m)).abs() < 1e-6);
}
#[test]
fn one_acre_foot_in_cubic_meters() {
let expected = 43_560.0 * CUBIC_METERS_PER_CUBIC_FOOT;
assert!((acre_feet_to_cubic_meters(1.0) - expected).abs() < 1e-6);
assert!((acre_feet_to_cubic_meters(1.0) - 1233.48183754752).abs() < 1e-6);
}
#[test]
fn cubic_meters_to_acre_feet_round_trip() {
for v in [0.0, 1.0, 1000.0, 1_234_567.89] {
assert!((cubic_meters_to_acre_feet(acre_feet_to_cubic_meters(v)) - v).abs() < 1e-6);
}
}
#[test]
fn percent_at_min_is_zero() {
assert!((percent_of_pool(800.0, 800.0, 900.0) - 0.0).abs() < 1e-9);
}
#[test]
fn percent_at_full_is_hundred() {
assert!((percent_of_pool(900.0, 800.0, 900.0) - 100.0).abs() < 1e-9);
}
#[test]
fn percent_at_midpoint_is_fifty() {
assert!((percent_of_pool(850.0, 800.0, 900.0) - 50.0).abs() < 1e-9);
}
#[test]
fn percent_reports_overspill_unclamped() {
assert!((percent_of_pool(910.0, 800.0, 900.0) - 110.0).abs() < 1e-9);
}
#[test]
fn percent_reports_below_min_negative() {
assert!((percent_of_pool(790.0, 800.0, 900.0) - (-10.0)).abs() < 1e-9);
}
#[test]
fn percent_degenerate_pool_is_nan() {
assert!(percent_of_pool(5.0, 10.0, 10.0).is_nan()); assert!(percent_of_pool(5.0, 10.0, 5.0).is_nan()); }
#[test]
fn percent_works_in_meters_too() {
assert!((percent_of_pool(300.0, 290.0, 310.0) - 50.0).abs() < 1e-9);
}
#[test]
fn storage_empty_pool_is_zero() {
assert!((storage_in_pool_acre_feet(800.0, 800.0, 900.0, 1000.0) - 0.0).abs() < 1e-9);
}
#[test]
fn storage_full_pool_matches_total() {
assert!((storage_in_pool_acre_feet(900.0, 800.0, 900.0, 1000.0) - 100_000.0).abs() < 1e-6);
assert!((conservation_pool_storage_acre_feet(800.0, 900.0, 1000.0) - 100_000.0).abs() < 1e-6);
}
#[test]
fn storage_clamps_overspill() {
let s = storage_in_pool_acre_feet(910.0, 800.0, 900.0, 1000.0);
assert!((s - 100_000.0).abs() < 1e-6);
}
#[test]
fn storage_clamps_below_min() {
let s = storage_in_pool_acre_feet(790.0, 800.0, 900.0, 1000.0);
assert!((s - 0.0).abs() < 1e-9);
}
#[test]
fn storage_midpoint_is_half_total() {
let s = storage_in_pool_acre_feet(850.0, 800.0, 900.0, 1000.0);
assert!((s - 50_000.0).abs() < 1e-6);
}
#[test]
fn storage_negative_area_is_nan() {
assert!(storage_in_pool_acre_feet(850.0, 800.0, 900.0, -1.0).is_nan());
}
#[test]
fn feet_below_full_positive_when_low() {
assert!((feet_below_full(895.0, 900.0) - 5.0).abs() < 1e-9);
}
#[test]
fn feet_below_full_negative_when_in_flood_pool() {
assert!((feet_below_full(910.0, 900.0) - (-10.0)).abs() < 1e-9);
}
#[test]
fn flood_pool_detection() {
assert!(is_in_flood_pool(901.0, 900.0));
assert!(!is_in_flood_pool(900.0, 900.0)); assert!(!is_in_flood_pool(899.9, 900.0));
}
#[test]
fn below_min_pool_detection() {
assert!(is_below_min_pool(799.0, 800.0));
assert!(!is_below_min_pool(800.0, 800.0)); assert!(!is_below_min_pool(801.0, 800.0));
}
}