File indexing completed on 2025-02-02 05:02:38

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "tripgroupmanager.h"
0008 #include "tripgroup.h"
0009 #include "constants.h"
0010 #include "logging.h"
0011 #include "reservationmanager.h"
0012 #include "transfermanager.h"
0013 
0014 #include <KItinerary/LocationUtil>
0015 #include <KItinerary/Organization>
0016 #include <KItinerary/Reservation>
0017 #include <KItinerary/SortUtil>
0018 
0019 #include <KLocalizedString>
0020 
0021 #include <QDateTime>
0022 #include <QDebug>
0023 #include <QDirIterator>
0024 #include <QStandardPaths>
0025 #include <QUuid>
0026 
0027 #include <set>
0028 
0029 using namespace KItinerary;
0030 
0031 enum {
0032     MaximumTripDuration = 20, // in days
0033     MaximumTripElements = 20,
0034     MinimumTripElements = 2,
0035 };
0036 
0037 TripGroupManager::TripGroupManager(QObject* parent) :
0038     QObject(parent)
0039 {
0040     load();
0041 }
0042 
0043 TripGroupManager::~TripGroupManager() = default;
0044 
0045 void TripGroupManager::setReservationManager(ReservationManager *resMgr)
0046 {
0047     m_resMgr = resMgr;
0048     connect(m_resMgr, &ReservationManager::batchAdded, this, &TripGroupManager::batchAdded);
0049     connect(m_resMgr, &ReservationManager::batchContentChanged, this, &TripGroupManager::batchContentChanged);
0050     connect(m_resMgr, &ReservationManager::batchRemoved, this, &TripGroupManager::batchRemoved);
0051     connect(m_resMgr, &ReservationManager::batchRenamed, this, &TripGroupManager::batchRenamed);
0052 
0053     const auto allReservations = m_resMgr->batches();
0054     m_reservations.clear();
0055     m_reservations.reserve(allReservations.size());
0056     std::copy(allReservations.begin(), allReservations.end(), std::back_inserter(m_reservations));
0057     std::sort(m_reservations.begin(), m_reservations.end(), [this](const auto &lhs, const auto &rhs) {
0058         return SortUtil::isBefore(m_resMgr->reservation(lhs), m_resMgr->reservation(rhs));
0059     });
0060 
0061     checkConsistency();
0062     scanAll();
0063 }
0064 
0065 void TripGroupManager::setTransferManager(TransferManager *transferMgr)
0066 {
0067     m_transferMgr = transferMgr;
0068 }
0069 
0070 QVector<QString> TripGroupManager::tripGroups() const
0071 {
0072     QVector<QString> groups;
0073     groups.reserve(m_tripGroups.size());
0074     std::copy(m_tripGroups.keyBegin(), m_tripGroups.keyEnd(), std::back_inserter(groups));
0075     return groups;
0076 }
0077 
0078 TripGroup TripGroupManager::tripGroup(const QString &id) const
0079 {
0080     return m_tripGroups.value(id);
0081 }
0082 
0083 QString TripGroupManager::tripGroupIdForReservation(const QString &resId) const
0084 {
0085     return m_reservationToGroupMap.value(resId);
0086 }
0087 
0088 TripGroup TripGroupManager::tripGroupForReservation(const QString &resId) const
0089 {
0090     return tripGroup(m_reservationToGroupMap.value(resId));
0091 }
0092 
0093 QString TripGroupManager::basePath()
0094 {
0095     return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/tripgroups/");
0096 }
0097 
0098 void TripGroupManager::load()
0099 {
0100     const auto base = basePath();
0101     QDir::root().mkpath(base);
0102 
0103     for (QDirIterator it(base, QDir::NoDotAndDotDot | QDir::Files); it.hasNext();) {
0104         it.next();
0105         TripGroup g(this);
0106         if (g.load(it.filePath())) {
0107             const auto tgId = it.fileInfo().baseName();
0108             m_tripGroups.insert(tgId, g);
0109             for (const auto &resId : g.elements()) {
0110                 const auto groupIt = m_reservationToGroupMap.constFind(resId);
0111                 if (groupIt != m_reservationToGroupMap.constEnd()) {
0112                     qCWarning(Log) << "Overlapping trip groups found - removing" << g.name();
0113                     const auto groupId = groupIt.value(); // copy before we modify what groupIt points to
0114                     removeTripGroup(groupId);
0115                     removeTripGroup(tgId);
0116                     break;
0117                 }
0118                 m_reservationToGroupMap.insert(resId, tgId);
0119             }
0120         }
0121     }
0122 }
0123 
0124 void TripGroupManager::removeTripGroup(const QString &groupId)
0125 {
0126     const auto groupIt = m_tripGroups.constFind(groupId);
0127     if (groupIt == m_tripGroups.constEnd()) {
0128         return;
0129     }
0130 
0131     for (const auto &elem : groupIt.value().elements()) {
0132         const auto it = m_reservationToGroupMap.find(elem);
0133         // check if this still points to the removed group (might not be the case if an overlapping group was added meanwhile)
0134         if (it != m_reservationToGroupMap.end() && it.value() == groupId) {
0135             m_reservationToGroupMap.erase(it);
0136         }
0137     }
0138     m_tripGroups.erase(groupIt);
0139     if (!QFile::remove(basePath() + groupId + QLatin1StringView(".json"))) {
0140         qCWarning(Log) << "Failed to delete trip group file!" << groupId;
0141     }
0142     Q_EMIT tripGroupRemoved(groupId);
0143 }
0144 
0145 void TripGroupManager::clear()
0146 {
0147     qCDebug(Log) << "deleting" << basePath();
0148     QDir d(basePath());
0149     d.removeRecursively();
0150 }
0151 
0152 void TripGroupManager::removeReservationsInGroup(const QString &groupId)
0153 {
0154     const auto groupIt = m_tripGroups.constFind(groupId);
0155     if (groupIt == m_tripGroups.constEnd()) {
0156         return;
0157     }
0158 
0159     const auto elements = groupIt.value().elements();
0160     for (const auto &element : elements) {
0161         m_resMgr->removeBatch(element);
0162     }
0163 }
0164 
0165 void TripGroupManager::batchAdded(const QString &resId)
0166 {
0167     auto it = std::lower_bound(m_reservations.begin(), m_reservations.end(), resId, [this](const auto &lhs, const auto &rhs) {
0168         return SortUtil::isBefore(m_resMgr->reservation(lhs), m_resMgr->reservation(rhs));
0169     });
0170     m_reservations.insert(it, resId);
0171     // ### we can optimize this by only scanning it +/- MaximumTripElements
0172     scanAll();
0173 }
0174 
0175 void TripGroupManager::batchContentChanged(const QString &resId)
0176 {
0177     // ### we can probably make this more efficient
0178     batchRemoved(resId);
0179     batchAdded(resId);
0180 }
0181 
0182 void TripGroupManager::batchRenamed(const QString &oldBatchId, const QString &newBatchId)
0183 {
0184     // ### this can be done more efficiently
0185     batchRemoved(oldBatchId);
0186     batchAdded(newBatchId);
0187 }
0188 
0189 void TripGroupManager::batchRemoved(const QString &resId)
0190 {
0191     // check if resId is part of a group
0192     const auto mapIt = m_reservationToGroupMap.constFind(resId);
0193     if (mapIt != m_reservationToGroupMap.constEnd()) {
0194         const auto groupIt = m_tripGroups.find(mapIt.value());
0195         Q_ASSERT(groupIt != m_tripGroups.end());
0196         const auto groupId = groupIt.key(); // copy as the iterator might become invalid below
0197 
0198         auto elems = groupIt.value().elements();
0199         elems.removeAll(resId);
0200         if (elems.size() < MinimumTripElements) { // group deleted
0201             qDebug() << "removing trip group due to getting too small";
0202             removeTripGroup(groupId);
0203         } else { // group changed
0204             qDebug() << "removing element from trip group" << resId << elems;
0205             groupIt.value().setElements(elems);
0206             groupIt.value().store(basePath() + mapIt.value() + QLatin1StringView(".json"));
0207             m_reservationToGroupMap.erase(mapIt);
0208             Q_EMIT tripGroupChanged(groupId);
0209         }
0210     }
0211 
0212     // remove the reservation
0213     const auto resIt = std::find(m_reservations.begin(), m_reservations.end(), resId);
0214     if (resIt != m_reservations.end()) {
0215         m_reservations.erase(resIt);
0216     }
0217 }
0218 
0219 void TripGroupManager::scanAll()
0220 {
0221     qCDebug(Log);
0222     QString prevGroup;
0223     for (auto it = m_reservations.begin(); it != m_reservations.end(); ++it) {
0224         auto groupIt = m_reservationToGroupMap.constFind(*it);
0225         if (groupIt != m_reservationToGroupMap.constEnd() && groupIt.value() == prevGroup) {
0226             // in the middle of an existing group
0227             continue;
0228         }
0229 
0230         if (groupIt == m_reservationToGroupMap.constEnd()) {
0231             prevGroup.clear();
0232         } else {
0233             prevGroup = groupIt.value();
0234         }
0235 
0236         // not a location change? -> continue
0237         if (!LocationUtil::isLocationChange(m_resMgr->reservation(*it))) {
0238             continue;
0239         }
0240 
0241         scanOne(it);
0242         prevGroup = m_reservationToGroupMap.value(*it);
0243     }
0244 }
0245 
0246 static bool isConnectedTransition(const QVariant &fromRes, const QVariant &toRes)
0247 {
0248     const auto from = LocationUtil::arrivalLocation(fromRes);
0249     const auto to = LocationUtil::departureLocation(toRes);
0250     if (LocationUtil::isSameLocation(from, to, LocationUtil::CityLevel)) {
0251         return true;
0252     }
0253 
0254     const auto dep = SortUtil::endDateTime(fromRes);
0255     const auto arr = SortUtil::startDateTime(toRes);
0256     return dep.date() == arr.date() && dep.secsTo(arr) < Constants::MaximumLayoverTime.count();
0257 }
0258 
0259 void TripGroupManager::scanOne(std::vector<QString>::const_iterator beginIt)
0260 {
0261     const auto beginRes = m_resMgr->reservation(*beginIt);
0262     const auto beginDeparture = LocationUtil::departureLocation(beginRes);
0263     const auto beginDt = SortUtil::startDateTime(beginRes);
0264 
0265     m_resNumSearch.clear();
0266     if (JsonLd::canConvert<Reservation>(beginRes)) {
0267         m_resNumSearch.push_back({beginRes.userType(), JsonLd::convert<Reservation>(beginRes).reservationNumber()});
0268     }
0269 
0270     qDebug() << "starting scan at" << LocationUtil::name(beginDeparture);
0271     auto res = beginRes;
0272     auto resNumIt = m_reservations.cend(); // result of the search using reservation ids
0273     auto connectedIt = m_reservations.cend(); // result of the search using trip connectivity
0274 
0275     bool resNumSearchDone = false;
0276     bool connectedSearchDone = false;
0277 
0278     // scan by location change
0279     for (auto it = beginIt + 1; it != m_reservations.end(); ++it) {
0280         const auto prevRes = res;
0281         const auto curRes = m_resMgr->reservation(*it);
0282 
0283         // not a location change? -> continue searching
0284         if (!LocationUtil::isLocationChange(curRes)) {
0285             continue;
0286         }
0287         res = curRes;
0288 
0289         // all search strategies think they are done
0290         if (resNumSearchDone && connectedSearchDone) {
0291             break;
0292         }
0293 
0294         // search depth reached
0295         // ### we probably don't want to count multi-traveler elements for this!
0296         if (std::distance(beginIt, it) > MaximumTripElements) {
0297             qDebug() << "  aborting search, maximum search depth reached";
0298             break;
0299         }
0300 
0301         // maximum trip duration exceeded?
0302         const auto endDt = SortUtil::endDateTime(res);
0303         if (beginDt.daysTo(endDt) > MaximumTripDuration) {
0304             qDebug() << "  aborting search, maximum trip duration reached";
0305             break;
0306         }
0307 
0308         // check for connected transitions (ie. previous arrival == current departure)
0309         const auto prevArrival = LocationUtil::arrivalLocation(prevRes);
0310         const auto curDeparture = LocationUtil::departureLocation(res);
0311         const auto connectedTransition = isConnectedTransition(prevRes, res);
0312         qDebug() << "  current transition goes from" << LocationUtil::name(prevArrival) << "to" << LocationUtil::name(LocationUtil::arrivalLocation(res));
0313 
0314         if (!connectedSearchDone) {
0315             if (!connectedTransition) {
0316                 qDebug() << "  aborting connectivity search, not an adjacent transition from" << LocationUtil::name(prevArrival) << "to" << LocationUtil::name(curDeparture);
0317                 connectedIt = m_reservations.end();
0318                 connectedSearchDone = true;
0319             } else {
0320                 connectedIt = it;
0321             }
0322 
0323             // same location as beginIt? -> we reached the end of the trip (break)
0324             const auto curArrival = LocationUtil::arrivalLocation(res);
0325             if (LocationUtil::isSameLocation(beginDeparture, curArrival, LocationUtil::CityLevel)) {
0326                 qDebug() << "  aborting connectivity search, arrived at the start again" << LocationUtil::name(curArrival);
0327                 connectedSearchDone = true;
0328             }
0329         }
0330 
0331         if (!resNumSearchDone &&  JsonLd::canConvert<Reservation>(res)) {
0332             const auto resNum = JsonLd::convert<Reservation>(res).reservationNumber();
0333             if (!resNum.isEmpty()) {
0334                 const auto r = std::find_if(m_resNumSearch.begin(), m_resNumSearch.end(), [res, resNum](const auto &elem) {
0335                     return elem.type == res.userType() && elem.resNum == resNum;
0336                 });
0337                 if (r == m_resNumSearch.end()) {
0338                     // mode of transport or reservation changed: we consider this still part of the trip if connectivity
0339                     // search thinks this is part of the same trip too, and we are not at home again yet
0340                     if (connectedTransition && !LocationUtil::isSameLocation(prevArrival, beginDeparture, LocationUtil::CityLevel)) {
0341                         qDebug() << "  considering transition to" << LocationUtil::name(LocationUtil::arrivalLocation(res)) << "as part of trip despite unknown reservation number";
0342                         m_resNumSearch.push_back({res.userType(), resNum});
0343                         resNumIt = it;
0344                     } else {
0345                         qDebug() << "  aborting reservation number search due to mismatch";
0346                         resNumSearchDone = true;
0347                     }
0348                 } else {
0349                     resNumIt = it;
0350                 }
0351             }
0352         }
0353     }
0354 
0355     // determine which search strategy found the larger result
0356     auto it = m_reservations.cend();
0357     if (!connectedSearchDone) {
0358         connectedIt = m_reservations.end();
0359     }
0360     if (connectedIt != m_reservations.end() && resNumIt != m_reservations.end()) {
0361         it = std::max(connectedIt, resNumIt);
0362     } else {
0363         it = connectedIt == m_reservations.end() ? resNumIt : connectedIt;
0364     }
0365 
0366     if (it == m_reservations.end()) {
0367         qDebug() << "nothing found";
0368         return;
0369     }
0370 
0371     // remove leading loop appendices (trailing ones will be cut by the loop check above already)
0372     const auto endRes = m_resMgr->reservation(*it);
0373     const auto endArrival = LocationUtil::arrivalLocation(endRes);
0374     for (auto it2 = beginIt; it2 != it; ++it2) {
0375         const auto res = m_resMgr->reservation(*it2);
0376         if (!LocationUtil::isLocationChange(res)) {
0377             continue;
0378         }
0379         const auto curDeparture = LocationUtil::departureLocation(res);
0380         if (LocationUtil::isSameLocation(endArrival, curDeparture, LocationUtil::CityLevel)) {
0381             if (beginIt != it2) {
0382                 qDebug() << "  removing leading appendix, starting at" << LocationUtil::name(curDeparture);
0383             }
0384             beginIt = it2;
0385             break;
0386         }
0387     }
0388 
0389     if (std::distance(beginIt, it) < MinimumTripElements - 1) {
0390         qDebug() << "trip too short";
0391         return;
0392     }
0393 
0394     // create a trip for [beginIt, it)
0395     ++it; // so this marks the end
0396     QVector<QString> elems;
0397     elems.reserve(std::distance(beginIt, it));
0398     std::copy(beginIt, it, std::back_inserter(elems));
0399 
0400     // if we are looking at an existing group, did that expand?
0401     const auto groupIt = m_tripGroups.find(m_reservationToGroupMap.value(*beginIt));
0402     if (groupIt != m_tripGroups.end() && groupIt.value().elements() == elems) {
0403         qDebug() << "existing group unchanged" << groupIt.value().name();
0404         return;
0405     }
0406 
0407     std::set<QString> pendingGroupRemovals;
0408     if (groupIt == m_tripGroups.end()) {
0409         const auto tgId = QUuid::createUuid().toString(QUuid::WithoutBraces);
0410         TripGroup g(this);
0411         g.setElements(elems);
0412         for (auto it2 = beginIt; it2 != it; ++it2) {
0413             // remove overlapping/nested groups, delay this until the end though, as that will invalidate our iterators
0414             const auto previousGroupId = m_reservationToGroupMap.value(*it2);
0415             if (!previousGroupId.isEmpty() && previousGroupId != tgId) {
0416                 pendingGroupRemovals.insert(previousGroupId);
0417             }
0418             m_reservationToGroupMap.insert(*it2, tgId);
0419         }
0420         g.setName(guessName(g));
0421         qDebug() << "creating trip group" << g.name();
0422         m_tripGroups.insert(tgId, g);
0423         g.store(basePath() + tgId + QLatin1StringView(".json"));
0424         Q_EMIT tripGroupAdded(tgId);
0425     } else {
0426         auto &g = groupIt.value();
0427         for (const auto &elem : g.elements()) { // remove old element mappings, some of them might no longer be valid
0428             m_reservationToGroupMap.remove(elem);
0429         }
0430         g.setElements(elems);
0431         for (auto it2 = beginIt; it2 != it; ++it2) {
0432             m_reservationToGroupMap.insert(*it2, groupIt.key());
0433         }
0434         g.setName(guessName(g));
0435         qDebug() << "updating trip group" << g.name();
0436         g.store(basePath() + groupIt.key() + QLatin1StringView(".json"));
0437         Q_EMIT tripGroupChanged(groupIt.key());
0438     }
0439 
0440     for (const auto &tgId : pendingGroupRemovals) {
0441         removeTripGroup(tgId);
0442     }
0443 }
0444 
0445 void TripGroupManager::checkConsistency()
0446 {
0447     std::vector<QString> tgIds;
0448     tgIds.reserve(m_tripGroups.size());
0449 
0450     // look for dangling reservation references
0451     for (auto it = m_reservationToGroupMap.constBegin(); it != m_reservationToGroupMap.constEnd(); ++it) {
0452         if (!m_resMgr->hasBatch(it.key())) {
0453             tgIds.push_back(it.value());
0454         }
0455     }
0456 
0457     for (const auto &groupId : tgIds) {
0458         qCWarning(Log) << "Removing group" << m_tripGroups.value(groupId).name() << "with dangling reservation references";
0459         removeTripGroup(groupId);
0460     }
0461     tgIds.clear();
0462 
0463     // look for nested groups
0464     std::copy(m_tripGroups.keyBegin(), m_tripGroups.keyEnd(), std::back_inserter(tgIds));
0465     std::sort(tgIds.begin(), tgIds.end(), [this](const auto &lhs, const auto &rhs) {
0466         return m_tripGroups.value(lhs).beginDateTime() < m_tripGroups.value(rhs).beginDateTime();
0467     });
0468     for (auto it = tgIds.begin();;) {
0469         it = std::adjacent_find(it, tgIds.end(), [this](const auto &lhs, const auto &rhs) {
0470             return m_tripGroups.value(lhs).endDateTime() > m_tripGroups.value(rhs).beginDateTime();
0471         });
0472         if (it == tgIds.end()) {
0473             break;
0474         }
0475         // remove both nested groups
0476         qCWarning(Log) << "Removing group" << m_tripGroups.value(*it).name() << "due to overlapping with following group";
0477         it = tgIds.erase(it);
0478         qCWarning(Log) << "Removing group" << m_tripGroups.value(*it).name() << "due to overlapping with previous group";
0479         it = tgIds.erase(it);
0480     }
0481 }
0482 
0483 static QString destinationName(const QVariant &loc)
0484 {
0485     const auto addr = LocationUtil::address(loc);
0486     if (!addr.addressLocality().isEmpty()) {
0487         return addr.addressLocality();
0488     }
0489     return LocationUtil::name(loc);
0490 }
0491 
0492 QString TripGroupManager::guessDestinationFromLodging(const TripGroup &g) const
0493 {
0494     // we assume that lodging indicates the actual destination, not a stopover location
0495     QStringList dests;
0496     for (const auto &resId : g.elements()) {
0497         const auto res = m_resMgr->reservation(resId);
0498         if (!JsonLd::isA<LodgingReservation>(res)) {
0499             continue;
0500         }
0501 
0502         const auto lodging = res.value<LodgingReservation>().reservationFor().value<LodgingBusiness>();
0503         if (!lodging.address().addressLocality().isEmpty() && !dests.contains(lodging.address().addressLocality())) {
0504             dests.push_back(lodging.address().addressLocality());
0505             continue;
0506         }
0507         if (!lodging.name().isEmpty() && !dests.contains(lodging.name())) { // fall back to hotel name if we don't know the city
0508             dests.push_back(lodging.name());
0509             continue;
0510         }
0511 
0512         // TODO consider the country if that differs from where we started from
0513     }
0514 
0515     return dests.join(QLatin1StringView(" - "));
0516 }
0517 
0518 bool TripGroupManager::isRoundTrip(const TripGroup& g) const
0519 {
0520     const auto depId = g.elements().at(0);
0521     const auto arrId = g.elements().constLast();
0522     const auto dep = LocationUtil::departureLocation(m_resMgr->reservation(depId));
0523     const auto arr = LocationUtil::arrivalLocation(m_resMgr->reservation(arrId));
0524     return LocationUtil::isSameLocation(dep, arr, LocationUtil::CityLevel);
0525 }
0526 
0527 QString TripGroupManager::guessDestinationFromTransportTimeGap(const TripGroup &g) const
0528 {
0529     // we must only do this for return trips
0530     if (!isRoundTrip(g)) {
0531         return {};
0532     }
0533 
0534     // we assume that the largest time interval between arrival and departure of two adjacent location changes is the destination
0535     QDateTime beginDt;
0536     QString destName;
0537     qint64 maxLength = 0;
0538 
0539     for (const auto &resId : g.elements()) {
0540         const auto res = m_resMgr->reservation(resId);
0541         if (!LocationUtil::isLocationChange(res)) {
0542             continue;
0543         }
0544 
0545         if (!beginDt.isValid()) { // first transport element
0546             beginDt = SortUtil::endDateTime(res);
0547             continue;
0548         }
0549 
0550         const auto endDt = SortUtil::startDateTime(res);
0551         const auto newLength = beginDt.secsTo(endDt);
0552         if (newLength > maxLength) {
0553             destName = LocationUtil::name(LocationUtil::departureLocation(res));
0554             maxLength = newLength;
0555         }
0556         beginDt = endDt;
0557     }
0558 
0559     return destName;
0560 }
0561 
0562 QString TripGroupManager::guessName(const TripGroup& g) const
0563 {
0564     // part 1: the destination of the trip
0565     QString dest = guessDestinationFromLodging(g);
0566     if (dest.isEmpty()) {
0567         dest = guessDestinationFromTransportTimeGap(g);
0568     }
0569     if (dest.isEmpty()) {
0570         // two fallback cases: round-trips and one-way trips
0571         const auto beginLoc = LocationUtil::departureLocation(m_resMgr->reservation(g.elements().at(0)));
0572         const auto endLoc = LocationUtil::arrivalLocation(m_resMgr->reservation(g.elements().constLast()));
0573         if (LocationUtil::isSameLocation(beginLoc, endLoc, LocationUtil::CityLevel)) {
0574             const auto middleIdx = (g.elements().size() - 1 + (g.elements().size() % 2)) / 2;
0575             const auto middleRes = m_resMgr->reservation(g.elements().at(middleIdx));
0576             if (LocationUtil::isLocationChange(middleRes)) {
0577                 dest = destinationName(LocationUtil::arrivalLocation(middleRes));
0578             } else {
0579                 dest = destinationName(LocationUtil::location(middleRes));
0580             }
0581         } else {
0582             // TODO we want the city (or country, if differing from start) here, if available
0583             dest = destinationName(endLoc);
0584         }
0585     }
0586 
0587     // part 2: the time range of the trip
0588     // three cases: within 1 month, crossing a month boundary in one year, crossing a year boundary
0589     const auto beginDt = SortUtil::startDateTime(m_resMgr->reservation(g.elements().at(0)));
0590     const auto endDt = SortUtil::endDateTime(m_resMgr->reservation(g.elements().constLast()));
0591     Q_ASSERT(beginDt.daysTo(endDt) <= MaximumTripDuration);
0592     if (beginDt.date().year() == endDt.date().year()) {
0593         if (beginDt.date().month() == endDt.date().month()) {
0594             return i18nc("%1 is destination, %2 is the standalone month name, %3 is the year", "%1 (%2 %3)", dest, QLocale().standaloneMonthName(beginDt.date().month(), QLocale::LongFormat), beginDt.date().toString(QStringLiteral("yyyy")));
0595         }
0596         return i18nc("%1 is destination, %2 and %3 are the standalone month names and %4 is the year", "%1 (%2/%3 %4)", dest, QLocale().monthName(beginDt.date().month(), QLocale::LongFormat), QLocale().standaloneMonthName(endDt.date().month(), QLocale::LongFormat), beginDt.date().toString(QStringLiteral("yyyy")));
0597     }
0598     return i18nc("%1 is destination, %2 and %3 are years", "%1 (%2/%3)", dest, beginDt.date().toString(QStringLiteral("yyyy")), endDt.date().toString(QStringLiteral("yyyy")));
0599 }
0600 
0601 #include "moc_tripgroupmanager.cpp"