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 search;
|
||||||
pub mod session_v2;
|
pub mod session_v2;
|
||||||
pub mod text;
|
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