use chrono::{Datelike, NaiveDate};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Default)]
pub enum GameDate {
#[default]
Now,
Date(NaiveDate),
}
impl GameDate {
pub fn today() -> Self {
Self::Date(chrono::Local::now().date_naive())
}
pub fn from_date(date: NaiveDate) -> Self {
Self::Date(date)
}
pub fn from_ymd(year: i32, month: u32, day: u32) -> Option<Self> {
NaiveDate::from_ymd_opt(year, month, day).map(Self::Date)
}
pub fn to_api_string(&self) -> String {
match self {
Self::Now => "now".to_string(),
Self::Date(date) => date.format("%Y-%m-%d").to_string(),
}
}
fn as_date(&self) -> NaiveDate {
match self {
Self::Now => chrono::Local::now().date_naive(),
Self::Date(date) => *date,
}
}
pub fn add_days(&self, days: i64) -> Self {
Self::Date(self.as_date() + chrono::Duration::days(days))
}
}
impl FromStr for GameDate {
type Err = chrono::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "now" {
Ok(Self::Now)
} else {
NaiveDate::parse_from_str(s, "%Y-%m-%d").map(Self::Date)
}
}
}
impl From<NaiveDate> for GameDate {
fn from(date: NaiveDate) -> Self {
Self::Date(date)
}
}
impl fmt::Display for GameDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_api_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Season {
pub start_year: u16,
}
impl Season {
pub fn new(start_year: u16) -> Self {
Self { start_year }
}
pub fn from_years(start_year: u16, end_year: u16) -> Self {
debug_assert_eq!(
end_year,
start_year + 1,
"End year should be start year + 1"
);
Self { start_year }
}
pub fn end_year(&self) -> u16 {
self.start_year + 1
}
#[allow(clippy::wrong_self_convention)]
pub fn to_api_string(&self) -> String {
format!("{}{}", self.start_year, self.end_year())
}
pub fn parse(s: &str) -> Option<Self> {
if s.len() != 8 {
return None;
}
let start_year: u16 = s[0..4].parse().ok()?;
let end_year: u16 = s[4..8].parse().ok()?;
if end_year != start_year + 1 {
return None;
}
Some(Self { start_year })
}
pub fn current() -> Self {
let now = chrono::Local::now().date_naive();
let year = now.year() as u16;
if now.month() < 10 {
Self::new(year - 1)
} else {
Self::new(year)
}
}
}
impl fmt::Display for Season {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_api_string())
}
}
impl From<i32> for Season {
fn from(season_id: i32) -> Self {
let start_year = (season_id / 10000) as u16;
Self { start_year }
}
}
impl From<i64> for Season {
fn from(season_id: i64) -> Self {
let start_year = (season_id / 10000) as u16;
Self { start_year }
}
}
impl From<u16> for Season {
fn from(start_year: u16) -> Self {
Self::new(start_year)
}
}
impl FromStr for Season {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s).ok_or(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_game_date_now() {
let date = GameDate::Now;
assert_eq!(date.to_api_string(), "now");
}
#[test]
fn test_game_date_specific() {
let date = GameDate::from_ymd(2024, 10, 19).unwrap();
assert_eq!(date.to_api_string(), "2024-10-19");
}
#[test]
fn test_game_date_from_str() {
let date: GameDate = "2024-10-19".parse().unwrap();
assert_eq!(date.to_api_string(), "2024-10-19");
let now: GameDate = "now".parse().unwrap();
assert_eq!(now, GameDate::Now);
}
#[test]
fn test_game_date_from_str_trait() {
let date: GameDate = "2024-10-19".parse().unwrap();
assert_eq!(date.to_api_string(), "2024-10-19");
let now: GameDate = "now".parse().unwrap();
assert_eq!(now, GameDate::Now);
assert_eq!(now.to_api_string(), "now");
}
#[test]
fn test_game_date_today() {
let date = GameDate::today();
match date {
GameDate::Date(_) => {} GameDate::Now => panic!("today() should return a specific date"),
}
}
#[test]
fn test_season_to_string() {
let season = Season::new(2023);
assert_eq!(season.to_api_string(), "20232024");
assert_eq!(season.end_year(), 2024);
}
#[test]
fn test_season_from_str() {
let season = Season::from_str("20232024").unwrap();
assert_eq!(season.start_year, 2023);
assert_eq!(season.end_year(), 2024);
assert!(Season::from_str("2023").is_err());
assert!(Season::from_str("20232025").is_err()); }
#[test]
fn test_season_from_years() {
let season = Season::from_years(2023, 2024);
assert_eq!(season.start_year, 2023);
assert_eq!(season.to_api_string(), "20232024");
}
#[test]
fn test_season_current() {
let season = Season::current();
assert!(season.start_year >= 2024);
}
#[test]
fn test_game_date_from_date() {
let naive_date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
let game_date = GameDate::from_date(naive_date);
assert_eq!(game_date.to_api_string(), "2024-03-15");
}
#[test]
fn test_game_date_default() {
let default_date = GameDate::default();
assert_eq!(default_date, GameDate::Now);
assert_eq!(default_date.to_api_string(), "now");
}
#[test]
fn test_game_date_from_naive_date() {
let naive_date = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
let game_date: GameDate = naive_date.into();
assert_eq!(game_date.to_api_string(), "2024-12-25");
}
#[test]
fn test_game_date_display() {
let now = GameDate::Now;
assert_eq!(format!("{}", now), "now");
let date = GameDate::from_ymd(2024, 3, 15).unwrap();
assert_eq!(format!("{}", date), "2024-03-15");
}
#[test]
fn test_game_date_from_ymd_invalid() {
assert!(GameDate::from_ymd(2024, 13, 1).is_none());
assert!(GameDate::from_ymd(2024, 2, 30).is_none());
assert!(GameDate::from_ymd(2024, 4, 31).is_none());
assert!(GameDate::from_ymd(2024, 1, 0).is_none());
assert!(GameDate::from_ymd(2024, 0, 1).is_none());
}
#[test]
fn test_game_date_from_str_invalid() {
assert!("2024/10/19".parse::<GameDate>().is_err());
assert!("10-19-2024".parse::<GameDate>().is_err());
assert!("2024-10".parse::<GameDate>().is_err());
assert!("".parse::<GameDate>().is_err());
assert!("not-a-date".parse::<GameDate>().is_err());
assert!("2024-13-01".parse::<GameDate>().is_err());
assert!("2024-02-30".parse::<GameDate>().is_err());
}
#[test]
fn test_game_date_equality() {
let date1 = GameDate::from_ymd(2024, 10, 19).unwrap();
let date2 = GameDate::from_ymd(2024, 10, 19).unwrap();
let date3 = GameDate::from_ymd(2024, 10, 20).unwrap();
assert_eq!(date1, date2);
assert_ne!(date1, date3);
assert_ne!(date1, GameDate::Now);
let now1 = GameDate::Now;
let now2 = GameDate::Now;
assert_eq!(now1, now2);
}
#[test]
fn test_season_display() {
let season = Season::new(2023);
assert_eq!(format!("{}", season), "20232024");
let season2 = Season::new(2019);
assert_eq!(format!("{}", season2), "20192020");
}
#[test]
fn test_season_from_str_edge_cases() {
assert!(Season::from_str("").is_err());
assert!(Season::from_str("2023").is_err());
assert!(Season::from_str("202324").is_err());
assert!(Season::from_str("202320240").is_err());
assert!(Season::from_str("abcd efgh").is_err());
assert!(Season::from_str("2023abcd").is_err());
assert!(Season::from_str("20232025").is_err());
assert!(Season::from_str("20232023").is_err());
assert!(Season::from_str("20242023").is_err());
let season = Season::from_str("19992000").unwrap();
assert_eq!(season.start_year, 1999);
let season = Season::from_str("20502051").unwrap();
assert_eq!(season.start_year, 2050);
}
#[test]
fn test_add_days_with_specific_date() {
let date = GameDate::from_ymd(2024, 10, 19).unwrap();
let future = date.add_days(5);
assert_eq!(future.to_api_string(), "2024-10-24");
let past = date.add_days(-5);
assert_eq!(past.to_api_string(), "2024-10-14");
let same = date.add_days(0);
assert_eq!(same.to_api_string(), "2024-10-19");
}
#[test]
fn test_add_days_across_month_boundary() {
let date = GameDate::from_ymd(2024, 10, 30).unwrap();
let next_month = date.add_days(5);
assert_eq!(next_month.to_api_string(), "2024-11-04");
let date2 = GameDate::from_ymd(2024, 11, 3).unwrap();
let prev_month = date2.add_days(-5);
assert_eq!(prev_month.to_api_string(), "2024-10-29");
}
#[test]
fn test_add_days_across_year_boundary() {
let date = GameDate::from_ymd(2024, 12, 30).unwrap();
let next_year = date.add_days(5);
assert_eq!(next_year.to_api_string(), "2025-01-04");
let date2 = GameDate::from_ymd(2025, 1, 3).unwrap();
let prev_year = date2.add_days(-5);
assert_eq!(prev_year.to_api_string(), "2024-12-29");
}
#[test]
fn test_add_days_leap_year() {
let date = GameDate::from_ymd(2024, 2, 28).unwrap();
let leap_day = date.add_days(1);
assert_eq!(leap_day.to_api_string(), "2024-02-29");
let march_1 = date.add_days(2);
assert_eq!(march_1.to_api_string(), "2024-03-01");
let date_2023 = GameDate::from_ymd(2023, 2, 28).unwrap();
let march_1_2023 = date_2023.add_days(1);
assert_eq!(march_1_2023.to_api_string(), "2023-03-01");
}
#[test]
fn test_add_days_with_now() {
let now = GameDate::Now;
let future = now.add_days(7);
match future {
GameDate::Date(_) => {} GameDate::Now => panic!("add_days(7) on Now should return a specific date"),
}
let future_str = future.to_api_string();
assert_ne!(future_str, "now");
assert!(future_str.contains("-")); }
#[test]
fn test_add_days_large_values() {
let date = GameDate::from_ymd(2024, 1, 1).unwrap();
let next_year = date.add_days(366);
assert_eq!(next_year.to_api_string(), "2025-01-01");
let prev_year = date.add_days(-365);
assert_eq!(prev_year.to_api_string(), "2023-01-01");
let far_future = date.add_days(365 * 5);
assert_eq!(far_future.to_api_string(), "2028-12-30");
}
#[test]
fn test_add_days_chaining() {
let date = GameDate::from_ymd(2024, 10, 15).unwrap();
let result = date.add_days(5).add_days(3).add_days(-2);
assert_eq!(result.to_api_string(), "2024-10-21");
let direct = date.add_days(6);
assert_eq!(result.to_api_string(), direct.to_api_string());
}
#[test]
fn test_season_from_i32() {
let season: Season = 20232024_i32.into();
assert_eq!(season.start_year, 2023);
assert_eq!(season.end_year(), 2024);
let season: Season = 19992000_i32.into();
assert_eq!(season.start_year, 1999);
assert_eq!(season.end_year(), 2000);
let season: Season = 19171918_i32.into();
assert_eq!(season.start_year, 1917);
let season: Season = 20502051_i32.into();
assert_eq!(season.start_year, 2050);
}
#[test]
fn test_season_from_i64() {
let season: Season = 20232024_i64.into();
assert_eq!(season.start_year, 2023);
assert_eq!(season.end_year(), 2024);
let season: Season = 19992000_i64.into();
assert_eq!(season.start_year, 1999);
let season_i32: Season = 20232024_i32.into();
let season_i64: Season = 20232024_i64.into();
assert_eq!(season_i32, season_i64);
}
#[test]
fn test_season_from_u16() {
let season: Season = 2023_u16.into();
assert_eq!(season.start_year, 2023);
assert_eq!(season.end_year(), 2024);
assert_eq!(season.to_api_string(), "20232024");
let season: Season = 1999_u16.into();
assert_eq!(season.start_year, 1999);
let from_new = Season::new(2023);
let from_u16: Season = 2023_u16.into();
assert_eq!(from_new, from_u16);
}
#[test]
fn test_season_hash() {
use std::collections::HashSet;
let season1 = Season::new(2023);
let season2 = Season::new(2023);
let season3 = Season::new(2024);
let mut set = HashSet::new();
set.insert(season1);
set.insert(season2); set.insert(season3);
assert_eq!(set.len(), 2);
assert!(set.contains(&Season::new(2023)));
assert!(set.contains(&Season::new(2024)));
assert!(!set.contains(&Season::new(2022)));
}
#[test]
fn test_season_copy() {
let season1 = Season::new(2023);
let season2 = season1;
assert_eq!(season1.start_year, 2023);
assert_eq!(season2.start_year, 2023);
assert_eq!(season1, season2);
}
#[test]
fn test_season_eq() {
let season1 = Season::new(2023);
let season2 = Season::new(2023);
let season3 = Season::new(2024);
assert_eq!(season1, season1); assert_eq!(season1, season2); assert_eq!(season2, season1);
assert_ne!(season1, season3);
assert_ne!(season3, season1);
}
#[test]
fn test_season_parse_non_numeric() {
assert!(Season::parse("abcd2024").is_none());
assert!(Season::parse("2023abcd").is_none());
assert!(Season::parse("2023-024").is_none());
assert!(Season::parse("2023/024").is_none());
assert!(Season::parse("2023 024").is_none());
assert!(Season::parse(" 2032024").is_none());
}
#[test]
fn test_game_date_clone() {
let date = GameDate::from_ymd(2024, 10, 19).unwrap();
let cloned = date.clone();
assert_eq!(date, cloned);
assert_eq!(date.to_api_string(), cloned.to_api_string());
let now = GameDate::Now;
let now_cloned = now.clone();
assert_eq!(now, now_cloned);
}
#[test]
fn test_game_date_debug() {
let date = GameDate::from_ymd(2024, 10, 19).unwrap();
let debug_str = format!("{:?}", date);
assert!(debug_str.contains("Date"));
assert!(debug_str.contains("2024"));
let now = GameDate::Now;
let debug_str = format!("{:?}", now);
assert_eq!(debug_str, "Now");
}
#[test]
fn test_season_debug() {
let season = Season::new(2023);
let debug_str = format!("{:?}", season);
assert!(debug_str.contains("Season"));
assert!(debug_str.contains("2023"));
}
#[test]
fn test_season_clone() {
let season = Season::new(2023);
let cloned = season.clone();
assert_eq!(season, cloned);
assert_eq!(season.start_year, cloned.start_year);
}
#[test]
fn test_season_parse_boundary_years() {
assert!(Season::parse("00000001").is_some());
assert!(Season::parse("99999999").is_none());
let season = Season::parse("99989999").unwrap();
assert_eq!(season.start_year, 9998);
}
#[test]
fn test_game_date_as_date_now_variant() {
let now = GameDate::Now;
let tomorrow = now.add_days(1);
let yesterday_from_tomorrow = tomorrow.add_days(-1);
let today = GameDate::today();
assert_eq!(
yesterday_from_tomorrow.to_api_string(),
today.to_api_string()
);
}
#[test]
fn test_season_from_years_equivalence() {
let from_years = Season::from_years(2023, 2024);
let from_new = Season::new(2023);
assert_eq!(from_years, from_new);
assert_eq!(from_years.start_year, from_new.start_year);
assert_eq!(from_years.end_year(), from_new.end_year());
}
}