diff --git a/src/analysis.rs b/src/analysis.rs new file mode 100644 index 0000000..4a63b36 --- /dev/null +++ b/src/analysis.rs @@ -0,0 +1,301 @@ +use std::cmp::Reverse; +use std::collections::BTreeSet; + +use crate::nhlapi::{self, schedule::Game, standings::TeamRecord, teams::Team}; +use crate::simulation; + +pub struct Api { + pub teams: Vec, + pub past_standings: Vec, + pub standings: Vec, + pub results: nhlapi::schedule::Date, + pub games: nhlapi::schedule::Date, +} + +impl Api { + pub fn download() -> Api { + let teams = nhlapi::teams::get().expect("error getting teams"); + let past_standings = nhlapi::standings::yesterday().expect("error getting past standings"); + let standings = nhlapi::standings::today().expect("error getting standings"); + let results = nhlapi::schedule::yesterday().expect("error getting results"); + let games = nhlapi::schedule::today().expect("error getting games"); + + Api { + teams, + past_standings, + standings, + results, + games, + } + } + + pub fn get_team_by_abbrev(&self, abbrev: &str) -> &Team { + let abbrev = abbrev.to_ascii_uppercase(); + self.teams + .iter() + .find(|t| t.abbrev == abbrev) + .expect("team abbrev not found") + } + + pub fn get_team_by_id(&self, team_id: u32) -> &Team { + self.teams + .iter() + .find(|t| t.id == team_id) + .expect("team id not found") + } + + pub fn get_points(&self, team_id: u32, past: bool) -> u32 { + if !past { + self.standings + .iter() + .find(|t| t.team.id == team_id) + .expect("team id not found") + .points + } else { + self.past_standings + .iter() + .find(|t| t.team.id == team_id) + .expect("team id not found") + .points + } + } +} + +pub struct Analyzer<'a> { + api: &'a Api, + my_team: &'a Team, + own_conference_team_ids: BTreeSet, +} + +impl Analyzer<'_> { + pub fn new<'a>(api: &'a Api, my_team: &'a Team) -> Analyzer<'a> { + let mut own_conference_team_ids = BTreeSet::new(); + for team in &api.teams { + if team.conference.id == my_team.conference.id { + own_conference_team_ids.insert(team.id); + } + } + Analyzer { + api, + my_team, + own_conference_team_ids, + } + } + + pub fn perform(&self) -> Analysis { + let mut my_game = None; + let mut games = vec![]; + let mut my_result = None; + let mut results = vec![]; + + for game in &self.api.games.games { + let m = MatchupPre::create(self, game, false); + if m.is_relevant(self) { + if m.is_my_team_involed { + my_game = Some(m.pick_winner(self)); + } else { + games.push(m.pick_winner(self)); + } + } + } + + for game in &self.api.results.games { + let m = MatchupPre::create(self, game, true); + if m.is_relevant(self) { + if m.is_my_team_involed { + my_result = Some(m.pick_winner(self)); + } else { + results.push(m.pick_winner(self)); + } + } + } + + let mut own_division_seed = vec![]; + let mut other_division_seed = vec![]; + let mut wildcard_seed = vec![]; + for record in &self.api.standings { + if self.own_conference_team_ids.contains(&record.team.id) { + let team = self.api.get_team_by_id(record.team.id); + + if team.division.id == self.my_team.division.id { + if own_division_seed.len() < 3 { + own_division_seed.push(Seed { + seed: own_division_seed.len() as u32 + 1, + record: record, + }); + } else { + wildcard_seed.push(Seed { + seed: wildcard_seed.len() as u32 + 1, + record: record, + }) + } + } else { + if other_division_seed.len() < 3 { + other_division_seed.push(Seed { + seed: other_division_seed.len() as u32 + 1, + record: record, + }); + } else { + wildcard_seed.push(Seed { + seed: wildcard_seed.len() as u32 + 1, + record: record, + }) + } + } + } + } + + let mut tops = vec![&own_division_seed[0], &other_division_seed[0]]; + tops.sort_unstable_by_key(|s| Reverse(s.record.points)); + + let playoffs = vec![ + PlayoffMatchup::new(&tops[0].record, &wildcard_seed[1].record), + PlayoffMatchup::new(&tops[1].record, &wildcard_seed[0].record), + PlayoffMatchup::new(&own_division_seed[1].record, &own_division_seed[2].record), + PlayoffMatchup::new( + &other_division_seed[1].record, + &other_division_seed[2].record, + ), + ]; + + Analysis { + my_team: self.my_team, + my_game: my_game, + games: games, + my_result: my_result, + results: results, + own_division_seed, + other_division_seed, + wildcard_seed, + playoffs, + } + } +} + +#[derive(Debug)] +pub struct Seed<'a> { + pub seed: u32, + pub record: &'a TeamRecord, +} + +#[derive(Debug)] +pub struct PlayoffMatchup<'a> { + pub high_team: &'a TeamRecord, + pub low_team: &'a TeamRecord, +} + +impl PlayoffMatchup<'_> { + fn new<'a>(high_team: &'a TeamRecord, low_team: &'a TeamRecord) -> PlayoffMatchup<'a> { + PlayoffMatchup { + high_team, + low_team, + } + } +} + +#[derive(Debug)] +pub struct Analysis<'a> { + pub my_team: &'a Team, + pub my_result: Option>, + pub results: Vec>, + pub my_game: Option>, + pub games: Vec>, + pub own_division_seed: Vec>, + pub other_division_seed: Vec>, + pub wildcard_seed: Vec>, + pub playoffs: Vec>, +} + +#[derive(Debug)] +pub struct Matchup<'a> { + pub game: &'a Game, + pub is_result: bool, + pub is_my_team_involed: bool, + pub ideal_loser: &'a nhlapi::Team, +} + +impl Matchup<'_> { + pub fn cheer_for(&self) -> &nhlapi::Team { + if self.game.home_team().id == self.ideal_loser.id { + self.game.away_team() + } else if self.game.away_team().id == self.ideal_loser.id { + self.game.home_team() + } else { + panic!("invalid match loser") + } + } + + pub fn get_mood(&self) -> &str { + if self.game.loser().id == self.ideal_loser.id { + if self.game.overtime() { + "Good" + } else { + "Great" + } + } else { + "Bad" + } + } +} + +struct MatchupPre<'a> { + pub game: &'a Game, + pub is_result: bool, + pub is_my_team_involed: bool, +} + +impl<'m> MatchupPre<'m> { + pub fn create<'a>(a: &'a Analyzer, game: &'a Game, is_result: bool) -> MatchupPre<'a> { + let is_my_team_involed = + game.teams.away.team.id == a.my_team.id || game.teams.home.team.id == a.my_team.id; + MatchupPre { + game, + is_result, + is_my_team_involed, + } + } + + pub fn is_relevant(&self, a: &Analyzer) -> bool { + self.is_my_team_involed + || a.own_conference_team_ids + .contains(&self.game.home_team().id) + || a.own_conference_team_ids + .contains(&self.game.away_team().id) + } + + pub fn pick_winner(self, a: &'m Analyzer) -> Matchup<'m> { + let home_team = self.game.home_team(); + let away_team = self.game.away_team(); + + let ideal_loser = if self.is_my_team_involed { + if a.my_team.id == home_team.id { + away_team + } else if a.my_team.id == away_team.id { + home_team + } else { + panic!("unexpected case in pick_winner"); + } + } else if a.own_conference_team_ids.contains(&home_team.id) + && !a.own_conference_team_ids.contains(&away_team.id) + { + home_team + } else if a.own_conference_team_ids.contains(&away_team.id) + && !a.own_conference_team_ids.contains(&home_team.id) + { + away_team + } else { + if self.is_result { + simulation::pick_ideal_loser(a.api, a.my_team, &a.api.past_standings, self.game) + } else { + simulation::pick_ideal_loser(a.api, a.my_team, &a.api.standings, self.game) + } + }; + + Matchup { + game: self.game, + is_result: self.is_result, + is_my_team_involed: self.is_my_team_involed, + ideal_loser, + } + } +} diff --git a/src/generate.rs b/src/generate.rs new file mode 100644 index 0000000..33b64be --- /dev/null +++ b/src/generate.rs @@ -0,0 +1,165 @@ +use std::iter; + +use crate::analysis::{Analysis, Api, Matchup, PlayoffMatchup, Seed}; +use crate::markdown::*; +use crate::nhlapi::{self, standings::TeamRecord}; +use crate::simulation; + +pub struct MarkdownGenerator<'a> { + api: &'a Api, + an: &'a Analysis<'a>, +} + +impl MarkdownGenerator<'_> { + pub fn new<'a>(api: &'a Api, an: &'a Analysis<'a>) -> MarkdownGenerator<'a> { + MarkdownGenerator { api, an } + } + + fn fmt_team(&self, team: &nhlapi::Team) -> String { + let team = self.api.get_team_by_id(team.id); + format!("{}", team.abbrev) + } + + fn fmt_vs(&self, home_team: &nhlapi::Team, away_team: &nhlapi::Team) -> String { + format!( + "{} at {}", + self.fmt_team(away_team), + self.fmt_team(home_team) + ) + } + + fn fmt_seed(&self, record: &TeamRecord) -> String { + format!( + "{} ({})", + self.fmt_team(&record.team), + record.conference_rank + ) + } + + fn make_result_table<'a>(&self, matchups: impl Iterator>) -> Table { + let mut table = Table::new(&["Game", "Score", "Overtime"]); + for m in matchups { + table.add(&[ + self.fmt_vs(m.game.home_team(), m.game.away_team()), + format!( + "{}-{} {}", + m.game.teams.home.score, + m.game.teams.away.score, + self.fmt_team(m.game.winner()) + ), + m.get_mood().to_string(), + ]); + } + table + } + + fn make_game_table<'a>(&self, games: impl Iterator>) -> Table { + let mut table = Table::new(&["Game", "Cheer for", "Time"]); + for m in games { + table.add(&[ + self.fmt_vs(m.game.home_team(), m.game.away_team()), + self.fmt_team(m.cheer_for()), + m.game.local_time(), + ]); + } + table + } + + fn make_standings_table(&self, seeds: &[Seed], wildcard: bool) -> Table { + let mut table = Table::new(&[ + "Place", "Team", "GP", "Record", "Points", "ROW", "L10", "P%", "P-82", + ]); + for (index, seed) in seeds.iter().enumerate() { + let record = &seed.record; + + if index == 2 && wildcard { + table.add(&["-", "-", "-", "-", "-", "-", "-", "-", "-"]); + } + + table.add(&[ + format!("{}", seed.seed), + self.fmt_team(&record.team), + format!("{}", record.games_played), + record.format(), + format!("{}", record.points), + format!("{}", record.row), + record.last10().unwrap_or("".into()), + record.point_percent(), + record.point_82(), + ]); + } + table + } + + fn make_playoffs_table(&self, playoffs: &[PlayoffMatchup]) -> Table { + let mut table = Table::new(&["High seed", "", "Low seed"]); + for pm in playoffs { + table.add(&[ + self.fmt_seed(&pm.high_team), + "vs".to_string(), + self.fmt_seed(&pm.low_team), + ]); + } + table + } + + pub fn markdown(&self) -> Document { + let mut doc = Document::new(); + doc.add(H1::new("Playoffs race!")); + + let yesterday_odds = simulation::odds_for_team(self.api, self.an.my_team, true); + let today_odds = simulation::odds_for_team(self.api, self.an.my_team, false); + + doc.add(Paragraph::new(format!( + "Playoffs odds yesterday: {:.1}%, playoffs odds today: {:.1}%", + yesterday_odds * 100.0, + today_odds * 100.0 + ))); + + // + // Last night + // + doc.add(H2::new("Last night's race")); + + doc.add(List::from(&["Our race:"])); + if let Some(my_result) = &self.an.my_result { + doc.add(self.make_result_table(iter::once(my_result))); + } else { + doc.add(Paragraph::new("Nothing")); + } + + doc.add(List::from(&["Outside of town"])); + doc.add(self.make_result_table(self.an.results.iter())); + + // + // Standings + // + doc.add(H2::new("Standings")); + doc.add(self.make_standings_table(&self.an.own_division_seed, false)); + doc.add(self.make_standings_table(&self.an.other_division_seed, false)); + doc.add(self.make_standings_table(&self.an.wildcard_seed, true)); + + // + // Playoffs matchups + // + doc.add(H2::new("Playoffs matchups")); + doc.add(self.make_playoffs_table(&self.an.playoffs)); + + // + // Tonight + // + doc.add(H2::new("Tonight's race")); + + doc.add(List::from(&["Our race:"])); + if let Some(my_game) = &self.an.my_game { + doc.add(self.make_game_table(iter::once(my_game))); + } else { + doc.add(Paragraph::new("Nothing")); + } + + doc.add(List::from(&["Outside of town"])); + doc.add(self.make_game_table(self.an.games.iter())); + + doc + } +} diff --git a/src/main.rs b/src/main.rs index c6285a9..7eb0d05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,217 +1,31 @@ -use std::collections::BTreeSet; - -use nhlapi::schedule::Game; -use nhlapi::standings::TeamRecord; -use nhlapi::teams::Team; +#![allow(dead_code)] +mod analysis; +mod generate; +mod markdown; mod nhlapi; mod simulation; -pub struct Api { - teams: Vec, - past_standings: Vec, - standings: Vec, - results: nhlapi::schedule::Date, - games: nhlapi::schedule::Date, -} - -impl Api { - pub fn get_team_by_abbrev(&self, abbrev: &str) -> &Team { - let abbrev = abbrev.to_ascii_uppercase(); - self.teams - .iter() - .find(|t| t.abbrev == abbrev) - .expect("team abbrev not found") - } - - pub fn get_team_by_id(&self, team_id: u32) -> &Team { - self.teams - .iter() - .find(|t| t.id == team_id) - .expect("team id not found") - } - - pub fn get_points(&self, team_id: u32, past: bool) -> u32 { - if !past { - self.standings - .iter() - .find(|t| t.team.id == team_id) - .expect("team id not found") - .points - } else { - self.past_standings - .iter() - .find(|t| t.team.id == team_id) - .expect("team id not found") - .points - } - } -} - -struct Analyzer<'a> { - api: &'a Api, - my_team: &'a Team, - own_conference_team_ids: BTreeSet, -} - -impl Analyzer<'_> { - pub fn new<'a>(api: &'a Api, my_team: &'a Team) -> Analyzer<'a> { - let mut own_conference_team_ids = BTreeSet::new(); - for team in &api.teams { - if team.conference.id == my_team.conference.id { - own_conference_team_ids.insert(team.id); - } - } - Analyzer { - api, - my_team, - own_conference_team_ids, - } - } - - pub fn perform(&self) -> Analysis { - let mut my_game = None; - let mut games = vec![]; - let mut my_result = None; - let mut results = vec![]; - - for game in &self.api.games.games { - let m = MatchupPre::create(self, game, false); - if m.is_relevant(self) { - if m.is_my_team_involed { - my_game = Some(m.pick_winner(self)); - } else { - games.push(m.pick_winner(self)); - } - } - } - - for game in &self.api.results.games { - let m = MatchupPre::create(self, game, true); - if m.is_relevant(self) { - if m.is_my_team_involed { - my_result = Some(m.pick_winner(self)); - } else { - results.push(m.pick_winner(self)); - } - } - } - - Analysis { - my_team: self.my_team, - my_game: my_game, - games: games, - my_result: my_result, - results: results, - } - } -} - -#[derive(Debug)] -struct Analysis<'a> { - pub my_team: &'a Team, - pub my_result: Option>, - pub results: Vec>, - pub my_game: Option>, - pub games: Vec>, -} - -#[derive(Debug)] -struct Matchup<'a> { - pub game: &'a Game, - pub is_result: bool, - pub is_my_team_involed: bool, - pub ideal_winner: &'a nhlapi::Team, -} - -struct MatchupPre<'a> { - pub game: &'a Game, - pub is_result: bool, - pub is_my_team_involed: bool, -} - -impl<'m> MatchupPre<'m> { - pub fn create<'a>(a: &'a Analyzer, game: &'a Game, is_result: bool) -> MatchupPre<'a> { - let is_my_team_involed = - game.teams.away.team.id == a.my_team.id || game.teams.home.team.id == a.my_team.id; - MatchupPre { - game, - is_result, - is_my_team_involed, - } - } - - pub fn is_relevant(&self, a: &Analyzer) -> bool { - self.is_my_team_involed - || a.own_conference_team_ids - .contains(&self.game.home_team().id) - || a.own_conference_team_ids - .contains(&self.game.away_team().id) - } - - pub fn pick_winner(self, a: &'m Analyzer) -> Matchup<'m> { - let home_team = self.game.home_team(); - let away_team = self.game.away_team(); - - let ideal_winner = if self.is_my_team_involed { - if a.my_team.id == home_team.id { - home_team - } else if a.my_team.id == away_team.id { - away_team - } else { - panic!("unexpected case in pick_winner"); - } - } else if a.own_conference_team_ids.contains(&home_team.id) - && !a.own_conference_team_ids.contains(&away_team.id) - { - away_team - } else if a.own_conference_team_ids.contains(&away_team.id) - && !a.own_conference_team_ids.contains(&home_team.id) - { - home_team - } else { - const TIMES: u32 = 50_000; - if self.is_result { - simulation::pick_ideal_winner( - a.api, - a.my_team, - &a.api.past_standings, - self.game, - TIMES, - ) - } else { - simulation::pick_ideal_winner(a.api, a.my_team, &a.api.standings, self.game, TIMES) - } - }; - - Matchup { - game: self.game, - is_result: self.is_result, - is_my_team_involed: self.is_my_team_involed, - ideal_winner: ideal_winner, - } - } -} +use analysis::{Analyzer, Api}; +use generate::MarkdownGenerator; fn main() -> reqwest::Result<()> { - let teams = nhlapi::teams::get().expect("error getting teams"); - let past_standings = nhlapi::standings::yesterday().expect("error getting past standings"); - let standings = nhlapi::standings::today().expect("error getting standings"); - let results = nhlapi::schedule::yesterday().expect("error getting results"); - let games = nhlapi::schedule::today().expect("error getting games"); + let api = Api::download(); - let api = Api { - teams, - past_standings, - standings, - results, - games, - }; + // let teams = ["mtl", "car", "cbj"]; + // for abbrev in &teams { + // let team = api.get_team_by_abbrev(abbrev); + // println!( + // "{} {:.3}", + // team.abbrev, + // simulation::odds_for_team(&api, team, 50_000) + // ); + // } - let a = Analyzer::new(&api, api.get_team_by_abbrev("mtl")); - let xd = a.perform(); - - println!("{:#?}", xd); + let analyzer = Analyzer::new(&api, api.get_team_by_abbrev("mtl")); + let an = analyzer.perform(); + let gen = MarkdownGenerator::new(&api, &an); + println!("{}", gen.markdown().as_str()); Ok(()) } diff --git a/src/markdown.rs b/src/markdown.rs index e69de29..2e8fee5 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -0,0 +1,351 @@ +use std::fmt::{self, Display, Write}; +use std::iter::Extend; + +pub trait Element: Display {} + +pub struct Document { + buff: String, +} + +impl Document { + pub fn new() -> Document { + Document { + buff: String::new(), + } + } + + pub fn add(&mut self, elem: E) + where + E: Element, + { + let _ = write!(self.buff, "{}", elem); + } + + pub fn as_str(&self) -> &str { + &self.buff[..] + } +} + +// Elements + +/// Paragraph +pub struct Paragraph(String); + +impl Paragraph { + pub fn new(content: D) -> Paragraph + where + D: Display, + { + Paragraph(content.to_string()) + } +} + +impl Display for Paragraph { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}\n", self.0) + } +} + +impl Element for Paragraph {} + +/// H1 header +pub struct H1(String); + +impl H1 { + pub fn new(content: D) -> H1 + where + D: Display, + { + H1(content.to_string()) + } +} + +impl Display for H1 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "# {}\n", self.0) + } +} + +impl Element for H1 {} + +/// H2 header +pub struct H2(String); + +impl H2 { + pub fn new(content: D) -> H2 + where + D: Display, + { + H2(content.to_string()) + } +} + +impl Display for H2 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "## {}\n", self.0) + } +} + +impl Element for H2 {} + +/// H3 header +pub struct H3(String); + +impl H3 { + pub fn new(content: D) -> H3 + where + D: Display, + { + H3(content.to_string()) + } +} + +impl Display for H3 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "### {}\n", self.0) + } +} + +impl Element for H3 {} + +/// List +pub struct List(Vec); + +impl List { + pub fn new() -> List { + List(Vec::new()) + } + + pub fn add(&mut self, item: D) + where + D: Display, + { + self.0.push(item.to_string()) + } +} + +impl Extend for List +where + D: Display, +{ + fn extend(&mut self, iter: T) + where + T: IntoIterator, + { + for item in iter.into_iter() { + self.add(item); + } + } +} + +impl From for List +where + D: Display, + I: IntoIterator, +{ + fn from(iter: I) -> List { + let mut list = List::new(); + list.extend(iter); + list + } +} + +impl Display for List { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for item in self.0.iter() { + write!(f, "* {}\n", item)?; + } + write!(f, "\n") + } +} + +impl Element for List {} + +/// Numbered List +pub struct NumberedList(Vec); + +impl NumberedList { + pub fn new() -> NumberedList { + NumberedList(Vec::new()) + } + + pub fn add(&mut self, item: D) + where + D: Display, + { + self.0.push(item.to_string()) + } +} + +impl Extend for NumberedList +where + D: Display, +{ + fn extend(&mut self, iter: T) + where + T: IntoIterator, + { + for item in iter.into_iter() { + self.add(item); + } + } +} + +impl Display for NumberedList { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for (index, item) in self.0.iter().enumerate() { + write!(f, "{}. {}\n", index + 1, item)?; + } + write!(f, "\n") + } +} + +impl Element for NumberedList {} + +/// Table +pub struct Table { + headers: Vec, + rows: Vec>, +} + +impl Table { + pub fn new(headers: I) -> Table + where + D: Display, + I: IntoIterator, + { + Table { + headers: headers.into_iter().map(|h| h.to_string()).collect(), + rows: vec![], + } + } + + pub fn add(&mut self, row: I) + where + D: Display, + I: IntoIterator, + { + let row: Vec<_> = row.into_iter().map(|i| i.to_string()).collect(); + if row.len() != self.headers.len() { + panic!("number of rows is not the same as the number of headers"); + } + self.rows.push(row); + } +} + +impl Display for Table { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for (index, header) in self.headers.iter().enumerate() { + if index > 0 { + write!(f, "|{}", header)?; + } else { + write!(f, "{}", header)?; + } + } + write!(f, "\n")?; + + for (index, _) in self.headers.iter().enumerate() { + if index > 0 { + write!(f, "|:---:")?; + } else { + write!(f, ":---:")?; + } + } + write!(f, "\n")?; + + for row in self.rows.iter() { + for (index, item) in row.iter().enumerate() { + if index > 0 { + write!(f, "|{}", item)?; + } else { + write!(f, "{}", item)?; + } + } + write!(f, "\n")?; + } + + write!(f, "\n") + } +} + +impl Element for Table {} + +/// Code +pub struct Code(String); + +impl Code { + pub fn new(content: D) -> Code + where + D: Display, + { + Code(content.to_string()) + } +} + +impl Display for Code { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "\n")?; + for line in self.0.lines() { + write!(f, " {}\n", line)?; + } + write!(f, "\n") + } +} + +impl Element for Code {} + +#[test] +fn test_h1() { + let mut doc = Document::new(); + doc.add(H1::new("hello")); + assert_eq!(doc.as_str(), "# hello\n"); +} + +#[test] +fn test_h2() { + let mut doc = Document::new(); + doc.add(H2::new("hello")); + assert_eq!(doc.as_str(), "## hello\n"); +} + +#[test] +fn test_h3() { + let mut doc = Document::new(); + doc.add(H3::new("hello")); + assert_eq!(doc.as_str(), "### hello\n"); +} + +#[test] +fn test_list() { + let mut doc = Document::new(); + let mut list = List::new(); + list.extend(&["hello", "world"]); + doc.add(list); + assert_eq!(doc.as_str(), "* hello\n* world\n\n"); +} + +#[test] +fn test_numbered_list() { + let mut doc = Document::new(); + let mut list = NumberedList::new(); + list.extend(&["hello", "world"]); + doc.add(list); + assert_eq!(doc.as_str(), "1. hello\n2. world\n\n"); +} + +#[test] +fn test_table_format() { + let mut doc = Document::new(); + let mut table = Table::new(&["and", "T", "F"]); + table.add(&["T", "T", "F"]); + table.add(&["F", "F", "F"]); + doc.add(table); + assert_eq!(doc.as_str(), "and|T|F\n:---:|:---:|:---:\nT|T|F\nF|F|F\n\n"); +} + +#[test] +fn test_code() { + let mut doc = Document::new(); + doc.add(Code::new("let x = 3;\nlet y = x**2;\n")); + assert_eq!(doc.as_str(), "\n let x = 3;\n let y = x**2;\n\n"); +} diff --git a/src/nhlapi.rs b/src/nhlapi.rs index 819805a..295d898 100644 --- a/src/nhlapi.rs +++ b/src/nhlapi.rs @@ -1,4 +1,3 @@ -#![allow(dead_code)] //! Docs: https://gitlab.com/dword4/nhlapi use std::fmt::Display; @@ -87,15 +86,44 @@ pub mod schedule { #[serde(rename = "gameDate")] pub game_date: DateTime, pub teams: Teams, + pub linescore: LineScore, } impl Game { pub fn home_team(&self) -> &Team { &self.teams.home.team } + pub fn away_team(&self) -> &Team { &self.teams.away.team } + + pub fn winner(&self) -> &Team { + if self.teams.home.score > self.teams.away.score { + self.home_team() + } else { + self.away_team() + } + } + + pub fn loser(&self) -> &Team { + if self.teams.home.score > self.teams.away.score { + self.away_team() + } else { + self.home_team() + } + } + + pub fn local_time(&self) -> String { + self.game_date + .with_timezone(&Local) + .format("%H:%M") + .to_string() + } + + pub fn overtime(&self) -> bool { + self.linescore.periods.len() > 3 + } } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -112,6 +140,17 @@ pub mod schedule { pub score: u32, } + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct LineScore { + pub periods: Vec, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct Period { + #[serde(rename = "periodType")] + pub period_type: String, + } + pub fn get(date: &NaiveDate) -> reqwest::Result { let date = format!("{}", date.format("%Y-%m-%d")); @@ -130,7 +169,7 @@ pub mod schedule { let client = reqwest::Client::new(); let root: Root = client - .get("https://statsapi.web.nhl.com/api/v1/schedule") + .get("https://statsapi.web.nhl.com/api/v1/schedule?expand=schedule.linescore") .query(&[("startDate", begin), ("endDate", end)]) .send()? .json()?; @@ -154,11 +193,11 @@ pub mod standings { #[derive(Debug, Clone, Deserialize, Serialize)] struct Root { - pub records: Vec, + pub records: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] - struct Records { + struct RootRecords { #[serde(rename = "teamRecords")] pub team_records: Vec, } @@ -186,13 +225,59 @@ pub mod standings { pub league_rank: u32, #[serde(rename = "wildCardRank", deserialize_with = "from_str")] pub wildcard_rank: u32, + + pub records: Records, + } + + impl TeamRecord { + pub fn format(&self) -> String { + format!( + "{}-{}-{}", + self.league_record.wins, self.league_record.losses, self.league_record.ot + ) + } + + pub fn last10(&self) -> Option { + self.records + .overall_records + .iter() + .find(|x| x.kind == "lastTen") + .map(|x| format!("{}-{}-{}", x.wins, x.losses, x.ot)) + } + + pub fn point_percent(&self) -> String { + format!("{:.3}", self.points as f64 / (self.games_played * 2) as f64) + } + + pub fn point_82(&self) -> String { + format!( + "{:.0}", + (self.points as f64 / self.games_played as f64) * 82.0 + ) + } + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct Records { + #[serde(rename = "overallRecords")] + overall_records: Vec, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct Record { + wins: u32, + losses: u32, + #[serde(default)] + ot: u32, + #[serde(rename = "type")] + kind: String, } pub fn get(date: &NaiveDate) -> reqwest::Result> { let date = format!("{}", date.format("%Y-%m-%d")); let client = reqwest::Client::new(); let mut root: Root = client - .get("https://statsapi.web.nhl.com/api/v1/standings/byLeague") + .get("https://statsapi.web.nhl.com/api/v1/standings/byLeague?expand=standings.record") .query(&[("date", date)]) .send()? .json()?; diff --git a/src/simulation.rs b/src/simulation.rs index 01a8679..2620a4a 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -9,11 +9,12 @@ use crate::nhlapi::standings::TeamRecord; use crate::nhlapi::teams::Team; use crate::Api; +pub const TIMES: u32 = 100_000; + #[derive(Debug, Copy, Clone)] struct Entry { team_id: u32, division_id: u32, - conference_id: u32, wins: u32, losses: u32, ot: u32, @@ -49,49 +50,40 @@ fn random_event(base: &Entry) -> Event { .0 } -pub fn pick_ideal_winner<'a>( +pub fn odds_for_team<'a>(api: &'a Api, team: &'a Team, past: bool) -> f64 { + let sim = if !past { + Simulation::new(api, team, &api.standings) + } else { + Simulation::new(api, team, &api.past_standings) + }; + let x = sim.run_for(TIMES); + x as f64 / TIMES as f64 +} + +pub fn pick_ideal_loser<'a>( api: &'a Api, my_team: &'a Team, records: &'a [TeamRecord], game: &'a Game, - times: u32, ) -> &'a nhlapi::Team { let mut home_win_sim = Simulation::new(api, my_team, records); home_win_sim.give_team_win(game.home_team().id); home_win_sim.give_team_loss(game.away_team().id); - let mut home_win_x = 0; - for _ in 0..times { - if home_win_sim.run() { - home_win_x += 1; - } - } + let home_win_x = home_win_sim.run_for(TIMES); let mut away_win_sim = Simulation::new(api, my_team, records); away_win_sim.give_team_win(game.away_team().id); away_win_sim.give_team_loss(game.home_team().id); - let mut away_win_x = 0; - for _ in 0..times { - if away_win_sim.run() { - away_win_x += 1; - } - } - - eprintln!( - "{} ({}) at {} ({})", - game.away_team().name, - away_win_x, - game.home_team().name, - home_win_x - ); + let away_win_x = away_win_sim.run_for(TIMES); if home_win_x > away_win_x { - game.home_team() - } else { game.away_team() + } else { + game.home_team() } } -struct Simulation<'a> { +pub struct Simulation<'a> { my_team: &'a Team, base: Vec, } @@ -105,7 +97,6 @@ impl Simulation<'_> { base.push(Entry { team_id: team.id, division_id: team.division.id, - conference_id: team.conference.id, wins: record.league_record.wins, losses: record.league_record.losses, ot: record.league_record.ot, @@ -132,7 +123,19 @@ impl Simulation<'_> { } } - pub fn run(&self) -> bool { + /// Run the simulation for `times` times, and return the number of times + /// `self.my_team` made the playoffs. + pub fn run_for(&self, times: u32) -> u32 { + let mut x = 0; + for _ in 0..times { + if self.run() { + x += 1 + } + } + x + } + + fn run(&self) -> bool { let mut entries = self.base.clone(); for (base, entry) in self.base.iter().zip(entries.iter_mut()) { while entry.games_played < 82 { @@ -146,6 +149,7 @@ impl Simulation<'_> { } } } + entries.sort_unstable_by_key(|e| Reverse((e.points, e.wins))); let top_3_teams: BTreeSet = entries