core/clock/timeband.c
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file timeband.c | ||
| 3 | * @brief The window sums: build one, place a moment on it, and clip a span to it. | ||
| 4 | * | ||
| 5 | * @ingroup lib_core | ||
| 6 | */ | ||
| 7 | #include "clock/timeband.h" | ||
| 8 | |||
| 9 | #include "math/scale.h" | ||
| 10 | |||
| 11 | /// Seconds in a minute, so the epoch maths reads as what it is | ||
| 12 | #define SECONDS_PER_MINUTE 60 | ||
| 13 | |||
| 14 | /** | ||
| 15 | * @brief Bring any minute count into 0..1439, counting backwards from midnight for a negative one. | ||
| 16 | * | ||
| 17 | * @param minutes The count to wrap. | ||
| 18 | * @return The same moment of the day, 0 to 1439. | ||
| 19 | */ | ||
| 20 | 38 | static int wrap_day(int minutes) | |
| 21 | { | ||
| 22 | 38 | int wrapped = minutes % TIMEBAND_DAY_MINUTES; | |
| 23 |
2/2✓ Branch 0 taken 9 times.
✓ Branch 1 taken 29 times.
|
38 | return wrapped < 0 ? wrapped + TIMEBAND_DAY_MINUTES : wrapped; |
| 24 | } | ||
| 25 | |||
| 26 | /** | ||
| 27 | * @brief Whole minutes in a second count, rounding down so a part minute before the window still | ||
| 28 | * counts as the minute it started in. | ||
| 29 | * | ||
| 30 | * @param seconds The count, which may be negative. | ||
| 31 | * @return The count in minutes. | ||
| 32 | */ | ||
| 33 | 6 | static int floor_minutes(int seconds) | |
| 34 | { | ||
| 35 | 6 | int minutes = seconds / SECONDS_PER_MINUTE; | |
| 36 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
6 | if (seconds % SECONDS_PER_MINUTE != 0 && seconds < 0) |
| 37 | { | ||
| 38 | ✗ | minutes--; | |
| 39 | } | ||
| 40 | 6 | return minutes; | |
| 41 | } | ||
| 42 | |||
| 43 | /** | ||
| 44 | * @brief Whole minutes in a second count, rounding up so a span ending part way through a minute | ||
| 45 | * still covers it. | ||
| 46 | * | ||
| 47 | * @param seconds The count, which may be negative. | ||
| 48 | * @return The count in minutes. | ||
| 49 | */ | ||
| 50 | 6 | static int ceil_minutes(int seconds) | |
| 51 | { | ||
| 52 | 6 | int minutes = seconds / SECONDS_PER_MINUTE; | |
| 53 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
6 | if (seconds % SECONDS_PER_MINUTE != 0 && seconds > 0) |
| 54 | { | ||
| 55 | ✗ | minutes++; | |
| 56 | } | ||
| 57 | 6 | return minutes; | |
| 58 | } | ||
| 59 | |||
| 60 | 10 | TimeBand timeband_full_day(void) | |
| 61 | { | ||
| 62 | 10 | return (TimeBand){.start_min = 0, .span_min = TIMEBAND_DAY_MINUTES}; | |
| 63 | } | ||
| 64 | |||
| 65 | 3 | TimeBand timeband_rolling(int now_min, int span_min, int lead_min) | |
| 66 | { | ||
| 67 | 3 | int span = clamp_int(span_min, 1, TIMEBAND_DAY_MINUTES); | |
| 68 | // the far end belongs to the next window, so the last minute is as far in as the moment can sit | ||
| 69 | 3 | int lead = clamp_int(lead_min, 0, span - 1); | |
| 70 | |||
| 71 | 3 | return (TimeBand){.start_min = wrap_day(now_min - lead), .span_min = span}; | |
| 72 | } | ||
| 73 | |||
| 74 | 4 | TimeBand timeband_from_hour(int base_hour, int span_min) | |
| 75 | { | ||
| 76 | 4 | int hour = wrap_day(base_hour * 60) / 60; | |
| 77 | |||
| 78 | 4 | return (TimeBand){ | |
| 79 | 4 | .start_min = hour * 60, | |
| 80 | 4 | .span_min = clamp_int(span_min, 1, TIMEBAND_DAY_MINUTES), | |
| 81 | }; | ||
| 82 | } | ||
| 83 | |||
| 84 | 11 | int timeband_offset(TimeBand band, int minute_of_day) | |
| 85 | { | ||
| 86 | 11 | int offset = wrap_day(minute_of_day - band.start_min); | |
| 87 | |||
| 88 | // the far end belongs to whatever comes next, so two windows laid end to end share no minute | ||
| 89 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 3 times.
|
11 | return offset < band.span_min ? offset : -1; |
| 90 | } | ||
| 91 | |||
| 92 | 3 | int timeband_pos(TimeBand band, int length, int minute_of_day) | |
| 93 | { | ||
| 94 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
|
3 | if (length <= 0) |
| 95 | { | ||
| 96 | 1 | return -1; | |
| 97 | } | ||
| 98 | |||
| 99 | 2 | int offset = timeband_offset(band, minute_of_day); | |
| 100 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | return offset < 0 ? -1 : fraction_px(length, offset, band.span_min); |
| 101 | } | ||
| 102 | |||
| 103 | 2 | int timeband_pos_offset(TimeBand band, int length, int offset_min) | |
| 104 | { | ||
| 105 |
4/6✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 1 time.
|
2 | if (length <= 0 || offset_min < 0 || offset_min > band.span_min) |
| 106 | { | ||
| 107 | 1 | return -1; | |
| 108 | } | ||
| 109 | |||
| 110 | // the span's own end is allowed here where a moment's is not: a block ending with the window | ||
| 111 | // has to reach the end of the axis rather than stopping a unit short of it | ||
| 112 | 1 | return fraction_px(length, offset_min, band.span_min); | |
| 113 | } | ||
| 114 | |||
| 115 | 10 | bool timeband_clip(TimeBand band, time_t window_epoch, time_t start, time_t end, | |
| 116 | TimeBandSpan *out) | ||
| 117 | { | ||
| 118 | 10 | time_t window_end = window_epoch + (time_t)band.span_min * SECONDS_PER_MINUTE; | |
| 119 | |||
| 120 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 9 times.
|
10 | if (end < start) |
| 121 | { | ||
| 122 | 1 | end = start; | |
| 123 | } | ||
| 124 | |||
| 125 | // an event pinned to a moment rather than a stretch still has to draw, so give it the minute | ||
| 126 | // it starts in. the flags below read the real end so this never reports as running off the band | ||
| 127 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 8 times.
|
10 | time_t occupied_end = (end == start) ? start + SECONDS_PER_MINUTE : end; |
| 128 | |||
| 129 |
4/4✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 6 times.
|
10 | if (occupied_end <= window_epoch || start >= window_end) |
| 130 | { | ||
| 131 | 4 | return false; | |
| 132 | } | ||
| 133 | |||
| 134 | // pin to the window before the subtraction so a span from last week cannot make a minute count | ||
| 135 | // too big to hold | ||
| 136 | 6 | time_t first = start < window_epoch ? window_epoch : start; | |
| 137 | 6 | time_t last = occupied_end > window_end ? window_end : occupied_end; | |
| 138 | |||
| 139 | // both ends are pinned inside the window, so each is at most a day of seconds past its start | ||
| 140 | // an int holds that, and the watch then needs no 64-bit division. that is 754 bytes of libgcc | ||
| 141 | 6 | int from = floor_minutes((int)(first - window_epoch)); | |
| 142 | 6 | int to = ceil_minutes((int)(last - window_epoch)); | |
| 143 | |||
| 144 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
|
6 | if (to <= from) |
| 145 | { | ||
| 146 | ✗ | to = from + 1; | |
| 147 | } | ||
| 148 | |||
| 149 | 6 | out->from = from; | |
| 150 | 6 | out->to = to; | |
| 151 | 6 | out->clipped_start = start < window_epoch; | |
| 152 | 6 | out->clipped_end = end > window_end; | |
| 153 | |||
| 154 | 6 | return true; | |
| 155 | } | ||
| 156 | |||
| 157 | 6 | int timeband_clip_daily(TimeBand band, int from_min, int to_min, | |
| 158 | TimeBandSpan *out, int max_out) | ||
| 159 | { | ||
| 160 |
4/6✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 5 times.
|
6 | if (from_min < 0 || to_min < 0 || max_out <= 0) |
| 161 | { | ||
| 162 | 1 | return 0; | |
| 163 | } | ||
| 164 | |||
| 165 | 5 | int start = wrap_day(from_min); | |
| 166 | 5 | int length = wrap_day(to_min - from_min); | |
| 167 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 4 times.
|
5 | if (length == 0) |
| 168 | { | ||
| 169 | // the two readings landing on the same minute is the sun never setting, not a span with | ||
| 170 | // no width, so it covers the whole day | ||
| 171 | 1 | length = TIMEBAND_DAY_MINUTES; | |
| 172 | } | ||
| 173 | |||
| 174 | // the span repeats every day, so the window can catch the tail of yesterday's as well as | ||
| 175 | // today's. anything a day further out starts past the end of a window this short | ||
| 176 | 5 | int written = 0; | |
| 177 |
4/4✓ Branch 0 taken 10 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 1 time.
|
14 | for (int day = -1; day <= 0 && written < max_out; day++) |
| 178 | { | ||
| 179 | 9 | int at = wrap_day(start - band.start_min) + day * TIMEBAND_DAY_MINUTES; | |
| 180 | 9 | int from = at < 0 ? 0 : at; | |
| 181 | 9 | int to = (at + length) > band.span_min ? band.span_min : (at + length); | |
| 182 | |||
| 183 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 8 times.
|
9 | if (to <= from) |
| 184 | { | ||
| 185 | 1 | continue; | |
| 186 | } | ||
| 187 | |||
| 188 | 8 | out[written].from = from; | |
| 189 | 8 | out[written].to = to; | |
| 190 | 8 | out[written].clipped_start = at < 0; | |
| 191 | 8 | out[written].clipped_end = (at + length) > band.span_min; | |
| 192 | 8 | written++; | |
| 193 | } | ||
| 194 | |||
| 195 | 5 | return written; | |
| 196 | } | ||
| 197 | |||
| 198 | 5 | time_t timeband_window_epoch(TimeBand band, time_t now, int now_min) | |
| 199 | { | ||
| 200 | // the top of the current minute. every time zone sits a whole number of minutes off UTC, so | ||
| 201 | // the seconds on the epoch are the seconds on the wall clock too | ||
| 202 | 5 | time_t minute_start = now - now % SECONDS_PER_MINUTE; | |
| 203 | |||
| 204 | // while the window is open, its start is that many real minutes back | ||
| 205 | 5 | int in = timeband_offset(band, now_min); | |
| 206 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 time.
|
5 | if (in >= 0) |
| 207 | { | ||
| 208 | 4 | return minute_start - (time_t)in * SECONDS_PER_MINUTE; | |
| 209 | } | ||
| 210 | |||
| 211 | // not open yet, so it opens this many minutes ahead | ||
| 212 | 1 | int ahead = wrap_day(band.start_min - now_min); | |
| 213 | 1 | return minute_start + (time_t)ahead * SECONDS_PER_MINUTE; | |
| 214 | } | ||
| 215 |