| 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 |