functional markdown generation

This commit is contained in:
2019-03-09 15:12:50 -05:00
parent f785062d70
commit c2e90731ab
6 changed files with 959 additions and 239 deletions

301
src/analysis.rs Normal file
View 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
View 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
}
}

View File

@@ -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(())
}

View File

@@ -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");
}

View File

@@ -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()?;

View File

@@ -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