functional markdown generation
This commit is contained in:
301
src/analysis.rs
Normal file
301
src/analysis.rs
Normal file
@@ -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<Team>,
|
||||
pub past_standings: Vec<TeamRecord>,
|
||||
pub standings: Vec<TeamRecord>,
|
||||
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<u32>,
|
||||
}
|
||||
|
||||
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<Matchup<'a>>,
|
||||
pub results: Vec<Matchup<'a>>,
|
||||
pub my_game: Option<Matchup<'a>>,
|
||||
pub games: Vec<Matchup<'a>>,
|
||||
pub own_division_seed: Vec<Seed<'a>>,
|
||||
pub other_division_seed: Vec<Seed<'a>>,
|
||||
pub wildcard_seed: Vec<Seed<'a>>,
|
||||
pub playoffs: Vec<PlayoffMatchup<'a>>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
165
src/generate.rs
Normal file
165
src/generate.rs
Normal file
@@ -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<Item = &'a Matchup<'a>>) -> 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<Item = &'a Matchup<'a>>) -> 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
|
||||
}
|
||||
}
|
||||
226
src/main.rs
226
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<Team>,
|
||||
past_standings: Vec<TeamRecord>,
|
||||
standings: Vec<TeamRecord>,
|
||||
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<u32>,
|
||||
}
|
||||
|
||||
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<Matchup<'a>>,
|
||||
pub results: Vec<Matchup<'a>>,
|
||||
pub my_game: Option<Matchup<'a>>,
|
||||
pub games: Vec<Matchup<'a>>,
|
||||
}
|
||||
|
||||
#[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(())
|
||||
}
|
||||
|
||||
351
src/markdown.rs
351
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<E>(&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<D>(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<D>(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<D>(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<D>(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<String>);
|
||||
|
||||
impl List {
|
||||
pub fn new() -> List {
|
||||
List(Vec::new())
|
||||
}
|
||||
|
||||
pub fn add<D>(&mut self, item: D)
|
||||
where
|
||||
D: Display,
|
||||
{
|
||||
self.0.push(item.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Extend<D> for List
|
||||
where
|
||||
D: Display,
|
||||
{
|
||||
fn extend<T>(&mut self, iter: T)
|
||||
where
|
||||
T: IntoIterator<Item = D>,
|
||||
{
|
||||
for item in iter.into_iter() {
|
||||
self.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, I> From<I> for List
|
||||
where
|
||||
D: Display,
|
||||
I: IntoIterator<Item = D>,
|
||||
{
|
||||
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<String>);
|
||||
|
||||
impl NumberedList {
|
||||
pub fn new() -> NumberedList {
|
||||
NumberedList(Vec::new())
|
||||
}
|
||||
|
||||
pub fn add<D>(&mut self, item: D)
|
||||
where
|
||||
D: Display,
|
||||
{
|
||||
self.0.push(item.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Extend<D> for NumberedList
|
||||
where
|
||||
D: Display,
|
||||
{
|
||||
fn extend<T>(&mut self, iter: T)
|
||||
where
|
||||
T: IntoIterator<Item = D>,
|
||||
{
|
||||
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<String>,
|
||||
rows: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
pub fn new<D, I>(headers: I) -> Table
|
||||
where
|
||||
D: Display,
|
||||
I: IntoIterator<Item = D>,
|
||||
{
|
||||
Table {
|
||||
headers: headers.into_iter().map(|h| h.to_string()).collect(),
|
||||
rows: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add<D, I>(&mut self, row: I)
|
||||
where
|
||||
D: Display,
|
||||
I: IntoIterator<Item = D>,
|
||||
{
|
||||
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<D>(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");
|
||||
}
|
||||
|
||||
@@ -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<Utc>,
|
||||
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<Period>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Period {
|
||||
#[serde(rename = "periodType")]
|
||||
pub period_type: String,
|
||||
}
|
||||
|
||||
pub fn get(date: &NaiveDate) -> reqwest::Result<Date> {
|
||||
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<Records>,
|
||||
pub records: Vec<RootRecords>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct Records {
|
||||
struct RootRecords {
|
||||
#[serde(rename = "teamRecords")]
|
||||
pub team_records: Vec<TeamRecord>,
|
||||
}
|
||||
@@ -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<String> {
|
||||
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<Record>,
|
||||
}
|
||||
|
||||
#[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<Vec<TeamRecord>> {
|
||||
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()?;
|
||||
|
||||
@@ -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<Entry>,
|
||||
}
|
||||
@@ -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<u32> = entries
|
||||
|
||||
Reference in New Issue
Block a user