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

8
flake.lock generated
View file

@ -2,16 +2,16 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1768127708,
"narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=",
"lastModified": 1769933782,
"narHash": "sha256-GlZemJ2dxhXMMq6TNyt588OFv4/jIt3J1QVBO9MspBE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38",
"rev": "64728753f1a42c81c5688a136a6bee173665acc9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"ref": "nixos-25.11-small",
"repo": "nixpkgs",
"type": "github"
}

View file

@ -2,7 +2,7 @@
description = "Koucha rust flake";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11-small";
};
outputs = { self, nixpkgs }:

View file

@ -39,7 +39,9 @@ CREATE TABLE feeds (
CREATE TABLE feed_channels (
feed_id INTEGER NOT NULL,
channel_id INTEGER NOT NULL,
-- Decay settings will go here
initial_score INTEGER,
gravity INTEGER,
boost INTEGER,
PRIMARY KEY (feed_id, channel_id),
FOREIGN KEY (feed_id) REFERENCES feeds(id),
FOREIGN KEY (channel_id) REFERENCES channels(id)

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
}
}
}