use chrono::{Datelike, NaiveDate, Weekday};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParsedDate {
pub weekday: Weekday,
pub month: u32,
pub day: u32,
pub year: i32,
}
impl ParsedDate {
pub fn from_iso(date_str: &str) -> Option<Self> {
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d").ok()?;
Some(Self {
weekday: date.weekday(),
month: date.month(),
day: date.day(),
year: date.year(),
})
}
}
pub fn format_hour(time_str: &str, military_time: bool) -> String {
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(time_str) {
return format_chrono_time(&dt, military_time);
}
if let Some(hour) = time_str
.split('T')
.nth(1)
.and_then(|t| t.split(':').next()?.parse::<u32>().ok())
{
return format_hour_minute(hour, 0, military_time);
}
time_str.to_string()
}
pub fn format_time(time_str: &str, military_time: bool) -> String {
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(time_str) {
return format_chrono_time(&dt, military_time);
}
if let Some(time_part) = time_str.split('T').nth(1) {
let parts: Vec<&str> = time_part.split(':').collect();
if let (Some(Ok(hour)), Some(Ok(minute))) = (
parts.first().map(|s| s.parse::<u32>()),
parts.get(1).map(|s| s.parse::<u32>()),
) {
return format_hour_minute(hour, minute, military_time);
}
}
time_str.to_string()
}
pub fn is_night_time(sunrise: &str, sunset: &str, utc_offset_seconds: i32) -> bool {
use chrono::{Duration, NaiveDateTime, Timelike, Utc};
let now = Utc::now().naive_utc() + Duration::seconds(utc_offset_seconds as i64);
let parse_time = |time_str: &str| -> Option<NaiveDateTime> {
NaiveDateTime::parse_from_str(time_str, "%Y-%m-%dT%H:%M:%S")
.or_else(|_| NaiveDateTime::parse_from_str(time_str, "%Y-%m-%dT%H:%M"))
.ok()
};
match (parse_time(sunrise), parse_time(sunset)) {
(Some(sunrise_time), Some(sunset_time)) => now < sunrise_time || now > sunset_time,
_ => {
let hour = now.hour();
!(6..18).contains(&hour)
}
}
}
fn format_chrono_time<Tz: chrono::TimeZone>(
dt: &chrono::DateTime<Tz>,
military_time: bool,
) -> String
where
Tz::Offset: std::fmt::Display,
{
if military_time {
dt.format("%H:%M").to_string()
} else {
let formatted = dt.format("%I:%M %p").to_string();
formatted.trim_start_matches('0').to_string()
}
}
fn format_hour_minute(hour: u32, minute: u32, military_time: bool) -> String {
if military_time {
format!("{:02}:{:02}", hour, minute)
} else {
let (display_hour, period) = match hour {
0 => (12, "AM"),
1..=11 => (hour, "AM"),
12 => (12, "PM"),
_ => (hour - 12, "PM"),
};
format!("{}:{:02} {}", display_hour, minute, period)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parsed_date_from_iso() {
let d = ParsedDate::from_iso("2025-11-25").expect("valid ISO date");
assert_eq!(d.year, 2025);
assert_eq!(d.month, 11);
assert_eq!(d.day, 25);
assert!(ParsedDate::from_iso("not-a-date").is_none());
}
#[test]
fn format_hour_handles_rfc3339_and_naive() {
assert_eq!(format_hour("2025-01-20T14:00:00+09:00", true), "14:00");
assert_eq!(format_hour("2025-01-20T14:00", true), "14:00");
assert_eq!(format_hour("2025-01-20T00:00", false), "12:00 AM");
}
#[test]
fn night_when_now_is_outside_the_window() {
assert!(is_night_time(
"1970-01-01T06:00:00",
"1970-01-01T18:00:00",
0
));
assert!(is_night_time(
"2999-01-01T06:00:00",
"2999-01-01T18:00:00",
9 * 3600
));
}
#[test]
fn day_when_now_is_inside_the_window() {
assert!(!is_night_time(
"1970-01-01T00:00:00",
"2999-12-31T23:59:59",
-5 * 3600
));
}
}