File indexing completed on 2024-04-28 16:44:43

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 #include <QMutex>
0018 
0019 #include <cmath>
0020 
0021 static QMutex s_initMutex;
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, const QVariantList &args)
0028     : AbstractRunner(parent, metaData, args)
0029 {
0030     setObjectName(QStringLiteral("Converter"));
0031 
0032     const QString description = i18n(
0033         "Converts the value of :q: when :q: is made up of "
0034         "\"value unit [>, to, as, in] unit\". You can use the "
0035         "Unit converter applet to find all available units.");
0036     addSyntax(RunnerSyntax(QStringLiteral(":q:"), description));
0037 }
0038 
0039 void ConverterRunner::init()
0040 {
0041     valueRegex = QRegularExpression(QStringLiteral("^([0-9,./+-]+)"));
0042     const QStringList conversionWords = i18nc("list of words that can used as amount of 'unit1' [in|to|as] 'unit2'", "in;to;as").split(QLatin1Char(';'));
0043     QString conversionRegex;
0044     for (const auto &word : conversionWords) {
0045         conversionRegex.append(QLatin1Char(' ') + word + QStringLiteral(" |"));
0046     }
0047     conversionRegex.append(QStringLiteral(" ?> ?"));
0048     unitSeperatorRegex = QRegularExpression(conversionRegex);
0049 
0050     actionList = {new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy unit and number"), this)};
0051     setMinLetterCount(2);
0052     setMatchRegex(valueRegex);
0053 }
0054 
0055 ConverterRunner::~ConverterRunner() = default;
0056 
0057 void ConverterRunner::match(RunnerContext &context)
0058 {
0059     const QRegularExpressionMatch valueRegexMatch = valueRegex.match(context.query());
0060     if (!valueRegexMatch.hasMatch()) {
0061         return;
0062     }
0063 
0064     const QString inputValueString = valueRegexMatch.captured(1);
0065 
0066     // Get the different units by splitting up the query with the regex
0067     QStringList unitStrings = context.query().simplified().remove(valueRegex).split(unitSeperatorRegex);
0068     if (unitStrings.isEmpty() || unitStrings.at(0).isEmpty()) {
0069         return;
0070     }
0071 
0072     // Initialize if not done already
0073     {
0074         QMutexLocker lock(&s_initMutex);
0075         if (!converter) {
0076             converter = std::make_unique<KUnitConversion::Converter>();
0077             insertCompatibleUnits();
0078         }
0079     }
0080 
0081     // Check if unit is valid, otherwise check for the value in the compatibleUnits map
0082     QString inputUnitString = unitStrings.first().simplified();
0083     KUnitConversion::UnitCategory inputCategory = converter->categoryForUnit(inputUnitString);
0084     if (inputCategory.id() == KUnitConversion::InvalidCategory) {
0085         inputUnitString = compatibleUnits.value(inputUnitString.toUpper());
0086         inputCategory = converter->categoryForUnit(inputUnitString);
0087         if (inputCategory.id() == KUnitConversion::InvalidCategory) {
0088             return;
0089         }
0090     }
0091 
0092     QString outputUnitString;
0093     if (unitStrings.size() == 2) {
0094         outputUnitString = unitStrings.at(1).simplified();
0095     }
0096 
0097     const KUnitConversion::Unit inputUnit = inputCategory.unit(inputUnitString);
0098     const QList<KUnitConversion::Unit> outputUnits = createResultUnits(outputUnitString, inputCategory);
0099     const auto numberDataPair = getValidatedNumberValue(inputValueString);
0100     // Return on invalid user input
0101     if (!numberDataPair.first) {
0102         return;
0103     }
0104 
0105     const double numberValue = numberDataPair.second;
0106     QList<QueryMatch> matches;
0107     for (const KUnitConversion::Unit &outputUnit : outputUnits) {
0108         KUnitConversion::Value outputValue = inputCategory.convert(KUnitConversion::Value(numberValue, inputUnit), outputUnit);
0109         if (!outputValue.isValid() || inputUnit == outputUnit) {
0110             continue;
0111         }
0112 
0113         QueryMatch match(this);
0114         match.setType(QueryMatch::HelperMatch);
0115         match.setIconName(QStringLiteral("accessories-calculator"));
0116         if (outputUnit.categoryId() == KUnitConversion::CurrencyCategory) {
0117             match.setText(QStringLiteral("%1 (%2)").arg(outputValue.toString(0, 'f', 2), outputUnit.symbol()));
0118         } else {
0119             match.setText(QStringLiteral("%1 (%2)").arg(outputValue.toString(), outputUnit.symbol()));
0120         }
0121         match.setData(QVariant::fromValue(outputValue));
0122         match.setRelevance(1.0 - std::abs(std::log10(outputValue.number())) / 50.0);
0123         match.setActions(actionList);
0124         matches.append(match);
0125     }
0126 
0127     context.addMatches(matches);
0128 }
0129 
0130 void ConverterRunner::run(const RunnerContext &context, const QueryMatch &match)
0131 {
0132     Q_UNUSED(context)
0133 
0134     const auto value = match.data().value<KUnitConversion::Value>();
0135 
0136     if (match.selectedAction()) {
0137         QGuiApplication::clipboard()->setText(value.toString());
0138     } else {
0139         QGuiApplication::clipboard()->setText(QString::number(value.number(), 'f', QLocale::FloatingPointShortest));
0140     }
0141 }
0142 
0143 QMimeData *ConverterRunner::mimeDataForMatch(const QueryMatch &match)
0144 {
0145     const auto value = match.data().value<KUnitConversion::Value>();
0146 
0147     auto *mimeData = new QMimeData();
0148     mimeData->setText(value.toSymbolString());
0149     return mimeData;
0150 }
0151 
0152 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0153 QPair<bool, double> ConverterRunner::stringToDouble(const QStringRef &value)
0154 #else
0155 QPair<bool, double> ConverterRunner::stringToDouble(const QStringView &value)
0156 #endif
0157 {
0158     bool ok;
0159     double numberValue = locale.toDouble(value, &ok);
0160     if (!ok) {
0161         numberValue = value.toDouble(&ok);
0162     }
0163     return {ok, numberValue};
0164 }
0165 
0166 QPair<bool, double> ConverterRunner::getValidatedNumberValue(const QString &value)
0167 {
0168 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0169     const auto fractionParts = value.splitRef(QLatin1Char('/'), Qt::SkipEmptyParts);
0170 #else
0171     const auto fractionParts = QStringView(value).split(QLatin1Char('/'), Qt::SkipEmptyParts);
0172 #endif
0173     if (fractionParts.isEmpty() || fractionParts.count() > 2) {
0174         return {false, 0};
0175     }
0176 
0177     if (fractionParts.count() == 2) {
0178         const QPair<bool, double> doubleFirstResults = stringToDouble(fractionParts.first());
0179         if (!doubleFirstResults.first) {
0180             return {false, 0};
0181         }
0182         const QPair<bool, double> doubleSecondResult = stringToDouble(fractionParts.last());
0183         if (!doubleSecondResult.first || qFuzzyIsNull(doubleSecondResult.second)) {
0184             return {false, 0};
0185         }
0186         return {true, doubleFirstResults.second / doubleSecondResult.second};
0187     } else if (fractionParts.count() == 1) {
0188         const QPair<bool, double> doubleResult = stringToDouble(fractionParts.first());
0189         if (!doubleResult.first) {
0190             return {false, 0};
0191         }
0192         return {true, doubleResult.second};
0193     } else {
0194         return {true, 0};
0195     }
0196 }
0197 
0198 QList<KUnitConversion::Unit> ConverterRunner::createResultUnits(QString &outputUnitString, const KUnitConversion::UnitCategory &category)
0199 {
0200     QList<KUnitConversion::Unit> units;
0201     if (!outputUnitString.isEmpty()) {
0202         KUnitConversion::Unit outputUnit = category.unit(outputUnitString);
0203         if (!outputUnit.isNull() && outputUnit.isValid()) {
0204             units.append(outputUnit);
0205         } else {
0206             // Autocompletion for the target units
0207             outputUnitString = outputUnitString.toUpper();
0208             for (auto it = compatibleUnits.constBegin(); it != compatibleUnits.constEnd(); it++) {
0209                 if (it.key().startsWith(outputUnitString)) {
0210                     outputUnit = category.unit(it.value());
0211                     if (!units.contains(outputUnit)) {
0212                         units << outputUnit;
0213                     }
0214                 }
0215             }
0216         }
0217     } else {
0218         units = category.mostCommonUnits();
0219         // suggest converting to the user's local currency
0220         if (category.id() == KUnitConversion::CurrencyCategory) {
0221             const QString &currencyIsoCode = QLocale().currencySymbol(QLocale::CurrencyIsoCode);
0222 
0223             const KUnitConversion::Unit localCurrency = category.unit(currencyIsoCode);
0224             if (localCurrency.isValid() && !units.contains(localCurrency)) {
0225                 units << localCurrency;
0226             }
0227         }
0228     }
0229 
0230     return units;
0231 }
0232 
0233 void ConverterRunner::insertCompatibleUnits()
0234 {
0235     // Add all currency symbols to the map, if their ISO code is supported by backend
0236     const QList<QLocale> allLocales = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale::AnyScript, QLocale::AnyCountry);
0237     KUnitConversion::UnitCategory currencyCategory = converter->category(QStringLiteral("Currency"));
0238     const QStringList availableISOCodes = currencyCategory.allUnits();
0239     const QRegularExpression hasCurrencyRegex = QRegularExpression(QStringLiteral("\\p{Sc}")); // clazy:exclude=use-static-qregularexpression
0240     for (const auto &currencyLocale : allLocales) {
0241         const QString symbol = currencyLocale.currencySymbol(QLocale::CurrencySymbol);
0242         const QString isoCode = currencyLocale.currencySymbol(QLocale::CurrencyIsoCode);
0243 
0244         if (isoCode.isEmpty() || !symbol.contains(hasCurrencyRegex)) {
0245             continue;
0246         }
0247         if (availableISOCodes.contains(isoCode)) {
0248             compatibleUnits.insert(symbol.toUpper(), isoCode);
0249         }
0250     }
0251 
0252     // Add all units as uppercase in the map
0253     const auto categories = converter->categories();
0254     for (const auto &category : categories) {
0255         const auto allUnits = category.allUnits();
0256         for (const auto &unit : allUnits) {
0257             compatibleUnits.insert(unit.toUpper(), unit);
0258         }
0259     }
0260 }
0261 #include "converterrunner.moc"