The day's time as hearts: how many quarters of the child's allowance are left, and what the details page says.
The picture is Zelda's heart containers: [HEARTS] of them, each four quarters, depleted from the right.
Everything here is a pure function of numbers the keeper already has; the shell draws and words it.
6use crate::limits::Quiet;
How many hearts the side panel shows.
9pub const HEARTS: u8 = 3;
Quarters in all hearts.
12pub const QUARTERS: u8 = HEARTS * 4;
The quarters of the heart row still full, out of [QUARTERS], for a day of allowance_ms of which used_ms is spent.
A quarter is lost as soon as more than the previous quarters' worth is spent, the way a hit takes a quarter at once. So: nothing spent is all of them; anything spent is at most one fewer than all; time still left is at 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}
What the hearts' details page needs, all of it already worked out by the core. Minutes are whole minutes.
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>,
Minutes left of it, rounded up so thirty seconds left says one; None when there is no limit.
38 pub minutes_left: Option<u32>,
Quarters of the hearts still full, out of [QUARTERS]; None when there is no limit, since there is
nothing to deplete and a row of full hearts would say a limit was there.
41 pub quarters: Option<u8>,
The quiet hours, minutes since midnight, if set.
43 pub quiet: Option<Quiet>,
Minutes until quiet time next starts; None with no quiet hours, or when it is quiet time now.
48impl TimeSheet {
The sheet for a day: limit_minutes and extra_ms (what the grown-ups added) make the allowance, used_ms
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}
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}