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:
parent
82e8cdfc1f
commit
66649bb815
@ -16,3 +16,4 @@ pub mod reading_material;
|
||||
pub mod search;
|
||||
pub mod session_v2;
|
||||
pub mod text;
|
||||
pub mod time_tracker;
|
||||
|
||||
166
crates/zx_document_core/src/time_tracker.rs
Normal file
166
crates/zx_document_core/src/time_tracker.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user