use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug)]
pub enum ColumnError {
InvalidLength(f64),
InvalidPoints(usize),
InvalidPorosity(f64),
InvalidDiameter(f64),
}
impl fmt::Display for ColumnError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ColumnError::InvalidLength(v) => {
write!(f, "column: length must be > 0, got {v}")
}
ColumnError::InvalidPoints(n) => {
write!(f, "column: n_points must be ≥ 2, got {n}")
}
ColumnError::InvalidPorosity(v) => {
write!(f, "column: porosity must be in (0, 1), got {v}")
}
ColumnError::InvalidDiameter(v) => {
write!(f, "column: diameter must be > 0, got {v}")
}
}
}
}
impl std::error::Error for ColumnError {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Column {
pub column_length: f64,
pub n_points: usize,
pub porosity: f64,
pub diameter: Option<f64>,
}
impl Column {
pub fn new(
column_length: f64,
n_points: usize,
porosity: f64,
diameter: Option<f64>,
) -> Result<Self, ColumnError> {
if column_length <= 0.0 {
return Err(ColumnError::InvalidLength(column_length));
}
if n_points < 2 {
return Err(ColumnError::InvalidPoints(n_points));
}
if porosity <= 0.0 || porosity >= 1.0 {
return Err(ColumnError::InvalidPorosity(porosity));
}
if let Some(d) = diameter
&& d <= 0.0
{
return Err(ColumnError::InvalidDiameter(d));
}
Ok(Self {
column_length,
n_points,
porosity,
diameter,
})
}
#[inline]
pub fn dz(&self) -> f64 {
self.column_length / self.n_points as f64
}
#[inline]
pub fn phase_ratio(&self) -> f64 {
(1.0 - self.porosity) / self.porosity
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_column_valid() {
let col = Column::new(0.25, 100, 0.4, None).unwrap();
assert_eq!(col.column_length, 0.25);
assert_eq!(col.n_points, 100);
assert!((col.porosity - 0.4).abs() < 1e-12);
assert!(col.diameter.is_none());
}
#[test]
fn test_column_with_diameter() {
let col = Column::new(0.25, 100, 0.4, Some(0.01)).unwrap();
assert!((col.diameter.unwrap() - 0.01).abs() < 1e-12);
}
#[test]
fn test_column_invalid_length() {
assert!(matches!(
Column::new(0.0, 100, 0.4, None),
Err(ColumnError::InvalidLength(_))
));
assert!(matches!(
Column::new(-1.0, 100, 0.4, None),
Err(ColumnError::InvalidLength(_))
));
}
#[test]
fn test_column_invalid_points() {
assert!(matches!(
Column::new(0.25, 0, 0.4, None),
Err(ColumnError::InvalidPoints(_))
));
assert!(matches!(
Column::new(0.25, 1, 0.4, None),
Err(ColumnError::InvalidPoints(_))
));
}
#[test]
fn test_column_invalid_porosity() {
assert!(matches!(
Column::new(0.25, 100, 0.0, None),
Err(ColumnError::InvalidPorosity(_))
));
assert!(matches!(
Column::new(0.25, 100, 1.0, None),
Err(ColumnError::InvalidPorosity(_))
));
assert!(matches!(
Column::new(0.25, 100, 1.5, None),
Err(ColumnError::InvalidPorosity(_))
));
}
#[test]
fn test_column_invalid_diameter() {
assert!(matches!(
Column::new(0.25, 100, 0.4, Some(0.0)),
Err(ColumnError::InvalidDiameter(_))
));
assert!(matches!(
Column::new(0.25, 100, 0.4, Some(-0.01)),
Err(ColumnError::InvalidDiameter(_))
));
}
#[test]
fn test_dz() {
let col = Column::new(0.25, 100, 0.4, None).unwrap();
assert!((col.dz() - 0.0025).abs() < 1e-12);
}
#[test]
fn test_phase_ratio() {
let col = Column::new(0.25, 100, 0.4, None).unwrap();
assert!((col.phase_ratio() - 1.5).abs() < 1e-12);
}
#[test]
fn test_column_serde_roundtrip() {
let col = Column::new(0.25, 100, 0.4, Some(0.01)).unwrap();
let json = serde_json::to_string(&col).unwrap();
let col2: Column = serde_json::from_str(&json).unwrap();
assert_eq!(col.column_length, col2.column_length);
assert_eq!(col.n_points, col2.n_points);
assert!((col.porosity - col2.porosity).abs() < 1e-12);
assert_eq!(col.diameter, col2.diameter);
}
}