File indexing completed on 2024-05-19 09:31:37

0001 /*
0002     SPDX-FileCopyrightText: 2007 Barış Metin <baris@pardus.org.tr>
0003     SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org>
0004     SPDX-FileCopyrightText: 2007 Richard Moore <rich@kde.org>
0005     SPDX-FileCopyrightText: 2010 Matteo Agostinelli <agostinelli@gmail.com>
0006     SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
0007 
0008     SPDX-License-Identifier: LGPL-2.0-only
0009 */
0010 
0011 #include "calculatorrunner.h"
0012 
0013 #include "qalculate_engine.h"
0014 
0015 #include <QApplication>
0016 #include <QClipboard>
0017 #include <QDebug>
0018 #include <QIcon>
0019 #include <QRegularExpression>
0020 
0021 #include <KLocalizedString>
0022 #include <krunner/querymatch.h>
0023 
0024 K_PLUGIN_CLASS_WITH_JSON(CalculatorRunner, "plasma-runner-calculator.json")
0025 
0026 CalculatorRunner::CalculatorRunner(QObject *parent, const KPluginMetaData &metaData)
0027     : KRunner::AbstractRunner(parent, metaData)
0028     , m_actions({KRunner::Action(QStringLiteral("copy"), QStringLiteral("edit-copy"), i18n("Copy to Clipboard"))})
0029 {
0030     QString description = i18n(
0031         "Calculates the value of :q: when :q: is made up of numbers and "
0032         "mathematical symbols such as +, -, /, *, ! and ^.");
0033     addSyntax(QStringLiteral(":q:"), description);
0034     addSyntax(QStringLiteral("=:q:"), description);
0035     addSyntax(QStringLiteral(":q:="), description);
0036     addSyntax(QStringLiteral("sqrt(4)"), i18n("Enter a common math function"));
0037 
0038     setMinLetterCount(2);
0039 }
0040 
0041 CalculatorRunner::~CalculatorRunner() = default;
0042 
0043 void CalculatorRunner::userFriendlySubstitutions(QString &cmd)
0044 {
0045     if (QLocale().decimalPoint() != QLatin1Char('.')) {
0046         cmd.replace(QLocale().decimalPoint(), QLatin1String("."), Qt::CaseInsensitive);
0047     } else if (!cmd.contains(QLatin1Char('[')) && !cmd.contains(QLatin1Char(']'))) {
0048         // If we are sure that the user does not want to use vectors we can replace this char
0049         // Especially when switching between locales that use a different decimal separator
0050         // this ensures that the results are valid, see BUG: 406388
0051         cmd.replace(QLatin1Char(','), QLatin1Char('.'), Qt::CaseInsensitive);
0052     }
0053 }
0054 
0055 void CalculatorRunner::match(KRunner::RunnerContext &context)
0056 {
0057     const QString term = context.query();
0058     QString cmd = term;
0059 
0060     // no meanless space between friendly guys: helps simplify code
0061     cmd = std::move(cmd).trimmed();
0062     cmd.remove(QLatin1Char(' '));
0063 
0064     if (cmd.length() < 2) {
0065         return;
0066     }
0067 
0068     if (cmd.compare(QLatin1String("universe"), Qt::CaseInsensitive) == 0 || cmd.compare(QLatin1String("life"), Qt::CaseInsensitive) == 0) {
0069         KRunner::QueryMatch match(this);
0070         match.setCategoryRelevance(KRunner::QueryMatch::CategoryRelevance::Moderate);
0071         match.setIconName(QStringLiteral("accessories-calculator"));
0072         match.setText(QStringLiteral("42"));
0073         match.setData(QStringLiteral("42"));
0074         match.setId(term);
0075         context.addMatch(match);
0076         return;
0077     }
0078 
0079     int base = 10;
0080     QString customBase;
0081 
0082     bool foundPrefix = false; // is there `=` or `hex=` or other base prefix in cmd
0083 
0084     int equalSignPosition = cmd.indexOf(QLatin1Char('='));
0085     if (equalSignPosition != -1 && equalSignPosition != cmd.length() - 1) {
0086         foundPrefix = QalculateEngine::findPrefix(cmd.left(equalSignPosition), &base, &customBase);
0087     }
0088 
0089     const static QRegularExpression hexRegex(QStringLiteral("0x[0-9a-f]+"), QRegularExpression::CaseInsensitiveOption);
0090     const bool parseHex = cmd.contains(hexRegex);
0091     if (!parseHex) {
0092         userFriendlyMultiplication(cmd);
0093     }
0094 
0095     const static QRegularExpression functionName(QStringLiteral("^([a-zA-Z]+)\\(.+\\)"));
0096     if (foundPrefix) {
0097         cmd.remove(0, cmd.indexOf(QLatin1Char('=')) + 1);
0098     } else if (cmd.endsWith(QLatin1Char('='))) {
0099         cmd.chop(1);
0100     } else if (auto match = functionName.match(cmd); match.hasMatch()) { // BUG: 467418
0101         if (!m_engine) {
0102             m_engine = std::make_unique<QalculateEngine>();
0103         }
0104         const QString functionName = match.captured(1);
0105         if (!m_engine->isKnownFunction(functionName)) {
0106             return;
0107         }
0108 
0109     } else if (!parseHex) {
0110         bool foundDigit = false;
0111         for (int i = 0; i < cmd.length(); ++i) {
0112             QChar c = cmd.at(i);
0113             if (c.isLetter() && c != QLatin1Char('!')) {
0114                 // not just numbers and symbols, so we return
0115                 return;
0116             }
0117             if (c.isDigit()) {
0118                 foundDigit = true;
0119             }
0120         }
0121         if (!foundDigit) {
0122             return;
0123         }
0124     }
0125 
0126     if (cmd.isEmpty()) {
0127         return;
0128     }
0129 
0130     userFriendlySubstitutions(cmd);
0131 
0132     bool isApproximate = false;
0133     QString result = calculate(cmd, &isApproximate, base, customBase);
0134     if (!result.isEmpty() && (foundPrefix || result != cmd)) {
0135         KRunner::QueryMatch match(this);
0136         match.setCategoryRelevance(KRunner::QueryMatch::CategoryRelevance::High);
0137         match.setIconName(QStringLiteral("accessories-calculator"));
0138         match.setText(result);
0139         if (isApproximate) {
0140             match.setSubtext(i18nc("The result of the calculation is only an approximation", "Approximation"));
0141         }
0142         match.setData(result);
0143         match.setId(term);
0144         match.setActions(m_actions);
0145         context.addMatch(match);
0146     }
0147 }
0148 
0149 QString CalculatorRunner::calculate(const QString &term, bool *isApproximate, int base, const QString &customBase)
0150 {
0151     {
0152         if (!m_engine) {
0153             m_engine = std::make_unique<QalculateEngine>();
0154         }
0155     }
0156 
0157     QString result;
0158     try {
0159         result = m_engine->evaluate(term, isApproximate, base, customBase);
0160     } catch (std::exception &e) {
0161         qDebug() << "qalculate error: " << e.what();
0162     }
0163 
0164     return result.replace(QLatin1Char('.'), QLocale().decimalPoint(), Qt::CaseInsensitive);
0165 }
0166 
0167 void CalculatorRunner::run(const KRunner::RunnerContext &context, const KRunner::QueryMatch &match)
0168 {
0169     if (match.selectedAction()) {
0170         QApplication::clipboard()->setText(match.text());
0171     } else {
0172         context.requestQueryStringUpdate(match.text(), match.text().length());
0173     }
0174 }
0175 
0176 QMimeData *CalculatorRunner::mimeDataForMatch(const KRunner::QueryMatch &match)
0177 {
0178     QMimeData *result = new QMimeData();
0179     result->setText(match.text());
0180     return result;
0181 }
0182 
0183 void CalculatorRunner::userFriendlyMultiplication(QString &cmd)
0184 {
0185     // convert multiplication sign to *
0186     cmd.replace(QChar(U'\u00D7'), QChar('*'));
0187 
0188     for (int i = 0; i < cmd.length(); ++i) {
0189         if (i == 0 || i == cmd.length() - 1) {
0190             continue;
0191         }
0192         const QChar prev = cmd.at(i - 1);
0193         const QChar current = cmd.at(i);
0194         const QChar next = cmd.at(i + 1);
0195         if (current == QLatin1Char('x')) {
0196             if (prev.isDigit() && (next.isDigit() || next == QLatin1Char(',') || next == QLatin1Char('.'))) {
0197                 cmd[i] = '*';
0198             }
0199         }
0200     }
0201 }
0202 
0203 #include "calculatorrunner.moc"