data/RecurrenceExpansion.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "RecurrenceExpansion.h" | ||
| 2 | |||
| 3 | #include "ICalTime.h" | ||
| 4 | #include "RecurrenceRule.h" | ||
| 5 | #include "util/SecurityLog.h" | ||
| 6 | |||
| 7 | #include <QLoggingCategory> | ||
| 8 | #include <QSet> | ||
| 9 | #include <QTimeZone> | ||
| 10 | #include <algorithm> | ||
| 11 | |||
| 12 |
4/7✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 8 taken 9 times.
✓ Branch 4 → 5 taken 5 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 5 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
|
14 | Q_LOGGING_CATEGORY(lcCalendar, "mailjd.calendar") |
| 13 | |||
| 14 | namespace { | ||
| 15 | |||
| 16 | // All-day values are calendar dates stored at UTC midnight; timed values | ||
| 17 | // are shown on the local calendar. | ||
| 18 | 224346 | QDate displayDate(const QDateTime &value, bool allDay) { | |
| 19 |
10/14✓ Branch 2 → 3 taken 2824 times.
✓ Branch 2 → 5 taken 221522 times.
✓ Branch 3 → 4 taken 2824 times.
✓ Branch 4 → 7 taken 2824 times.
✓ Branch 5 → 6 taken 221522 times.
✓ Branch 6 → 7 taken 221522 times.
✓ Branch 8 → 9 taken 221522 times.
✓ Branch 8 → 10 taken 2824 times.
✓ Branch 10 → 11 taken 2824 times.
✓ Branch 10 → 12 taken 221522 times.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
|
448692 | return allDay ? value.toUTC().date() : value.toLocalTime().date(); |
| 20 | } | ||
| 21 | |||
| 22 | // T-79.D3: an occurrence is excluded when its start matches one of the | ||
| 23 | // event's EXDATEs (date-level match for all-day events, exact instant | ||
| 24 | // otherwise). The keys are built once per series: comparing every | ||
| 25 | // occurrence with every EXDATE grew with their product (Sprint 102 fuzzing). | ||
| 26 | 70056 | qint64 exclusionKey(const QDateTime &start, bool allDay) { | |
| 27 |
7/9✓ Branch 2 → 3 taken 564 times.
✓ Branch 2 → 6 taken 69492 times.
✓ Branch 3 → 4 taken 564 times.
✓ Branch 4 → 5 taken 564 times.
✓ Branch 6 → 7 taken 69492 times.
✓ Branch 9 → 10 taken 564 times.
✓ Branch 9 → 11 taken 69492 times.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
|
70056 | return allDay ? start.toUTC().date().toJulianDay() : start.toMSecsSinceEpoch(); |
| 28 | } | ||
| 29 | |||
| 30 | 1377 | QSet<qint64> exclusionKeys(const CalendarEvent &event) { | |
| 31 | 1377 | QSet<qint64> keys; | |
| 32 |
2/2✓ Branch 13 → 5 taken 60 times.
✓ Branch 13 → 14 taken 1377 times.
|
1437 | for (const QDateTime &exdate : event.exdates) |
| 33 |
2/3✓ Branch 6 → 7 taken 60 times.
✓ Branch 7 → 8 taken 60 times.
✗ Branch 7 → 11 not taken.
|
60 | if (exdate.isValid()) |
| 34 |
2/2✓ Branch 8 → 9 taken 60 times.
✓ Branch 9 → 10 taken 60 times.
|
60 | keys.insert(exclusionKey(exdate, event.allDay)); |
| 35 | 1377 | return keys; | |
| 36 | ✗ | } | |
| 37 | |||
| 38 | 74785 | QDate displayEndDate(const CalendarEvent &event) { | |
| 39 |
1/1✓ Branch 2 → 3 taken 74785 times.
|
74785 | const QDate startDate = displayDate(event.dtStart, event.allDay); |
| 40 |
3/3✓ Branch 3 → 4 taken 74785 times.
✓ Branch 4 → 5 taken 9 times.
✓ Branch 4 → 6 taken 74776 times.
|
74785 | if (!event.dtEnd.isValid()) |
| 41 | 9 | return startDate; | |
| 42 |
2/2✓ Branch 6 → 7 taken 938 times.
✓ Branch 6 → 12 taken 73838 times.
|
74776 | if (event.allDay) { |
| 43 | // iCal all-day DTEND is exclusive → last day is the day before. Clamp | ||
| 44 | // to the start date so legacy events stored with an INCLUSIVE end | ||
| 45 | // (pre-Sprint-65 EventEditDialog) still render instead of vanishing. | ||
| 46 |
2/2✓ Branch 7 → 8 taken 938 times.
✓ Branch 8 → 9 taken 938 times.
|
938 | return qMax(startDate, displayDate(event.dtEnd, true).addDays(-1)); |
| 47 | } | ||
| 48 | // F-11 (SPRINT-104): a zero-length event exactly at midnight | ||
| 49 | // (DTSTART == DTEND == 00:00) is shown on ITS day — the −1 ms that | ||
| 50 | // makes an ending exclusive moved it to the previous day and left an | ||
| 51 | // empty day range, so the event vanished. | ||
| 52 |
3/3✓ Branch 12 → 13 taken 73838 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 16 taken 73836 times.
|
73838 | if (event.dtEnd == event.dtStart) |
| 53 |
1/1✓ Branch 14 → 15 taken 2 times.
|
2 | return displayDate(event.dtEnd, false); |
| 54 |
2/2✓ Branch 16 → 17 taken 73836 times.
✓ Branch 17 → 18 taken 73836 times.
|
73836 | return displayDate(event.dtEnd.addMSecs(-1), false); |
| 55 | } | ||
| 56 | |||
| 57 | 74786 | void appendDisplayOccurrence(QList<CalendarEvent> *expanded, const CalendarEvent &event, | |
| 58 | const QDate &rangeStart, const QDate &rangeEnd, bool splitMultiDay) { | ||
| 59 |
3/3✓ Branch 2 → 3 taken 74786 times.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 74785 times.
|
74786 | if (!event.isSchedulable()) |
| 60 | 74718 | return; | |
| 61 |
1/1✓ Branch 5 → 6 taken 74785 times.
|
74785 | const QDate startDate = displayDate(event.dtStart, event.allDay); |
| 62 |
1/1✓ Branch 6 → 7 taken 74785 times.
|
74785 | const QDate endDate = displayEndDate(event); |
| 63 | 74785 | QDate day = qMax(startDate, rangeStart); | |
| 64 | 74785 | const QDate lastDay = qMin(endDate, rangeEnd); | |
| 65 |
2/2✓ Branch 9 → 10 taken 74717 times.
✓ Branch 9 → 20 taken 68 times.
|
74785 | if (!splitMultiDay) { |
| 66 |
6/7✓ Branch 10 → 11 taken 74717 times.
✓ Branch 11 → 12 taken 74717 times.
✗ Branch 11 → 16 not taken.
✓ Branch 14 → 15 taken 60713 times.
✓ Branch 14 → 16 taken 14004 times.
✓ Branch 17 → 18 taken 60713 times.
✓ Branch 17 → 19 taken 14004 times.
|
74717 | if (day.isValid() && day <= lastDay) |
| 67 |
1/1✓ Branch 18 → 19 taken 60713 times.
|
60713 | expanded->append(event); |
| 68 | 74717 | return; | |
| 69 | } | ||
| 70 |
6/7✓ Branch 68 → 69 taken 133 times.
✓ Branch 69 → 70 taken 133 times.
✗ Branch 69 → 74 not taken.
✓ Branch 72 → 73 taken 65 times.
✓ Branch 72 → 74 taken 68 times.
✓ Branch 75 → 21 taken 65 times.
✓ Branch 75 → 76 taken 68 times.
|
133 | while (day.isValid() && day <= lastDay) { |
| 71 | 65 | CalendarEvent displayEvent = event; | |
| 72 |
2/2✓ Branch 23 → 24 taken 6 times.
✓ Branch 23 → 38 taken 59 times.
|
65 | if (day != startDate) { |
| 73 |
6/8✓ Branch 24 → 25 taken 4 times.
✓ Branch 24 → 28 taken 2 times.
✓ Branch 25 → 26 taken 4 times.
✓ Branch 26 → 27 taken 4 times.
✓ Branch 27 → 31 taken 4 times.
✓ Branch 30 → 31 taken 2 times.
✗ Branch 85 → 86 not taken.
✗ Branch 85 → 87 not taken.
|
20 | displayEvent.dtStart = event.allDay ? QDateTime(day, QTime(0, 0), QTimeZone::utc()) |
| 74 |
6/8✓ Branch 28 → 29 taken 2 times.
✓ Branch 29 → 30 taken 2 times.
✓ Branch 33 → 34 taken 2 times.
✓ Branch 33 → 35 taken 4 times.
✓ Branch 35 → 36 taken 4 times.
✓ Branch 35 → 37 taken 2 times.
✗ Branch 80 → 81 not taken.
✗ Branch 80 → 82 not taken.
|
14 | : QDateTime(day, QTime(0, 0), QTimeZone::systemTimeZone()); |
| 75 | } | ||
| 76 |
6/7✓ Branch 39 → 40 taken 6 times.
✓ Branch 39 → 43 taken 59 times.
✓ Branch 40 → 41 taken 6 times.
✓ Branch 41 → 42 taken 6 times.
✗ Branch 41 → 43 not taken.
✓ Branch 44 → 45 taken 6 times.
✓ Branch 44 → 61 taken 59 times.
|
65 | if (day != endDate && event.dtEnd.isValid()) { |
| 77 | 6 | displayEvent.dtEnd = event.allDay | |
| 78 |
8/10✓ Branch 45 → 46 taken 4 times.
✓ Branch 45 → 50 taken 2 times.
✓ Branch 46 → 47 taken 4 times.
✓ Branch 47 → 48 taken 4 times.
✓ Branch 48 → 49 taken 4 times.
✓ Branch 49 → 54 taken 4 times.
✓ Branch 52 → 53 taken 2 times.
✓ Branch 53 → 54 taken 2 times.
✗ Branch 96 → 97 not taken.
✗ Branch 96 → 98 not taken.
|
20 | ? QDateTime(day.addDays(1), QTime(0, 0), QTimeZone::utc()) |
| 79 |
6/8✓ Branch 50 → 51 taken 2 times.
✓ Branch 51 → 52 taken 2 times.
✓ Branch 56 → 57 taken 2 times.
✓ Branch 56 → 58 taken 4 times.
✓ Branch 58 → 59 taken 4 times.
✓ Branch 58 → 60 taken 2 times.
✗ Branch 91 → 92 not taken.
✗ Branch 91 → 93 not taken.
|
14 | : QDateTime(day.addDays(1), QTime(0, 0), QTimeZone::systemTimeZone()); |
| 80 | } | ||
| 81 |
2/2✓ Branch 62 → 63 taken 8 times.
✓ Branch 62 → 64 taken 57 times.
|
65 | if (startDate != endDate) |
| 82 | 8 | displayEvent.allDay = true; | |
| 83 |
1/1✓ Branch 64 → 65 taken 65 times.
|
65 | expanded->append(displayEvent); |
| 84 |
1/1✓ Branch 65 → 66 taken 65 times.
|
65 | day = day.addDays(1); |
| 85 | 65 | } | |
| 86 | } | ||
| 87 | |||
| 88 | 69949 | CalendarEvent shiftedOccurrence(const CalendarEvent &event, const QDateTime &start) { | |
| 89 | 69949 | CalendarEvent shifted = event; | |
| 90 | 69949 | shifted.dtStart = start; | |
| 91 |
3/3✓ Branch 4 → 5 taken 69949 times.
✓ Branch 5 → 6 taken 69943 times.
✓ Branch 5 → 14 taken 6 times.
|
69949 | if (event.dtEnd.isValid()) { |
| 92 |
5/5✓ Branch 6 → 7 taken 560 times.
✓ Branch 6 → 9 taken 69383 times.
✓ Branch 7 → 8 taken 560 times.
✓ Branch 8 → 11 taken 560 times.
✓ Branch 10 → 11 taken 69383 times.
|
209269 | shifted.dtEnd = event.allDay ? start.addDays(event.dtStart.daysTo(event.dtEnd)) |
| 93 |
1/1✓ Branch 9 → 10 taken 69383 times.
|
139326 | : start.addSecs(event.dtStart.secsTo(event.dtEnd)); |
| 94 | } | ||
| 95 | 69949 | return shifted; | |
| 96 | ✗ | } | |
| 97 | |||
| 98 | // The zone the event's wall clock lives in: its TZID zone, UTC, or invalid | ||
| 99 | // for floating (local) time. | ||
| 100 | 1515 | QTimeZone wallClockZone(const QDateTime &value) { | |
| 101 |
2/5✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1315 times.
✗ Branch 3 → 6 not taken.
✓ Branch 3 → 8 taken 200 times.
✗ Branch 3 → 9 not taken.
|
1515 | switch (value.timeSpec()) { |
| 102 | ✗ | case Qt::UTC: return QTimeZone::utc(); | |
| 103 | 1315 | case Qt::TimeZone: return value.timeZone(); | |
| 104 | ✗ | case Qt::OffsetFromUTC: return QTimeZone(value.offsetFromUtc()); | |
| 105 | 200 | case Qt::LocalTime: return {}; | |
| 106 | } | ||
| 107 | ✗ | return {}; | |
| 108 | } | ||
| 109 | |||
| 110 | 70113 | QDateTime toInstant(const QDateTime &naive, bool allDay, const QTimeZone &zone) { | |
| 111 |
2/2✓ Branch 2 → 3 taken 569 times.
✓ Branch 2 → 9 taken 69544 times.
|
70113 | if (allDay) |
| 112 |
4/4✓ Branch 3 → 4 taken 569 times.
✓ Branch 4 → 5 taken 569 times.
✓ Branch 5 → 6 taken 569 times.
✓ Branch 6 → 7 taken 569 times.
|
569 | return QDateTime(naive.date(), QTime(0, 0), QTimeZone::utc()); |
| 113 |
3/3✓ Branch 9 → 10 taken 69544 times.
✓ Branch 10 → 11 taken 69544 times.
✓ Branch 11 → 12 taken 69544 times.
|
69544 | return ICalTime::fromWallClock(naive.date(), naive.time(), zone); |
| 114 | } | ||
| 115 | |||
| 116 | 1614 | QDateTime toNaive(const QDateTime &instant, bool allDay, const QTimeZone &zone) { | |
| 117 |
2/2✓ Branch 2 → 3 taken 65 times.
✓ Branch 2 → 12 taken 1549 times.
|
1614 | if (allDay) |
| 118 |
5/5✓ Branch 3 → 4 taken 65 times.
✓ Branch 4 → 5 taken 65 times.
✓ Branch 5 → 6 taken 65 times.
✓ Branch 6 → 7 taken 65 times.
✓ Branch 7 → 8 taken 65 times.
|
65 | return QDateTime(instant.toUTC().date(), QTime(0, 0), QTimeZone::utc()); |
| 119 |
5/5✓ Branch 12 → 13 taken 1549 times.
✓ Branch 13 → 14 taken 1335 times.
✓ Branch 13 → 15 taken 214 times.
✓ Branch 14 → 16 taken 1335 times.
✓ Branch 15 → 16 taken 214 times.
|
1549 | const QDateTime local = zone.isValid() ? instant.toTimeZone(zone) : instant.toLocalTime(); |
| 120 |
4/4✓ Branch 16 → 17 taken 1549 times.
✓ Branch 17 → 18 taken 1549 times.
✓ Branch 18 → 19 taken 1549 times.
✓ Branch 19 → 20 taken 1549 times.
|
1549 | return QDateTime(local.date(), local.time(), QTimeZone::utc()); |
| 121 | 1549 | } | |
| 122 | |||
| 123 | // UNTIL on the event's wall clock (RFC 5545: inclusive bound). | ||
| 124 | 1374 | QDateTime untilOnWallClock(const RecurrenceRule &rule, bool allDay, const QTimeZone &zone) { | |
| 125 |
2/2✓ Branch 3 → 4 taken 1293 times.
✓ Branch 3 → 5 taken 81 times.
|
1374 | if (!rule.hasUntil()) |
| 126 | 1293 | return {}; | |
| 127 |
2/2✓ Branch 6 → 7 taken 7 times.
✓ Branch 6 → 13 taken 74 times.
|
81 | if (rule.untilIsDate()) |
| 128 |
5/5✓ Branch 7 → 8 taken 6 times.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 8 → 10 taken 6 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 7 times.
|
7 | return rule.untilNaive(allDay ? QTime(0, 0) : QTime(23, 59, 59)); |
| 129 |
2/2✓ Branch 13 → 14 taken 74 times.
✓ Branch 14 → 15 taken 74 times.
|
74 | const QDateTime naive = rule.untilNaive(QTime(0, 0)); |
| 130 |
2/3✓ Branch 15 → 16 taken 74 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 74 times.
|
74 | if (!rule.untilIsUtc()) |
| 131 | ✗ | return naive; // floating UNTIL: already a wall clock | |
| 132 |
5/5✓ Branch 18 → 19 taken 74 times.
✓ Branch 19 → 20 taken 74 times.
✓ Branch 20 → 21 taken 74 times.
✓ Branch 21 → 22 taken 74 times.
✓ Branch 22 → 23 taken 74 times.
|
148 | return toNaive(QDateTime(naive.date(), naive.time(), QTimeZone::utc()), allDay, zone); |
| 133 | 74 | } | |
| 134 | |||
| 135 | // Sprint 93 S93.1.3/S93.1.4: occurrence starts of a recurring master in | ||
| 136 | // [rangeStart, rangeEnd] (with margin), stepped on the event's wall clock. | ||
| 137 | 1377 | QList<QDateTime> occurrenceStarts(const CalendarEvent &event, const QDate &rangeStart, | |
| 138 | const QDate &rangeEnd, bool *partial, int maxStarts, bool *limited) { | ||
| 139 |
2/3✓ Branch 2 → 3 taken 1377 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1377 times.
|
1377 | if (!event.isSchedulable()) |
| 140 | ✗ | return {}; | |
| 141 |
1/1✓ Branch 5 → 6 taken 1377 times.
|
1377 | const QTimeZone zone = wallClockZone(event.dtStart); |
| 142 |
1/1✓ Branch 6 → 7 taken 1377 times.
|
1377 | const QDateTime naiveStart = toNaive(event.dtStart, event.allDay, zone); |
| 143 | // Occurrences that start before the range but last into it are found by | ||
| 144 | // looking back by the duration, capped at a year: a series whose | ||
| 145 | // occurrences last 1 000 years made every display expand from year 1026 | ||
| 146 | // (Sprint 102 fuzzing). Longer occurrences show from the year before. | ||
| 147 | 1377 | constexpr qint64 kMaxLookBackDays = 366; | |
| 148 | 1377 | const qint64 durationDays = qMin<qint64>( | |
| 149 |
4/4✓ Branch 7 → 8 taken 1377 times.
✓ Branch 8 → 9 taken 1375 times.
✓ Branch 8 → 12 taken 2 times.
✓ Branch 9 → 10 taken 1375 times.
|
1377 | kMaxLookBackDays, event.dtEnd.isValid() ? qMax<qint64>(0, event.dtStart.daysTo(event.dtEnd)) : 0); |
| 150 | // Margins: occurrences that start before the range but still overlap it, | ||
| 151 | // and the offset between the event zone and the display zone. | ||
| 152 |
4/4✓ Branch 14 → 15 taken 1377 times.
✓ Branch 15 → 16 taken 1377 times.
✓ Branch 16 → 17 taken 1377 times.
✓ Branch 17 → 18 taken 1377 times.
|
1377 | const QDateTime windowStart(rangeStart.addDays(-durationDays - 2), QTime(0, 0), QTimeZone::utc()); |
| 153 |
4/4✓ Branch 19 → 20 taken 1377 times.
✓ Branch 20 → 21 taken 1377 times.
✓ Branch 21 → 22 taken 1377 times.
✓ Branch 22 → 23 taken 1377 times.
|
1377 | const QDateTime windowEnd(rangeEnd.addDays(2), QTime(23, 59, 59), QTimeZone::utc()); |
| 154 | 1377 | QList<QDateTime> starts; | |
| 155 |
3/3✓ Branch 24 → 25 taken 1377 times.
✓ Branch 27 → 28 taken 1374 times.
✓ Branch 27 → 71 taken 3 times.
|
1377 | if (!event.rrule.trimmed().isEmpty()) { |
| 156 |
1/1✓ Branch 28 → 29 taken 1374 times.
|
1374 | const RecurrenceRule rule = RecurrenceRule::parse(event.rrule); |
| 157 | // The limit counts from the range start (minus the duration), not from | ||
| 158 | // the zone margin: a minutely series would spend it before the range. | ||
| 159 |
4/4✓ Branch 29 → 30 taken 1374 times.
✓ Branch 30 → 31 taken 1374 times.
✓ Branch 31 → 32 taken 1374 times.
✓ Branch 32 → 33 taken 1374 times.
|
1374 | const QDateTime countFrom(rangeStart.addDays(-durationDays), QTime(0, 0), QTimeZone::utc()); |
| 160 | const NaiveExpansion naive = expandNaive(rule, naiveStart, windowStart, windowEnd, | ||
| 161 |
2/2✓ Branch 34 → 35 taken 1374 times.
✓ Branch 35 → 36 taken 1374 times.
|
1374 | untilOnWallClock(rule, event.allDay, zone), maxStarts, countFrom); |
| 162 | 1374 | *limited = naive.limited; | |
| 163 |
4/4✓ Branch 37 → 38 taken 1368 times.
✓ Branch 37 → 39 taken 6 times.
✓ Branch 38 → 39 taken 8 times.
✓ Branch 38 → 58 taken 1360 times.
|
1374 | if (naive.partial || naive.truncated) { |
| 164 | 14 | *partial = true; | |
| 165 |
7/7✓ Branch 39 → 40 taken 14 times.
✓ Branch 43 → 44 taken 14 times.
✓ Branch 44 → 45 taken 14 times.
✓ Branch 45 → 46 taken 14 times.
✓ Branch 46 → 47 taken 14 times.
✓ Branch 56 → 41 taken 14 times.
✓ Branch 56 → 57 taken 14 times.
|
28 | qCWarning(lcCalendar) << "RRULE shown incompletely" << event.rrule << "for event" |
| 166 |
4/4✓ Branch 47 → 48 taken 14 times.
✓ Branch 48 → 49 taken 13 times.
✓ Branch 48 → 50 taken 1 time.
✓ Branch 51 → 52 taken 14 times.
|
14 | << event.uid << (rule.valid ? QString() : rule.error); |
| 167 | } | ||
| 168 |
2/2✓ Branch 66 → 60 taken 69975 times.
✓ Branch 66 → 67 taken 1374 times.
|
71349 | for (const QDateTime &stamp : naive.starts) |
| 169 |
2/2✓ Branch 61 → 62 taken 69975 times.
✓ Branch 62 → 63 taken 69975 times.
|
69975 | starts.append(toInstant(stamp, event.allDay, zone)); |
| 170 |
5/8✓ Branch 71 → 72 taken 3 times.
✓ Branch 73 → 74 taken 3 times.
✗ Branch 73 → 78 not taken.
✓ Branch 74 → 75 taken 3 times.
✓ Branch 76 → 77 taken 3 times.
✗ Branch 76 → 78 not taken.
✓ Branch 79 → 80 taken 3 times.
✗ Branch 79 → 81 not taken.
|
1377 | } else if (naiveStart >= windowStart && naiveStart <= windowEnd) { |
| 171 |
1/1✓ Branch 80 → 81 taken 3 times.
|
3 | starts.append(event.dtStart); |
| 172 | } | ||
| 173 | 1377 | QSet<qint64> seen; | |
| 174 |
4/4✓ Branch 82 → 83 taken 1377 times.
✓ Branch 83 → 84 taken 1377 times.
✓ Branch 90 → 85 taken 69978 times.
✓ Branch 90 → 91 taken 1377 times.
|
71355 | for (const QDateTime &start : starts) |
| 175 |
2/2✓ Branch 86 → 87 taken 69978 times.
✓ Branch 87 → 88 taken 69978 times.
|
69978 | seen.insert(start.toSecsSinceEpoch()); |
| 176 |
2/2✓ Branch 141 → 93 taken 25 times.
✓ Branch 141 → 142 taken 1377 times.
|
1402 | for (const QDateTime &rdate : event.rdates) { |
| 177 |
1/8✗ Branch 95 → 96 not taken.
✗ Branch 97 → 98 not taken.
✗ Branch 101 → 102 not taken.
✓ Branch 101 → 103 taken 25 times.
✗ Branch 195 → 196 not taken.
✗ Branch 195 → 197 not taken.
✗ Branch 200 → 201 not taken.
✗ Branch 200 → 202 not taken.
|
25 | const QDateTime instant = event.allDay ? QDateTime(rdate.toUTC().date(), QTime(0, 0), QTimeZone::utc()) |
| 178 |
2/7✗ Branch 94 → 95 not taken.
✓ Branch 94 → 100 taken 25 times.
✗ Branch 96 → 97 not taken.
✗ Branch 98 → 99 not taken.
✗ Branch 99 → 101 not taken.
✗ Branch 103 → 104 not taken.
✓ Branch 103 → 105 taken 25 times.
|
50 | : rdate; |
| 179 |
1/1✓ Branch 105 → 106 taken 25 times.
|
25 | const QDateTime naive = toNaive(instant, event.allDay, zone); |
| 180 |
10/11✓ Branch 106 → 107 taken 25 times.
✓ Branch 108 → 109 taken 21 times.
✓ Branch 108 → 115 taken 4 times.
✓ Branch 109 → 110 taken 21 times.
✓ Branch 111 → 112 taken 21 times.
✗ Branch 111 → 115 not taken.
✓ Branch 112 → 113 taken 21 times.
✓ Branch 114 → 115 taken 3 times.
✓ Branch 114 → 116 taken 18 times.
✓ Branch 117 → 118 taken 7 times.
✓ Branch 117 → 119 taken 18 times.
|
25 | if (naive < windowStart || naive > windowEnd || seen.contains(instant.toSecsSinceEpoch())) |
| 181 | 7 | continue; | |
| 182 |
2/2✓ Branch 119 → 120 taken 18 times.
✓ Branch 120 → 121 taken 18 times.
|
18 | seen.insert(instant.toSecsSinceEpoch()); |
| 183 | // Keep the series zone so later wall-clock math stays in it. | ||
| 184 |
6/7✓ Branch 121 → 122 taken 18 times.
✓ Branch 122 → 123 taken 17 times.
✓ Branch 122 → 126 taken 1 time.
✓ Branch 123 → 124 taken 17 times.
✗ Branch 123 → 126 not taken.
✓ Branch 124 → 125 taken 17 times.
✓ Branch 127 → 128 taken 18 times.
|
18 | starts.append(zone.isValid() && !event.allDay ? instant.toTimeZone(zone) : instant); |
| 185 | 32 | } | |
| 186 |
3/3✓ Branch 142 → 143 taken 1377 times.
✓ Branch 143 → 144 taken 1377 times.
✓ Branch 144 → 145 taken 1377 times.
|
1377 | std::sort(starts.begin(), starts.end()); |
| 187 | 1377 | return starts; | |
| 188 | 1377 | } | |
| 189 | |||
| 190 | 108 | bool matchesAnchor(const CalendarEvent &occurrence, const CalendarEvent &exception) { | |
| 191 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 108 times.
|
108 | if (occurrence.uid != exception.uid) |
| 192 | ✗ | return false; | |
| 193 |
2/4✓ Branch 5 → 6 taken 108 times.
✗ Branch 5 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 15 taken 108 times.
|
108 | if (occurrence.allDay || exception.allDay) |
| 194 | ✗ | return occurrence.dtStart.toUTC().date() == exception.recurrenceId.toUTC().date(); | |
| 195 |
3/3✓ Branch 15 → 16 taken 108 times.
✓ Branch 16 → 17 taken 108 times.
✓ Branch 17 → 18 taken 108 times.
|
108 | return occurrence.dtStart.toUTC() == exception.recurrenceId.toUTC(); |
| 196 | } | ||
| 197 | |||
| 198 | } // namespace | ||
| 199 | |||
| 200 | 2040 | QDateTime seriesEnd(const CalendarEvent &event) { | |
| 201 |
3/3✓ Branch 2 → 3 taken 2040 times.
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 5 taken 2034 times.
|
2040 | if (!event.isSchedulable()) |
| 202 | 6 | return {}; | |
| 203 |
4/4✓ Branch 5 → 6 taken 2034 times.
✓ Branch 6 → 7 taken 2014 times.
✓ Branch 6 → 10 taken 20 times.
✓ Branch 7 → 8 taken 2014 times.
|
2034 | const qint64 duration = event.dtEnd.isValid() ? qMax<qint64>(0, event.dtStart.secsTo(event.dtEnd)) : 0; |
| 204 |
12/14✓ Branch 11 → 12 taken 2034 times.
✓ Branch 12 → 13 taken 2023 times.
✓ Branch 12 → 18 taken 11 times.
✓ Branch 13 → 14 taken 2023 times.
✓ Branch 15 → 16 taken 1837 times.
✓ Branch 15 → 19 taken 186 times.
✓ Branch 17 → 18 taken 1836 times.
✓ Branch 17 → 19 taken 1 time.
✓ Branch 20 → 21 taken 2023 times.
✓ Branch 20 → 22 taken 11 times.
✓ Branch 22 → 23 taken 1847 times.
✓ Branch 22 → 25 taken 187 times.
✗ Branch 129 → 130 not taken.
✗ Branch 129 → 131 not taken.
|
2034 | if (event.recurrenceId.isValid() || (event.rrule.trimmed().isEmpty() && event.rdates.isEmpty())) |
| 205 |
1/1✓ Branch 23 → 24 taken 1847 times.
|
1847 | return event.dtStart.addSecs(duration); |
| 206 | 187 | QDateTime last = event.dtStart; | |
| 207 |
3/3✓ Branch 26 → 27 taken 187 times.
✓ Branch 29 → 30 taken 186 times.
✓ Branch 29 → 111 taken 1 time.
|
187 | if (!event.rrule.trimmed().isEmpty()) { |
| 208 |
1/1✓ Branch 30 → 31 taken 186 times.
|
186 | const RecurrenceRule rule = RecurrenceRule::parse(event.rrule); |
| 209 |
11/11✓ Branch 31 → 32 taken 183 times.
✓ Branch 31 → 37 taken 3 times.
✓ Branch 32 → 33 taken 183 times.
✓ Branch 33 → 34 taken 182 times.
✓ Branch 33 → 37 taken 1 time.
✓ Branch 34 → 35 taken 44 times.
✓ Branch 34 → 38 taken 138 times.
✓ Branch 36 → 37 taken 34 times.
✓ Branch 36 → 38 taken 10 times.
✓ Branch 39 → 40 taken 38 times.
✓ Branch 39 → 42 taken 148 times.
|
186 | if (!rule.valid || !rule.fullySupported() || (rule.count == 0 && !rule.hasUntil())) |
| 210 | 38 | return {}; | |
| 211 |
2/2✓ Branch 43 → 44 taken 10 times.
✓ Branch 43 → 66 taken 138 times.
|
148 | if (rule.hasUntil()) { |
| 212 | // UNTIL is an upper bound already: its wall clock read as UTC plus a | ||
| 213 | // day covers every zone offset - no expansion (a daily series over | ||
| 214 | // five years would cost ~2 ms per stored row). | ||
| 215 |
2/2✓ Branch 44 → 45 taken 10 times.
✓ Branch 45 → 46 taken 10 times.
|
10 | const QDateTime until = rule.untilNaive(QTime(23, 59, 59)); |
| 216 | // S106-125: an UNTIL of the right shape that is no real date | ||
| 217 | // ("UNTIL=99999999") gives no bound. Before, the invalid date became an | ||
| 218 | // epoch-0 end that hid the whole series from every range query. | ||
| 219 |
2/3✓ Branch 46 → 47 taken 10 times.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 50 taken 10 times.
|
10 | if (!until.isValid()) |
| 220 | ✗ | return {}; | |
| 221 |
6/6✓ Branch 50 → 51 taken 10 times.
✓ Branch 51 → 52 taken 10 times.
✓ Branch 52 → 53 taken 10 times.
✓ Branch 53 → 54 taken 10 times.
✓ Branch 54 → 55 taken 10 times.
✓ Branch 55 → 56 taken 10 times.
|
10 | last = qMax(last, QDateTime(until.date(), QTime(23, 59, 59), QTimeZone::utc()).addDays(1)); |
| 222 |
1/2✓ Branch 62 → 63 taken 10 times.
✗ Branch 62 → 65 not taken.
|
10 | } else { |
| 223 | // COUNT: the last of the counted occurrences. | ||
| 224 |
1/1✓ Branch 66 → 67 taken 138 times.
|
138 | const QTimeZone zone = wallClockZone(event.dtStart); |
| 225 |
1/1✓ Branch 67 → 68 taken 138 times.
|
138 | const QDateTime naiveStart = toNaive(event.dtStart, event.allDay, zone); |
| 226 | 276 | const NaiveExpansion naive = expandNaive(rule, naiveStart, naiveStart, | |
| 227 |
5/5✓ Branch 70 → 71 taken 138 times.
✓ Branch 71 → 72 taken 138 times.
✓ Branch 72 → 73 taken 138 times.
✓ Branch 73 → 74 taken 138 times.
✓ Branch 74 → 75 taken 138 times.
|
138 | QDateTime(QDate(9999, 12, 31), QTime(0, 0), QTimeZone::utc())); |
| 228 |
2/4✓ Branch 79 → 80 taken 138 times.
✗ Branch 79 → 81 not taken.
✗ Branch 80 → 81 not taken.
✓ Branch 80 → 83 taken 138 times.
|
138 | if (naive.partial || naive.truncated) |
| 229 | ✗ | return {}; | |
| 230 |
1/2✓ Branch 84 → 85 taken 138 times.
✗ Branch 84 → 91 not taken.
|
138 | if (!naive.starts.isEmpty()) |
| 231 |
2/2✓ Branch 86 → 87 taken 138 times.
✓ Branch 87 → 88 taken 138 times.
|
138 | last = qMax(last, toInstant(naive.starts.last(), event.allDay, zone)); |
| 232 | 138 | } | |
| 233 | 186 | } | |
| 234 |
2/2✓ Branch 118 → 113 taken 3 times.
✓ Branch 118 → 119 taken 149 times.
|
152 | for (const QDateTime &rdate : event.rdates) |
| 235 |
1/1✓ Branch 114 → 115 taken 3 times.
|
3 | last = qMax(last, rdate); |
| 236 |
2/2✓ Branch 119 → 120 taken 149 times.
✓ Branch 120 → 121 taken 149 times.
|
149 | return last.addSecs(duration).addDays(1); |
| 237 | 187 | } | |
| 238 | |||
| 239 | 1380 | QString seriesKey(const CalendarEvent &event) { | |
| 240 |
4/4✓ Branch 6 → 7 taken 1380 times.
✓ Branch 7 → 8 taken 1380 times.
✓ Branch 8 → 9 taken 1380 times.
✓ Branch 9 → 10 taken 1380 times.
|
1380 | return event.accountId + QLatin1Char('\n') + event.calendarPath + QLatin1Char('\n') + event.uid; |
| 241 | } | ||
| 242 | |||
| 243 | 5438 | QList<CalendarEvent> expandEventsForDisplay(const QList<CalendarEvent> &events, | |
| 244 | const QDate &rangeStart, const QDate &rangeEnd, | ||
| 245 | bool splitMultiDay, const DisplayLimit &limit) { | ||
| 246 | 5438 | QList<CalendarEvent> expanded; | |
| 247 | // Exceptions replace the master occurrence at their RECURRENCE-ID anchor; | ||
| 248 | // collect them first so the master pass can skip those anchors before | ||
| 249 | // splitting multi-day occurrences into display days. | ||
| 250 | 5438 | QList<CalendarEvent> exceptions; | |
| 251 |
2/2✓ Branch 10 → 4 taken 6234 times.
✓ Branch 10 → 11 taken 5438 times.
|
11672 | for (const auto &event : events) |
| 252 |
3/3✓ Branch 5 → 6 taken 6234 times.
✓ Branch 6 → 7 taken 20 times.
✓ Branch 6 → 8 taken 6214 times.
|
6234 | if (event.recurrenceId.isValid()) |
| 253 |
1/1✓ Branch 7 → 8 taken 20 times.
|
20 | exceptions.append(event); |
| 254 | |||
| 255 |
2/2✓ Branch 66 → 13 taken 6234 times.
✓ Branch 66 → 67 taken 5438 times.
|
11672 | for (const auto &event : events) { |
| 256 |
3/3✓ Branch 14 → 15 taken 6234 times.
✓ Branch 15 → 16 taken 20 times.
✓ Branch 15 → 17 taken 6214 times.
|
6234 | if (event.recurrenceId.isValid()) |
| 257 | 4857 | continue; | |
| 258 |
8/11✓ Branch 17 → 18 taken 6214 times.
✓ Branch 19 → 20 taken 4840 times.
✓ Branch 19 → 23 taken 1374 times.
✓ Branch 21 → 22 taken 4837 times.
✓ Branch 21 → 23 taken 3 times.
✓ Branch 24 → 25 taken 6214 times.
✗ Branch 24 → 26 not taken.
✓ Branch 26 → 27 taken 4837 times.
✓ Branch 26 → 29 taken 1377 times.
✗ Branch 82 → 83 not taken.
✗ Branch 82 → 84 not taken.
|
6214 | if (event.rrule.trimmed().isEmpty() && event.rdates.isEmpty()) { |
| 259 |
1/1✓ Branch 27 → 28 taken 4837 times.
|
4837 | appendDisplayOccurrence(&expanded, event, rangeStart, rangeEnd, splitMultiDay); |
| 260 | 4837 | continue; | |
| 261 | } | ||
| 262 | 1377 | bool partial = false, limited = false; | |
| 263 |
3/3✓ Branch 29 → 30 taken 1377 times.
✓ Branch 31 → 32 taken 5 times.
✓ Branch 31 → 33 taken 1372 times.
|
1377 | const int maxStarts = limit.unlimitedSeries.contains(seriesKey(event)) ? 0 : limit.perSeries; |
| 264 |
1/1✓ Branch 35 → 36 taken 1377 times.
|
1377 | const QList<QDateTime> starts = occurrenceStarts(event, rangeStart, rangeEnd, &partial, maxStarts, &limited); |
| 265 |
1/1✓ Branch 36 → 37 taken 1377 times.
|
1377 | const QSet<qint64> excluded = exclusionKeys(event); |
| 266 |
2/2✓ Branch 59 → 39 taken 69996 times.
✓ Branch 59 → 60 taken 1377 times.
|
71373 | for (const QDateTime &start : starts) { |
| 267 | // T-79.D3: EXDATE removes the occurrence (it still consumed COUNT). | ||
| 268 |
7/8✓ Branch 40 → 41 taken 69996 times.
✓ Branch 41 → 42 taken 69996 times.
✗ Branch 41 → 46 not taken.
✓ Branch 42 → 43 taken 69996 times.
✓ Branch 44 → 45 taken 47 times.
✓ Branch 44 → 46 taken 69949 times.
✓ Branch 47 → 48 taken 47 times.
✓ Branch 47 → 49 taken 69949 times.
|
69996 | if (start.isValid() && excluded.contains(exclusionKey(start, event.allDay))) |
| 269 | 47 | continue; | |
| 270 |
1/1✓ Branch 49 → 50 taken 69949 times.
|
69949 | CalendarEvent occurrence = shiftedOccurrence(event, start); |
| 271 | 69949 | occurrence.partialRecurrence = partial; | |
| 272 | 69949 | occurrence.occurrencesLimited = limited; | |
| 273 |
1/1✓ Branch 52 → 53 taken 69949 times.
|
69949 | const bool replaced = std::any_of(exceptions.cbegin(), exceptions.cend(), |
| 274 | 108 | [&occurrence](const CalendarEvent &exception) { | |
| 275 | 108 | return matchesAnchor(occurrence, exception); | |
| 276 | }); | ||
| 277 |
2/2✓ Branch 53 → 54 taken 69929 times.
✓ Branch 53 → 55 taken 20 times.
|
69949 | if (!replaced) |
| 278 |
1/1✓ Branch 54 → 55 taken 69929 times.
|
69929 | appendDisplayOccurrence(&expanded, occurrence, rangeStart, rangeEnd, splitMultiDay); |
| 279 | 69949 | } | |
| 280 | 1377 | } | |
| 281 | |||
| 282 | // Q11: recurrence exceptions render at their own (moved) start. | ||
| 283 |
4/4✓ Branch 67 → 68 taken 5438 times.
✓ Branch 68 → 69 taken 5438 times.
✓ Branch 74 → 70 taken 20 times.
✓ Branch 74 → 75 taken 5438 times.
|
5458 | for (const auto &exception : exceptions) |
| 284 |
1/1✓ Branch 71 → 72 taken 20 times.
|
20 | appendDisplayOccurrence(&expanded, exception, rangeStart, rangeEnd, splitMultiDay); |
| 285 | |||
| 286 |
3/3✓ Branch 75 → 76 taken 5438 times.
✓ Branch 76 → 77 taken 5438 times.
✓ Branch 77 → 78 taken 5438 times.
|
5438 | std::sort(expanded.begin(), expanded.end(), |
| 287 | 666526 | [](const CalendarEvent &a, const CalendarEvent &b) { | |
| 288 | 666526 | return a.dtStart < b.dtStart; | |
| 289 | }); | ||
| 290 | 5438 | return expanded; | |
| 291 | 5438 | } | |
| 292 |