use crate::error::{KimiyaError, Result};
use hisab::EPSILON_F64;
pub const WATER_SPECIFIC_HEAT: f64 = 4.184;
#[must_use]
#[inline]
pub fn heat_transfer(mass_g: f64, specific_heat: f64, delta_t: f64) -> f64 {
mass_g * specific_heat * delta_t
}
#[must_use]
pub fn hess_law(enthalpies_j: &[f64]) -> f64 {
enthalpies_j.iter().sum()
}
#[must_use]
pub fn enthalpy_from_formation(products_hf: &[f64], reactants_hf: &[f64]) -> f64 {
let sum_products: f64 = products_hf.iter().sum();
let sum_reactants: f64 = reactants_hf.iter().sum();
sum_products - sum_reactants
}
#[inline]
pub fn specific_heat_from_calorimetry(energy_j: f64, mass_g: f64, delta_t: f64) -> Result<f64> {
if mass_g <= 0.0 {
return Err(KimiyaError::InvalidInput("mass must be positive".into()));
}
if delta_t.abs() < EPSILON_F64 {
return Err(KimiyaError::InvalidInput(
"temperature change must be non-zero".into(),
));
}
Ok(energy_j / (mass_g * delta_t))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn heat_1kg_water_1c() {
let q = heat_transfer(1000.0, WATER_SPECIFIC_HEAT, 1.0);
assert!((q - 4184.0).abs() < 1.0, "1kg water +1°C = 4184 J, got {q}");
}
#[test]
fn heat_100g_water_10c() {
let q = heat_transfer(100.0, WATER_SPECIFIC_HEAT, 10.0);
assert!(
(q - 4184.0).abs() < 1.0,
"100g water +10°C = 4184 J, got {q}"
);
}
#[test]
fn heat_cooling_is_negative() {
let q = heat_transfer(100.0, WATER_SPECIFIC_HEAT, -5.0);
assert!(q < 0.0, "cooling should give negative heat");
}
#[test]
fn hess_law_basic() {
let total = hess_law(&[-100.0, 50.0, -30.0]);
assert!((total - (-80.0)).abs() < f64::EPSILON);
}
#[test]
fn formation_enthalpy() {
let dh = enthalpy_from_formation(&[-400.0], &[-200.0, -100.0]);
assert!((dh - (-100.0)).abs() < f64::EPSILON);
}
#[test]
fn specific_heat_roundtrip() {
let c = specific_heat_from_calorimetry(4184.0, 1000.0, 1.0).unwrap();
assert!((c - WATER_SPECIFIC_HEAT).abs() < 0.001);
}
#[test]
fn water_specific_heat_value() {
assert!((WATER_SPECIFIC_HEAT - 4.184).abs() < 0.001);
}
#[test]
fn zero_mass_calorimetry_is_error() {
assert!(specific_heat_from_calorimetry(100.0, 0.0, 1.0).is_err());
}
#[test]
fn zero_delta_t_calorimetry_is_error() {
assert!(specific_heat_from_calorimetry(100.0, 100.0, 0.0).is_err());
}
}