File indexing completed on 2024-05-12 09:31:47

0001 /*
0002  * SPDX-FileCopyrightText: 2007, 2008 Petri Damstén <damu@iki.fi>
0003  * SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "converterrunner.h"
0009 
0010 #include <KLocalizedString>
0011 #include <QClipboard>
0012 #include <QDebug>
0013 #include <QDesktopServices>
0014 #include <QGuiApplication>
0015 #include <QLocale>
0016 #include <QMimeData>
0017 
0018 #include <chrono>
0019 #include <cmath>
0020 
0021 using namespace std::chrono_literals;
0022 
0023 K_PLUGIN_CLASS_WITH_JSON(ConverterRunner, "plasma-runner-converter.json")
0024 
0025 Q_DECLARE_METATYPE(KUnitConversion::Value)
0026 
0027 ConverterRunner::ConverterRunner(QObject *parent, const KPluginMetaData &metaData)
0028     : AbstractRunner(parent, metaData)
0029     , actionList({Action(QStringLiteral("copy"), QStringLiteral("edit-copy"), i18n("Copy unit and number"))})
0030 {
0031     addSyntax(QStringLiteral(":q:"),
0032               i18n("Converts the value of :q: when :q: is made up of value unit [>, to, as, in] unit."
0033                    "You can use the Unit converter applet to find all available units."));
0034 }
0035 
0036 void ConverterRunner::init()
0037 {
0038     valueRegex = QRegularExpression(QStringLiteral("^([0-9,./+-]+)"));
0039     const QStringList conversionWords = i18nc("list of words that can used as amount of 'unit1' [in|to|as] 'unit2'", "in;to;as").split(QLatin1Char(';'));
0040     QString conversionRegex;
0041     for (const auto &word : conversionWords) {
0042         conversionRegex.append(QLatin1Char(' ') + word + QStringLiteral(" |"));
0043     }
0044     conversionRegex.append(QStringLiteral(" ?> ?"));
0045     unitSeperatorRegex = QRegularExpression(conversionRegex);
0046 
0047     setMinLetterCount(2);
0048     setMatchRegex(valueRegex);
0049 
0050     converter = std::make_unique<KUnitConversion::Converter>();
0051     updateCompatibleUnits();
0052 
0053     m_currencyUpdateTimer.setInterval(24h);
0054     connect(&m_currencyUpdateTimer, &QTimer::timeout, this, &ConverterRunner::updateCompatibleUnits);
0055     m_currencyUpdateTimer.start();
0056 }
0057 
0058 ConverterRunner::~ConverterRunner() = default;
0059 
0060 void ConverterRunner::match(RunnerContext &context)
0061 {
0062     const QRegularExpressionMatch valueRegexMatch = valueRegex.match(context.query());
0063     if (!valueRegexMatch.hasMatch()) {
0064         return;
0065     }
0066 
0067     const QString inputValueString = valueRegexMatch.captured(1);
0068 
0069     // Get the different units by splitting up the query with the regex
0070     QStringList unitStrings = context.query().simplified().remove(valueRegex).split(unitSeperatorRegex);
0071     if (unitStrings.isEmpty() || unitStrings.at(0).isEmpty()) {
0072         return;
0073     }
0074 
0075     // Check if unit is valid, otherwise check for the value in the compatibleUnits map
0076     QString inputUnitString = unitStrings.first().simplified();
0077     KUnitConversion::UnitCategory inputCategory = converter->categoryForUnit(inputUnitString);
0078     if (inputCategory.id() == KUnitConversion::InvalidCategory) {
0079         inputUnitString = compatibleUnits.value(inputUnitString.toUpper());
0080         inputCategory = converter->categoryForUnit(inputUnitString);
0081         if (inputCategory.id() == KUnitConversion::InvalidCategory) {
0082             return;
0083         }
0084     }
0085 
0086     QString outputUnitString;
0087     if (unitStrings.size() == 2) {
0088         outputUnitString = unitStrings.at(1).simplified();
0089     }
0090 
0091     const KUnitConversion::Unit inputUnit = inputCategory.unit(inputUnitString);
0092     const QList<KUnitConversion::Unit> outputUnits = createResultUnits(outputUnitString, inputCategory);
0093     const auto numberDataPair = getValidatedNumberValue(inputValueString);
0094     // Return on invalid user input
0095     if (!numberDataPair.first) {
0096         return;
0097     }
0098 
0099     const double numberValue = numberDataPair.second;
0100     QList<QueryMatch> matches;
0101     for (const KUnitConversion::Unit &outputUnit : outputUnits) {
0102         KUnitConversion::Value outputValue = inputCategory.convert(KUnitConversion::Value(numberValue, inputUnit), outputUnit);
0103         if (!outputValue.isValid() || inputUnit == outputUnit) {
0104             continue;
0105         }
0106 
0107         QueryMatch match(this);
0108         match.setCategoryRelevance(QueryMatch::CategoryRelevance::Moderate);
0109         match.setIconName(QStringLiteral("accessories-calculator"));
0110         if (outputUnit.categoryId() == KUnitConversion::CurrencyCategory) {
0111             match.setText(QStringLiteral("%1 (%2)").arg(outputValue.toString(0, 'f', 2), outputUnit.symbol()));
0112         } else {
0113             match.setText(QStringLiteral("%1 (%2)").arg(outputValue.toString(), outputUnit.symbol()));
0114         }
0115         match.setData(QVariant::fromValue(outputValue));
0116         match.setRelevance(1.0 - std::abs(std::log10(outputValue.number())) / 50.0);
0117         match.setActions(actionList);
0118         matches.append(match);
0119     }
0120 
0121     context.addMatches(matches);
0122 }
0123 
0124 void ConverterRunner::run(const RunnerContext & /*context*/, const QueryMatch &match)
0125 {
0126     const auto value = match.data().value<KUnitConversion::Value>();
0127 
0128     if (match.selectedAction()) {
0129         QGuiApplication::clipboard()->setText(value.toString());
0130     } else {
0131         QGuiApplication::clipboard()->setText(QString::number(value.number(), 'f', QLocale::FloatingPointShortest));
0132     }
0133 }
0134 
0135 QMimeData *ConverterRunner::mimeDataForMatch(const QueryMatch &match)
0136 {
0137     const auto value = match.data().value<KUnitConversion::Value>();
0138 
0139     auto *mimeData = new QMimeData();
0140     mimeData->setText(value.toSymbolString());
0141     return mimeData;
0142 }
0143 
0144 QPair<bool, double> ConverterRunner::stringToDouble(const QStringView &value)
0145 {
0146     bool ok;
0147     double numberValue = locale.toDouble(value, &ok);
0148     if (!ok) {
0149         numberValue = value.toDouble(&ok);
0150     }
0151     return {ok, numberValue};
0152 }
0153 
0154 QPair<bool, double> ConverterRunner::getValidatedNumberValue(const QString &value)
0155 {
0156     const auto fractionParts = QStringView(value).split(QLatin1Char('/'), Qt::SkipEmptyParts);
0157     if (fractionParts.isEmpty() || fractionParts.count() > 2) {
0158         return {false, 0};
0159     }
0160 
0161     if (fractionParts.count() == 2) {
0162         const QPair<bool, double> doubleFirstResults = stringToDouble(fractionParts.first());
0163         if (!doubleFirstResults.first) {
0164             return {false, 0};
0165         }
0166         const QPair<bool, double> doubleSecondResult = stringToDouble(fractionParts.last());
0167         if (!doubleSecondResult.first || qFuzzyIsNull(doubleSecondResult.second)) {
0168             return {false, 0};
0169         }
0170         return {true, doubleFirstResults.second / doubleSecondResult.second};
0171     } else if (fractionParts.count() == 1) {
0172         const QPair<bool, double> doubleResult = stringToDouble(fractionParts.first());
0173         if (!doubleResult.first) {
0174             return {false, 0};
0175         }
0176         return {true, doubleResult.second};
0177     } else {
0178         return {true, 0};
0179     }
0180 }
0181 
0182 QList<KUnitConversion::Unit> ConverterRunner::createResultUnits(QString &outputUnitString, const KUnitConversion::UnitCategory &category)
0183 {
0184     QList<KUnitConversion::Unit> units;
0185     if (!outputUnitString.isEmpty()) {
0186         KUnitConversion::Unit outputUnit = category.unit(outputUnitString);
0187         if (!outputUnit.isNull() && outputUnit.isValid()) {
0188             units.append(outputUnit);
0189         } else {
0190             // Autocompletion for the target units
0191             outputUnitString = outputUnitString.toUpper();
0192             for (auto it = compatibleUnits.constBegin(); it != compatibleUnits.constEnd(); it++) {
0193                 if (it.key().startsWith(outputUnitString)) {
0194                     outputUnit = category.unit(it.value());
0195                     if (!units.contains(outputUnit)) {
0196                         units << outputUnit;
0197                     }
0198                 }
0199             }
0200         }
0201     } else {
0202         units = category.mostCommonUnits();
0203         // suggest converting to the user's local currency
0204         if (category.id() == KUnitConversion::CurrencyCategory) {
0205             const QString &currencyIsoCode = QLocale().currencySymbol(QLocale::CurrencyIsoCode);
0206 
0207             const KUnitConversion::Unit localCurrency = category.unit(currencyIsoCode);
0208             if (localCurrency.isValid() && !units.contains(localCurrency)) {
0209                 units << localCurrency;
0210             }
0211         }
0212     }
0213 
0214     return units;
0215 }
0216 
0217 void ConverterRunner::updateCompatibleUnits()
0218 {
0219     // Add all currency symbols to the map, if their ISO code is supported by backend
0220     bool isLatest = false;
0221     QMetaObject::invokeMethod(
0222         QCoreApplication::instance(),
0223         [this] {
0224             KUnitConversion::UnitCategory currencyCategory = converter->category(KUnitConversion::CurrencyCategory);
0225             auto updateJob = currencyCategory.syncConversionTable();
0226             if (!updateJob) [[unlikely]] {
0227                 return !compatibleUnits.empty();
0228             }
0229             QEventLoop loop;
0230             loop.connect(updateJob, &KUnitConversion::UpdateJob::finished, &loop, &QEventLoop::quit);
0231             loop.exec();
0232             return false;
0233         },
0234         Qt::BlockingQueuedConnection,
0235         &isLatest);
0236     if (isLatest) {
0237         return;
0238     }
0239 
0240     KUnitConversion::UnitCategory currencyCategory = converter->category(KUnitConversion::CurrencyCategory);
0241     const QList<QLocale> allLocales = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale::AnyScript, QLocale::AnyCountry);
0242     const QStringList availableISOCodes = currencyCategory.allUnits();
0243     const QRegularExpression hasCurrencyRegex = QRegularExpression(QStringLiteral("\\p{Sc}")); // clazy:exclude=use-static-qregularexpression
0244     for (const auto &currencyLocale : allLocales) {
0245         const QString symbol = currencyLocale.currencySymbol(QLocale::CurrencySymbol);
0246         const QString isoCode = currencyLocale.currencySymbol(QLocale::CurrencyIsoCode);
0247 
0248         if (isoCode.isEmpty() || !symbol.contains(hasCurrencyRegex)) {
0249             continue;
0250         }
0251         if (availableISOCodes.contains(isoCode)) {
0252             compatibleUnits.insert(symbol.toUpper(), isoCode);
0253         }
0254     }
0255 
0256     // Add all units as uppercase in the map
0257     const auto categories = converter->categories();
0258     for (const auto &category : categories) {
0259         const auto allUnits = category.allUnits();
0260         for (const auto &unit : allUnits) {
0261             compatibleUnits.insert(unit.toUpper(), unit);
0262         }
0263     }
0264 }
0265 #include "converterrunner.moc"