mergeutil.cpp 21.5 KB
Newer Older
1
/*
2
    SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
3

4
    SPDX-License-Identifier: LGPL-2.0-or-later
5
6
7
*/

#include "mergeutil.h"
8
#include "logging.h"
9
#include "compare-logging.h"
10
#include "stringutil.h"
11
#include "sortutil.h"
12

13
#include <KItinerary/BusTrip>
14
#include <KItinerary/Event>
15
16
#include <KItinerary/Flight>
#include <KItinerary/JsonLdDocument>
Laurent Montel's avatar
Laurent Montel committed
17
#include <KItinerary/RentalCar>
18
#include <KItinerary/Organization>
19
#include <KItinerary/Place>
20
21
#include <KItinerary/Person>
#include <KItinerary/Reservation>
22
#include <KItinerary/Taxi>
23
#include <KItinerary/Ticket>
24
#include <KItinerary/TrainTrip>
25
#include <KItinerary/Visit>
26

Volker Krause's avatar
Volker Krause committed
27
#include <QDate>
28
#include <QDebug>
29
30
#include <QMetaObject>
#include <QMetaProperty>
31
32
#include <QVariant>

Volker Krause's avatar
Volker Krause committed
33
34
#include <cmath>

35
36
using namespace KItinerary;

37
/* Checks that @p lhs and @p rhs are non-empty and equal. */
38
static bool equalAndPresent(const QString &lhs, const QString &rhs, Qt::CaseSensitivity caseSensitive = Qt::CaseSensitive)
39
{
40
    return !lhs.isEmpty() && (lhs.compare(rhs, caseSensitive) == 0);
41
42
43
44
45
}
static bool equalAndPresent(const QDate &lhs, const QDate &rhs)
{
    return lhs.isValid() && lhs == rhs;
}
46
47
48
49
static bool equalAndPresent(const QDateTime &lhs, const QDateTime &rhs)
{
    return lhs.isValid() && lhs == rhs;
}
50
51

/* Checks that @p lhs and @p rhs are not non-equal if both values are set. */
Volker Krause's avatar
Volker Krause committed
52
static bool conflictIfPresent(const QString &lhs, const QString &rhs, Qt::CaseSensitivity caseSensitive = Qt::CaseSensitive)
53
{
Volker Krause's avatar
Volker Krause committed
54
    return !lhs.isEmpty() && !rhs.isEmpty() && lhs.compare(rhs, caseSensitive) != 0;
55
56
57
58
59
60
61
}
static bool conflictIfPresent(const QDateTime &lhs, const QDateTime &rhs)
{
    return lhs.isValid() && rhs.isValid() && lhs != rhs;
}

static bool isSameFlight(const Flight &lhs, const Flight &rhs);
62
static bool isSameTrainTrip(const TrainTrip &lhs, const TrainTrip &rhs);
63
static bool isSameBusTrip(const BusTrip &lhs, const BusTrip &rhs);
64
static bool isSameLodingBusiness(const LodgingBusiness &lhs, const LodgingBusiness &rhs);
65
static bool isSameFoodEstablishment(const FoodEstablishment &lhs, const FoodEstablishment &rhs);
66
67
static bool isSameTouristAttractionVisit(const TouristAttractionVisit &lhs, const TouristAttractionVisit &rhs);
static bool isSameTouristAttraction(const TouristAttraction &lhs, const TouristAttraction &rhs);
68
static bool isSameEvent(const Event &lhs, const Event &rhs);
Laurent Montel's avatar
Laurent Montel committed
69
static bool isSameRentalCar(const RentalCar &lhs, const RentalCar &rhs);
70
static bool isSameTaxiTrip(const Taxi &lhs, const Taxi &rhs);
71
static bool isMinimalCancelationFor(const QVariant &r, const Reservation &cancel);
72

73
bool MergeUtil::isSame(const QVariant& lhs, const QVariant& rhs)
74
{
75
    if (lhs.isNull() || rhs.isNull()) {
76
77
78
79
80
81
        return false;
    }
    if (lhs.userType() != rhs.userType()) {
        return false;
    }

82
    // for all reservations check underName and ticket
Volker Krause's avatar
Volker Krause committed
83
    if (JsonLd::canConvert<Reservation>(lhs)) {
84
85
86
        const auto lhsRes = JsonLd::convert<Reservation>(lhs);
        const auto rhsRes = JsonLd::convert<Reservation>(rhs);

Volker Krause's avatar
Volker Krause committed
87
        // for all: underName either matches or is not set
88
89
        const auto lhsUN = lhsRes.underName().value<Person>();
        const auto rhsUN = rhsRes.underName().value<Person>();
Volker Krause's avatar
Volker Krause committed
90
91
92
        if (!lhsUN.name().isEmpty() && !rhsUN.name().isEmpty() &&  !isSamePerson(lhsUN, rhsUN)) {
            return false;
        }
93

94
95
        const auto lhsTicket = lhsRes.reservedTicket().value<Ticket>();
        const auto rhsTicket = rhsRes.reservedTicket().value<Ticket>();
96
97
98
        if (conflictIfPresent(lhsTicket.ticketedSeat().seatNumber(), rhsTicket.ticketedSeat().seatNumber(), Qt::CaseInsensitive)) {
            return false;
        }
99
100
101
102
        // flight ticket tokens (IATA BCBP) can differ, so we need to compare the relevant bits in them manually
        // this however happens automatically as they are unpacked to other fields by post-processing
        // so we can simply skip this here for flights
        if (!JsonLd::isA<FlightReservation>(lhs) && conflictIfPresent(lhsTicket.ticketTokenData(), rhsTicket.ticketTokenData())) {
Volker Krause's avatar
Volker Krause committed
103
            return false;
104
        }
105
106
107
108
109
110

        // one side is a minimal cancelation, matches the reservation number and has a plausible modification time
        // in this case don't bother comparing content (which will fail), we accept this directly
        if (isMinimalCancelationFor(lhs, rhsRes) || isMinimalCancelationFor(rhs, lhsRes)) {
            return true;
        }
Volker Krause's avatar
Volker Krause committed
111
112
    }

113
    // flight: booking ref, flight number and departure day match
114
    if (JsonLd::isA<FlightReservation>(lhs)) {
115
116
        const auto lhsRes = lhs.value<FlightReservation>();
        const auto rhsRes = rhs.value<FlightReservation>();
117
        if (conflictIfPresent(lhsRes.reservationNumber(), rhsRes.reservationNumber()) || conflictIfPresent(lhsRes.passengerSequenceNumber(), rhsRes.passengerSequenceNumber())) {
118
119
            return false;
        }
Volker Krause's avatar
Volker Krause committed
120
121
122
123
124
125
        return isSame(lhsRes.reservationFor(), rhsRes.reservationFor());
    }
    if (JsonLd::isA<Flight>(lhs)) {
        const auto lhsFlight = lhs.value<Flight>();
        const auto rhsFlight = rhs.value<Flight>();
        return isSameFlight(lhsFlight, rhsFlight);
126
127
    }

128
    // train: booking ref, train number and depature day match
129
    if (JsonLd::isA<TrainReservation>(lhs)) {
130
131
        const auto lhsRes = lhs.value<TrainReservation>();
        const auto rhsRes = rhs.value<TrainReservation>();
132
        if (conflictIfPresent(lhsRes.reservationNumber(), rhsRes.reservationNumber())) {
133
134
            return false;
        }
Volker Krause's avatar
Volker Krause committed
135
136
137
138
139
140
        return isSame(lhsRes.reservationFor(), rhsRes.reservationFor());
    }
    if (JsonLd::isA<TrainTrip>(lhs)) {
        const auto lhsTrip = lhs.value<TrainTrip>();
        const auto rhsTrip = rhs.value<TrainTrip>();
        return isSameTrainTrip(lhsTrip, rhsTrip);
141
142
    }

143
    // bus: booking ref, number and depature time match
144
    if (JsonLd::isA<BusReservation>(lhs)) {
145
146
147
148
149
        const auto lhsRes = lhs.value<BusReservation>();
        const auto rhsRes = rhs.value<BusReservation>();
        if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
            return false;
        }
Volker Krause's avatar
Volker Krause committed
150
151
152
153
154
155
        return isSame(lhsRes.reservationFor(), rhsRes.reservationFor());
    }
    if (JsonLd::isA<BusTrip>(lhs)) {
        const auto lhsTrip = lhs.value<BusTrip>();
        const auto rhsTrip = rhs.value<BusTrip>();
        return isSameBusTrip(lhsTrip, rhsTrip);
156
    }
157
158

    // hotel: booking ref, checkin day, name match
159
    if (JsonLd::isA<LodgingReservation>(lhs)) {
160
161
162
163
164
        const auto lhsRes = lhs.value<LodgingReservation>();
        const auto rhsRes = rhs.value<LodgingReservation>();
        if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
            return false;
        }
Laurent Montel's avatar
Laurent Montel committed
165
        return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.checkinTime().date() == rhsRes.checkinTime().date();
Volker Krause's avatar
Volker Krause committed
166
167
168
169
170
    }
    if (JsonLd::isA<LodgingBusiness>(lhs)) {
        const auto lhsHotel = lhs.value<LodgingBusiness>();
        const auto rhsHotel = rhs.value<LodgingBusiness>();
        return isSameLodingBusiness(lhsHotel, rhsHotel);
171
    }
172

Laurent Montel's avatar
Laurent Montel committed
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
    // Rental Car
    if (JsonLd::isA<RentalCarReservation>(lhs)) {
        const auto lhsRes = lhs.value<RentalCarReservation>();
        const auto rhsRes = rhs.value<RentalCarReservation>();
        if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
            return false;
        }
        return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.pickupTime().date() == rhsRes.pickupTime().date();
    }
    if (JsonLd::isA<RentalCar>(lhs)) {
        const auto lhsEv = lhs.value<RentalCar>();
        const auto rhsEv = rhs.value<RentalCar>();
        return isSameRentalCar(lhsEv, rhsEv);
    }

188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
    // Taxi
    if (JsonLd::isA<TaxiReservation>(lhs)) {
        const auto lhsRes = lhs.value<TaxiReservation>();
        const auto rhsRes = rhs.value<TaxiReservation>();
        if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
            return false;
        }
        return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.pickupTime().date() == rhsRes.pickupTime().date();
    }
    if (JsonLd::isA<Taxi>(lhs)) {
        const auto lhsEv = lhs.value<Taxi>();
        const auto rhsEv = rhs.value<Taxi>();
        return isSameTaxiTrip(lhsEv, rhsEv);
    }

203
    // restaurant reservation: same restaurant, same booking ref, same day
204
    if (JsonLd::isA<FoodEstablishmentReservation>(lhs)) {
205
206
207
208
209
        const auto lhsRes = lhs.value<FoodEstablishmentReservation>();
        const auto rhsRes = rhs.value<FoodEstablishmentReservation>();
        if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
            return false;
        }
210
211
212
213
214
215
        auto endTime = rhsRes.endTime();
        if (!endTime.isValid()) {
            endTime = QDateTime(rhsRes.startTime().date(), QTime(23, 59, 59));
        }

        return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.startTime().date() == endTime.date();
Volker Krause's avatar
Volker Krause committed
216
217
218
219
220
    }
    if (JsonLd::isA<FoodEstablishment>(lhs)) {
        const auto lhsRestaurant = lhs.value<FoodEstablishment>();
        const auto rhsRestaurant = rhs.value<FoodEstablishment>();
        return isSameFoodEstablishment(lhsRestaurant, rhsRestaurant);
221
222
    }

223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
    // event reservation
    if (JsonLd::isA<EventReservation>(lhs)) {
        const auto lhsRes = lhs.value<EventReservation>();
        const auto rhsRes = rhs.value<EventReservation>();
        if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
            return false;
        }
        return isSame(lhsRes.reservationFor(), rhsRes.reservationFor());
    }
    if (JsonLd::isA<Event>(lhs)) {
        const auto lhsEv = lhs.value<Event>();
        const auto rhsEv = rhs.value<Event>();
        return isSameEvent(lhsEv, rhsEv);
    }

238
239
240
241
242
243
244
245
    // tourist attraction visit
    if (JsonLd::isA<TouristAttractionVisit>(lhs)) {
        const auto l = lhs.value<TouristAttractionVisit>();
        const auto r = rhs.value<TouristAttractionVisit>();
        return isSameTouristAttractionVisit(l, r);
    }

    return true;
246
247
}

248
static bool isSameFlight(const Flight& lhs, const Flight& rhs)
249
{
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
    // if there is a conflict on where this is going, or when, this is obviously not the same flight
    if (conflictIfPresent(lhs.departureAirport().iataCode(), rhs.departureAirport().iataCode()) ||
        conflictIfPresent(lhs.arrivalAirport().iataCode(), rhs.arrivalAirport().iataCode()) ||
        !equalAndPresent(lhs.departureDay(), rhs.departureDay())) {
        return false;
    }

    // same flight number and airline (on the same day) -> we assume same flight
    if (equalAndPresent(lhs.flightNumber(), rhs.flightNumber()) && equalAndPresent(lhs.airline().iataCode(), rhs.airline().iataCode())) {
        return true;
    }

    // we get here if we have matching origin/destination on the same day, but mismatching flight numbers
    // so this might be a codeshare flight
    // our caller checks for matching booking ref, so just look for a few counter-indicators here
    // (that is, if this is ever made available as standalone API, the last return should not be true)
    if (conflictIfPresent(lhs.departureTime(), rhs.departureTime())) {
267
268
269
        return false;
    }

270
    return true;
271
272
}

273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
// see kpublictrainport, line.cpp
template <typename Iter>
static bool isSameLineName(const Iter &lBegin, const Iter &lEnd, const Iter &rBegin, const Iter &rEnd)
{
    auto lIt = lBegin;
    auto rIt = rBegin;
    while (lIt != lEnd && rIt != rEnd) {
        // ignore spaces etc.
        if (!(*lIt).isLetter() && !(*lIt).isDigit()) {
            ++lIt;
            continue;
        }
        if (!(*rIt).isLetter() && !(*rIt).isDigit()) {
            ++rIt;
            continue;
        }

        if ((*lIt).toCaseFolded() != (*rIt).toCaseFolded()) {
            return false;
        }

        ++lIt;
        ++rIt;
    }

    if (lIt == lEnd && rIt == rEnd) { // both inputs fully consumed, and no mismatch found
        return true;
    }

    // one input is prefix of the other, that is ok if there's a separator
    return (lIt != lEnd && (*lIt).isSpace()) || (rIt != rEnd && (*rIt).isSpace());
}

static bool isSameLineName(const QString &lhs, const QString &rhs)
{
    return isSameLineName(lhs.begin(), lhs.end(), rhs.begin(), rhs.end())
        || isSameLineName(lhs.rbegin(), lhs.rend(), rhs.rbegin(), rhs.rend());
}

312
static bool isSameTrainTrip(const TrainTrip &lhs, const TrainTrip &rhs)
313
{
314
315
316
317
318
319
320
    if (lhs.departureDay() != rhs.departureDay()) {
        return false;
    }

    // for unbound tickets, comparing the line number below wont help
    // so we have to use the slightly less robust location comparisson
    if (!lhs.departureTime().isValid() && !rhs.departureTime().isValid()) {
321
322
        qCDebug(CompareLog) << "unbound trip" << lhs.departureStation().name() << rhs.departureStation().name() << lhs.arrivalStation().name() << rhs.arrivalStation().name();
        return lhs.departureStation().name() == rhs.departureStation().name() && lhs.arrivalStation().name() == rhs.arrivalStation().name();
323
324
    }

325
    if (lhs.trainNumber().isEmpty() || rhs.trainNumber().isEmpty()) {
326
        qCDebug(CompareLog) << "missing train number" << lhs.trainNumber() << rhs.trainNumber();
327
328
329
        return false;
    }

330
331
332
333
334
    const auto isSameLine = isSameLineName(lhs.trainNumber(), rhs.trainNumber());
    qCDebug(CompareLog) << "left:" << lhs.trainName() << lhs.trainNumber() << lhs.departureTime();
    qCDebug(CompareLog) << "right:" << rhs.trainName() << rhs.trainNumber() << rhs.departureTime();
    qCDebug(CompareLog) << "same line:" << isSameLine;
    return !conflictIfPresent(lhs.trainName(),rhs.trainName()) && isSameLine && lhs.departureTime().date() == rhs.departureTime().date();
335
336
}

337
338
339
340
341
342
343
344
345
static bool isSameBusTrip(const BusTrip &lhs, const BusTrip &rhs)
{
    if (lhs.busNumber().isEmpty() || rhs.busNumber().isEmpty()) {
        return false;
    }

    return lhs.busName() == rhs.busName() && lhs.busNumber() == rhs.busNumber() && lhs.departureTime() == rhs.departureTime();
}

346
347
348
349
350
351
352
353
354
static bool isSameLodingBusiness(const LodgingBusiness &lhs, const LodgingBusiness &rhs)
{
    if (lhs.name().isEmpty() || rhs.name().isEmpty()) {
        return false;
    }

    return lhs.name() == rhs.name();
}

355
356
357
358
359
360
361
362
363
static bool isSameFoodEstablishment(const FoodEstablishment &lhs, const FoodEstablishment &rhs)
{
    if (lhs.name().isEmpty() || rhs.name().isEmpty()) {
        return false;
    }

    return lhs.name() == rhs.name();
}

364
365
366
367
368
369
370
371
372
373
static bool isSameTouristAttractionVisit(const TouristAttractionVisit &lhs, const TouristAttractionVisit &rhs)
{
    return lhs.arrivalTime() == rhs.arrivalTime() && isSameTouristAttraction(lhs.touristAttraction(), rhs.touristAttraction());
}

static bool isSameTouristAttraction(const TouristAttraction &lhs, const TouristAttraction &rhs)
{
    return lhs.name() == rhs.name();
}

Volker Krause's avatar
Volker Krause committed
374
375
// compute the "difference" between @p lhs and @p rhs
static QString diffString(const QString &lhs, const QString &rhs)
376
{
Volker Krause's avatar
Volker Krause committed
377
378
379
380
    QString diff;
    // this is just a basic linear-time heuristic, this would need to be more something like
    // the Levenstein Distance algorithm
    for (int i = 0, j = 0; i < lhs.size() || j < rhs.size();) {
381
        if (i < lhs.size() && j < rhs.size() && StringUtil::normalize(lhs[i]) == StringUtil::normalize(rhs[j])) {
Volker Krause's avatar
Volker Krause committed
382
383
384
385
386
387
388
389
390
391
392
            ++i;
            ++j;
            continue;
        }
        if ((j < rhs.size() && (lhs.size() < rhs.size() || (lhs.size() == rhs.size() && j < i))) || i == lhs.size()) {
            diff += rhs[j];
            ++j;
        } else {
            diff += lhs[i];
            ++i;
        }
393
    }
Volker Krause's avatar
Volker Krause committed
394
395
    return diff.trimmed();
}
396

Volker Krause's avatar
Volker Krause committed
397
398
399
static bool isNameEqualish(const QString &lhs, const QString &rhs)
{
    if (lhs.isEmpty() || rhs.isEmpty()) {
400
401
402
        return false;
    }

Volker Krause's avatar
Volker Krause committed
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
    auto diff = diffString(lhs, rhs).toUpper();

    // remove honoric prefixes from the diff, in case the previous check didn't catch that
    diff.remove(QLatin1String("MRS"));
    diff.remove(QLatin1String("MR"));
    diff.remove(QLatin1String("MS"));

    // if there's letters in the diff, we assume this is different
    for (const auto c : diff) {
        if (c.isLetter()) {
            return false;
        }
    }

    return true;
}
419

Volker Krause's avatar
Volker Krause committed
420
421
422
423
bool MergeUtil::isSamePerson(const Person& lhs, const Person& rhs)
{
    return isNameEqualish(lhs.name(), rhs.name()) ||
        (isNameEqualish(lhs.givenName(), rhs.givenName()) && isNameEqualish(lhs.familyName(), rhs.familyName()));
424
}
425
426
427
428
429
430

static bool isSameEvent(const Event &lhs, const Event &rhs)
{
    return equalAndPresent(lhs.name(), rhs.name())
        && equalAndPresent(lhs.startDate(), rhs.startDate());
}
Laurent Montel's avatar
Laurent Montel committed
431
432
433
434
435

static bool isSameRentalCar(const RentalCar &lhs, const RentalCar &rhs)
{
    return lhs.name() == rhs.name();
}
436
437
438
439
440
441

static bool isSameTaxiTrip(const Taxi &lhs, const Taxi &rhs)
{
    //TODO verify
    return lhs.name() == rhs.name();
}
442

Volker Krause's avatar
Volker Krause committed
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
static bool containsNonAscii(const QString &s)
{
    for (const auto c : s) {
        if (c.row() != 0 || c.cell() > 127) {
            return true;
        }
    }

    return false;
}

static bool isMixedCase(const QString &s)
{
    const auto upperCount = std::count_if(s.begin(), s.end(), [](auto c) { return c.isUpper(); });
    return upperCount != s.size() && upperCount != 0;
}

/** Assuming both sides refer to the same thing, this tries to find the "better" one. */
static QString mergeString(const QString &lhs, const QString &rhs)
{
    // prefer the one that exists at all
    if (lhs.isEmpty()) {
        return rhs;
    }
    if (rhs.isEmpty()) {
        return lhs;
    }

    // prefer Unicode over ASCII normalization
    const auto lhsNonAscii = containsNonAscii(lhs);
    const auto rhsNonAscii = containsNonAscii(rhs);
    if (lhsNonAscii && !rhsNonAscii) {
        return lhs;
    }
    if (!lhsNonAscii && rhsNonAscii) {
        return rhs;
    }

    // prefer better casing
    const auto lhsMixedCase = isMixedCase(lhs);
    const auto rhsMixedCase = isMixedCase(rhs);
    if (lhsMixedCase && !rhsMixedCase) {
        return lhs;
    }
    if (!lhsMixedCase && rhsMixedCase) {
        return rhs;
    }

    // prefer longer == more detailed version
    if (rhs.size() < lhs.size()) {
        return lhs;
    }
    return rhs;
}
497
498
499
500

static Airline mergeValue(const Airline &lhs, const Airline &rhs)
{
    auto a = JsonLdDocument::apply(lhs, rhs).value<Airline>();
Volker Krause's avatar
Volker Krause committed
501
    a.setName(mergeString(lhs.name(), rhs.name()));
502
503
504
505
506
507
508
509
510
    return a;
}

static QDateTime mergeValue(const QDateTime &lhs, const QDateTime &rhs)
{
    // prefer value with timezone
    return lhs.isValid() && lhs.timeSpec() == Qt::TimeZone && rhs.timeSpec() != Qt::TimeZone ? lhs : rhs;
}

Volker Krause's avatar
Volker Krause committed
511
512
513
514
515
516
517
518
519
static Person mergeValue(const Person &lhs, const Person &rhs)
{
    auto p = JsonLdDocument::apply(lhs, rhs).value<Person>();
    p.setFamilyName(mergeString(lhs.familyName(), rhs.familyName()));
    p.setGivenName(mergeString(lhs.givenName(), rhs.givenName()));
    p.setName(mergeString(lhs.name(), rhs.name()));
    return p;
}

520
521
522
523
524
525
526
527
528
529
static Ticket mergeValue(const Ticket &lhs, const Ticket &rhs)
{
    auto t = JsonLdDocument::apply(lhs, rhs).value<Ticket>();
    // prefer barcode ticket tokens over URLs
    if (t.ticketTokenType() == Ticket::Url && lhs.ticketTokenType() != Ticket::Url && lhs.ticketTokenType() != Ticket::Unknown) {
        t.setTicketToken(lhs.ticketToken());
    }
    return t;
}

530
static bool checkValueIsNull(const QVariant &v)
Volker Krause's avatar
Volker Krause committed
531
532
533
534
535
536
537
{
    if (v.type() == qMetaTypeId<float>()) {
        return std::isnan(v.toFloat());
    }
    return v.isNull();
}

538
539
540
541
542
543
544
545
546
547
548
549
550
QVariant MergeUtil::merge(const QVariant &lhs, const QVariant &rhs)
{
    if (rhs.isNull()) {
        return lhs;
    }
    if (lhs.isNull()) {
        return rhs;
    }
    if (lhs.userType() != rhs.userType()) {
        qCWarning(Log) << "type mismatch during merging:" << lhs << rhs;
        return {};
    }

551
552
553
554
555
556
557
558
559
    // prefer the element with the newer mtime, if we have that information
    if (JsonLd::canConvert<Reservation>(lhs) && JsonLd::canConvert<Reservation>(rhs)) {
        const auto lhsDt = JsonLd::convert<Reservation>(lhs).modifiedTime();
        const auto rhsDt = JsonLd::convert<Reservation>(rhs).modifiedTime();
        if (lhsDt.isValid() && rhsDt.isValid() && rhsDt < lhsDt) {
            return MergeUtil::merge(rhs, lhs);
        }
    }

560
561
562
563
564
565
566
567
568
569
570
571
572
573
    auto res = lhs;
    const auto mo = QMetaType(res.userType()).metaObject();
    for (int i = 0; i < mo->propertyCount(); ++i) {
        const auto prop = mo->property(i);
        if (!prop.isStored()) {
            continue;
        }

        auto lv = prop.readOnGadget(lhs.constData());
        auto rv = prop.readOnGadget(rhs.constData());
        auto mt = rv.userType();

        if (mt == qMetaTypeId<Airline>()) {
            rv = mergeValue(lv.value<Airline>(), rv.value<Airline>());
Volker Krause's avatar
Volker Krause committed
574
575
        } else if (mt == qMetaTypeId<Person>()) {
            rv = mergeValue(lv.value<Person>(), rv.value<Person>());
576
577
        } else if (mt == qMetaTypeId<QDateTime>()) {
            rv = mergeValue(lv.toDateTime(), rv.toDateTime());
578
579
        } else if (mt == qMetaTypeId<Ticket>()) {
            rv = mergeValue(lv.value<Ticket>(), rv.value<Ticket>());
580
581
582
583
        } else if (QMetaType(mt).metaObject()) {
            rv = merge(prop.readOnGadget(lhs.constData()), rv);
        }

584
        if (!checkValueIsNull(rv)) {
585
586
587
588
589
590
            prop.writeOnGadget(res.data(), rv);
        }
    }

    return res;
}
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605

bool isMinimalCancelationFor(const QVariant &r, const Reservation &cancel)
{
    const auto res = JsonLd::convert<Reservation>(r);
    if (res.reservationStatus() == Reservation::ReservationCancelled || cancel.reservationStatus() != Reservation::ReservationCancelled) {
        return false;
    }
    if (!equalAndPresent(res.reservationNumber(), cancel.reservationNumber())) {
        return false;
    }
    if (!cancel.modifiedTime().isValid() || !cancel.reservationFor().isNull()) {
        return false;
    }
    return SortUtil::startDateTime(r) > cancel.modifiedTime();
}