app/SearchQuery.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "SearchQuery.h" | ||
| 2 | |||
| 3 | #include <QDate> | ||
| 4 | #include <QLocale> | ||
| 5 | #include <QDateTime> | ||
| 6 | #include <QStringList> | ||
| 7 | |||
| 8 | using Tri = MailCache::SearchFilter::Tri; | ||
| 9 | |||
| 10 | namespace { | ||
| 11 | |||
| 12 | // Split the input into tokens, honoring double quotes so a prefix value can | ||
| 13 | // contain spaces (subject:"offene Rechnung" → one token "subject:offene | ||
| 14 | // Rechnung"). Quote characters are dropped; spaces inside quotes are kept. | ||
| 15 | // `quoted`: the token held a quote after its start (prefix:"value"). | ||
| 16 | struct QueryToken { QString text; bool literal = false; bool quoted = false; }; | ||
| 17 | 14837 | QList<QueryToken> tokenize(const QString &input) { | |
| 18 | 14837 | QList<QueryToken> tokens; | |
| 19 | 14837 | QString cur; | |
| 20 | 14837 | bool inQuotes = false; | |
| 21 | 14837 | bool hasToken = false; | |
| 22 | 14837 | bool literal = false; | |
| 23 | 14837 | bool quoted = false; | |
| 24 |
2/2✓ Branch 53 → 3 taken 213158 times.
✓ Branch 53 → 54 taken 14837 times.
|
227995 | for (qsizetype index = 0; index < input.size(); ++index) { |
| 25 | 213158 | const QChar ch = input.at(index); | |
| 26 |
7/8✓ Branch 4 → 5 taken 292 times.
✓ Branch 4 → 22 taken 212866 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 22 taken 289 times.
✓ Branch 10 → 11 taken 3 times.
✗ Branch 10 → 22 not taken.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 21 taken 1 time.
|
213163 | if (inQuotes && ch == QLatin1Char('\\') && index + 1 < input.size() && |
| 27 |
3/4✓ Branch 20 → 21 taken 2 times.
✗ Branch 20 → 22 not taken.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 23 → 27 taken 213155 times.
|
213163 | (input.at(index + 1) == QLatin1Char('\\') || input.at(index + 1) == QLatin1Char('"'))) { |
| 28 |
1/1✓ Branch 25 → 26 taken 3 times.
|
3678 | cur.append(input.at(++index)); hasToken = true; continue; |
| 29 | } | ||
| 30 |
2/2✓ Branch 30 → 31 taken 55 times.
✓ Branch 30 → 35 taken 213100 times.
|
213155 | if (ch == QLatin1Char('"')) { |
| 31 |
3/4✓ Branch 31 → 32 taken 7 times.
✓ Branch 31 → 34 taken 48 times.
✓ Branch 32 → 33 taken 7 times.
✗ Branch 32 → 34 not taken.
|
55 | if (!hasToken && !inQuotes) literal = true; |
| 32 | 55 | quoted = true; | |
| 33 | 55 | inQuotes = !inQuotes; | |
| 34 | 55 | hasToken = true; // an empty quoted string is still a (empty) token start | |
| 35 | 55 | continue; | |
| 36 | } | ||
| 37 |
6/6✓ Branch 36 → 37 taken 3648 times.
✓ Branch 36 → 39 taken 209452 times.
✓ Branch 37 → 38 taken 3620 times.
✓ Branch 37 → 39 taken 28 times.
✓ Branch 40 → 41 taken 3620 times.
✓ Branch 40 → 48 taken 209480 times.
|
213100 | if (ch.isSpace() && !inQuotes) { |
| 38 |
2/2✓ Branch 41 → 42 taken 3613 times.
✓ Branch 41 → 47 taken 7 times.
|
3620 | if (hasToken) { |
| 39 | 3613 | tokens.append({cur, literal, quoted}); | |
| 40 | 3613 | cur.clear(); | |
| 41 | 3613 | hasToken = false; | |
| 42 | 3613 | literal = false; | |
| 43 | 3613 | quoted = false; | |
| 44 | } | ||
| 45 | 3620 | continue; | |
| 46 | } | ||
| 47 |
1/1✓ Branch 48 → 49 taken 209480 times.
|
209480 | cur.append(ch); |
| 48 | 209480 | hasToken = true; | |
| 49 | } | ||
| 50 |
2/2✓ Branch 54 → 55 taken 14497 times.
✓ Branch 54 → 59 taken 340 times.
|
14837 | if (hasToken) |
| 51 | 14497 | tokens.append({cur, literal, quoted}); | |
| 52 | 14837 | return tokens; | |
| 53 | 32947 | } | |
| 54 | |||
| 55 | // The English preset word for a relative date value, or "" when the value | ||
| 56 | // is no preset. German words are keywords (SearchVocabulary.h) and map to | ||
| 57 | // their English word so the canonical query is language-independent. | ||
| 58 | 11602 | QString canonicalDatePreset(const QString &val) { | |
| 59 |
12/14✓ Branch 6 → 7 taken 11596 times.
✓ Branch 6 → 12 taken 6 times.
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 13 taken 11594 times.
✓ Branch 14 → 15 taken 11596 times.
✓ Branch 14 → 16 taken 6 times.
✓ Branch 16 → 17 taken 11596 times.
✓ Branch 16 → 18 taken 6 times.
✓ Branch 18 → 19 taken 11602 times.
✗ Branch 18 → 20 not taken.
✓ Branch 20 → 21 taken 11602 times.
✗ Branch 20 → 22 not taken.
✓ Branch 22 → 23 taken 8 times.
✓ Branch 22 → 28 taken 11594 times.
|
34808 | if (val == QStringLiteral("heute") || val == QStringLiteral("today")) return QStringLiteral("today"); |
| 60 |
12/14✓ Branch 32 → 33 taken 11592 times.
✓ Branch 32 → 38 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
✓ Branch 37 → 39 taken 11590 times.
✓ Branch 40 → 41 taken 11592 times.
✓ Branch 40 → 42 taken 2 times.
✓ Branch 42 → 43 taken 11592 times.
✓ Branch 42 → 44 taken 2 times.
✓ Branch 44 → 45 taken 11594 times.
✗ Branch 44 → 46 not taken.
✓ Branch 46 → 47 taken 11594 times.
✗ Branch 46 → 48 not taken.
✓ Branch 48 → 49 taken 4 times.
✓ Branch 48 → 54 taken 11590 times.
|
34784 | if (val == QStringLiteral("gestern") || val == QStringLiteral("yesterday")) return QStringLiteral("yesterday"); |
| 61 |
12/14✓ Branch 58 → 59 taken 11588 times.
✓ Branch 58 → 64 taken 2 times.
✓ Branch 63 → 64 taken 4 times.
✓ Branch 63 → 65 taken 11584 times.
✓ Branch 66 → 67 taken 11588 times.
✓ Branch 66 → 68 taken 2 times.
✓ Branch 68 → 69 taken 11588 times.
✓ Branch 68 → 70 taken 2 times.
✓ Branch 70 → 71 taken 11590 times.
✗ Branch 70 → 72 not taken.
✓ Branch 72 → 73 taken 11590 times.
✗ Branch 72 → 74 not taken.
✓ Branch 74 → 75 taken 6 times.
✓ Branch 74 → 80 taken 11584 times.
|
34774 | if (val == QStringLiteral("woche") || val == QStringLiteral("week")) return QStringLiteral("week"); |
| 62 |
12/14✓ Branch 84 → 85 taken 11580 times.
✓ Branch 84 → 90 taken 4 times.
✓ Branch 89 → 90 taken 20 times.
✓ Branch 89 → 91 taken 11560 times.
✓ Branch 92 → 93 taken 11580 times.
✓ Branch 92 → 94 taken 4 times.
✓ Branch 94 → 95 taken 11580 times.
✓ Branch 94 → 96 taken 4 times.
✓ Branch 96 → 97 taken 11584 times.
✗ Branch 96 → 98 not taken.
✓ Branch 98 → 99 taken 11584 times.
✗ Branch 98 → 100 not taken.
✓ Branch 100 → 101 taken 24 times.
✓ Branch 100 → 106 taken 11560 times.
|
34772 | if (val == QStringLiteral("monat") || val == QStringLiteral("month")) return QStringLiteral("month"); |
| 63 |
12/14✓ Branch 110 → 111 taken 11558 times.
✓ Branch 110 → 116 taken 2 times.
✓ Branch 115 → 116 taken 2 times.
✓ Branch 115 → 117 taken 11556 times.
✓ Branch 118 → 119 taken 11558 times.
✓ Branch 118 → 120 taken 2 times.
✓ Branch 120 → 121 taken 11558 times.
✓ Branch 120 → 122 taken 2 times.
✓ Branch 122 → 123 taken 11560 times.
✗ Branch 122 → 124 not taken.
✓ Branch 124 → 125 taken 11560 times.
✗ Branch 124 → 126 not taken.
✓ Branch 126 → 127 taken 4 times.
✓ Branch 126 → 132 taken 11556 times.
|
34682 | if (val == QStringLiteral("jahr") || val == QStringLiteral("year")) return QStringLiteral("year"); |
| 64 | 11556 | return {}; | |
| 65 | } | ||
| 66 | |||
| 67 | // F-56 (SPRINT-104): ISO first, then the system locale's short date form | ||
| 68 | // (01.03.2025 on a German system) — a local-format date no longer fails | ||
| 69 | // to parse. | ||
| 70 | 7012 | QDate parseFlexibleDate(const QString &value) { | |
| 71 |
1/1✓ Branch 2 → 3 taken 7012 times.
|
7012 | QDate d = QDate::fromString(value, Qt::ISODate); |
| 72 |
3/3✓ Branch 3 → 4 taken 7012 times.
✓ Branch 4 → 5 taken 1015 times.
✓ Branch 4 → 9 taken 5997 times.
|
7012 | if (!d.isValid()) |
| 73 |
2/2✓ Branch 5 → 6 taken 1015 times.
✓ Branch 6 → 7 taken 1015 times.
|
1015 | d = QLocale().toDate(value, QLocale::ShortFormat); |
| 74 | // Numeric forms the locale may not cover: the German short format is | ||
| 75 | // dd.MM.yy, so the commonly typed 01.03.2025 needs its own pattern; the | ||
| 76 | // dotted, dashed and slashed variants cover the European and US orders. | ||
| 77 |
3/3✓ Branch 9 → 10 taken 7012 times.
✓ Branch 10 → 11 taken 1015 times.
✓ Branch 10 → 18 taken 5997 times.
|
7012 | if (!d.isValid()) |
| 78 |
1/1✓ Branch 14 → 15 taken 1015 times.
|
1015 | d = QDate::fromString(value, QStringLiteral("d.M.yyyy")); |
| 79 |
3/3✓ Branch 18 → 19 taken 7012 times.
✓ Branch 19 → 20 taken 1012 times.
✓ Branch 19 → 27 taken 6000 times.
|
7012 | if (!d.isValid()) |
| 80 |
1/1✓ Branch 23 → 24 taken 1012 times.
|
1012 | d = QDate::fromString(value, QStringLiteral("d-M-yyyy")); |
| 81 |
3/3✓ Branch 27 → 28 taken 7012 times.
✓ Branch 28 → 29 taken 1012 times.
✓ Branch 28 → 36 taken 6000 times.
|
7012 | if (!d.isValid()) |
| 82 |
1/1✓ Branch 32 → 33 taken 1012 times.
|
1012 | d = QDate::fromString(value, QStringLiteral("d/M/yyyy")); |
| 83 |
3/3✓ Branch 36 → 37 taken 7012 times.
✓ Branch 37 → 38 taken 1012 times.
✓ Branch 37 → 45 taken 6000 times.
|
7012 | if (!d.isValid()) |
| 84 |
1/1✓ Branch 41 → 42 taken 1012 times.
|
1012 | d = QDate::fromString(value, QStringLiteral("M/d/yyyy")); |
| 85 |
3/3✓ Branch 45 → 46 taken 7012 times.
✓ Branch 46 → 47 taken 1012 times.
✓ Branch 46 → 54 taken 6000 times.
|
7012 | if (!d.isValid()) |
| 86 |
1/1✓ Branch 50 → 51 taken 1012 times.
|
1012 | d = QDate::fromString(value, QStringLiteral("d.M.yy")); |
| 87 | 7012 | return d; | |
| 88 | } | ||
| 89 | |||
| 90 | // A "date:" year ("2025"): four digits in (1900, 2100), else 0. Shared by | ||
| 91 | // single values and range ends, so "-210" or "+999" never becomes a year | ||
| 92 | // whose canonical form cannot be read back. | ||
| 93 | 309 | int searchYear(const QString &value) { | |
| 94 |
2/2✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 5 taken 298 times.
|
309 | if (value.length() != 4) |
| 95 | 11 | return 0; | |
| 96 |
2/2✓ Branch 22 → 7 taken 1180 times.
✓ Branch 22 → 23 taken 294 times.
|
1474 | for (const QChar ch : value) |
| 97 |
6/6✓ Branch 11 → 12 taken 1178 times.
✓ Branch 11 → 17 taken 2 times.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 18 taken 1176 times.
✓ Branch 19 → 20 taken 4 times.
✓ Branch 19 → 21 taken 1176 times.
|
1180 | if (ch < QLatin1Char('0') || ch > QLatin1Char('9')) |
| 98 | 4 | return 0; | |
| 99 | 294 | const int year = value.toInt(); | |
| 100 |
4/4✓ Branch 24 → 25 taken 291 times.
✓ Branch 24 → 27 taken 3 times.
✓ Branch 25 → 26 taken 290 times.
✓ Branch 25 → 27 taken 1 time.
|
294 | return year > 1900 && year < 2100 ? year : 0; |
| 101 | } | ||
| 102 | |||
| 103 | // Parse a "date:" value into an inclusive [from, to] range. Supports presets | ||
| 104 | // (heute/gestern/woche/monat/jahr and today/yesterday/week/month/year), ISO | ||
| 105 | // and local short dates (year/month/day) and "A..B" | ||
| 106 | // ranges with open ends. | ||
| 107 | 6232 | QPair<QDateTime, QDateTime> parseDateValue(const QString &val) { | |
| 108 | 6232 | QDateTime from, to; | |
| 109 |
1/1✓ Branch 4 → 5 taken 6232 times.
|
6232 | const QDate today = QDate::currentDate(); |
| 110 | |||
| 111 |
1/1✓ Branch 8 → 9 taken 6232 times.
|
6232 | const int dotdot = val.indexOf(QStringLiteral("..")); |
| 112 |
2/2✓ Branch 11 → 12 taken 834 times.
✓ Branch 11 → 84 taken 5398 times.
|
6232 | if (dotdot >= 0) { |
| 113 |
1/1✓ Branch 12 → 13 taken 834 times.
|
834 | const QString a = val.left(dotdot); |
| 114 |
1/1✓ Branch 13 → 14 taken 834 times.
|
834 | const QString b = val.mid(dotdot + 2); |
| 115 |
2/2✓ Branch 15 → 16 taken 820 times.
✓ Branch 15 → 46 taken 14 times.
|
834 | if (!a.isEmpty()) { |
| 116 |
1/1✓ Branch 16 → 17 taken 820 times.
|
820 | QDate d = parseFlexibleDate(a); |
| 117 |
7/7✓ Branch 17 → 18 taken 820 times.
✓ Branch 18 → 19 taken 26 times.
✓ Branch 18 → 22 taken 794 times.
✓ Branch 20 → 21 taken 11 times.
✓ Branch 20 → 22 taken 15 times.
✓ Branch 23 → 24 taken 11 times.
✓ Branch 23 → 28 taken 809 times.
|
820 | if (!d.isValid() && a.length() == 7) // "2025-02" |
| 118 |
2/2✓ Branch 24 → 25 taken 11 times.
✓ Branch 25 → 26 taken 11 times.
|
11 | d = QDate::fromString(a + "-01", Qt::ISODate); |
| 119 |
8/8✓ Branch 28 → 29 taken 820 times.
✓ Branch 29 → 30 taken 16 times.
✓ Branch 29 → 33 taken 804 times.
✓ Branch 30 → 31 taken 16 times.
✓ Branch 31 → 32 taken 12 times.
✓ Branch 31 → 33 taken 4 times.
✓ Branch 34 → 35 taken 12 times.
✓ Branch 34 → 38 taken 808 times.
|
820 | if (!d.isValid() && searchYear(a) > 0) // "2025" |
| 120 |
2/2✓ Branch 35 → 36 taken 12 times.
✓ Branch 36 → 37 taken 12 times.
|
12 | d = QDate(searchYear(a), 1, 1); |
| 121 |
3/3✓ Branch 38 → 39 taken 820 times.
✓ Branch 39 → 40 taken 816 times.
✓ Branch 39 → 45 taken 4 times.
|
820 | if (d.isValid()) |
| 122 |
2/2✓ Branch 40 → 41 taken 816 times.
✓ Branch 41 → 42 taken 816 times.
|
816 | from = QDateTime(d, QTime(0, 0)); |
| 123 | } | ||
| 124 |
2/2✓ Branch 47 → 48 taken 817 times.
✓ Branch 47 → 80 taken 17 times.
|
834 | if (!b.isEmpty()) { |
| 125 |
1/1✓ Branch 48 → 49 taken 817 times.
|
817 | QDate d = parseFlexibleDate(b); |
| 126 |
7/7✓ Branch 49 → 50 taken 817 times.
✓ Branch 50 → 51 taken 18 times.
✓ Branch 50 → 54 taken 799 times.
✓ Branch 52 → 53 taken 12 times.
✓ Branch 52 → 54 taken 6 times.
✓ Branch 55 → 56 taken 12 times.
✓ Branch 55 → 62 taken 805 times.
|
817 | if (!d.isValid() && b.length() == 7) |
| 127 |
4/4✓ Branch 56 → 57 taken 12 times.
✓ Branch 57 → 58 taken 12 times.
✓ Branch 58 → 59 taken 12 times.
✓ Branch 59 → 60 taken 12 times.
|
12 | d = QDate::fromString(b + "-01", Qt::ISODate).addMonths(1).addDays(-1); |
| 128 |
8/8✓ Branch 62 → 63 taken 817 times.
✓ Branch 63 → 64 taken 7 times.
✓ Branch 63 → 67 taken 810 times.
✓ Branch 64 → 65 taken 7 times.
✓ Branch 65 → 66 taken 1 time.
✓ Branch 65 → 67 taken 6 times.
✓ Branch 68 → 69 taken 1 time.
✓ Branch 68 → 72 taken 816 times.
|
817 | if (!d.isValid() && searchYear(b) > 0) |
| 129 |
2/2✓ Branch 69 → 70 taken 1 time.
✓ Branch 70 → 71 taken 1 time.
|
1 | d = QDate(searchYear(b), 12, 31); |
| 130 |
3/3✓ Branch 72 → 73 taken 817 times.
✓ Branch 73 → 74 taken 811 times.
✓ Branch 73 → 79 taken 6 times.
|
817 | if (d.isValid()) |
| 131 |
2/2✓ Branch 74 → 75 taken 811 times.
✓ Branch 75 → 76 taken 811 times.
|
811 | to = QDateTime(d, QTime(23, 59, 59)); |
| 132 | } | ||
| 133 | 834 | return {from, to}; | |
| 134 | 834 | } | |
| 135 | |||
| 136 | // Presets in German and English (Sprint 98: SearchVocabulary.h lists the | ||
| 137 | // English words; the German ones are its keywords and stay accepted). | ||
| 138 |
1/1✓ Branch 84 → 85 taken 5398 times.
|
5398 | const QString preset = canonicalDatePreset(val); |
| 139 |
2/2✓ Branch 91 → 92 taken 4 times.
✓ Branch 91 → 101 taken 5394 times.
|
5398 | if (preset == QStringLiteral("today")) { |
| 140 |
2/2✓ Branch 92 → 93 taken 4 times.
✓ Branch 93 → 94 taken 4 times.
|
4 | from = QDateTime(today, QTime(0, 0)); |
| 141 |
2/2✓ Branch 96 → 97 taken 4 times.
✓ Branch 97 → 98 taken 4 times.
|
4 | to = QDateTime(today, QTime(23, 59, 59)); |
| 142 |
2/2✓ Branch 107 → 108 taken 2 times.
✓ Branch 107 → 118 taken 5392 times.
|
5394 | } else if (preset == QStringLiteral("yesterday")) { |
| 143 |
1/1✓ Branch 108 → 109 taken 2 times.
|
2 | const QDate y = today.addDays(-1); |
| 144 |
2/2✓ Branch 109 → 110 taken 2 times.
✓ Branch 110 → 111 taken 2 times.
|
2 | from = QDateTime(y, QTime(0, 0)); |
| 145 |
2/2✓ Branch 113 → 114 taken 2 times.
✓ Branch 114 → 115 taken 2 times.
|
2 | to = QDateTime(y, QTime(23, 59, 59)); |
| 146 |
2/2✓ Branch 124 → 125 taken 3 times.
✓ Branch 124 → 135 taken 5389 times.
|
5392 | } else if (preset == QStringLiteral("week")) { |
| 147 |
3/3✓ Branch 125 → 126 taken 3 times.
✓ Branch 126 → 127 taken 3 times.
✓ Branch 127 → 128 taken 3 times.
|
3 | from = QDateTime(today.addDays(-7), QTime(0, 0)); |
| 148 |
2/2✓ Branch 130 → 131 taken 3 times.
✓ Branch 131 → 132 taken 3 times.
|
3 | to = QDateTime(today, QTime(23, 59, 59)); |
| 149 |
2/2✓ Branch 141 → 142 taken 12 times.
✓ Branch 141 → 152 taken 5377 times.
|
5389 | } else if (preset == QStringLiteral("month")) { |
| 150 |
3/3✓ Branch 142 → 143 taken 12 times.
✓ Branch 143 → 144 taken 12 times.
✓ Branch 144 → 145 taken 12 times.
|
12 | from = QDateTime(today.addDays(-30), QTime(0, 0)); |
| 151 |
2/2✓ Branch 147 → 148 taken 12 times.
✓ Branch 148 → 149 taken 12 times.
|
12 | to = QDateTime(today, QTime(23, 59, 59)); |
| 152 |
2/2✓ Branch 158 → 159 taken 2 times.
✓ Branch 158 → 169 taken 5375 times.
|
5377 | } else if (preset == QStringLiteral("year")) { |
| 153 |
3/3✓ Branch 159 → 160 taken 2 times.
✓ Branch 160 → 161 taken 2 times.
✓ Branch 161 → 162 taken 2 times.
|
2 | from = QDateTime(today.addYears(-1), QTime(0, 0)); |
| 154 |
2/2✓ Branch 164 → 165 taken 2 times.
✓ Branch 165 → 166 taken 2 times.
|
2 | to = QDateTime(today, QTime(23, 59, 59)); |
| 155 | } else { | ||
| 156 |
1/1✓ Branch 169 → 170 taken 5375 times.
|
5375 | QDate d = parseFlexibleDate(val); |
| 157 |
3/3✓ Branch 170 → 171 taken 5375 times.
✓ Branch 171 → 172 taken 4407 times.
✓ Branch 171 → 181 taken 968 times.
|
5375 | if (d.isValid()) { |
| 158 |
2/2✓ Branch 172 → 173 taken 4407 times.
✓ Branch 173 → 174 taken 4407 times.
|
4407 | from = QDateTime(d, QTime(0, 0)); |
| 159 |
2/2✓ Branch 176 → 177 taken 4407 times.
✓ Branch 177 → 178 taken 4407 times.
|
4407 | to = QDateTime(d, QTime(23, 59, 59)); |
| 160 |
2/2✓ Branch 182 → 183 taken 695 times.
✓ Branch 182 → 200 taken 273 times.
|
968 | } else if (val.length() == 7) { // "2025-02" |
| 161 |
2/2✓ Branch 183 → 184 taken 695 times.
✓ Branch 184 → 185 taken 695 times.
|
695 | const QDate start = QDate::fromString(val + "-01", Qt::ISODate); |
| 162 |
3/3✓ Branch 186 → 187 taken 695 times.
✓ Branch 187 → 188 taken 694 times.
✓ Branch 187 → 199 taken 1 time.
|
695 | if (start.isValid()) { |
| 163 |
2/2✓ Branch 188 → 189 taken 694 times.
✓ Branch 189 → 190 taken 694 times.
|
694 | from = QDateTime(start, QTime(0, 0)); |
| 164 |
4/4✓ Branch 192 → 193 taken 694 times.
✓ Branch 193 → 194 taken 694 times.
✓ Branch 194 → 195 taken 694 times.
✓ Branch 195 → 196 taken 694 times.
|
694 | to = QDateTime(start.addMonths(1).addDays(-1), QTime(23, 59, 59)); |
| 165 | } | ||
| 166 |
3/3✓ Branch 200 → 201 taken 273 times.
✓ Branch 201 → 202 taken 264 times.
✓ Branch 201 → 213 taken 9 times.
|
273 | } else if (const int year = searchYear(val); year > 0) { // "2025" |
| 167 |
3/3✓ Branch 202 → 203 taken 264 times.
✓ Branch 203 → 204 taken 264 times.
✓ Branch 204 → 205 taken 264 times.
|
264 | from = QDateTime(QDate(year, 1, 1), QTime(0, 0)); |
| 168 |
3/3✓ Branch 207 → 208 taken 264 times.
✓ Branch 208 → 209 taken 264 times.
✓ Branch 209 → 210 taken 264 times.
|
264 | to = QDateTime(QDate(year, 12, 31), QTime(23, 59, 59)); |
| 169 | } | ||
| 170 | } | ||
| 171 | 5398 | return {from, to}; | |
| 172 | 6232 | } | |
| 173 | |||
| 174 | // Quote a value for serialization if it contains whitespace (or is empty), so | ||
| 175 | // it survives tokenization on the way back in. | ||
| 176 | 325 | QString quoteIfNeeded(const QString &value) { | |
| 177 | 325 | bool needsQuote = value.isEmpty(); | |
| 178 |
2/2✓ Branch 16 → 5 taken 2493 times.
✓ Branch 16 → 17 taken 308 times.
|
2801 | for (const QChar ch : value) { |
| 179 |
5/6✓ Branch 6 → 7 taken 2476 times.
✓ Branch 6 → 11 taken 17 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 2476 times.
✓ Branch 13 → 14 taken 17 times.
✓ Branch 13 → 15 taken 2476 times.
|
2493 | if (ch.isSpace() || ch == QLatin1Char('"')) { |
| 180 | 17 | needsQuote = true; | |
| 181 | 17 | break; | |
| 182 | } | ||
| 183 | } | ||
| 184 |
2/2✓ Branch 17 → 18 taken 306 times.
✓ Branch 17 → 19 taken 19 times.
|
325 | if (!needsQuote) |
| 185 | 306 | return value; | |
| 186 | 19 | QString escaped = value; | |
| 187 |
1/1✓ Branch 25 → 26 taken 19 times.
|
19 | escaped.replace(QLatin1Char('\\'), QStringLiteral("\\\\")); |
| 188 |
1/1✓ Branch 33 → 34 taken 19 times.
|
19 | escaped.replace(QLatin1Char('"'), QStringLiteral("\\\"")); |
| 189 |
2/2✓ Branch 40 → 41 taken 19 times.
✓ Branch 41 → 42 taken 19 times.
|
38 | return QLatin1Char('"') + escaped + QLatin1Char('"'); |
| 190 | 19 | } | |
| 191 | |||
| 192 | 110958 | bool startsWithCi(const QString &s, const QString &prefix) { | |
| 193 | 110958 | return s.startsWith(prefix, Qt::CaseInsensitive); | |
| 194 | } | ||
| 195 | |||
| 196 | } // namespace | ||
| 197 | |||
| 198 | |||
| 199 | namespace { | ||
| 200 | |||
| 201 | // is:/has: values. Returns false for an unknown value (kept as free text). | ||
| 202 | 802 | bool applyState(const QString &prefix, const QString &value, bool negated, | |
| 203 | MailCache::SearchFilter &filter) { | ||
| 204 |
2/2✓ Branch 2 → 3 taken 706 times.
✓ Branch 2 → 4 taken 89 times.
|
1597 | const auto set = [negated](Tri &field, bool yes) { field = (yes != negated) ? Tri::Yes : Tri::No; }; |
| 205 |
2/2✓ Branch 8 → 9 taken 586 times.
✓ Branch 8 → 87 taken 216 times.
|
802 | if (prefix == QStringLiteral("is:")) { |
| 206 |
2/2✓ Branch 15 → 16 taken 430 times.
✓ Branch 15 → 17 taken 156 times.
|
586 | if (value == QStringLiteral("unread")) set(filter.unread, true); |
| 207 |
2/2✓ Branch 23 → 24 taken 19 times.
✓ Branch 23 → 25 taken 137 times.
|
156 | else if (value == QStringLiteral("read")) set(filter.unread, false); |
| 208 |
12/14✓ Branch 29 → 30 taken 134 times.
✓ Branch 29 → 35 taken 3 times.
✓ Branch 34 → 35 taken 65 times.
✓ Branch 34 → 36 taken 69 times.
✓ Branch 37 → 38 taken 134 times.
✓ Branch 37 → 39 taken 3 times.
✓ Branch 39 → 40 taken 134 times.
✓ Branch 39 → 41 taken 3 times.
✓ Branch 41 → 42 taken 137 times.
✗ Branch 41 → 43 not taken.
✓ Branch 43 → 44 taken 137 times.
✗ Branch 43 → 45 not taken.
✓ Branch 45 → 46 taken 68 times.
✓ Branch 45 → 47 taken 69 times.
|
408 | else if (value == QStringLiteral("starred") || value == QStringLiteral("flagged")) set(filter.flagged, true); |
| 209 |
12/14✓ Branch 51 → 52 taken 57 times.
✓ Branch 51 → 57 taken 12 times.
✓ Branch 56 → 57 taken 13 times.
✓ Branch 56 → 58 taken 44 times.
✓ Branch 59 → 60 taken 57 times.
✓ Branch 59 → 61 taken 12 times.
✓ Branch 61 → 62 taken 57 times.
✓ Branch 61 → 63 taken 12 times.
✓ Branch 63 → 64 taken 69 times.
✗ Branch 63 → 65 not taken.
✓ Branch 65 → 66 taken 69 times.
✗ Branch 65 → 67 not taken.
✓ Branch 67 → 68 taken 25 times.
✓ Branch 67 → 69 taken 44 times.
|
195 | else if (value == QStringLiteral("unstarred") || value == QStringLiteral("unflagged")) set(filter.flagged, false); |
| 210 |
2/2✓ Branch 75 → 76 taken 25 times.
✓ Branch 75 → 77 taken 19 times.
|
44 | else if (value == QStringLiteral("answered")) set(filter.answered, true); |
| 211 |
2/2✓ Branch 83 → 84 taken 15 times.
✓ Branch 83 → 85 taken 4 times.
|
19 | else if (value == QStringLiteral("unanswered")) set(filter.answered, false); |
| 212 | 4 | else return false; | |
| 213 | 582 | return true; | |
| 214 | } | ||
| 215 |
12/14✓ Branch 91 → 92 taken 24 times.
✓ Branch 91 → 97 taken 192 times.
✓ Branch 96 → 97 taken 2 times.
✓ Branch 96 → 98 taken 22 times.
✓ Branch 99 → 100 taken 24 times.
✓ Branch 99 → 101 taken 192 times.
✓ Branch 101 → 102 taken 24 times.
✓ Branch 101 → 103 taken 192 times.
✓ Branch 103 → 104 taken 216 times.
✗ Branch 103 → 105 not taken.
✓ Branch 105 → 106 taken 216 times.
✗ Branch 105 → 107 not taken.
✓ Branch 107 → 108 taken 194 times.
✓ Branch 107 → 109 taken 22 times.
|
456 | if (value == QStringLiteral("attachment") || value == QStringLiteral("attachments")) set(filter.hasAttachment, true); |
| 216 |
12/14✓ Branch 113 → 114 taken 5 times.
✓ Branch 113 → 119 taken 17 times.
✓ Branch 118 → 119 taken 2 times.
✓ Branch 118 → 120 taken 3 times.
✓ Branch 121 → 122 taken 5 times.
✓ Branch 121 → 123 taken 17 times.
✓ Branch 123 → 124 taken 5 times.
✓ Branch 123 → 125 taken 17 times.
✓ Branch 125 → 126 taken 22 times.
✗ Branch 125 → 127 not taken.
✓ Branch 127 → 128 taken 22 times.
✗ Branch 127 → 129 not taken.
✓ Branch 129 → 130 taken 19 times.
✓ Branch 129 → 131 taken 3 times.
|
49 | else if (value == QStringLiteral("noattachment") || value == QStringLiteral("noattachments")) set(filter.hasAttachment, false); |
| 217 | 3 | else return false; | |
| 218 | 213 | return true; | |
| 219 | } | ||
| 220 | |||
| 221 | // A "prefix:value" token that constrains the search. Returns false when the | ||
| 222 | // token names no known prefix (the caller keeps it as free text). | ||
| 223 | 18102 | bool applyPositive(const QString &token, MailCache::SearchFilter &filter) { | |
| 224 | 28695 | const auto valueAfter = [&token](int length) { return token.mid(length); }; | |
| 225 |
3/3✓ Branch 5 → 6 taken 18102 times.
✓ Branch 8 → 9 taken 339 times.
✓ Branch 8 → 17 taken 17763 times.
|
18102 | if (startsWithCi(token, QStringLiteral("folder:"))) { |
| 226 | // Sprint 60 (Q1): repeatable — every folder: token (OR-semantics). | ||
| 227 |
5/5✓ Branch 9 → 10 taken 339 times.
✓ Branch 12 → 13 taken 338 times.
✓ Branch 12 → 177 taken 1 time.
✓ Branch 13 → 14 taken 338 times.
✓ Branch 14 → 15 taken 338 times.
|
339 | if (!valueAfter(7).isEmpty()) filter.folderPatterns.append(valueAfter(7)); |
| 228 |
3/3✓ Branch 20 → 21 taken 17763 times.
✓ Branch 23 → 24 taken 6232 times.
✓ Branch 23 → 56 taken 11531 times.
|
17763 | } else if (startsWithCi(token, QStringLiteral("date:"))) { |
| 229 |
2/2✓ Branch 24 → 25 taken 6232 times.
✓ Branch 25 → 26 taken 6232 times.
|
6232 | const QString value = valueAfter(5).toLower(); |
| 230 |
1/1✓ Branch 27 → 28 taken 6232 times.
|
6232 | const auto [from, to] = parseDateValue(value); |
| 231 |
8/8✓ Branch 30 → 31 taken 6232 times.
✓ Branch 31 → 32 taken 28 times.
✓ Branch 31 → 35 taken 6204 times.
✓ Branch 32 → 33 taken 28 times.
✓ Branch 33 → 34 taken 15 times.
✓ Branch 33 → 35 taken 13 times.
✓ Branch 36 → 37 taken 15 times.
✓ Branch 36 → 38 taken 6217 times.
|
6232 | if (!from.isValid() && !to.isValid()) { |
| 232 | // F-56 (SPRINT-104): an unreadable value must not silently widen the | ||
| 233 | // search — it stays in the query as visible free text (like unknown | ||
| 234 | // prefixes), so the user sees the limit is not applied. | ||
| 235 | 15 | return false; | |
| 236 | } | ||
| 237 | 6217 | filter.dateFrom = from; | |
| 238 | 6217 | filter.dateTo = to; | |
| 239 |
4/4✓ Branch 40 → 41 taken 6217 times.
✓ Branch 41 → 42 taken 6204 times.
✓ Branch 41 → 43 taken 13 times.
✓ Branch 42 → 44 taken 6204 times.
|
6217 | filter.datePreset = from.isValid() ? canonicalDatePreset(value) : QString(); |
| 240 |
7/7✓ Branch 48 → 49 taken 6217 times.
✓ Branch 48 → 50 taken 15 times.
✓ Branch 52 → 53 taken 6217 times.
✓ Branch 52 → 55 taken 15 times.
✓ Branch 59 → 60 taken 11531 times.
✓ Branch 62 → 63 taken 3327 times.
✓ Branch 62 → 67 taken 8204 times.
|
17778 | } else if (startsWithCi(token, QStringLiteral("from:"))) { |
| 241 |
1/1✓ Branch 63 → 64 taken 3327 times.
|
3327 | filter.fromFilter = valueAfter(5); |
| 242 |
3/3✓ Branch 70 → 71 taken 8204 times.
✓ Branch 73 → 74 taken 16 times.
✓ Branch 73 → 78 taken 8188 times.
|
8204 | } else if (startsWithCi(token, QStringLiteral("to:"))) { |
| 243 |
1/1✓ Branch 74 → 75 taken 16 times.
|
16 | filter.toFilter = valueAfter(3); |
| 244 |
3/3✓ Branch 81 → 82 taken 8188 times.
✓ Branch 84 → 85 taken 50 times.
✓ Branch 84 → 89 taken 8138 times.
|
8188 | } else if (startsWithCi(token, QStringLiteral("list:"))) { |
| 245 |
1/1✓ Branch 85 → 86 taken 50 times.
|
50 | filter.listFilter = valueAfter(5); // Sprint 95: List-Id contains |
| 246 |
3/3✓ Branch 92 → 93 taken 8138 times.
✓ Branch 95 → 96 taken 7 times.
✓ Branch 95 → 100 taken 8131 times.
|
8138 | } else if (startsWithCi(token, QStringLiteral("about:"))) { |
| 247 |
1/1✓ Branch 96 → 97 taken 7 times.
|
7 | filter.aboutFilter = valueAfter(6); // Sprint 103 D-21: mails linked to a contact |
| 248 |
3/3✓ Branch 103 → 104 taken 8131 times.
✓ Branch 106 → 107 taken 190 times.
✓ Branch 106 → 111 taken 7941 times.
|
8131 | } else if (startsWithCi(token, QStringLiteral("subject:"))) { |
| 249 |
1/1✓ Branch 107 → 108 taken 190 times.
|
190 | filter.subjectFilter = valueAfter(8); |
| 250 |
14/24✓ Branch 114 → 115 taken 7941 times.
✓ Branch 115 → 116 taken 7851 times.
✓ Branch 115 → 121 taken 90 times.
✓ Branch 119 → 120 taken 7851 times.
✓ Branch 120 → 121 taken 4 times.
✓ Branch 120 → 122 taken 7847 times.
✓ Branch 123 → 124 taken 7851 times.
✓ Branch 123 → 125 taken 90 times.
✓ Branch 125 → 126 taken 7851 times.
✓ Branch 125 → 127 taken 90 times.
✓ Branch 127 → 128 taken 7941 times.
✗ Branch 127 → 129 not taken.
✓ Branch 129 → 130 taken 7941 times.
✗ Branch 129 → 131 not taken.
✓ Branch 131 → 132 taken 94 times.
✓ Branch 131 → 141 taken 7847 times.
✗ Branch 240 → 241 not taken.
✗ Branch 240 → 242 not taken.
✗ Branch 244 → 245 not taken.
✗ Branch 244 → 246 not taken.
✗ Branch 248 → 249 not taken.
✗ Branch 248 → 250 not taken.
✗ Branch 252 → 253 not taken.
✗ Branch 252 → 254 not taken.
|
23733 | } else if (startsWithCi(token, QStringLiteral("tag:")) || startsWithCi(token, QStringLiteral("label:"))) { |
| 251 |
1/1✓ Branch 135 → 136 taken 94 times.
|
94 | const QString value = valueAfter(token.indexOf(QLatin1Char(':')) + 1); |
| 252 |
3/3✓ Branch 137 → 138 taken 89 times.
✓ Branch 137 → 139 taken 5 times.
✓ Branch 138 → 139 taken 89 times.
|
94 | if (!value.isEmpty()) filter.tags.append(value); |
| 253 |
14/24✓ Branch 144 → 145 taken 7847 times.
✓ Branch 145 → 146 taken 7262 times.
✓ Branch 145 → 151 taken 585 times.
✓ Branch 149 → 150 taken 7262 times.
✓ Branch 150 → 151 taken 206 times.
✓ Branch 150 → 152 taken 7056 times.
✓ Branch 153 → 154 taken 7262 times.
✓ Branch 153 → 155 taken 585 times.
✓ Branch 155 → 156 taken 7262 times.
✓ Branch 155 → 157 taken 585 times.
✓ Branch 157 → 158 taken 7847 times.
✗ Branch 157 → 159 not taken.
✓ Branch 159 → 160 taken 7847 times.
✗ Branch 159 → 161 not taken.
✓ Branch 161 → 162 taken 791 times.
✓ Branch 161 → 176 taken 7056 times.
✗ Branch 261 → 262 not taken.
✗ Branch 261 → 263 not taken.
✗ Branch 265 → 266 not taken.
✗ Branch 265 → 267 not taken.
✗ Branch 269 → 270 not taken.
✗ Branch 269 → 271 not taken.
✗ Branch 273 → 274 not taken.
✗ Branch 273 → 275 not taken.
|
23050 | } else if (startsWithCi(token, QStringLiteral("is:")) || startsWithCi(token, QStringLiteral("has:"))) { |
| 254 | 791 | const int colon = token.indexOf(QLatin1Char(':')); | |
| 255 |
5/5✓ Branch 165 → 166 taken 791 times.
✓ Branch 166 → 167 taken 791 times.
✓ Branch 167 → 168 taken 791 times.
✓ Branch 168 → 169 taken 791 times.
✓ Branch 169 → 170 taken 791 times.
|
791 | return applyState(token.left(colon + 1).toLower(), token.mid(colon + 1).toLower(), false, filter); |
| 256 | } else { | ||
| 257 | 7056 | return false; | |
| 258 | } | ||
| 259 | 10240 | return true; | |
| 260 | } | ||
| 261 | |||
| 262 | // S101.1.8: "-prefix:value" and "-word". Returns false when the token is no | ||
| 263 | // negation (the caller keeps it as free text). | ||
| 264 | 7078 | bool applyNegated(const QString &token, MailCache::SearchFilter &filter) { | |
| 265 |
8/8✓ Branch 3 → 4 taken 7033 times.
✓ Branch 3 → 12 taken 45 times.
✓ Branch 8 → 9 taken 375 times.
✓ Branch 8 → 12 taken 6658 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 374 times.
✓ Branch 14 → 15 taken 6704 times.
✓ Branch 14 → 16 taken 374 times.
|
7078 | if (token.size() < 2 || token.at(0) != QLatin1Char('-') || !token.at(1).isLetterOrNumber()) return false; |
| 266 |
1/1✓ Branch 16 → 17 taken 374 times.
|
374 | const QString rest = token.mid(1); |
| 267 | 374 | const int colon = rest.indexOf(QLatin1Char(':')); | |
| 268 |
6/8✓ Branch 20 → 21 taken 330 times.
✓ Branch 20 → 23 taken 44 times.
✓ Branch 21 → 22 taken 330 times.
✓ Branch 22 → 24 taken 330 times.
✓ Branch 24 → 25 taken 330 times.
✓ Branch 24 → 26 taken 44 times.
✗ Branch 153 → 154 not taken.
✗ Branch 153 → 155 not taken.
|
374 | const QString prefix = colon > 0 ? rest.left(colon + 1).toLower() : QString(); |
| 269 |
3/3✓ Branch 26 → 27 taken 330 times.
✓ Branch 26 → 28 taken 44 times.
✓ Branch 27 → 29 taken 330 times.
|
374 | const QString value = colon > 0 ? rest.mid(colon + 1) : rest; |
| 270 |
4/6✓ Branch 29 → 30 taken 330 times.
✓ Branch 29 → 33 taken 44 times.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 330 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 374 times.
|
374 | if (colon > 0 && value.isEmpty()) return false; |
| 271 |
3/3✓ Branch 42 → 43 taken 66 times.
✓ Branch 42 → 44 taken 308 times.
✓ Branch 43 → 147 taken 66 times.
|
374 | if (prefix == QStringLiteral("from:")) filter.excludedFrom.append(value); |
| 272 |
3/3✓ Branch 50 → 51 taken 5 times.
✓ Branch 50 → 52 taken 303 times.
✓ Branch 51 → 147 taken 5 times.
|
308 | else if (prefix == QStringLiteral("to:")) filter.excludedTo.append(value); |
| 273 |
3/3✓ Branch 58 → 59 taken 4 times.
✓ Branch 58 → 60 taken 299 times.
✓ Branch 59 → 147 taken 4 times.
|
303 | else if (prefix == QStringLiteral("subject:")) filter.excludedSubjects.append(value); |
| 274 |
3/3✓ Branch 66 → 67 taken 218 times.
✓ Branch 66 → 68 taken 81 times.
✓ Branch 67 → 147 taken 218 times.
|
299 | else if (prefix == QStringLiteral("folder:")) filter.excludedFolders.append(value); |
| 275 |
3/3✓ Branch 74 → 75 taken 12 times.
✓ Branch 74 → 76 taken 69 times.
✓ Branch 75 → 147 taken 12 times.
|
81 | else if (prefix == QStringLiteral("list:")) filter.excludedLists.append(value); |
| 276 |
12/15✓ Branch 80 → 81 taken 56 times.
✓ Branch 80 → 86 taken 13 times.
✗ Branch 85 → 86 not taken.
✓ Branch 85 → 87 taken 56 times.
✓ Branch 88 → 89 taken 56 times.
✓ Branch 88 → 90 taken 13 times.
✓ Branch 90 → 91 taken 56 times.
✓ Branch 90 → 92 taken 13 times.
✓ Branch 92 → 93 taken 69 times.
✗ Branch 92 → 94 not taken.
✓ Branch 94 → 95 taken 69 times.
✗ Branch 94 → 96 not taken.
✓ Branch 96 → 97 taken 13 times.
✓ Branch 96 → 98 taken 56 times.
✓ Branch 97 → 147 taken 13 times.
|
194 | else if (prefix == QStringLiteral("tag:") || prefix == QStringLiteral("label:")) filter.excludedTags.append(value); |
| 277 |
14/16✓ Branch 102 → 103 taken 55 times.
✓ Branch 102 → 108 taken 1 time.
✓ Branch 107 → 108 taken 10 times.
✓ Branch 107 → 109 taken 45 times.
✓ Branch 110 → 111 taken 55 times.
✓ Branch 110 → 112 taken 1 time.
✓ Branch 112 → 113 taken 55 times.
✓ Branch 112 → 114 taken 1 time.
✓ Branch 114 → 115 taken 56 times.
✗ Branch 114 → 116 not taken.
✓ Branch 116 → 117 taken 56 times.
✗ Branch 116 → 118 not taken.
✓ Branch 118 → 119 taken 11 times.
✓ Branch 118 → 124 taken 45 times.
✓ Branch 119 → 120 taken 11 times.
✓ Branch 120 → 121 taken 11 times.
|
167 | else if (prefix == QStringLiteral("is:") || prefix == QStringLiteral("has:")) return applyState(prefix, value.toLower(), true, filter); |
| 278 |
11/14✓ Branch 128 → 129 taken 44 times.
✓ Branch 128 → 134 taken 1 time.
✗ Branch 133 → 134 not taken.
✓ Branch 133 → 135 taken 44 times.
✓ Branch 136 → 137 taken 44 times.
✓ Branch 136 → 138 taken 1 time.
✓ Branch 138 → 139 taken 44 times.
✓ Branch 138 → 140 taken 1 time.
✓ Branch 140 → 141 taken 45 times.
✗ Branch 140 → 142 not taken.
✓ Branch 142 → 143 taken 45 times.
✗ Branch 142 → 144 not taken.
✓ Branch 144 → 145 taken 1 time.
✓ Branch 144 → 146 taken 44 times.
|
134 | else if (prefix == QStringLiteral("date:") || prefix == QStringLiteral("about:")) return false; // kept as free text |
| 279 |
1/1✓ Branch 146 → 147 taken 44 times.
|
44 | else filter.excludedTerms.append(rest); |
| 280 | 362 | return true; | |
| 281 | 374 | } | |
| 282 | |||
| 283 | 5180 | void appendTri(QStringList &parts, Tri value, const char *yes, const char *no) { | |
| 284 |
4/4✓ Branch 2 → 3 taken 86 times.
✓ Branch 2 → 8 taken 5094 times.
✓ Branch 4 → 5 taken 86 times.
✓ Branch 5 → 6 taken 86 times.
|
5180 | if (value == Tri::Yes) parts.append(QString::fromLatin1(yes)); |
| 285 |
4/4✓ Branch 8 → 9 taken 14 times.
✓ Branch 8 → 14 taken 5080 times.
✓ Branch 10 → 11 taken 14 times.
✓ Branch 11 → 12 taken 14 times.
|
5094 | else if (value == Tri::No) parts.append(QString::fromLatin1(no)); |
| 286 | 5180 | } | |
| 287 | |||
| 288 | 10360 | void appendEach(QStringList &parts, const char *prefix, const QStringList &values, bool keepEmpty = false) { | |
| 289 |
2/2✓ Branch 22 → 4 taken 110 times.
✓ Branch 22 → 23 taken 10360 times.
|
10470 | for (const QString &value : values) |
| 290 |
10/10✓ Branch 5 → 6 taken 99 times.
✓ Branch 5 → 8 taken 11 times.
✓ Branch 7 → 8 taken 97 times.
✓ Branch 7 → 9 taken 2 times.
✓ Branch 10 → 11 taken 108 times.
✓ Branch 10 → 20 taken 2 times.
✓ Branch 11 → 12 taken 108 times.
✓ Branch 13 → 14 taken 108 times.
✓ Branch 14 → 15 taken 108 times.
✓ Branch 15 → 16 taken 108 times.
|
110 | if (keepEmpty || !value.isEmpty()) parts.append(QString::fromLatin1(prefix) + quoteIfNeeded(value)); |
| 291 | 10360 | } | |
| 292 | |||
| 293 | } // namespace | ||
| 294 | |||
| 295 | 5570 | QString canonicalDateToken(const QDate &from, const QDate &to) { | |
| 296 |
4/6✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 7 taken 5567 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 3 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 5570 times.
|
5570 | if (!from.isValid() && !to.isValid()) return {}; |
| 297 |
6/6✓ Branch 11 → 12 taken 5567 times.
✓ Branch 11 → 15 taken 3 times.
✓ Branch 13 → 14 taken 5559 times.
✓ Branch 13 → 15 taken 8 times.
✓ Branch 16 → 17 taken 5559 times.
✓ Branch 16 → 50 taken 11 times.
|
5570 | if (from.isValid() && to.isValid()) { |
| 298 |
2/2✓ Branch 18 → 19 taken 4387 times.
✓ Branch 18 → 20 taken 1172 times.
|
5559 | if (from == to) return from.toString(Qt::ISODate); |
| 299 |
9/9✓ Branch 20 → 21 taken 1172 times.
✓ Branch 21 → 22 taken 549 times.
✓ Branch 21 → 27 taken 623 times.
✓ Branch 22 → 23 taken 549 times.
✓ Branch 23 → 24 taken 549 times.
✓ Branch 25 → 26 taken 351 times.
✓ Branch 25 → 27 taken 198 times.
✓ Branch 28 → 29 taken 351 times.
✓ Branch 28 → 37 taken 821 times.
|
1172 | if (from.day() == 1 && to == from.addMonths(1).addDays(-1)) |
| 300 |
1/1✓ Branch 32 → 33 taken 351 times.
|
351 | return from.toString(QStringLiteral("yyyy-MM")); |
| 301 |
12/12✓ Branch 37 → 38 taken 821 times.
✓ Branch 38 → 39 taken 227 times.
✓ Branch 38 → 46 taken 594 times.
✓ Branch 39 → 40 taken 227 times.
✓ Branch 40 → 41 taken 175 times.
✓ Branch 40 → 46 taken 52 times.
✓ Branch 41 → 42 taken 175 times.
✓ Branch 42 → 43 taken 175 times.
✓ Branch 44 → 45 taken 123 times.
✓ Branch 44 → 46 taken 52 times.
✓ Branch 47 → 48 taken 123 times.
✓ Branch 47 → 50 taken 698 times.
|
821 | if (from.month() == 1 && from.day() == 1 && to == QDate(from.year(), 12, 31)) |
| 302 | 123 | return QString::number(from.year()); | |
| 303 | } | ||
| 304 |
5/5✓ Branch 57 → 58 taken 709 times.
✓ Branch 58 → 59 taken 706 times.
✓ Branch 58 → 60 taken 3 times.
✓ Branch 59 → 61 taken 706 times.
✓ Branch 61 → 62 taken 709 times.
|
1418 | return (from.isValid() ? from.toString(Qt::ISODate) : QString()) + QStringLiteral("..") + |
| 305 |
5/5✓ Branch 50 → 51 taken 709 times.
✓ Branch 51 → 52 taken 701 times.
✓ Branch 51 → 53 taken 8 times.
✓ Branch 52 → 54 taken 701 times.
✓ Branch 62 → 63 taken 709 times.
|
2127 | (to.isValid() ? to.toString(Qt::ISODate) : QString()); |
| 306 | } | ||
| 307 | |||
| 308 | 13903 | QString parseSearchQuery(const QString &input, | |
| 309 | MailCache::SearchFilter &filter) { | ||
| 310 | 13903 | QStringList remaining; | |
| 311 |
5/5✓ Branch 2 → 3 taken 13903 times.
✓ Branch 3 → 4 taken 13903 times.
✓ Branch 4 → 5 taken 13903 times.
✓ Branch 45 → 6 taken 17127 times.
✓ Branch 45 → 46 taken 13903 times.
|
31030 | for (const auto &token : tokenize(input)) { |
| 312 | // S106-207: buildSearchQuery writes an empty label as tag:"" — read it | ||
| 313 | // back as one, so parse(build(text, f)) == f holds for it too. A bare | ||
| 314 | // "tag:" (no quotes) still adds nothing. | ||
| 315 |
6/6✓ Branch 7 → 8 taken 17120 times.
✓ Branch 7 → 20 taken 7 times.
✓ Branch 8 → 9 taken 22 times.
✓ Branch 8 → 20 taken 17098 times.
✓ Branch 13 → 14 taken 21 times.
✓ Branch 13 → 19 taken 1 time.
|
17170 | if (!token.literal && token.quoted && |
| 316 |
5/6✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 21 times.
✓ Branch 27 → 28 taken 22 times.
✓ Branch 27 → 29 taken 17105 times.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 29 → 34 taken 17126 times.
|
17192 | (token.text.compare(QStringLiteral("tag:"), Qt::CaseInsensitive) == 0 || |
| 317 |
6/6✓ Branch 21 → 22 taken 21 times.
✓ Branch 21 → 23 taken 17106 times.
✓ Branch 23 → 24 taken 21 times.
✓ Branch 23 → 25 taken 17106 times.
✓ Branch 25 → 26 taken 22 times.
✓ Branch 25 → 27 taken 17105 times.
|
17169 | token.text.compare(QStringLiteral("label:"), Qt::CaseInsensitive) == 0)) { |
| 318 |
1/1✓ Branch 31 → 32 taken 1 time.
|
1 | filter.tags.append(QString()); |
| 319 | 1 | continue; | |
| 320 | } | ||
| 321 |
10/10✓ Branch 34 → 35 taken 17119 times.
✓ Branch 34 → 39 taken 7 times.
✓ Branch 35 → 36 taken 17119 times.
✓ Branch 36 → 37 taken 6096 times.
✓ Branch 36 → 40 taken 11023 times.
✓ Branch 37 → 38 taken 6096 times.
✓ Branch 38 → 39 taken 5724 times.
✓ Branch 38 → 40 taken 372 times.
✓ Branch 41 → 42 taken 5731 times.
✓ Branch 41 → 43 taken 11395 times.
|
17126 | if (token.literal || (!applyPositive(token.text, filter) && !applyNegated(token.text, filter))) |
| 322 |
1/1✓ Branch 42 → 43 taken 5731 times.
|
5731 | remaining.append(token.text); // unknown "word:" tokens stay free text |
| 323 | 13903 | } | |
| 324 |
1/1✓ Branch 49 → 50 taken 13903 times.
|
27806 | return remaining.join(QLatin1Char(' ')); |
| 325 | 13903 | } | |
| 326 | |||
| 327 | 1295 | QString buildSearchQuery(const QString &freeText, | |
| 328 | const MailCache::SearchFilter &filter) { | ||
| 329 | 1295 | QStringList parts; | |
| 330 | |||
| 331 | // Free text first so parseSearchQuery() returns it verbatim as the | ||
| 332 | // remainder; excluded words belong to the text (D-14). | ||
| 333 |
1/1✓ Branch 2 → 3 taken 1295 times.
|
1295 | const QString trimmedText = freeText.trimmed(); |
| 334 |
2/2✓ Branch 4 → 5 taken 934 times.
✓ Branch 4 → 64 taken 361 times.
|
1295 | if (!trimmedText.isEmpty()) { |
| 335 |
1/1✓ Branch 7 → 8 taken 934 times.
|
934 | bool literal = trimmedText.contains(QLatin1Char('"')); |
| 336 | 934 | QStringList words; | |
| 337 |
5/5✓ Branch 8 → 9 taken 934 times.
✓ Branch 9 → 10 taken 934 times.
✓ Branch 10 → 11 taken 934 times.
✓ Branch 25 → 12 taken 983 times.
✓ Branch 25 → 26 taken 934 times.
|
1917 | for (const auto &token : tokenize(trimmedText)) { |
| 338 | 983 | MailCache::SearchFilter probe; | |
| 339 |
7/8✓ Branch 14 → 15 taken 983 times.
✗ Branch 14 → 19 not taken.
✓ Branch 15 → 16 taken 983 times.
✓ Branch 16 → 17 taken 982 times.
✓ Branch 16 → 19 taken 1 time.
✓ Branch 17 → 18 taken 982 times.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 981 times.
|
983 | literal |= token.literal || applyPositive(token.text, probe) || applyNegated(token.text, probe); |
| 340 |
1/1✓ Branch 21 → 22 taken 983 times.
|
983 | words.append(token.text); |
| 341 | 1917 | } | |
| 342 |
2/2✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 56 taken 933 times.
|
934 | if (literal) { |
| 343 | 1 | QString escaped = trimmedText; | |
| 344 |
1/1✓ Branch 34 → 35 taken 1 time.
|
1 | escaped.replace(QLatin1Char('\\'), QStringLiteral("\\\\")); |
| 345 |
1/1✓ Branch 42 → 43 taken 1 time.
|
1 | escaped.replace(QLatin1Char('"'), QStringLiteral("\\\"")); |
| 346 |
3/3✓ Branch 49 → 50 taken 1 time.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 51 → 52 taken 1 time.
|
1 | parts.append(QLatin1Char('"') + escaped + QLatin1Char('"')); |
| 347 | 1 | } else { | |
| 348 | // Unquoted words come back joined by one space, so the canonical form | ||
| 349 | // is that join (a tab or a run of spaces is not stable otherwise). | ||
| 350 |
2/2✓ Branch 58 → 59 taken 933 times.
✓ Branch 59 → 60 taken 933 times.
|
933 | parts.append(words.join(QLatin1Char(' '))); |
| 351 | } | ||
| 352 | 934 | } | |
| 353 |
2/2✓ Branch 79 → 66 taken 4 times.
✓ Branch 79 → 80 taken 1295 times.
|
1299 | for (const QString &term : filter.excludedTerms) |
| 354 |
4/5✓ Branch 68 → 69 taken 4 times.
✗ Branch 68 → 77 not taken.
✓ Branch 69 → 70 taken 4 times.
✓ Branch 72 → 73 taken 4 times.
✓ Branch 73 → 74 taken 4 times.
|
4 | if (!term.isEmpty()) parts.append(QLatin1Char('-') + quoteIfNeeded(term)); |
| 355 | |||
| 356 | // Sprint 60 (Q2): one folder: token per pattern, order preserved so the | ||
| 357 | // parse(build(...)) round-trip yields the same list. | ||
| 358 |
1/1✓ Branch 80 → 81 taken 1295 times.
|
1295 | appendEach(parts, "folder:", filter.folderPatterns); |
| 359 |
2/2✓ Branch 82 → 83 taken 180 times.
✓ Branch 82 → 94 taken 1115 times.
|
1295 | if (!filter.fromFilter.isEmpty()) |
| 360 |
3/3✓ Branch 83 → 84 taken 180 times.
✓ Branch 87 → 88 taken 180 times.
✓ Branch 88 → 89 taken 180 times.
|
360 | parts.append(QStringLiteral("from:") + quoteIfNeeded(filter.fromFilter)); |
| 361 |
2/2✓ Branch 95 → 96 taken 4 times.
✓ Branch 95 → 107 taken 1291 times.
|
1295 | if (!filter.toFilter.isEmpty()) |
| 362 |
3/3✓ Branch 96 → 97 taken 4 times.
✓ Branch 100 → 101 taken 4 times.
✓ Branch 101 → 102 taken 4 times.
|
8 | parts.append(QStringLiteral("to:") + quoteIfNeeded(filter.toFilter)); |
| 363 |
2/2✓ Branch 108 → 109 taken 4 times.
✓ Branch 108 → 120 taken 1291 times.
|
1295 | if (!filter.listFilter.isEmpty()) |
| 364 |
3/3✓ Branch 109 → 110 taken 4 times.
✓ Branch 113 → 114 taken 4 times.
✓ Branch 114 → 115 taken 4 times.
|
8 | parts.append(QStringLiteral("list:") + quoteIfNeeded(filter.listFilter)); |
| 365 |
2/2✓ Branch 121 → 122 taken 24 times.
✓ Branch 121 → 133 taken 1271 times.
|
1295 | if (!filter.subjectFilter.isEmpty()) |
| 366 |
1/1✓ Branch 127 → 128 taken 24 times.
|
48 | parts.append(QStringLiteral("subject:") + |
| 367 |
2/2✓ Branch 122 → 123 taken 24 times.
✓ Branch 126 → 127 taken 24 times.
|
72 | quoteIfNeeded(filter.subjectFilter)); |
| 368 |
2/2✓ Branch 134 → 135 taken 1 time.
✓ Branch 134 → 146 taken 1294 times.
|
1295 | if (!filter.aboutFilter.isEmpty()) |
| 369 |
3/3✓ Branch 135 → 136 taken 1 time.
✓ Branch 139 → 140 taken 1 time.
✓ Branch 140 → 141 taken 1 time.
|
2 | parts.append(QStringLiteral("about:") + quoteIfNeeded(filter.aboutFilter)); |
| 370 | |||
| 371 | // S101.1.5: the relative word when the range came from one, else the | ||
| 372 | // shortest absolute form (2025, 2025-03, 2025-03-14, A..B, open ends). | ||
| 373 |
8/8✓ Branch 146 → 147 taken 1295 times.
✓ Branch 147 → 148 taken 1136 times.
✓ Branch 147 → 150 taken 159 times.
✓ Branch 148 → 149 taken 1136 times.
✓ Branch 149 → 150 taken 2 times.
✓ Branch 149 → 151 taken 1134 times.
✓ Branch 152 → 153 taken 161 times.
✓ Branch 152 → 169 taken 1134 times.
|
1295 | if (filter.dateFrom.isValid() || filter.dateTo.isValid()) { |
| 374 |
1/1✓ Branch 163 → 164 taken 161 times.
|
322 | parts.append(QStringLiteral("date:") + |
| 375 |
3/3✓ Branch 154 → 155 taken 5 times.
✓ Branch 154 → 156 taken 156 times.
✓ Branch 158 → 159 taken 156 times.
|
478 | (!filter.datePreset.isEmpty() ? filter.datePreset |
| 376 |
3/3✓ Branch 156 → 157 taken 156 times.
✓ Branch 157 → 158 taken 156 times.
✓ Branch 162 → 163 taken 161 times.
|
478 | : canonicalDateToken(filter.dateFrom.date(), filter.dateTo.date()))); |
| 377 | } | ||
| 378 | |||
| 379 |
1/1✓ Branch 169 → 170 taken 1295 times.
|
1295 | appendEach(parts, "tag:", filter.tags, true); // an empty label stays visible as tag:"" |
| 380 |
1/1✓ Branch 170 → 171 taken 1295 times.
|
1295 | appendTri(parts, filter.unread, "is:unread", "is:read"); |
| 381 |
1/1✓ Branch 171 → 172 taken 1295 times.
|
1295 | appendTri(parts, filter.flagged, "is:flagged", "is:unflagged"); |
| 382 |
1/1✓ Branch 172 → 173 taken 1295 times.
|
1295 | appendTri(parts, filter.answered, "is:answered", "is:unanswered"); |
| 383 |
1/1✓ Branch 173 → 174 taken 1295 times.
|
1295 | appendTri(parts, filter.hasAttachment, "has:attachment", "has:noattachment"); |
| 384 | |||
| 385 |
1/1✓ Branch 174 → 175 taken 1295 times.
|
1295 | appendEach(parts, "-folder:", filter.excludedFolders); |
| 386 |
1/1✓ Branch 175 → 176 taken 1295 times.
|
1295 | appendEach(parts, "-from:", filter.excludedFrom); |
| 387 |
1/1✓ Branch 176 → 177 taken 1295 times.
|
1295 | appendEach(parts, "-to:", filter.excludedTo); |
| 388 |
1/1✓ Branch 177 → 178 taken 1295 times.
|
1295 | appendEach(parts, "-list:", filter.excludedLists); |
| 389 |
1/1✓ Branch 178 → 179 taken 1295 times.
|
1295 | appendEach(parts, "-subject:", filter.excludedSubjects); |
| 390 |
1/1✓ Branch 179 → 180 taken 1295 times.
|
1295 | appendEach(parts, "-tag:", filter.excludedTags); |
| 391 | |||
| 392 |
1/1✓ Branch 182 → 183 taken 1295 times.
|
2590 | return parts.join(QLatin1Char(' ')); |
| 393 | 1295 | } | |
| 394 |