scoring starter code, and flake to stable
This commit is contained in:
parent
544e380835
commit
5487a1801f
6 changed files with 98 additions and 7 deletions
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
89
koucha/src/score.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue