1//! The day's time as hearts: how many quarters of the child's allowance are left, and what the details page says. 2//! 3//! The picture is Zelda's heart containers: [`HEARTS`] of them, each four quarters, depleted from the right. 4//! Everything here is a pure function of numbers the keeper already has; the shell draws and words it. 5 6use crate::limits::Quiet; 7 8/// How many hearts the side panel shows. 9pub const HEARTS: u8 = 3; 10 11/// Quarters in all hearts. 12pub const QUARTERS: u8 = HEARTS * 4; 13 14/// The quarters of the heart row still full, out of [`QUARTERS`], for a day of `allowance_ms` of which `used_ms` is spent. 15/// 16/// A quarter is lost as soon as more than the previous quarters' worth is spent, the way a hit takes a quarter at 17/// once. So: nothing spent is all of them; anything spent is at most one fewer than all; time still left is at 18/// least one; nothing left is none. Spent beyond the allowance is none, never negative. 19pub fn heart_quarters(allowance_ms: u64, used_ms: u64) -> u8 { 20 if allowance_ms == 0 || used_ms >= allowance_ms { 21 return 0; 22 } 23 if used_ms == 0 { 24 return QUARTERS; 25 } 26 let total = u64::from(QUARTERS); 27 // Ceiling division, in u128 so a huge allowance cannot overflow the product. 28 let lost = (u128::from(used_ms) * u128::from(total)).div_ceil(u128::from(allowance_ms)) as u64; 29 (total - lost.min(total)).clamp(1, total - 1) as u8 30} 31 32/// What the hearts' details page needs, all of it already worked out by the core. Minutes are whole minutes. 33#[derive(Clone, Copy, Debug, PartialEq, Eq)] 34pub struct TimeSheet { 35 /// The day's allowance (the limit and anything the grown-ups added today); `None` when there is no limit. 36 pub allowance_minutes: Option<u32>, 37 /// Minutes left of it, rounded up so thirty seconds left says one; `None` when there is no limit. 38 pub minutes_left: Option<u32>, 39 /// Quarters of the hearts still full, out of [`QUARTERS`]; `None` when there is no limit, since there is 40 /// nothing to deplete and a row of full hearts would say a limit was there. 41 pub quarters: Option<u8>, 42 /// The quiet hours, minutes since midnight, if set. 43 pub quiet: Option<Quiet>, 44 /// Minutes until quiet time next starts; `None` with no quiet hours, or when it is quiet time now. 45 pub minutes_until_quiet: Option<u32>, 46} 47 48impl TimeSheet { 49 /// The sheet for a day: `limit_minutes` and `extra_ms` (what the grown-ups added) make the allowance, `used_ms` 50 /// is spent across every device, `minute_of_day` is her local time. 51 pub fn of(limit_minutes: Option<u32>, extra_ms: u64, used_ms: u64, quiet: Option<Quiet>, minute_of_day: u16) -> TimeSheet { 52 let allowance_ms = limit_minutes.map(|m| u64::from(m) * 60_000 + extra_ms); 53 TimeSheet { 54 allowance_minutes: allowance_ms.map(|a| a.div_ceil(60_000) as u32), 55 minutes_left: allowance_ms.map(|a| a.saturating_sub(used_ms).div_ceil(60_000) as u32), 56 quarters: allowance_ms.map(|a| heart_quarters(a, used_ms)), 57 quiet, 58 minutes_until_quiet: quiet.and_then(|q| q.starts_in(minute_of_day)).map(u32::from), 59 } 60 } 61} 62 63#[cfg(test)] 64mod tests { 65 use super::*; 66 67 const MIN: u64 = 60_000; 68 69 #[test] 70 fn nothing_spent_is_every_quarter() { 71 assert_eq!(heart_quarters(30 * MIN, 0), QUARTERS); 72 } 73 74 #[test] 75 fn one_minute_spent_already_costs_a_quarter() { 76 assert_eq!(heart_quarters(30 * MIN, MIN), QUARTERS - 1); 77 assert_eq!(heart_quarters(30 * MIN, 1), QUARTERS - 1); 78 } 79 80 #[test] 81 fn a_quarter_is_lost_when_more_than_its_share_is_spent() { 82 // 30 minutes over 12 quarters is 150 seconds a quarter. 83 let q = 150_000; 84 assert_eq!(heart_quarters(30 * MIN, q), QUARTERS - 1, "exactly one quarter's worth spent"); 85 assert_eq!(heart_quarters(30 * MIN, q + 1), QUARTERS - 2, "a hair over it"); 86 assert_eq!(heart_quarters(30 * MIN, 6 * q), 6, "half spent is half left: two hearts' worth of three"); 87 } 88 89 #[test] 90 fn time_still_left_always_shows_a_quarter() { 91 assert_eq!(heart_quarters(30 * MIN, 30 * MIN - 1), 1); 92 assert_eq!(heart_quarters(30 * MIN, 29 * MIN), 1); 93 } 94 95 #[test] 96 fn all_used_and_over_used_are_none() { 97 assert_eq!(heart_quarters(30 * MIN, 30 * MIN), 0); 98 assert_eq!(heart_quarters(30 * MIN, 45 * MIN), 0); 99 assert_eq!(heart_quarters(30 * MIN, u64::MAX), 0); 100 } 101 102 #[test] 103 fn a_huge_allowance_does_not_overflow() { 104 assert_eq!(heart_quarters(u64::MAX, u64::MAX - 1), 1); 105 } 106 107 #[test] 108 fn no_limit_has_nothing_to_deplete() { 109 let s = TimeSheet::of(None, 0, 40 * MIN, None, 600); 110 assert_eq!((s.allowance_minutes, s.minutes_left, s.quarters), (None, None, None)); 111 } 112 113 #[test] 114 fn the_sheet_counts_added_time_in_the_allowance() { 115 let s = TimeSheet::of(Some(30), 10 * MIN, 5 * MIN, None, 600); 116 assert_eq!(s.allowance_minutes, Some(40)); 117 assert_eq!(s.minutes_left, Some(35)); 118 assert_eq!(s.quarters, Some(heart_quarters(40 * MIN, 5 * MIN))); 119 } 120 121 #[test] 122 fn thirty_seconds_left_says_one_minute_and_over_used_says_none() { 123 assert_eq!(TimeSheet::of(Some(30), 0, 30 * MIN - 30_000, None, 0).minutes_left, Some(1)); 124 let over = TimeSheet::of(Some(30), 0, 50 * MIN, None, 0); 125 assert_eq!((over.minutes_left, over.quarters), (Some(0), Some(0))); 126 } 127 128 #[test] 129 fn quiet_time_is_counted_from_now() { 130 let q = Quiet::new(19 * 60 + 30, 7 * 60).unwrap(); 131 assert_eq!(TimeSheet::of(None, 0, 0, Some(q), 17 * 60 + 30).minutes_until_quiet, Some(120)); 132 assert_eq!(TimeSheet::of(None, 0, 0, Some(q), 3 * 60).minutes_until_quiet, None, "it is quiet now"); 133 assert_eq!(TimeSheet::of(None, 0, 0, None, 3 * 60).minutes_until_quiet, None); 134 } 135}