diff --git a/Cargo.lock b/Cargo.lock index 3ca555c..fb737b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,12 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cc" version = "1.0.83" @@ -67,13 +73,25 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "fsrs" -version = "1.0.0" +version = "1.1.0" dependencies = [ "chrono", + "rand", "serde", "serde_json", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "iana-time-zone" version = "0.1.59" @@ -114,9 +132,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.151" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "log" @@ -139,6 +157,15 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.74" @@ -157,6 +184,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "ryu" version = "1.0.16" @@ -211,6 +268,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.89" @@ -387,3 +450,24 @@ name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 2a58730..3719a65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,15 @@ [package] name = "fsrs" -version = "1.0.0" +version = "1.1.0" edition = "2021" [dependencies] -chrono = {version = "0.4.23", features = ["serde"]} +chrono = { version = "0.4.23", features = ["serde"] } serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0.93", optional = true } +[dev-dependencies] +rand = "0.8.5" + [features] -serde = [ - "dep:serde", - "dep:serde_json" -] +serde = ["dep:serde", "dep:serde_json"] diff --git a/src/alea.rs b/src/alea.rs new file mode 100644 index 0000000..2a5ab38 --- /dev/null +++ b/src/alea.rs @@ -0,0 +1,161 @@ +use crate::Seed; + +#[derive(Debug, PartialEq)] +pub struct AleaState { + pub c: f64, + pub s0: f64, + pub s1: f64, + pub s2: f64, +} + +impl From for AleaState { + fn from(alea: Alea) -> Self { + Self { + c: alea.c, + s0: alea.s0, + s1: alea.s1, + s2: alea.s2, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Alea { + c: f64, + s0: f64, + s1: f64, + s2: f64, +} + +impl Alea { + fn new(seed: Seed) -> Self { + let mut mash = Mash::new(); + let blank_seed = Seed::new(" "); + let mut alea = Self { + c: 1.0, + s0: mash.mash(&blank_seed), + s1: mash.mash(&blank_seed), + s2: mash.mash(&blank_seed), + }; + + alea.s0 -= mash.mash(&seed); + if alea.s0 < 0.0 { + alea.s0 += 1.0; + } + alea.s1 -= mash.mash(&seed); + if alea.s1 < 0.0 { + alea.s1 += 1.0; + } + alea.s2 -= mash.mash(&seed); + if alea.s2 < 0.0 { + alea.s2 += 1.0; + } + + alea + } +} + +impl Iterator for Alea { + type Item = f64; + + fn next(&mut self) -> Option { + let t = 2091639.0f64.mul_add(self.s0, self.c * TWO_TO_THE_POWER_OF_MINUS_32); + self.s0 = self.s1; + self.s1 = self.s2; + self.c = t.floor(); + self.s2 = t - self.c; + + Some(self.s2) + } +} + +impl From for Alea { + fn from(state: AleaState) -> Self { + Self { + c: state.c, + s0: state.s0, + s1: state.s1, + s2: state.s2, + } + } +} + +const TWO_TO_THE_POWER_OF_32: u64 = 0x100000000; // 2^32 +const TWO_TO_THE_POWER_OF_21: u64 = 0x200000; // 2^21 +const TWO_TO_THE_POWER_OF_MINUS_32: f64 = 1.0 / ((1_u64 << 32) as f64); +const TWO_TO_THE_POWER_OF_MINUS_53: f64 = 1.0 / ((1_u64 << 53) as f64); + +struct Mash { + n: f64, +} + +impl Mash { + const N: u64 = 0xefc8249d; + const fn new() -> Self { + Self { n: Self::N as f64 } + } + + fn mash(&mut self, seed: &Seed) -> f64 { + let mut n: f64 = self.n; + for c in seed.inner_str().chars() { + n += c as u32 as f64; + let mut h = 0.02519603282416938 * n; + n = (h as u32) as f64; + h -= n; + h *= n; + n = (h as u32) as f64; + h -= n; + n += h * TWO_TO_THE_POWER_OF_32 as f64; + } + self.n = n; + self.n * TWO_TO_THE_POWER_OF_MINUS_32 // 2^-32 + } +} + +#[derive(Debug)] +pub struct Prng { + pub xg: Alea, +} + +impl Prng { + fn new(seed: Seed) -> Self { + Self { + xg: Alea::new(seed), + } + } + + pub fn gen_next(&mut self) -> f64 { + self.xg.next().unwrap() + } + + pub fn int32(&mut self) -> i32 { + wrap_to_i32(self.gen_next() * TWO_TO_THE_POWER_OF_32 as f64) + } + + pub fn double(&mut self) -> f64 { + ((self.gen_next() * TWO_TO_THE_POWER_OF_21 as f64) as u64 as f64) + .mul_add(TWO_TO_THE_POWER_OF_MINUS_53, self.gen_next()) + } + + pub fn get_state(&self) -> AleaState { + self.xg.into() + } + + pub fn import_state(mut self, state: AleaState) -> Self { + self.xg = state.into(); + self + } +} + +// The rem_euclid() wraps within a positive range, then casting u32 to i32 makes half of that range negative. +fn wrap_to_i32(input: f64) -> i32 { + input.rem_euclid((u32::MAX as f64) + 1.0) as u32 as i32 +} + +pub fn alea(seed: Seed) -> Prng { + match seed { + Seed::String(_) => Prng::new(seed), + Seed::Empty => Prng::new(Seed::default()), + Seed::Default => Prng::new(Seed::default()), + } +} diff --git a/src/algo.rs b/src/algo.rs index 03ca9f7..beb7a4e 100644 --- a/src/algo.rs +++ b/src/algo.rs @@ -6,7 +6,7 @@ use crate::ImplScheduler; use chrono::{DateTime, Utc}; -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone)] pub struct FSRS { parameters: Parameters, } @@ -18,9 +18,9 @@ impl FSRS { pub fn scheduler(&self, card: Card, now: DateTime) -> Box { if self.parameters.enable_short_term { - Box::new(BasicScheduler::new(self.parameters, card, now)) + Box::new(BasicScheduler::new(self.parameters.clone(), card, now)) } else { - Box::new(LongtermScheduler::new(self.parameters, card, now)) + Box::new(LongtermScheduler::new(self.parameters.clone(), card, now)) } } diff --git a/src/lib.rs b/src/lib.rs index 78aa403..74a3845 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,9 @@ mod algo; pub use algo::FSRS; +mod alea; +pub use alea::{alea, Alea, AleaState, Prng}; + mod scheduler; pub use scheduler::{ImplScheduler, Scheduler}; @@ -14,5 +17,5 @@ mod models; pub use models::{Card, Rating, RecordLog, ReviewLog, SchedulingInfo, State}; mod parameters; -pub use crate::parameters::Parameters; +pub use crate::parameters::{Parameters, Seed}; mod tests; diff --git a/src/parameters.rs b/src/parameters.rs index ca664e4..9356b06 100644 --- a/src/parameters.rs +++ b/src/parameters.rs @@ -1,3 +1,8 @@ +use core::f64; + +use chrono::Utc; + +use crate::alea; use crate::Rating; type Weights = [f64; 19]; @@ -6,7 +11,7 @@ const DEFAULT_WEIGHTS: Weights = [ 2.0225, 0.0904, 0.3025, 2.1214, 0.2498, 2.9466, 0.4891, 0.6468, ]; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct Parameters { pub request_retention: f64, pub maximum_interval: i32, @@ -14,6 +19,8 @@ pub struct Parameters { pub decay: f64, pub factor: f64, pub enable_short_term: bool, + pub enable_fuzz: bool, + pub seed: Seed, } impl Parameters { @@ -37,10 +44,12 @@ impl Parameters { } #[allow(clippy::suboptimal_flops)] - pub fn next_interval(&self, stability: f64) -> i64 { - let new_interval = - stability / Self::FACTOR * (self.request_retention.powf(1.0 / Self::DECAY) - 1.0); - (new_interval.round() as i64).clamp(1, self.maximum_interval as i64) + pub fn next_interval(&self, stability: f64, elapsed_days: i64) -> f64 { + let new_interval = (stability / Self::FACTOR + * (self.request_retention.powf(1.0 / Self::DECAY) - 1.0)) + .round() + .clamp(1.0, self.maximum_interval as f64); + self.apply_fuzz(new_interval, elapsed_days) } pub fn next_difficulty(&self, difficulty: f64, rating: Rating) -> f64 { @@ -92,6 +101,22 @@ impl Parameters { fn mean_reversion(&self, initial: f64, current: f64) -> f64 { self.w[7].mul_add(initial, (1.0 - self.w[7]) * current) } + + fn apply_fuzz(&self, interval: f64, elapsed_days: i64) -> f64 { + if !self.enable_fuzz || interval < 2.5 { + return interval; + } + + let mut generator = alea(self.seed.clone()); + let fuzz_factor = generator.double(); + let (min_interval, max_interval) = + FuzzRange::get_fuzz_range(interval, elapsed_days, self.maximum_interval); + + fuzz_factor.mul_add( + max_interval as f64 - min_interval as f64 + 1.0, + min_interval as f64, + ) + } } impl Default for Parameters { @@ -103,6 +128,110 @@ impl Default for Parameters { decay: Self::DECAY, factor: Self::FACTOR, enable_short_term: true, + enable_fuzz: false, + seed: Seed::default(), + } + } +} + +struct FuzzRange { + start: f64, + end: f64, + factor: f64, +} + +impl FuzzRange { + const fn new(start: f64, end: f64, factor: f64) -> Self { + Self { start, end, factor } + } + + fn get_fuzz_range(interval: f64, elapsed_days: i64, maximum_interval: i32) -> (i64, i64) { + let mut delta: f64 = 1.0; + for fuzz_range in FUZZ_RANGE { + delta += fuzz_range.factor + * f64::max(f64::min(interval, fuzz_range.end) - fuzz_range.start, 0.0); + } + + let i = f64::min(interval, maximum_interval as f64); + let mut min_interval = f64::max(2.0, f64::round(i - delta)); + let max_interval: f64 = f64::min(f64::round(i + delta), maximum_interval as f64); + + if i > elapsed_days as f64 { + min_interval = f64::max(min_interval, elapsed_days as f64 + 1.0); + } + + min_interval = f64::min(min_interval, max_interval); + + (min_interval as i64, max_interval as i64) + } +} + +const FUZZ_RANGE: [FuzzRange; 3] = [ + FuzzRange::new(2.5, 7.0, 0.15), + FuzzRange::new(7.0, 20.0, 0.1), + FuzzRange::new(20.0, f64::MAX, 0.05), +]; + +#[derive(Debug, Clone)] +pub enum Seed { + String(String), + Empty, + Default, +} + +impl Seed { + pub fn new(value: T) -> Self + where + T: std::fmt::Display, + { + if value.to_string().is_empty() { + Self::default() + } else { + Self::String(value.to_string()) } } + + pub fn inner_str(&self) -> &str { + match self { + Self::String(str) => str, + Self::Empty => Self::Default.inner_str(), + Self::Default => Self::Default.inner_str(), + } + } +} + +impl std::fmt::Display for Seed { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.inner_str()) + } +} + +impl From<&Seed> for String { + fn from(d: &Seed) -> Self { + d.inner_str().to_string() + } +} + +impl From for Seed { + fn from(num: i32) -> Self { + Self::String(num.to_string()) + } +} + +impl From for Seed { + fn from(s: String) -> Self { + Self::String(s) + } +} + +impl<'a> From<&'a str> for Seed { + fn from(s: &'a str) -> Self { + Self::String(s.to_string()) + } +} + +impl Default for Seed { + fn default() -> Self { + Self::String(Utc::now().timestamp_millis().to_string()) + } } diff --git a/src/scheduler.rs b/src/scheduler.rs index 3d661ba..e5ebdde 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use crate::models::State::*; +use crate::Seed; use crate::{ models::{RecordLog, SchedulingInfo}, Card, Parameters, Rating, ReviewLog, @@ -24,14 +25,16 @@ impl Scheduler { }; current_card.last_review = now; current_card.reps += 1; - - Self { + let mut scheduler = Self { parameters, last: card, current: current_card, now, next: RecordLog::new(), - } + }; + scheduler.init_seed(); + + scheduler } pub const fn build_log(&self, rating: Rating) -> ReviewLog { @@ -43,6 +46,13 @@ impl Scheduler { reviewed_date: self.now, } } + + fn init_seed(&mut self) { + let time = self.now.timestamp_millis(); + let reps = self.current.reps; + let mul = self.current.difficulty * self.current.stability; + self.parameters.seed = Seed::new(format!("{}_{}_{}", time, reps, mul)); + } } pub trait ImplScheduler { diff --git a/src/scheduler_basic.rs b/src/scheduler_basic.rs index 30a8211..39cf2d8 100644 --- a/src/scheduler_basic.rs +++ b/src/scheduler_basic.rs @@ -38,9 +38,12 @@ impl BasicScheduler { next.state = Learning; } Easy => { - let easy_interval = self.scheduler.parameters.next_interval(next.stability); - next.scheduled_days = easy_interval; - next.due = self.scheduler.now + Duration::days(easy_interval); + let easy_interval = self + .scheduler + .parameters + .next_interval(next.stability, next.elapsed_days); + next.scheduled_days = easy_interval as i64; + next.due = self.scheduler.now + Duration::days(easy_interval as i64); next.state = Review; } }; @@ -59,6 +62,7 @@ impl BasicScheduler { } let mut next = self.scheduler.current.clone(); + let interval = self.scheduler.current.elapsed_days; next.difficulty = self .scheduler .parameters @@ -80,9 +84,12 @@ impl BasicScheduler { next.state = self.scheduler.last.state; } Good => { - let good_interval = self.scheduler.parameters.next_interval(next.stability); - next.scheduled_days = good_interval; - next.due = self.scheduler.now + Duration::days(good_interval); + let good_interval = self + .scheduler + .parameters + .next_interval(next.stability, interval); + next.scheduled_days = good_interval as i64; + next.due = self.scheduler.now + Duration::days(good_interval as i64); next.state = Review; } Easy => { @@ -90,14 +97,17 @@ impl BasicScheduler { .scheduler .parameters .short_term_stability(self.scheduler.last.stability, Good); - let good_interval = self.scheduler.parameters.next_interval(good_stability); + let good_interval = self + .scheduler + .parameters + .next_interval(good_stability, interval); let easy_interval = self .scheduler .parameters - .next_interval(next.stability) - .max(good_interval + 1); - next.scheduled_days = easy_interval; - next.due = self.scheduler.now + Duration::days(easy_interval); + .next_interval(next.stability, interval) + .max(good_interval + 1.0); + next.scheduled_days = easy_interval as i64; + next.due = self.scheduler.now + Duration::days(easy_interval as i64); next.state = Review; } } @@ -143,6 +153,7 @@ impl BasicScheduler { &mut next_hard, &mut next_good, &mut next_easy, + interval, ); self.next_state( &mut next_again, @@ -225,28 +236,35 @@ impl BasicScheduler { next_hard: &mut Card, next_good: &mut Card, next_easy: &mut Card, + elapsed_days: i64, ) { - let mut hard_interval = self.scheduler.parameters.next_interval(next_hard.stability); - let mut good_interval = self.scheduler.parameters.next_interval(next_good.stability); + let mut hard_interval = self + .scheduler + .parameters + .next_interval(next_hard.stability, elapsed_days); + let mut good_interval = self + .scheduler + .parameters + .next_interval(next_good.stability, elapsed_days); hard_interval = hard_interval.min(good_interval); - good_interval = good_interval.max(hard_interval + 1); + good_interval = good_interval.max(hard_interval + 1.0); let easy_interval = self .scheduler .parameters - .next_interval(next_easy.stability) - .max(good_interval + 1); + .next_interval(next_easy.stability, elapsed_days) + .max(good_interval + 1.0); next_again.scheduled_days = 0; next_again.due = self.scheduler.now + Duration::minutes(5); - next_hard.scheduled_days = hard_interval; - next_hard.due = self.scheduler.now + Duration::days(hard_interval); + next_hard.scheduled_days = hard_interval as i64; + next_hard.due = self.scheduler.now + Duration::days(hard_interval as i64); - next_good.scheduled_days = good_interval; - next_good.due = self.scheduler.now + Duration::days(good_interval); + next_good.scheduled_days = good_interval as i64; + next_good.due = self.scheduler.now + Duration::days(good_interval as i64); - next_easy.scheduled_days = easy_interval; - next_easy.due = self.scheduler.now + Duration::days(easy_interval); + next_easy.scheduled_days = easy_interval as i64; + next_easy.due = self.scheduler.now + Duration::days(easy_interval as i64); } fn next_state( diff --git a/src/scheduler_longterm.rs b/src/scheduler_longterm.rs index 66b939b..fb33887 100644 --- a/src/scheduler_longterm.rs +++ b/src/scheduler_longterm.rs @@ -39,6 +39,7 @@ impl LongtermScheduler { &mut next_hard, &mut next_good, &mut next_easy, + 0, ); self.next_state( &mut next_again, @@ -88,6 +89,7 @@ impl LongtermScheduler { &mut next_hard, &mut next_good, &mut next_easy, + interval, ); self.next_state( &mut next_again, @@ -169,31 +171,41 @@ impl LongtermScheduler { next_hard: &mut Card, next_good: &mut Card, next_easy: &mut Card, + elapsed_days: i64, ) { let mut again_interval = self .scheduler .parameters - .next_interval(next_again.stability); - let mut hard_interval = self.scheduler.parameters.next_interval(next_hard.stability); - let mut good_interval = self.scheduler.parameters.next_interval(next_good.stability); - let mut easy_interval = self.scheduler.parameters.next_interval(next_easy.stability); + .next_interval(next_again.stability, elapsed_days); + let mut hard_interval = self + .scheduler + .parameters + .next_interval(next_hard.stability, elapsed_days); + let mut good_interval = self + .scheduler + .parameters + .next_interval(next_good.stability, elapsed_days); + let mut easy_interval = self + .scheduler + .parameters + .next_interval(next_easy.stability, elapsed_days); again_interval = again_interval.min(hard_interval); - hard_interval = hard_interval.max(again_interval + 1); - good_interval = good_interval.max(hard_interval + 1); - easy_interval = easy_interval.max(good_interval + 1); + hard_interval = hard_interval.max(again_interval + 1.0); + good_interval = good_interval.max(hard_interval + 1.0); + easy_interval = easy_interval.max(good_interval + 1.0); - next_again.scheduled_days = again_interval; - next_again.due = self.scheduler.now + Duration::days(again_interval); + next_again.scheduled_days = again_interval as i64; + next_again.due = self.scheduler.now + Duration::days(again_interval as i64); - next_hard.scheduled_days = hard_interval; - next_hard.due = self.scheduler.now + Duration::days(hard_interval); + next_hard.scheduled_days = hard_interval as i64; + next_hard.due = self.scheduler.now + Duration::days(hard_interval as i64); - next_good.scheduled_days = good_interval; - next_good.due = self.scheduler.now + Duration::days(good_interval); + next_good.scheduled_days = good_interval as i64; + next_good.due = self.scheduler.now + Duration::days(good_interval as i64); - next_easy.scheduled_days = easy_interval; - next_easy.due = self.scheduler.now + Duration::days(easy_interval); + next_easy.scheduled_days = easy_interval as i64; + next_easy.due = self.scheduler.now + Duration::days(easy_interval as i64); } fn next_state( diff --git a/src/tests.rs b/src/tests.rs index 52b6953..4d58e7d 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,11 +1,13 @@ #[cfg(test)] use { crate::{ + alea::{alea, AleaState}, algo::FSRS, models::{Card, Rating, State}, - parameters::Parameters, + parameters::{Parameters, Seed}, }, chrono::{DateTime, Duration, TimeZone, Utc}, + rand::Rng, }; #[cfg(test)] @@ -170,3 +172,108 @@ fn test_long_term_scheduler() { assert_eq!(stability_history, expected_stability); assert_eq!(difficulty_history, expected_difficulty); } + +#[test] +fn test_prng_get_state() { + let prng_1 = alea(Seed::new(1)); + let prng_2 = alea(Seed::new(2)); + let prng_3 = alea(Seed::new(1)); + + let alea_state_1 = prng_1.get_state(); + let alea_state_2 = prng_2.get_state(); + let alea_state_3 = prng_3.get_state(); + + assert_eq!(alea_state_1, alea_state_3); + assert_ne!(alea_state_1, alea_state_2); +} + +#[test] +fn test_alea_get_next() { + let seed = Seed::new(12345); + let mut generator = alea(seed); + assert_eq!(generator.gen_next(), 0.27138191112317145); + assert_eq!(generator.gen_next(), 0.19615925149992108); + assert_eq!(generator.gen_next(), 0.6810678059700876); +} + +#[test] +fn test_alea_int32() { + let seed = Seed::new(12345); + let mut generator = alea(seed); + assert_eq!(generator.int32(), 1165576433); + assert_eq!(generator.int32(), 842497570); + assert_eq!(generator.int32(), -1369803343); +} + +#[test] +fn test_alea_import_state() { + let mut rng = rand::thread_rng(); + let mut prng_1 = alea(Seed::new(rng.gen::())); + prng_1.gen_next(); + prng_1.gen_next(); + prng_1.gen_next(); + let prng_1_state = prng_1.get_state(); + let mut prng_2 = alea(Seed::Empty).import_state(prng_1_state); + + assert_eq!(prng_1.get_state(), prng_2.get_state()); + + for _ in 1..10000 { + let a = prng_1.gen_next(); + let b = prng_2.gen_next(); + + assert_eq!(a, b); + assert!(a >= 0.0 && a < 1.0); + assert!(b >= 0.0 && b < 1.0); + } +} + +#[test] +fn test_seed_example_1() { + let seed = Seed::new("1727015666066"); + let mut generator = alea(seed); + let results = generator.gen_next(); + let state = generator.get_state(); + + let expect_alea_state = AleaState { + c: 1828249.0, + s0: 0.5888567129150033, + s1: 0.5074866858776659, + s2: 0.6320083506871015, + }; + assert_eq!(results, 0.6320083506871015); + assert_eq!(state, expect_alea_state); +} + +#[test] +fn test_seed_example_2() { + let seed = Seed::new("Seedp5fxh9kf4r0"); + let mut generator = alea(seed); + let results = generator.gen_next(); + let state = generator.get_state(); + + let expect_alea_state = AleaState { + c: 1776946.0, + s0: 0.6778371171094477, + s1: 0.0770602801349014, + s2: 0.14867847645655274, + }; + assert_eq!(results, 0.14867847645655274); + assert_eq!(state, expect_alea_state); +} + +#[test] +fn test_seed_example_3() { + let seed = Seed::new("NegativeS2Seed"); + let mut generator = alea(seed); + let results = generator.gen_next(); + let state = generator.get_state(); + + let expect_alea_state = AleaState { + c: 952982.0, + s0: 0.25224833423271775, + s1: 0.9213257452938706, + s2: 0.830770346801728, + }; + assert_eq!(results, 0.830770346801728); + assert_eq!(state, expect_alea_state); +}