diff --git a/runners/converter/CMakeLists.txt b/runners/converter/CMakeLists.txt --- a/runners/converter/CMakeLists.txt +++ b/runners/converter/CMakeLists.txt @@ -1,11 +1,9 @@ add_definitions(-DTRANSLATION_DOMAIN=\"plasma_runner_converterrunner\") -set(krunner_converter_SRCS - converterrunner.cpp -) +set(krunner_converter_SRCS converterrunner.cpp) add_library(krunner_converter MODULE ${krunner_converter_SRCS}) -target_link_libraries(krunner_converter KF5::UnitConversion KF5::KIOCore KF5::I18n KF5::Runner) +target_link_libraries(krunner_converter KF5::UnitConversion KF5::I18n KF5::Runner) + install(TARGETS krunner_converter DESTINATION ${KDE_INSTALL_PLUGINDIR}) install(FILES plasma-runner-converter.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) - diff --git a/runners/converter/converterrunner.h b/runners/converter/converterrunner.h --- a/runners/converter/converterrunner.h +++ b/runners/converter/converterrunner.h @@ -1,5 +1,6 @@ /* * Copyright (C) 2007,2008 Petri Damstén + * Copyright (C) 2020 Alexander Lohnau * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -18,24 +19,45 @@ #ifndef CONVERTERRUNNER_H #define CONVERTERRUNNER_H -#include +#include +#include +#include +#include +#include + /** * This class converts values to different units. */ -class ConverterRunner : public Plasma::AbstractRunner +class ConverterRunner: public Plasma::AbstractRunner { Q_OBJECT public: - ConverterRunner(QObject* parent, const QVariantList &args); + ConverterRunner(QObject *parent, const QVariantList &args); + void init() override; ~ConverterRunner() override; void match(Plasma::RunnerContext &context) override; void run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match) override; private: QStringList m_separators; + KUnitConversion::Converter converter; + const QLocale locale; + /** Keys for named regex */ + const QString valueKey = QStringLiteral("queryValue"); + const QString sourceUnitKey = QStringLiteral("sourceUnit"); + const QString targetUnitKey = QStringLiteral("targetUnit"); + QRegularExpression matchRegex; + QRegularExpression hasCurrencyRegex; + /** To convert currency symbols back to ISO string */ + QMap symbolToISOMap; + + QPair stringToDouble(const QStringRef &value); + QPair getValidatedNumberValue(const QString &value); + QList createResultUnits(const QString &outputUnitString, + const KUnitConversion::UnitCategory &category); }; #endif diff --git a/runners/converter/converterrunner.cpp b/runners/converter/converterrunner.cpp --- a/runners/converter/converterrunner.cpp +++ b/runners/converter/converterrunner.cpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2007,2008 Petri Damstén + * Copyright (C) 2020 Alexander Lohnau * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -15,131 +16,68 @@ * along with this program. If not, see . */ + #include "converterrunner.h" + #include #include #include -#include #include #include -#include -#include #include -#define CONVERSION_CHAR QLatin1Char( '>' ) - K_EXPORT_PLASMA_RUNNER(converterrunner, ConverterRunner) -class StringParser +ConverterRunner::ConverterRunner(QObject *parent, const QVariantList &args) + : Plasma::AbstractRunner(parent, args) { -public: - enum GetType - { - GetString = 1, - GetDigit = 2 - }; - - StringParser(const QString &s) : m_index(0), m_s(s) {} - ~StringParser() {} - - QString get(int type) - { - QChar current; - QString result; + setObjectName(QStringLiteral("Converter")); + //can not ignore commands: we have things like m4 + setIgnoredTypes(Plasma::RunnerContext::Directory | Plasma::RunnerContext::File | + Plasma::RunnerContext::NetworkLocation); - passWhiteSpace(); - while (true) { - current = next(); - if (current.isNull()) { - break; - } - if (current.isSpace()) { - break; - } - bool number = isNumber(current); - if (type == GetDigit && !number) { - break; - } - if (type == GetString && number) { - break; - } - if(current == QLatin1Char( CONVERSION_CHAR )) { - break; - } - ++m_index; - result += current; - } - return result; - } + const QString description = i18n("Converts the value of :q: when :q: is made up of " + "\"value unit [>, to, as, in] unit\". You can use the " + "Unit converter applet to find all available units."); + addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:"), description)); +} - bool isNumber(const QChar &ch) - { - if (ch.isNumber()) { - return true; - } - if (QStringLiteral(".,-+/").contains(ch)) { - return true; - } - return false; - } +void ConverterRunner::init() +{ + m_separators << QStringLiteral("<"); + m_separators << i18nc("list of words that can used as amount of 'unit1' [in|to|as] 'unit2'", + "in;to;as").split(QLatin1Char(';')); - QString rest() - { - return m_s.mid(m_index).simplified(); - } + const QList allLocales = QLocale::matchingLocales( + QLocale::AnyLanguage, QLocale::AnyScript, QLocale::AnyCountry); + KUnitConversion::UnitCategory currencyCategory = converter.category(QStringLiteral("Currency")); + const QStringList availableISOCodes = currencyCategory.allUnits(); + hasCurrencyRegex = QRegularExpression(QStringLiteral("\\p{Sc}")); + hasCurrencyRegex.optimize(); - void pass(const QStringList &strings) - { - passWhiteSpace(); - const QString temp = m_s.mid(m_index); + // Get all valid and by backend supported currency symbols + for (const auto ¤cyLocale: allLocales) { + const QString symbol = currencyLocale.currencySymbol(QLocale::CurrencySymbol); + const QString isoCode = currencyLocale.currencySymbol(QLocale::CurrencyIsoCode); - foreach (const QString& s, strings) { - if (temp.startsWith(s)) { - m_index += s.length(); - return; - } + if (isoCode.isEmpty() || !symbol.contains(hasCurrencyRegex)) { + continue; } - } - -private: - void passWhiteSpace() - { - while (next().isSpace()) { - ++m_index; + if (availableISOCodes.contains(isoCode)) { + symbolToISOMap.insert(symbol, isoCode); } } - QChar next() - { - if (m_index >= m_s.size()) { - return QChar::Null; - } - return m_s.at(m_index); - } - - int m_index; - QString m_s; -}; - -ConverterRunner::ConverterRunner(QObject* parent, const QVariantList &args) - : Plasma::AbstractRunner(parent, args) -{ - Q_UNUSED(args) - setObjectName(QLatin1String( "Converter" )); - - m_separators << QString( CONVERSION_CHAR ); - m_separators << i18nc("list of words that can used as amount of 'unit1' [in|to|as] 'unit2'", - "in;to;as").split(QLatin1Char( ';' )); - - //can not ignore commands: we have things like m4 - setIgnoredTypes(Plasma::RunnerContext::Directory | Plasma::RunnerContext::File | - Plasma::RunnerContext::NetworkLocation); - - QString description = i18n("Converts the value of :q: when :q: is made up of " - "\"value unit [>, to, as, in] unit\". You can use the " - "Unit converter applet to find all available units."); - addSyntax(Plasma::RunnerSyntax(QLatin1String(":q:"), description)); + // Construct match regex, see https://regex101.com/r/EUDzQi/17 for demo + const QString valueGroup = QStringLiteral("(?P<%1>[0-9,./]+)").arg(valueKey); + const QString unitGroups = QStringLiteral("(?P<%1>[a-zA-Z/\"'^0-9\\p{Sc}]+)"); + const QString sourceUnitGroup = unitGroups.arg(sourceUnitKey); + const QString targetUnitGroup = unitGroups.arg(targetUnitKey); + const QString separatorsGroup = QStringLiteral("(?: in | to | as | ?> ?)"); + matchRegex = QRegularExpression(QStringLiteral("^%1 ?%2(?:%3%4)?$") + .arg(valueGroup, sourceUnitGroup, separatorsGroup, targetUnitGroup)); + matchRegex.optimize(); } ConverterRunner::~ConverterRunner() @@ -149,170 +87,137 @@ void ConverterRunner::match(Plasma::RunnerContext &context) { const QString term = context.query(); - if (term.size() < 2) { + if (term.size() < 2 || !context.isValid()) { return; } - StringParser cmd(term); - QString unit1; - QString value; - QString unit2; + const QRegularExpressionMatch regexMatch = matchRegex.match(context.query()); + if (!regexMatch.hasMatch() || regexMatch.capturedTexts().size() < 2) { + return; + } + const QString inputValueString = regexMatch.captured(valueKey); - unit1 = cmd.get(StringParser::GetString); - value = cmd.get(StringParser::GetDigit); - if (value.isEmpty()) { + // If the units contain a currency symbol convert them to ISO + QString inputUnitString = regexMatch.captured(sourceUnitKey); + if (inputUnitString.contains(hasCurrencyRegex)) { + inputUnitString = symbolToISOMap.value(inputUnitString.toUpper(), inputUnitString); + } + QString outputUnitString; + if (regexMatch.lastCapturedIndex() == 3) { + outputUnitString = regexMatch.captured(targetUnitKey); + if (outputUnitString.contains(hasCurrencyRegex)) { + outputUnitString = symbolToISOMap.value(outputUnitString.toUpper(), outputUnitString); + } + } + + KUnitConversion::UnitCategory inputCategory = converter.categoryForUnit(inputUnitString); + const KUnitConversion::Unit inputUnit = inputCategory.unit(inputUnitString); + const QList outputUnits = createResultUnits(outputUnitString, inputCategory); + const auto numberDataPair = getValidatedNumberValue(inputValueString); + // Return on invalid user input + if (!numberDataPair.first) { return; } - if (unit1.isEmpty()) { - unit1 = cmd.get(StringParser::GetString | StringParser::GetDigit); - if (unit1.isEmpty()) { - return; + + const double numberValue = numberDataPair.second; + QList matches; + for (const KUnitConversion::Unit &outputUnit: outputUnits) { + const KUnitConversion::Value &outputValue = inputCategory.convert( + KUnitConversion::Value(numberValue, inputUnit), outputUnit); + if (!outputValue.isValid() || inputUnit == outputUnit) { + continue; } + + Plasma::QueryMatch match(this); + match.setType(Plasma::QueryMatch::ExactMatch); + match.setIconName(QStringLiteral("edit-copy")); + match.setText(QStringLiteral("%1 (%2)").arg(outputValue.toString(), outputUnit.symbol())); + match.setData(outputValue.number()); + match.setRelevance(1.0 - std::abs(std::log10(outputValue.number())) / 50.0); + matches.append(match); } - const QString s = cmd.get(StringParser::GetString); + context.addMatches(matches); +} + +void ConverterRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match) +{ + Q_UNUSED(context) + + QGuiApplication::clipboard()->setText(match.data().toString()); +} - if (!s.isEmpty() && !m_separators.contains(s)) { - unit1 += QLatin1Char( ' ' ) + s; +QPair ConverterRunner::stringToDouble(const QStringRef &value) +{ + bool ok; + double numberValue = locale.toDouble(value, &ok); + if (!ok) { + numberValue = value.toDouble(&ok); } - if (s.isEmpty() || !m_separators.contains(s)) { - cmd.pass(m_separators); + return {ok, numberValue}; +} + +QPair ConverterRunner::getValidatedNumberValue(const QString &value) +{ + const auto fractionParts = value.splitRef(QLatin1Char('/'), QString::SkipEmptyParts); + if (fractionParts.isEmpty() || fractionParts.count() > 2) { + return {false, 0}; } - unit2 = cmd.rest(); - KUnitConversion::Converter converter; - KUnitConversion::UnitCategory category = converter.categoryForUnit(unit1); - bool found = false; - if (category.id() == KUnitConversion::InvalidCategory) { - foreach (category, converter.categories()) { - foreach (const QString& s, category.allUnits()) { - if (s.compare(unit1, Qt::CaseInsensitive) == 0) { - unit1 = s; - found = true; - break; - } - } - if (found) { - break; - } + if (fractionParts.count() == 2) { + const QPair doubleFirstResults = stringToDouble(fractionParts.first()); + if (!doubleFirstResults.first) { + return {false, 0}; } - if (!found) { - return; + const QPair doubleSecondResult = stringToDouble(fractionParts.last()); + if (!doubleSecondResult.first || qFuzzyIsNull(doubleSecondResult.second)) { + return {false, 0}; } - } + return {true, doubleFirstResults.second / doubleSecondResult.second}; + } else if (fractionParts.count() == 1) { + const QPair doubleResult = stringToDouble(fractionParts.first()); + if (!doubleResult.first) { + return {false, 0}; + } + return {true, doubleResult.second}; + } else { + return {true, 0}; + } +} +QList ConverterRunner::createResultUnits(const QString &outputUnitString, + const KUnitConversion::UnitCategory &category) +{ QList units; - - if (!unit2.isEmpty()) { - KUnitConversion::Unit u = category.unit(unit2); - if (!u.isNull() && u.isValid()) { - units.append(u); - config().writeEntry(category.name(), u.symbol()); + if (!outputUnitString.isEmpty()) { + KUnitConversion::Unit outputUnit = category.unit(outputUnitString); + if (!outputUnit.isNull() && outputUnit.isValid()) { + units.append(outputUnit); } else { const QStringList unitStrings = category.allUnits(); - QList matchingUnits; - foreach (const QString& s, unitStrings) { - if (s.startsWith(unit2, Qt::CaseInsensitive)) { - u = category.unit(s); - if (!matchingUnits.contains(u)) { - matchingUnits << u; + for (const QString &unitString: unitStrings) { + if (unitString.startsWith(outputUnitString, Qt::CaseInsensitive)) { + outputUnit = category.unit(unitString); + if (!units.contains(outputUnit)) { + units << outputUnit; } } } - units = matchingUnits; - if (units.count() == 1) { - config().writeEntry(category.name(), units[0].symbol()); - } } } else { units = category.mostCommonUnits(); - KUnitConversion::Unit u = category.unit(config().readEntry(category.name())); - if (!u.isNull() && units.indexOf(u) < 0) { - units << u; - } - // suggest converting to the user's local currency if (category.id() == KUnitConversion::CurrencyCategory) { const QString ¤cyIsoCode = QLocale().currencySymbol(QLocale::CurrencyIsoCode); - KUnitConversion::Unit localCurrency = category.unit(currencyIsoCode); + const KUnitConversion::Unit localCurrency = category.unit(currencyIsoCode); if (localCurrency.isValid() && !units.contains(localCurrency)) { units << localCurrency; } } } - QList matches; - - QLocale locale; - auto stringToDouble = [&locale](const QStringRef &value, bool *ok) { - double numberValue = locale.toDouble(value, ok); - if (!(*ok)) { - numberValue = value.toDouble(ok); - } - return numberValue; - }; - - KUnitConversion::Unit u1 = category.unit(unit1); - foreach (const KUnitConversion::Unit& u, units) { - if (u1 == u) { - continue; - } - - double numberValue = 0.0; - - const auto fractionParts = value.splitRef(QLatin1Char('/'), QString::SkipEmptyParts); - if (fractionParts.isEmpty() || fractionParts.count() > 2) { - continue; - } - - if (fractionParts.count() == 2) { - bool ok; - const double numerator = stringToDouble(fractionParts.first(), &ok); - if (!ok) { - continue; - } - const double denominator = stringToDouble(fractionParts.last(), &ok); - if (!ok || qFuzzyIsNull(denominator)) { - continue; - } - - numberValue = numerator / denominator; - } else if (fractionParts.count() == 1) { - bool ok; - numberValue = stringToDouble(fractionParts.first(), &ok); - if (!ok) { - continue; - } - } - - KUnitConversion::Value v = category.convert(KUnitConversion::Value(numberValue, u1), u); - - if (!v.isValid()) { - continue; - } - - Plasma::QueryMatch match(this); - match.setType(Plasma::QueryMatch::InformationalMatch); - match.setIconName(QStringLiteral("edit-copy")); - match.setText(QStringLiteral("%1 (%2)").arg(v.toString(), u.symbol())); - match.setData(v.number()); - match.setRelevance(1.0 - std::abs(std::log10(v.number())) / 50.0); - matches.append(match); - } - - context.addMatches(matches); -} - -void ConverterRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match) -{ - Q_UNUSED(context) - const QString data = match.data().toString(); - if (data.startsWith(QLatin1String("http://"))) { - QDesktopServices::openUrl(QUrl(data)); - } else { - QGuiApplication::clipboard()->setText(data); - } + return units; } #include "converterrunner.moc"