use chrono::{DateTime, NaiveDate, NaiveTime, Utc};
fn market_close() -> NaiveTime {
NaiveTime::from_hms_opt(21, 0, 0).unwrap_or(NaiveTime::MIN)
}
pub fn parse_expiration_date(date_str: &str, fallback: DateTime<Utc>) -> DateTime<Utc> {
match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
Ok(date) => expiration_instant(date),
Err(_) => fallback,
}
}
pub fn expiration_instant(date: NaiveDate) -> DateTime<Utc> {
date.and_time(market_close()).and_utc()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_expiration_lands_at_the_market_close_on_its_own_day() {
let date = NaiveDate::from_ymd_opt(2025, 9, 19).unwrap();
let instant = expiration_instant(date);
assert_eq!(instant.date_naive(), date, "the day must not shift");
assert_eq!(instant.to_rfc3339(), "2025-09-19T21:00:00+00:00");
}
#[test]
fn a_parseable_string_agrees_with_the_typed_path() {
let fallback = DateTime::from_timestamp(0, 0).unwrap();
let date = NaiveDate::from_ymd_opt(2025, 9, 19).unwrap();
assert_eq!(
parse_expiration_date("2025-09-19", fallback),
expiration_instant(date)
);
}
#[test]
fn an_unparseable_string_uses_the_fallback() {
let fallback = DateTime::from_timestamp(0, 0).unwrap();
assert_eq!(parse_expiration_date("19/09/2025", fallback), fallback);
}
}