calculatorrunner.cpp 12.7 KB
Newer Older
1
2
3
4
5
/*
 *   Copyright (C) 2007 Barış Metin <baris@pardus.org.tr>
 *   Copyright (C) 2006 David Faure <faure@kde.org>
 *   Copyright (C) 2007 Richard Moore <rich@kde.org>
 *   Copyright (C) 2010 Matteo Agostinelli <agostinelli@gmail.com>
6
 *   Copyright (C) 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License version 2 as
 *   published by the Free Software Foundation
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the
 *   Free Software Foundation, Inc.,
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include "calculatorrunner.h"

#ifdef ENABLE_QALCULATE
#include "qalculate_engine.h"
#else
28
#include <QClipboard>
Alexander Lohnau's avatar
Alexander Lohnau committed
29
30
#include <QGuiApplication>
#include <QJSEngine>
31
32
33
#endif

#include <QDebug>
Alexander Lohnau's avatar
Alexander Lohnau committed
34
35
#include <QIcon>
#include <QRegularExpression>
36

37
#include <KLocalizedString>
38
39
#include <krunner/querymatch.h>

40
K_EXPORT_PLASMA_RUNNER_WITH_JSON(CalculatorRunner, "plasma-runner-calculator.json")
41

42
43
CalculatorRunner::CalculatorRunner(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args)
    : Plasma::AbstractRunner(parent, metaData, args)
44
{
Alexander Lohnau's avatar
Alexander Lohnau committed
45
#ifdef ENABLE_QALCULATE
46
    m_engine = new QalculateEngine;
Alexander Lohnau's avatar
Alexander Lohnau committed
47
#endif
48

Alexander Lohnau's avatar
Alexander Lohnau committed
49
    setObjectName(QStringLiteral("Calculator"));
50

Alexander Lohnau's avatar
Alexander Lohnau committed
51
52
53
    QString description = i18n(
        "Calculates the value of :q: when :q: is made up of numbers and "
        "mathematical symbols such as +, -, /, *, ! and ^.");
54
55
56
    addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:"), description));
    addSyntax(Plasma::RunnerSyntax(QStringLiteral("=:q:"), description));
    addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:="), description));
57

Alexander Lohnau's avatar
Alexander Lohnau committed
58
    addAction(QStringLiteral("copyToClipboard"), QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy to Clipboard"));
59
    setMinLetterCount(2);
60
61
62
63
}

CalculatorRunner::~CalculatorRunner()
{
Alexander Lohnau's avatar
Alexander Lohnau committed
64
#ifdef ENABLE_QALCULATE
65
    delete m_engine;
Alexander Lohnau's avatar
Alexander Lohnau committed
66
#endif
67
68
}

69
#ifndef ENABLE_QALCULATE
Alexander Lohnau's avatar
Alexander Lohnau committed
70
void CalculatorRunner::powSubstitutions(QString &cmd)
71
{
Laurent Montel's avatar
Laurent Montel committed
72
73
    if (cmd.contains(QLatin1String("e+"), Qt::CaseInsensitive)) {
        cmd.replace(QLatin1String("e+"), QLatin1String("*10^"), Qt::CaseInsensitive);
74
75
    }

Laurent Montel's avatar
Laurent Montel committed
76
77
    if (cmd.contains(QLatin1String("e-"), Qt::CaseInsensitive)) {
        cmd.replace(QLatin1String("e-"), QLatin1String("*10^-"), Qt::CaseInsensitive);
78
79
80
81
    }

    // the below code is scary mainly because we have to honor priority
    // honor decimal numbers and parenthesis.
82
83
    while (cmd.contains(QLatin1Char('^'))) {
        int where = cmd.indexOf(QLatin1Char('^'));
Laurent Montel's avatar
Laurent Montel committed
84
        cmd.replace(where, 1, QLatin1Char(','));
85
86
87
88
        int preIndex = where - 1;
        int postIndex = where + 1;
        int count = 0;

89
        QChar decimalSymbol = QLocale().decimalPoint();
Alexander Lohnau's avatar
Alexander Lohnau committed
90
        // avoid out of range on weird commands
91
        preIndex = qMax(0, preIndex);
Alexander Lohnau's avatar
Alexander Lohnau committed
92
        postIndex = qMin(postIndex, cmd.length() - 1);
93

Alexander Lohnau's avatar
Alexander Lohnau committed
94
        // go backwards looking for the beginning of the number or expression
95
96
        while (preIndex != 0) {
            QChar current = cmd.at(preIndex);
Alexander Lohnau's avatar
Alexander Lohnau committed
97
98
            QChar next = cmd.at(preIndex - 1);
            // qDebug() << "index " << preIndex << " char " << current;
99
            if (current == QLatin1Char(')')) {
100
                count++;
101
            } else if (current == QLatin1Char('(')) {
102
103
                count--;
            } else {
Alexander Lohnau's avatar
Alexander Lohnau committed
104
                if (((next <= QLatin1Char('9')) && (next >= QLatin1Char('0'))) || next == decimalSymbol) {
105
106
107
108
109
                    preIndex--;
                    continue;
                }
            }
            if (count == 0) {
Alexander Lohnau's avatar
Alexander Lohnau committed
110
111
                // check for functions
                if (!((next <= QLatin1Char('z')) && (next >= QLatin1Char('a')))) {
112
113
114
115
116
117
                    break;
                }
            }
            preIndex--;
        }

Alexander Lohnau's avatar
Alexander Lohnau committed
118
        // go forwards looking for the end of the number or expression
119
120
        count = 0;
        while (postIndex != cmd.size() - 1) {
Alexander Lohnau's avatar
Alexander Lohnau committed
121
122
            QChar current = cmd.at(postIndex);
            QChar next = cmd.at(postIndex + 1);
123

Alexander Lohnau's avatar
Alexander Lohnau committed
124
            // check for functions
125
            if ((count == 0) && (current <= QLatin1Char('z')) && (current >= QLatin1Char('a'))) {
126
127
128
129
                postIndex++;
                continue;
            }

130
            if (current == QLatin1Char('(')) {
131
                count++;
132
            } else if (current == QLatin1Char(')')) {
133
134
                count--;
            } else {
Alexander Lohnau's avatar
Alexander Lohnau committed
135
                if (((next <= QLatin1Char('9')) && (next >= QLatin1Char('0'))) || next == decimalSymbol) {
136
137
                    postIndex++;
                    continue;
Alexander Lohnau's avatar
Alexander Lohnau committed
138
                }
139
140
141
142
143
144
145
146
147
148
            }
            if (count == 0) {
                break;
            }
            postIndex++;
        }

        preIndex = qMax(0, preIndex);
        postIndex = qMin(postIndex, cmd.length());

Alexander Lohnau's avatar
Alexander Lohnau committed
149
        cmd.insert(preIndex, QLatin1String("pow("));
150
        // +1 +4 == next position to the last number after we add 4 new characters pow(
151
        cmd.insert(postIndex + 1 + 4, QLatin1Char(')'));
Alexander Lohnau's avatar
Alexander Lohnau committed
152
        // qDebug() << "from" << preIndex << " to " << postIndex << " got: " << cmd;
153
154
155
    }
}

Alexander Lohnau's avatar
Alexander Lohnau committed
156
void CalculatorRunner::hexSubstitutions(QString &cmd)
157
{
Laurent Montel's avatar
Laurent Montel committed
158
    if (cmd.contains(QLatin1String("0x"))) {
Alexander Lohnau's avatar
Alexander Lohnau committed
159
        // Append +0 so that the calculator can serve also as a hex converter
Laurent Montel's avatar
Laurent Montel committed
160
        cmd.append(QLatin1String("+0"));
161
162
163
164
        bool ok;
        int pos = 0;
        QString hex;

Laurent Montel's avatar
Laurent Montel committed
165
        while (cmd.contains(QLatin1String("0x"))) {
166
            hex.clear();
Laurent Montel's avatar
Laurent Montel committed
167
            pos = cmd.indexOf(QLatin1String("0x"), pos);
168

Alexander Lohnau's avatar
Alexander Lohnau committed
169
170
171
172
            for (int q = 0; q < cmd.size(); q++) { // find end of hex number
                QChar current = cmd[pos + q + 2];
                if (((current <= QLatin1Char('9')) && (current >= QLatin1Char('0'))) || ((current <= QLatin1Char('F')) && (current >= QLatin1Char('A')))
                    || ((current <= QLatin1Char('f')) && (current >= QLatin1Char('a')))) { // Check if valid hex sign
173
174
175
176
177
                    hex[q] = current;
                } else {
                    break;
                }
            }
Alexander Lohnau's avatar
Alexander Lohnau committed
178
            cmd = cmd.replace(pos, 2 + hex.length(), QString::number(hex.toInt(&ok, 16))); // replace hex with decimal
179
180
181
        }
    }
}
182
#endif
183

Alexander Lohnau's avatar
Alexander Lohnau committed
184
void CalculatorRunner::userFriendlySubstitutions(QString &cmd)
185
{
186
187
188
189
190
191
192
193
    if (QLocale().decimalPoint() != QLatin1Char('.')) {
        cmd.replace(QLocale().decimalPoint(), QLatin1Char('.'), Qt::CaseInsensitive);
    } else if (!cmd.contains(QLatin1Char('[')) && !cmd.contains(QLatin1Char(']'))) {
        // If we are sure that the user does not want to use vectors we can replace this char
        // Especially when switching between locales that use a different decimal separator
        // this ensures that the results are valid, see BUG: 406388
        cmd.replace(QLatin1Char(','), QLatin1Char('.'), Qt::CaseInsensitive);
    }
194
195

    // the following substitutions are not needed with libqalculate
196
#ifndef ENABLE_QALCULATE
197
198
199
    hexSubstitutions(cmd);
    powSubstitutions(cmd);

200
201
202
203
204
205
206
207
208
    QRegularExpression re(QStringLiteral("(\\d+)and(\\d+)"));
    cmd.replace(re, QStringLiteral("\\1&\\2"));

    re.setPattern(QStringLiteral("(\\d+)or(\\d+)"));
    cmd.replace(re, QStringLiteral("\\1|\\2"));

    re.setPattern(QStringLiteral("(\\d+)xor(\\d+)"));
    cmd.replace(re, QStringLiteral("\\1^\\2"));
#endif
209
210
211
212
213
214
215
}

void CalculatorRunner::match(Plasma::RunnerContext &context)
{
    const QString term = context.query();
    QString cmd = term;

Alexander Lohnau's avatar
Alexander Lohnau committed
216
    // no meanless space between friendly guys: helps simplify code
217
    cmd = cmd.trimmed().remove(QLatin1Char(' '));
218

219
    if (cmd.length() < 2) {
220
221
222
        return;
    }

223
    if (cmd.toLower() == QLatin1String("universe") || cmd.toLower() == QLatin1String("life")) {
224
225
        Plasma::QueryMatch match(this);
        match.setType(Plasma::QueryMatch::InformationalMatch);
226
        match.setIconName(QStringLiteral("accessories-calculator"));
227
        match.setText(QStringLiteral("42"));
228
        match.setData(QStringLiteral("42"));
229
        match.setId(term);
230
        context.addMatch(match);
231
232
233
234
        return;
    }

    bool toHex = cmd.startsWith(QLatin1String("hex="));
235
    bool startsWithEquals = !toHex && cmd[0] == QLatin1Char('=');
236
    const static QRegularExpression hexRegex(QStringLiteral("0x[0-9a-f]+"), QRegularExpression::CaseInsensitiveOption);
237
238
239
240
    const bool parseHex = cmd.contains(hexRegex);
    if (!parseHex) {
        userFriendlyMultiplication(cmd);
    }
241

242
    if (toHex || startsWithEquals) {
243
244
        cmd.remove(0, cmd.indexOf(QLatin1Char('=')) + 1);
    } else if (cmd.endsWith(QLatin1Char('='))) {
245
        cmd.chop(1);
246
    } else if (!parseHex) {
247
248
249
        bool foundDigit = false;
        for (int i = 0; i < cmd.length(); ++i) {
            QChar c = cmd.at(i);
250
            if (c.isLetter() && c != QLatin1Char('!')) {
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
                // not just numbers and symbols, so we return
                return;
            }
            if (c.isDigit()) {
                foundDigit = true;
            }
        }
        if (!foundDigit) {
            return;
        }
    }

    if (cmd.isEmpty()) {
        return;
    }

    userFriendlySubstitutions(cmd);
Alexander Lohnau's avatar
Alexander Lohnau committed
268
269
#ifndef ENABLE_QALCULATE
    // needed for accessing math functions like sin(),....
270
    cmd.replace(QRegularExpression(QStringLiteral("([a-zA-Z]+)")), QStringLiteral("Math.\\1"));
Alexander Lohnau's avatar
Alexander Lohnau committed
271
#endif
272

273
274
    bool isApproximate = false;
    QString result = calculate(cmd, &isApproximate);
275
    if (!result.isEmpty() && (result != cmd || toHex)) {
276
        if (toHex) {
Laurent Montel's avatar
Laurent Montel committed
277
            result = QLatin1String("0x") + QString::number(result.toInt(), 16).toUpper();
278
279
280
281
        }

        Plasma::QueryMatch match(this);
        match.setType(Plasma::QueryMatch::InformationalMatch);
282
        match.setIconName(QStringLiteral("accessories-calculator"));
283
        match.setText(result);
284
285
286
        if (isApproximate) {
            match.setSubtext(i18nc("The result of the calculation is only an approximation", "Approximation"));
        }
287
288
        match.setData(result);
        match.setId(term);
289
        match.setActions(actions().values());
290
        context.addMatch(match);
291
292
293
    }
}

Alexander Lohnau's avatar
Alexander Lohnau committed
294
QString CalculatorRunner::calculate(const QString &term, bool *isApproximate)
295
{
Alexander Lohnau's avatar
Alexander Lohnau committed
296
#ifdef ENABLE_QALCULATE
297
298
299
    QString result;

    try {
300
        result = m_engine->evaluate(term, isApproximate);
Alexander Lohnau's avatar
Alexander Lohnau committed
301
    } catch (std::exception &e) {
302
303
304
        qDebug() << "qalculate error: " << e.what();
    }

305
    return result.replace(QLatin1Char('.'), QLocale().decimalPoint(), Qt::CaseInsensitive);
Alexander Lohnau's avatar
Alexander Lohnau committed
306
#else
307
    Q_UNUSED(isApproximate);
Alexander Lohnau's avatar
Alexander Lohnau committed
308
    // qDebug() << "calculating" << term;
309
310
    QJSEngine eng;
    QJSValue result = eng.evaluate(QStringLiteral("var result = %1; result").arg(term));
311
312
313
314
315
316
317
318
319
320

    if (result.isError()) {
        return QString();
    }

    const QString resultString = result.toString();
    if (resultString.isEmpty()) {
        return QString();
    }

321
    if (!resultString.contains(QLatin1Char('.'))) {
322
323
324
        return resultString;
    }

Alexander Lohnau's avatar
Alexander Lohnau committed
325
326
    // ECMAScript has issues with the last digit in simple rational computations
    // This script rounds off the last digit; see bug 167986
327
    QString roundedResultString = eng.evaluate(QStringLiteral("var exponent = 14-(1+Math.floor(Math.log(Math.abs(result))/Math.log(10)));\
328
                                                var order=Math.pow(10,exponent);\
Alexander Lohnau's avatar
Alexander Lohnau committed
329
330
                                                (order > 0? Math.round(result*order)/order : 0)"))
                                      .toString();
331

332
    roundedResultString.replace(QLatin1Char('.'), QLocale().decimalPoint(), Qt::CaseInsensitive);
333
334

    return roundedResultString;
Alexander Lohnau's avatar
Alexander Lohnau committed
335
#endif
336
337
}

338
339
void CalculatorRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match)
{
Alexander Lohnau's avatar
Alexander Lohnau committed
340
341
    Q_UNUSED(context)
    if (match.selectedAction()) {
342
#ifdef ENABLE_QALCULATE
343
        m_engine->copyToClipboard();
344
#else
345
        QGuiApplication::clipboard()->setText(match.text());
346
#endif
347
    }
348
349
}

Alexander Lohnau's avatar
Alexander Lohnau committed
350
QMimeData *CalculatorRunner::mimeDataForMatch(const Plasma::QueryMatch &match)
351
{
Alexander Lohnau's avatar
Alexander Lohnau committed
352
    // qDebug();
353
    QMimeData *result = new QMimeData();
354
    result->setText(match.text());
355
356
357
    return result;
}

358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
void CalculatorRunner::userFriendlyMultiplication(QString &cmd)
{
    // convert multiplication sign to *
    cmd.replace(QChar(U'\u00D7'), QChar('*'));

    for (int i = 0; i < cmd.length(); ++i) {
        if (i == 0 || i == cmd.length() - 1) {
            continue;
        }
        const QChar prev = cmd.at(i - 1);
        const QChar current = cmd.at(i);
        const QChar next = cmd.at(i + 1);
        if (current == QLatin1Char('x')) {
            if (prev.isDigit() && (next.isDigit() || next == QLatin1Char(',') || next == QLatin1Char('.'))) {
                cmd[i] = '*';
            }
        }
    }
}

378
#include "calculatorrunner.moc"