GCC Code Coverage Report


Directory: ./
File: ScheduleEntry.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 589 674 87.4%
Functions: 33 38 86.8%
Branches: 606 1137 53.3%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16
17 #include "Warnings.h" // WarningHolder -- needed directly for NOPCH builds
18
19 #include <algorithm>
20 #include <cerrno>
21 #include <cmath>
22 #include <ctime>
23 #include <ctype.h>
24 #include <math.h>
25 #include <memory>
26 #include <mutex>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <string>
30 #include <utility>
31 #include <vector>
32
33 #include "FPPLocale.h"
34 #include "SunRise.h"
35 #include "common.h"
36 #include "log.h"
37 #include "settings.h"
38 #include "commands/Commands.h"
39
40 #include "ScheduleEntry.h"
41
42 static std::mutex s_loadWarningsLock;
43 static std::vector<std::pair<int, std::string>> s_loadWarnings;
44
45 // The innermost SchedulePreviewScope on this thread, if any.
46 static thread_local SchedulePreviewScope* s_previewScope = nullptr;
47
48 16 SchedulePreviewScope::SchedulePreviewScope() :
49 16 m_prev(s_previewScope) {
50 16 s_previewScope = this;
51 16 }
52
53 16 SchedulePreviewScope::~SchedulePreviewScope() {
54 16 s_previewScope = m_prev;
55 16 }
56
57 140 void ScheduleLoadWarning(int id, const std::string& msg) {
58
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 140 times.
140 if (s_previewScope) {
59 // A preview expansion (the calendar) is not a load: nothing here
60 // would ever retract a banner it raised, and a year the viewer merely
61 // browsed to is not one the scheduler will run.
62 ✗ LogDebug(VB_SCHEDULE, "Schedule preview: %s\n", msg.c_str());
63 ✗ for (auto& w : s_previewScope->m_warnings) {
64 ✗ if ((w.first == id) && (w.second == msg))
65 ✗ return;
66 }
67 ✗ s_previewScope->m_warnings.emplace_back(id, msg);
68 ✗ return;
69 }
70
1/2
✓ Branch 0 taken 140 times.
✗ Branch 1 not taken.
140 WarningHolder::AddWarning(id, msg);
71
1/2
✓ Branch 0 taken 140 times.
✗ Branch 1 not taken.
140 std::lock_guard<std::mutex> l(s_loadWarningsLock);
72
2/2
✓ Branch 0 taken 128 times.
✓ Branch 1 taken 24 times.
152 for (auto& w : s_loadWarnings) {
73
5/6
✓ Branch 0 taken 128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 116 times.
✓ Branch 3 taken 12 times.
✓ Branch 4 taken 116 times.
✓ Branch 5 taken 12 times.
128 if ((w.first == id) && (w.second == msg))
74 116 return;
75 }
76
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 s_loadWarnings.emplace_back(id, msg);
77
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 116 times.
140 }
78
79 1964 void ClearScheduleLoadWarnings() {
80 1964 std::vector<std::pair<int, std::string>> old;
81 {
82
1/2
✓ Branch 0 taken 1964 times.
✗ Branch 1 not taken.
1964 std::lock_guard<std::mutex> l(s_loadWarningsLock);
83 1964 old.swap(s_loadWarnings);
84 1964 }
85
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1964 times.
1968 for (auto& w : old) {
86
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
8 WarningHolder::RemoveWarning(w.first, w.second);
87 }
88 1964 }
89
90 // Days since 1970-01-01 for a proleptic Gregorian civil date (Howard
91 // Hinnant's days_from_civil). Pure calendar arithmetic: no time zone, no DST.
92 90 static long DaysFromCivil(long y, unsigned m, unsigned d) {
93 90 y -= m <= 2;
94
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 90 times.
90 const long era = (y >= 0 ? y : y - 399) / 400;
95 90 const unsigned yoe = (unsigned)(y - era * 400);
96
1/2
✓ Branch 0 taken 90 times.
✗ Branch 1 not taken.
90 const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
97 90 const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
98 90 return era * 146097 + (long)doe - 719468;
99 }
100
101 // The inverse of DaysFromCivil (Hinnant's civil_from_days).
102 4 static void CivilFromDays(long z, int& y, int& m, int& d) {
103 4 z += 719468;
104
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 const long era = (z >= 0 ? z : z - 146096) / 146097;
105 4 const unsigned doe = (unsigned)(z - era * 146097);
106 4 const unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
107 4 const unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
108 4 const unsigned mp = (5 * doy + 2) / 153;
109 4 d = (int)(doy - (153 * mp + 2) / 5 + 1);
110
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 m = (int)(mp < 10 ? mp + 3 : mp - 9);
111 4 y = (int)(yoe + era * 400 + (m <= 2));
112 4 }
113
114 // 'base' is the schedule load's single "now". Reading the clock here, per
115 // call, let a load that ran across midnight anchor an occurrence's start to
116 // one week and its end to the next (Saturday->Sunday): a week-long window.
117 58852 static time_t GetTimeOnDOW(time_t base, int day, int hour, int minute, int second) {
118 struct tm now;
119 58852 localtime_r(&base, &now);
120
121 // 'day' is an offset relative to Sunday of the current week so our
122 // new mday is adjusted by the current day of the week we are on now
123 58852 now.tm_mday = now.tm_mday - now.tm_wday + day;
124 58852 now.tm_hour = hour;
125 58852 now.tm_min = minute;
126 58852 now.tm_sec = second;
127 58852 now.tm_isdst = -1; // take Daylight Saving Time into account on new date
128
129 58852 time_t result = mktime(&now);
130
131 58852 return result;
132 }
133
134 /*
135 * Build a time_t for an absolute calendar date rather than an offset from the
136 * current week. Used by the arbitrary-range expansion behind the calendar view.
137 */
138 47603 static time_t GetTimeOnDate(int year, int month, int mday,
139 int hour, int minute, int second) {
140 47603 struct tm when = {};
141
142 47603 when.tm_year = year - 1900;
143 47603 when.tm_mon = month - 1;
144 47603 when.tm_mday = mday;
145 47603 when.tm_hour = hour;
146 47603 when.tm_min = minute;
147 47603 when.tm_sec = second;
148 47603 when.tm_isdst = -1; // take Daylight Saving Time into account on new date
149
150 47603 return mktime(&when);
151 }
152
153 /*
154 * Day of week (0 == Sunday) for an absolute calendar date.
155 */
156 23836 static int GetDOWOnDate(int year, int month, int mday) {
157 23836 struct tm when = {};
158
159 23836 when.tm_year = year - 1900;
160 23836 when.tm_mon = month - 1;
161 23836 when.tm_mday = mday;
162 23836 when.tm_hour = 12; // midday avoids DST edges shifting the date
163 23836 when.tm_isdst = -1;
164
165 23836 time_t t = mktime(&when);
166 struct tm result;
167 23836 localtime_r(&t, &result);
168
169 23836 return result.tm_wday;
170 }
171
172 346 ScheduleEntry::ScheduleEntry() :
173 346 enabled(false),
174
1/2
✓ Branch 0 taken 346 times.
✗ Branch 1 not taken.
692 playlist(""),
175 346 sequence(false),
176 346 repeat(false),
177 346 dayIndex(0),
178 346 startHour(12),
179 346 startMinute(0),
180 346 startSecond(0),
181 346 endHour(12),
182 346 endMinute(0),
183 346 endSecond(0),
184 346 startDate(0),
185 346 endDate(0),
186 346 stopType(0),
187 346 startTimeOffset(0),
188 346 endTimeOffset(0),
189 346 startDateOffset(0),
190 346 endDateOffset(0),
191
1/2
✓ Branch 0 taken 346 times.
✗ Branch 1 not taken.
346 repeatInterval(0) {
192 346 }
193
194 1050 ScheduleEntry::~ScheduleEntry() {
195 1050 }
196
197 3686 void ScheduleEntry::CalculateEaster(int year, int& month, int& day) {
198 // Formula gleaned from various sources around the 'net.
199 int Y, a, c, e, h, k, L;
200 double b, d, f, g, i, m;
201
202 3686 Y = year;
203 3686 a = Y % 19;
204 3686 b = floor(Y / 100);
205 3686 c = Y % 100;
206 3686 d = floor(b / 4);
207 3686 e = (int)b % 4;
208 3686 f = floor((b + 8) / 25);
209 3686 g = floor((b - f + 1) / 3);
210 3686 h = (19 * a + (int)b - (int)d - (int)g + 15) % 30;
211 3686 i = floor(c / 4);
212 3686 k = c % 4;
213 3686 L = (32 + 2 * e + 2 * (int)i - h - k) % 7;
214 3686 m = floor((a + 11 * h + 22 * L) / 451);
215 3686 month = (int)floor((h + L - 7 * m + 114) / 31);
216 3686 day = (int)((h + L - 7 * (int)m + 114) % 31) + 1;
217 3686 }
218
219 2 void ScheduleEntry::CalculateChineseNewYear(int year, int& month, int& day) {
220 // Simplified Chinese New Year calculation
221 // Second new moon after winter solstice (approximately)
222 // This is an approximation - for exact dates, a full lunar calendar is needed
223
224 // These are pre-calculated Chinese New Year dates for 2024-2035
225 // Format: {year, month, day}
226 2 int cnyDates[][3] = {
227 {2024, 2, 10}, {2025, 1, 29}, {2026, 2, 17}, {2027, 2, 6},
228 {2028, 1, 26}, {2029, 2, 13}, {2030, 2, 3}, {2031, 1, 23},
229 {2032, 2, 11}, {2033, 1, 31}, {2034, 2, 19}, {2035, 2, 8}
230 };
231
232
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 2 times.
26 for (int i = 0; i < 12; i++) {
233
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (cnyDates[i][0] == year) {
234 ✗ month = cnyDates[i][1];
235 ✗ day = cnyDates[i][2];
236 ✗ return;
237 }
238 }
239
240 // Not in the table: say so rather than silently scheduling on a made-up
241 // date. The table needs extending for later years.
242
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 ScheduleLoadWarning(47, "Schedule: no Chinese New Year date known for " + std::to_string(year) + "; FPP's table needs updating");
243 2 month = 2;
244 2 day = 1;
245 }
246
247 40 void ScheduleEntry::CalculateHebrewDate(int year, const std::string& hebrewMonth, int hebrewDay, int& month, int& day) {
248 // Simplified Hebrew calendar conversion
249 // For precise dates, a full Hebrew calendar library would be needed
250
251 // Approximate Rosh Hashanah (1 Tishrei) dates for 2024-2035
252 // Passover is typically ~6 months earlier
253 40 int roshHashanahDates[][3] = {
254 {2024, 10, 3}, {2025, 9, 23}, {2026, 9, 12}, {2027, 10, 2},
255 {2028, 9, 21}, {2029, 9, 10}, {2030, 9, 28}, {2031, 9, 18},
256 {2032, 9, 6}, {2033, 9, 24}, {2034, 9, 14}, {2035, 10, 4}
257 };
258
259
6/8
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✓ Branch 3 taken 20 times.
✓ Branch 4 taken 20 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 20 times.
✓ Branch 7 taken 20 times.
40 if (hebrewMonth == "Tishrei" && hebrewDay == 1) {
260 // Rosh Hashanah
261
2/2
✓ Branch 0 taken 208 times.
✓ Branch 1 taken 4 times.
212 for (int i = 0; i < 12; i++) {
262
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 192 times.
208 if (roshHashanahDates[i][0] == year) {
263 16 month = roshHashanahDates[i][1];
264 16 day = roshHashanahDates[i][2];
265 32 return;
266 }
267 }
268
4/8
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 20 times.
✗ Branch 7 not taken.
20 } else if (hebrewMonth == "Nisan" && hebrewDay == 14) {
269 // Passover - approximately 6 months before Rosh Hashanah
270 20 int passoverDates[][3] = {
271 {2024, 4, 23}, {2025, 4, 13}, {2026, 4, 2}, {2027, 4, 22},
272 {2028, 4, 11}, {2029, 3, 31}, {2030, 4, 18}, {2031, 4, 8},
273 {2032, 3, 27}, {2033, 4, 14}, {2034, 4, 4}, {2035, 4, 24}
274 };
275
276
2/2
✓ Branch 0 taken 208 times.
✓ Branch 1 taken 4 times.
212 for (int i = 0; i < 12; i++) {
277
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 192 times.
208 if (passoverDates[i][0] == year) {
278 16 month = passoverDates[i][1];
279 16 day = passoverDates[i][2];
280 16 return;
281 }
282 }
283 }
284
285 // Not in the table (or not a date it covers): say so rather than silently
286 // scheduling on Jan 1.
287
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
8 ScheduleLoadWarning(47, "Schedule: no Hebrew calendar date known for " + std::to_string(year) + "; FPP's table needs updating");
288 8 month = 1;
289 8 day = 1;
290 }
291
292 26 void ScheduleEntry::CalculateIslamicDate(int year, int islamicMonth, int islamicDay, int& month, int& day) {
293 // Simplified Islamic calendar conversion
294 // Islamic calendar moves ~11 days earlier each year
295
296 // Ramadan (month 9) approximate start dates for 2024-2035. The Islamic
297 // year is ~354 days, so in 2030 Ramadan starts twice (January 6 and
298 // December 26); the table holds the first. 2031-2035 used to hold the
299 // previous Gregorian year's second date (Ramadan 2032 on December 15
300 // rather than the 4th); these are the tabular Islamic calendar's dates,
301 // which observation can move by a day.
302 26 int ramadanDates[][3] = {
303 {2024, 3, 11}, {2025, 3, 1}, {2026, 2, 18}, {2027, 2, 8},
304 {2028, 1, 28}, {2029, 1, 17}, {2030, 1, 6}, {2031, 12, 15},
305 {2032, 12, 4}, {2033, 11, 23}, {2034, 11, 12}, {2035, 11, 2}
306 };
307
308 // Eid al-Fitr (month 10, day 1) - approximately 30 days after Ramadan.
309 // 2033 has two (January 3 and December 23); the table holds the first.
310 // 2034 and 2035 used to hold the previous year's second date.
311 26 int eidFitrDates[][3] = {
312 {2024, 4, 10}, {2025, 3, 31}, {2026, 3, 20}, {2027, 3, 10},
313 {2028, 2, 27}, {2029, 2, 16}, {2030, 2, 5}, {2031, 1, 25},
314 {2032, 1, 14}, {2033, 1, 3}, {2034, 12, 12}, {2035, 12, 2}
315 };
316
317
3/4
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 14 times.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
26 if (islamicMonth == 9 && islamicDay == 1) {
318 // Ramadan start
319
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114 for (int i = 0; i < 12; i++) {
320
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 102 times.
114 if (ramadanDates[i][0] == year) {
321 12 month = ramadanDates[i][1];
322 12 day = ramadanDates[i][2];
323 26 return;
324 }
325 }
326
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
14 } else if (islamicMonth == 10 && islamicDay == 1) {
327 // Eid al-Fitr
328
1/2
✓ Branch 0 taken 132 times.
✗ Branch 1 not taken.
132 for (int i = 0; i < 12; i++) {
329
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 118 times.
132 if (eidFitrDates[i][0] == year) {
330 14 month = eidFitrDates[i][1];
331 14 day = eidFitrDates[i][2];
332 14 return;
333 }
334 }
335 }
336
337 // Not in the table (or not a date it covers): say so rather than silently
338 // scheduling on Jan 1.
339 ✗ ScheduleLoadWarning(47, "Schedule: no Islamic calendar date known for " + std::to_string(year) + "; FPP's table needs updating");
340 ✗ month = 1;
341 ✗ day = 1;
342 }
343
344 20 void ScheduleEntry::CalculateHinduDiwali(int year, int& month, int& day) {
345 // Diwali falls on the new moon of Kartika (October/November)
346 // Pre-calculated dates for 2024-2035
347 20 int diwaliDates[][3] = {
348 {2024, 11, 1}, {2025, 10, 21}, {2026, 11, 9}, {2027, 10, 29},
349 {2028, 10, 17}, {2029, 11, 5}, {2030, 10, 26}, {2031, 11, 14},
350 {2032, 11, 2}, {2033, 10, 23}, {2034, 11, 10}, {2035, 10, 30}
351 };
352
353
2/2
✓ Branch 0 taken 183 times.
✓ Branch 1 taken 2 times.
185 for (int i = 0; i < 12; i++) {
354
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 165 times.
183 if (diwaliDates[i][0] == year) {
355 18 month = diwaliDates[i][1];
356 18 day = diwaliDates[i][2];
357 18 return;
358 }
359 }
360
361 // Not in the table: say so rather than silently scheduling on Nov 1.
362
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 ScheduleLoadWarning(47, "Schedule: no Diwali date known for " + std::to_string(year) + "; FPP's table needs updating");
363 2 month = 11;
364 2 day = 1;
365 }
366
367 43809 std::string ScheduleEntry::DateFromLocaleHoliday(Json::Value& holiday, int refYear) {
368 43809 int year = 0;
369 43809 int month = 0;
370 43809 int day = 0;
371 43809 time_t currTime = time(NULL);
372 struct tm now;
373 43809 localtime_r(&currTime, &now);
374
375 // When a reference year is supplied we resolve the holiday strictly within
376 // that year. The roll-forward heuristics below only make sense for "the
377 // next occurrence from today" and would skew a date asked for explicitly.
378 43809 bool rollForward = (refYear == 0);
379
380
3/4
✓ Branch 0 taken 43809 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15552 times.
✓ Branch 3 taken 28257 times.
43809 if (holiday.isMember("calc")) {
381 // We need to calculate the month/day of the holiday since it changes yearly
382
2/4
✓ Branch 0 taken 15552 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15552 times.
✗ Branch 3 not taken.
15552 Json::Value c = holiday["calc"];
383
2/4
✓ Branch 0 taken 15552 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15552 times.
✗ Branch 3 not taken.
15552 std::string type = c["type"].asString();
384
2/4
✓ Branch 0 taken 15552 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15552 times.
✗ Branch 3 not taken.
15552 int week = c["week"].asInt();
385
2/4
✓ Branch 0 taken 15552 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15552 times.
✗ Branch 3 not taken.
15552 int dow = c["dow"].asInt();
386
2/4
✓ Branch 0 taken 15552 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15552 times.
✗ Branch 3 not taken.
15552 int offset = c["offset"].asInt();
387
388
2/2
✓ Branch 0 taken 83 times.
✓ Branch 1 taken 15469 times.
15552 year = rollForward ? (now.tm_year + 1900) : refYear;
389
390
8/10
✓ Branch 0 taken 15552 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7443 times.
✓ Branch 3 taken 8109 times.
✓ Branch 4 taken 7443 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3682 times.
✓ Branch 7 taken 3761 times.
✓ Branch 8 taken 11791 times.
✓ Branch 9 taken 3761 times.
15552 if ((type == "head") || (type == "tail")) {
391
2/4
✓ Branch 0 taken 11791 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11791 times.
✗ Branch 3 not taken.
11791 month = c["month"].asInt();
392
2/4
✓ Branch 0 taken 11791 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 11791 times.
11791 if ((month < 1) || (month > 12)) {
393 ✗ LogErr(VB_SCHEDULE, "Invalid month %d in holiday definition, using January\n", month);
394 ✗ ScheduleLoadWarning(47, "Schedule: invalid month in holiday definition");
395 ✗ month = 1;
396 }
397
3/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 11787 times.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
11791 if (rollForward && (month < (now.tm_mon + 1)))
398 4 year++;
399 }
400
401 15552 int mdays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
402 15552 int wday = 0;
403
404
3/6
✓ Branch 0 taken 2986 times.
✓ Branch 1 taken 12566 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2986 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
15552 if ((year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)))
405 2986 mdays[1]++; // add one day to February for leap year
406
407
8/10
✓ Branch 0 taken 15552 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7443 times.
✓ Branch 3 taken 8109 times.
✓ Branch 4 taken 7443 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3682 times.
✓ Branch 7 taken 3761 times.
✓ Branch 8 taken 11791 times.
✓ Branch 9 taken 3761 times.
15552 if ((type == "head") || (type == "tail")) {
408 // Day of week of the 1st of the month. Only head/tail use wday,
409 // and only they set `month` - computing it unconditionally up here
410 // indexed t[month - 1] with month still 0 for every easter/lunar
411 // holiday, i.e. t[-1].
412 // https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week
413 static const int t[] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 };
414 11791 int d = 1;
415 11791 int y = year - (month < 3);
416 11791 wday = (y + y / 4 - y / 100 + y / 400 + t[month - 1] + d) % 7;
417 }
418
419
3/4
✓ Branch 0 taken 15552 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3686 times.
✓ Branch 3 taken 11866 times.
15552 if (type == "easter") {
420
1/2
✓ Branch 0 taken 3686 times.
✗ Branch 1 not taken.
3686 CalculateEaster(year, month, day);
421
422 // Check if Easter was more than 4 months ago. We do this since some Easter-related
423 // special days are 2 months after Easter so we want to calculate those based on the
424 // current year.
425
3/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 3682 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
3686 if (rollForward && ((month + 3) < now.tm_mon)) {
426 ✗ year++;
427 ✗ CalculateEaster(year, month, day);
428 }
429
430 // We have Easter, see if we are looking for something offset from Easter
431
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 3682 times.
3686 if (offset != 0) {
432 // Calendar arithmetic, no time zone. This used to build UTC
433 // midnight of the day after and convert with localtime(),
434 // which is right west of UTC and a day late east of it (UK
435 // Mothering Sunday, Easter - 21, came out a Monday).
436 4 int y = year;
437 4 CivilFromDays(DaysFromCivil(year, month, day) + offset, y, month, day);
438 }
439
3/4
✓ Branch 0 taken 11866 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8109 times.
✓ Branch 3 taken 3757 times.
11866 } else if (type == "head") {
440 8109 day = ((dow + 7 - wday) % 7) + ((week - 1) * 7) + 1;
441 // if (dow == wday)
442 // day += 7;
443
444 8109 day += offset;
445
446 // See if offset pushed us into last month
447
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8109 times.
8109 if (day < 1) {
448 ✗ day -= offset;
449 ✗ month--;
450 ✗ if (month < 1)
451 ✗ month = 12;
452
453 ✗ offset += day;
454 ✗ day = mdays[month - 1] + offset;
455 }
456
3/4
✓ Branch 0 taken 3757 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3682 times.
✓ Branch 3 taken 75 times.
3757 } else if (type == "tail") {
457 3682 int ldow = wday + ((mdays[month - 1] - 1) % 7);
458 3682 day = mdays[month - 1] - ((ldow + 7 - dow) % 7) - ((week - 1) * 7);
459 3682 day += offset;
460
2/4
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75 times.
✗ Branch 3 not taken.
75 } else if (type == "lunar") {
461 // Handle lunar calendar calculations
462
5/14
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 75 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 75 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 75 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
75 std::string calendar = c.isMember("calendar") ? c["calendar"].asString() : "";
463
464
3/4
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 73 times.
75 if (calendar == "chinese") {
465
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 CalculateChineseNewYear(year, month, day);
466
3/4
✓ Branch 0 taken 73 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✓ Branch 3 taken 41 times.
73 } else if (calendar == "hebrew") {
467
5/14
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 32 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 32 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 32 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
32 std::string hebrewMonth = c.isMember("hebrewMonth") ? c["hebrewMonth"].asString() : "";
468
4/8
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 32 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 32 times.
✗ Branch 7 not taken.
32 int hebrewDay = c.isMember("hebrewDay") ? c["hebrewDay"].asInt() : 1;
469
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 CalculateHebrewDate(year, hebrewMonth, hebrewDay, month, day);
470
3/4
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✓ Branch 3 taken 17 times.
73 } else if (calendar == "islamic") {
471
4/8
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 24 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 24 times.
✗ Branch 7 not taken.
24 int islamicMonth = c.isMember("islamicMonth") ? c["islamicMonth"].asInt() : 1;
472
4/8
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 24 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 24 times.
✗ Branch 7 not taken.
24 int islamicDay = c.isMember("islamicDay") ? c["islamicDay"].asInt() : 1;
473
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 CalculateIslamicDate(year, islamicMonth, islamicDay, month, day);
474
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 } else if (calendar == "hindu") {
475
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 CalculateHinduDiwali(year, month, day);
476 } else {
477 ✗ LogErr(VB_SCHEDULE, "Unknown lunar calendar type %s\n", calendar.c_str());
478 ✗ ScheduleLoadWarning(47, "Schedule: unknown lunar calendar type '" + calendar + "'");
479 ✗ return "0000-00-00";
480 }
481
482 // Check if the calculated date has passed this year
483
1/2
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
75 if (rollForward &&
484
2/2
✓ Branch 0 taken 73 times.
✓ Branch 1 taken 2 times.
75 ((month < (now.tm_mon + 1)) ||
485
4/4
✓ Branch 0 taken 55 times.
✓ Branch 1 taken 18 times.
✓ Branch 2 taken 11 times.
✓ Branch 3 taken 44 times.
73 (month == (now.tm_mon + 1) && day < now.tm_mday))) {
486 13 int thisMonth = month;
487 13 int thisDay = day;
488 13 year++;
489 // Recalculate for next year
490
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (calendar == "chinese") {
491 ✗ CalculateChineseNewYear(year, month, day);
492
3/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 5 times.
13 } else if (calendar == "hebrew") {
493
5/14
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 8 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
8 std::string hebrewMonth = c.isMember("hebrewMonth") ? c["hebrewMonth"].asString() : "";
494
4/8
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
8 int hebrewDay = c.isMember("hebrewDay") ? c["hebrewDay"].asInt() : 1;
495
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 CalculateHebrewDate(year, hebrewMonth, hebrewDay, month, day);
496
3/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 3 times.
13 } else if (calendar == "islamic") {
497
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
2 int islamicMonth = c.isMember("islamicMonth") ? c["islamicMonth"].asInt() : 1;
498
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
2 int islamicDay = c.isMember("islamicDay") ? c["islamicDay"].asInt() : 1;
499
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 CalculateIslamicDate(year, islamicMonth, islamicDay, month, day);
500
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 } else if (calendar == "hindu") {
501
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 CalculateHinduDiwali(year, month, day);
502 }
503 // Yearless, next year's date matches this year too if it is
504 // still to come: keep this year's, which has passed. The daily
505 // reload picks up next year's.
506
6/6
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 4 times.
13 if ((month > (now.tm_mon + 1)) || (month == (now.tm_mon + 1) && day >= now.tm_mday)) {
507 7 month = thisMonth;
508 7 day = thisDay;
509 }
510 }
511
1/2
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
75 } else {
512 ✗ LogErr(VB_SCHEDULE, "Unknown holiday calculation type %s\n", type.c_str());
513 ✗ ScheduleLoadWarning(47, "Schedule: unknown holiday calculation type '" + type + "'");
514 ✗ return "0000-00-00";
515 }
516
517 15552 year = 0;
518
2/4
✓ Branch 0 taken 15552 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15552 times.
✗ Branch 3 not taken.
15552 } else {
519
2/4
✓ Branch 0 taken 28257 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28257 times.
✗ Branch 3 not taken.
28257 year = holiday["year"].asInt();
520
2/4
✓ Branch 0 taken 28257 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28257 times.
✗ Branch 3 not taken.
28257 month = holiday["month"].asInt();
521
2/4
✓ Branch 0 taken 28257 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28257 times.
✗ Branch 3 not taken.
28257 day = holiday["day"].asInt();
522 }
523
524 char str[11];
525 43809 snprintf(str, 11, "%04d-%02d-%02d", year, month, day);
526
527
1/2
✓ Branch 0 taken 43809 times.
✗ Branch 1 not taken.
43809 std::string result = str;
528
529 43809 return result;
530 43809 }
531
532 43813 std::string ScheduleEntry::CheckHoliday(std::string date, int refYear) {
533
1/2
✓ Branch 0 taken 43813 times.
✗ Branch 1 not taken.
43813 Json::Value locale = LocaleHolder::GetLocale();
534 43813 std::string result;
535
536
2/4
✓ Branch 0 taken 43813 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 43813 times.
43813 if (!locale.isMember("holidays"))
537 ✗ return date;
538
539
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 43813 times.
43813 if (isdigit(date.c_str()[0]))
540 ✗ return date;
541
542
4/6
✓ Branch 0 taken 413888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 413888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 413884 times.
✓ Branch 5 taken 4 times.
413888 for (int i = 0; i < locale["holidays"].size(); i++) {
543
6/10
✓ Branch 0 taken 413884 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 413884 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 413884 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 413884 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 43809 times.
✓ Branch 9 taken 370075 times.
413884 if (date == locale["holidays"][i]["shortName"].asString())
544
3/6
✓ Branch 0 taken 43809 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 43809 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 43809 times.
✗ Branch 5 not taken.
43809 return DateFromLocaleHoliday(locale["holidays"][i], refYear);
545 }
546
547 4 return date;
548 43813 }
549
550 /*
551 * Shift a YYYY-MM-DD date by a number of days. Used to schedule relative to
552 * a floating holiday ("7 days before Thanksgiving") the same way
553 * startTimeOffset shifts a sunrise/sunset time.
554 *
555 * Holidays resolve to a yearless "0000-MM-DD" (see DateFromLocaleHoliday) and
556 * DateInRange() treats MMDD values as wrapping across Dec 31/Jan 1, so the
557 * arithmetic is done in a real year (refYear, else the current year, so leap
558 * days land where they should) and the result is stamped back to year 0.
559 * Anything DateStrToInt() rejects - an unresolved holiday name or the
560 * "0000-00-00" error sentinel - is passed through untouched.
561 */
562 43813 static std::string ApplyDateOffset(const std::string& date, int offsetDays, int refYear) {
563
7/10
✓ Branch 0 taken 37826 times.
✓ Branch 1 taken 5987 times.
✓ Branch 2 taken 37826 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 37826 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 37826 times.
✓ Branch 8 taken 5987 times.
✓ Branch 9 taken 37826 times.
43813 if ((offsetDays == 0) || (date.length() != 10) || (DateStrToInt(date.c_str()) == 0))
564
1/2
✓ Branch 0 taken 5987 times.
✗ Branch 1 not taken.
5987 return date;
565
566 37826 int year = atoi(date.c_str());
567 37826 int calcYear = year;
568
1/2
✓ Branch 0 taken 37826 times.
✗ Branch 1 not taken.
37826 if (calcYear == 0) {
569
1/2
✓ Branch 0 taken 37826 times.
✗ Branch 1 not taken.
37826 if (refYear != 0) {
570 37826 calcYear = refYear;
571 } else {
572 ✗ time_t currTime = time(NULL);
573 struct tm now;
574 ✗ localtime_r(&currTime, &now);
575 ✗ calcYear = now.tm_year + 1900;
576 }
577 }
578
579 37826 struct tm t = {};
580 37826 t.tm_year = calcYear - 1900;
581 37826 t.tm_mon = atoi(date.c_str() + 5) - 1;
582 37826 t.tm_mday = atoi(date.c_str() + 8) + offsetDays;
583
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 37826 times.
37826 if (timegm(&t) == (time_t)-1)
584 ✗ return date;
585
586 char str[11];
587
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 37826 times.
37826 snprintf(str, sizeof(str), "%04d-%02d-%02d", year == 0 ? 0 : t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);
588
1/2
✓ Branch 0 taken 37826 times.
✗ Branch 1 not taken.
75652 return str;
589 }
590
591 /*
592 * Turn a schedule start/end date string (literal YYYY-MM-DD or holiday name)
593 * into a YYYY-MM-DD string. The day offset only applies to holiday names.
594 *
595 * With no refYear, DateFromLocaleHoliday rolls a floating holiday forward to
596 * next year once this year's has passed. That is fine for the bare holiday
597 * but wrong once an offset extends the range past it - "Diwali +7" reloaded
598 * the day after Diwali would anchor on next year's date and drop the rest of
599 * the window. So with an offset we resolve explicitly in the current year,
600 * apply the offset, and only roll to next year if the *offset* date has passed.
601 */
602 48240 std::string ScheduleEntry::ResolveDate(const std::string& date, int offsetDays, int refYear) {
603
5/6
✓ Branch 0 taken 48240 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4430 times.
✓ Branch 3 taken 43810 times.
✓ Branch 4 taken 4430 times.
✓ Branch 5 taken 43810 times.
48240 if (date.empty() || isdigit((unsigned char)date[0]))
604
1/2
✓ Branch 0 taken 4430 times.
✗ Branch 1 not taken.
4430 return date;
605
606
4/4
✓ Branch 0 taken 228 times.
✓ Branch 1 taken 43582 times.
✓ Branch 2 taken 115 times.
✓ Branch 3 taken 113 times.
43810 if ((refYear != 0) || (offsetDays == 0))
607
3/6
✓ Branch 0 taken 43697 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 43697 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 43697 times.
✗ Branch 5 not taken.
43697 return ApplyDateOffset(CheckHoliday(date, refYear), offsetDays, refYear);
608
609 113 time_t currTime = time(NULL);
610 struct tm now;
611 113 localtime_r(&currTime, &now);
612 113 int year = now.tm_year + 1900;
613 113 int today = ((now.tm_mon + 1) * 100) + now.tm_mday;
614
615
3/6
✓ Branch 0 taken 113 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 113 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 113 times.
✗ Branch 5 not taken.
113 std::string resolved = ApplyDateOffset(CheckHoliday(date, year), offsetDays, year);
616
1/2
✓ Branch 0 taken 113 times.
✗ Branch 1 not taken.
113 if (resolved.length() == 10) {
617
1/2
✓ Branch 0 taken 113 times.
✗ Branch 1 not taken.
113 int mmdd = DateStrToInt(resolved.c_str());
618 // yearless (< 10000) and already behind us this year
619
4/6
✓ Branch 0 taken 113 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 113 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 110 times.
113 if ((mmdd > 0) && (mmdd < 10000) && (mmdd < today))
620
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 resolved = ApplyDateOffset(CheckHoliday(date, year + 1), offsetDays, year + 1);
621 }
622 113 return resolved;
623 113 }
624
625 105616 static int WallSeconds(int h, int m, int s) {
626 105616 return (h * SECONDS_PER_HOUR) + (m * SECONDS_PER_MINUTE) + s;
627 }
628
629 679 static void mapTimeString(const std::string& tm, int& h, int& m, int& s) {
630
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 679 times.
679 if (!ParseTimeString(tm, h, m, s)) {
631 ✗ LogErr(VB_SCHEDULE, "Invalid time '%s' in schedule, using midnight\n", tm.c_str());
632 ✗ ScheduleLoadWarning(47, "Schedule: invalid time '" + tm + "'");
633 ✗ h = 0;
634 ✗ m = 0;
635 ✗ s = 0;
636 }
637 679 }
638
639 29313 void ScheduleEntry::pushStartEndTimes(int dow, int& delta, time_t deltaThreshold, time_t now) {
640 // Local copies, as in GetOccurrencesOnDate(): GetTimeFromSun() writes the
641 // day's resolved time into them, and writing it into the entry's own
642 // fields made each day start from the previous day's sun time.
643 29313 int sh = startHour, sm = startMinute, ss = startSecond;
644 29313 int eh = endHour, em = endMinute, es = endSecond;
645 29313 time_t startTime = GetTimeOnDOW(now, dow, sh, sm, ss);
646 29313 time_t endTime = GetTimeOnDOW(now, dow, eh, em, es);
647
648
2/2
✓ Branch 0 taken 281 times.
✓ Branch 1 taken 29032 times.
29313 if (startTimeStr.find(":") == std::string::npos) {
649
2/4
✓ Branch 0 taken 281 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 281 times.
✗ Branch 3 not taken.
281 GetTimeFromSun(startTime, startTimeStr, startTimeOffset, sh, sm, ss);
650 }
651
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 29277 times.
29313 if (endTimeStr.find(":") == std::string::npos) {
652
2/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
36 GetTimeFromSun(endTime, endTimeStr, endTimeOffset, eh, em, es);
653 }
654 // Decide "runs past midnight" from the wall-clock times, not the epoch
655 // times: on a DST change day those differ by an hour, and a 02:30 start
656 // on the spring-forward day normalizes to 03:30, past a 03:00 end, which
657 // turned a half-hour entry into a 23.5 hour one. And build the next-day
658 // end with mktime rather than adding 86400, which is an hour off across
659 // the change.
660 // A sun-relative time can itself fall after midnight, so with one of
661 // those compare the actual times; the wall-clock test is for fixed times.
662
3/4
✓ Branch 0 taken 29032 times.
✓ Branch 1 taken 281 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 29032 times.
29313 bool sunTimes = (startTimeStr.find(":") == std::string::npos) || (endTimeStr.find(":") == std::string::npos);
663
2/2
✓ Branch 0 taken 281 times.
✓ Branch 1 taken 29032 times.
58345 bool crossesMidnight = sunTimes ? (endTime < startTime)
664 29032 : (WallSeconds(eh, em, es) < WallSeconds(sh, sm, ss));
665
2/2
✓ Branch 0 taken 226 times.
✓ Branch 1 taken 29087 times.
29313 if (crossesMidnight) {
666 226 endTime = GetTimeOnDOW(now, dow + 1, eh, em, es);
667
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 226 times.
226 if (endTimeStr.find(":") == std::string::npos) {
668 ✗ GetTimeFromSun(endTime, endTimeStr, endTimeOffset, eh, em, es);
669 }
670
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 29086 times.
29087 } else if (endTime < startTime) {
671 // Same-day window squeezed by the spring-forward gap.
672 1 endTime = startTime;
673 }
674
675 // A command occurrence has no duration (the UI saves endTime = startTime),
676 // so once its second passed it used to be dropped here -- by any reload in
677 // the same second or a few seconds later, which is exactly the stall or
678 // clock step the scheduler's command catch-up exists for. Keep commands
679 // through the catch-up window; the scheduler's check window stops them
680 // firing twice.
681
2/2
✓ Branch 0 taken 595 times.
✓ Branch 1 taken 28718 times.
29313 time_t cutoff = playlist.empty() ? now - SCHEDULE_COMMAND_CATCHUP_S : now;
682
2/2
✓ Branch 0 taken 1108 times.
✓ Branch 1 taken 28205 times.
29313 if (endTime < cutoff)
683 26762 return;
684
685
3/4
✓ Branch 0 taken 28205 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25654 times.
✓ Branch 3 taken 2551 times.
28205 if (!DateInRange(startTime, startDate, endDate)) {
686 25654 return;
687 }
688
689
2/2
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 2491 times.
2551 if (delta != 0) {
690 // Only adjust if the threshold and start times are in the future
691
1/2
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
60 if (deltaThreshold > now) {
692
4/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 58 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
60 if (deltaThreshold > startTime && startTime > now) {
693 1 startTime += delta;
694 }
695
696
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 58 times.
60 if (deltaThreshold > endTime) {
697 2 endTime += delta;
698 }
699 } else {
700 // if threshold time has passed, set delta back to 0
701 ✗ delta = 0;
702 }
703 }
704
705 // Occurrences end where the next one starts, half-open like every endTime
706 // check. Ending a second early let a lower entry start in the gap (#3141).
707
3/4
✓ Branch 0 taken 322 times.
✓ Branch 1 taken 2229 times.
✓ Branch 2 taken 322 times.
✗ Branch 3 not taken.
2551 if ((repeatInterval > 0) && (startTime != endTime)) {
708 322 time_t horizon = now + SCHEDULE_REPEAT_HORIZON_S;
709 322 time_t newEnd = std::min(startTime + repeatInterval, endTime);
710
4/4
✓ Branch 0 taken 1423 times.
✓ Branch 1 taken 20 times.
✓ Branch 2 taken 1121 times.
✓ Branch 3 taken 302 times.
1443 while ((startTime < endTime) && (startTime < horizon)) {
711
2/2
✓ Branch 0 taken 978 times.
✓ Branch 1 taken 143 times.
1121 if (newEnd >= cutoff) {
712
1/2
✓ Branch 0 taken 978 times.
✗ Branch 1 not taken.
978 startEndTimes.emplace_back(startTime, newEnd);
713 }
714 1121 newEnd += repeatInterval;
715
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 1097 times.
1121 if (newEnd > endTime) {
716 24 newEnd = endTime;
717 }
718 1121 startTime += repeatInterval;
719 }
720 322 } else {
721
1/2
✓ Branch 0 taken 2229 times.
✗ Branch 1 not taken.
2229 startEndTimes.emplace_back(startTime, endTime);
722 }
723 }
724
725 /*
726 * Resolve this entry's start/end date range as it applies to a given year.
727 *
728 * startDate/endDate are resolved once at load time, so a holiday-anchored range
729 * carries whichever date the holiday fell on for the year FPP was started in.
730 * Floating holidays (Easter, Thanksgiving, the lunar calendars) move year to
731 * year, so re-resolve them against the year actually being expanded.
732 */
733 23776 void ScheduleEntry::GetDateRangeForYear(int year, int& sDate, int& eDate) {
734 23776 sDate = startDate;
735 23776 eDate = endDate;
736
737
1/2
✓ Branch 0 taken 23776 times.
✗ Branch 1 not taken.
23776 std::string resolved = ResolveDate(startDateStr, startDateOffset, year);
738
1/2
✓ Branch 0 taken 23776 times.
✗ Branch 1 not taken.
23776 if (resolved.length() == 10)
739
1/2
✓ Branch 0 taken 23776 times.
✗ Branch 1 not taken.
23776 sDate = DateStrToInt(resolved.c_str());
740
741
1/2
✓ Branch 0 taken 23776 times.
✗ Branch 1 not taken.
23776 resolved = ResolveDate(endDateStr, endDateOffset, year);
742
1/2
✓ Branch 0 taken 23776 times.
✗ Branch 1 not taken.
23776 if (resolved.length() == 10)
743
1/2
✓ Branch 0 taken 23776 times.
✗ Branch 1 not taken.
23776 eDate = DateStrToInt(resolved.c_str());
744 23776 }
745
746 24168 int ScheduleEntry::DayIndexToMask(int dayIndex) {
747
12/15
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 23997 times.
✓ Branch 8 taken 21 times.
✓ Branch 9 taken 21 times.
✓ Branch 10 taken 21 times.
✓ Branch 11 taken 21 times.
✓ Branch 12 taken 21 times.
✓ Branch 13 taken 21 times.
✓ Branch 14 taken 21 times.
24168 switch (dayIndex) {
748 1 case INX_SUN:
749 1 return INX_DAY_MASK_SUNDAY;
750 1 case INX_MON:
751 1 return INX_DAY_MASK_MONDAY;
752 ✗ case INX_TUE:
753 ✗ return INX_DAY_MASK_TUESDAY;
754 21 case INX_WED:
755 21 return INX_DAY_MASK_WEDNESDAY;
756 ✗ case INX_THU:
757 ✗ return INX_DAY_MASK_THURSDAY;
758 ✗ case INX_FRI:
759 ✗ return INX_DAY_MASK_FRIDAY;
760 1 case INX_SAT:
761 1 return INX_DAY_MASK_SATURDAY;
762 23997 case INX_EVERYDAY:
763 23997 return INX_DAY_MASK_EVERYDAY;
764 21 case INX_WKDAYS:
765 21 return INX_DAY_MASK_WEEKDAYS;
766 21 case INX_WKEND:
767 21 return INX_DAY_MASK_WEEKEND;
768 21 case INX_M_W_F:
769 21 return INX_DAY_MASK_M_W_F;
770 21 case INX_T_TH:
771 21 return INX_DAY_MASK_T_TH;
772 21 case INX_SUN_TO_THURS:
773 21 return INX_DAY_MASK_SUN_TO_THURS;
774 21 case INX_FRI_SAT:
775 21 return INX_DAY_MASK_FRI_SAT;
776 }
777
778 // A custom day mask (INX_DAY_MASK set) or an odd/even index carries no
779 // enumerated case; it is already a mask or is resolved separately, so
780 // return it unchanged.
781 21 return dayIndex;
782 }
783
784 43 bool ScheduleEntry::IsOddEvenMatch(time_t dayTime) const {
785 // Odd/Even is based on the FPP 'epoch', the date of the first commit to
786 // the FPP repository on github, July 15, 2013. Count calendar days, not
787 // seconds: the epoch used to be built with tm_isdst=0, an hour off during
788 // DST, so from 00:00 to 00:59 in summer -- exactly when the midnight
789 // schedule reload runs -- the count came out a day short and odd entries
790 // were scheduled on even days.
791 struct tm local;
792 43 localtime_r(&dayTime, &local);
793 43 long daysSince = DaysFromCivil(local.tm_year + 1900, local.tm_mon + 1, local.tm_mday) -
794 43 DaysFromCivil(2013, 7, 15);
795
796
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 22 times.
43 if (daysSince % 2)
797 21 return dayIndex == INX_ODD_DAY;
798
799 22 return dayIndex == INX_EVEN_DAY;
800 }
801
802 /*
803 * Does this entry's day selection include the given absolute date?
804 */
805 23868 bool ScheduleEntry::OccursOnDate(int year, int month, int mday) {
806
4/4
✓ Branch 0 taken 23852 times.
✓ Branch 1 taken 16 times.
✓ Branch 2 taken 16 times.
✓ Branch 3 taken 23836 times.
23868 if ((dayIndex == INX_ODD_DAY) || (dayIndex == INX_EVEN_DAY))
807 32 return IsOddEvenMatch(GetTimeOnDate(year, month, mday, 12, 0, 0));
808
809 23836 int mask = DayIndexToMask(dayIndex);
810
811 // Day masks run Sunday (0x04000) through Saturday (0x00100)
812 23836 int dowBit = INX_DAY_MASK_SUNDAY >> GetDOWOnDate(year, month, mday);
813
814 23836 return (mask & dowBit) != 0;
815 }
816
817 /*
818 * Expand this entry into the occurrences that fall on a given absolute date.
819 *
820 * This mirrors pushStartEndTimes() but is anchored to a real date instead of an
821 * offset from the current week, and keeps occurrences that are already in the
822 * past, so the calendar view can show any range the user navigates to. The
823 * scheduler's time delta is deliberately not applied - it is a transient
824 * adjustment to the running schedule, not part of the configured schedule.
825 *
826 * The entry's enabled flag is not consulted here so callers can choose to show
827 * disabled entries; filter on it yourself if you only want live occurrences.
828 */
829 23868 void ScheduleEntry::GetOccurrencesOnDate(int year, int month, int mday,
830 std::vector<std::pair<time_t, time_t>>& occurrences) {
831
3/4
✓ Branch 0 taken 23868 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 92 times.
✓ Branch 3 taken 23776 times.
23868 if (!OccursOnDate(year, month, mday))
832 23634 return;
833
834 23776 time_t startTime = GetTimeOnDate(year, month, mday, startHour, startMinute, startSecond);
835 23776 time_t endTime = GetTimeOnDate(year, month, mday, endHour, endMinute, endSecond);
836
837 23776 int sh = startHour, sm = startMinute, ss = startSecond;
838 23776 int eh = endHour, em = endMinute, es = endSecond;
839
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23776 times.
23776 if (startTimeStr.find(":") == std::string::npos) {
840 ✗ GetTimeFromSun(startTime, startTimeStr, startTimeOffset, sh, sm, ss);
841 }
842
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23776 times.
23776 if (endTimeStr.find(":") == std::string::npos) {
843 ✗ GetTimeFromSun(endTime, endTimeStr, endTimeOffset, eh, em, es);
844 }
845 // As in pushStartEndTimes(): wall-clock crossover test, next-day end via
846 // mktime.
847
2/4
✓ Branch 0 taken 23776 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 23776 times.
23776 bool sunTimes = (startTimeStr.find(":") == std::string::npos) || (endTimeStr.find(":") == std::string::npos);
848
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23776 times.
23776 bool crossesMidnight = sunTimes ? (endTime < startTime) : (WallSeconds(eh, em, es) < WallSeconds(sh, sm, ss));
849
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 23757 times.
23776 if (crossesMidnight) {
850 19 endTime = GetTimeOnDate(year, month, mday + 1, eh, em, es);
851
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (endTimeStr.find(":") == std::string::npos) {
852 ✗ GetTimeFromSun(endTime, endTimeStr, endTimeOffset, eh, em, es);
853 }
854
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23757 times.
23757 } else if (endTime < startTime) {
855 ✗ endTime = startTime;
856 }
857
858 23776 int sDate = 0;
859 23776 int eDate = 0;
860
1/2
✓ Branch 0 taken 23776 times.
✗ Branch 1 not taken.
23776 GetDateRangeForYear(year, sDate, eDate);
861
862
3/4
✓ Branch 0 taken 23776 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23542 times.
✓ Branch 3 taken 234 times.
23776 if (!DateInRange(startTime, sDate, eDate))
863 23542 return;
864
865 // Occurrences end where the next starts, as in pushStartEndTimes().
866
3/4
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 209 times.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
234 if ((repeatInterval > 0) && (startTime != endTime)) {
867 25 time_t newEnd = std::min(startTime + repeatInterval, endTime);
868
2/2
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 25 times.
110 while (startTime < endTime) {
869
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 occurrences.push_back(std::pair<time_t, time_t>(startTime, newEnd));
870 85 newEnd += repeatInterval;
871
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 57 times.
85 if (newEnd > endTime) {
872 28 newEnd = endTime;
873 }
874 85 startTime += repeatInterval;
875 }
876 25 } else {
877
1/2
✓ Branch 0 taken 209 times.
✗ Branch 1 not taken.
209 occurrences.push_back(std::pair<time_t, time_t>(startTime, endTime));
878 }
879 }
880
881 // Raised whenever the Latitude/Longitude settings can't be used, whether they
882 // are unset or unparseable, so that the add and the remove always pair on the
883 // same text. See www/help/warning-helpers/warning-48.md.
884 static const char* LATLON_WARNING = "Schedule: Latitude/Longitude not set or not numeric — dawn/dusk times use default coordinates";
885
886 // strtod() plus an end-pointer check. A coordinate has to consume the whole
887 // string, so "38.9N" is rejected rather than silently becoming 38.9. It also
888 // has to be a real angle within +-limit: strtod() happily accepts "nan",
889 // "inf" and 1e300, which went on into the sun calculation as garbage instead
890 // of raising the "not usable" warning.
891 529 static bool ParseCoordinate(const std::string& str, double limit, double& out) {
892
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 529 times.
529 if (str.empty()) {
893 ✗ return false;
894 }
895
896 529 const char* start = str.c_str();
897 529 char* end = nullptr;
898 529 errno = 0;
899 529 double v = strtod(start, &end);
900
901
3/4
✓ Branch 0 taken 459 times.
✓ Branch 1 taken 70 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 459 times.
529 if ((end == start) || (errno == ERANGE)) {
902 70 return false;
903 }
904
905
4/8
✗ Branch 0 not taken.
✓ Branch 1 taken 459 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 459 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 459 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 459 times.
459 while ((*end == ' ') || (*end == '\t') || (*end == '\r') || (*end == '\n')) {
906 ✗ end++;
907 }
908
909
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 459 times.
459 if (*end != '\0') {
910 ✗ return false;
911 }
912
913
5/6
✓ Branch 0 taken 424 times.
✓ Branch 1 taken 35 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 424 times.
✓ Branch 4 taken 35 times.
✓ Branch 5 taken 424 times.
459 if (!std::isfinite(v) || (std::fabs(v) > limit)) {
914 35 return false;
915 }
916
917 424 out = v;
918 424 return true;
919 }
920
921 317 void ScheduleEntry::GetTimeFromSun(time_t& when, const std::string info,
922 const int infoOffset,
923 int& h, int& m, int& s) {
924 317 time_t origWhen = when;
925
1/2
✓ Branch 0 taken 317 times.
✗ Branch 1 not taken.
317 std::string latStr = getSetting("Latitude");
926
1/2
✓ Branch 0 taken 317 times.
✗ Branch 1 not taken.
317 std::string lonStr = getSetting("Longitude");
927
928 317 double lat = 0.0;
929 317 double lon = 0.0;
930
931 // Latitude/Longitude are free-text settings living in a hand-editable file,
932 // so they can hold anything - "N38.9", a stray unit, an empty string.
933 // std::stod() throws on those, and this runs on the daemon startup path
934 // (Scheduler ctor -> LoadScheduleFromFile -> pushStartEndTimes) with no
935 // catch anywhere above it, which turned one bad character into a boot loop.
936 // Parse without exceptions and fall back to the defaults instead.
937
5/6
✓ Branch 0 taken 212 times.
✓ Branch 1 taken 105 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 212 times.
✓ Branch 4 taken 105 times.
✓ Branch 5 taken 212 times.
317 if (!ParseCoordinate(latStr, 90.0, lat) || !ParseCoordinate(lonStr, 180.0, lon)) {
938 105 lat = 38.938524;
939 105 lon = -104.600945;
940
941
1/2
✓ Branch 0 taken 105 times.
✗ Branch 1 not taken.
105 LogErr(VB_SCHEDULE, "Error, Latitude/Longitude not usable ('%s', '%s'), using Falcon, Colorado coordinates!\n",
942 latStr.c_str(), lonStr.c_str());
943 // A load warning, so the next load retracts it: removing it only here,
944 // when sun times are next computed with good coordinates, left it up
945 // for good once the last sun-relative entry was deleted instead. A
946 // Latitude/Longitude change reloads the schedule (see Scheduler()).
947
2/4
✓ Branch 0 taken 105 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 105 times.
✗ Branch 3 not taken.
210 ScheduleLoadWarning(48, LATLON_WARNING);
948 }
949 317 time_t currTime = when;
950 struct tm local;
951 struct tm mtm;
952
953 317 localtime_r(&currTime, &local);
954 317 localtime_r(&currTime, &mtm);
955
956
1/2
✓ Branch 0 taken 317 times.
✗ Branch 1 not taken.
317 LogExcess(VB_SCHEDULE, "Lat/Lon: %.6f, %.6f\n", lat, lon);
957
1/2
✓ Branch 0 taken 317 times.
✗ Branch 1 not taken.
317 LogExcess(VB_SCHEDULE, "Lookup Date is %02d/%02d/%04d\n",
958 local.tm_mon + 1, local.tm_mday, local.tm_year + 1900);
959
960 // Ask about local noon of the day. SunRise finds the events within 12
961 // hours either side of the time it is given, so asked from a time near
962 // midnight it returned the other day's sunset or sunrise -- and the time
963 // passed in here can be any time on the day.
964 317 struct tm noonTm = local;
965 317 noonTm.tm_hour = 12;
966 317 noonTm.tm_min = 0;
967 317 noonTm.tm_sec = 0;
968 317 noonTm.tm_isdst = -1;
969 317 time_t noon = mktime(&noonTm);
970
971 SunRise sr;
972
5/10
✓ Branch 0 taken 317 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 317 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 317 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 317 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 317 times.
317 if ((info == "Dawn") || (info == "Dusk"))
973 ✗ sr.calculate(lat, lon, noon, TT_CIVIL);
974 else
975
1/2
✓ Branch 0 taken 317 times.
✗ Branch 1 not taken.
317 sr.calculate(lat, lon, noon);
976
977 // Find midnight on the 'when' day. tm_isdst = -1: on a DST change day
978 // midnight and 'when' are on different sides of the change.
979 317 mtm.tm_hour = 0;
980 317 mtm.tm_min = 0;
981 317 mtm.tm_sec = 0;
982 317 mtm.tm_isdst = -1;
983 317 time_t midnight = mktime(&mtm);
984
985
4/8
✓ Branch 0 taken 317 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 317 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 317 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 317 times.
317 const bool isRise = (info == "SunRise") || (info == "Dawn");
986
2/8
✓ Branch 0 taken 317 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 317 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
317 const bool isSet = (info == "SunSet") || (info == "Dusk");
987
988 // Far enough north or south the sun can stay up (or down) all day, and
989 // then there is no rise or set to anchor to. SunRise reports that as
990 // hasRise/hasSet false with the time left at 0, which the clamp below
991 // read as "before the day starts" and put a SunSet show at 8AM. Use the
992 // same conventional times the clamp falls back to -- 8AM for a rise,
993 // 8PM for a set -- with the entry's offset still applied, so entries
994 // keep their order and a dusk show stays in the evening.
995
4/8
✗ Branch 0 not taken.
✓ Branch 1 taken 317 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 317 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 35 times.
✓ Branch 7 taken 282 times.
317 if ((isRise && !sr.hasRise) || (isSet && !sr.hasSet)) {
996 35 struct tm ftm = local;
997
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 35 times.
35 ftm.tm_hour = isRise ? 8 : 20;
998 35 ftm.tm_min = 0;
999 35 ftm.tm_sec = 0;
1000 35 ftm.tm_isdst = -1;
1001 35 time_t target = mktime(&ftm) + (time_t)infoOffset * SECONDS_PER_MINUTE;
1002 struct tm tt;
1003 35 localtime_r(&target, &tt);
1004 35 h = tt.tm_hour;
1005 35 m = tt.tm_min;
1006 35 s = tt.tm_sec;
1007 35 when = target;
1008
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 35 times.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
35 LogWarn(VB_SCHEDULE, "No %s on %02d/%02d/%04d at %.6f, %.6f (sun %s all day), using %02d:%02d:%02d\n",
1009 info.c_str(), local.tm_mon + 1, local.tm_mday, local.tm_year + 1900, lat, lon,
1010 sr.isVisible ? "up" : "down", h, m, s);
1011 35 return;
1012 }
1013
1014 282 time_t event = midnight;
1015
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 282 times.
282 if (isRise)
1016 ✗ event = sr.riseTime;
1017
1/2
✓ Branch 0 taken 282 times.
✗ Branch 1 not taken.
282 else if (isSet)
1018 282 event = sr.setTime;
1019
1020 // Work from the absolute event time and let localtime() give the wall
1021 // clock. This used to add "seconds since midnight" to the wall-clock
1022 // midnight, which is an hour off on a DST change day: a 17:00 sunset on
1023 // the fall-back day was scheduled at 18:00.
1024 282 time_t target = event + (time_t)infoOffset * SECONDS_PER_MINUTE;
1025 struct tm tt;
1026 282 localtime_r(&target, &tt);
1027 struct tm et;
1028 282 localtime_r(&event, &et);
1029
1030 // Clamp only when the sun event itself is not on this day, which means
1031 // the time zone or location is wrong. An offset may legitimately carry
1032 // the time past midnight (SunSet + 4 hours far north in summer); that used
1033 // to be clamped to 8PM, hours from what was asked for.
1034
2/4
✓ Branch 0 taken 282 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 282 times.
282 if ((et.tm_year != local.tm_year) || (et.tm_yday != local.tm_yday)) {
1035 ✗ if (event < midnight) {
1036 ✗ LogWarn(VB_SCHEDULE, "%s with offset %d falls before the day starts, using 8AM. "
1037 "Check your time zone to make sure it is valid.\n",
1038 info.c_str(), infoOffset);
1039 ✗ h = 8;
1040 } else {
1041 ✗ LogWarn(VB_SCHEDULE, "%s with offset %d falls after the day ends, using 8PM. "
1042 "Check your time zone to make sure it is valid.\n",
1043 info.c_str(), infoOffset);
1044 ✗ h = 20;
1045 }
1046 ✗ m = 0;
1047 ✗ s = 0;
1048
1049 // FIXME, move the following to Excess once the issues are worked out with SunRise class
1050 ✗ LogWarn(VB_SCHEDULE, "w: %lld, m: %lld, rt: %lld, st: %lld\n",
1051 (long long)when, (long long)midnight, (long long)sr.riseTime, (long long)sr.setTime);
1052 ✗ LogWarn(VB_SCHEDULE, "info: %s, infoOffset: %d, lat: %.6f, lon: %.6f, date: %02d/%02d/%04d\n",
1053 info.c_str(), infoOffset, lat, lon,
1054 local.tm_mon + 1, local.tm_mday, local.tm_year + 1900);
1055
1056 ✗ mtm.tm_hour = h;
1057 ✗ mtm.tm_min = 0;
1058 ✗ mtm.tm_sec = 0;
1059 ✗ mtm.tm_isdst = -1;
1060 ✗ when = mktime(&mtm);
1061 ✗ return;
1062 }
1063
1064 282 h = tt.tm_hour;
1065 282 m = tt.tm_min;
1066 282 s = tt.tm_sec;
1067 282 when = target;
1068
1069
1/2
✓ Branch 0 taken 282 times.
✗ Branch 1 not taken.
282 LogExcess(VB_SCHEDULE, "%s on %0d/%d/%d is at %02d:%02d:%02d\n", info.c_str(),
1070 local.tm_mon + 1, local.tm_mday, local.tm_year + 1900, h, m, s);
1071
4/4
✓ Branch 0 taken 282 times.
✓ Branch 1 taken 35 times.
✓ Branch 2 taken 282 times.
✓ Branch 3 taken 35 times.
352 }
1072
1073 346 int ScheduleEntry::LoadFromJson(Json::Value& entry) {
1074
3/4
✓ Branch 0 taken 345 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 345 times.
✗ Branch 3 not taken.
346 enabled = entry["enabled"].asInt();
1075
2/4
✓ Branch 0 taken 345 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 345 times.
✗ Branch 3 not taken.
345 playlist = entry["playlist"].asString();
1076
3/4
✓ Branch 0 taken 345 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✓ Branch 3 taken 1 times.
345 dayIndex = entry["day"].asInt();
1077
2/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✗ Branch 3 not taken.
344 repeatInterval = entry["repeat"].asInt();
1078
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 343 times.
344 if (repeatInterval < 0) {
1079 // The UI only offers values from a dropdown, but schedule.json is
1080 // hand- and API-editable. A negative interval walks startTime
1081 // backwards in both occurrence-expansion loops, so they never reach
1082 // endTime and spin forever appending pairs until the box is out of
1083 // memory - on the scheduler thread or the calendar API thread.
1084
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_SCHEDULE, "Negative repeat interval %d for playlist %s, treating as no repeat\n",
1085 repeatInterval, playlist.c_str());
1086
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 ScheduleLoadWarning(47, "Schedule: negative repeat interval for playlist '" + playlist + "'");
1087 1 repeatInterval = 0;
1088 }
1089 344 repeat = repeatInterval == 1;
1090
1091
2/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 344 times.
344 if (entry.isMember("sequence"))
1092 ✗ sequence = entry["sequence"].asBool();
1093
1094
3/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 327 times.
344 if (entry.isMember("command"))
1095
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 command = entry["command"].asString();
1096
1097
3/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 327 times.
344 if (entry.isMember("args"))
1098
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 args = entry["args"];
1099
1100
3/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 338 times.
344 if (entry.isMember("multisyncCommand"))
1101
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 multisyncCommand = (bool)entry["multisyncCommand"].asInt();
1102 else
1103 338 multisyncCommand = false;
1104
1105
3/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 338 times.
344 if (entry.isMember("multisyncCommand"))
1106
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 multisyncHosts = entry["multisyncHosts"].asString();
1107 else
1108
1/2
✓ Branch 0 taken 338 times.
✗ Branch 1 not taken.
338 multisyncHosts = "";
1109
1110 344 repeatInterval /= 100;
1111 344 repeatInterval *= 60; //seconds betweeen intervals
1112
1113
8/18
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 344 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 344 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 344 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 344 times.
✓ Branch 12 taken 344 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 344 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
344 if (!entry.isMember("startTime") || (entry["startTime"].asString() == "")) {
1114 ✗ LogErr(VB_SCHEDULE, "Missing or invalid startTime for playlist %s\n",
1115 playlist.c_str());
1116 ✗ ScheduleLoadWarning(47, "Schedule: missing or invalid start time for playlist '" + playlist + "'");
1117 ✗ return 0;
1118 }
1119
2/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✗ Branch 3 not taken.
344 startTimeStr = entry["startTime"].asString();
1120 344 std::size_t found = startTimeStr.find(":");
1121
2/2
✓ Branch 0 taken 336 times.
✓ Branch 1 taken 8 times.
344 if (found != std::string::npos)
1122
1/2
✓ Branch 0 taken 336 times.
✗ Branch 1 not taken.
336 mapTimeString(startTimeStr, startHour, startMinute, startSecond);
1123
1124
8/18
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 344 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 344 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 344 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 344 times.
✓ Branch 12 taken 344 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 344 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
344 if (!entry.isMember("endTime") || (entry["endTime"].asString() == "")) {
1125 ✗ LogErr(VB_SCHEDULE, "Missing or invalid endTime for playlist %s\n",
1126 playlist.c_str());
1127 ✗ ScheduleLoadWarning(47, "Schedule: missing or invalid end time for playlist '" + playlist + "'");
1128 ✗ return 0;
1129 }
1130
2/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✗ Branch 3 not taken.
344 endTimeStr = entry["endTime"].asString();
1131 344 found = endTimeStr.find(":");
1132
2/2
✓ Branch 0 taken 343 times.
✓ Branch 1 taken 1 times.
344 if (found != std::string::npos)
1133
1/2
✓ Branch 0 taken 343 times.
✗ Branch 1 not taken.
343 mapTimeString(endTimeStr, endHour, endMinute, endSecond);
1134
1135 // The UI limits these offsets (date offsets to a year, sun-relative time
1136 // offsets to two hours); keep API/hand-edited files honest too, checking
1137 // before the int64 -> int narrowing so huge values can't wrap into range.
1138 // An unbounded time offset put an occurrence decades away, and the repeat
1139 // expansion then stepped from there: billions of iterations on the main
1140 // loop at every schedule load.
1141 1376 auto offsetFromJson = [&entry](const char* key, int limit, const char* unit) -> int {
1142
2/2
✓ Branch 0 taken 1219 times.
✓ Branch 1 taken 157 times.
1376 if (!entry.isMember(key))
1143 1219 return 0;
1144 157 const Json::Value& e = entry[key];
1145 Json::Value::LargestInt v;
1146
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 152 times.
157 if (e.isNull()) {
1147 5 return 0;
1148
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 148 times.
152 } else if (e.isString()) {
1149
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 v = atoll(e.asString().c_str());
1150
2/2
✓ Branch 0 taken 138 times.
✓ Branch 1 taken 10 times.
148 } else if (e.isIntegral()) {
1151 // asLargestInt() throws on an object, array or non-integral
1152 // double; a bad value in schedule.json must not abort fppd.
1153 138 v = e.asLargestInt();
1154 } else {
1155 10 LogErr(VB_SCHEDULE, "Invalid %s in schedule (not a number), using 0\n", key);
1156
4/8
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 10 times.
✗ Branch 7 not taken.
10 ScheduleLoadWarning(47, std::string("Schedule: ") + key + " is not a number");
1157 10 return 0;
1158 }
1159
4/4
✓ Branch 0 taken 140 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 133 times.
142 if ((v < -limit) || (v > limit)) {
1160 9 LogErr(VB_SCHEDULE, "Invalid %s %lld in schedule, using 0\n", key, (long long)v);
1161
5/10
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 9 times.
✗ Branch 9 not taken.
45 ScheduleLoadWarning(47, std::string("Schedule: ") + key + " out of range (-" + std::to_string(limit) +
1162
5/10
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 9 times.
✗ Branch 9 not taken.
36 ".." + std::to_string(limit) + " " + unit + ")");
1163 9 return 0;
1164 }
1165 133 return (int)v;
1166 344 };
1167
1/2
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
344 startTimeOffset = offsetFromJson("startTimeOffset", 1440, "minutes");
1168
1/2
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
344 endTimeOffset = offsetFromJson("endTimeOffset", 1440, "minutes");
1169
1/2
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
344 startDateOffset = offsetFromJson("startDateOffset", 366, "days");
1170
1/2
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
344 endDateOffset = offsetFromJson("endDateOffset", 366, "days");
1171
1172
8/18
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 344 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 344 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 344 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 344 times.
✓ Branch 12 taken 344 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 344 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
344 if (!entry.isMember("startDate") || (entry["startDate"].asString() == "")) {
1173 ✗ entry["startDate"] = "2019-01-01";
1174 }
1175
2/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✗ Branch 3 not taken.
344 startDateStr = entry["startDate"].asString();
1176
1/2
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
344 std::string tempStr = ResolveDate(startDateStr, startDateOffset);
1177
1178
2/2
✓ Branch 0 taken 342 times.
✓ Branch 1 taken 2 times.
344 if (tempStr.length() == 10) {
1179
1/2
✓ Branch 0 taken 342 times.
✗ Branch 1 not taken.
342 startDate = DateStrToInt(tempStr.c_str());
1180 } else {
1181 2 startDate = 20190101;
1182
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 startDateStr = "20190101";
1183 }
1184
1185
8/18
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 344 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 344 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 344 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 344 times.
✓ Branch 12 taken 344 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 344 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
344 if (!entry.isMember("endDate") || (entry["endDate"].asString() == "")) {
1186 ✗ entry["endDate"] = "2099-12-31";
1187 }
1188
2/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✗ Branch 3 not taken.
344 endDateStr = entry["endDate"].asString();
1189
1/2
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
344 tempStr = ResolveDate(endDateStr, endDateOffset);
1190
1191
2/2
✓ Branch 0 taken 342 times.
✓ Branch 1 taken 2 times.
344 if (tempStr.length() == 10) {
1192
1/2
✓ Branch 0 taken 342 times.
✗ Branch 1 not taken.
342 endDate = DateStrToInt(tempStr.c_str());
1193 } else {
1194 2 endDate = 20991231;
1195
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 endDateStr = "20991231";
1196 }
1197
1198
2/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 344 times.
✗ Branch 3 not taken.
344 stopType = entry["stopType"].asInt();
1199
2/4
✓ Branch 0 taken 344 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 344 times.
344 if ((stopType < 0) || (stopType > 2)) {
1200 // 0 graceful, 1 hard, 2 graceful after loop. Anything else indexed
1201 // past SchedulePrint's table; fall back to the default.
1202 ✗ LogErr(VB_SCHEDULE, "Invalid stop type %d for playlist %s, using graceful stop\n",
1203 stopType, playlist.c_str());
1204 ✗ stopType = 0;
1205 }
1206
1207 344 return 1;
1208 344 }
1209
1210 35431 Json::Value ScheduleEntry::GetJson(void) {
1211
1/2
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
35431 Json::Value e;
1212 char timeText[9];
1213
1214
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["enabled"] = (int)enabled;
1215
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["playlist"] = playlist;
1216
3/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 35423 times.
35431 if (playlist == "") {
1217
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 e["command"] = command;
1218
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 e["args"] = args;
1219
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 e["multisyncCommand"] = multisyncCommand;
1220
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 e["multisyncHosts"] = multisyncHosts;
1221 }
1222
1223
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["day"] = dayIndex;
1224
1225 35431 snprintf(timeText, sizeof(timeText), "%02d:%02d:%02d", startHour, startMinute, startSecond);
1226
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["startTime"] = timeText;
1227
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["startTimeStr"] = startTimeStr;
1228
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["startTimeOffset"] = startTimeOffset;
1229
1230 35431 snprintf(timeText, sizeof(timeText), "%02d:%02d:%02d", endHour, endMinute, endSecond);
1231
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["endTime"] = timeText;
1232
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["endTimeStr"] = endTimeStr;
1233
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["endTimeOffset"] = endTimeOffset;
1234
1235
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["repeat"] = (int)repeat;
1236
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["repeatInterval"] = (int)repeatInterval;
1237
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["startDate"] = startDateStr;
1238
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["startDateOffset"] = startDateOffset;
1239
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["startDateInt"] = startDate;
1240
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["endDate"] = endDateStr;
1241
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["endDateOffset"] = endDateOffset;
1242
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["endDateInt"] = endDate;
1243
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
35431 e["stopType"] = stopType;
1244
1/2
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
35431 e["stopTypeStr"] =
1245
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 35431 times.
35431 stopType == 2 ? "Graceful Loop" : stopType == 1 ? "Hard"
1246
2/4
✓ Branch 0 taken 35431 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35431 times.
✗ Branch 3 not taken.
106293 : "Graceful";
1247
1248 35431 return e;
1249 ✗ }
1250