use chrono::Utc;
use serde::{Deserialize, Serialize};
#[cfg(feature = "sqlx")]
use sqlx::FromRow;
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
pub struct UserInfo {
pub rating: f64,
pub reviews: i64,
pub operating_days: u64,
}
#[cfg_attr(feature = "sqlx", derive(FromRow))]
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
pub struct User {
pub pubkey: String,
pub is_admin: i64,
pub admin_password: Option<String>,
pub is_solver: i64,
pub is_banned: i64,
pub category: i64,
pub last_trade_index: i64,
pub total_reviews: i64,
pub total_rating: f64,
pub last_rating: i64,
pub max_rating: i64,
pub min_rating: i64,
pub created_at: i64,
}
impl User {
pub fn new(
pubkey: String,
is_admin: i64,
is_solver: i64,
is_banned: i64,
category: i64,
trade_index: i64,
) -> Self {
Self {
pubkey,
is_admin,
admin_password: None,
is_solver,
is_banned,
category,
last_trade_index: trade_index,
total_reviews: 0,
total_rating: 0.0,
last_rating: 0,
max_rating: 0,
min_rating: 0,
created_at: Utc::now().timestamp(),
}
}
pub fn update_rating(&mut self, rating: u8) {
self.total_reviews += 1;
let old_rating = self.total_rating;
if self.total_reviews <= 1 {
let first_rating = rating as f64;
self.total_rating = first_rating / 2.0;
self.max_rating = rating.into();
self.min_rating = rating.into();
} else {
self.total_rating =
old_rating + ((rating as f64) - old_rating) / (self.total_reviews as f64);
if self.max_rating < rating.into() {
self.max_rating = rating.into();
}
if self.min_rating > rating.into() {
self.min_rating = rating.into();
}
}
self.last_rating = rating.into();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_vote_is_weighted_by_half() {
let mut user = User::default();
user.update_rating(5);
assert_eq!(user.total_reviews, 1);
assert_eq!(user.total_rating, 2.5);
assert_eq!(user.last_rating, 5);
assert_eq!(user.max_rating, 5);
assert_eq!(user.min_rating, 5);
}
#[test]
fn second_vote_is_folded_into_the_average() {
let mut user = User::default();
user.update_rating(5);
user.update_rating(1);
assert_eq!(user.total_reviews, 2);
assert!((user.total_rating - 1.75).abs() < 1e-9);
assert_eq!(user.last_rating, 1);
}
#[test]
fn low_vote_lowers_a_high_average() {
let mut user = User::default();
for _ in 0..10 {
user.update_rating(5);
}
let farmed_average = user.total_rating;
assert!((farmed_average - 4.75).abs() < 1e-9);
user.update_rating(1);
let expected = 48.5 / 11.0;
assert!(
user.total_rating < farmed_average,
"a 1-star review must lower the average, got {} (was {})",
user.total_rating,
farmed_average
);
assert!((user.total_rating - expected).abs() < 1e-9);
assert_eq!(user.last_rating, 1);
assert_eq!(user.min_rating, 1);
assert_eq!(user.max_rating, 5);
}
#[test]
fn high_vote_raises_a_low_average() {
let mut user = User::default();
user.update_rating(1);
user.update_rating(1);
let low_average = user.total_rating;
user.update_rating(5);
assert!(
user.total_rating > low_average,
"a 5-star review must raise the average, got {} (was {})",
user.total_rating,
low_average
);
assert!((user.total_rating - 6.5 / 3.0).abs() < 1e-9);
}
#[test]
fn min_and_max_track_extremes() {
let mut user = User::default();
user.update_rating(3);
user.update_rating(5);
user.update_rating(1);
assert_eq!(user.max_rating, 5);
assert_eq!(user.min_rating, 1);
}
}