File indexing completed on 2024-05-19 09:22:09
0001 /* 0002 * SPDX-FileCopyrightText: 2006 Aaron Seigo <aseigo@kde.org> 0003 * SPDX-FileCopyrightText: 2010 Marco Martin <notmart@gmail.com> 0004 * SPDX-FileCopyrightText: 2015 Vishesh Handa <vhanda@kde.org> 0005 * SPDX-FileCopyrightText: 2022 Natalie Clarius <natalie_clarius@yahoo.de> 0006 * 0007 * SPDX-License-Identifier: LGPL-2.0-only 0008 */ 0009 0010 #include "datetimerunner.h" 0011 0012 #include <KFormat> 0013 #include <KLocalizedString> 0014 0015 #include <QDateTime> 0016 #include <QIcon> 0017 #include <QLocale> 0018 #include <QRegularExpression> 0019 #include <QTimeZone> 0020 0021 #include <math.h> 0022 0023 static const QString dateWord = i18nc("Note this is a KRunner keyword", "date"); 0024 static const QString timeWord = i18nc("Note this is a KRunner keyword", "time"); 0025 static const QString conversionWords = i18nc( 0026 "words to specify a time in a time zone or to convert a time to a time zone, e.g. 'current time in Berlin' or '18:00 UTC to CET', separated by '|' (will " 0027 "be used as a regex)", 0028 "to|in|as|at"); 0029 static const QRegularExpression conversionWordsRegex = QRegularExpression(QString("\\s(%1|>|->)\\s").arg(conversionWords)); 0030 0031 DateTimeRunner::DateTimeRunner(QObject *parent, const KPluginMetaData &metaData) 0032 : AbstractRunner(parent, metaData) 0033 { 0034 addSyntax(dateWord, i18n("Displays the current date")); 0035 addSyntax(timeWord, i18n("Displays the current time")); 0036 addSyntax(dateWord + i18nc("The <> and space are part of the example query", " <timezone>"), 0037 i18n("Displays the current date and difference to system date in a given timezone")); 0038 addSyntax(timeWord + i18nc("The <> and space are part of the example query", " <timezone>"), 0039 i18n("Displays the current time and difference to system time in a given timezone")); 0040 addSyntax(i18nc("The <> and space are part of the example query", "<time> <timezone> in <timezone>"), 0041 i18n("Converts the time from the first timezone to the second timezone. If only one time zone is given, the other will be the " 0042 "system time zone. If no date or time is given, it will be the current date and time.")); 0043 } 0044 0045 DateTimeRunner::~DateTimeRunner() 0046 { 0047 } 0048 0049 void DateTimeRunner::match(RunnerContext &context) 0050 { 0051 QString term = context.query(); 0052 0053 if (term.compare(dateWord, Qt::CaseInsensitive) == 0) { 0054 // current date in local time zone 0055 0056 const QDate date = QDate::currentDate(); 0057 const QString dateStr = QLocale().toString(date); 0058 addMatch(i18n("Today's date is %1", dateStr), dateStr, 1.0, QStringLiteral("view-calendar-day"), context); 0059 } else if (term.startsWith(dateWord + QLatin1Char(' '), Qt::CaseInsensitive)) { 0060 // current date in remote time zone 0061 0062 term = term.replace(conversionWordsRegex, QStringLiteral(" ")); // strip away conversion words 0063 const auto zoneTerm = QStringView(term).right(term.length() - dateWord.length() - 1); 0064 const auto zones = matchingTimeZones(zoneTerm); 0065 for (auto it = zones.constBegin(), itEnd = zones.constEnd(); it != itEnd; ++it) { 0066 const QTimeZone zone = it.value(); 0067 const QString zoneStr = it.key(); 0068 const QDateTime datetime = QDateTime::currentDateTime().toTimeZone(zone); 0069 const QString dateStr = QLocale().toString(datetime.date()); 0070 0071 const qint64 dateDiff = QDateTime::currentDateTime().daysTo(QDateTime(datetime.date(), datetime.time())) * (24 * 60 * 60 * 1000); // full days in ms 0072 const QString dateDiffNumStr = KFormat().formatSpelloutDuration(abs(dateDiff)); 0073 const QString dateDiffLaterStr = 0074 i18nc("time difference between time zones, e.g. in Stockholm it's 1 calendar day later than in Brasilia", "%1 later", dateDiffNumStr); 0075 const QString dateDiffEarlierStr = 0076 i18nc("date difference between time zones, e.g. in Brasilia it's 1 calendar day earlier than in Stockholm", "%1 earlier", dateDiffNumStr); 0077 const QString dateDiffSameStr = 0078 i18nc("no date difference between time zones, e.g. in Stockholm it's the same calendar day as in Berlin", "no date difference"); 0079 const QString dateDiffStr = dateDiff > 0 ? dateDiffLaterStr : dateDiff < 0 ? dateDiffEarlierStr : dateDiffSameStr; 0080 0081 addMatch(QStringLiteral("%1: %2 (%3)").arg(zoneStr, dateStr, dateDiffStr), 0082 dateStr, 0083 ((qreal)(zoneStr.count(zoneTerm, Qt::CaseInsensitive)) * zoneTerm.length() - (qreal)zoneStr.indexOf(zoneTerm, Qt::CaseInsensitive)) 0084 / zoneStr.length(), 0085 QStringLiteral("view-calendar-day"), 0086 context); 0087 } 0088 } else if (term.compare(timeWord, Qt::CaseInsensitive) == 0) { 0089 // current time in local time zone 0090 0091 const QTime time = QTime::currentTime(); 0092 const QString timeStr = QLocale().toString(time); 0093 addMatch(i18n("Current time is %1", timeStr), timeStr, 1.0, QStringLiteral("clock"), context); 0094 } else if (term.startsWith(timeWord + QLatin1Char(' '), Qt::CaseInsensitive)) { 0095 // current time in remote time zone 0096 0097 term = term.replace(conversionWordsRegex, QStringLiteral(" ")); // strip away conversion words 0098 const auto zoneTerm = QStringView(term).right(term.length() - timeWord.length() - 1); 0099 const auto zones = matchingTimeZones(zoneTerm); 0100 for (auto it = zones.constBegin(), itEnd = zones.constEnd(); it != itEnd; ++it) { 0101 const QTimeZone zone = it.value(); 0102 const QString zoneStr = it.key(); 0103 const QDateTime datetime = QDateTime::currentDateTime().toTimeZone(zone); 0104 const QString timeStr = QLocale().toString(datetime.time(), QLocale::ShortFormat); 0105 0106 const qint64 dateDiff = QDateTime::currentDateTime().daysTo(QDateTime(datetime.date(), datetime.time())) * (24 * 60 * 60 * 1000); // full days in ms 0107 const QString dayDiffNumStr = KFormat().formatSpelloutDuration(abs(dateDiff)); 0108 const QString timeDayLaterStr = i18nc( 0109 "time difference with calendar date difference between time zones, e.g. 22:00 Brasilia time in Stockholm = " 0110 "02:00 + 1 day, where %1 is the time and %2 is " 0111 "the days later", 0112 "%1 + %2", 0113 timeStr, 0114 dayDiffNumStr); 0115 const QString timeDayEarlierStr = i18nc( 0116 "time difference with calendar date difference between time zones, e.g. 02:00 Stockholm time in Brasilia " 0117 "= 22:00 - 1 day, where %1 is the time and %2 is " 0118 "the days earlier", 0119 "%1 - %2", 0120 timeStr, 0121 dayDiffNumStr); 0122 const QString timeDayStr = dateDiff > 0 ? timeDayLaterStr : dateDiff < 0 ? timeDayEarlierStr : timeStr; 0123 0124 const qint64 timeDiff = round((double)QDateTime::currentDateTime().secsTo(QDateTime(datetime.date(), datetime.time())) / 60) 0125 * (60 * 1000); // time in ms rounded to the nearest full 0126 const QString timeDiffNumStr = KFormat().formatSpelloutDuration(abs(timeDiff)); 0127 const QString timeDiffLaterStr = 0128 i18nc("time difference between time zones, e.g. in Stockholm it's 4 hours later than in Brasilia", "%1 later", timeDiffNumStr); 0129 const QString timeDiffEarlierStr = 0130 i18nc("time difference between time zones, e.g. in Brasilia it's 4 hours earlier than in Stockholm", "%1 earlier", timeDiffNumStr); 0131 const QString timeDiffSameStr = 0132 i18nc("no time difference between time zones, e.g. in Stockholm it's the same time as in Berlin", "no time difference"); 0133 const QString timeDiffStr = timeDiff > 0 ? timeDiffLaterStr : timeDiff < 0 ? timeDiffEarlierStr : timeDiffSameStr; 0134 0135 addMatch(QStringLiteral("%1: %2 (%3)").arg(zoneStr, timeDayStr, timeDiffStr), 0136 timeStr, 0137 ((qreal)(zoneStr.count(zoneTerm, Qt::CaseInsensitive)) * zoneTerm.length() - (qreal)zoneStr.indexOf(zoneTerm, Qt::CaseInsensitive)) 0138 / zoneStr.length(), 0139 QStringLiteral("clock"), 0140 context); 0141 } 0142 } else { 0143 // convert user-given time between user-given timezones 0144 0145 QDate date; 0146 QTime time; 0147 QString dtTerm; 0148 // shortest possible string: only time without leading zeros 0149 const int minLen = QLocale().timeFormat(QLocale::ShortFormat).length(); 0150 // longest possible string: date and time with more characters for two-digit numbers in each of the components and different AP format 0151 const int maxLen = QLocale().dateTimeFormat(QLocale::ShortFormat).length() + 8; 0152 // check all long and short enough initial substrings 0153 for (int n = minLen; n <= maxLen && n <= term.length() && time.isNull(); ++n) { 0154 dtTerm = term.mid(0, n); 0155 // try to parse query substring as datetime or time 0156 if (QDateTime dateTimeParse = QLocale().toDateTime(dtTerm, QLocale::ShortFormat); dateTimeParse.isValid()) { 0157 date = dateTimeParse.date(); 0158 time = dateTimeParse.time(); 0159 } else if (QTime timeParse = QLocale().toTime(dtTerm, QLocale::ShortFormat); timeParse.isValid()) { 0160 time = timeParse; 0161 } 0162 } 0163 // unspecified date and/or time will later be initialized to current date/time in from time zone 0164 0165 const QString zonesTerm = time.isValid() ? term.mid(dtTerm.length()) : term; 0166 const QRegularExpressionMatch convTermMatch = conversionWordsRegex.match(zonesTerm); 0167 if (!time.isValid() && !convTermMatch.hasMatch()) { 0168 return; 0169 } 0170 const int start = convTermMatch.hasMatch() ? convTermMatch.capturedStart() : zonesTerm.length() + 1; 0171 const int end = convTermMatch.hasMatch() ? convTermMatch.capturedEnd() : zonesTerm.length() + 1; 0172 0173 // time zone to convert from: left of conversion word, if empty default to system time zone 0174 const QStringView fromZoneTerm = QStringView(zonesTerm).mid(0, start).trimmed(); 0175 const QHash<QString, QTimeZone> fromZones = matchingTimeZones(fromZoneTerm, QDateTime(date, time)); 0176 0177 // time zone to convert to: right of conversion word, if empty default to system time zone 0178 const QStringView toZoneTerm = QStringView(zonesTerm).mid(end).trimmed(); 0179 const QHash<QString, QTimeZone> toZones = matchingTimeZones(toZoneTerm, QDateTime(date, time)); 0180 0181 if (fromZoneTerm.isEmpty() && toZoneTerm.isEmpty()) { 0182 return; 0183 } 0184 0185 for (auto it = fromZones.constBegin(), itEnd = fromZones.constEnd(); it != itEnd; ++it) { 0186 const QTimeZone fromZone = it.value(); 0187 const QString fromZoneStr = it.key(); 0188 const QDate fromDate = date.isValid() ? date : QDateTime::currentDateTimeUtc().toTimeZone(fromZone).date(); 0189 const QTime fromTime = time.isValid() ? time : QDateTime::currentDateTimeUtc().toTimeZone(fromZone).time(); 0190 const QDateTime fromDatetime = QDateTime(fromDate, fromTime, fromZone); 0191 const QString fromTimeStr = QLocale().toString(fromDatetime.time(), QLocale::ShortFormat); 0192 0193 for (auto jt = toZones.constBegin(), itEnd = toZones.constEnd(); jt != itEnd; ++jt) { 0194 const QTimeZone toZone = jt.value(); 0195 const QString toZoneStr = jt.key(); 0196 const QDateTime toDatetime = fromDatetime.toTimeZone(toZone); 0197 const QString toTimeStr = QLocale().toString(toDatetime.time(), QLocale::ShortFormat); 0198 0199 const qint64 dateDiff = QDateTime(fromDatetime.date(), fromDatetime.time()).daysTo(QDateTime(toDatetime.date(), toDatetime.time())) 0200 * (24 * 60 * 60 * 1000); // full days in ms 0201 const QString dayDiffNumStr = KFormat().formatSpelloutDuration(abs(dateDiff)); 0202 const QString toTimeDayLaterStr = i18nc( 0203 "time difference with calendar date difference between time zones, e.g. 22:00 Brasilia time in Stockholm = " 0204 "02:00 + 1 day, where %1 is the time and %2 is " 0205 "the days later", 0206 "%1 + %2", 0207 toTimeStr, 0208 dayDiffNumStr); 0209 const QString toTimeDayEarlierStr = i18nc( 0210 "time difference with calendar date difference between time zones, e.g. 02:00 Stockholm time in Brasilia " 0211 "= 22:00 - 1 day, where %1 is the time and %2 is " 0212 "the days earlier", 0213 "%1 - %2", 0214 toTimeStr, 0215 dayDiffNumStr); 0216 const QString toTimeDayStr = dateDiff > 0 ? toTimeDayLaterStr : dateDiff < 0 ? toTimeDayEarlierStr : toTimeStr; 0217 0218 const qint64 timeDiff = 0219 round((double)QDateTime(fromDatetime.date(), fromDatetime.time()).secsTo(QDateTime(toDatetime.date(), toDatetime.time())) / 60) 0220 * (60 * 1000); // time in ms rounded to the nearest full minutes 0221 const QString timeDiffNumStr = KFormat().formatSpelloutDuration(abs(timeDiff)); 0222 const QString timeDiffLaterStr = 0223 i18nc("time difference between time zones, e.g. in Stockholm it's 4 hours later than in Brasilia", "%1 later", timeDiffNumStr); 0224 const QString timeDiffEarlierStr = 0225 i18nc("time difference between time zones, e.g. in Brasilia it's 4 hours earlier than in Stockholm", "%1 earlier", timeDiffNumStr); 0226 const QString timeDiffSameStr = 0227 i18nc("no time difference between time zones, e.g. in Stockholm it's the same time as in Berlin", "no time difference"); 0228 const QString timeDiffStr = timeDiff > 0 ? timeDiffLaterStr : timeDiff < 0 ? timeDiffEarlierStr : timeDiffSameStr; 0229 0230 const qreal toZoneRelevance = ((qreal)(toZoneStr.count(toZoneTerm, Qt::CaseInsensitive)) * toZoneTerm.length() 0231 - (qreal)toZoneStr.indexOf(toZoneTerm, Qt::CaseInsensitive)) 0232 / toZoneStr.length(); 0233 const qreal fromZoneRelevance = ((qreal)(fromZoneStr.count(fromZoneTerm, Qt::CaseInsensitive)) * fromZoneTerm.length() 0234 - (qreal)fromZoneStr.indexOf(fromZoneTerm, Qt::CaseInsensitive)) 0235 / fromZoneStr.length(); 0236 const qreal relevance = toZoneRelevance / 2 + fromZoneRelevance / 2; 0237 0238 addMatch(QStringLiteral("%1: %2 (%3)<br>%4: %5").arg(toZoneStr, toTimeDayStr, timeDiffStr, fromZoneStr, fromTimeStr), 0239 toTimeStr, 0240 relevance, 0241 QStringLiteral("clock"), 0242 context); 0243 } 0244 } 0245 } 0246 } 0247 0248 void DateTimeRunner::run(const RunnerContext &context, const QueryMatch &match) 0249 { 0250 const QString clipboardText = match.data().toString(); 0251 context.requestQueryStringUpdate(clipboardText, clipboardText.length()); 0252 } 0253 0254 QHash<QString, QTimeZone> DateTimeRunner::matchingTimeZones(const QStringView &zoneTerm, const QDateTime referenceDatetime) 0255 { 0256 QHash<QString, QTimeZone> ret; 0257 0258 if (zoneTerm.isEmpty()) { 0259 const QTimeZone systemTimeZone = QTimeZone::systemTimeZone().isValid() ? QTimeZone::systemTimeZone() : QTimeZone::utc(); // needed for FreeBSD CI 0260 const QDate atDate = referenceDatetime.date().isValid() ? referenceDatetime.date() : QDateTime::currentDateTime().date(); 0261 const QTime atTime = referenceDatetime.time().isValid() ? referenceDatetime.time() : QDateTime::currentDateTime().time(); 0262 const QDateTime atDatetime(atDate, atTime, systemTimeZone); 0263 ret[systemTimeZone.abbreviation(atDatetime)] = systemTimeZone; 0264 return ret; 0265 } 0266 0267 const QList<QByteArray> timeZoneIds = QTimeZone::availableTimeZoneIds(); 0268 for (const QByteArray &zoneId : timeZoneIds) { 0269 QTimeZone timeZone(zoneId); 0270 const QDate atDate = referenceDatetime.date().isValid() ? referenceDatetime.date() : QDateTime::currentDateTime().toTimeZone(timeZone).date(); 0271 const QTime atTime = referenceDatetime.time().isValid() ? referenceDatetime.time() : QDateTime::currentDateTime().toTimeZone(timeZone).time(); 0272 const QDateTime atDatetime(atDate, atTime, timeZone); 0273 0274 if (zoneId.startsWith(QByteArrayView("UTC+")) || zoneId.startsWith(QByteArrayView("UTC-"))) { 0275 // Qt time zones are either of the form 0276 // (where {zone name} {long name} {abbreviation} {short name} {offset name} {country}) 0277 // - "Europe/Stockholm" "Central European Standard Time" "CET" "GMT+1" "UTC+01:00" "Sweden" 0278 // - "UTC+01:00" "UTC+01:00" "UTC+01:00" "UTC+01:00" "UTC+01:00" "Default" 0279 // The latter are already covered by the offset name of the former, which we want to match exactly, so skip these 0280 continue; 0281 } 0282 0283 const QString zoneName = QString::fromUtf8(zoneId); 0284 // eg "Stockholm" 0285 const QString city = zoneName.mid(zoneName.indexOf(QLatin1Char('/')) + 1).replace(QLatin1Char('_'), QLatin1Char(' ')); 0286 if (city.contains(zoneTerm, Qt::CaseInsensitive)) { 0287 ret[city] = timeZone; 0288 continue; 0289 } 0290 0291 // eg "Sweden" 0292 const QString country = QLocale::countryToString(timeZone.country()); 0293 const QString comment = timeZone.comment(); 0294 if (country.contains(zoneTerm, Qt::CaseInsensitive) || comment.contains(zoneTerm, Qt::CaseInsensitive)) { 0295 const QString regionName = comment.isEmpty() ? country : QLatin1String("%1 - %2").arg(country, comment); 0296 ret[regionName] = timeZone; 0297 continue; 0298 } 0299 // eg "Central European Standard Time" 0300 const QString longName = timeZone.displayName(atDatetime, QTimeZone::LongName); 0301 if (longName.contains(zoneTerm, Qt::CaseInsensitive)) { 0302 ret[longName] = timeZone; 0303 continue; 0304 } 0305 0306 // eg "CET" 0307 // FIXME: This only includes the current abbreviation and not old abbreviation or other possible names. 0308 // Eg - depending on the current date, only CET or CEST will work 0309 const QString abbr = timeZone.abbreviation(atDatetime); 0310 if (abbr.contains(zoneTerm, Qt::CaseInsensitive)) { 0311 // Combine abbreviation with display name to disambiguate 0312 // Eg - Pacific Standard Time (PST) and Philippine Standard Time (PST) 0313 const QString abbrName = QString("%1 (%2)").arg(longName, abbr); 0314 ret[abbrName] = timeZone; 0315 continue; 0316 } 0317 0318 // eg "GMT+1" 0319 const QString shortName = timeZone.displayName(atDatetime, QTimeZone::ShortName); 0320 if (shortName.compare(zoneTerm, Qt::CaseInsensitive) == 0) { 0321 ret[shortName] = timeZone; 0322 continue; 0323 } 0324 0325 // eg "UTC+01:00" 0326 const QString offsetName = timeZone.displayName(atDatetime, QTimeZone::OffsetName); 0327 if (offsetName.compare(zoneTerm, Qt::CaseInsensitive) == 0) { 0328 ret[offsetName] = timeZone; 0329 continue; 0330 } 0331 } 0332 0333 return ret; 0334 } 0335 0336 void DateTimeRunner::addMatch(const QString &text, const QString &clipboardText, const qreal &relevance, const QString &iconName, RunnerContext &context) 0337 { 0338 QueryMatch match(this); 0339 match.setText(text); 0340 match.setData(clipboardText); 0341 match.setCategoryRelevance(QueryMatch::CategoryRelevance::Moderate); 0342 match.setRelevance(relevance); 0343 match.setIconName(iconName); 0344 match.setMultiLine(true); 0345 0346 context.addMatch(match); 0347 } 0348 0349 K_PLUGIN_CLASS_WITH_JSON(DateTimeRunner, "plasma-runner-datetime.json") 0350 0351 #include "datetimerunner.moc"