use chrono::NaiveDate; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Instrument { pub symbol: String, pub name: String, pub board: String, pub round_lot: u32, #[serde(default, with = "optional_date_format")] pub listed_at: Option, #[serde(default, with = "optional_date_format")] pub delisted_at: Option, #[serde(default = "default_status")] pub status: String, } impl Instrument { pub fn effective_round_lot(&self) -> u32 { self.round_lot.max(1) } pub fn is_delisted_before(&self, date: NaiveDate) -> bool { self.delisted_at .is_some_and(|delisted_at| delisted_at < date) } } fn default_status() -> String { "active".to_string() } mod optional_date_format { use chrono::NaiveDate; use serde::{self, Deserialize, Deserializer, Serializer}; const FORMAT: &str = "%Y-%m-%d"; pub fn serialize(value: &Option, serializer: S) -> Result where S: Serializer, { match value { Some(date) => serializer.serialize_some(&date.format(FORMAT).to_string()), None => serializer.serialize_none(), } } pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let value = Option::::deserialize(deserializer)?; match value.as_deref().map(str::trim).filter(|v| !v.is_empty()) { Some(text) => NaiveDate::parse_from_str(text, FORMAT) .map(Some) .map_err(serde::de::Error::custom), None => Ok(None), } } }