scoring starter code, and flake to stable

This commit is contained in:
Julia Lange 2026-02-01 13:04:33 -08:00
parent 544e380835
commit 5487a1801f
Signed by: Julia
SSH key fingerprint: SHA256:KI8YxpkPRbnDRkXPgCuQCVz181++Vy7NAvmQj8alOhM
6 changed files with 98 additions and 7 deletions

View file

@ -9,7 +9,6 @@ pub use channel::Channel;
mod item;
pub use item::Item;
macro_rules! define_id {
($name:ident) => {
#[derive(PartialEq, Debug, Copy, Clone)]

View file

@ -4,6 +4,7 @@ type Result<T> = std::result::Result<T, Box<dyn Error>>;
pub mod db;
pub mod fetch;
pub mod score;
#[cfg(test)]
pub mod test_utils;

89
koucha/src/score.rs Normal file
View file

@ -0,0 +1,89 @@
use chrono::{DateTime, Utc, TimeDelta};
mod default {
pub const INITIAL_SCORE: i64 = 70;
pub const GRAVITY: i64 = -10;
pub const BOOST: i64 = 12;
}
macro_rules! rich_i64 {
($name:ident, $default:expr) => {
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct $name(i64);
impl From<$name> for i64 { fn from(id: $name) -> Self { id.0 } }
impl $name {
pub fn new(value: Option<i64>) -> Self {
Self(value.unwrap_or($default))
}
}
};
}
rich_i64!(Gravity, default::GRAVITY);
rich_i64!(Boost, default::BOOST);
impl Boost {
fn should_apply(
old_boost_time: Option<DateTime<Utc>>, new_boost_time: DateTime<Utc>
) -> bool {
old_boost_time.map_or(true, |obs| {
let time_delta = obs - &new_boost_time;
time_delta.num_days() >= 1
})
}
}
pub struct Score {
value: i64,
last_updated: DateTime<Utc>,
last_boosted: Option<DateTime<Utc>>,
}
impl Score {
pub fn new() -> Self {
Self::new_at_time(Utc::now())
}
pub fn new_at_time(creation_time: DateTime<Utc>) -> Self {
Score {
value: default::INITIAL_SCORE,
last_updated: creation_time,
last_boosted: Some(creation_time),
}
}
pub fn update(self) -> Self {
self.update_at_time(Utc::now())
}
pub fn update_at_time(self, update_time: DateTime<Utc>) -> Self {
let to_elapse_from = self.last_boosted.map_or(self.last_updated, |lb| {
std::cmp::max(self.last_updated, lb + TimeDelta::days(1))
});
let elapsed_time = update_time.signed_duration_since(to_elapse_from);
self
}
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
}
}
}