File indexing completed on 2024-12-29 04:50:10

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "timezonedb_p.h"
0008 
0009 #include <KCountry>
0010 #include <KTimeZone>
0011 
0012 #include <QDebug>
0013 #include <QTimeZone>
0014 
0015 #include <cmath>
0016 #include <cstring>
0017 
0018 using namespace KItinerary;
0019 
0020 static QTimeZone toQTimeZone(const char *tzId)
0021 {
0022     if (!tzId || std::strcmp(tzId, "") == 0) {
0023         return {};
0024     }
0025     return QTimeZone(tzId);
0026 }
0027 
0028 static QList<const char*> timezonesForCountry(const KCountry &country)
0029 {
0030     auto tzs = country.timeZoneIds();
0031     if (tzs.size() <= 1) {
0032         return tzs;
0033     }
0034     // filter overseas territories that map back to a different country code
0035     tzs.erase(std::remove_if(tzs.begin(), tzs.end(), [country](const char *tz) {
0036         return !(KTimeZone::country(tz) == country);
0037     }), tzs.end());
0038     return tzs;
0039 }
0040 
0041 static bool compareOffsetData(const QTimeZone::OffsetData &lhs, const QTimeZone::OffsetData &rhs)
0042 {
0043     return lhs.offsetFromUtc == rhs.offsetFromUtc
0044         && lhs.standardTimeOffset == rhs.standardTimeOffset
0045         && lhs.daylightTimeOffset == rhs.daylightTimeOffset
0046         && lhs.atUtc == rhs.atUtc
0047         && lhs.abbreviation == rhs.abbreviation;
0048 }
0049 
0050 static bool isEquivalentTimezone(const QTimeZone &lhs, const QTimeZone &rhs)
0051 {
0052     auto dt = QDateTime::currentDateTimeUtc();
0053     if (lhs.offsetFromUtc(dt) != rhs.offsetFromUtc(dt) || lhs.hasTransitions() != rhs.hasTransitions()) {
0054         return false;
0055     }
0056 
0057     for (int i = 0; i < 2 && dt.isValid(); ++i) {
0058         const auto lhsOff = lhs.nextTransition(dt);
0059         const auto rhsOff = rhs.nextTransition(dt);
0060         if (!compareOffsetData(lhsOff, rhsOff)) {
0061             return false;
0062         }
0063         dt = lhsOff.atUtc;
0064     }
0065 
0066     return true;
0067 }
0068 
0069 QTimeZone KnowledgeDb::timezoneForLocation(float lat, float lon, QStringView alpha2CountryCode, QStringView regionCode)
0070 {
0071 
0072     const auto coordTz = KTimeZone::fromLocation(lat, lon);
0073     const auto coordZone = toQTimeZone(coordTz);
0074 
0075     const auto country = KCountry::fromAlpha2(alpha2CountryCode);
0076     auto countryTzs = KCountrySubdivision::fromCode(QString(alpha2CountryCode + QLatin1Char('-') + regionCode)).timeZoneIds();
0077     if (countryTzs.isEmpty()) {
0078         countryTzs = timezonesForCountry(country);
0079     }
0080     const auto countryFromTz = KTimeZone::country(coordTz);
0081 
0082     // if we determine a different country than was provided, search for an equivalent timezone
0083     // in the requested country
0084     // example: Tijuana airport ending up in America/Los Angeles, and America/Tijuna being the only MX timezone equivalent to that
0085     if (coordTz && countryFromTz.isValid() && country.isValid() && !(countryFromTz == country)) { // ### clean up once KCountry has op!=
0086         bool nonUnique = false;
0087         QTimeZone foundTz;
0088 
0089         for (const char *countryTz : countryTzs) {
0090             const auto t = toQTimeZone(countryTz);
0091             if (!isEquivalentTimezone(t, coordZone)) {
0092                 continue;
0093             }
0094             if (foundTz.isValid()) {
0095                 nonUnique = true;
0096                 break;
0097             }
0098             foundTz = t;
0099         }
0100 
0101         if (!nonUnique && foundTz.isValid()) {
0102             return foundTz;
0103         }
0104     }
0105 
0106     // only one method found a result, let's use that one
0107     if ((coordZone.isValid() && countryTzs.contains(coordTz)) || countryTzs.isEmpty()) {
0108         return coordZone;
0109     }
0110     if (!coordZone.isValid() && countryTzs.size() == 1) {
0111         return toQTimeZone(countryTzs.at(0));
0112     }
0113 
0114     // if the coordinate-based timezone is also in @p country, that takes precedence
0115     // example: the various AR sub-zones, or the MY sub-zone
0116     if (country == countryFromTz) {
0117         return coordZone;
0118     }
0119 
0120     // if both timezones are equivalent, the country-based one wins, otherwise we use the coordinate one
0121     if (countryTzs.size() == 1) {
0122         const auto countryQtz = toQTimeZone(countryTzs.at(0));
0123         return isEquivalentTimezone(coordZone, countryQtz) ? countryQtz : coordZone;
0124     }
0125     return coordZone;
0126 }