data/RecurrenceRule.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "RecurrenceRule.h" | ||
| 2 | #include "ICalTime.h" | ||
| 3 | |||
| 4 | #include <QCoreApplication> | ||
| 5 | #include <QLocale> | ||
| 6 | #include <QLoggingCategory> | ||
| 7 | #include <QRegularExpression> | ||
| 8 | #include <QSet> | ||
| 9 | #include <QTimeZone> | ||
| 10 | #include <algorithm> | ||
| 11 | #include <limits> | ||
| 12 | |||
| 13 | ✗ | Q_LOGGING_CATEGORY(lcRecurrence, "mailjd.calendar.rrule") | |
| 14 | |||
| 15 | namespace { | ||
| 16 | |||
| 17 | const char *const kWeekdayTokens[] = {"MO", "TU", "WE", "TH", "FR", "SA", "SU"}; | ||
| 18 | constexpr qint64 kMaximumExpansionWork = 10000; | ||
| 19 | |||
| 20 | // Translation context "RecurrenceRule" (lupdate reads RecurrenceText::tr). | ||
| 21 | class RecurrenceText { | ||
| 22 | 168 | Q_DECLARE_TR_FUNCTIONS(RecurrenceRule) | |
| 23 | }; | ||
| 24 | |||
| 25 | 409 | bool parseIntList(const QString &value, int minimum, int maximum, bool allowNegative, | |
| 26 | QList<int> *out) { | ||
| 27 |
5/5✓ Branch 5 → 6 taken 409 times.
✓ Branch 6 → 7 taken 409 times.
✓ Branch 7 → 8 taken 409 times.
✓ Branch 30 → 9 taken 980 times.
✓ Branch 30 → 31 taken 408 times.
|
1388 | for (const QString &part : value.split(QLatin1Char(','), Qt::SkipEmptyParts)) { |
| 28 | 980 | bool ok = false; | |
| 29 |
2/2✓ Branch 10 → 11 taken 980 times.
✓ Branch 11 → 12 taken 980 times.
|
980 | const int number = part.trimmed().toInt(&ok); |
| 30 |
4/6✓ Branch 13 → 14 taken 980 times.
✗ Branch 13 → 16 not taken.
✓ Branch 14 → 15 taken 88 times.
✓ Branch 14 → 17 taken 892 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 88 times.
|
980 | if (!ok || number == 0 && minimum > 0) |
| 31 | 1 | return false; | |
| 32 |
5/6✓ Branch 17 → 18 taken 979 times.
✓ Branch 17 → 22 taken 1 time.
✓ Branch 18 → 19 taken 214 times.
✓ Branch 18 → 20 taken 765 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 979 times.
|
980 | if (number > maximum || number < (allowNegative ? -maximum : minimum)) |
| 33 | 1 | return false; | |
| 34 |
3/4✓ Branch 23 → 24 taken 765 times.
✓ Branch 23 → 26 taken 214 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 765 times.
|
979 | if (!allowNegative && number < minimum) |
| 35 | ✗ | return false; | |
| 36 |
1/1✓ Branch 26 → 27 taken 979 times.
|
979 | out->append(number); |
| 37 | 409 | } | |
| 38 | 408 | return !out->isEmpty(); | |
| 39 | } | ||
| 40 | |||
| 41 | 34 | QString joinInts(const QList<int> &values) { | |
| 42 | 34 | QStringList parts; | |
| 43 |
2/2✓ Branch 10 → 4 taken 316 times.
✓ Branch 10 → 11 taken 34 times.
|
350 | for (int value : values) |
| 44 |
2/2✓ Branch 5 → 6 taken 316 times.
✓ Branch 6 → 7 taken 316 times.
|
316 | parts.append(QString::number(value)); |
| 45 |
1/1✓ Branch 13 → 14 taken 34 times.
|
68 | return parts.join(QLatin1Char(',')); |
| 46 | 34 | } | |
| 47 | |||
| 48 | 224916 | QDateTime naive(const QDate &date, const QTime &time) { | |
| 49 |
2/2✓ Branch 2 → 3 taken 224916 times.
✓ Branch 3 → 4 taken 224916 times.
|
224916 | return QDateTime(date, time, QTimeZone::utc()); |
| 50 | } | ||
| 51 | |||
| 52 | 51995 | QDate weekStartOf(const QDate &date, int weekStart) { | |
| 53 | 51995 | return date.addDays(-((date.dayOfWeek() - weekStart + 7) % 7)); | |
| 54 | } | ||
| 55 | |||
| 56 | // First day of week 1 of `year` (RFC 5545: the first week with at least | ||
| 57 | // four days in that year, weeks starting on WKST). | ||
| 58 | 43836 | QDate firstWeekStart(int year, int weekStart) { | |
| 59 |
1/1✓ Branch 2 → 3 taken 43836 times.
|
43836 | const QDate january1(year, 1, 1); |
| 60 |
1/1✓ Branch 3 → 4 taken 43836 times.
|
43836 | const QDate start = weekStartOf(january1, weekStart); |
| 61 |
1/1✓ Branch 4 → 5 taken 43836 times.
|
43836 | const int daysInYear = 7 - int(start.daysTo(january1)); |
| 62 |
3/3✓ Branch 5 → 6 taken 21924 times.
✓ Branch 5 → 7 taken 21912 times.
✓ Branch 7 → 8 taken 21912 times.
|
87672 | return daysInYear >= 4 ? start : start.addDays(7); |
| 63 | } | ||
| 64 | |||
| 65 | // nth (n > 0) or nth-last (n < 0) `weekday` inside [first, last]. | ||
| 66 | 1424 | QDate nthWeekday(const QDate &first, const QDate &last, int weekday, int ordinal) { | |
| 67 |
2/2✓ Branch 2 → 3 taken 231 times.
✓ Branch 2 → 13 taken 1193 times.
|
1424 | if (ordinal > 0) { |
| 68 |
2/2✓ Branch 3 → 4 taken 231 times.
✓ Branch 4 → 5 taken 231 times.
|
231 | const QDate firstMatch = first.addDays((weekday - first.dayOfWeek() + 7) % 7); |
| 69 |
1/1✓ Branch 5 → 6 taken 231 times.
|
231 | const QDate result = firstMatch.addDays(qint64(ordinal - 1) * 7); |
| 70 |
1/3✓ Branch 8 → 9 taken 231 times.
✗ Branch 8 → 10 not taken.
✗ Branch 10 → 11 not taken.
|
231 | return result <= last ? result : QDate(); |
| 71 | } | ||
| 72 |
2/2✓ Branch 13 → 14 taken 1193 times.
✓ Branch 14 → 15 taken 1193 times.
|
1193 | const QDate lastMatch = last.addDays(-((last.dayOfWeek() - weekday + 7) % 7)); |
| 73 |
1/1✓ Branch 15 → 16 taken 1193 times.
|
1193 | const QDate result = lastMatch.addDays(qint64(ordinal + 1) * 7); |
| 74 |
1/3✓ Branch 18 → 19 taken 1193 times.
✗ Branch 18 → 20 not taken.
✗ Branch 20 → 21 not taken.
|
1193 | return result >= first ? result : QDate(); |
| 75 | } | ||
| 76 | |||
| 77 | struct Period { | ||
| 78 | QList<QDate> days; | ||
| 79 | QDateTime earliest; // earliest possible stamp of the period | ||
| 80 | int hour = -1; // HOURLY/MINUTELY: the period's hour | ||
| 81 | int minute = -1; // MINUTELY: the period's minute | ||
| 82 | }; | ||
| 83 | |||
| 84 | class Expander { | ||
| 85 | public: | ||
| 86 | 1548 | Expander(const RecurrenceRule &rule, const QDateTime &start) | |
| 87 | 1548 | : m_rule(rule), m_start(start) { | |
| 88 | // BY lists are sets. Deduplicate only the interpretation; stored rules | ||
| 89 | // retain their lossless representation and original property order. | ||
| 90 | 1548 | for (auto *values : {&m_rule.bySecond, &m_rule.byMinute, &m_rule.byHour, &m_rule.byMonthDay, | |
| 91 |
2/2✓ Branch 23 → 12 taken 12384 times.
✓ Branch 23 → 24 taken 1548 times.
|
15480 | &m_rule.byYearDay, &m_rule.byWeekNo, &m_rule.byMonth, &m_rule.bySetPos}) { |
| 92 |
3/3✓ Branch 12 → 13 taken 12384 times.
✓ Branch 13 → 14 taken 12384 times.
✓ Branch 14 → 15 taken 12384 times.
|
12384 | std::sort(values->begin(), values->end()); |
| 93 |
5/5✓ Branch 15 → 16 taken 12384 times.
✓ Branch 17 → 18 taken 12384 times.
✓ Branch 18 → 19 taken 12384 times.
✓ Branch 19 → 20 taken 12384 times.
✓ Branch 21 → 22 taken 12384 times.
|
12384 | values->erase(std::unique(values->begin(), values->end()), values->end()); |
| 94 | } | ||
| 95 |
3/3✓ Branch 24 → 25 taken 1548 times.
✓ Branch 25 → 26 taken 1548 times.
✓ Branch 26 → 27 taken 1548 times.
|
1548 | std::sort(m_rule.byDay.begin(), m_rule.byDay.end(), [](const auto &a, const auto &b) { |
| 96 |
4/6✓ Branch 2 → 3 taken 1626 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 6 taken 1620 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 6 times.
|
1626 | return a.weekday < b.weekday || (a.weekday == b.weekday && a.ordinal < b.ordinal); |
| 97 | }); | ||
| 98 |
5/5✓ Branch 27 → 28 taken 1548 times.
✓ Branch 29 → 30 taken 1548 times.
✓ Branch 30 → 31 taken 1548 times.
✓ Branch 31 → 32 taken 1548 times.
✓ Branch 33 → 34 taken 1548 times.
|
1548 | m_rule.byDay.erase(std::unique(m_rule.byDay.begin(), m_rule.byDay.end()), m_rule.byDay.end()); |
| 99 |
1/1✓ Branch 34 → 35 taken 1548 times.
|
1548 | applyDefaults(); |
| 100 |
4/4✓ Branch 35 → 36 taken 1548 times.
✓ Branch 36 → 37 taken 1548 times.
✓ Branch 50 → 38 taken 1722 times.
✓ Branch 50 → 51 taken 1548 times.
|
3270 | for (const auto &entry : m_rule.byDay) { |
| 101 |
6/6✓ Branch 39 → 40 taken 366 times.
✓ Branch 39 → 42 taken 1356 times.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 41 → 43 taken 365 times.
✓ Branch 44 → 45 taken 1357 times.
✓ Branch 44 → 47 taken 365 times.
|
1722 | if (entry.ordinal == 0 || !ordinalsApply()) |
| 102 |
1/1✓ Branch 45 → 46 taken 1357 times.
|
1357 | m_plainWeekdays.insert(entry.weekday); |
| 103 | else | ||
| 104 |
1/1✓ Branch 47 → 48 taken 365 times.
|
365 | m_ordinalEntries.append(entry); |
| 105 | } | ||
| 106 |
1/1✓ Branch 51 → 52 taken 1548 times.
|
1548 | buildTimes(); |
| 107 | 1548 | } | |
| 108 | 1548 | bool timesLimited() const { return m_timesLimited; } | |
| 109 | |||
| 110 | // Period index of the first period worth visiting for `windowStart`. | ||
| 111 | 1546 | qint64 firstUsefulPeriod(const QDateTime &windowStart) const { | |
| 112 |
7/7✓ Branch 2 → 3 taken 800 times.
✓ Branch 2 → 6 taken 746 times.
✓ Branch 3 → 4 taken 800 times.
✓ Branch 5 → 6 taken 427 times.
✓ Branch 5 → 7 taken 373 times.
✓ Branch 8 → 9 taken 1173 times.
✓ Branch 8 → 10 taken 373 times.
|
1546 | if (m_rule.count > 0 || windowStart <= m_start) |
| 113 | 1173 | return 0; | |
| 114 | 373 | const int interval = qMax(1, m_rule.interval); | |
| 115 |
2/2✓ Branch 11 → 12 taken 373 times.
✓ Branch 12 → 13 taken 373 times.
|
373 | const QDate from = m_start.date(), to = windowStart.date(); |
| 116 | 373 | qint64 units = 0; | |
| 117 |
6/7✓ Branch 13 → 14 taken 22 times.
✓ Branch 13 → 17 taken 286 times.
✓ Branch 13 → 22 taken 55 times.
✓ Branch 13 → 24 taken 3 times.
✓ Branch 13 → 26 taken 1 time.
✓ Branch 13 → 28 taken 6 times.
✗ Branch 13 → 30 not taken.
|
373 | switch (m_rule.frequency) { |
| 118 |
2/2✓ Branch 14 → 15 taken 22 times.
✓ Branch 15 → 16 taken 22 times.
|
22 | case RecurrenceRule::Frequency::Yearly: units = qint64(to.year()) - from.year(); break; |
| 119 | 286 | case RecurrenceRule::Frequency::Monthly: | |
| 120 |
4/4✓ Branch 17 → 18 taken 286 times.
✓ Branch 18 → 19 taken 286 times.
✓ Branch 19 → 20 taken 286 times.
✓ Branch 20 → 21 taken 286 times.
|
286 | units = (qint64(to.year()) - from.year()) * 12 + (to.month() - from.month()); break; |
| 121 |
1/1✓ Branch 22 → 23 taken 55 times.
|
55 | case RecurrenceRule::Frequency::Weekly: units = from.daysTo(to) / 7; break; |
| 122 |
1/1✓ Branch 24 → 25 taken 3 times.
|
3 | case RecurrenceRule::Frequency::Daily: units = from.daysTo(to); break; |
| 123 |
1/1✓ Branch 26 → 27 taken 1 time.
|
1 | case RecurrenceRule::Frequency::Hourly: units = m_start.secsTo(windowStart) / 3600; break; |
| 124 |
1/1✓ Branch 28 → 29 taken 6 times.
|
6 | case RecurrenceRule::Frequency::Minutely: units = m_start.secsTo(windowStart) / 60; break; |
| 125 | ✗ | default: return 0; | |
| 126 | } | ||
| 127 | 373 | return qMax<qint64>(0, units / interval - 1); | |
| 128 | } | ||
| 129 | |||
| 130 | 108931 | Period period(qint64 index) const { | |
| 131 | 108931 | Period period; | |
| 132 | 108931 | const qint64 step = index * qMax(1, m_rule.interval); | |
| 133 |
6/7✓ Branch 4 → 5 taken 515 times.
✓ Branch 4 → 44 taken 2647 times.
✓ Branch 4 → 72 taken 8159 times.
✓ Branch 4 → 86 taken 38912 times.
✓ Branch 4 → 95 taken 3027 times.
✓ Branch 4 → 110 taken 55671 times.
✗ Branch 4 → 129 not taken.
|
108931 | switch (m_rule.frequency) { |
| 134 | 515 | case RecurrenceRule::Frequency::Yearly: { | |
| 135 |
2/2✓ Branch 5 → 6 taken 515 times.
✓ Branch 6 → 7 taken 515 times.
|
515 | const qint64 candidateYear = qint64(m_start.date().year()) + step; |
| 136 |
2/4✓ Branch 8 → 9 taken 515 times.
✗ Branch 8 → 11 not taken.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 515 times.
|
1030 | if (candidateYear <= std::numeric_limits<int>::min() || |
| 137 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 515 times.
|
515 | candidateYear >= std::numeric_limits<int>::max()) |
| 138 | ✗ | return period; | |
| 139 | 515 | const int year = int(candidateYear); | |
| 140 |
2/2✓ Branch 15 → 16 taken 515 times.
✓ Branch 16 → 17 taken 515 times.
|
515 | QDate first(year, 1, 1), last(year, 12, 31); |
| 141 |
2/2✓ Branch 18 → 19 taken 30 times.
✓ Branch 18 → 23 taken 485 times.
|
515 | if (!m_rule.byWeekNo.isEmpty()) { |
| 142 |
1/1✓ Branch 19 → 20 taken 30 times.
|
30 | first = firstWeekStart(year, m_rule.weekStart); |
| 143 |
2/2✓ Branch 20 → 21 taken 30 times.
✓ Branch 21 → 22 taken 30 times.
|
30 | last = firstWeekStart(year + 1, m_rule.weekStart).addDays(-1); |
| 144 | } | ||
| 145 |
5/8✓ Branch 23 → 24 taken 515 times.
✓ Branch 24 → 25 taken 515 times.
✗ Branch 24 → 27 not taken.
✓ Branch 25 → 26 taken 515 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 515 times.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 515 times.
|
515 | if (!first.isValid() || !last.isValid()) |
| 146 | ✗ | return period; | |
| 147 | // A calendar year has at most 366 days; a week-number year at most 371. | ||
| 148 |
1/1✓ Branch 31 → 32 taken 515 times.
|
515 | const int dayCount = int(qBound(qint64(0), first.daysTo(last) + 1, qint64(371))); |
| 149 |
2/2✓ Branch 37 → 34 taken 188104 times.
✓ Branch 37 → 38 taken 515 times.
|
188619 | for (int day = 0; day < dayCount; ++day) |
| 150 |
2/2✓ Branch 34 → 35 taken 188104 times.
✓ Branch 35 → 36 taken 188104 times.
|
188104 | period.days.append(first.addDays(day)); |
| 151 |
2/2✓ Branch 38 → 39 taken 515 times.
✓ Branch 39 → 40 taken 515 times.
|
515 | period.earliest = naive(first, QTime(0, 0)); |
| 152 | 515 | break; | |
| 153 | } | ||
| 154 | 2647 | case RecurrenceRule::Frequency::Monthly: { | |
| 155 |
4/4✓ Branch 44 → 45 taken 2647 times.
✓ Branch 45 → 46 taken 2647 times.
✓ Branch 46 → 47 taken 2647 times.
✓ Branch 47 → 48 taken 2647 times.
|
2647 | const qint64 monthIndex = qint64(m_start.date().year()) * 12 + (m_start.date().month() - 1) + step; |
| 156 | 2647 | const qint64 year = monthIndex / 12; | |
| 157 |
3/6✓ Branch 49 → 50 taken 2647 times.
✗ Branch 49 → 52 not taken.
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 53 taken 2647 times.
✗ Branch 54 → 55 not taken.
✓ Branch 54 → 56 taken 2647 times.
|
2647 | if (year <= std::numeric_limits<int>::min() || year >= std::numeric_limits<int>::max()) |
| 158 | ✗ | return period; | |
| 159 |
1/1✓ Branch 56 → 57 taken 2647 times.
|
2647 | const QDate first(int(year), int(monthIndex % 12) + 1, 1); |
| 160 |
2/3✓ Branch 57 → 58 taken 2647 times.
✗ Branch 58 → 59 not taken.
✓ Branch 58 → 60 taken 2647 times.
|
2647 | if (!first.isValid()) |
| 161 | ✗ | return period; | |
| 162 | // Use the month length even if addDays reaches QDate's representable end. | ||
| 163 |
3/3✓ Branch 64 → 65 taken 82937 times.
✓ Branch 65 → 61 taken 80290 times.
✓ Branch 65 → 66 taken 2647 times.
|
82937 | for (int day = 0; day < first.daysInMonth(); ++day) |
| 164 |
2/2✓ Branch 61 → 62 taken 80290 times.
✓ Branch 62 → 63 taken 80290 times.
|
80290 | period.days.append(first.addDays(day)); |
| 165 |
2/2✓ Branch 66 → 67 taken 2647 times.
✓ Branch 67 → 68 taken 2647 times.
|
2647 | period.earliest = naive(first, QTime(0, 0)); |
| 166 | 2647 | break; | |
| 167 | } | ||
| 168 | 8159 | case RecurrenceRule::Frequency::Weekly: { | |
| 169 |
3/3✓ Branch 72 → 73 taken 8159 times.
✓ Branch 73 → 74 taken 8159 times.
✓ Branch 74 → 75 taken 8159 times.
|
8159 | const QDate first = weekStartOf(m_start.date(), m_rule.weekStart).addDays(step * 7); |
| 170 |
2/2✓ Branch 79 → 76 taken 57113 times.
✓ Branch 79 → 80 taken 8159 times.
|
65272 | for (int i = 0; i < 7; ++i) |
| 171 |
2/2✓ Branch 76 → 77 taken 57113 times.
✓ Branch 77 → 78 taken 57113 times.
|
57113 | period.days.append(first.addDays(i)); |
| 172 |
2/2✓ Branch 80 → 81 taken 8159 times.
✓ Branch 81 → 82 taken 8159 times.
|
8159 | period.earliest = naive(first, QTime(0, 0)); |
| 173 | 8159 | break; | |
| 174 | } | ||
| 175 | 38912 | case RecurrenceRule::Frequency::Daily: { | |
| 176 |
2/2✓ Branch 86 → 87 taken 38912 times.
✓ Branch 87 → 88 taken 38912 times.
|
38912 | const QDate day = m_start.date().addDays(step); |
| 177 |
1/1✓ Branch 88 → 89 taken 38912 times.
|
38912 | period.days.append(day); |
| 178 |
2/2✓ Branch 89 → 90 taken 38912 times.
✓ Branch 90 → 91 taken 38912 times.
|
38912 | period.earliest = naive(day, QTime(0, 0)); |
| 179 | 38912 | break; | |
| 180 | } | ||
| 181 | 3027 | case RecurrenceRule::Frequency::Hourly: { | |
| 182 |
5/5✓ Branch 95 → 96 taken 3027 times.
✓ Branch 96 → 97 taken 3027 times.
✓ Branch 97 → 98 taken 3027 times.
✓ Branch 98 → 99 taken 3027 times.
✓ Branch 99 → 100 taken 3027 times.
|
3027 | const QDateTime base = naive(m_start.date(), QTime(m_start.time().hour(), 0)); |
| 183 |
1/1✓ Branch 100 → 101 taken 3027 times.
|
3027 | const QDateTime stamp = base.addSecs(step * 3600); |
| 184 |
2/2✓ Branch 101 → 102 taken 3027 times.
✓ Branch 102 → 103 taken 3027 times.
|
3027 | period.days.append(stamp.date()); |
| 185 |
2/2✓ Branch 103 → 104 taken 3027 times.
✓ Branch 104 → 105 taken 3027 times.
|
3027 | period.hour = stamp.time().hour(); |
| 186 | 3027 | period.earliest = stamp; | |
| 187 | 3027 | break; | |
| 188 | 3027 | } | |
| 189 | 55671 | case RecurrenceRule::Frequency::Minutely: { | |
| 190 | const QDateTime base = | ||
| 191 |
7/7✓ Branch 110 → 111 taken 55671 times.
✓ Branch 111 → 112 taken 55671 times.
✓ Branch 112 → 113 taken 55671 times.
✓ Branch 113 → 114 taken 55671 times.
✓ Branch 114 → 115 taken 55671 times.
✓ Branch 115 → 116 taken 55671 times.
✓ Branch 116 → 117 taken 55671 times.
|
55671 | naive(m_start.date(), QTime(m_start.time().hour(), m_start.time().minute())); |
| 192 |
1/1✓ Branch 117 → 118 taken 55671 times.
|
55671 | const QDateTime stamp = base.addSecs(step * 60); |
| 193 |
2/2✓ Branch 118 → 119 taken 55671 times.
✓ Branch 119 → 120 taken 55671 times.
|
55671 | period.days.append(stamp.date()); |
| 194 |
2/2✓ Branch 120 → 121 taken 55671 times.
✓ Branch 121 → 122 taken 55671 times.
|
55671 | period.hour = stamp.time().hour(); |
| 195 |
2/2✓ Branch 122 → 123 taken 55671 times.
✓ Branch 123 → 124 taken 55671 times.
|
55671 | period.minute = stamp.time().minute(); |
| 196 | 55671 | period.earliest = stamp; | |
| 197 | 55671 | break; | |
| 198 | 55671 | } | |
| 199 | ✗ | default: | |
| 200 | ✗ | break; | |
| 201 | } | ||
| 202 | 108931 | return period; | |
| 203 | ✗ | } | |
| 204 | |||
| 205 | 108486 | QList<QDateTime> candidates(const Period &period, qint64 *remaining, bool *limited) const { | |
| 206 |
1/1✓ Branch 2 → 3 taken 108486 times.
|
108486 | const QSet<QDate> ordinalDates = ordinalDatesFor(period); |
| 207 | 108486 | QList<QDateTime> result; | |
| 208 |
8/8✓ Branch 3 → 4 taken 58686 times.
✓ Branch 3 → 9 taken 49800 times.
✓ Branch 5 → 6 taken 387 times.
✓ Branch 5 → 9 taken 58299 times.
✓ Branch 7 → 8 taken 342 times.
✓ Branch 7 → 9 taken 45 times.
✓ Branch 10 → 11 taken 342 times.
✓ Branch 10 → 13 taken 108144 times.
|
108486 | if (period.hour >= 0 && !m_rule.byHour.isEmpty() && !m_rule.byHour.contains(period.hour)) |
| 209 | 342 | return result; | |
| 210 |
4/8✓ Branch 13 → 14 taken 55469 times.
✓ Branch 13 → 19 taken 52675 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 19 taken 55469 times.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 23 taken 108144 times.
|
108144 | if (period.minute >= 0 && !m_rule.byMinute.isEmpty() && !m_rule.byMinute.contains(period.minute)) |
| 211 | ✗ | return result; | |
| 212 |
1/1✓ Branch 23 → 24 taken 108144 times.
|
108144 | const auto times = timesFor(period); |
| 213 |
2/2✓ Branch 48 → 26 taken 404277 times.
✓ Branch 48 → 49 taken 108134 times.
|
512411 | for (const QDate &day : period.days) { |
| 214 |
2/2✓ Branch 27 → 28 taken 10 times.
✓ Branch 27 → 29 taken 404267 times.
|
404277 | if (--*remaining < 0) { *limited = true; return {}; } |
| 215 |
3/3✓ Branch 29 → 30 taken 404267 times.
✓ Branch 30 → 31 taken 288597 times.
✓ Branch 30 → 32 taken 115670 times.
|
404267 | if (!dayMatches(day, ordinalDates)) |
| 216 | 288597 | continue; | |
| 217 |
1/2✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 115670 times.
|
115670 | if (times.size() > *remaining) { *limited = true; return {}; } |
| 218 | 115670 | *remaining -= times.size(); | |
| 219 |
2/2✓ Branch 44 → 38 taken 115715 times.
✓ Branch 44 → 45 taken 115670 times.
|
231385 | for (const QTime &time : times) |
| 220 |
2/2✓ Branch 39 → 40 taken 115715 times.
✓ Branch 40 → 41 taken 115715 times.
|
115715 | result.append(naive(day, time)); |
| 221 | } | ||
| 222 |
3/3✓ Branch 49 → 51 taken 108134 times.
✓ Branch 51 → 52 taken 108134 times.
✓ Branch 52 → 53 taken 108134 times.
|
108134 | std::sort(result.begin(), result.end()); |
| 223 |
2/2✓ Branch 54 → 55 taken 107930 times.
✓ Branch 54 → 56 taken 204 times.
|
108134 | if (m_rule.bySetPos.isEmpty()) |
| 224 | 107930 | return result; | |
| 225 | 204 | QList<QDateTime> selected; | |
| 226 |
2/2✓ Branch 76 → 58 taken 216 times.
✓ Branch 76 → 77 taken 204 times.
|
420 | for (int position : m_rule.bySetPos) { |
| 227 |
2/2✓ Branch 59 → 60 taken 48 times.
✓ Branch 59 → 61 taken 168 times.
|
216 | const int index = position > 0 ? position - 1 : int(result.size()) + position; |
| 228 |
4/8✓ Branch 63 → 64 taken 216 times.
✗ Branch 63 → 70 not taken.
✓ Branch 65 → 66 taken 216 times.
✗ Branch 65 → 70 not taken.
✓ Branch 68 → 69 taken 216 times.
✗ Branch 68 → 70 not taken.
✓ Branch 71 → 72 taken 216 times.
✗ Branch 71 → 74 not taken.
|
216 | if (index >= 0 && index < result.size() && !selected.contains(result.at(index))) |
| 229 |
1/1✓ Branch 73 → 74 taken 216 times.
|
216 | selected.append(result.at(index)); |
| 230 | } | ||
| 231 |
3/3✓ Branch 77 → 78 taken 204 times.
✓ Branch 78 → 79 taken 204 times.
✓ Branch 79 → 80 taken 204 times.
|
204 | std::sort(selected.begin(), selected.end()); |
| 232 | 204 | return selected; | |
| 233 | 108486 | } | |
| 234 | |||
| 235 | private: | ||
| 236 | 366 | bool ordinalsApply() const { | |
| 237 |
2/2✓ Branch 2 → 3 taken 32 times.
✓ Branch 2 → 4 taken 334 times.
|
398 | return m_rule.frequency == RecurrenceRule::Frequency::Monthly || |
| 238 |
2/2✓ Branch 3 → 4 taken 31 times.
✓ Branch 3 → 5 taken 1 time.
|
398 | m_rule.frequency == RecurrenceRule::Frequency::Yearly; |
| 239 | } | ||
| 240 | |||
| 241 | // RFC 5545 §3.3.10 / python-dateutil: a rule without day-level BY parts | ||
| 242 | // repeats on DTSTART's day of the week/month/year. | ||
| 243 | 1548 | void applyDefaults() { | |
| 244 |
2/2✓ Branch 5 → 6 taken 1530 times.
✓ Branch 5 → 10 taken 9 times.
|
3087 | const bool dayParts = !m_rule.byWeekNo.isEmpty() || !m_rule.byYearDay.isEmpty() || |
| 245 |
6/6✓ Branch 3 → 4 taken 1539 times.
✓ Branch 3 → 10 taken 9 times.
✓ Branch 7 → 8 taken 1438 times.
✓ Branch 7 → 10 taken 92 times.
✓ Branch 9 → 10 taken 637 times.
✓ Branch 9 → 11 taken 801 times.
|
3087 | !m_rule.byMonthDay.isEmpty() || !m_rule.byDay.isEmpty(); |
| 246 |
2/2✓ Branch 12 → 13 taken 747 times.
✓ Branch 12 → 14 taken 801 times.
|
1548 | if (dayParts) |
| 247 | 747 | return; | |
| 248 |
1/1✓ Branch 14 → 15 taken 801 times.
|
801 | const QDate date = m_start.date(); |
| 249 |
4/4✓ Branch 15 → 16 taken 32 times.
✓ Branch 15 → 24 taken 49 times.
✓ Branch 15 → 27 taken 260 times.
✓ Branch 15 → 30 taken 460 times.
|
801 | switch (m_rule.frequency) { |
| 250 | 32 | case RecurrenceRule::Frequency::Yearly: | |
| 251 |
2/2✓ Branch 17 → 18 taken 29 times.
✓ Branch 17 → 21 taken 3 times.
|
32 | if (m_rule.byMonth.isEmpty()) |
| 252 |
2/2✓ Branch 18 → 19 taken 29 times.
✓ Branch 19 → 20 taken 29 times.
|
29 | m_rule.byMonth = {date.month()}; |
| 253 |
2/2✓ Branch 21 → 22 taken 32 times.
✓ Branch 22 → 23 taken 32 times.
|
32 | m_rule.byMonthDay = {date.day()}; |
| 254 | 32 | break; | |
| 255 | 49 | case RecurrenceRule::Frequency::Monthly: | |
| 256 |
2/2✓ Branch 24 → 25 taken 49 times.
✓ Branch 25 → 26 taken 49 times.
|
49 | m_rule.byMonthDay = {date.day()}; |
| 257 | 49 | break; | |
| 258 | 260 | case RecurrenceRule::Frequency::Weekly: | |
| 259 |
2/2✓ Branch 27 → 28 taken 260 times.
✓ Branch 28 → 29 taken 260 times.
|
260 | m_rule.byDay = {{0, date.dayOfWeek()}}; |
| 260 | 260 | break; | |
| 261 | 460 | default: | |
| 262 | 460 | break; | |
| 263 | } | ||
| 264 | } | ||
| 265 | |||
| 266 | 1548 | void buildTimes() { | |
| 267 |
1/1✓ Branch 2 → 3 taken 1548 times.
|
1548 | const QTime time = m_start.time(); |
| 268 |
4/4✓ Branch 4 → 5 taken 1510 times.
✓ Branch 4 → 7 taken 38 times.
✓ Branch 5 → 6 taken 1510 times.
✓ Branch 6 → 8 taken 1510 times.
|
1548 | const QList<int> hours = m_rule.byHour.isEmpty() ? QList<int>{time.hour()} : m_rule.byHour; |
| 269 |
4/4✓ Branch 9 → 10 taken 1520 times.
✓ Branch 9 → 12 taken 28 times.
✓ Branch 10 → 11 taken 1520 times.
✓ Branch 11 → 13 taken 1520 times.
|
1548 | const QList<int> minutes = m_rule.byMinute.isEmpty() ? QList<int>{time.minute()} : m_rule.byMinute; |
| 270 |
4/4✓ Branch 14 → 15 taken 1525 times.
✓ Branch 14 → 17 taken 23 times.
✓ Branch 15 → 16 taken 1525 times.
✓ Branch 16 → 18 taken 1525 times.
|
1548 | const QList<int> seconds = m_rule.bySecond.isEmpty() ? QList<int>{time.second()} : m_rule.bySecond; |
| 271 | // Check before constructing a potentially huge Cartesian product. | ||
| 272 |
3/4✓ Branch 20 → 21 taken 1548 times.
✗ Branch 20 → 25 not taken.
✓ Branch 27 → 28 taken 2 times.
✓ Branch 27 → 29 taken 1546 times.
|
3096 | if (hours.size() > kMaximumExpansionWork / minutes.size() || |
| 273 |
2/2✓ Branch 24 → 25 taken 2 times.
✓ Branch 24 → 26 taken 1546 times.
|
1548 | hours.size() * minutes.size() > kMaximumExpansionWork / seconds.size()) { |
| 274 | 2 | m_timesLimited = true; | |
| 275 | 2 | return; | |
| 276 | } | ||
| 277 |
2/2✓ Branch 49 → 31 taken 1567 times.
✓ Branch 49 → 50 taken 1546 times.
|
3113 | for (int hour : hours) |
| 278 |
2/2✓ Branch 46 → 34 taken 1573 times.
✓ Branch 46 → 47 taken 1567 times.
|
3140 | for (int minute : minutes) |
| 279 |
2/2✓ Branch 43 → 37 taken 1573 times.
✓ Branch 43 → 44 taken 1573 times.
|
3146 | for (int second : seconds) |
| 280 |
2/2✓ Branch 39 → 40 taken 1573 times.
✓ Branch 40 → 41 taken 1573 times.
|
1573 | m_dayTimes.append(QTime(hour, minute, qMin(second, 59))); |
| 281 |
3/3✓ Branch 50 → 51 taken 1546 times.
✓ Branch 51 → 52 taken 1546 times.
✓ Branch 52 → 53 taken 1546 times.
|
1546 | std::sort(m_dayTimes.begin(), m_dayTimes.end()); |
| 282 |
2/2✓ Branch 66 → 55 taken 1549 times.
✓ Branch 66 → 67 taken 1546 times.
|
3095 | for (int minute : minutes) |
| 283 |
2/2✓ Branch 63 → 58 taken 1549 times.
✓ Branch 63 → 64 taken 1549 times.
|
3098 | for (int second : seconds) |
| 284 |
1/1✓ Branch 60 → 61 taken 1549 times.
|
1549 | m_hourTimes.append({minute, second}); |
| 285 |
3/3✓ Branch 67 → 68 taken 1546 times.
✓ Branch 68 → 69 taken 1546 times.
✓ Branch 69 → 70 taken 1546 times.
|
1546 | std::sort(m_hourTimes.begin(), m_hourTimes.end()); |
| 286 |
2/2✓ Branch 76 → 72 taken 1546 times.
✓ Branch 76 → 77 taken 1546 times.
|
3092 | for (int second : seconds) |
| 287 |
1/1✓ Branch 73 → 74 taken 1546 times.
|
1546 | m_minuteSeconds.append(second); |
| 288 |
3/3✓ Branch 77 → 78 taken 1546 times.
✓ Branch 78 → 79 taken 1546 times.
✓ Branch 79 → 80 taken 1546 times.
|
1546 | std::sort(m_minuteSeconds.begin(), m_minuteSeconds.end()); |
| 289 | 1552 | } | |
| 290 | |||
| 291 | 108144 | QList<QTime> timesFor(const Period &period) const { | |
| 292 |
2/2✓ Branch 2 → 3 taken 55469 times.
✓ Branch 2 → 15 taken 52675 times.
|
108144 | if (period.minute >= 0) { |
| 293 | ✗ | QList<QTime> times; | |
| 294 |
2/2✓ Branch 11 → 5 taken 55469 times.
✓ Branch 11 → 12 taken 55469 times.
|
110938 | for (int second : m_minuteSeconds) |
| 295 |
2/2✓ Branch 7 → 8 taken 55469 times.
✓ Branch 8 → 9 taken 55469 times.
|
55469 | times.append(QTime(period.hour, period.minute, qMin(second, 59))); |
| 296 |
1/2✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 55469 times.
|
55469 | return times; |
| 297 | ✗ | } | |
| 298 |
2/2✓ Branch 15 → 16 taken 2875 times.
✓ Branch 15 → 29 taken 49800 times.
|
52675 | if (period.hour >= 0) { |
| 299 | 2875 | QList<QTime> times; | |
| 300 |
2/2✓ Branch 24 → 18 taken 2875 times.
✓ Branch 24 → 25 taken 2875 times.
|
5750 | for (const auto &pair : m_hourTimes) |
| 301 |
2/2✓ Branch 20 → 21 taken 2875 times.
✓ Branch 21 → 22 taken 2875 times.
|
2875 | times.append(QTime(period.hour, pair.first, qMin(pair.second, 59))); |
| 302 | 2875 | return times; | |
| 303 | 2875 | } | |
| 304 | 49800 | return m_dayTimes; | |
| 305 | } | ||
| 306 | |||
| 307 | 108486 | QSet<QDate> ordinalDatesFor(const Period &period) const { | |
| 308 | 108486 | QSet<QDate> dates; | |
| 309 |
5/6✓ Branch 4 → 5 taken 1406 times.
✓ Branch 4 → 7 taken 107080 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 1406 times.
✓ Branch 9 → 10 taken 107080 times.
✓ Branch 9 → 11 taken 1406 times.
|
108486 | if (m_ordinalEntries.isEmpty() || period.days.isEmpty()) |
| 310 | 107080 | return dates; | |
| 311 | 1406 | QList<std::pair<QDate, QDate>> spans; | |
| 312 |
2/2✓ Branch 11 → 12 taken 1230 times.
✓ Branch 11 → 20 taken 176 times.
|
1406 | if (m_rule.frequency == RecurrenceRule::Frequency::Monthly) { |
| 313 | 1230 | const QDate first = period.days.first(); | |
| 314 |
5/5✓ Branch 13 → 14 taken 1230 times.
✓ Branch 14 → 15 taken 1230 times.
✓ Branch 15 → 16 taken 1230 times.
✓ Branch 16 → 17 taken 1230 times.
✓ Branch 18 → 19 taken 1230 times.
|
1230 | spans.append({first, QDate(first.year(), first.month(), first.daysInMonth())}); |
| 315 |
2/2✓ Branch 21 → 22 taken 167 times.
✓ Branch 21 → 37 taken 9 times.
|
176 | } else if (!m_rule.byMonth.isEmpty()) { |
| 316 |
1/1✓ Branch 24 → 25 taken 167 times.
|
167 | const int year = period.days.at(period.days.size() / 2).year(); |
| 317 |
2/2✓ Branch 35 → 27 taken 167 times.
✓ Branch 35 → 36 taken 167 times.
|
334 | for (int month : m_rule.byMonth) { |
| 318 |
1/1✓ Branch 28 → 29 taken 167 times.
|
167 | const QDate first(year, month, 1); |
| 319 |
3/3✓ Branch 29 → 30 taken 167 times.
✓ Branch 30 → 31 taken 167 times.
✓ Branch 32 → 33 taken 167 times.
|
167 | spans.append({first, QDate(year, month, first.daysInMonth())}); |
| 320 | } | ||
| 321 | } else { | ||
| 322 |
1/1✓ Branch 39 → 40 taken 9 times.
|
9 | const int year = period.days.at(period.days.size() / 2).year(); |
| 323 |
3/3✓ Branch 40 → 41 taken 9 times.
✓ Branch 41 → 42 taken 9 times.
✓ Branch 43 → 44 taken 9 times.
|
9 | spans.append({QDate(year, 1, 1), QDate(year, 12, 31)}); |
| 324 | } | ||
| 325 |
4/4✓ Branch 45 → 46 taken 1406 times.
✓ Branch 46 → 47 taken 1406 times.
✓ Branch 62 → 48 taken 1406 times.
✓ Branch 62 → 63 taken 1406 times.
|
2812 | for (const auto &span : spans) |
| 326 |
2/2✓ Branch 59 → 51 taken 1424 times.
✓ Branch 59 → 60 taken 1406 times.
|
2830 | for (const auto &entry : m_ordinalEntries) { |
| 327 |
1/1✓ Branch 52 → 53 taken 1424 times.
|
1424 | const QDate date = nthWeekday(span.first, span.second, entry.weekday, entry.ordinal); |
| 328 |
2/3✓ Branch 53 → 54 taken 1424 times.
✓ Branch 54 → 55 taken 1424 times.
✗ Branch 54 → 57 not taken.
|
1424 | if (date.isValid()) |
| 329 |
1/1✓ Branch 55 → 56 taken 1424 times.
|
1424 | dates.insert(date); |
| 330 | } | ||
| 331 | 1406 | return dates; | |
| 332 | 1406 | } | |
| 333 | |||
| 334 | 404267 | bool dayMatches(const QDate &day, const QSet<QDate> &ordinalDates) const { | |
| 335 |
7/7✓ Branch 3 → 4 taken 153405 times.
✓ Branch 3 → 8 taken 250862 times.
✓ Branch 4 → 5 taken 153405 times.
✓ Branch 6 → 7 taken 139510 times.
✓ Branch 6 → 8 taken 13895 times.
✓ Branch 9 → 10 taken 139510 times.
✓ Branch 9 → 11 taken 264757 times.
|
404267 | if (!m_rule.byMonth.isEmpty() && !m_rule.byMonth.contains(day.month())) |
| 336 | 139510 | return false; | |
| 337 |
6/6✓ Branch 12 → 13 taken 10941 times.
✓ Branch 12 → 16 taken 253816 times.
✓ Branch 14 → 15 taken 10731 times.
✓ Branch 14 → 16 taken 210 times.
✓ Branch 17 → 18 taken 10731 times.
✓ Branch 17 → 19 taken 254026 times.
|
264757 | if (!m_rule.byWeekNo.isEmpty() && !weekNumberMatches(day)) |
| 338 | 10731 | return false; | |
| 339 |
2/2✓ Branch 20 → 21 taken 9864 times.
✓ Branch 20 → 32 taken 244162 times.
|
254026 | if (!m_rule.byYearDay.isEmpty()) { |
| 340 |
1/1✓ Branch 21 → 22 taken 9864 times.
|
9864 | const int forward = day.dayOfYear(); |
| 341 |
1/1✓ Branch 22 → 23 taken 9864 times.
|
9864 | const int backward = forward - day.daysInYear() - 1; |
| 342 |
6/6✓ Branch 24 → 25 taken 9810 times.
✓ Branch 24 → 28 taken 54 times.
✓ Branch 26 → 27 taken 9801 times.
✓ Branch 26 → 28 taken 9 times.
✓ Branch 29 → 30 taken 9801 times.
✓ Branch 29 → 31 taken 63 times.
|
9864 | if (!m_rule.byYearDay.contains(forward) && !m_rule.byYearDay.contains(backward)) |
| 343 | 9801 | return false; | |
| 344 | } | ||
| 345 |
2/2✓ Branch 33 → 34 taken 40146 times.
✓ Branch 33 → 45 taken 204079 times.
|
244225 | if (!m_rule.byMonthDay.isEmpty()) { |
| 346 |
1/1✓ Branch 34 → 35 taken 40146 times.
|
40146 | const int forward = day.day(); |
| 347 |
1/1✓ Branch 35 → 36 taken 40146 times.
|
40146 | const int backward = forward - day.daysInMonth() - 1; |
| 348 |
6/6✓ Branch 37 → 38 taken 39184 times.
✓ Branch 37 → 41 taken 962 times.
✓ Branch 39 → 40 taken 38981 times.
✓ Branch 39 → 41 taken 203 times.
✓ Branch 42 → 43 taken 38981 times.
✓ Branch 42 → 44 taken 1165 times.
|
40146 | if (!m_rule.byMonthDay.contains(forward) && !m_rule.byMonthDay.contains(backward)) |
| 349 | 38981 | return false; | |
| 350 | } | ||
| 351 |
2/2✓ Branch 46 → 47 taken 107675 times.
✓ Branch 46 → 56 taken 97569 times.
|
205244 | if (!m_rule.byDay.isEmpty()) { |
| 352 |
7/7✓ Branch 47 → 48 taken 107675 times.
✓ Branch 49 → 50 taken 90998 times.
✓ Branch 49 → 53 taken 16677 times.
✓ Branch 51 → 52 taken 89574 times.
✓ Branch 51 → 53 taken 1424 times.
✓ Branch 54 → 55 taken 89574 times.
✓ Branch 54 → 56 taken 18101 times.
|
107675 | if (!m_plainWeekdays.contains(day.dayOfWeek()) && !ordinalDates.contains(day)) |
| 353 | 89574 | return false; | |
| 354 | } | ||
| 355 | 115670 | return true; | |
| 356 | } | ||
| 357 | |||
| 358 | 10941 | bool weekNumberMatches(const QDate &day) const { | |
| 359 | // The week year of `day`: the year whose week-1 start is the latest one | ||
| 360 | // not after `day`. | ||
| 361 |
1/1✓ Branch 2 → 3 taken 10941 times.
|
10941 | int year = day.year() + 1; |
| 362 |
3/3✓ Branch 5 → 6 taken 21894 times.
✓ Branch 8 → 4 taken 10953 times.
✓ Branch 8 → 9 taken 10941 times.
|
21894 | while (firstWeekStart(year, m_rule.weekStart) > day) |
| 363 | 10953 | --year; | |
| 364 |
1/1✓ Branch 9 → 10 taken 10941 times.
|
10941 | const QDate week1 = firstWeekStart(year, m_rule.weekStart); |
| 365 |
2/2✓ Branch 10 → 11 taken 10941 times.
✓ Branch 11 → 12 taken 10941 times.
|
10941 | const int weeks = int(week1.daysTo(firstWeekStart(year + 1, m_rule.weekStart)) / 7); |
| 366 |
1/1✓ Branch 12 → 13 taken 10941 times.
|
10941 | const int number = int(week1.daysTo(day) / 7) + 1; |
| 367 |
3/4✓ Branch 14 → 15 taken 10731 times.
✓ Branch 14 → 17 taken 210 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 10731 times.
|
10941 | return m_rule.byWeekNo.contains(number) || m_rule.byWeekNo.contains(number - weeks - 1); |
| 368 | } | ||
| 369 | |||
| 370 | RecurrenceRule m_rule; | ||
| 371 | QDateTime m_start; | ||
| 372 | QSet<int> m_plainWeekdays; | ||
| 373 | QList<RecurrenceRule::WeekdayNum> m_ordinalEntries; | ||
| 374 | QList<QTime> m_dayTimes; | ||
| 375 | QList<std::pair<int, int>> m_hourTimes; | ||
| 376 | QList<int> m_minuteSeconds; | ||
| 377 | bool m_timesLimited = false; | ||
| 378 | }; | ||
| 379 | |||
| 380 | // "d MMMM" / "MMMM d" / "d. MMMM" derived from the locale's long date | ||
| 381 | // format without weekday and year. | ||
| 382 | 2 | QString dayMonthFormat() { | |
| 383 |
2/2✓ Branch 2 → 3 taken 2 times.
✓ Branch 3 → 4 taken 2 times.
|
2 | QString format = QLocale().dateFormat(QLocale::LongFormat); |
| 384 |
2/2✓ Branch 9 → 10 taken 2 times.
✓ Branch 10 → 11 taken 2 times.
|
2 | format.remove(QRegularExpression(QStringLiteral("d{4}[,.]?\\s*"))); |
| 385 |
2/2✓ Branch 18 → 19 taken 2 times.
✓ Branch 19 → 20 taken 2 times.
|
2 | format.remove(QRegularExpression(QStringLiteral("[,\\s]*y+[.\\s]*"))); |
| 386 |
1/1✓ Branch 23 → 24 taken 2 times.
|
2 | format = format.trimmed(); |
| 387 |
2/3✓ Branch 30 → 31 taken 2 times.
✗ Branch 31 → 27 not taken.
✓ Branch 31 → 32 taken 2 times.
|
2 | while (format.endsWith(QLatin1Char(','))) |
| 388 | ✗ | format.chop(1); | |
| 389 |
2/4✗ Branch 33 → 34 not taken.
✓ Branch 33 → 37 taken 2 times.
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 2 times.
|
4 | return format.isEmpty() ? QStringLiteral("d MMMM") : format; |
| 390 | 2 | } | |
| 391 | |||
| 392 | 80 | QString weekdayName(int weekday) { | |
| 393 |
2/2✓ Branch 2 → 3 taken 80 times.
✓ Branch 3 → 4 taken 80 times.
|
80 | return QLocale().dayName(weekday, QLocale::LongFormat); |
| 394 | } | ||
| 395 | |||
| 396 | 48 | QString joinNames(const QStringList &names) { | |
| 397 |
2/2✓ Branch 3 → 4 taken 39 times.
✓ Branch 3 → 5 taken 9 times.
|
48 | if (names.size() <= 1) |
| 398 | 39 | return names.value(0); | |
| 399 |
3/3✓ Branch 5 → 6 taken 9 times.
✓ Branch 8 → 9 taken 9 times.
✓ Branch 12 → 13 taken 9 times.
|
36 | return RecurrenceText::tr("%1 and %2").arg(names.mid(0, names.size() - 1).join(QStringLiteral(", ")), |
| 400 |
1/1✓ Branch 13 → 14 taken 9 times.
|
18 | names.last()); |
| 401 | } | ||
| 402 | |||
| 403 | 9 | QString ordinalWord(int ordinal) { | |
| 404 |
3/8✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
✗ Branch 2 → 5 not taken.
✗ Branch 2 → 6 not taken.
✓ Branch 2 → 7 taken 3 times.
✓ Branch 2 → 8 taken 4 times.
✗ Branch 2 → 9 not taken.
✗ Branch 2 → 10 not taken.
|
9 | switch (ordinal) { |
| 405 | ✗ | case 1: return RecurrenceText::tr("first"); | |
| 406 | 2 | case 2: return RecurrenceText::tr("second"); | |
| 407 | ✗ | case 3: return RecurrenceText::tr("third"); | |
| 408 | ✗ | case 4: return RecurrenceText::tr("fourth"); | |
| 409 | 3 | case 5: return RecurrenceText::tr("fifth"); | |
| 410 | 4 | case -1: return RecurrenceText::tr("last"); | |
| 411 | ✗ | case -2: return RecurrenceText::tr("second to last"); | |
| 412 | ✗ | default: return QString::number(ordinal) + QLatin1Char('.'); | |
| 413 | } | ||
| 414 | } | ||
| 415 | |||
| 416 | 72 | bool isWorkweek(const QList<RecurrenceRule::WeekdayNum> &days) { | |
| 417 |
2/2✓ Branch 3 → 4 taken 69 times.
✓ Branch 3 → 5 taken 3 times.
|
72 | if (days.size() != 5) |
| 418 | 69 | return false; | |
| 419 | 3 | QSet<int> set; | |
| 420 |
2/2✓ Branch 14 → 8 taken 15 times.
✓ Branch 14 → 15 taken 3 times.
|
18 | for (const auto &day : days) { |
| 421 |
1/2✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 15 times.
|
15 | if (day.ordinal != 0) |
| 422 | ✗ | return false; | |
| 423 |
1/1✓ Branch 11 → 12 taken 15 times.
|
15 | set.insert(day.weekday); |
| 424 | } | ||
| 425 |
1/1✓ Branch 15 → 16 taken 3 times.
|
3 | return set == QSet<int>{1, 2, 3, 4, 5}; |
| 426 | 3 | } | |
| 427 | |||
| 428 | } // namespace | ||
| 429 | |||
| 430 | // ─── RecurrenceRule ──────────────────────────────────────── | ||
| 431 | |||
| 432 | 10208 | QString RecurrenceRule::frequencyName(Frequency frequency) { | |
| 433 |
7/8✓ Branch 2 → 3 taken 2004 times.
✓ Branch 2 → 8 taken 2002 times.
✓ Branch 2 → 13 taken 1960 times.
✓ Branch 2 → 18 taken 1964 times.
✓ Branch 2 → 23 taken 1410 times.
✓ Branch 2 → 28 taken 725 times.
✓ Branch 2 → 33 taken 143 times.
✗ Branch 2 → 38 not taken.
|
10208 | switch (frequency) { |
| 434 | 4008 | case Frequency::Secondly: return QStringLiteral("SECONDLY"); | |
| 435 | 4004 | case Frequency::Minutely: return QStringLiteral("MINUTELY"); | |
| 436 | 3920 | case Frequency::Hourly: return QStringLiteral("HOURLY"); | |
| 437 | 3928 | case Frequency::Daily: return QStringLiteral("DAILY"); | |
| 438 | 2820 | case Frequency::Weekly: return QStringLiteral("WEEKLY"); | |
| 439 | 1450 | case Frequency::Monthly: return QStringLiteral("MONTHLY"); | |
| 440 | 286 | case Frequency::Yearly: return QStringLiteral("YEARLY"); | |
| 441 | ✗ | default: return {}; | |
| 442 | } | ||
| 443 | } | ||
| 444 | |||
| 445 | 2004 | RecurrenceRule::Frequency RecurrenceRule::frequencyFromName(const QString &name) { | |
| 446 |
2/2✓ Branch 2 → 3 taken 2004 times.
✓ Branch 3 → 4 taken 2004 times.
|
2004 | const QString upper = name.trimmed().toUpper(); |
| 447 | 10114 | for (Frequency frequency : {Frequency::Secondly, Frequency::Minutely, Frequency::Hourly, | |
| 448 | Frequency::Daily, Frequency::Weekly, Frequency::Monthly, | ||
| 449 |
2/2✓ Branch 14 → 8 taken 10112 times.
✓ Branch 14 → 15 taken 2 times.
|
12118 | Frequency::Yearly}) |
| 450 |
3/3✓ Branch 8 → 9 taken 10112 times.
✓ Branch 11 → 12 taken 2002 times.
✓ Branch 11 → 13 taken 8110 times.
|
10112 | if (frequencyName(frequency) == upper) |
| 451 | 2002 | return frequency; | |
| 452 | 2 | return Frequency::None; | |
| 453 | 2004 | } | |
| 454 | |||
| 455 | 3455 | QString RecurrenceRule::weekdayToken(int isoWeekday) { | |
| 456 |
2/4✓ Branch 2 → 3 taken 3455 times.
✗ Branch 2 → 7 not taken.
✓ Branch 3 → 4 taken 3455 times.
✗ Branch 3 → 7 not taken.
|
3455 | return isoWeekday >= 1 && isoWeekday <= 7 ? QString::fromLatin1(kWeekdayTokens[isoWeekday - 1]) |
| 457 |
1/1✓ Branch 5 → 6 taken 3455 times.
|
6910 | : QString(); |
| 458 | } | ||
| 459 | |||
| 460 | 2118 | int RecurrenceRule::weekdayFromToken(const QString &token) { | |
| 461 |
2/2✓ Branch 2 → 3 taken 2118 times.
✓ Branch 3 → 4 taken 2118 times.
|
2118 | const QString upper = token.trimmed().toUpper(); |
| 462 |
2/2✓ Branch 11 → 6 taken 8763 times.
✓ Branch 11 → 12 taken 2 times.
|
8765 | for (int i = 0; i < 7; ++i) |
| 463 |
2/2✓ Branch 8 → 9 taken 2116 times.
✓ Branch 8 → 10 taken 6647 times.
|
8763 | if (upper == QLatin1String(kWeekdayTokens[i])) |
| 464 | 2116 | return i + 1; | |
| 465 | 2 | return 0; | |
| 466 | 2118 | } | |
| 467 | |||
| 468 | 2005 | RecurrenceRule RecurrenceRule::parse(const QString &text) { | |
| 469 | 2005 | RecurrenceRule rule; | |
| 470 |
1/1✓ Branch 2 → 3 taken 2005 times.
|
2005 | QString body = text.trimmed(); |
| 471 |
3/3✓ Branch 6 → 7 taken 2005 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 14 taken 2004 times.
|
2005 | if (body.startsWith(QStringLiteral("RRULE:"), Qt::CaseInsensitive)) |
| 472 |
1/1✓ Branch 10 → 11 taken 1 time.
|
1 | body = body.mid(6); |
| 473 | 18 | const auto fail = [&rule](const QString &message) { | |
| 474 | 18 | rule.valid = false; | |
| 475 | 18 | rule.error = message; | |
| 476 | 18 | return rule; | |
| 477 | 2005 | }; | |
| 478 |
5/5✓ Branch 17 → 18 taken 2005 times.
✓ Branch 18 → 19 taken 2005 times.
✓ Branch 19 → 20 taken 2005 times.
✓ Branch 228 → 21 taken 4337 times.
✓ Branch 228 → 229 taken 1989 times.
|
6326 | for (const QString &part : body.split(QLatin1Char(';'), Qt::SkipEmptyParts)) { |
| 479 | 4337 | const int eq = part.indexOf(QLatin1Char('=')); | |
| 480 |
2/2✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 36 taken 4336 times.
|
4337 | if (eq <= 0) |
| 481 |
1/1✓ Branch 30 → 31 taken 1 time.
|
2 | return fail(QStringLiteral("Malformed rule part '%1'").arg(part)); |
| 482 |
3/3✓ Branch 36 → 37 taken 4336 times.
✓ Branch 37 → 38 taken 4336 times.
✓ Branch 38 → 39 taken 4336 times.
|
4336 | const QString key = part.left(eq).trimmed().toUpper(); |
| 483 |
2/2✓ Branch 41 → 42 taken 4336 times.
✓ Branch 42 → 43 taken 4336 times.
|
4336 | const QString value = part.mid(eq + 1).trimmed(); |
| 484 | 4336 | bool ok = true; | |
| 485 |
2/2✓ Branch 46 → 47 taken 2004 times.
✓ Branch 46 → 49 taken 2332 times.
|
4336 | if (key == QLatin1String("FREQ")) { |
| 486 |
1/1✓ Branch 47 → 48 taken 2004 times.
|
2004 | rule.frequency = frequencyFromName(value); |
| 487 | 2004 | ok = rule.frequency != Frequency::None; | |
| 488 |
2/2✓ Branch 51 → 52 taken 161 times.
✓ Branch 51 → 58 taken 2171 times.
|
2332 | } else if (key == QLatin1String("INTERVAL")) { |
| 489 |
1/1✓ Branch 52 → 53 taken 161 times.
|
161 | rule.interval = value.toInt(&ok); |
| 490 |
3/4✓ Branch 53 → 54 taken 161 times.
✗ Branch 53 → 56 not taken.
✓ Branch 54 → 55 taken 160 times.
✓ Branch 54 → 56 taken 1 time.
|
161 | ok = ok && rule.interval >= 1; |
| 491 |
2/2✓ Branch 60 → 61 taken 811 times.
✓ Branch 60 → 67 taken 1360 times.
|
2171 | } else if (key == QLatin1String("COUNT")) { |
| 492 |
1/1✓ Branch 61 → 62 taken 811 times.
|
811 | rule.count = value.toInt(&ok); |
| 493 |
2/4✓ Branch 62 → 63 taken 811 times.
✗ Branch 62 → 65 not taken.
✓ Branch 63 → 64 taken 811 times.
✗ Branch 63 → 65 not taken.
|
811 | ok = ok && rule.count >= 1; |
| 494 |
2/2✓ Branch 69 → 70 taken 152 times.
✓ Branch 69 → 107 taken 1208 times.
|
1360 | } else if (key == QLatin1String("UNTIL")) { |
| 495 |
1/1✓ Branch 70 → 71 taken 152 times.
|
152 | rule.until = value.toUpper(); |
| 496 |
2/2✓ Branch 76 → 77 taken 152 times.
✓ Branch 79 → 80 taken 152 times.
|
152 | const QString digits = QString(rule.until).remove(QLatin1Char('T')).remove(QLatin1Char('Z')); |
| 497 |
3/4✓ Branch 85 → 86 taken 134 times.
✓ Branch 85 → 88 taken 1 time.
✓ Branch 87 → 88 taken 134 times.
✗ Branch 87 → 95 not taken.
|
287 | ok = (rule.until.size() == 8 || rule.until.size() == 15 || rule.until.size() == 16) && |
| 498 |
5/7✓ Branch 83 → 84 taken 135 times.
✓ Branch 83 → 88 taken 17 times.
✓ Branch 89 → 90 taken 152 times.
✗ Branch 89 → 95 not taken.
✓ Branch 92 → 93 taken 152 times.
✓ Branch 93 → 94 taken 152 times.
✗ Branch 93 → 95 not taken.
|
2313 | !digits.isEmpty() && std::all_of(digits.begin(), digits.end(), [](QChar c) { return c.isDigit(); }); |
| 499 | // S106-134: a well-shaped but impossible date (20261340) is no | ||
| 500 | // bound; it used to read as "no UNTIL" and the series ran forever. | ||
| 501 |
7/11✓ Branch 96 → 97 taken 152 times.
✗ Branch 96 → 102 not taken.
✓ Branch 97 → 98 taken 152 times.
✓ Branch 98 → 99 taken 152 times.
✓ Branch 99 → 100 taken 152 times.
✓ Branch 100 → 101 taken 143 times.
✓ Branch 100 → 102 taken 9 times.
✓ Branch 103 → 104 taken 152 times.
✗ Branch 103 → 105 not taken.
✗ Branch 295 → 296 not taken.
✗ Branch 295 → 297 not taken.
|
152 | ok = ok && rule.untilNaive(QTime(0, 0)).isValid(); |
| 502 |
2/2✓ Branch 109 → 110 taken 40 times.
✓ Branch 109 → 112 taken 1168 times.
|
1360 | } else if (key == QLatin1String("BYSECOND")) { |
| 503 |
1/1✓ Branch 110 → 111 taken 40 times.
|
40 | ok = parseIntList(value, 0, 60, false, &rule.bySecond); |
| 504 |
2/2✓ Branch 114 → 115 taken 47 times.
✓ Branch 114 → 117 taken 1121 times.
|
1168 | } else if (key == QLatin1String("BYMINUTE")) { |
| 505 |
1/1✓ Branch 115 → 116 taken 47 times.
|
47 | ok = parseIntList(value, 0, 59, false, &rule.byMinute); |
| 506 |
2/2✓ Branch 119 → 120 taken 55 times.
✓ Branch 119 → 122 taken 1066 times.
|
1121 | } else if (key == QLatin1String("BYHOUR")) { |
| 507 |
1/1✓ Branch 120 → 121 taken 55 times.
|
55 | ok = parseIntList(value, 0, 23, false, &rule.byHour); |
| 508 |
2/2✓ Branch 124 → 125 taken 770 times.
✓ Branch 124 → 169 taken 296 times.
|
1066 | } else if (key == QLatin1String("BYDAY")) { |
| 509 |
5/5✓ Branch 128 → 129 taken 770 times.
✓ Branch 129 → 130 taken 770 times.
✓ Branch 130 → 131 taken 770 times.
✓ Branch 166 → 132 taken 1715 times.
✓ Branch 166 → 167 taken 768 times.
|
2483 | for (const QString &token : value.split(QLatin1Char(','), Qt::SkipEmptyParts)) { |
| 510 |
2/2✓ Branch 133 → 134 taken 1715 times.
✓ Branch 134 → 135 taken 1715 times.
|
1715 | const QString trimmed = token.trimmed().toUpper(); |
| 511 | 1715 | WeekdayNum entry; | |
| 512 |
2/2✓ Branch 136 → 137 taken 1715 times.
✓ Branch 137 → 138 taken 1715 times.
|
1715 | entry.weekday = weekdayFromToken(trimmed.right(2)); |
| 513 |
1/1✓ Branch 140 → 141 taken 1715 times.
|
1715 | const QString prefix = trimmed.left(trimmed.size() - 2); |
| 514 |
2/2✓ Branch 142 → 143 taken 400 times.
✓ Branch 142 → 151 taken 1315 times.
|
1715 | if (!prefix.isEmpty()) { |
| 515 |
1/1✓ Branch 143 → 144 taken 400 times.
|
400 | entry.ordinal = prefix.toInt(&ok); |
| 516 |
3/6✓ Branch 144 → 145 taken 400 times.
✗ Branch 144 → 149 not taken.
✓ Branch 145 → 146 taken 400 times.
✗ Branch 145 → 149 not taken.
✓ Branch 147 → 148 taken 400 times.
✗ Branch 147 → 149 not taken.
|
400 | ok = ok && entry.ordinal != 0 && qAbs(entry.ordinal) <= 53; |
| 517 | } | ||
| 518 |
3/4✓ Branch 151 → 152 taken 1715 times.
✗ Branch 151 → 153 not taken.
✓ Branch 152 → 153 taken 2 times.
✓ Branch 152 → 154 taken 1713 times.
|
1715 | if (!ok || entry.weekday == 0) { |
| 519 | 2 | ok = false; | |
| 520 | 2 | break; | |
| 521 | } | ||
| 522 |
1/1✓ Branch 154 → 155 taken 1713 times.
|
1713 | rule.byDay.append(entry); |
| 523 | 2487 | } | |
| 524 |
2/2✓ Branch 171 → 172 taken 135 times.
✓ Branch 171 → 174 taken 161 times.
|
296 | } else if (key == QLatin1String("BYMONTHDAY")) { |
| 525 |
1/1✓ Branch 172 → 173 taken 135 times.
|
135 | ok = parseIntList(value, 1, 31, true, &rule.byMonthDay); |
| 526 |
2/2✓ Branch 176 → 177 taken 9 times.
✓ Branch 176 → 179 taken 152 times.
|
161 | } else if (key == QLatin1String("BYYEARDAY")) { |
| 527 |
1/1✓ Branch 177 → 178 taken 9 times.
|
9 | ok = parseIntList(value, 1, 366, true, &rule.byYearDay); |
| 528 |
2/2✓ Branch 181 → 182 taken 12 times.
✓ Branch 181 → 184 taken 140 times.
|
152 | } else if (key == QLatin1String("BYWEEKNO")) { |
| 529 |
1/1✓ Branch 182 → 183 taken 12 times.
|
12 | ok = parseIntList(value, 1, 53, true, &rule.byWeekNo); |
| 530 |
2/2✓ Branch 186 → 187 taken 76 times.
✓ Branch 186 → 189 taken 64 times.
|
140 | } else if (key == QLatin1String("BYMONTH")) { |
| 531 |
1/1✓ Branch 187 → 188 taken 76 times.
|
76 | ok = parseIntList(value, 1, 12, false, &rule.byMonth); |
| 532 |
2/2✓ Branch 191 → 192 taken 35 times.
✓ Branch 191 → 194 taken 29 times.
|
64 | } else if (key == QLatin1String("BYSETPOS")) { |
| 533 |
1/1✓ Branch 192 → 193 taken 35 times.
|
35 | ok = parseIntList(value, 1, 366, true, &rule.bySetPos); |
| 534 |
2/2✓ Branch 196 → 197 taken 23 times.
✓ Branch 196 → 199 taken 6 times.
|
29 | } else if (key == QLatin1String("WKST")) { |
| 535 |
1/1✓ Branch 197 → 198 taken 23 times.
|
23 | rule.weekStart = weekdayFromToken(value); |
| 536 | 23 | rule.weekStartGiven = true; | |
| 537 | 23 | ok = rule.weekStart != 0; | |
| 538 | } else { | ||
| 539 |
3/3✓ Branch 201 → 202 taken 6 times.
✓ Branch 202 → 203 taken 6 times.
✓ Branch 203 → 204 taken 6 times.
|
6 | rule.unknownParts.append(key + QLatin1Char('=') + value); |
| 540 | } | ||
| 541 |
2/2✓ Branch 207 → 208 taken 15 times.
✓ Branch 207 → 217 taken 4321 times.
|
4336 | if (!ok) |
| 542 |
1/1✓ Branch 211 → 212 taken 15 times.
|
15 | return fail(QStringLiteral("Invalid %1 value '%2'").arg(key, value)); |
| 543 | 6356 | } | |
| 544 |
2/2✓ Branch 233 → 235 taken 1 time.
✓ Branch 233 → 242 taken 1988 times.
|
1989 | if (rule.frequency == Frequency::None) |
| 545 | 1 | return fail(QStringLiteral("Missing FREQ")); | |
| 546 |
6/6✓ Branch 242 → 243 taken 811 times.
✓ Branch 242 → 246 taken 1177 times.
✓ Branch 244 → 245 taken 1 time.
✓ Branch 244 → 246 taken 810 times.
✓ Branch 247 → 248 taken 1 time.
✓ Branch 247 → 255 taken 1987 times.
|
1988 | if (rule.count > 0 && rule.hasUntil()) |
| 547 | 1 | return fail(QStringLiteral("COUNT and UNTIL are mutually exclusive")); | |
| 548 | 1987 | rule.valid = true; | |
| 549 | 1987 | return rule; | |
| 550 | 2005 | } | |
| 551 | |||
| 552 | 42 | QString RecurrenceRule::toString() const { | |
| 553 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 42 times.
|
42 | if (frequency == Frequency::None) |
| 554 | ✗ | return {}; | |
| 555 |
2/4✓ Branch 11 → 12 taken 42 times.
✓ Branch 11 → 13 taken 42 times.
✗ Branch 205 → 206 not taken.
✗ Branch 205 → 207 not taken.
|
126 | QStringList parts{QStringLiteral("FREQ=") + frequencyName(frequency)}; |
| 556 |
2/2✓ Branch 16 → 17 taken 7 times.
✓ Branch 16 → 27 taken 35 times.
|
42 | if (interval > 1) |
| 557 |
2/2✓ Branch 21 → 22 taken 7 times.
✓ Branch 22 → 23 taken 7 times.
|
14 | parts.append(QStringLiteral("INTERVAL=%1").arg(interval)); |
| 558 |
2/2✓ Branch 27 → 28 taken 7 times.
✓ Branch 27 → 38 taken 35 times.
|
42 | if (count > 0) |
| 559 |
2/2✓ Branch 32 → 33 taken 7 times.
✓ Branch 33 → 34 taken 7 times.
|
14 | parts.append(QStringLiteral("COUNT=%1").arg(count)); |
| 560 |
2/2✓ Branch 39 → 40 taken 12 times.
✓ Branch 39 → 49 taken 30 times.
|
42 | if (hasUntil()) |
| 561 |
2/2✓ Branch 43 → 44 taken 12 times.
✓ Branch 44 → 45 taken 12 times.
|
24 | parts.append(QStringLiteral("UNTIL=") + until); |
| 562 |
5/5✓ Branch 50 → 51 taken 9 times.
✓ Branch 50 → 62 taken 33 times.
✓ Branch 51 → 52 taken 9 times.
✓ Branch 55 → 56 taken 9 times.
✓ Branch 56 → 57 taken 9 times.
|
51 | if (!bySecond.isEmpty()) parts.append(QStringLiteral("BYSECOND=") + joinInts(bySecond)); |
| 563 |
5/5✓ Branch 63 → 64 taken 9 times.
✓ Branch 63 → 75 taken 33 times.
✓ Branch 64 → 65 taken 9 times.
✓ Branch 68 → 69 taken 9 times.
✓ Branch 69 → 70 taken 9 times.
|
51 | if (!byMinute.isEmpty()) parts.append(QStringLiteral("BYMINUTE=") + joinInts(byMinute)); |
| 564 |
5/5✓ Branch 76 → 77 taken 9 times.
✓ Branch 76 → 88 taken 33 times.
✓ Branch 77 → 78 taken 9 times.
✓ Branch 81 → 82 taken 9 times.
✓ Branch 82 → 83 taken 9 times.
|
51 | if (!byHour.isEmpty()) parts.append(QStringLiteral("BYHOUR=") + joinInts(byHour)); |
| 565 |
2/2✓ Branch 89 → 90 taken 14 times.
✓ Branch 89 → 119 taken 28 times.
|
42 | if (!byDay.isEmpty()) { |
| 566 | 14 | QStringList days; | |
| 567 |
2/2✓ Branch 104 → 92 taken 32 times.
✓ Branch 104 → 105 taken 14 times.
|
46 | for (const auto &entry : byDay) |
| 568 |
4/4✓ Branch 94 → 95 taken 6 times.
✓ Branch 94 → 96 taken 26 times.
✓ Branch 95 → 97 taken 6 times.
✓ Branch 98 → 99 taken 32 times.
|
64 | days.append((entry.ordinal ? QString::number(entry.ordinal) : QString()) + |
| 569 |
2/2✓ Branch 93 → 94 taken 32 times.
✓ Branch 97 → 98 taken 32 times.
|
96 | weekdayToken(entry.weekday)); |
| 570 |
3/3✓ Branch 107 → 108 taken 14 times.
✓ Branch 111 → 112 taken 14 times.
✓ Branch 112 → 113 taken 14 times.
|
28 | parts.append(QStringLiteral("BYDAY=") + days.join(QLatin1Char(','))); |
| 571 | 14 | } | |
| 572 |
5/5✓ Branch 120 → 121 taken 3 times.
✓ Branch 120 → 132 taken 39 times.
✓ Branch 121 → 122 taken 3 times.
✓ Branch 125 → 126 taken 3 times.
✓ Branch 126 → 127 taken 3 times.
|
45 | if (!byMonthDay.isEmpty()) parts.append(QStringLiteral("BYMONTHDAY=") + joinInts(byMonthDay)); |
| 573 |
1/5✗ Branch 133 → 134 not taken.
✓ Branch 133 → 145 taken 42 times.
✗ Branch 134 → 135 not taken.
✗ Branch 138 → 139 not taken.
✗ Branch 139 → 140 not taken.
|
42 | if (!byYearDay.isEmpty()) parts.append(QStringLiteral("BYYEARDAY=") + joinInts(byYearDay)); |
| 574 |
5/5✓ Branch 146 → 147 taken 1 time.
✓ Branch 146 → 158 taken 41 times.
✓ Branch 147 → 148 taken 1 time.
✓ Branch 151 → 152 taken 1 time.
✓ Branch 152 → 153 taken 1 time.
|
43 | if (!byWeekNo.isEmpty()) parts.append(QStringLiteral("BYWEEKNO=") + joinInts(byWeekNo)); |
| 575 |
5/5✓ Branch 159 → 160 taken 3 times.
✓ Branch 159 → 171 taken 39 times.
✓ Branch 160 → 161 taken 3 times.
✓ Branch 164 → 165 taken 3 times.
✓ Branch 165 → 166 taken 3 times.
|
45 | if (!byMonth.isEmpty()) parts.append(QStringLiteral("BYMONTH=") + joinInts(byMonth)); |
| 576 |
1/5✗ Branch 172 → 173 not taken.
✓ Branch 172 → 184 taken 42 times.
✗ Branch 173 → 174 not taken.
✗ Branch 177 → 178 not taken.
✗ Branch 178 → 179 not taken.
|
42 | if (!bySetPos.isEmpty()) parts.append(QStringLiteral("BYSETPOS=") + joinInts(bySetPos)); |
| 577 |
5/5✓ Branch 184 → 185 taken 3 times.
✓ Branch 184 → 196 taken 39 times.
✓ Branch 185 → 186 taken 3 times.
✓ Branch 189 → 190 taken 3 times.
✓ Branch 190 → 191 taken 3 times.
|
45 | if (weekStartGiven) parts.append(QStringLiteral("WKST=") + weekdayToken(weekStart)); |
| 578 |
1/1✓ Branch 196 → 197 taken 42 times.
|
42 | parts.append(unknownParts); |
| 579 |
1/1✓ Branch 199 → 200 taken 42 times.
|
42 | return parts.join(QLatin1Char(';')); |
| 580 | 126 | } | |
| 581 | |||
| 582 | 1875 | bool RecurrenceRule::fullySupported() const { | |
| 583 |
6/6✓ Branch 2 → 3 taken 1863 times.
✓ Branch 2 → 5 taken 12 times.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 6 taken 1860 times.
✓ Branch 7 → 8 taken 15 times.
✓ Branch 7 → 9 taken 1860 times.
|
1875 | if (!valid || !unknownParts.isEmpty()) |
| 584 | 15 | return false; | |
| 585 |
3/4✓ Branch 9 → 10 taken 1856 times.
✓ Branch 9 → 11 taken 4 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1856 times.
|
1860 | if (frequency == Frequency::Secondly || frequency == Frequency::None) |
| 586 | 4 | return false; | |
| 587 | // Sub-period expansion below HOURLY/MINUTELY granularity is not implemented. | ||
| 588 |
4/8✓ Branch 12 → 13 taken 21 times.
✓ Branch 12 → 18 taken 1835 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 18 taken 21 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 1856 times.
|
1856 | if (frequency == Frequency::Hourly && !bySecond.isEmpty() && bySecond.size() > 1) |
| 589 | ✗ | return false; | |
| 590 |
4/8✓ Branch 21 → 22 taken 34 times.
✓ Branch 21 → 27 taken 1822 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 27 taken 34 times.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 27 not taken.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 1856 times.
|
1856 | if (frequency == Frequency::Minutely && !bySecond.isEmpty() && bySecond.size() > 1) |
| 591 | ✗ | return false; | |
| 592 | // S106-135: combinations RFC 5545 §3.3.10 forbids expand to nothing; as | ||
| 593 | // unsupported they show DTSTART with the partial marker instead of an | ||
| 594 | // empty series. BYSETPOS needs another BYxxx part; a numeric BYDAY is not | ||
| 595 | // allowed in a YEARLY rule with BYWEEKNO. | ||
| 596 |
5/8✓ Branch 33 → 34 taken 29 times.
✗ Branch 33 → 49 not taken.
✓ Branch 35 → 36 taken 29 times.
✗ Branch 35 → 49 not taken.
✓ Branch 37 → 38 taken 29 times.
✗ Branch 37 → 49 not taken.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 39 → 49 taken 28 times.
|
1914 | if (!bySetPos.isEmpty() && bySecond.isEmpty() && byMinute.isEmpty() && byHour.isEmpty() && |
| 597 |
8/12✓ Branch 31 → 32 taken 29 times.
✓ Branch 31 → 49 taken 1827 times.
✓ Branch 41 → 42 taken 1 time.
✗ Branch 41 → 49 not taken.
✓ Branch 43 → 44 taken 1 time.
✗ Branch 43 → 49 not taken.
✓ Branch 45 → 46 taken 1 time.
✗ Branch 45 → 49 not taken.
✓ Branch 47 → 48 taken 1 time.
✗ Branch 47 → 49 not taken.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 52 taken 1855 times.
|
1915 | byDay.isEmpty() && byMonthDay.isEmpty() && byYearDay.isEmpty() && byWeekNo.isEmpty() && |
| 598 | 1 | byMonth.isEmpty()) | |
| 599 | 1 | return false; | |
| 600 |
6/6✓ Branch 52 → 53 taken 117 times.
✓ Branch 52 → 60 taken 1738 times.
✓ Branch 54 → 55 taken 11 times.
✓ Branch 54 → 60 taken 106 times.
✓ Branch 61 → 62 taken 1 time.
✓ Branch 61 → 63 taken 1854 times.
|
1866 | if (frequency == Frequency::Yearly && !byWeekNo.isEmpty() && |
| 601 |
2/2✓ Branch 58 → 59 taken 1 time.
✓ Branch 58 → 60 taken 10 times.
|
22 | std::any_of(byDay.cbegin(), byDay.cend(), [](const WeekdayNum &day) { return day.ordinal != 0; })) |
| 602 | 1 | return false; | |
| 603 | 1854 | return true; | |
| 604 | } | ||
| 605 | |||
| 606 | 292 | QDateTime RecurrenceRule::untilNaive(const QTime &dateOnlyTime) const { | |
| 607 |
2/2✓ Branch 3 → 4 taken 16 times.
✓ Branch 3 → 5 taken 276 times.
|
292 | if (!hasUntil()) |
| 608 | 16 | return {}; | |
| 609 | 276 | QDate date; // stays invalid when the text is no date | |
| 610 |
2/2✓ Branch 6 → 7 taken 29 times.
✓ Branch 6 → 11 taken 247 times.
|
276 | if (untilIsDate()) { |
| 611 |
1/1✓ Branch 8 → 9 taken 29 times.
|
29 | ICalTime::parseBasicDate(until, &date); |
| 612 |
1/1✓ Branch 9 → 10 taken 29 times.
|
29 | return naive(date, dateOnlyTime); |
| 613 | } | ||
| 614 | 247 | QStringView raw = until; | |
| 615 |
2/2✓ Branch 15 → 16 taken 245 times.
✓ Branch 15 → 17 taken 2 times.
|
247 | if (raw.endsWith(QLatin1Char('Z'))) |
| 616 | 245 | raw.chop(1); | |
| 617 | 247 | QTime time; | |
| 618 |
4/4✓ Branch 17 → 18 taken 247 times.
✓ Branch 18 → 19 taken 241 times.
✓ Branch 18 → 20 taken 6 times.
✓ Branch 19 → 21 taken 241 times.
|
247 | return ICalTime::parseBasicDateTime(raw, &date, &time) ? naive(date, time) : QDateTime(); |
| 619 | } | ||
| 620 | |||
| 621 | // ─── Expansion ───────────────────────────────────────────── | ||
| 622 | |||
| 623 | 1572 | NaiveExpansion expandNaive(const RecurrenceRule &rule, const QDateTime &start, | |
| 624 | const QDateTime &windowStart, const QDateTime &windowEnd, | ||
| 625 | const QDateTime &until, qsizetype maxStarts, const QDateTime &countFrom) { | ||
| 626 | 1572 | NaiveExpansion out; | |
| 627 | 1572 | qsizetype counted = 0; | |
| 628 | // DTSTART and the display window are required; an invalid date never advances. | ||
| 629 |
11/14✓ Branch 2 → 3 taken 1572 times.
✓ Branch 3 → 4 taken 1554 times.
✓ Branch 3 → 11 taken 18 times.
✓ Branch 4 → 5 taken 1554 times.
✓ Branch 5 → 6 taken 1554 times.
✗ Branch 5 → 11 not taken.
✓ Branch 6 → 7 taken 1554 times.
✓ Branch 7 → 8 taken 1554 times.
✗ Branch 7 → 11 not taken.
✓ Branch 8 → 9 taken 1554 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1554 times.
✓ Branch 13 → 14 taken 18 times.
✓ Branch 13 → 15 taken 1554 times.
|
1572 | if (!start.isValid() || !windowStart.isValid() || !windowEnd.isValid() || windowEnd < windowStart) |
| 630 | 18 | return out; | |
| 631 | 111226 | const auto keep = [&](const QDateTime &stamp) { | |
| 632 |
5/6✓ Branch 4 → 5 taken 105222 times.
✓ Branch 4 → 8 taken 6004 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 105222 times.
✓ Branch 10 → 11 taken 6004 times.
✓ Branch 10 → 12 taken 105222 times.
|
111226 | if (stamp < windowStart || stamp > windowEnd) |
| 633 | 6004 | return; | |
| 634 |
6/6✓ Branch 12 → 13 taken 22944 times.
✓ Branch 12 → 19 taken 82278 times.
✓ Branch 14 → 15 taken 22933 times.
✓ Branch 14 → 18 taken 11 times.
✓ Branch 17 → 18 taken 21648 times.
✓ Branch 17 → 19 taken 1285 times.
|
105222 | const bool counts = maxStarts > 0 && (!countFrom.isValid() || stamp >= countFrom); |
| 635 |
4/4✓ Branch 20 → 21 taken 21659 times.
✓ Branch 20 → 23 taken 83563 times.
✓ Branch 21 → 22 taken 18 times.
✓ Branch 21 → 23 taken 21641 times.
|
105222 | if (counts && counted >= maxStarts) { |
| 636 | 18 | out.limited = true; | |
| 637 | 18 | return; | |
| 638 | } | ||
| 639 |
2/2✓ Branch 23 → 24 taken 21641 times.
✓ Branch 23 → 25 taken 83563 times.
|
105204 | counted += counts ? 1 : 0; |
| 640 | 105204 | out.starts.append(stamp); | |
| 641 | 1554 | }; | |
| 642 |
1/1✓ Branch 15 → 16 taken 1554 times.
|
1554 | keep(start); |
| 643 |
3/3✓ Branch 16 → 17 taken 1554 times.
✓ Branch 17 → 18 taken 6 times.
✓ Branch 17 → 19 taken 1548 times.
|
1554 | if (!rule.fullySupported()) { |
| 644 | 6 | out.partial = true; | |
| 645 | 6 | return out; | |
| 646 | } | ||
| 647 | 1548 | int emitted = 1; | |
| 648 |
3/4✓ Branch 19 → 20 taken 747 times.
✓ Branch 19 → 22 taken 801 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 747 times.
|
1548 | if (rule.count > 0 && emitted >= rule.count) |
| 649 | ✗ | return out; | |
| 650 |
1/1✓ Branch 22 → 23 taken 1548 times.
|
1548 | const Expander expander(rule, start); |
| 651 |
2/2✓ Branch 24 → 25 taken 2 times.
✓ Branch 24 → 26 taken 1546 times.
|
1548 | if (expander.timesLimited()) { out.truncated = true; return out; } |
| 652 | 1546 | qint64 remainingWork = kMaximumExpansionWork; | |
| 653 | // A safety net against rules that can never match inside the window | ||
| 654 | // (e.g. BYMONTH=2;BYMONTHDAY=30): the window end stops them anyway, the | ||
| 655 | // cap only guards against pathological windows. | ||
| 656 | 1546 | constexpr qint64 kMaxPeriods = 400000; | |
| 657 | 1546 | qint64 visited = 0; | |
| 658 |
1/1✓ Branch 26 → 27 taken 1546 times.
|
1546 | for (qint64 index = expander.firstUsefulPeriod(windowStart);; ++index) { |
| 659 |
1/2✗ Branch 28 → 29 not taken.
✓ Branch 28 → 45 taken 108931 times.
|
108931 | if (++visited > kMaxPeriods) { |
| 660 | ✗ | out.truncated = true; | |
| 661 | ✗ | qCWarning(lcRecurrence) << "RRULE expansion stopped after" << kMaxPeriods << "periods:" | |
| 662 | ✗ | << rule.toString(); | |
| 663 | 1546 | return out; | |
| 664 | } | ||
| 665 |
1/1✓ Branch 45 → 46 taken 108931 times.
|
108931 | const Period period = expander.period(index); |
| 666 |
7/8✓ Branch 46 → 47 taken 108931 times.
✓ Branch 47 → 48 taken 108931 times.
✗ Branch 47 → 51 not taken.
✓ Branch 48 → 49 taken 108931 times.
✓ Branch 50 → 51 taken 402 times.
✓ Branch 50 → 52 taken 108529 times.
✓ Branch 53 → 54 taken 402 times.
✓ Branch 53 → 55 taken 108529 times.
|
108931 | if (!period.earliest.isValid() || period.earliest > windowEnd) |
| 667 | 402 | return out; | |
| 668 |
8/8✓ Branch 55 → 56 taken 108529 times.
✓ Branch 56 → 57 taken 1099 times.
✓ Branch 56 → 61 taken 107430 times.
✓ Branch 57 → 58 taken 1099 times.
✓ Branch 59 → 60 taken 43 times.
✓ Branch 59 → 61 taken 1056 times.
✓ Branch 62 → 63 taken 43 times.
✓ Branch 62 → 64 taken 108486 times.
|
108529 | if (until.isValid() && period.earliest > until) |
| 669 | 43 | return out; | |
| 670 | 108486 | bool limited = false; | |
| 671 |
1/1✓ Branch 64 → 65 taken 108486 times.
|
108486 | const auto candidates = expander.candidates(period, &remainingWork, &limited); |
| 672 |
2/2✓ Branch 65 → 66 taken 10 times.
✓ Branch 65 → 67 taken 108476 times.
|
108486 | if (limited) { out.truncated = true; return out; } |
| 673 |
2/2✓ Branch 95 → 69 taken 111451 times.
✓ Branch 95 → 96 taken 107385 times.
|
218836 | for (const QDateTime &candidate : candidates) { |
| 674 |
3/3✓ Branch 70 → 71 taken 111451 times.
✓ Branch 72 → 73 taken 1279 times.
✓ Branch 72 → 74 taken 110172 times.
|
111451 | if (candidate <= start) |
| 675 | 1279 | continue; | |
| 676 |
8/8✓ Branch 74 → 75 taken 110172 times.
✓ Branch 75 → 76 taken 1031 times.
✓ Branch 75 → 80 taken 109141 times.
✓ Branch 76 → 77 taken 1031 times.
✓ Branch 78 → 79 taken 30 times.
✓ Branch 78 → 80 taken 1001 times.
✓ Branch 81 → 82 taken 30 times.
✓ Branch 81 → 83 taken 110142 times.
|
110172 | if (until.isValid() && candidate > until) |
| 677 | 1091 | return out; | |
| 678 |
3/3✓ Branch 83 → 84 taken 110142 times.
✓ Branch 85 → 86 taken 470 times.
✓ Branch 85 → 87 taken 109672 times.
|
110142 | if (candidate > windowEnd) |
| 679 | 470 | return out; | |
| 680 | 109672 | ++emitted; | |
| 681 |
1/1✓ Branch 87 → 88 taken 109672 times.
|
109672 | keep(candidate); |
| 682 |
2/2✓ Branch 88 → 89 taken 18 times.
✓ Branch 88 → 90 taken 109654 times.
|
109672 | if (out.limited) |
| 683 | 18 | return out; | |
| 684 |
4/4✓ Branch 90 → 91 taken 35258 times.
✓ Branch 90 → 93 taken 74396 times.
✓ Branch 91 → 92 taken 573 times.
✓ Branch 91 → 93 taken 34685 times.
|
109654 | if (rule.count > 0 && emitted >= rule.count) |
| 685 | 573 | return out; | |
| 686 | } | ||
| 687 | 217417 | } | |
| 688 | 1548 | } | |
| 689 | |||
| 690 | // ─── Description ─────────────────────────────────────────── | ||
| 691 | |||
| 692 | 91 | QString describeRecurrence(const RecurrenceRule &rule, const QDateTime &start) { | |
| 693 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 91 times.
|
91 | if (!rule.valid) |
| 694 | ✗ | return {}; | |
| 695 | 91 | const int n = qMax(1, rule.interval); | |
| 696 |
1/1✓ Branch 5 → 6 taken 91 times.
|
91 | const QDate date = start.date(); |
| 697 | 91 | QString text; | |
| 698 | 91 | QStringList weekdays; | |
| 699 |
2/2✓ Branch 16 → 8 taken 53 times.
✓ Branch 16 → 17 taken 91 times.
|
144 | for (const auto &entry : rule.byDay) |
| 700 |
2/2✓ Branch 9 → 10 taken 46 times.
✓ Branch 9 → 14 taken 7 times.
|
53 | if (entry.ordinal == 0) |
| 701 |
2/2✓ Branch 10 → 11 taken 46 times.
✓ Branch 11 → 12 taken 46 times.
|
46 | weekdays.append(weekdayName(entry.weekday)); |
| 702 |
4/4✓ Branch 18 → 19 taken 20 times.
✓ Branch 18 → 22 taken 71 times.
✓ Branch 20 → 21 taken 7 times.
✓ Branch 20 → 22 taken 13 times.
|
91 | const bool singleOrdinal = rule.byDay.size() == 1 && rule.byDay.first().ordinal != 0; |
| 703 |
2/4✓ Branch 26 → 27 taken 90 times.
✗ Branch 26 → 36 not taken.
✓ Branch 28 → 29 taken 90 times.
✗ Branch 28 → 36 not taken.
|
271 | const bool simpleDays = rule.byWeekNo.isEmpty() && rule.byYearDay.isEmpty() && |
| 704 |
2/4✓ Branch 30 → 31 taken 90 times.
✗ Branch 30 → 36 not taken.
✓ Branch 32 → 33 taken 90 times.
✗ Branch 32 → 36 not taken.
|
270 | rule.byHour.isEmpty() && rule.byMinute.isEmpty() && |
| 705 |
4/4✓ Branch 24 → 25 taken 90 times.
✓ Branch 24 → 36 taken 1 time.
✓ Branch 34 → 35 taken 89 times.
✓ Branch 34 → 36 taken 1 time.
|
271 | rule.bySecond.isEmpty() && rule.unknownParts.isEmpty(); |
| 706 |
5/7✓ Branch 37 → 38 taken 21 times.
✓ Branch 37 → 66 taken 49 times.
✓ Branch 37 → 114 taken 15 times.
✓ Branch 37 → 226 taken 4 times.
✗ Branch 37 → 317 not taken.
✓ Branch 37 → 333 taken 2 times.
✗ Branch 37 → 347 not taken.
|
91 | switch (rule.frequency) { |
| 707 | 21 | case RecurrenceRule::Frequency::Daily: | |
| 708 |
3/9✓ Branch 38 → 39 taken 21 times.
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 44 taken 21 times.
✗ Branch 40 → 41 not taken.
✗ Branch 40 → 44 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 50 taken 21 times.
|
21 | if (isWorkweek(rule.byDay) && n == 1 && rule.byMonth.isEmpty()) |
| 709 | ✗ | text = RecurrenceText::tr("Every weekday"); | |
| 710 |
4/8✓ Branch 51 → 52 taken 21 times.
✗ Branch 51 → 57 not taken.
✓ Branch 53 → 54 taken 21 times.
✗ Branch 53 → 57 not taken.
✓ Branch 55 → 56 taken 21 times.
✗ Branch 55 → 57 not taken.
✓ Branch 58 → 59 taken 21 times.
✗ Branch 58 → 65 not taken.
|
21 | else if (rule.byDay.isEmpty() && rule.byMonth.isEmpty() && rule.byMonthDay.isEmpty()) |
| 711 |
4/4✓ Branch 59 → 60 taken 20 times.
✓ Branch 59 → 61 taken 1 time.
✓ Branch 60 → 62 taken 20 times.
✓ Branch 61 → 62 taken 1 time.
|
21 | text = n == 1 ? RecurrenceText::tr("Daily") : RecurrenceText::tr("Every %n days", nullptr, n); |
| 712 | 21 | break; | |
| 713 | 49 | case RecurrenceRule::Frequency::Weekly: { | |
| 714 |
4/8✓ Branch 67 → 68 taken 49 times.
✗ Branch 67 → 72 not taken.
✓ Branch 69 → 70 taken 49 times.
✗ Branch 69 → 72 not taken.
✗ Branch 71 → 72 not taken.
✓ Branch 71 → 73 taken 49 times.
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 76 taken 49 times.
|
49 | if (!rule.byMonth.isEmpty() || !rule.byMonthDay.isEmpty() || !rule.bySetPos.isEmpty()) |
| 715 | ✗ | break; | |
| 716 |
2/2✓ Branch 77 → 78 taken 27 times.
✓ Branch 77 → 83 taken 22 times.
|
49 | if (weekdays.isEmpty()) |
| 717 |
3/3✓ Branch 78 → 79 taken 27 times.
✓ Branch 79 → 80 taken 27 times.
✓ Branch 80 → 81 taken 27 times.
|
27 | weekdays.append(weekdayName(date.dayOfWeek())); |
| 718 |
4/6✓ Branch 85 → 86 taken 27 times.
✓ Branch 85 → 89 taken 22 times.
✗ Branch 87 → 88 not taken.
✓ Branch 87 → 89 taken 27 times.
✗ Branch 90 → 91 not taken.
✓ Branch 90 → 92 taken 49 times.
|
49 | if (weekdays.size() != rule.byDay.size() && !rule.byDay.isEmpty()) |
| 719 | ✗ | break; | |
| 720 |
6/7✓ Branch 92 → 93 taken 49 times.
✓ Branch 93 → 94 taken 1 time.
✓ Branch 93 → 96 taken 48 times.
✓ Branch 94 → 95 taken 1 time.
✗ Branch 94 → 96 not taken.
✓ Branch 97 → 98 taken 1 time.
✓ Branch 97 → 102 taken 48 times.
|
49 | if (isWorkweek(rule.byDay) && n == 1) |
| 721 |
1/1✓ Branch 98 → 99 taken 1 time.
|
1 | text = RecurrenceText::tr("Every weekday"); |
| 722 | else | ||
| 723 |
6/6✓ Branch 102 → 103 taken 45 times.
✓ Branch 102 → 104 taken 3 times.
✓ Branch 103 → 105 taken 45 times.
✓ Branch 104 → 105 taken 3 times.
✓ Branch 106 → 107 taken 48 times.
✓ Branch 107 → 108 taken 48 times.
|
96 | text = (n == 1 ? RecurrenceText::tr("Weekly on %1") : RecurrenceText::tr("Every %n weeks on %1", nullptr, n)).arg(joinNames(weekdays)); |
| 724 | 49 | break; | |
| 725 | } | ||
| 726 | 15 | case RecurrenceRule::Frequency::Monthly: { | |
| 727 |
1/2✗ Branch 115 → 116 not taken.
✓ Branch 115 → 117 taken 15 times.
|
15 | if (!rule.byMonth.isEmpty()) |
| 728 | ✗ | break; | |
| 729 | 15 | QString day; | |
| 730 |
5/6✓ Branch 118 → 119 taken 7 times.
✓ Branch 118 → 122 taken 8 times.
✓ Branch 120 → 121 taken 7 times.
✗ Branch 120 → 122 not taken.
✓ Branch 123 → 124 taken 7 times.
✓ Branch 123 → 173 taken 8 times.
|
15 | if (rule.byDay.isEmpty() && rule.bySetPos.isEmpty()) { |
| 731 |
1/4✗ Branch 125 → 126 not taken.
✓ Branch 125 → 128 taken 7 times.
✗ Branch 126 → 127 not taken.
✗ Branch 127 → 129 not taken.
|
7 | QList<int> days = rule.byMonthDay.isEmpty() ? QList<int>{date.day()} : rule.byMonthDay; |
| 732 |
4/4✓ Branch 129 → 130 taken 7 times.
✓ Branch 130 → 131 taken 7 times.
✓ Branch 132 → 133 taken 2 times.
✓ Branch 132 → 137 taken 5 times.
|
7 | if (days == QList<int>{-1}) { |
| 733 |
1/1✓ Branch 133 → 134 taken 2 times.
|
2 | day = RecurrenceText::tr("on the last day"); |
| 734 |
4/5✓ Branch 137 → 138 taken 5 times.
✓ Branch 138 → 139 taken 5 times.
✓ Branch 139 → 140 taken 5 times.
✓ Branch 140 → 141 taken 5 times.
✗ Branch 140 → 171 not taken.
|
10 | } else if (std::all_of(days.begin(), days.end(), [](int value) { return value > 0; })) { |
| 735 | 5 | QStringList numbers; | |
| 736 |
4/4✓ Branch 141 → 142 taken 5 times.
✓ Branch 142 → 143 taken 5 times.
✓ Branch 150 → 144 taken 5 times.
✓ Branch 150 → 151 taken 5 times.
|
10 | for (int value : days) |
| 737 |
2/2✓ Branch 145 → 146 taken 5 times.
✓ Branch 146 → 147 taken 5 times.
|
5 | numbers.append(QString::number(value)); |
| 738 |
4/8✓ Branch 152 → 153 taken 5 times.
✗ Branch 152 → 157 not taken.
✓ Branch 153 → 154 taken 5 times.
✓ Branch 155 → 156 taken 5 times.
✓ Branch 156 → 161 taken 5 times.
✗ Branch 160 → 161 not taken.
✗ Branch 433 → 434 not taken.
✗ Branch 433 → 435 not taken.
|
15 | day = days.size() == 1 ? RecurrenceText::tr("on day %1").arg(numbers.first()) |
| 739 |
3/12✗ Branch 157 → 158 not taken.
✗ Branch 159 → 160 not taken.
✗ Branch 163 → 164 not taken.
✓ Branch 163 → 165 taken 5 times.
✗ Branch 165 → 166 not taken.
✓ Branch 165 → 167 taken 5 times.
✓ Branch 167 → 168 taken 5 times.
✗ Branch 167 → 169 not taken.
✗ Branch 423 → 424 not taken.
✗ Branch 423 → 425 not taken.
✗ Branch 428 → 429 not taken.
✗ Branch 428 → 430 not taken.
|
10 | : RecurrenceText::tr("on days %1").arg(joinNames(numbers)); |
| 740 | 5 | } | |
| 741 |
6/8✓ Branch 173 → 174 taken 6 times.
✓ Branch 173 → 179 taken 2 times.
✓ Branch 175 → 176 taken 6 times.
✗ Branch 175 → 179 not taken.
✓ Branch 177 → 178 taken 6 times.
✗ Branch 177 → 179 not taken.
✓ Branch 180 → 181 taken 6 times.
✓ Branch 180 → 193 taken 2 times.
|
15 | } else if (singleOrdinal && rule.byMonthDay.isEmpty() && rule.bySetPos.isEmpty()) { |
| 742 |
3/3✓ Branch 181 → 182 taken 6 times.
✓ Branch 185 → 186 taken 6 times.
✓ Branch 186 → 187 taken 6 times.
|
24 | day = RecurrenceText::tr("on the %1 %2").arg(ordinalWord(rule.byDay.first().ordinal), |
| 743 |
1/1✓ Branch 183 → 184 taken 6 times.
|
18 | weekdayName(rule.byDay.first().weekday)); |
| 744 |
5/9✓ Branch 193 → 194 taken 2 times.
✓ Branch 194 → 195 taken 2 times.
✗ Branch 194 → 200 not taken.
✓ Branch 196 → 197 taken 2 times.
✗ Branch 196 → 200 not taken.
✓ Branch 198 → 199 taken 2 times.
✗ Branch 198 → 200 not taken.
✓ Branch 201 → 202 taken 2 times.
✗ Branch 201 → 212 not taken.
|
2 | } else if (isWorkweek(rule.byDay) && rule.bySetPos.size() == 1 && rule.byMonthDay.isEmpty()) { |
| 745 |
3/3✓ Branch 202 → 203 taken 2 times.
✓ Branch 205 → 206 taken 2 times.
✓ Branch 206 → 207 taken 2 times.
|
4 | day = RecurrenceText::tr("on the %1 weekday").arg(ordinalWord(rule.bySetPos.first())); |
| 746 | } | ||
| 747 |
1/2✓ Branch 213 → 214 taken 15 times.
✗ Branch 213 → 223 not taken.
|
15 | if (!day.isEmpty()) |
| 748 |
5/5✓ Branch 214 → 215 taken 11 times.
✓ Branch 214 → 216 taken 4 times.
✓ Branch 215 → 217 taken 11 times.
✓ Branch 216 → 217 taken 4 times.
✓ Branch 218 → 219 taken 15 times.
|
30 | text = (n == 1 ? RecurrenceText::tr("Monthly %1") : RecurrenceText::tr("Every %n months %1", nullptr, n)).arg(day); |
| 749 | 15 | break; | |
| 750 | 15 | } | |
| 751 | 4 | case RecurrenceRule::Frequency::Yearly: { | |
| 752 | 4 | QString day; | |
| 753 |
4/4✓ Branch 227 → 228 taken 3 times.
✓ Branch 227 → 230 taken 1 time.
✓ Branch 228 → 229 taken 3 times.
✓ Branch 229 → 231 taken 3 times.
|
4 | const QList<int> months = rule.byMonth.isEmpty() ? QList<int>{date.month()} : rule.byMonth; |
| 754 |
1/2✗ Branch 232 → 233 not taken.
✓ Branch 232 → 234 taken 4 times.
|
4 | if (months.size() != 1) |
| 755 | ✗ | break; | |
| 756 |
2/2✓ Branch 234 → 235 taken 4 times.
✓ Branch 236 → 237 taken 4 times.
|
4 | const QString monthName = QLocale().standaloneMonthName(months.first(), QLocale::LongFormat); |
| 757 |
5/6✓ Branch 239 → 240 taken 2 times.
✓ Branch 239 → 243 taken 2 times.
✓ Branch 241 → 242 taken 2 times.
✗ Branch 241 → 243 not taken.
✓ Branch 244 → 245 taken 2 times.
✓ Branch 244 → 281 taken 2 times.
|
4 | if (rule.byDay.isEmpty() && rule.bySetPos.isEmpty()) { |
| 758 |
3/4✓ Branch 246 → 247 taken 2 times.
✗ Branch 246 → 249 not taken.
✓ Branch 247 → 248 taken 2 times.
✓ Branch 248 → 250 taken 2 times.
|
2 | const QList<int> days = rule.byMonthDay.isEmpty() ? QList<int>{date.day()} : rule.byMonthDay; |
| 759 |
3/6✓ Branch 251 → 252 taken 2 times.
✗ Branch 251 → 255 not taken.
✓ Branch 253 → 254 taken 2 times.
✗ Branch 253 → 255 not taken.
✓ Branch 256 → 257 taken 2 times.
✗ Branch 256 → 268 not taken.
|
2 | if (days.size() == 1 && days.first() > 0) |
| 760 |
4/4✓ Branch 257 → 258 taken 2 times.
✓ Branch 258 → 259 taken 2 times.
✓ Branch 261 → 262 taken 2 times.
✓ Branch 262 → 263 taken 2 times.
|
2 | day = QLocale().toString(QDate(2000, months.first(), days.first()), dayMonthFormat()); |
| 761 | ✗ | else if (days == QList<int>{-1}) | |
| 762 | ✗ | day = RecurrenceText::tr("the last day of %1").arg(monthName); | |
| 763 |
6/8✓ Branch 281 → 282 taken 1 time.
✓ Branch 281 → 287 taken 1 time.
✓ Branch 283 → 284 taken 1 time.
✗ Branch 283 → 287 not taken.
✓ Branch 285 → 286 taken 1 time.
✗ Branch 285 → 287 not taken.
✓ Branch 288 → 289 taken 1 time.
✓ Branch 288 → 301 taken 1 time.
|
4 | } else if (singleOrdinal && rule.byMonthDay.isEmpty() && rule.bySetPos.isEmpty()) { |
| 764 |
3/3✓ Branch 289 → 290 taken 1 time.
✓ Branch 293 → 294 taken 1 time.
✓ Branch 294 → 295 taken 1 time.
|
4 | day = RecurrenceText::tr("the %1 %2 of %3").arg(ordinalWord(rule.byDay.first().ordinal), |
| 765 |
1/1✓ Branch 291 → 292 taken 1 time.
|
3 | weekdayName(rule.byDay.first().weekday), monthName); |
| 766 | } | ||
| 767 |
2/2✓ Branch 302 → 303 taken 3 times.
✓ Branch 302 → 312 taken 1 time.
|
4 | if (!day.isEmpty()) |
| 768 |
3/5✓ Branch 303 → 304 taken 3 times.
✗ Branch 303 → 305 not taken.
✓ Branch 304 → 306 taken 3 times.
✗ Branch 305 → 306 not taken.
✓ Branch 307 → 308 taken 3 times.
|
6 | text = (n == 1 ? RecurrenceText::tr("Annually on %1") : RecurrenceText::tr("Every %n years on %1", nullptr, n)).arg(day); |
| 769 | 4 | break; | |
| 770 | 4 | } | |
| 771 | ✗ | case RecurrenceRule::Frequency::Hourly: | |
| 772 | ✗ | if (rule.byDay.isEmpty() && rule.byMonth.isEmpty() && rule.byHour.isEmpty()) | |
| 773 | ✗ | text = n == 1 ? RecurrenceText::tr("Hourly") : RecurrenceText::tr("Every %n hours", nullptr, n); | |
| 774 | ✗ | break; | |
| 775 | 2 | case RecurrenceRule::Frequency::Minutely: | |
| 776 |
4/8✓ Branch 334 → 335 taken 2 times.
✗ Branch 334 → 340 not taken.
✓ Branch 336 → 337 taken 2 times.
✗ Branch 336 → 340 not taken.
✓ Branch 338 → 339 taken 2 times.
✗ Branch 338 → 340 not taken.
✓ Branch 341 → 342 taken 2 times.
✗ Branch 341 → 346 not taken.
|
2 | if (rule.byDay.isEmpty() && rule.byMonth.isEmpty() && rule.byHour.isEmpty()) |
| 777 |
1/1✓ Branch 342 → 343 taken 2 times.
|
2 | text = RecurrenceText::tr("Every %n minutes", nullptr, n); |
| 778 | 2 | break; | |
| 779 | ✗ | default: | |
| 780 | ✗ | break; | |
| 781 | } | ||
| 782 |
7/8✓ Branch 349 → 350 taken 90 times.
✓ Branch 349 → 353 taken 1 time.
✓ Branch 350 → 351 taken 1 time.
✓ Branch 350 → 354 taken 89 times.
✓ Branch 351 → 352 taken 1 time.
✗ Branch 351 → 354 not taken.
✓ Branch 355 → 356 taken 2 times.
✓ Branch 355 → 365 taken 89 times.
|
92 | if (text.isEmpty() || (!simpleDays && rule.frequency != RecurrenceRule::Frequency::Hourly && |
| 783 |
1/2✓ Branch 352 → 353 taken 1 time.
✗ Branch 352 → 354 not taken.
|
1 | rule.frequency != RecurrenceRule::Frequency::Minutely)) |
| 784 |
3/3✓ Branch 356 → 357 taken 2 times.
✓ Branch 358 → 359 taken 2 times.
✓ Branch 359 → 360 taken 2 times.
|
4 | text = RecurrenceText::tr("Custom recurrence (%1)").arg(rule.toString()); |
| 785 |
2/2✓ Branch 365 → 366 taken 22 times.
✓ Branch 365 → 370 taken 69 times.
|
91 | if (rule.count > 0) |
| 786 |
2/2✓ Branch 366 → 367 taken 22 times.
✓ Branch 367 → 368 taken 22 times.
|
22 | text += RecurrenceText::tr(", %n times", nullptr, rule.count); |
| 787 |
2/2✓ Branch 371 → 372 taken 20 times.
✓ Branch 371 → 390 taken 49 times.
|
69 | else if (rule.hasUntil()) { |
| 788 |
2/2✓ Branch 372 → 373 taken 20 times.
✓ Branch 373 → 374 taken 20 times.
|
20 | const QDateTime untilStamp = rule.untilNaive(QTime(0, 0)); |
| 789 |
2/3✓ Branch 374 → 375 taken 20 times.
✓ Branch 375 → 376 taken 20 times.
✗ Branch 375 → 388 not taken.
|
20 | if (untilStamp.isValid()) |
| 790 |
6/6✓ Branch 376 → 377 taken 20 times.
✓ Branch 378 → 379 taken 20 times.
✓ Branch 379 → 380 taken 20 times.
✓ Branch 380 → 381 taken 20 times.
✓ Branch 381 → 382 taken 20 times.
✓ Branch 382 → 383 taken 20 times.
|
40 | text += RecurrenceText::tr(", until %1").arg(QLocale().toString(untilStamp.date(), QLocale::ShortFormat)); |
| 791 | 20 | } | |
| 792 | 91 | return text; | |
| 793 | 91 | } | |
| 794 |