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 "locationutil.h"
11 #include "stringutil.h"
12 #include "sortutil.h"
13
14 #include <KItinerary/BusTrip>
15 #include <KItinerary/Event>
16 #include <KItinerary/Flight>
17 #include <KItinerary/JsonLdDocument>
18 #include <KItinerary/RentalCar>
19 #include <KItinerary/Organization>
20 #include <KItinerary/Place>
21 #include <KItinerary/Person>
22 #include <KItinerary/Reservation>
23 #include <KItinerary/Taxi>
24 #include <KItinerary/Ticket>
25 #include <KItinerary/TrainTrip>
26 #include <KItinerary/Visit>
27
28 #include <QDate>
29 #include <QDebug>
30 #include <QMetaObject>
31 #include <QMetaProperty>
32 #include <QVariant>
33
34 #include <cmath>
35
36 using namespace KItinerary;
37
38 /* Checks that @p lhs and @p rhs are non-empty and equal. */
equalAndPresent(const QString & lhs,const QString & rhs,Qt::CaseSensitivity caseSensitive=Qt::CaseSensitive)39 static bool equalAndPresent(const QString &lhs, const QString &rhs, Qt::CaseSensitivity caseSensitive = Qt::CaseSensitive)
40 {
41 return !lhs.isEmpty() && (lhs.compare(rhs, caseSensitive) == 0);
42 }
43 template <typename T>
equalAndPresent(const T & lhs,const T & rhs)44 static bool equalAndPresent(const T &lhs, const T &rhs)
45 {
46 return lhs.isValid() && lhs == rhs;
47 }
48
49 /* Checks that @p lhs and @p rhs are not non-equal if both values are set. */
conflictIfPresent(const QString & lhs,const QString & rhs,Qt::CaseSensitivity caseSensitive=Qt::CaseSensitive)50 static bool conflictIfPresent(const QString &lhs, const QString &rhs, Qt::CaseSensitivity caseSensitive = Qt::CaseSensitive)
51 {
52 return !lhs.isEmpty() && !rhs.isEmpty() && lhs.compare(rhs, caseSensitive) != 0;
53 }
54 template <typename T>
conflictIfPresent(const T & lhs,const T & rhs)55 static bool conflictIfPresent(const T &lhs, const T &rhs)
56 {
57 return lhs.isValid() && rhs.isValid() && lhs != rhs;
58 }
59
60 static bool isSameFlight(const Flight &lhs, const Flight &rhs);
61 static bool isSameTrainTrip(const TrainTrip &lhs, const TrainTrip &rhs);
62 static bool isSameBusTrip(const BusTrip &lhs, const BusTrip &rhs);
63 static bool isSameLodingBusiness(const LodgingBusiness &lhs, const LodgingBusiness &rhs);
64 static bool isSameFoodEstablishment(const FoodEstablishment &lhs, const FoodEstablishment &rhs);
65 static bool isSameTouristAttractionVisit(const TouristAttractionVisit &lhs, const TouristAttractionVisit &rhs);
66 static bool isSameTouristAttraction(const TouristAttraction &lhs, const TouristAttraction &rhs);
67 static bool isSameEvent(const Event &lhs, const Event &rhs);
68 static bool isSameRentalCar(const RentalCar &lhs, const RentalCar &rhs);
69 static bool isSameTaxiTrip(const Taxi &lhs, const Taxi &rhs);
70 static bool isMinimalCancelationFor(const QVariant &r, const Reservation &cancel);
71
isSame(const QVariant & lhs,const QVariant & rhs)72 bool MergeUtil::isSame(const QVariant& lhs, const QVariant& rhs)
73 {
74 if (lhs.isNull() || rhs.isNull()) {
75 return false;
76 }
77 if (lhs.userType() != rhs.userType()) {
78 return false;
79 }
80
81 // for all reservations check underName and ticket
82 if (JsonLd::canConvert<Reservation>(lhs)) {
83 const auto lhsRes = JsonLd::convert<Reservation>(lhs);
84 const auto rhsRes = JsonLd::convert<Reservation>(rhs);
85
86 // for all: underName either matches or is not set
87 const auto lhsUN = lhsRes.underName().value<Person>();
88 const auto rhsUN = rhsRes.underName().value<Person>();
89 if (!lhsUN.name().isEmpty() && !rhsUN.name().isEmpty() && !isSamePerson(lhsUN, rhsUN)) {
90 return false;
91 }
92
93 const auto lhsTicket = lhsRes.reservedTicket().value<Ticket>();
94 const auto rhsTicket = rhsRes.reservedTicket().value<Ticket>();
95 if (conflictIfPresent(lhsTicket.ticketedSeat().seatNumber(), rhsTicket.ticketedSeat().seatNumber(), Qt::CaseInsensitive)) {
96 return false;
97 }
98 // flight ticket tokens (IATA BCBP) can differ, so we need to compare the relevant bits in them manually
99 // this however happens automatically as they are unpacked to other fields by post-processing
100 // so we can simply skip this here for flights
101 if (!JsonLd::isA<FlightReservation>(lhs) && conflictIfPresent(lhsTicket.ticketTokenData(), rhsTicket.ticketTokenData())) {
102 return false;
103 }
104
105 // one side is a minimal cancellation, matches the reservation number and has a plausible modification time
106 // in this case don't bother comparing content (which will fail), we accept this directly
107 if (isMinimalCancelationFor(lhs, rhsRes) || isMinimalCancelationFor(rhs, lhsRes)) {
108 return true;
109 }
110 }
111
112 // flight: booking ref, flight number and departure day match
113 if (JsonLd::isA<FlightReservation>(lhs)) {
114 const auto lhsRes = lhs.value<FlightReservation>();
115 const auto rhsRes = rhs.value<FlightReservation>();
116 if (conflictIfPresent(lhsRes.reservationNumber(), rhsRes.reservationNumber()) || conflictIfPresent(lhsRes.passengerSequenceNumber(), rhsRes.passengerSequenceNumber())) {
117 return false;
118 }
119 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor());
120 }
121 if (JsonLd::isA<Flight>(lhs)) {
122 const auto lhsFlight = lhs.value<Flight>();
123 const auto rhsFlight = rhs.value<Flight>();
124 return isSameFlight(lhsFlight, rhsFlight);
125 }
126
127 // train: booking ref, train number and depature day match
128 if (JsonLd::isA<TrainReservation>(lhs)) {
129 const auto lhsRes = lhs.value<TrainReservation>();
130 const auto rhsRes = rhs.value<TrainReservation>();
131 if (conflictIfPresent(lhsRes.reservationNumber(), rhsRes.reservationNumber())) {
132 return false;
133 }
134 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor());
135 }
136 if (JsonLd::isA<TrainTrip>(lhs)) {
137 const auto lhsTrip = lhs.value<TrainTrip>();
138 const auto rhsTrip = rhs.value<TrainTrip>();
139 return isSameTrainTrip(lhsTrip, rhsTrip);
140 }
141
142 // bus: booking ref, number and depature time match
143 if (JsonLd::isA<BusReservation>(lhs)) {
144 const auto lhsRes = lhs.value<BusReservation>();
145 const auto rhsRes = rhs.value<BusReservation>();
146 if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
147 return false;
148 }
149 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor());
150 }
151 if (JsonLd::isA<BusTrip>(lhs)) {
152 const auto lhsTrip = lhs.value<BusTrip>();
153 const auto rhsTrip = rhs.value<BusTrip>();
154 return isSameBusTrip(lhsTrip, rhsTrip);
155 }
156
157 // hotel: booking ref, checkin day, name match
158 if (JsonLd::isA<LodgingReservation>(lhs)) {
159 const auto lhsRes = lhs.value<LodgingReservation>();
160 const auto rhsRes = rhs.value<LodgingReservation>();
161 if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
162 return false;
163 }
164 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.checkinTime().date() == rhsRes.checkinTime().date();
165 }
166 if (JsonLd::isA<LodgingBusiness>(lhs)) {
167 const auto lhsHotel = lhs.value<LodgingBusiness>();
168 const auto rhsHotel = rhs.value<LodgingBusiness>();
169 return isSameLodingBusiness(lhsHotel, rhsHotel);
170 }
171
172 // Rental Car
173 if (JsonLd::isA<RentalCarReservation>(lhs)) {
174 const auto lhsRes = lhs.value<RentalCarReservation>();
175 const auto rhsRes = rhs.value<RentalCarReservation>();
176 if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
177 return false;
178 }
179 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.pickupTime().date() == rhsRes.pickupTime().date();
180 }
181 if (JsonLd::isA<RentalCar>(lhs)) {
182 const auto lhsEv = lhs.value<RentalCar>();
183 const auto rhsEv = rhs.value<RentalCar>();
184 return isSameRentalCar(lhsEv, rhsEv);
185 }
186
187 // Taxi
188 if (JsonLd::isA<TaxiReservation>(lhs)) {
189 const auto lhsRes = lhs.value<TaxiReservation>();
190 const auto rhsRes = rhs.value<TaxiReservation>();
191 if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
192 return false;
193 }
194 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.pickupTime().date() == rhsRes.pickupTime().date();
195 }
196 if (JsonLd::isA<Taxi>(lhs)) {
197 const auto lhsEv = lhs.value<Taxi>();
198 const auto rhsEv = rhs.value<Taxi>();
199 return isSameTaxiTrip(lhsEv, rhsEv);
200 }
201
202 // restaurant reservation: same restaurant, same booking ref, same day
203 if (JsonLd::isA<FoodEstablishmentReservation>(lhs)) {
204 const auto lhsRes = lhs.value<FoodEstablishmentReservation>();
205 const auto rhsRes = rhs.value<FoodEstablishmentReservation>();
206 if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
207 return false;
208 }
209 auto endTime = rhsRes.endTime();
210 if (!endTime.isValid()) {
211 endTime = QDateTime(rhsRes.startTime().date(), QTime(23, 59, 59));
212 }
213
214 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.startTime().date() == endTime.date();
215 }
216 if (JsonLd::isA<FoodEstablishment>(lhs)) {
217 const auto lhsRestaurant = lhs.value<FoodEstablishment>();
218 const auto rhsRestaurant = rhs.value<FoodEstablishment>();
219 return isSameFoodEstablishment(lhsRestaurant, rhsRestaurant);
220 }
221
222 // event reservation
223 if (JsonLd::isA<EventReservation>(lhs)) {
224 const auto lhsRes = lhs.value<EventReservation>();
225 const auto rhsRes = rhs.value<EventReservation>();
226 if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
227 return false;
228 }
229 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor());
230 }
231 if (JsonLd::isA<Event>(lhs)) {
232 const auto lhsEv = lhs.value<Event>();
233 const auto rhsEv = rhs.value<Event>();
234 return isSameEvent(lhsEv, rhsEv);
235 }
236
237 // tourist attraction visit
238 if (JsonLd::isA<TouristAttractionVisit>(lhs)) {
239 const auto l = lhs.value<TouristAttractionVisit>();
240 const auto r = rhs.value<TouristAttractionVisit>();
241 return isSameTouristAttractionVisit(l, r);
242 }
243
244 return true;
245 }
246
isSameFlight(const Flight & lhs,const Flight & rhs)247 static bool isSameFlight(const Flight& lhs, const Flight& rhs)
248 {
249 // if there is a conflict on where this is going, or when, this is obviously not the same flight
250 if (conflictIfPresent(lhs.departureAirport().iataCode(), rhs.departureAirport().iataCode()) ||
251 conflictIfPresent(lhs.arrivalAirport().iataCode(), rhs.arrivalAirport().iataCode()) ||
252 !equalAndPresent(lhs.departureDay(), rhs.departureDay())) {
253 return false;
254 }
255
256 // same flight number and airline (on the same day) -> we assume same flight
257 if (equalAndPresent(lhs.flightNumber(), rhs.flightNumber()) && equalAndPresent(lhs.airline().iataCode(), rhs.airline().iataCode())) {
258 return true;
259 }
260
261 // we get here if we have matching origin/destination on the same day, but mismatching flight numbers
262 // so this might be a codeshare flight
263 // our caller checks for matching booking ref, so just look for a few counter-indicators here
264 // (that is, if this is ever made available as standalone API, the last return should not be true)
265 if (conflictIfPresent(lhs.departureTime(), rhs.departureTime())) {
266 return false;
267 }
268
269 return true;
270 }
271
272 // see kpublictrainport, line.cpp
273 template <typename Iter>
isSameLineName(const Iter & lBegin,const Iter & lEnd,const Iter & rBegin,const Iter & rEnd)274 static bool isSameLineName(const Iter &lBegin, const Iter &lEnd, const Iter &rBegin, const Iter &rEnd)
275 {
276 auto lIt = lBegin;
277 auto rIt = rBegin;
278 while (lIt != lEnd && rIt != rEnd) {
279 // ignore spaces etc.
280 if (!(*lIt).isLetter() && !(*lIt).isDigit()) {
281 ++lIt;
282 continue;
283 }
284 if (!(*rIt).isLetter() && !(*rIt).isDigit()) {
285 ++rIt;
286 continue;
287 }
288
289 if ((*lIt).toCaseFolded() != (*rIt).toCaseFolded()) {
290 return false;
291 }
292
293 ++lIt;
294 ++rIt;
295 }
296
297 if (lIt == lEnd && rIt == rEnd) { // both inputs fully consumed, and no mismatch found
298 return true;
299 }
300
301 // one input is prefix of the other, that is ok if there's a separator
302 return (lIt != lEnd && (*lIt).isSpace()) || (rIt != rEnd && (*rIt).isSpace());
303 }
304
isSameLineName(const QString & lhs,const QString & rhs)305 static bool isSameLineName(const QString &lhs, const QString &rhs)
306 {
307 return isSameLineName(lhs.begin(), lhs.end(), rhs.begin(), rhs.end())
308 || isSameLineName(lhs.rbegin(), lhs.rend(), rhs.rbegin(), rhs.rend());
309 }
310
isSameTrainTrip(const TrainTrip & lhs,const TrainTrip & rhs)311 static bool isSameTrainTrip(const TrainTrip &lhs, const TrainTrip &rhs)
312 {
313 if (lhs.departureDay() != rhs.departureDay()) {
314 return false;
315 }
316
317 // for unbound tickets, comparing the line number below won't help
318 // so we have to use the slightly less robust location comparison
319 if (!lhs.departureTime().isValid() && !rhs.departureTime().isValid()) {
320 qCDebug(CompareLog) << "unbound trip" << lhs.departureStation().name() << rhs.departureStation().name() << lhs.arrivalStation().name() << rhs.arrivalStation().name();
321 return lhs.departureStation().name() == rhs.departureStation().name() && lhs.arrivalStation().name() == rhs.arrivalStation().name();
322 } else if (!equalAndPresent(lhs.departureTime(), rhs.departureTime())) {
323 return false;
324 }
325
326 if (lhs.trainNumber().isEmpty() || rhs.trainNumber().isEmpty()) {
327 qCDebug(CompareLog) << "missing train number" << lhs.trainNumber() << rhs.trainNumber();
328 return false;
329 }
330
331 const auto isSameLine = isSameLineName(lhs.trainNumber(), rhs.trainNumber());
332 qCDebug(CompareLog) << "left:" << lhs.trainName() << lhs.trainNumber() << lhs.departureTime();
333 qCDebug(CompareLog) << "right:" << rhs.trainName() << rhs.trainNumber() << rhs.departureTime();
334 qCDebug(CompareLog) << "same line:" << isSameLine;
335 return !conflictIfPresent(lhs.trainName(),rhs.trainName()) && isSameLine && lhs.departureTime().date() == rhs.departureTime().date();
336 }
337
isSameBusTrip(const BusTrip & lhs,const BusTrip & rhs)338 static bool isSameBusTrip(const BusTrip &lhs, const BusTrip &rhs)
339 {
340 if (lhs.busNumber().isEmpty() || rhs.busNumber().isEmpty()) {
341 return false;
342 }
343
344 return lhs.busName() == rhs.busName() && lhs.busNumber() == rhs.busNumber() && lhs.departureTime() == rhs.departureTime();
345 }
346
isSameLodingBusiness(const LodgingBusiness & lhs,const LodgingBusiness & rhs)347 static bool isSameLodingBusiness(const LodgingBusiness &lhs, const LodgingBusiness &rhs)
348 {
349 if (lhs.name().isEmpty() || rhs.name().isEmpty()) {
350 return false;
351 }
352
353 return lhs.name() == rhs.name();
354 }
355
isSameFoodEstablishment(const FoodEstablishment & lhs,const FoodEstablishment & rhs)356 static bool isSameFoodEstablishment(const FoodEstablishment &lhs, const FoodEstablishment &rhs)
357 {
358 if (lhs.name().isEmpty() || rhs.name().isEmpty()) {
359 return false;
360 }
361
362 return lhs.name() == rhs.name();
363 }
364
isSameTouristAttractionVisit(const TouristAttractionVisit & lhs,const TouristAttractionVisit & rhs)365 static bool isSameTouristAttractionVisit(const TouristAttractionVisit &lhs, const TouristAttractionVisit &rhs)
366 {
367 return lhs.arrivalTime() == rhs.arrivalTime() && isSameTouristAttraction(lhs.touristAttraction(), rhs.touristAttraction());
368 }
369
isSameTouristAttraction(const TouristAttraction & lhs,const TouristAttraction & rhs)370 static bool isSameTouristAttraction(const TouristAttraction &lhs, const TouristAttraction &rhs)
371 {
372 return lhs.name() == rhs.name();
373 }
374
375 // compute the "difference" between @p lhs and @p rhs
diffString(const QString & lhs,const QString & rhs)376 static QString diffString(const QString &lhs, const QString &rhs)
377 {
378 QString diff;
379 // this is just a basic linear-time heuristic, this would need to be more something like
380 // the Levenstein Distance algorithm
381 for (int i = 0, j = 0; i < lhs.size() || j < rhs.size();) {
382 if (i < lhs.size() && j < rhs.size() && StringUtil::normalize(lhs[i]) == StringUtil::normalize(rhs[j])) {
383 ++i;
384 ++j;
385 continue;
386 }
387 if ((j < rhs.size() && (lhs.size() < rhs.size() || (lhs.size() == rhs.size() && j < i))) || i == lhs.size()) {
388 diff += rhs[j];
389 ++j;
390 } else {
391 diff += lhs[i];
392 ++i;
393 }
394 }
395 return diff.trimmed();
396 }
397
isNameEqualish(const QString & lhs,const QString & rhs)398 static bool isNameEqualish(const QString &lhs, const QString &rhs)
399 {
400 if (lhs.isEmpty() || rhs.isEmpty()) {
401 return false;
402 }
403
404 auto diff = diffString(lhs, rhs).toUpper();
405
406 // remove honoric prefixes from the diff, in case the previous check didn't catch that
407 diff.remove(QLatin1String("MRS"));
408 diff.remove(QLatin1String("MR"));
409 diff.remove(QLatin1String("MS"));
410
411 // if there's letters in the diff, we assume this is different
412 for (const auto c : diff) {
413 if (c.isLetter()) {
414 return false;
415 }
416 }
417
418 return true;
419 }
420
isSamePerson(const Person & lhs,const Person & rhs)421 bool MergeUtil::isSamePerson(const Person& lhs, const Person& rhs)
422 {
423 return isNameEqualish(lhs.name(), rhs.name()) ||
424 (isNameEqualish(lhs.givenName(), rhs.givenName()) && isNameEqualish(lhs.familyName(), rhs.familyName()));
425 }
426
isSameEvent(const Event & lhs,const Event & rhs)427 static bool isSameEvent(const Event &lhs, const Event &rhs)
428 {
429 if (!equalAndPresent(lhs.startDate(), rhs.startDate())) {
430 return false;
431 }
432
433 // event names can contain additional qualifiers, like for Adult/Child tickets,
434 // those don't change the event though
435 const auto namePrefix = StringUtil::prefixSimilarity(lhs.name(), rhs.name());
436 return namePrefix == 1.0f || (namePrefix > 0.65f && LocationUtil::isSameLocation(lhs.location(), rhs.location(), LocationUtil::Exact));
437 }
438
isSameRentalCar(const RentalCar & lhs,const RentalCar & rhs)439 static bool isSameRentalCar(const RentalCar &lhs, const RentalCar &rhs)
440 {
441 return lhs.name() == rhs.name();
442 }
443
isSameTaxiTrip(const Taxi & lhs,const Taxi & rhs)444 static bool isSameTaxiTrip(const Taxi &lhs, const Taxi &rhs)
445 {
446 //TODO verify
447 return lhs.name() == rhs.name();
448 }
449
mergeValue(const Airline & lhs,const Airline & rhs)450 static Airline mergeValue(const Airline &lhs, const Airline &rhs)
451 {
452 auto a = JsonLdDocument::apply(lhs, rhs).value<Airline>();
453 a.setName(StringUtil::betterString(lhs.name(), rhs.name()).toString());
454 return a;
455 }
456
mergeValue(const QDateTime & lhs,const QDateTime & rhs)457 static QDateTime mergeValue(const QDateTime &lhs, const QDateTime &rhs)
458 {
459 // prefer value with timezone
460 return lhs.isValid() && lhs.timeSpec() == Qt::TimeZone && rhs.timeSpec() != Qt::TimeZone ? lhs : rhs;
461 }
462
mergeValue(const Person & lhs,const Person & rhs)463 static Person mergeValue(const Person &lhs, const Person &rhs)
464 {
465 auto p = JsonLdDocument::apply(lhs, rhs).value<Person>();
466 p.setFamilyName(StringUtil::betterString(lhs.familyName(), rhs.familyName()).toString());
467 p.setGivenName(StringUtil::betterString(lhs.givenName(), rhs.givenName()).toString());
468 p.setName(StringUtil::betterString(lhs.name(), rhs.name()).toString());
469 return p;
470 }
471
mergeValue(const Ticket & lhs,const Ticket & rhs)472 static Ticket mergeValue(const Ticket &lhs, const Ticket &rhs)
473 {
474 auto t = JsonLdDocument::apply(lhs, rhs).value<Ticket>();
475 // prefer barcode ticket tokens over URLs
476 if (t.ticketTokenType() == Ticket::Url && lhs.ticketTokenType() != Ticket::Url && lhs.ticketTokenType() != Ticket::Unknown) {
477 t.setTicketToken(lhs.ticketToken());
478 }
479 return t;
480 }
481
checkValueIsNull(const QVariant & v)482 static bool checkValueIsNull(const QVariant &v)
483 {
484 if (v.type() == qMetaTypeId<float>()) {
485 return std::isnan(v.toFloat());
486 }
487 return v.isNull();
488 }
489
merge(const QVariant & lhs,const QVariant & rhs)490 QVariant MergeUtil::merge(const QVariant &lhs, const QVariant &rhs)
491 {
492 if (rhs.isNull()) {
493 return lhs;
494 }
495 if (lhs.isNull()) {
496 return rhs;
497 }
498 if (lhs.userType() != rhs.userType()) {
499 qCWarning(Log) << "type mismatch during merging:" << lhs << rhs;
500 return {};
501 }
502
503 // prefer the element with the newer mtime, if we have that information
504 if (JsonLd::canConvert<Reservation>(lhs) && JsonLd::canConvert<Reservation>(rhs)) {
505 const auto lhsDt = JsonLd::convert<Reservation>(lhs).modifiedTime();
506 const auto rhsDt = JsonLd::convert<Reservation>(rhs).modifiedTime();
507 if (lhsDt.isValid() && rhsDt.isValid() && rhsDt < lhsDt) {
508 return MergeUtil::merge(rhs, lhs);
509 }
510 }
511
512 auto res = lhs;
513 const auto mo = QMetaType(res.userType()).metaObject();
514 for (int i = 0; i < mo->propertyCount(); ++i) {
515 const auto prop = mo->property(i);
516 if (!prop.isStored()) {
517 continue;
518 }
519
520 auto lv = prop.readOnGadget(lhs.constData());
521 auto rv = prop.readOnGadget(rhs.constData());
522 auto mt = rv.userType();
523
524 if (mt == qMetaTypeId<Airline>()) {
525 rv = mergeValue(lv.value<Airline>(), rv.value<Airline>());
526 } else if (mt == qMetaTypeId<Person>()) {
527 rv = mergeValue(lv.value<Person>(), rv.value<Person>());
528 } else if (mt == qMetaTypeId<QDateTime>()) {
529 rv = mergeValue(lv.toDateTime(), rv.toDateTime());
530 } else if (mt == qMetaTypeId<Ticket>()) {
531 rv = mergeValue(lv.value<Ticket>(), rv.value<Ticket>());
532 } else if (QMetaType(mt).metaObject()) {
533 rv = merge(prop.readOnGadget(lhs.constData()), rv);
534 }
535
536 if (!checkValueIsNull(rv)) {
537 prop.writeOnGadget(res.data(), rv);
538 }
539 }
540
541 return res;
542 }
543
isMinimalCancelationFor(const QVariant & r,const Reservation & cancel)544 bool isMinimalCancelationFor(const QVariant &r, const Reservation &cancel)
545 {
546 const auto res = JsonLd::convert<Reservation>(r);
547 if (res.reservationStatus() == Reservation::ReservationCancelled || cancel.reservationStatus() != Reservation::ReservationCancelled) {
548 return false;
549 }
550 if (!equalAndPresent(res.reservationNumber(), cancel.reservationNumber())) {
551 return false;
552 }
553 if (!cancel.modifiedTime().isValid() || !cancel.reservationFor().isNull()) {
554 return false;
555 }
556 return SortUtil::startDateTime(r) > cancel.modifiedTime();
557 }
558