use alloc::{format, string::String};
use crate::common::date::CalendarDate;
pub const MERCURY_MAX_CONTENT_PCT: f64 = 0.0005;
pub const CADMIUM_PORTABLE_MAX_CONTENT_PCT: f64 = 0.002;
pub const LEAD_PORTABLE_MAX_CONTENT_PCT: f64 = 0.01;
pub const LEAD_PORTABLE_RESTRICTION_FROM: CalendarDate = CalendarDate::new(2024, 8, 18);
pub const LEAD_ZINC_AIR_BUTTON_CELL_BOUND_FROM: CalendarDate = CalendarDate::new(2028, 8, 18);
#[must_use]
pub fn mercury_content_prohibited(content_pct: f64) -> bool {
content_pct > MERCURY_MAX_CONTENT_PCT
}
#[must_use]
pub fn cadmium_content_prohibited_for_portable(content_pct: f64) -> bool {
content_pct > CADMIUM_PORTABLE_MAX_CONTENT_PCT
}
#[must_use]
pub fn lead_content_prohibited_for_portable(
content_pct: f64,
placed_on_market: CalendarDate,
zinc_air_button_cell: bool,
) -> bool {
let binds_from = if zinc_air_button_cell {
LEAD_ZINC_AIR_BUTTON_CELL_BOUND_FROM
} else {
LEAD_PORTABLE_RESTRICTION_FROM
};
placed_on_market >= binds_from && content_pct > LEAD_PORTABLE_MAX_CONTENT_PCT
}
pub fn validate_operating_temp_range(min_c: Option<f64>, max_c: Option<f64>) -> Result<(), String> {
if let (Some(min), Some(max)) = (min_c, max_c)
&& min >= max
{
return Err(format!(
"operatingTempMinC ({min}°C) must be less than operatingTempMaxC ({max}°C)"
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mercury_at_and_below_threshold_allowed() {
assert!(!mercury_content_prohibited(0.0));
assert!(!mercury_content_prohibited(0.0005)); }
#[test]
fn mercury_above_threshold_prohibited() {
assert!(mercury_content_prohibited(0.0006));
assert!(mercury_content_prohibited(1.0));
}
#[test]
fn cadmium_at_and_below_threshold_allowed() {
assert!(!cadmium_content_prohibited_for_portable(0.0));
assert!(!cadmium_content_prohibited_for_portable(0.002)); }
#[test]
fn cadmium_above_threshold_prohibited() {
assert!(cadmium_content_prohibited_for_portable(0.0021));
}
#[test]
fn temp_range_valid_cases() {
assert!(validate_operating_temp_range(Some(-20.0), Some(60.0)).is_ok());
assert!(validate_operating_temp_range(None, Some(60.0)).is_ok()); assert!(validate_operating_temp_range(None, None).is_ok());
}
#[test]
fn temp_range_min_greater_than_max_rejected() {
let err = validate_operating_temp_range(Some(60.0), Some(-20.0)).unwrap_err();
assert!(err.contains("operatingTempMinC"), "unexpected: {err}");
}
#[test]
fn temp_range_equal_values_rejected() {
let err = validate_operating_temp_range(Some(25.0), Some(25.0)).unwrap_err();
assert!(err.contains("less than"), "unexpected: {err}");
}
#[test]
fn lead_above_the_limit_is_prohibited_once_the_restriction_binds() {
assert!(lead_content_prohibited_for_portable(
0.02,
CalendarDate::new(2025, 1, 1),
false
));
}
#[test]
fn lead_exactly_at_the_limit_is_permitted() {
assert!(!lead_content_prohibited_for_portable(
LEAD_PORTABLE_MAX_CONTENT_PCT,
CalendarDate::new(2025, 1, 1),
false
));
}
#[test]
fn lead_over_the_limit_is_not_prohibited_before_the_restriction_binds() {
assert!(!lead_content_prohibited_for_portable(
5.0,
CalendarDate::new(2024, 8, 17),
false
));
assert!(
lead_content_prohibited_for_portable(5.0, CalendarDate::new(2024, 8, 18), false),
"18 Aug 2024 is the first day it binds"
);
}
#[test]
fn zinc_air_button_cells_are_exempt_until_2028() {
let over = 0.5;
assert!(
!lead_content_prohibited_for_portable(over, CalendarDate::new(2028, 8, 17), true),
"still inside the carve-out"
);
assert!(
lead_content_prohibited_for_portable(over, CalendarDate::new(2028, 8, 18), true),
"the carve-out ends and the restriction binds"
);
assert!(
lead_content_prohibited_for_portable(over, CalendarDate::new(2025, 1, 1), false),
"a non-zinc-air portable cell had no carve-out to begin with"
);
}
}