MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 93.3% 581 / 0 / 623
Functions: 97.6% 40 / 0 / 41
Branches: 82.5% 955 / 0 / 1158

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