finalized score (for now lol)

This commit is contained in:
Julia Lange 2026-02-02 14:50:50 -08:00
parent 5487a1801f
commit e353977da9
Signed by: Julia
SSH key fingerprint: SHA256:5DJcfxa5/fKCYn57dcabJa2vN2e6eT0pBerYi5SUbto

View file

@ -1,10 +1,15 @@
use chrono::{DateTime, Utc, TimeDelta}; use chrono::{DateTime, Utc, TimeDelta};
use crate::{Result};
mod default { mod default {
use crate::score::SECONDS_IN_A_DAY;
pub const INITIAL_SCORE: i64 = 70; pub const INITIAL_SCORE: i64 = 70;
pub const GRAVITY: i64 = -10; pub const GRAVITY: i64 = -10;
pub const BOOST: i64 = 12; pub const BOOST: i64 = 12;
pub const BOOST_FREEZE_IN_SECONDS: i64 = SECONDS_IN_A_DAY;
} }
const SECONDS_IN_A_DAY: i64 = 60 * 60 * 24;
macro_rules! rich_i64 { macro_rules! rich_i64 {
($name:ident, $default:expr) => { ($name:ident, $default:expr) => {
@ -21,69 +26,144 @@ macro_rules! rich_i64 {
rich_i64!(Gravity, default::GRAVITY); rich_i64!(Gravity, default::GRAVITY);
rich_i64!(Boost, default::BOOST); rich_i64!(Boost, default::BOOST);
impl Boost { pub struct UnparsedScore {
fn should_apply( pub value: i64,
old_boost_time: Option<DateTime<Utc>>, new_boost_time: DateTime<Utc> pub last_updated: DateTime<Utc>,
) -> bool { pub last_boosted: Option<DateTime<Utc>>,
old_boost_time.map_or(true, |obs| { }
let time_delta = obs - &new_boost_time;
time_delta.num_days() >= 1 impl UnparsedScore {
}) pub fn parse(self) -> Score {
match self.last_boosted {
None => Score::Decaying(DecayingScore {
value: self.value,
last_updated: self.last_updated,
}),
Some(last_boosted) => Score::Boosted(BoostedScore {
value: self.value,
boosted_at: last_boosted,
}),
}
} }
} }
pub struct Score {
value: i64, pub enum Score {
last_updated: DateTime<Utc>, Decaying(DecayingScore),
last_boosted: Option<DateTime<Utc>>, Boosted(BoostedScore),
} }
impl Score { impl Score {
pub fn new() -> Self { pub fn new() -> DecayingScore {
Self::new_at_time(Utc::now()) DecayingScore {
}
pub fn new_at_time(creation_time: DateTime<Utc>) -> Self {
Score {
value: default::INITIAL_SCORE, value: default::INITIAL_SCORE,
last_updated: creation_time, last_updated: Utc::now(),
last_boosted: Some(creation_time),
} }
} }
pub fn update(self) -> Self { pub fn get_score(&self) -> i64 {
self.update_at_time(Utc::now()) match self {
Self::Decaying(s) => s.get_score(),
Self::Boosted(b) => b.get_score(),
}
} }
pub fn update_at_time(self, update_time: DateTime<Utc>) -> Self { pub fn update_score(self, gravity: Gravity) -> Self {
let to_elapse_from = self.last_boosted.map_or(self.last_updated, |lb| { match self {
std::cmp::max(self.last_updated, lb + TimeDelta::days(1)) Self::Decaying(d) => Score::Decaying(d.update_score(gravity)),
}); Self::Boosted(b) => {
let elapsed_time = update_time.signed_duration_since(to_elapse_from); let try_unfrozen = b.try_unfreeze();
match try_unfrozen {
self Self::Decaying(s) => Score::Decaying(s.update_score(gravity)),
} Self::Boosted(b) => Score::Boosted(b),
}
pub fn boost(self, boost: Boost) -> Self {
self.boost_at_time(boost, Utc::now())
}
// This function does not "fast-forward" the score's value to the time of
// boost, but still updates `last_updated`. Meaning that hours of gravity may
// not be applied.
// The expectation is that a boost is a user interaction, and that the user
// should be seeing a "fresh" score. We wouldn't want a boost to end up
// applying lots of gravity. But care should be taken to ensure that only
// "fresh" scores are boosted. Or that boost_time reflects the state of the
// user experience.
pub fn boost_at_time(self, boost: Boost, boost_time: DateTime<Utc>) -> Self {
if Boost::should_apply(self.last_boosted, boost_time) {
Self {
value: self.value + i64::from(boost),
last_updated: boost_time,
last_boosted: Some(boost_time),
} }
} else { }
self }
pub fn get_decaying(self) -> Result<DecayingScore> {
match self {
Self::Decaying(s) => Ok(s),
Self::Boosted(_) => Err("Attempted to get_decaying() of a boosted score".into()),
}
}
pub fn get_boosted(self) -> Result<BoostedScore> {
match self {
Self::Decaying(_) => Err("Attempted to get_boosted() of a decaying score".into()),
Self::Boosted(b) => Ok(b),
}
}
}
pub struct BoostedScore {
value: i64,
boosted_at: DateTime<Utc>,
}
pub struct DecayingScore {
value: i64,
last_updated: DateTime<Utc>,
}
impl DecayingScore {
fn get_score(&self) -> i64 {
self.value
}
fn update_score(self, gravity: Gravity) -> Self {
self.update_score_at_time(gravity, Utc::now())
}
fn update_score_at_time(
self, gravity: Gravity, update_time: DateTime<Utc>
) -> Self {
let elapsed_time: TimeDelta = update_time.signed_duration_since(self.last_updated);
let new_value = if elapsed_time <= TimeDelta::zero() { self.value } else {
i64::from(gravity) * (elapsed_time.num_seconds() / SECONDS_IN_A_DAY)
};
Self {
last_updated: update_time,
value: new_value,
}
}
pub fn boost(self, boost: Boost) -> BoostedScore {
self.boost_at_time(boost, Utc::now())
}
fn boost_at_time(self, boost: Boost, boost_time: DateTime<Utc>) -> BoostedScore {
BoostedScore {
value: self.value + i64::from(boost),
boosted_at: boost_time,
}
}
}
impl BoostedScore {
fn get_score(&self) -> i64 {
self.value
}
pub fn unboost(self, boost: Boost) -> DecayingScore {
DecayingScore {
value: self.value - i64::from(boost),
last_updated: self.boosted_at,
}
}
fn try_unfreeze(self) -> Score {
self.try_unfreeze_at_time(Utc::now())
}
fn try_unfreeze_at_time(self, update_time: DateTime<Utc>) -> Score {
let boost_end = self.boosted_at + TimeDelta::seconds(default::BOOST_FREEZE_IN_SECONDS);
if boost_end > update_time {
Score::Decaying(DecayingScore {
value: self.value,
last_updated: boost_end,
})
} else {
Score::Boosted(self)
} }
} }
} }