File indexing completed on 2024-05-05 17:45:01

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 <QDebug>
0016 #include <QIcon>
0017 #include <QMutex>
0018 #include <QRegularExpression>
0019 
0020 #include <KLocalizedString>
0021 #include <krunner/querymatch.h>
0022 
0023 K_PLUGIN_CLASS_WITH_JSON(CalculatorRunner, "plasma-runner-calculator.json")
0024 
0025 static QMutex s_initMutex;
0026 
0027 CalculatorRunner::CalculatorRunner(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args)
0028     : Plasma::AbstractRunner(parent, metaData, args)
0029 {
0030     setObjectName(QStringLiteral("Calculator"));
0031 
0032     QString description = i18n(
0033         "Calculates the value of :q: when :q: is made up of numbers and "
0034         "mathematical symbols such as +, -, /, *, ! and ^.");
0035     addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:"), description));
0036     addSyntax(Plasma::RunnerSyntax(QStringLiteral("=:q:"), description));
0037     addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:="), description));
0038 
0039     m_actions = {new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy to Clipboard"), this)};
0040     setMinLetterCount(2);
0041 }
0042 
0043 CalculatorRunner::~CalculatorRunner()
0044 {
0045 }
0046 
0047 void CalculatorRunner::userFriendlySubstitutions(QString &cmd)
0048 {
0049     if (QLocale().decimalPoint() != QLatin1Char('.')) {
0050         cmd.replace(QLocale().decimalPoint(), QLatin1String("."), Qt::CaseInsensitive);
0051     } else if (!cmd.contains(QLatin1Char('[')) && !cmd.contains(QLatin1Char(']'))) {
0052         // If we are sure that the user does not want to use vectors we can replace this char
0053         // Especially when switching between locales that use a different decimal separator
0054         // this ensures that the results are valid, see BUG: 406388
0055         cmd.replace(QLatin1Char(','), QLatin1Char('.'), Qt::CaseInsensitive);
0056     }
0057 }
0058 
0059 void CalculatorRunner::match(Plasma::RunnerContext &context)
0060 {
0061     const QString term = context.query();
0062     QString cmd = term;
0063 
0064     // no meanless space between friendly guys: helps simplify code
0065     cmd = cmd.trimmed().remove(QLatin1Char(' '));
0066 
0067     if (cmd.length() < 2) {
0068         return;
0069     }
0070 
0071     if (cmd.toLower() == QLatin1String("universe") || cmd.toLower() == QLatin1String("life")) {
0072         Plasma::QueryMatch match(this);
0073         match.setType(Plasma::QueryMatch::PossibleMatch);
0074         match.setIconName(QStringLiteral("accessories-calculator"));
0075         match.setText(QStringLiteral("42"));
0076         match.setData(QStringLiteral("42"));
0077         match.setId(term);
0078         context.addMatch(match);
0079         return;
0080     }
0081 
0082     int base = 10;
0083     QString customBase;
0084 
0085     bool foundPrefix = false; // is there `=` or `hex=` or other base prefix in cmd
0086 
0087     int equalSignPosition = cmd.indexOf(QLatin1Char('='));
0088     if (equalSignPosition != -1 && equalSignPosition != cmd.length() - 1) {
0089         foundPrefix = QalculateEngine::findPrefix(cmd.left(equalSignPosition), &base, &customBase);
0090     }
0091 
0092     const static QRegularExpression hexRegex(QStringLiteral("0x[0-9a-f]+"), QRegularExpression::CaseInsensitiveOption);
0093     const bool parseHex = cmd.contains(hexRegex);
0094     if (!parseHex) {
0095         userFriendlyMultiplication(cmd);
0096     }
0097 
0098     if (foundPrefix) {
0099         cmd.remove(0, cmd.indexOf(QLatin1Char('=')) + 1);
0100     } else if (cmd.endsWith(QLatin1Char('='))) {
0101         cmd.chop(1);
0102     } else if (!parseHex) {
0103         bool foundDigit = false;
0104         for (int i = 0; i < cmd.length(); ++i) {
0105             QChar c = cmd.at(i);
0106             if (c.isLetter() && c != QLatin1Char('!')) {
0107                 // not just numbers and symbols, so we return
0108                 return;
0109             }
0110             if (c.isDigit()) {
0111                 foundDigit = true;
0112             }
0113         }
0114         if (!foundDigit) {
0115             return;
0116         }
0117     }
0118 
0119     if (cmd.isEmpty()) {
0120         return;
0121     }
0122 
0123     userFriendlySubstitutions(cmd);
0124 
0125     bool isApproximate = false;
0126     QString result = calculate(cmd, &isApproximate, base, customBase);
0127     if (!result.isEmpty() && (foundPrefix || result != cmd)) {
0128         Plasma::QueryMatch match(this);
0129         match.setType(Plasma::QueryMatch::HelperMatch);
0130         match.setIconName(QStringLiteral("accessories-calculator"));
0131         match.setText(result);
0132         if (isApproximate) {
0133             match.setSubtext(i18nc("The result of the calculation is only an approximation", "Approximation"));
0134         }
0135         match.setData(result);
0136         match.setId(term);
0137         match.setActions(m_actions);
0138         context.addMatch(match);
0139     }
0140 }
0141 
0142 QString CalculatorRunner::calculate(const QString &term, bool *isApproximate, int base, const QString &customBase)
0143 {
0144     {
0145         QMutexLocker lock(&s_initMutex);
0146         if (!m_engine) {
0147             m_engine = std::make_unique<QalculateEngine>();
0148         }
0149     }
0150 
0151     QString result;
0152     try {
0153         result = m_engine->evaluate(term, isApproximate, base, customBase);
0154     } catch (std::exception &e) {
0155         qDebug() << "qalculate error: " << e.what();
0156     }
0157 
0158     return result.replace(QLatin1Char('.'), QLocale().decimalPoint(), Qt::CaseInsensitive);
0159 }
0160 
0161 void CalculatorRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match)
0162 {
0163     if (match.selectedAction()) {
0164         m_engine->copyToClipboard();
0165     } else {
0166         context.requestQueryStringUpdate(match.text(), match.text().length());
0167     }
0168 }
0169 
0170 QMimeData *CalculatorRunner::mimeDataForMatch(const Plasma::QueryMatch &match)
0171 {
0172     QMimeData *result = new QMimeData();
0173     result->setText(match.text());
0174     return result;
0175 }
0176 
0177 void CalculatorRunner::userFriendlyMultiplication(QString &cmd)
0178 {
0179     // convert multiplication sign to *
0180     cmd.replace(QChar(U'\u00D7'), QChar('*'));
0181 
0182     for (int i = 0; i < cmd.length(); ++i) {
0183         if (i == 0 || i == cmd.length() - 1) {
0184             continue;
0185         }
0186         const QChar prev = cmd.at(i - 1);
0187         const QChar current = cmd.at(i);
0188         const QChar next = cmd.at(i + 1);
0189         if (current == QLatin1Char('x')) {
0190             if (prev.isDigit() && (next.isDigit() || next == QLatin1Char(',') || next == QLatin1Char('.'))) {
0191                 cmd[i] = '*';
0192             }
0193         }
0194     }
0195 }
0196 
0197 #include "calculatorrunner.moc"