use crate::league::LeagueId;
use crate::season::SeasonId;
use crate::team::TeamId;
use crate::{Copyright, HomeAway, MLB_API_DATE_FORMAT};
use bon::Builder;
use chrono::{DateTime, Datelike, Local, NaiveDate, Utc};
use either::Either;
use serde::de::Error;
use serde::{Deserialize, Deserializer};
use std::cmp::Ordering;
use std::fmt::{Debug, Display, Formatter};
use std::iter::Sum;
use std::num::NonZeroU32;
use std::ops::Add;
use crate::game::GameId;
use crate::meta::GameType;
use crate::request::RequestURL;
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(from = "AttendanceResponseStruct")]
pub struct AttendanceResponse {
pub copyright: Copyright,
#[serde(rename = "records")]
pub annual_records: Vec<AttendanceRecord>,
}
impl AttendanceResponse {
#[must_use]
pub fn into_aggregate(self) -> AttendanceRecord {
self.annual_records.into_iter().sum()
}
}
#[derive(Deserialize)]
struct AttendanceResponseStruct {
copyright: Copyright,
records: Vec<AttendanceRecord>,
}
impl From<AttendanceResponseStruct> for AttendanceResponse {
fn from(value: AttendanceResponseStruct) -> Self {
let AttendanceResponseStruct { copyright, records } = value;
Self { copyright, annual_records: records }
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(from = "AnnualRecordStruct")]
pub struct AttendanceRecord {
pub total_openings: HomeAway<u32>,
pub total_openings_lost: u32,
pub total_games: HomeAway<u32>,
pub season: SeasonWithMinorId,
pub attendance_totals: HomeAway<u32>,
pub single_opening_min_max: Option<(DatedAttendance, DatedAttendance)>,
pub game_type: GameType,
}
impl Add for AttendanceRecord {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
total_openings: HomeAway {
home: self.total_openings.home + rhs.total_openings.home,
away: self.total_openings.away + rhs.total_openings.away,
},
total_openings_lost: self.total_openings_lost + rhs.total_openings_lost,
total_games: HomeAway {
home: self.total_games.home + rhs.total_games.home,
away: self.total_games.away + rhs.total_games.away,
},
season: SeasonWithMinorId::max(self.season, rhs.season),
attendance_totals: HomeAway {
home: self.attendance_totals.home + rhs.attendance_totals.home,
away: self.attendance_totals.away + rhs.attendance_totals.away,
},
single_opening_min_max: match (self.single_opening_min_max, rhs.single_opening_min_max) {
(None, None) => None,
(Some(min_max), None) | (None, Some(min_max)) => Some(min_max),
(Some((a_min, a_max)), Some((b_min, b_max))) => Some((b_min.min(a_min), a_max.max(b_max))),
},
game_type: rhs.game_type,
}
}
}
impl Default for AttendanceRecord {
#[allow(clippy::cast_sign_loss, reason = "jesus is not alive")]
fn default() -> Self {
Self {
total_openings: HomeAway::new(0, 0),
total_openings_lost: 0,
total_games: HomeAway::new(0, 0),
season: (Local::now().year() as u32).into(),
attendance_totals: HomeAway::new(0, 0),
single_opening_min_max: None,
game_type: GameType::default(),
}
}
}
impl Sum for AttendanceRecord {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
iter.fold(Self::default(), |acc, x| acc + x)
}
}
impl AttendanceRecord {
#[must_use]
pub const fn average_attendance(&self) -> HomeAway<u32> {
let HomeAway { home, away } = self.attendance_totals;
let HomeAway { home: num_at_home, away: num_at_away } = self.total_openings;
HomeAway::new((home + num_at_home / 2) / num_at_home, (away + num_at_away / 2) / num_at_away)
}
}
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)]
pub struct SeasonWithMinorId {
season: SeasonId,
minor: Option<NonZeroU32>,
}
impl From<SeasonId> for SeasonWithMinorId {
fn from(value: SeasonId) -> Self {
Self { season: value, minor: None }
}
}
impl From<u32> for SeasonWithMinorId {
fn from(value: u32) -> Self {
Self { season: value.into(), minor: None }
}
}
impl<'de> Deserialize<'de> for SeasonWithMinorId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>
{
struct Visitor;
impl serde::de::Visitor<'_> for Visitor {
type Value = SeasonWithMinorId;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("a season id, or a string with a . denoting the minor")
}
fn visit_u32<E>(self, value: u32) -> Result<Self::Value, E>
where
E: Error
{
Ok(SeasonWithMinorId { season: SeasonId::from(value), minor: None })
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
if let Some((season, minor)) = v.split_once('.') {
let season = season.parse::<u32>().map_err(Error::custom)?;
let minor = minor.parse::<u32>().map_err(Error::custom)?;
let minor = NonZeroU32::try_from(minor).map_err(Error::custom)?;
Ok(SeasonWithMinorId { season: SeasonId::from(season), minor: Some(minor) })
} else {
Ok(v.parse::<u32>().map(|season| SeasonWithMinorId { season: SeasonId::from(season), minor: None }).map_err(Error::custom)?)
}
}
}
deserializer.deserialize_any(Visitor)
}
}
impl Display for SeasonWithMinorId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.season)?;
if let Some(minor) = self.minor {
write!(f, ".{minor}")?;
}
Ok(())
}
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
struct AnnualRecordStruct {
openings_total_away: u32,
openings_total_home: u32,
openings_total_lost: u32,
games_away_total: u32,
games_home_total: u32,
year: SeasonWithMinorId,
attendance_high: Option<u32>,
attendance_high_date: Option<DateTime<Utc>>,
attendance_high_game: Option<GameId>,
attendance_low: Option<u32>,
attendance_low_date: Option<DateTime<Utc>>,
attendance_low_game: Option<GameId>,
attendance_total_away: Option<u32>,
attendance_total_home: Option<u32>,
game_type: GameType,
}
impl From<AnnualRecordStruct> for AttendanceRecord {
#[allow(clippy::too_many_lines, reason = "low cognitive complexity")]
fn from(value: AnnualRecordStruct) -> Self {
let AnnualRecordStruct {
openings_total_away,
openings_total_home,
openings_total_lost,
games_away_total,
games_home_total,
year,
attendance_high,
attendance_high_date,
attendance_high_game,
attendance_low,
attendance_low_date,
attendance_low_game,
attendance_total_away,
attendance_total_home,
game_type,
} = value;
let single_opening_min_max = if let Some(((attendance_high, attendance_high_date), attendance_high_game)) = attendance_high
.zip(attendance_high_date).zip(attendance_high_game) {
let max = DatedAttendance {
value: attendance_high,
date: attendance_high_date.date_naive(),
game: attendance_high_game,
};
let min = {
if let Some(((attendance_low, attendance_low_date), attendance_low_game)) = attendance_low
.zip(attendance_low_date).zip(attendance_low_game) {
DatedAttendance {
value: attendance_low,
date: attendance_low_date.date_naive(),
game: attendance_low_game,
}
} else {
max.clone()
}
};
Some((min, max))
} else {
None
};
Self {
total_openings: HomeAway {
home: openings_total_home,
away: openings_total_away,
},
total_openings_lost: openings_total_lost,
total_games: HomeAway {
home: games_home_total,
away: games_away_total,
},
season: year,
attendance_totals: HomeAway {
home: attendance_total_home.unwrap_or(0),
away: attendance_total_away.unwrap_or(0),
},
single_opening_min_max,
game_type,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct DatedAttendance {
pub value: u32,
pub date: NaiveDate,
pub game: GameId,
}
impl PartialOrd<Self> for DatedAttendance {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DatedAttendance {
fn cmp(&self, other: &Self) -> Ordering {
self.value.cmp(&other.value)
}
}
#[derive(Builder)]
#[builder(derive(Into))]
pub struct AttendanceRequest {
#[doc(hidden)]
#[builder(setters(vis = "", name = __id_internal))]
id: Either<TeamId, LeagueId>,
#[builder(into)]
season: Option<SeasonWithMinorId>,
#[builder(into)]
date: Option<NaiveDate>,
#[builder(default)]
game_type: GameType,
}
impl<S: attendance_request_builder::State + attendance_request_builder::IsComplete> crate::request::RequestURLBuilderExt for AttendanceRequestBuilder<S> {
type Built = AttendanceRequest;
}
#[allow(dead_code, reason = "optionally used by the end user")]
impl<S: attendance_request_builder::State> AttendanceRequestBuilder<S> {
#[doc = "_**Required.**_\n\n"]
pub fn team_id(self, id: impl Into<TeamId>) -> AttendanceRequestBuilder<attendance_request_builder::SetId<S>>
where
S::Id: attendance_request_builder::IsUnset,
{
self.__id_internal(Either::Left(id.into()))
}
#[doc = "_**Required.**_\n\n"]
pub fn league_id(self, id: impl Into<LeagueId>) -> AttendanceRequestBuilder<attendance_request_builder::SetId<S>>
where
S::Id: attendance_request_builder::IsUnset,
{
self.__id_internal(Either::Right(id.into()))
}
}
impl Display for AttendanceRequest {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"http://statsapi.mlb.com/api/v1/attendance{}",
gen_params! { "teamId"?: self.id.left(), "leagueId"?: self.id.right(), "season"?: self.season, "date"?: self.date.as_ref().map(|date| date.format(MLB_API_DATE_FORMAT)), "gameType": format!("{:?}", self.game_type) }
)
}
}
impl RequestURL for AttendanceRequest {
type Response = AttendanceResponse;
}
#[cfg(test)]
mod tests {
use crate::attendance::AttendanceRequest;
use crate::request::{RequestURL, RequestURLBuilderExt};
use crate::team::TeamsRequest;
use crate::TEST_YEAR;
#[tokio::test]
#[cfg_attr(not(feature = "_heavy_tests"), ignore)]
async fn parse_all_teams_test_year() {
let mlb_teams = TeamsRequest::all_sports()
.season(TEST_YEAR)
.build_and_get()
.await
.unwrap()
.teams;
for team in mlb_teams {
let request = AttendanceRequest::builder()
.team_id(team.id)
.build();
let _response = request.get()
.await
.unwrap();
}
}
}