data/RelationInference.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "RelationInference.h" | ||
| 2 | |||
| 3 | #include "RelationVocabulary.h" | ||
| 4 | |||
| 5 | #include <QHash> | ||
| 6 | #include <QLoggingCategory> | ||
| 7 | #include <QMap> | ||
| 8 | #include <QPair> | ||
| 9 | #include <QSet> | ||
| 10 | #include <algorithm> | ||
| 11 | |||
| 12 | // The category of ContactPeopleStore, defined here too: mailjd_vcard stands alone. | ||
| 13 |
3/7✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 8 not taken.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
|
1 | Q_LOGGING_CATEGORY(lcRelationInference, "mailjd.contacts.relations") |
| 14 | |||
| 15 | namespace RelationInference { | ||
| 16 | |||
| 17 | namespace { | ||
| 18 | |||
| 19 | struct Kin { | ||
| 20 | const char *type; | ||
| 21 | int up; | ||
| 22 | int down; | ||
| 23 | }; | ||
| 24 | |||
| 25 | // Blood relationships as kinship vectors (see the header). | ||
| 26 | const Kin kKin[] = { | ||
| 27 | {"parent", 1, 0}, {"grandparent", 2, 0}, {"great-grandparent", 3, 0}, | ||
| 28 | {"child", 0, 1}, {"grandchild", 0, 2}, {"great-grandchild", 0, 3}, | ||
| 29 | {"sibling", 1, 1}, {"aunt-uncle", 2, 1}, {"great-aunt-uncle", 3, 1}, | ||
| 30 | {"niece-nephew", 1, 2}, {"grand-niece-nephew", 1, 3}, {"cousin", 2, 2}, | ||
| 31 | }; | ||
| 32 | |||
| 33 | struct Rule { | ||
| 34 | const char *first; // what B is to A | ||
| 35 | const char *second; // what C is to B | ||
| 36 | const char *result; // what C is to A | ||
| 37 | }; | ||
| 38 | |||
| 39 | // Partners and in-laws, each with its mirror. "Aunt" and "uncle" include | ||
| 40 | // the partner of one, as in everyday speech; step relations (a parent's new | ||
| 41 | // partner, a partner's child) are left to the user — an explicit line wins. | ||
| 42 | const Rule kInLaw[] = { | ||
| 43 | {"spouse", "parent", "parent-in-law"}, // my partner's parent | ||
| 44 | {"child", "spouse", "child-in-law"}, // my child's partner | ||
| 45 | {"spouse", "sibling", "sibling-in-law"}, // my partner's sibling | ||
| 46 | {"sibling", "spouse", "sibling-in-law"}, // my sibling's partner | ||
| 47 | {"aunt-uncle", "spouse", "aunt-uncle"}, // my aunt's husband is my uncle | ||
| 48 | {"spouse", "niece-nephew", "niece-nephew"}, // my partner's niece is my niece | ||
| 49 | {"spouse", "aunt-uncle", "aunt-uncle"}, // my partner's uncle | ||
| 50 | {"niece-nephew", "spouse", "niece-nephew"}, // my nephew's wife | ||
| 51 | }; | ||
| 52 | |||
| 53 | using Pair = QPair<qint64, qint64>; | ||
| 54 | |||
| 55 | // Blood composition: B is (u1, d1) from A, C is (u2, d2) from B. False when | ||
| 56 | // the result is not certain. | ||
| 57 | 17858 | bool combine(int u1, int d1, int u2, int d2, int *up, int *down) { | |
| 58 |
2/2✓ Branch 2 → 3 taken 2446 times.
✓ Branch 2 → 9 taken 15412 times.
|
17858 | if (d1 == 0) { // B is an ancestor of A |
| 59 |
2/2✓ Branch 3 → 4 taken 2031 times.
✓ Branch 3 → 5 taken 415 times.
|
2446 | if (u2 >= 1) { // further up, then down a branch that is not A's line |
| 60 | 2031 | *up = u1 + u2; | |
| 61 | 2031 | *down = d2; | |
| 62 | 2031 | return true; | |
| 63 | } | ||
| 64 |
4/4✓ Branch 5 → 6 taken 394 times.
✓ Branch 5 → 8 taken 21 times.
✓ Branch 6 → 7 taken 393 times.
✓ Branch 6 → 8 taken 1 time.
|
415 | if (u1 == 1 && d2 == 1) { // my parent's child (not me): my sibling |
| 65 | 393 | *up = 1; | |
| 66 | 393 | *down = 1; | |
| 67 | 393 | return true; | |
| 68 | } | ||
| 69 | 22 | return false; // my grandparent's child: my parent or my aunt | |
| 70 | } | ||
| 71 |
2/2✓ Branch 9 → 10 taken 2027 times.
✓ Branch 9 → 11 taken 13385 times.
|
15412 | if (u2 == 0) { // further down B's own line |
| 72 | 2027 | *up = u1; | |
| 73 | 2027 | *down = d1 + d2; | |
| 74 | 2027 | return true; | |
| 75 | } | ||
| 76 | // Down to B, then up again: certain only through full siblings, who share | ||
| 77 | // every ancestor. | ||
| 78 |
8/8✓ Branch 11 → 12 taken 5327 times.
✓ Branch 11 → 16 taken 8058 times.
✓ Branch 12 → 13 taken 2479 times.
✓ Branch 12 → 16 taken 2848 times.
✓ Branch 13 → 14 taken 1907 times.
✓ Branch 13 → 15 taken 572 times.
✓ Branch 14 → 15 taken 1718 times.
✓ Branch 14 → 16 taken 189 times.
|
13385 | if (u1 == 1 && d1 == 1 && (u2 >= 2 || d2 <= 1)) { // my sibling's parent, aunt, cousin, sibling |
| 79 | 2290 | *up = u2; | |
| 80 | 2290 | *down = d2; | |
| 81 | 2290 | return true; | |
| 82 | } | ||
| 83 |
8/8✓ Branch 16 → 17 taken 3720 times.
✓ Branch 16 → 21 taken 7375 times.
✓ Branch 17 → 18 taken 2100 times.
✓ Branch 17 → 21 taken 1620 times.
✓ Branch 18 → 19 taken 1529 times.
✓ Branch 18 → 20 taken 571 times.
✓ Branch 19 → 20 taken 1340 times.
✓ Branch 19 → 21 taken 189 times.
|
11095 | if (u2 == 1 && d2 == 1 && (d1 >= 2 || u1 <= 1)) { // the sibling of my child, niece, cousin |
| 84 | 1911 | *up = u1; | |
| 85 | 1911 | *down = d1; | |
| 86 | 1911 | return true; | |
| 87 | } | ||
| 88 | 9184 | return false; // my child's parent, my niece's parent, my aunt's sibling: ambiguous | |
| 89 | } | ||
| 90 | |||
| 91 | } // namespace | ||
| 92 | |||
| 93 | 188914 | bool kinship(const QString &type, int *up, int *down) { | |
| 94 |
1/1✓ Branch 2 → 3 taken 188914 times.
|
188914 | const QString known = RelationVocabulary::canonical(type); |
| 95 |
2/2✓ Branch 9 → 4 taken 1770766 times.
✓ Branch 9 → 10 taken 114802 times.
|
1885568 | for (const Kin &kin : kKin) |
| 96 |
2/2✓ Branch 6 → 7 taken 74112 times.
✓ Branch 6 → 8 taken 1696654 times.
|
1770766 | if (known == QLatin1String(kin.type)) { |
| 97 | 74112 | *up = kin.up; | |
| 98 | 74112 | *down = kin.down; | |
| 99 | 74112 | return true; | |
| 100 | } | ||
| 101 | 114802 | return false; | |
| 102 | 188914 | } | |
| 103 | |||
| 104 | 8652 | QString typeOfKinship(int up, int down) { | |
| 105 |
2/2✓ Branch 9 → 3 taken 43370 times.
✓ Branch 9 → 10 taken 485 times.
|
43855 | for (const Kin &kin : kKin) |
| 106 |
4/4✓ Branch 3 → 4 taken 14370 times.
✓ Branch 3 → 8 taken 29000 times.
✓ Branch 4 → 5 taken 8167 times.
✓ Branch 4 → 8 taken 6203 times.
|
43370 | if (kin.up == up && kin.down == down) |
| 107 |
1/1✓ Branch 6 → 7 taken 8167 times.
|
8167 | return QString::fromLatin1(kin.type); |
| 108 | 485 | return {}; | |
| 109 | } | ||
| 110 | |||
| 111 | 128885 | QString compose(const QString &t1, const QString &t2) { | |
| 112 | 128885 | int u1 = 0, d1 = 0, u2 = 0, d2 = 0, up = 0, down = 0; | |
| 113 |
8/8✓ Branch 2 → 3 taken 128885 times.
✓ Branch 3 → 4 taken 51495 times.
✓ Branch 3 → 7 taken 77390 times.
✓ Branch 4 → 5 taken 51495 times.
✓ Branch 5 → 6 taken 17858 times.
✓ Branch 5 → 7 taken 33637 times.
✓ Branch 8 → 9 taken 17858 times.
✓ Branch 8 → 14 taken 111027 times.
|
128885 | if (kinship(t1, &u1, &d1) && kinship(t2, &u2, &d2)) |
| 114 |
3/3✓ Branch 10 → 11 taken 8652 times.
✓ Branch 10 → 12 taken 9206 times.
✓ Branch 11 → 13 taken 8652 times.
|
17858 | return combine(u1, d1, u2, d2, &up, &down) ? typeOfKinship(up, down) : QString(); |
| 115 | // S106-171: the in-law table is matched on canonical ids like the blood | ||
| 116 | // branch (aliased spellings composed on one path only). | ||
| 117 |
1/1✓ Branch 14 → 15 taken 111027 times.
|
111027 | const QString c1 = RelationVocabulary::canonical(t1); |
| 118 |
1/1✓ Branch 15 → 16 taken 111027 times.
|
111027 | const QString c2 = RelationVocabulary::canonical(t2); |
| 119 |
2/2✓ Branch 30 → 17 taken 865818 times.
✓ Branch 30 → 31 taken 106878 times.
|
972696 | for (const Rule &rule : kInLaw) |
| 120 |
6/6✓ Branch 19 → 20 taken 43382 times.
✓ Branch 19 → 24 taken 822436 times.
✓ Branch 22 → 23 taken 4149 times.
✓ Branch 22 → 24 taken 39233 times.
✓ Branch 25 → 26 taken 4149 times.
✓ Branch 25 → 29 taken 861669 times.
|
865818 | if (c1 == QLatin1String(rule.first) && c2 == QLatin1String(rule.second)) |
| 121 |
1/1✓ Branch 27 → 28 taken 4149 times.
|
4149 | return QString::fromLatin1(rule.result); |
| 122 | 106878 | return {}; | |
| 123 | 111027 | } | |
| 124 | |||
| 125 | 304 | QList<Inferred> infer(const QList<Fact> &stored) { | |
| 126 | // Known facts by holder; every related pair (either way) is closed for inference. | ||
| 127 | 304 | QHash<qint64, QList<Fact>> byHolder; | |
| 128 | 304 | QSet<Pair> related; | |
| 129 | 304 | QSet<Pair> ambiguous; // pairs whose derivations disagree (first < second) | |
| 130 | 304 | int count = 0; | |
| 131 | 26886 | const auto add = [&](const Fact &fact) { | |
| 132 | 26886 | byHolder[fact.holder].append(fact); | |
| 133 |
1/1✓ Branch 5 → 6 taken 26886 times.
|
26886 | related.insert({fact.holder, fact.other}); |
| 134 | 26886 | ++count; | |
| 135 | 26886 | }; | |
| 136 |
2/2✓ Branch 37 → 6 taken 8361 times.
✓ Branch 37 → 38 taken 304 times.
|
8665 | for (const Fact &fact : stored) { |
| 137 |
3/6✓ Branch 7 → 8 taken 8361 times.
✗ Branch 7 → 10 not taken.
✓ Branch 8 → 9 taken 8361 times.
✗ Branch 8 → 10 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 8361 times.
|
8361 | if (fact.holder <= 0 || fact.other <= 0 || fact.holder == fact.other) |
| 138 | ✗ | continue; | |
| 139 |
1/1✓ Branch 11 → 12 taken 8361 times.
|
8361 | const QString type = RelationVocabulary::canonical(fact.type); |
| 140 |
1/1✓ Branch 12 → 13 taken 8361 times.
|
8361 | const QString mirror = RelationVocabulary::inverseOf(type); |
| 141 |
2/2✓ Branch 15 → 16 taken 8172 times.
✓ Branch 15 → 20 taken 189 times.
|
8361 | if (!related.contains({fact.holder, fact.other})) |
| 142 |
1/1✓ Branch 17 → 18 taken 8172 times.
|
8172 | add({fact.holder, fact.other, type}); |
| 143 |
6/6✓ Branch 21 → 22 taken 8173 times.
✓ Branch 21 → 26 taken 188 times.
✓ Branch 24 → 25 taken 7984 times.
✓ Branch 24 → 26 taken 189 times.
✓ Branch 27 → 28 taken 7984 times.
✓ Branch 27 → 32 taken 377 times.
|
8361 | if (!mirror.isEmpty() && !related.contains({fact.other, fact.holder})) |
| 144 |
1/1✓ Branch 29 → 30 taken 7984 times.
|
7984 | add({fact.other, fact.holder, mirror}); |
| 145 | 8361 | } | |
| 146 | 304 | QList<Inferred> out; | |
| 147 |
2/4✓ Branch 170 → 171 taken 719 times.
✗ Branch 170 → 172 not taken.
✓ Branch 171 → 39 taken 719 times.
✗ Branch 171 → 172 not taken.
|
719 | for (int round = 0; round < MaxRounds && count < MaxFacts; ++round) { |
| 148 | // The derivations of this round by pair, seen from the smaller card id | ||
| 149 | // so that both directions of one pair compare. | ||
| 150 | 719 | QMap<Pair, Inferred> found; | |
| 151 |
1/1✓ Branch 39 → 40 taken 719 times.
|
719 | QList<qint64> holders = byHolder.keys(); |
| 152 |
3/3✓ Branch 40 → 41 taken 719 times.
✓ Branch 41 → 42 taken 719 times.
✓ Branch 42 → 43 taken 719 times.
|
719 | std::sort(holders.begin(), holders.end()); |
| 153 |
2/2✓ Branch 129 → 46 taken 18097 times.
✓ Branch 129 → 130 taken 719 times.
|
18816 | for (qint64 a : std::as_const(holders)) |
| 154 |
4/4✓ Branch 48 → 49 taken 18097 times.
✓ Branch 49 → 50 taken 18097 times.
✓ Branch 125 → 51 taken 65490 times.
✓ Branch 125 → 126 taken 18097 times.
|
83587 | for (const Fact &ab : byHolder.value(a)) |
| 155 |
4/4✓ Branch 53 → 54 taken 65490 times.
✓ Branch 54 → 55 taken 65490 times.
✓ Branch 121 → 56 taken 357194 times.
✓ Branch 121 → 122 taken 65490 times.
|
422684 | for (const Fact &bc : byHolder.value(ab.other)) { |
| 156 | 357194 | const qint64 c = bc.other; | |
| 157 |
8/8✓ Branch 57 → 58 taken 292268 times.
✓ Branch 57 → 64 taken 64926 times.
✓ Branch 60 → 61 taken 129980 times.
✓ Branch 60 → 64 taken 162288 times.
✓ Branch 63 → 64 taken 1128 times.
✓ Branch 63 → 65 taken 128852 times.
✓ Branch 66 → 67 taken 228342 times.
✓ Branch 66 → 68 taken 128852 times.
|
357194 | if (c == a || related.contains({a, c}) || related.contains({c, a})) |
| 158 | 344906 | continue; | |
| 159 |
1/1✓ Branch 68 → 69 taken 128852 times.
|
128852 | const QString type = compose(ab.type, bc.type); |
| 160 | 128852 | const Pair key{std::min(a, c), std::max(a, c)}; | |
| 161 |
6/6✓ Branch 73 → 74 taken 12294 times.
✓ Branch 73 → 76 taken 116558 times.
✓ Branch 75 → 76 taken 6 times.
✓ Branch 75 → 77 taken 12288 times.
✓ Branch 78 → 79 taken 116564 times.
✓ Branch 78 → 80 taken 12288 times.
|
128852 | if (type.isEmpty() || ambiguous.contains(key)) |
| 162 | 116564 | continue; | |
| 163 | const Inferred derived = | ||
| 164 |
0/8✗ Branch 200 → 201 not taken.
✗ Branch 200 → 203 not taken.
✗ Branch 201 → 202 not taken.
✗ Branch 201 → 203 not taken.
✗ Branch 204 → 205 not taken.
✗ Branch 204 → 207 not taken.
✗ Branch 205 → 206 not taken.
✗ Branch 205 → 207 not taken.
|
6145 | a < c ? Inferred{a, c, type, ab.other, ab.type, bc.type} |
| 165 |
1/9✓ Branch 85 → 86 taken 6143 times.
✗ Branch 192 → 193 not taken.
✗ Branch 192 → 195 not taken.
✗ Branch 193 → 194 not taken.
✗ Branch 193 → 195 not taken.
✗ Branch 196 → 197 not taken.
✗ Branch 196 → 199 not taken.
✗ Branch 197 → 198 not taken.
✗ Branch 197 → 199 not taken.
|
12286 | : Inferred{c, a, RelationVocabulary::inverseOf(type), ab.other, |
| 166 |
4/4✓ Branch 80 → 81 taken 6145 times.
✓ Branch 80 → 85 taken 6143 times.
✓ Branch 86 → 87 taken 6143 times.
✓ Branch 87 → 88 taken 6143 times.
|
18431 | RelationVocabulary::inverseOf(bc.type), RelationVocabulary::inverseOf(ab.type)}; |
| 167 |
1/1✓ Branch 89 → 90 taken 12288 times.
|
12288 | const auto it = found.constFind(key); |
| 168 |
3/3✓ Branch 90 → 91 taken 12288 times.
✓ Branch 92 → 93 taken 5366 times.
✓ Branch 92 → 94 taken 6922 times.
|
12288 | if (it == found.cend()) { |
| 169 |
1/1✓ Branch 93 → 112 taken 5366 times.
|
5366 | found.insert(key, derived); |
| 170 |
2/2✓ Branch 96 → 97 taken 1 time.
✓ Branch 96 → 112 taken 6921 times.
|
6922 | } else if (it->type != derived.type) { |
| 171 |
6/6✓ Branch 97 → 98 taken 1 time.
✓ Branch 101 → 102 taken 1 time.
✓ Branch 102 → 103 taken 1 time.
✓ Branch 103 → 104 taken 1 time.
✓ Branch 108 → 99 taken 1 time.
✓ Branch 108 → 109 taken 1 time.
|
2 | qCDebug(lcRelationInference) << "Inference: contradicting derivations for cards" << key.first |
| 172 |
1/1✓ Branch 104 → 105 taken 1 time.
|
1 | << key.second; |
| 173 |
1/1✓ Branch 109 → 110 taken 1 time.
|
1 | found.remove(key); |
| 174 |
1/1✓ Branch 110 → 111 taken 1 time.
|
1 | ambiguous.insert(key); |
| 175 | } | ||
| 176 | 212439 | } | |
| 177 |
3/3✓ Branch 130 → 131 taken 719 times.
✓ Branch 131 → 132 taken 304 times.
✓ Branch 131 → 133 taken 415 times.
|
719 | if (found.isEmpty()) |
| 178 | 304 | break; | |
| 179 |
4/4✓ Branch 134 → 135 taken 415 times.
✓ Branch 135 → 136 taken 415 times.
✓ Branch 159 → 137 taken 5365 times.
✓ Branch 159 → 160 taken 415 times.
|
5780 | for (const Inferred &f : std::as_const(found)) { |
| 180 |
1/1✓ Branch 139 → 140 taken 5365 times.
|
5365 | add({f.holder, f.other, f.type}); |
| 181 |
1/1✓ Branch 141 → 142 taken 5365 times.
|
5365 | out.append(f); |
| 182 | // The mirror, so both cards show it. | ||
| 183 |
2/3✓ Branch 142 → 143 taken 5365 times.
✓ Branch 144 → 145 taken 5365 times.
✗ Branch 144 → 154 not taken.
|
5365 | if (const QString mirror = RelationVocabulary::inverseOf(f.type); !mirror.isEmpty()) { |
| 184 |
1/1✓ Branch 146 → 147 taken 5365 times.
|
5365 | add({f.other, f.holder, mirror}); |
| 185 | 5365 | out.append({f.other, f.holder, mirror, f.via, RelationVocabulary::inverseOf(f.secondType), | |
| 186 | 5365 | RelationVocabulary::inverseOf(f.firstType)}); | |
| 187 | 5365 | } | |
| 188 |
1/2✗ Branch 155 → 156 not taken.
✓ Branch 155 → 157 taken 5365 times.
|
5365 | if (count >= MaxFacts) |
| 189 | ✗ | break; | |
| 190 | } | ||
| 191 | 1023 | } | |
| 192 |
3/3✓ Branch 172 → 173 taken 304 times.
✓ Branch 173 → 174 taken 304 times.
✓ Branch 174 → 175 taken 304 times.
|
304 | std::sort(out.begin(), out.end(), [](const Inferred &x, const Inferred &y) { |
| 193 |
2/2✓ Branch 2 → 3 taken 63467 times.
✓ Branch 2 → 4 taken 14609 times.
|
78076 | return x.holder != y.holder ? x.holder < y.holder : x.other < y.other; |
| 194 | }); | ||
| 195 | 304 | return out; | |
| 196 | 32555 | } | |
| 197 | |||
| 198 | } // namespace RelationInference | ||
| 199 |