use chrono::NaiveDate;
use crate::EntityId;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HydroStorage {
pub hydro_id: EntityId,
pub value_hm3: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HydroPastInflows {
pub hydro_id: EntityId,
pub values_m3s: Vec<f64>,
#[cfg_attr(feature = "serde", serde(default))]
pub season_ids: Option<Vec<u32>>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RecentObservation {
pub hydro_id: EntityId,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub value_m3s: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct InitialConditions {
pub storage: Vec<HydroStorage>,
pub filling_storage: Vec<HydroStorage>,
#[cfg_attr(feature = "serde", serde(default))]
pub past_inflows: Vec<HydroPastInflows>,
#[cfg_attr(feature = "serde", serde(default))]
pub recent_observations: Vec<RecentObservation>,
}
impl Default for InitialConditions {
fn default() -> Self {
Self {
storage: Vec::new(),
filling_storage: Vec::new(),
past_inflows: Vec::new(),
recent_observations: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initial_conditions_construction() {
let ic = InitialConditions {
storage: vec![
HydroStorage {
hydro_id: EntityId(0),
value_hm3: 15_000.0,
},
HydroStorage {
hydro_id: EntityId(1),
value_hm3: 8_500.0,
},
],
filling_storage: vec![HydroStorage {
hydro_id: EntityId(10),
value_hm3: 200.0,
}],
past_inflows: vec![HydroPastInflows {
hydro_id: EntityId(0),
values_m3s: vec![600.0, 500.0],
season_ids: None,
}],
recent_observations: vec![],
};
assert_eq!(ic.storage.len(), 2);
assert_eq!(ic.filling_storage.len(), 1);
assert_eq!(ic.past_inflows.len(), 1);
assert_eq!(ic.storage[0].hydro_id, EntityId(0));
assert_eq!(ic.storage[0].value_hm3, 15_000.0);
assert_eq!(ic.storage[1].hydro_id, EntityId(1));
assert_eq!(ic.filling_storage[0].hydro_id, EntityId(10));
assert_eq!(ic.filling_storage[0].value_hm3, 200.0);
assert_eq!(ic.past_inflows[0].hydro_id, EntityId(0));
assert_eq!(ic.past_inflows[0].values_m3s, vec![600.0, 500.0]);
assert!(ic.recent_observations.is_empty());
}
#[test]
fn test_initial_conditions_default_is_empty() {
let ic = InitialConditions::default();
assert!(ic.storage.is_empty());
assert!(ic.filling_storage.is_empty());
assert!(ic.past_inflows.is_empty());
assert!(ic.recent_observations.is_empty());
}
#[test]
fn test_hydro_storage_clone() {
let hs = HydroStorage {
hydro_id: EntityId(5),
value_hm3: 1_234.5,
};
let cloned = hs.clone();
assert_eq!(hs, cloned);
assert_eq!(cloned.hydro_id, EntityId(5));
assert_eq!(cloned.value_hm3, 1_234.5);
}
#[test]
fn test_hydro_past_inflows_clone() {
let hpi = HydroPastInflows {
hydro_id: EntityId(3),
values_m3s: vec![300.0, 200.0, 100.0],
season_ids: None,
};
let cloned = hpi.clone();
assert_eq!(hpi, cloned);
assert_eq!(cloned.hydro_id, EntityId(3));
assert_eq!(cloned.values_m3s, vec![300.0, 200.0, 100.0]);
}
#[cfg(feature = "serde")]
#[test]
fn test_initial_conditions_serde_roundtrip() {
let ic = InitialConditions {
storage: vec![
HydroStorage {
hydro_id: EntityId(0),
value_hm3: 15_000.0,
},
HydroStorage {
hydro_id: EntityId(1),
value_hm3: 8_500.0,
},
],
filling_storage: vec![HydroStorage {
hydro_id: EntityId(10),
value_hm3: 200.0,
}],
past_inflows: vec![HydroPastInflows {
hydro_id: EntityId(0),
values_m3s: vec![600.0, 500.0],
season_ids: None,
}],
recent_observations: vec![],
};
let json = serde_json::to_string(&ic).unwrap();
let deserialized: InitialConditions = serde_json::from_str(&json).unwrap();
assert_eq!(ic, deserialized);
}
#[cfg(feature = "serde")]
#[test]
fn test_initial_conditions_serde_roundtrip_empty_past_inflows() {
let ic = InitialConditions {
storage: vec![HydroStorage {
hydro_id: EntityId(0),
value_hm3: 1_000.0,
}],
filling_storage: vec![],
past_inflows: vec![],
recent_observations: vec![],
};
let json = serde_json::to_string(&ic).unwrap();
let deserialized: InitialConditions = serde_json::from_str(&json).unwrap();
assert_eq!(ic, deserialized);
assert_eq!(deserialized.past_inflows.len(), 0);
}
#[test]
fn test_recent_observation_construction_and_clone() {
let obs = RecentObservation {
hydro_id: EntityId(2),
start_date: NaiveDate::from_ymd_opt(2026, 4, 1)
.unwrap_or_else(|| unreachable!("hardcoded date is valid")),
end_date: NaiveDate::from_ymd_opt(2026, 4, 4)
.unwrap_or_else(|| unreachable!("hardcoded date is valid")),
value_m3s: 500.0,
};
let cloned = obs.clone();
assert_eq!(obs, cloned);
assert_eq!(cloned.hydro_id, EntityId(2));
assert_eq!(cloned.value_m3s, 500.0);
}
#[test]
fn test_initial_conditions_construction_with_recent_observations() {
let ic = InitialConditions {
storage: vec![HydroStorage {
hydro_id: EntityId(0),
value_hm3: 1_000.0,
}],
filling_storage: vec![],
past_inflows: vec![],
recent_observations: vec![RecentObservation {
hydro_id: EntityId(0),
start_date: NaiveDate::from_ymd_opt(2026, 4, 1)
.unwrap_or_else(|| unreachable!("hardcoded date is valid")),
end_date: NaiveDate::from_ymd_opt(2026, 4, 4)
.unwrap_or_else(|| unreachable!("hardcoded date is valid")),
value_m3s: 500.0,
}],
};
assert_eq!(ic.recent_observations.len(), 1);
assert_eq!(ic.recent_observations[0].hydro_id, EntityId(0));
assert_eq!(ic.recent_observations[0].value_m3s, 500.0);
}
#[cfg(feature = "serde")]
#[test]
fn test_initial_conditions_serde_roundtrip_with_recent_observations() {
let ic = InitialConditions {
storage: vec![HydroStorage {
hydro_id: EntityId(0),
value_hm3: 1_000.0,
}],
filling_storage: vec![],
past_inflows: vec![],
recent_observations: vec![
RecentObservation {
hydro_id: EntityId(0),
start_date: NaiveDate::from_ymd_opt(2026, 4, 1)
.unwrap_or_else(|| unreachable!("hardcoded date is valid")),
end_date: NaiveDate::from_ymd_opt(2026, 4, 4)
.unwrap_or_else(|| unreachable!("hardcoded date is valid")),
value_m3s: 500.0,
},
RecentObservation {
hydro_id: EntityId(0),
start_date: NaiveDate::from_ymd_opt(2026, 4, 4)
.unwrap_or_else(|| unreachable!("hardcoded date is valid")),
end_date: NaiveDate::from_ymd_opt(2026, 4, 11)
.unwrap_or_else(|| unreachable!("hardcoded date is valid")),
value_m3s: 480.0,
},
],
};
let json = serde_json::to_string(&ic).unwrap();
let deserialized: InitialConditions = serde_json::from_str(&json).unwrap();
assert_eq!(ic, deserialized);
assert_eq!(deserialized.recent_observations.len(), 2);
}
#[cfg(feature = "serde")]
#[test]
fn test_initial_conditions_serde_default_recent_observations_absent() {
let json = r#"{"storage":[],"filling_storage":[]}"#;
let ic: InitialConditions = serde_json::from_str(json).unwrap();
assert!(ic.recent_observations.is_empty());
}
#[test]
fn test_hydro_past_inflows_with_season_ids() {
let hpi = HydroPastInflows {
hydro_id: EntityId(5),
values_m3s: vec![600.0, 500.0],
season_ids: Some(vec![3, 2]),
};
assert_eq!(hpi.hydro_id, EntityId(5));
assert_eq!(hpi.values_m3s, vec![600.0, 500.0]);
assert_eq!(hpi.season_ids, Some(vec![3, 2]));
let cloned = hpi.clone();
assert_eq!(cloned, hpi);
}
#[cfg(feature = "serde")]
#[test]
fn test_hydro_past_inflows_serde_roundtrip_with_season_ids() {
let hpi = HydroPastInflows {
hydro_id: EntityId(5),
values_m3s: vec![600.0, 500.0],
season_ids: Some(vec![3, 2]),
};
let json = serde_json::to_string(&hpi).unwrap();
let deserialized: HydroPastInflows = serde_json::from_str(&json).unwrap();
assert_eq!(hpi, deserialized);
assert_eq!(deserialized.season_ids, Some(vec![3, 2]));
}
#[cfg(feature = "serde")]
#[test]
fn test_hydro_past_inflows_serde_default_season_ids_absent() {
let json = r#"{"hydro_id":0,"values_m3s":[600.0,500.0]}"#;
let hpi: HydroPastInflows = serde_json::from_str(json).unwrap();
assert_eq!(hpi.hydro_id, EntityId(0));
assert_eq!(hpi.values_m3s, vec![600.0, 500.0]);
assert_eq!(hpi.season_ids, None);
}
}