feat: DOC-FULL-004 ActiveTimeTracker

iOS控制tick节奏,Rust计算delta。start/pause/resume/tick/close。
毫秒余数累计。时间倒退不产生负数。8单元测试。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-07 19:41:09 +08:00
parent 82e8cdfc1f
commit 66649bb815
2 changed files with 167 additions and 0 deletions

View File

@ -16,3 +16,4 @@ pub mod reading_material;
pub mod search;
pub mod session_v2;
pub mod text;
pub mod time_tracker;

View File

@ -0,0 +1,166 @@
/// Tracks active reading time and calculates delta seconds.
///
/// iOS controls when to call tick (every 15s), pause (background), resume (foreground).
/// Rust does NOT create its own timer.
#[derive(Debug, Clone)]
pub struct ActiveTimeTracker {
last_tick_ms: Option<i64>,
is_active: bool,
remainder_ms: i64,
}
impl ActiveTimeTracker {
pub fn new() -> Self {
Self {
last_tick_ms: None,
is_active: false,
remainder_ms: 0,
}
}
/// Start tracking. Returns delta = 0.
pub fn start(&mut self, timestamp_ms: i64) -> u32 {
self.last_tick_ms = Some(timestamp_ms);
self.is_active = true;
self.remainder_ms = 0;
0
}
/// Pause tracking. Returns accumulated delta since last tick.
pub fn pause(&mut self, timestamp_ms: i64) -> u32 {
let delta = self.tick(timestamp_ms);
self.is_active = false;
delta
}
/// Resume after pause. No delta produced.
pub fn resume(&mut self, timestamp_ms: i64) {
self.is_active = true;
self.last_tick_ms = Some(timestamp_ms);
}
/// Regular tick. Returns delta seconds since last tick (or pause).
/// Returns 0 if not active or time goes backwards.
pub fn tick(&mut self, timestamp_ms: i64) -> u32 {
if !self.is_active {
return 0;
}
let last = match self.last_tick_ms {
Some(t) => t,
None => {
self.last_tick_ms = Some(timestamp_ms);
return 0;
}
};
if timestamp_ms <= last {
return 0;
}
let elapsed_ms = timestamp_ms - last;
let total_ms = elapsed_ms + self.remainder_ms;
let delta_seconds = (total_ms / 1000) as u32;
self.remainder_ms = total_ms % 1000;
self.last_tick_ms = Some(timestamp_ms);
delta_seconds
}
/// Close tracking. Returns final residual delta.
pub fn close(&mut self, timestamp_ms: i64) -> u32 {
let delta = self.tick(timestamp_ms);
self.is_active = false;
delta
}
pub fn is_active(&self) -> bool {
self.is_active
}
}
impl Default for ActiveTimeTracker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_start_returns_zero() {
let mut t = ActiveTimeTracker::new();
assert_eq!(t.start(1000), 0);
assert!(t.is_active());
}
#[test]
fn test_43_seconds_total() {
let mut t = ActiveTimeTracker::new();
t.start(0);
let d1 = t.tick(15_000); // 15s
let d2 = t.tick(30_000); // 15s
let d3 = t.close(43_000); // 13s residual
assert_eq!(d1, 15);
assert_eq!(d2, 15);
assert_eq!(d3, 13);
assert_eq!(d1 + d2 + d3, 43);
}
#[test]
fn test_pause_stops_time() {
let mut t = ActiveTimeTracker::new();
t.start(0);
t.tick(15_000); // 15s
t.pause(15_000);
t.resume(45_000); // 30s pause
let d = t.tick(60_000); // 15s after resume
assert_eq!(d, 15); // only 15s counted, not 45s
}
#[test]
fn test_time_goes_backwards() {
let mut t = ActiveTimeTracker::new();
t.start(10000);
assert_eq!(t.tick(5000), 0); // backwards
assert_eq!(t.tick(25000), 15); // normal after backwards reset
}
#[test]
fn test_inactive_tick_returns_zero() {
let mut t = ActiveTimeTracker::new();
assert_eq!(t.tick(5000), 0); // not started
t.start(0);
t.pause(5000);
assert_eq!(t.tick(10000), 0); // paused
}
#[test]
fn test_close_returns_residual() {
let mut t = ActiveTimeTracker::new();
t.start(0);
t.tick(15000);
let d = t.close(20500); // 5.5s → 5s, 500ms remainder
assert_eq!(d, 5);
assert!(!t.is_active());
}
#[test]
fn test_remainder_accumulates() {
let mut t = ActiveTimeTracker::new();
t.start(0);
// Each tick: 1500ms → 1s + 500ms remainder
let d1 = t.tick(1500);
let d2 = t.tick(3000);
let d3 = t.tick(4500);
// Total: 3 ticks → d1=1, d2=2(with remainder), d3=1
// 1500ms = 1s, remainder 500ms
// tick2: 1500ms + 500ms remainder = 2000ms = 2s
// tick3: 1500ms = 1s, remainder 500ms
assert_eq!(d1 + d2 + d3, 4); // 1 + 2 + 1 = 4
}
#[test]
fn test_close_inactive_returns_zero() {
let mut t = ActiveTimeTracker::new();
assert_eq!(t.close(1000), 0);
}
}