Skip to content

Commit

Permalink
Raise descriptive error instead of returning None
Browse files Browse the repository at this point in the history
  • Loading branch information
atomflunder committed Nov 24, 2022
1 parent b7a9b3e commit eeaf0fd
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 22 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

This is a broad overview of the changes that have been made over the lifespan of this library.

## v0.21.0 - 2022-11-24

- Return a more descriptive error on `get_first_dwz` function instead of returning None

## v0.20.0 - 2022-11-06

- Added FIFA rating algorithm (Men)
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "skillratings"
version = "0.20.0"
version = "0.21.0"
edition = "2021"
description = "Calculate a player's skill rating using algorithms like Elo, Glicko, Glicko-2, TrueSkill and many more."
readme = "README.md"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Alternatively, you can add the following to your `Cargo.toml` file manually:

```toml
[dependencies]
skillratings = "0.20"
skillratings = "0.21"
```

### Serde support
Expand All @@ -56,7 +56,7 @@ By editing `Cargo.toml` manually:

```toml
[dependencies]
skillratings = {version = "0.20", features = ["serde"]}
skillratings = {version = "0.21", features = ["serde"]}
```

## Usage and Examples
Expand Down
66 changes: 52 additions & 14 deletions src/dwz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
//! - [Official DWZ scoring system rules (German)](https://www.schachbund.de/wertungsordnung.html)
//! - [Probability Table](https://www.schachbund.de/wertungsordnung-anhang-2-tabellen/articles/wertungsordnung-anhang-21-wahrscheinlichkeitstabelle.html)
use std::collections::HashMap;
use std::{collections::HashMap, error::Error, fmt::Display};

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -121,6 +121,29 @@ impl From<EloRating> for DWZRating {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// The error types that can occur when calculating a new DWZ Rating.
/// Only gets raised in the [`get_first_dwz`] function.
pub enum GetFirstDWZError {
/// The player has played less than 5 games.
NotEnoughGames,
/// The player has a winrate of 0% or 100%.
InvalidWinRate,
}

impl Display for GetFirstDWZError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotEnoughGames => {
write!(f, "You need at least 5 games to calculate a DWZ Rating.")
}
Self::InvalidWinRate => write!(f, "Your winrate cannot be 0% or 100%."),
}
}
}

impl Error for GetFirstDWZError {}

#[must_use]
/// Calculates new [`DWZRating`] of two players based on their old rating, index, age and outcome of the game.
///
Expand Down Expand Up @@ -259,7 +282,6 @@ pub fn dwz(
/// assert_eq!(new_player.index, 18);
/// ```
pub fn dwz_rating_period(player: &DWZRating, results: &[(DWZRating, Outcomes)]) -> DWZRating {
// DWZ was designed to be used in tournaments, so we do not need to loop over the opponents here.
let points = results.iter().map(|r| r.1.to_chess_points()).sum();

let expected_points = results.iter().map(|r| expected_score(player, &r.0).0).sum();
Expand Down Expand Up @@ -317,7 +339,6 @@ pub fn expected_score(player_one: &DWZRating, player_two: &DWZRating) -> (f64, f
(exp_one, exp_two)
}

#[must_use]
/// Gets a proper first [`DWZRating`].
///
/// In the case that you do not have enough opponents to rate a player against,
Expand All @@ -327,8 +348,11 @@ pub fn expected_score(player_one: &DWZRating, player_two: &DWZRating) -> (f64, f
/// Takes in the player's age and their results as a Slice of tuples containing the opponent and the outcome.
/// If the actual player's age is unavailable or unknown, choose something `>25`.
///
/// This only returns a DWZ rating if the results include at least 5 matches,
/// and you don't have a 100% or a 0% win record. Otherwise it will return [`None`].
///
/// # Errors
///
/// This function returns [`GetFirstDWZError::NotEnoughGames`] if the player has played less than 5 games,
/// or [`GetFirstDWZError::InvalidWinRate`] if the player has a winrate of either 0% or 100%.
///
/// # Examples
/// ```
Expand Down Expand Up @@ -378,16 +402,19 @@ pub fn expected_score(player_one: &DWZRating, player_two: &DWZRating) -> (f64, f
/// assert!((player.rating - 1491.0).abs() < f64::EPSILON);
/// assert_eq!(player.index, 1);
/// ```
pub fn get_first_dwz(player_age: usize, results: &[(DWZRating, Outcomes)]) -> Option<DWZRating> {
pub fn get_first_dwz(
player_age: usize,
results: &[(DWZRating, Outcomes)],
) -> Result<DWZRating, GetFirstDWZError> {
if results.len() < 5 {
return None;
return Err(GetFirstDWZError::NotEnoughGames);
}

let points: f64 = results.iter().map(|r| r.1.to_chess_points()).sum();

// If you have a 100% or 0% win rate, we return None.
if (points - results.len() as f64).abs() < f64::EPSILON || points == 0.0 {
return None;
return Err(GetFirstDWZError::InvalidWinRate);
}

let average_rating = results.iter().map(|r| r.0.rating).sum::<f64>() / results.len() as f64;
Expand Down Expand Up @@ -457,20 +484,20 @@ pub fn get_first_dwz(player_age: usize, results: &[(DWZRating, Outcomes)]) -> Op
let mut new_rating = if p > 50 {
// If the performance is positive, we convert the values above to a positive number.
// The value for 30 is the same as for 70, but negative.
let temp = probability_table.get(&(p - 100).abs())?;
let temp = probability_table.get(&(p - 100).abs()).unwrap_or(&0.);

f64::abs(*temp) + average_rating
} else {
// Else we just use the negative number above.
probability_table.get(&p)? + average_rating
probability_table.get(&p).unwrap_or(&0.) + average_rating
};

// If the rating would be too low we revise it upwards.
if new_rating <= 800.0 {
new_rating = 700.0 + (new_rating / 8.0);
}

Some(DWZRating {
Ok(DWZRating {
rating: new_rating,
index: 1,
age: player_age,
Expand Down Expand Up @@ -733,7 +760,7 @@ mod tests {
],
);

assert_eq!(all_win_player, None);
assert_eq!(all_win_player, Err(GetFirstDWZError::InvalidWinRate));

let all_lose_player = get_first_dwz(
17,
Expand All @@ -746,7 +773,7 @@ mod tests {
],
);

assert_eq!(all_lose_player, None);
assert_eq!(all_lose_player, Err(GetFirstDWZError::InvalidWinRate));

let less_than_5 = get_first_dwz(
32,
Expand All @@ -758,7 +785,7 @@ mod tests {
],
);

assert_eq!(less_than_5, None);
assert_eq!(less_than_5, Err(GetFirstDWZError::NotEnoughGames));
}

#[test]
Expand Down Expand Up @@ -882,5 +909,16 @@ mod tests {
DWZRating::from((1400.0, 20)),
DWZRating::from((1400.0, 20, 26))
);

assert!(!format!("{:?}", GetFirstDWZError::NotEnoughGames).is_empty());
assert!(!format!("{:?}", GetFirstDWZError::InvalidWinRate).is_empty());

assert!(!format!("{}", GetFirstDWZError::NotEnoughGames).is_empty());
assert!(!format!("{}", GetFirstDWZError::InvalidWinRate).is_empty());

assert_eq!(
GetFirstDWZError::NotEnoughGames,
GetFirstDWZError::NotEnoughGames.clone()
);
}
}
6 changes: 3 additions & 3 deletions src/ingo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ mod tests {
fn test_ingo_rating_period() {
let player_one = IngoRating {
rating: 130.0,
age: 40,
age: 22,
};
let player_two = IngoRating {
rating: 160.0,
Expand All @@ -361,7 +361,7 @@ mod tests {

let player_four = IngoRating {
rating: 55.0,
age: 40,
age: 22,
};

let player_five = IngoRating {
Expand All @@ -378,7 +378,7 @@ mod tests {

let p1 = ingo_rating_period(&player_one, &results);

assert!((p1.rating.round() - 126.0).abs() < f64::EPSILON);
assert!((p1.rating.round() - 124.0).abs() < f64::EPSILON);
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
//!
//! ```toml
//! [dependencies]
//! skillratings = "0.20"
//! skillratings = "0.21"
//! ```
//!
//! ## Serde support
Expand All @@ -64,7 +64,7 @@
//!
//! ```toml
//! [dependencies]
//! skillratings = {version = "0.20", features = ["serde"]}
//! skillratings = {version = "0.21", features = ["serde"]}
//! ```
//!
//! # Usage and Examples
Expand Down

0 comments on commit eeaf0fd

Please sign in to comment.