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"