KItinerary

mergeutil.cpp
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#include "tickettokencomparator_p.h"
14
15#include <KItinerary/BoatTrip>
16#include <KItinerary/BusTrip>
17#include <KItinerary/Event>
18#include <KItinerary/Flight>
19#include <KItinerary/JsonLdDocument>
20#include <KItinerary/RentalCar>
21#include <KItinerary/Organization>
22#include <KItinerary/Place>
23#include <KItinerary/Person>
24#include <KItinerary/Reservation>
25#include <KItinerary/Taxi>
26#include <KItinerary/Ticket>
27#include <KItinerary/TrainTrip>
28#include <KItinerary/Visit>
29
30#include <QDate>
31#include <QDebug>
32#include <QMetaObject>
33#include <QMetaProperty>
34#include <QRegularExpression>
35#include <QTimeZone>
36
37#include <cmath>
38#include <cstring>
39#include <set>
40
41namespace KItinerary {
42struct CompareFunc {
43 int metaTypeId;
44 std::function<bool(const QVariant &, const QVariant &)> func;
45};
46
47static bool operator<(const CompareFunc &lhs, int rhs)
48{
49 return lhs.metaTypeId < rhs;
50}
51
52static std::vector<CompareFunc> s_mergeCompareFuncs;
53}
54
55using namespace KItinerary;
56
57/* Compare times without assuming times without a timezone are in the current time zone
58 * (they might be local to the destination instead).
59 */
60static bool dateTimeCompare(const QDateTime &lhs, const QDateTime &rhs)
61{
62 if (lhs == rhs) {
63 return true;
64 }
65 if (lhs.timeSpec() == Qt::LocalTime && rhs.timeSpec() != Qt::LocalTime) {
66 QDateTime dt(rhs);
67 dt.setTimeZone(QTimeZone::LocalTime);
68 return lhs == dt;
69 }
70 if (lhs.timeSpec() != Qt::LocalTime && rhs.timeSpec() == Qt::LocalTime) {
71 QDateTime dt(lhs);
72 dt.setTimeZone(QTimeZone::LocalTime);
73 return dt == rhs;
74 }
75 return false;
76}
77
78/* Checks that @p lhs and @p rhs are non-empty and equal. */
79static bool equalAndPresent(QStringView lhs, QStringView rhs, Qt::CaseSensitivity caseSensitive = Qt::CaseSensitive)
80{
81 return !lhs.isEmpty() && (lhs.compare(rhs, caseSensitive) == 0);
82}
83template <typename T>
84static typename std::enable_if<!std::is_same_v<T, QString>, bool>::type equalAndPresent(const T &lhs, const T &rhs)
85{
86 return lhs.isValid() && lhs == rhs;
87}
88static bool equalAndPresent(const QDateTime &lhs, const QDateTime &rhs)
89{
90 return lhs.isValid() && dateTimeCompare(lhs, rhs);
91}
92
93/* Checks that @p lhs and @p rhs are not non-equal if both values are set. */
94static bool conflictIfPresent(QStringView lhs, QStringView rhs, Qt::CaseSensitivity caseSensitive = Qt::CaseSensitive)
95{
96 return !lhs.isEmpty() && !rhs.isEmpty() && lhs.compare(rhs, caseSensitive) != 0;
97}
98template <typename T>
99static typename std::enable_if<!std::is_same_v<T, QString>, bool>::type conflictIfPresent(const T &lhs, const T &rhs)
100{
101 return lhs.isValid() && rhs.isValid() && lhs != rhs;
102}
103static bool conflictIfPresent(const QDateTime &lhs, const QDateTime &rhs)
104{
105 return lhs.isValid() && rhs.isValid() && !dateTimeCompare(lhs, rhs);
106}
107static bool conflictIfPresent(const Person &lhs, const Person &rhs)
108{
109 return !lhs.name().isEmpty() && !rhs.name().isEmpty() && !MergeUtil::isSamePerson(lhs, rhs);
110}
111
112static bool isSameFlight(const Flight &lhs, const Flight &rhs);
113static bool isSameTrainTrip(const TrainTrip &lhs, const TrainTrip &rhs);
114static bool isSameBusTrip(const BusTrip &lhs, const BusTrip &rhs);
115static bool isSameBoatTrip(const BoatTrip &lhs, const BoatTrip &rhs);
116static bool isSameLocalBusiness(const LocalBusiness &lhs, const LocalBusiness &rhs);
117static bool isSameTouristAttractionVisit(const TouristAttractionVisit &lhs, const TouristAttractionVisit &rhs);
118static bool isSameTouristAttraction(const TouristAttraction &lhs, const TouristAttraction &rhs);
119static bool isSameEvent(const Event &lhs, const Event &rhs);
120static bool isSameRentalCar(const RentalCar &lhs, const RentalCar &rhs);
121static bool isSameTaxiTrip(const Taxi &lhs, const Taxi &rhs);
122static bool isSameReservation(const Reservation &lhsRes, const Reservation &rhsRes);
123static bool isMinimalCancelationFor(const Reservation &res, const Reservation &cancel);
124
125bool isSameReservation(const Reservation &lhsRes, const Reservation &rhsRes)
126{
127 // underName either matches or is not set
128 if (conflictIfPresent(lhsRes.underName().value<Person>(), rhsRes.underName().value<Person>())
129 || conflictIfPresent(lhsRes.reservationNumber(), rhsRes.reservationNumber())) {
130 return false;
131 }
132
133 const auto lhsTicket = lhsRes.reservedTicket().value<Ticket>();
134 const auto rhsTicket = rhsRes.reservedTicket().value<Ticket>();
135 if (conflictIfPresent(lhsTicket.ticketedSeat().seatNumber(), rhsTicket.ticketedSeat().seatNumber(), Qt::CaseInsensitive)) {
136 return false;
137 }
138
139 return true;
140}
141
142bool isSubType(const QVariant &lhs, const QVariant &rhs)
143{
144 const auto lhsMt = QMetaType(lhs.userType());
145 const auto rhsMt = QMetaType(rhs.userType());
146 const auto lhsMo = lhsMt.metaObject();
147 const auto rhsMo = rhsMt.metaObject();
148 // for enums/flags, this is the enclosing meta object starting with Qt6!
149 if (!lhsMo || !rhsMo || (lhsMt.flags() & QMetaType::IsGadget) == 0 || (rhsMt.flags() & QMetaType::IsGadget) == 0) {
150 return false;
151 }
152 return lhsMo->inherits(rhsMo);
153}
154
155bool MergeUtil::isSame(const QVariant& lhs, const QVariant& rhs)
156{
157 if (lhs.isNull() || rhs.isNull()) {
158 return false;
159 }
160 if (!isSubType(lhs, rhs) && !isSubType(rhs, lhs)) {
161 return false;
162 }
163
164 // for all reservations check underName and ticket
166 const auto lhsRes = JsonLd::convert<Reservation>(lhs);
167 const auto rhsRes = JsonLd::convert<Reservation>(rhs);
168 if (!isSameReservation(lhsRes, rhsRes)) {
169 return false;
170 }
171
172 const auto lhsTicket = lhsRes.reservedTicket().value<Ticket>();
173 const auto rhsTicket = rhsRes.reservedTicket().value<Ticket>();
174 // flight ticket tokens (IATA BCBP) can differ, so we need to compare the relevant bits in them manually
175 // this however happens automatically as they are unpacked to other fields by post-processing
176 // so we can simply skip this here for flights
177 // for other ticket tokens (e.g. Renfe), shorter and longer versions of the same token exist as well
178 // so we look for matching prefixes here
179 if (!JsonLd::isA<FlightReservation>(lhs) && !TicketTokenComparator::isSame(lhsTicket.ticketTokenData(), rhsTicket.ticketTokenData())) {
180 return false;
181 }
182
183 // one side is a minimal cancellation, matches the reservation number and has a plausible modification time
184 // in this case don't bother comparing content (which will fail), we accept this directly
185 if (isMinimalCancelationFor(lhsRes, rhsRes) || isMinimalCancelationFor(rhsRes, lhsRes)) {
186 return true;
187 }
188 }
189
190 // flight: booking ref, flight number and departure day match
192 const auto lhsRes = lhs.value<FlightReservation>();
193 const auto rhsRes = rhs.value<FlightReservation>();
194 if (conflictIfPresent(lhsRes.reservationNumber(), rhsRes.reservationNumber()) || conflictIfPresent(lhsRes.passengerSequenceNumber(), rhsRes.passengerSequenceNumber())) {
195 return false;
196 }
197 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor());
198 }
199 if (JsonLd::isA<Flight>(lhs)) {
200 const auto lhsFlight = lhs.value<Flight>();
201 const auto rhsFlight = rhs.value<Flight>();
202 return isSameFlight(lhsFlight, rhsFlight);
203 }
204
205 // train: booking ref, train number and departure day match
207 const auto lhsRes = lhs.value<TrainReservation>();
208 const auto rhsRes = rhs.value<TrainReservation>();
209 if (conflictIfPresent(lhsRes.reservationNumber(), rhsRes.reservationNumber())) {
210 return false;
211 }
212 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor());
213 }
214 if (JsonLd::isA<TrainTrip>(lhs)) {
215 const auto lhsTrip = lhs.value<TrainTrip>();
216 const auto rhsTrip = rhs.value<TrainTrip>();
217 return isSameTrainTrip(lhsTrip, rhsTrip);
218 }
219
220 // bus: booking ref, number and depature time match
222 const auto lhsRes = lhs.value<BusReservation>();
223 const auto rhsRes = rhs.value<BusReservation>();
224 if (conflictIfPresent(lhsRes.reservationNumber(), rhsRes.reservationNumber())) {
225 return false;
226 }
227 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor());
228 }
229 if (JsonLd::isA<BusTrip>(lhs)) {
230 const auto lhsTrip = lhs.value<BusTrip>();
231 const auto rhsTrip = rhs.value<BusTrip>();
232 return isSameBusTrip(lhsTrip, rhsTrip);
233 }
234
235 // boat
237 const auto lhsRes = lhs.value<BoatReservation>();
238 const auto rhsRes = rhs.value<BoatReservation>();
239 if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
240 return false;
241 }
242 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor());
243 }
244 if (JsonLd::isA<BoatTrip>(lhs)) {
245 const auto lhsTrip = lhs.value<BoatTrip>();
246 const auto rhsTrip = rhs.value<BoatTrip>();
247 return isSameBoatTrip(lhsTrip, rhsTrip);
248 }
249
250 // hotel: booking ref, checkin day, name match
252 const auto lhsRes = lhs.value<LodgingReservation>();
253 const auto rhsRes = rhs.value<LodgingReservation>();
254 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.checkinTime().date() == rhsRes.checkinTime().date();
255 }
256
257 // Rental Car
259 const auto lhsRes = lhs.value<RentalCarReservation>();
260 const auto rhsRes = rhs.value<RentalCarReservation>();
261 if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
262 return false;
263 }
264 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.pickupTime().date() == rhsRes.pickupTime().date();
265 }
266 if (JsonLd::isA<RentalCar>(lhs)) {
267 const auto lhsEv = lhs.value<RentalCar>();
268 const auto rhsEv = rhs.value<RentalCar>();
269 return isSameRentalCar(lhsEv, rhsEv);
270 }
271
272 // Taxi
274 const auto lhsRes = lhs.value<TaxiReservation>();
275 const auto rhsRes = rhs.value<TaxiReservation>();
276 if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
277 return false;
278 }
279 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.pickupTime().date() == rhsRes.pickupTime().date();
280 }
281 if (JsonLd::isA<Taxi>(lhs)) {
282 const auto lhsEv = lhs.value<Taxi>();
283 const auto rhsEv = rhs.value<Taxi>();
284 return isSameTaxiTrip(lhsEv, rhsEv);
285 }
286
287 // restaurant reservation: same restaurant, same booking ref, same day
289 const auto lhsRes = lhs.value<FoodEstablishmentReservation>();
290 const auto rhsRes = rhs.value<FoodEstablishmentReservation>();
291 if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
292 return false;
293 }
294 auto endTime = rhsRes.endTime();
295 if (!endTime.isValid()) {
296 endTime = QDateTime(rhsRes.startTime().date(), QTime(23, 59, 59));
297 }
298
299 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.startTime().date() == endTime.date();
300 }
301
302 // generic busniess (hotel, restaurant)
304 const auto lhsBusiness = JsonLd::convert<LocalBusiness>(lhs);
305 const auto rhsBusiness = JsonLd::convert<LocalBusiness>(rhs);
306 return isSameLocalBusiness(lhsBusiness, rhsBusiness);
307 }
308
309 // event reservation
311 const auto lhsRes = lhs.value<EventReservation>();
312 const auto rhsRes = rhs.value<EventReservation>();
313 if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) {
314 return false;
315 }
316 return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) ||
317 // TODO replace by more general handling of incremental updates for existing elements,
318 // similar to how minimal cancellations are handled above
319 ((lhsRes.reservationFor().isNull() ^ rhsRes.reservationFor().isNull()) && equalAndPresent(lhsRes.reservationNumber(), rhsRes.reservationNumber()));
320 }
321 if (JsonLd::isA<Event>(lhs)) {
322 const auto lhsEv = lhs.value<Event>();
323 const auto rhsEv = rhs.value<Event>();
324 return isSameEvent(lhsEv, rhsEv);
325 }
326
327 // tourist attraction visit
329 const auto l = lhs.value<TouristAttractionVisit>();
330 const auto r = rhs.value<TouristAttractionVisit>();
331 return isSameTouristAttractionVisit(l, r);
332 }
333
334 // top-level tickets
335 if (JsonLd::isA<Ticket>(lhs)) {
336 const auto lhsTicket = lhs.value<Ticket>();
337 const auto rhsTicket = rhs.value<Ticket>();
338
339 if (conflictIfPresent(lhsTicket.underName(), rhsTicket.underName())
340 || conflictIfPresent(lhsTicket.ticketNumber(), rhsTicket.ticketNumber())
341 || conflictIfPresent(lhsTicket.name(), rhsTicket.name())
342 || conflictIfPresent(lhsTicket.validFrom(), rhsTicket.validFrom())
343 || !TicketTokenComparator::isSame(lhsTicket.ticketTokenData(), rhsTicket.ticketTokenData())
344 ) {
345 return false;
346 }
347 }
348
349 // program memberships
351 const auto lhsPM = lhs.value<ProgramMembership>();
352 const auto rhsPM = rhs.value<ProgramMembership>();
353
354 if (conflictIfPresent(lhsPM.member(), rhsPM.member())
355 || conflictIfPresent(lhsPM.programName(), rhsPM.programName())
356 || conflictIfPresent(lhsPM.membershipNumber(), rhsPM.membershipNumber())
357 || conflictIfPresent(lhsPM.validFrom(), rhsPM.validFrom())
358 || conflictIfPresent(lhsPM.validUntil(), rhsPM.validUntil())
359 || !TicketTokenComparator::isSame(lhsPM.tokenData(), rhsPM.tokenData())
360 ) {
361 return false;
362 }
363 }
364
365 if (lhs.userType() != rhs.userType()) {
366 return false;
367 }
368
369 // custom comparators
370 const auto it = std::lower_bound(s_mergeCompareFuncs.begin(), s_mergeCompareFuncs.end(), lhs.userType());
371 if (it != s_mergeCompareFuncs.end() && (*it).metaTypeId == lhs.userType()) {
372 return (*it).func(lhs, rhs);
373 }
374
375 return true;
376}
377
378static bool isSameFlight(const Flight& lhs, const Flight& rhs)
379{
380 // if there is a conflict on where this is going, or when, this is obviously not the same flight
381 if (conflictIfPresent(lhs.departureAirport().iataCode(), rhs.departureAirport().iataCode()) ||
382 conflictIfPresent(lhs.arrivalAirport().iataCode(), rhs.arrivalAirport().iataCode()) ||
383 conflictIfPresent(lhs.departureDay(), rhs.departureDay())) {
384 return false;
385 }
386
387 // same flight number and airline (on the same day) -> we assume same flight
388 // different airlines however could be codeshare flights
389 if (equalAndPresent(lhs.airline().iataCode(), rhs.airline().iataCode())) {
390 return equalAndPresent(lhs.flightNumber(), rhs.flightNumber());
391 }
392
393 // enforce same date of travel if the flight number has been inconclusive
394 if (!equalAndPresent(lhs.departureDay(), rhs.departureDay())) {
395 return false;
396 }
397
398 // we get here if we have matching origin/destination on the same day, but mismatching flight numbers
399 // so this might be a codeshare flight
400 // our caller checks for matching booking ref, so just look for a few counter-indicators here
401 // (that is, if this is ever made available as standalone API, the last return should not be true)
402 if (conflictIfPresent(lhs.departureTime(), rhs.departureTime())) {
403 return false;
404 }
405
406 return true;
407}
408
409// see kpublictrainport, line.cpp
410template <typename Iter>
411static bool isSameLineName(const Iter &lBegin, const Iter &lEnd, const Iter &rBegin, const Iter &rEnd)
412{
413 auto lIt = lBegin;
414 auto rIt = rBegin;
415 while (lIt != lEnd && rIt != rEnd) {
416 // ignore spaces etc.
417 if (!(*lIt).isLetter() && !(*lIt).isDigit()) {
418 ++lIt;
419 continue;
420 }
421 if (!(*rIt).isLetter() && !(*rIt).isDigit()) {
422 ++rIt;
423 continue;
424 }
425
426 if ((*lIt).toCaseFolded() != (*rIt).toCaseFolded()) {
427 return false;
428 }
429
430 ++lIt;
431 ++rIt;
432 }
433
434 if (lIt == lEnd && rIt == rEnd) { // both inputs fully consumed, and no mismatch found
435 return true;
436 }
437
438 // one input is prefix of the other, that is ok if there's a separator
439 return (lIt != lEnd && (*lIt).isSpace()) || (rIt != rEnd && (*rIt).isSpace());
440}
441
442static bool isSameTrainName(QStringView lhs, QStringView rhs)
443{
444 if (lhs.isEmpty() || rhs.isEmpty()) {
445 return false;
446 }
447 return lhs.startsWith(rhs) || rhs.startsWith(lhs);
448}
449
450static bool isSameLineName(const QString &lhs, const QString &rhs)
451{
452 if (isSameLineName(lhs.begin(), lhs.end(), rhs.begin(), rhs.end())
453 || isSameLineName(lhs.rbegin(), lhs.rend(), rhs.rbegin(), rhs.rend()))
454 {
455 return true;
456 }
457
458 // deal with both line and route numbers being specified
459 static QRegularExpression rx(QStringLiteral(R"(^(?<type>[A-Za-z]+) (?<line>\d+)(?: \‍((?<route>\d{4,})\))?$)"));
460 const auto lhsMatch = rx.match(lhs);
461 const auto rhsMatch = rx.match(rhs);
462 if (!lhsMatch.hasMatch() || !rhsMatch.hasMatch()) {
463 return false;
464 }
465 return isSameTrainName(lhsMatch.capturedView(u"type"), rhsMatch.capturedView(u"type")) && (
466 (equalAndPresent(lhsMatch.capturedView(u"line"), rhsMatch.capturedView(u"line")) && !conflictIfPresent(lhsMatch.capturedView(u"route"), rhsMatch.capturedView(u"route")))
467 || (equalAndPresent(lhsMatch.capturedView(u"line"), rhsMatch.capturedView(u"route")) && lhsMatch.capturedView(u"route").isEmpty())
468 || (equalAndPresent(lhsMatch.capturedView(u"route"), rhsMatch.capturedView(u"line")) && rhsMatch.capturedView(u"route").isEmpty())
469 );
470}
471
472static bool isSameTrainTrip(const TrainTrip &lhs, const TrainTrip &rhs)
473{
474 if (lhs.departureDay() != rhs.departureDay()) {
475 return false;
476 }
477
478 // for unbound tickets, comparing the line number below won't help
479 // so we have to use the slightly less robust location comparison
480 if (!lhs.departureTime().isValid() && !rhs.departureTime().isValid()) {
481 qCDebug(CompareLog) << "unbound trip" << lhs.departureStation().name() << rhs.departureStation().name() << lhs.arrivalStation().name() << rhs.arrivalStation().name();
482 return LocationUtil::isSameLocation(lhs.departureStation(), rhs.departureStation(), LocationUtil::Exact)
483 && LocationUtil::isSameLocation(lhs.arrivalStation(), rhs.arrivalStation(), LocationUtil::Exact);
484 } else if (lhs.departureTime().isValid() && rhs.departureTime().isValid()) {
485 // if we have both departure times, they have to match
486 qCDebug(CompareLog) << "departure time" << lhs.departureTime() << rhs.departureTime() <<equalAndPresent(lhs.departureTime(), rhs.departureTime());
487 if (!equalAndPresent(lhs.departureTime(), rhs.departureTime())) {
488 return false;
489 }
490 } else {
491 // only one departure time exists, so check if the locations don't conflict and rely on the train number for the rest
492 if (!LocationUtil::isSameLocation(lhs.departureStation(), rhs.departureStation(), LocationUtil::CityLevel)
493 || !LocationUtil::isSameLocation(lhs.arrivalStation(), rhs.arrivalStation(), LocationUtil::CityLevel)) {
494 return false;
495 }
496 }
497
498 // arrival times (when present) should either match exactly, or be almost the same at a matching arrival location
499 // (tickets even for the same connection booked on the same day sometimes have slight variation in the arrival time...)
500 if (conflictIfPresent(lhs.arrivalTime(), rhs.arrivalTime())) {
501 qCDebug(CompareLog) << "arrival time" << lhs.arrivalTime() << rhs.arrivalTime();
502 if (std::abs(lhs.arrivalTime().secsTo(rhs.arrivalTime())) > 180) {
503 return false;
504 }
505 if (!LocationUtil::isSameLocation(lhs.arrivalStation(), rhs.arrivalStation(), LocationUtil::Exact)) {
506 return false;
507 }
508 }
509
510 // if we don't have train numbers, also fall back to the less robust location comparison
511 if (lhs.trainNumber().isEmpty() || rhs.trainNumber().isEmpty()) {
512 qCDebug(CompareLog) << "missing train number" << lhs.trainNumber() << rhs.trainNumber();
513 return LocationUtil::isSameLocation(lhs.departureStation(), rhs.departureStation(), LocationUtil::Exact)
514 && LocationUtil::isSameLocation(lhs.arrivalStation(), rhs.arrivalStation(), LocationUtil::Exact);
515 }
516
517 const auto isSameLine = isSameLineName(lhs.trainNumber(), rhs.trainNumber());
518 qCDebug(CompareLog) << "left:" << lhs.trainName() << lhs.trainNumber() << lhs.departureTime();
519 qCDebug(CompareLog) << "right:" << rhs.trainName() << rhs.trainNumber() << rhs.departureTime();
520 qCDebug(CompareLog) << "same line:" << isSameLine;
521 return !conflictIfPresent(lhs.trainName(),rhs.trainName()) && isSameLine;
522}
523
524static bool isSameBusTrip(const BusTrip &lhs, const BusTrip &rhs)
525{
526 if (!equalAndPresent(lhs.departureTime(), rhs.departureTime())
527 || conflictIfPresent(lhs.busNumber(), rhs.busNumber())
528 || conflictIfPresent(lhs.arrivalTime(), rhs.arrivalTime())) {
529 return false;
530 }
531
532 return equalAndPresent(lhs.busNumber(), rhs.busNumber()) ||
533 (LocationUtil::isSameLocation(lhs.departureBusStop(), rhs.departureBusStop()) && LocationUtil::isSameLocation(lhs.arrivalBusStop(), rhs.arrivalBusStop()));
534}
535
536static bool isSameBoatTrip(const BoatTrip& lhs, const BoatTrip& rhs)
537{
538 return lhs.departureTime() == rhs.departureTime()
539 && LocationUtil::isSameLocation(lhs.departureBoatTerminal(), rhs.departureBoatTerminal())
540 && LocationUtil::isSameLocation(lhs.arrivalBoatTerminal(), rhs.arrivalBoatTerminal());
541}
542
543static bool isSameLocalBusiness(const LocalBusiness &lhs, const LocalBusiness &rhs)
544{
545 if (lhs.name().isEmpty() || rhs.name().isEmpty()) {
546 return false;
547 }
548
549 if (!LocationUtil::isSameLocation(lhs, rhs)) {
550 return false;
551 }
552
553 return lhs.name() == rhs.name();
554}
555
556static bool isSameTouristAttractionVisit(const TouristAttractionVisit &lhs, const TouristAttractionVisit &rhs)
557{
558 return lhs.arrivalTime() == rhs.arrivalTime() && isSameTouristAttraction(lhs.touristAttraction(), rhs.touristAttraction());
559}
560
561static bool isSameTouristAttraction(const TouristAttraction &lhs, const TouristAttraction &rhs)
562{
563 return lhs.name() == rhs.name();
564}
565
566// compute the "difference" between @p lhs and @p rhs
567static QString diffString(const QString &rawLhs, const QString &rawRhs)
568{
569 const auto lhs = StringUtil::normalize(rawLhs);
570 const auto rhs = StringUtil::normalize(rawRhs);
571
572 QString diff;
573 // this is just a basic linear-time heuristic, this would need to be more something like
574 // the Levenstein Distance algorithm
575 for (int i = 0, j = 0; i < lhs.size() || j < rhs.size();) {
576 if (i < lhs.size() && j < rhs.size() && lhs[i] == rhs[j]) {
577 ++i;
578 ++j;
579 continue;
580 }
581 if ((j < rhs.size() && (lhs.size() < rhs.size() || (lhs.size() == rhs.size() && j < i))) || i == lhs.size()) {
582 diff += rhs[j];
583 ++j;
584 } else {
585 diff += lhs[i];
586 ++i;
587 }
588 }
589 return diff.trimmed();
590}
591
592static bool isNameEqualish(const QString &lhs, const QString &rhs)
593{
594 if (lhs.isEmpty() || rhs.isEmpty()) {
595 return false;
596 }
597
598 auto diff = diffString(lhs, rhs).toUpper();
599
600 // remove honoric prefixes from the diff, in case the previous check didn't catch that
601 diff.remove(QLatin1StringView("MRS"));
602 diff.remove(QLatin1StringView("MR"));
603 diff.remove(QLatin1StringView("MS"));
604
605 // if there's letters in the diff, we assume this is different
606 for (const auto c : diff) {
607 if (c.isLetter()) {
608 return false;
609 }
610 }
611
612 return true;
613}
614
615static bool isPartialName(const Person &fullName, const Person &partialName)
616{
617 if (fullName.familyName().isEmpty() || fullName.givenName().isEmpty() || !fullName.givenName().startsWith(partialName.givenName(), Qt::CaseInsensitive)) {
618 return false;
619 }
620 return isNameEqualish(fullName.familyName(), partialName.name()) || isNameEqualish(fullName.familyName(), partialName.familyName());
621}
622
623bool MergeUtil::isSamePerson(const Person& lhs, const Person& rhs)
624{
625 if (isNameEqualish(lhs.name(), rhs.name()) ||
626 (isNameEqualish(lhs.givenName(), rhs.givenName()) && isNameEqualish(lhs.familyName(), rhs.familyName()))) {
627 return true;
628 }
629 if (isPartialName(lhs, rhs) || isPartialName(rhs, lhs)) {
630 return true;
631 }
632
633 const auto lhsNameT = StringUtil::transliterate(lhs.name());
634 const auto lhsGivenNameT = StringUtil::transliterate(lhs.givenName());
635 const auto lhsFamilyNameT = StringUtil::transliterate(lhs.familyName());
636
637 const auto rhsNameT = StringUtil::transliterate(rhs.name());
638 const auto rhsGivenNameT = StringUtil::transliterate(rhs.givenName());
639 const auto rhsFamilyNameT = StringUtil::transliterate(rhs.familyName());
640
641 return isNameEqualish(lhsNameT, rhsNameT) ||
642 (isNameEqualish(lhsGivenNameT, rhsGivenNameT) && isNameEqualish(lhsFamilyNameT, rhsFamilyNameT));
643}
644
645static bool isSameEvent(const Event &lhs, const Event &rhs)
646{
647 if (!equalAndPresent(lhs.startDate(), rhs.startDate())) {
648 return false;
649 }
650
651 // event names can contain additional qualifiers, like for Adult/Child tickets,
652 // those don't change the event though
653 const auto namePrefix = StringUtil::prefixSimilarity(lhs.name(), rhs.name());
654 return namePrefix == 1.0f || (namePrefix > 0.65f && LocationUtil::isSameLocation(lhs.location(), rhs.location(), LocationUtil::Exact));
655}
656
657static bool isSameRentalCar(const RentalCar &lhs, const RentalCar &rhs)
658{
659 return lhs.name() == rhs.name();
660}
661
662static bool isSameTaxiTrip(const Taxi &lhs, const Taxi &rhs)
663{
664 //TODO verify
665 return lhs.name() == rhs.name();
666}
667
668bool MergeUtil::isSameIncidence(const QVariant &lhs, const QVariant &rhs)
669{
671 return false;
672 }
673
674 // special case for LodgingReservation, their time range is in the Reservation object
676 if (MergeUtil::isSame(lhs, rhs)) { // incremental updates can have deviating times, that is ok
677 return true;
678 }
679 const auto lhsHotel = lhs.value<LodgingReservation>();
680 const auto rhsHotel = rhs.value<LodgingReservation>();
681 if (lhsHotel.checkinTime().date() != rhsHotel.checkinTime().date() ||
682 lhsHotel.checkoutTime().date() != rhsHotel.checkoutTime().date()) {
683 return false;
684 }
685 }
686
687 const auto lhsTrip = JsonLd::convert<Reservation>(lhs).reservationFor();
688 const auto rhsTrip = JsonLd::convert<Reservation>(rhs).reservationFor();
689 return MergeUtil::isSame(lhsTrip, rhsTrip);
690}
691
692static Airline mergeValue(const Airline &lhs, const Airline &rhs)
693{
694 auto a = JsonLdDocument::apply(lhs, rhs).value<Airline>();
695 a.setName(StringUtil::betterString(lhs.name(), rhs.name()).toString());
696 return a;
697}
698
699static QDateTime mergeValue(const QDateTime &lhs, const QDateTime &rhs)
700{
701 // if both sides have a timezone, prefer non-UTC
702 if (lhs.isValid() && lhs.timeSpec() == Qt::TimeZone && rhs.isValid() && rhs.timeSpec() == Qt::TimeZone) {
703 return rhs.timeZone() == QTimeZone::utc() ? lhs : rhs;
704 }
705 // prefer value with timezone
706 return lhs.isValid() && lhs.timeSpec() == Qt::TimeZone && rhs.timeSpec() != Qt::TimeZone ? lhs : rhs;
707}
708
709static Person mergeValue(const Person &lhs, const Person &rhs)
710{
711 auto p = JsonLdDocument::apply(lhs, rhs).value<Person>();
712 p.setFamilyName(StringUtil::betterString(lhs.familyName(), rhs.familyName()).toString());
713 p.setGivenName(StringUtil::betterString(lhs.givenName(), rhs.givenName()).toString());
714 p.setName(StringUtil::betterString(lhs.name(), rhs.name()).toString());
715 return p;
716}
717
718static QVariantList mergeSubjectOf(const QVariantList &lhs, const QVariantList &rhs)
719{
720 std::set<QString> mergedSet;
721 for (const auto &v : lhs) {
722 if (v.userType() != QMetaType::QString) {
723 return rhs.isEmpty() ? lhs : rhs;
724 }
725 mergedSet.insert(v.toString());
726 }
727 for (const auto &v : rhs) {
728 if (v.userType() != QMetaType::QString) {
729 return rhs.isEmpty() ? lhs : rhs;
730 }
731 mergedSet.insert(v.toString());
732 }
733
734 QVariantList result;
735 result.reserve(mergedSet.size());
736 std::copy(mergedSet.begin(), mergedSet.end(), std::back_inserter(result));
737 return result;
738}
739
740static int ticketTokenSize(const QVariant &v)
741{
742 if (v.userType() == QMetaType::QString) {
743 return v.toString().size();
744 }
745 if (v.userType() == QMetaType::QByteArray) {
746 return v.toByteArray().size();
747 }
748 return 0;
749}
750
751static Ticket mergeValue(const Ticket &lhs, const Ticket &rhs)
752{
753 auto t = JsonLdDocument::apply(lhs, rhs).value<Ticket>();
754 // prefer barcode ticket tokens over URLs
755 if (t.ticketTokenType() == Token::Url && lhs.ticketTokenType() != Token::Url && lhs.ticketTokenType() != Token::Unknown) {
756 t.setTicketToken(lhs.ticketToken());
757 } else if (lhs.ticketTokenType() != Token::Url && rhs.ticketTokenType() != Token::Url
759 t.setTicketToken(ticketTokenSize(lhs.ticketTokenData()) > ticketTokenSize(rhs.ticketTokenData()) ? lhs.ticketToken() : rhs.ticketToken());
760 }
761 return t;
762}
763
765{
766 if (rhs.isNull()) {
767 return lhs;
768 }
769 if (lhs.isNull()) {
770 return rhs;
771 }
772 if (!isSubType(lhs, rhs) && !isSubType(rhs, lhs)) {
773 qCWarning(Log) << "type mismatch during merging:" << lhs << rhs;
774 return {};
775 }
776
777 // prefer the element with the newer mtime, if we have that information
779 const auto lhsDt = JsonLd::convert<Reservation>(lhs).modifiedTime();
780 const auto rhsDt = JsonLd::convert<Reservation>(rhs).modifiedTime();
781 if (lhsDt.isValid() && rhsDt.isValid() && rhsDt < lhsDt) {
782 return MergeUtil::merge(rhs, lhs);
783 }
784 }
785
786 auto res = lhs;
787 if (lhs.userType() != rhs.userType() && isSubType(rhs, lhs)) {
788 res = rhs;
789 }
790
791 const auto mo = QMetaType(res.userType()).metaObject();
792 for (int i = 0; i < mo->propertyCount(); ++i) {
793 const auto prop = mo->property(i);
794 if (!prop.isStored()) {
795 continue;
796 }
797
798 auto lv = prop.readOnGadget(lhs.constData());
799 auto rv = prop.readOnGadget(rhs.constData());
800 auto mt = rv.userType();
801 const auto metaType = QMetaType(mt);
802
803 if (mt == qMetaTypeId<Airline>()) {
804 rv = mergeValue(lv.value<Airline>(), rv.value<Airline>());
805 } else if (mt == qMetaTypeId<Person>()) {
806 rv = mergeValue(lv.value<Person>(), rv.value<Person>());
807 } else if (mt == QMetaType::QDateTime) {
808 rv = mergeValue(lv.toDateTime(), rv.toDateTime());
809 } else if (mt == qMetaTypeId<Ticket>()) {
810 rv = mergeValue(lv.value<Ticket>(), rv.value<Ticket>());
811 } else if ((metaType.flags() & QMetaType::IsGadget) && metaType.metaObject()) {
812 rv = merge(prop.readOnGadget(lhs.constData()), rv);
813 } else if (mt == QMetaType::QVariantList && std::strcmp(prop.name(), "subjectOf") == 0) {
814 rv = mergeSubjectOf(lv.toList(), rv.toList());
815 } else if (mt == QMetaType::QString) {
816 rv = StringUtil::betterString(lv.toString(), rv.toString()).toString();
817 }
818
819 if (!JsonLd::valueIsNull(rv)) {
820 prop.writeOnGadget(res.data(), rv);
821 }
822 }
823
824 return res;
825}
826
827bool isMinimalCancelationFor(const Reservation &res, const Reservation &cancel)
828{
829 if (cancel.reservationStatus() != Reservation::ReservationCancelled) {
830 return false;
831 }
832 if (!equalAndPresent(res.reservationNumber(), cancel.reservationNumber())) {
833 return false;
834 }
835 if (!cancel.modifiedTime().isValid() || !cancel.reservationFor().isNull()) {
836 return false;
837 }
838 return SortUtil::startDateTime(res) > cancel.modifiedTime();
839}
840
841static bool isCompatibleLocationChange(const QVariant &lhs, const QVariant &rhs)
842{
843 const bool lhsTrainOrBus = JsonLd::isA<TrainReservation>(lhs) || JsonLd::isA<BusReservation>(lhs);
844 const bool rhsTrainOrBus = JsonLd::isA<TrainReservation>(rhs) || JsonLd::isA<BusReservation>(rhs);
845 return (lhsTrainOrBus && rhsTrainOrBus) || (JsonLd::isA<FlightReservation>(lhs) && JsonLd::isA<FlightReservation>(rhs));
846}
847
849{
850 if (!isCompatibleLocationChange(lhs, rhs)) {
851 return false;
852 }
853 const auto lhsRes = JsonLd::convert<Reservation>(lhs);
854 const auto rhsRes = JsonLd::convert<Reservation>(rhs);
855 if (!isSameReservation(lhsRes, rhsRes)) {
856 return false;
857 }
858
861 return false;
862 }
864 }
866 if (SortUtil::startDateTime(lhs).date() != SortUtil::startDateTime(rhs).date()) {
867 return false;
868 }
870 }
871
872 return false;
873}
874
875bool MergeUtil::hasSameArrival(const QVariant &lhs, const QVariant &rhs)
876{
877 if (!isCompatibleLocationChange(lhs, rhs)) {
878 return false;
879 }
880 const auto lhsRes = JsonLd::convert<Reservation>(lhs);
881 const auto rhsRes = JsonLd::convert<Reservation>(rhs);
882 if (!isSameReservation(lhsRes, rhsRes)) {
883 return false;
884 }
885
888 return false;
889 }
891 }
892
894 if (SortUtil::endDateTime(lhs).date() != SortUtil::endDateTime(rhs).date()) {
895 return false;
896 }
898 }
899
900 return false;
901}
902
903void MergeUtil::registerComparator(int metaTypeId, std::function<bool (const QVariant&, const QVariant&)> &&func)
904{
905 auto it = std::lower_bound(s_mergeCompareFuncs.begin(), s_mergeCompareFuncs.end(), metaTypeId);
906 if (it != s_mergeCompareFuncs.end() && (*it).metaTypeId == metaTypeId) {
907 (*it).func = std::move(func);
908 } else {
909 s_mergeCompareFuncs.insert(it, {metaTypeId, std::move(func)});
910 }
911}
A boat or ferry reservation.
A boat or ferry trip.
Definition boattrip.h:23
A bus reservation.
A bus trip.
Definition bustrip.h:22
An event reservation.
An event.
Definition event.h:21
A flight reservation.
Definition reservation.h:90
QString passengerSequenceNumber
Passenger sequence number Despite the name, do not expect this to be a number, infants without their ...
Definition reservation.h:97
A flight.
Definition flight.h:25
QDate departureDay
The scheduled day of departure.
Definition flight.h:46
static QVariant apply(const QVariant &lhs, const QVariant &rhs)
Apply all properties of rhs on to lhs.
A hotel reservation.
Definition reservation.h:77
static QVariant merge(const QVariant &lhs, const QVariant &rhs)
Merge the two given objects.
static void registerComparator(bool(*func)(const T &, const T &))
Register a comparator function for a custom type that will be used by isSame.
Definition mergeutil.h:75
static bool isSameIncidence(const QVariant &lhs, const QVariant &rhs)
Checks whether to elements refer to the same thing, just for different people.
static bool isSamePerson(const Person &lhs, const Person &rhs)
Checks if two Person objects refer to the same person.
static bool hasSameArrival(const QVariant &lhs, const QVariant &rhs)
Checks whether two transport reservation elements refer to the same arrival.
static bool isSame(const QVariant &lhs, const QVariant &rhs)
Checks if two Reservation or Trip values refer to the same booking element.
static bool hasSameDeparture(const QVariant &lhs, const QVariant &rhs)
Checks whether two transport reservation elements refer to the same departure.
A person.
Definition person.h:20
A frequent traveler, bonus points or discount scheme program membership.
QVariant tokenData
The token payload for barcodes, otherwise the same as ticketToken.
QDateTime validFrom
Non-standard extension for ticket validity time ranges.
A Rental Car reservation.
A car rental.
Definition rentalcar.h:22
Abstract base class for reservations.
Definition reservation.h:25
A Taxi reservation.
A booked ticket.
Definition ticket.h:41
QVariant ticketTokenData
The ticket token payload for barcodes, otherwise the same as ticketToken.
Definition ticket.h:57
QString ticketToken
The raw ticket token string.
Definition ticket.h:50
QDateTime validFrom
Non-standard extension for ticket validity time ranges.
Definition ticket.h:63
KItinerary::Token::TokenType ticketTokenType
The type of the content in ticketToken.
Definition ticket.h:53
@ Unknown
Unknown or empty ticket token.
Definition token.h:29
@ Url
A download URL, if shown in a barcode its format can be determined by the application.
Definition token.h:30
Tourist attraction (e.g.
Definition place.h:146
A train reservation.
A train trip.
Definition traintrip.h:24
QDate departureDay
The scheduled day of departure.
Definition traintrip.h:42
QString fullName(const PartType &type)
bool isA(const QVariant &value)
Returns true if value is of type T.
Definition datatypes.h:24
bool canConvert(const QVariant &value)
Checks if the given value can be up-cast to T.
Definition datatypes.h:31
bool valueIsNull(const QVariant &v)
Checks whether v holds a null-like value.
T convert(const QVariant &value)
Up-cast value to T.
Definition datatypes.h:47
bool isSameLocation(const QVariant &lhs, const QVariant &rhs, Accuracy accuracy=Exact)
Returns true if the given locations are the same.
QVariant departureLocation(const QVariant &res)
Returns the departure location of the given reservation.
QVariant arrivalLocation(const QVariant &res)
Returns the arrival location of the given reservation.
@ CityLevel
Locations are in the same city.
@ Exact
Locations match exactly.
QDateTime startDateTime(const QVariant &elem)
Returns the (start) time associated with the given element.
Definition sortutil.cpp:29
bool hasStartTime(const QVariant &elem)
Returns whether the given element has a start time.
Definition sortutil.cpp:198
bool hasEndTime(const QVariant &elem)
Returns whether the given element has an end time.
Definition sortutil.cpp:216
QDateTime endDateTime(const QVariant &res)
Returns the (end) time associated with the given element.
Definition sortutil.cpp:101
QString normalize(QStringView str)
Strips out diacritics and converts to case-folded form.
QString transliterate(QStringView s)
Transliterate diacritics or other special characters.
float prefixSimilarity(QStringView s1, QStringView s2)
Returns how much of the prefix of two given strings are equal, in relation to the longer of the two i...
QStringView betterString(QStringView lhs, QStringView rhs)
Assuming both sides are describing the same thing, this tries to find the "better" string.
Classes for reservation/travel data models, data extraction and data augmentation.
Definition berelement.h:17
KGuiItem cancel()
bool operator<(const PosRange< Trait > &l, const PosRange< Trait > &r)
qsizetype size() const const
QDate date() const const
bool isValid() const const
qint64 secsTo(const QDateTime &other) const const
Qt::TimeSpec timeSpec() const const
QTimeZone timeZone() const const
const QMetaObject * metaObject() const const
iterator begin()
iterator end()
bool isEmpty() const const
reverse_iterator rbegin()
QString & remove(QChar ch, Qt::CaseSensitivity cs)
reverse_iterator rend()
qsizetype size() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toUpper() const const
QString trimmed() const const
int compare(QChar ch) const const
bool isEmpty() const const
bool startsWith(QChar ch) const const
QString toString() const const
CaseSensitivity
LocalTime
QTimeZone utc()
const void * constData() const const
bool isNull() const const
QByteArray toByteArray() const const
QString toString() const const
int userType() const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 22 2024 12:00:34 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.