You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
378 lines
13 KiB
378 lines
13 KiB
/* |
|
* 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> |
|
* Copyright (C) 2021 Alexander Lohnau <alexander.lohnau@gmx.de> |
|
* |
|
* 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 |
|
#include <QClipboard> |
|
#include <QGuiApplication> |
|
#include <QJSEngine> |
|
#endif |
|
|
|
#include <QDebug> |
|
#include <QIcon> |
|
#include <QRegularExpression> |
|
|
|
#include <KLocalizedString> |
|
#include <krunner/querymatch.h> |
|
|
|
K_EXPORT_PLASMA_RUNNER_WITH_JSON(CalculatorRunner, "plasma-runner-calculator.json") |
|
|
|
CalculatorRunner::CalculatorRunner(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args) |
|
: Plasma::AbstractRunner(parent, metaData, args) |
|
{ |
|
#ifdef ENABLE_QALCULATE |
|
m_engine = new QalculateEngine; |
|
#endif |
|
|
|
setObjectName(QStringLiteral("Calculator")); |
|
|
|
QString description = i18n( |
|
"Calculates the value of :q: when :q: is made up of numbers and " |
|
"mathematical symbols such as +, -, /, *, ! and ^."); |
|
addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:"), description)); |
|
addSyntax(Plasma::RunnerSyntax(QStringLiteral("=:q:"), description)); |
|
addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:="), description)); |
|
|
|
addAction(QStringLiteral("copyToClipboard"), QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy to Clipboard")); |
|
setMinLetterCount(2); |
|
} |
|
|
|
CalculatorRunner::~CalculatorRunner() |
|
{ |
|
#ifdef ENABLE_QALCULATE |
|
delete m_engine; |
|
#endif |
|
} |
|
|
|
#ifndef ENABLE_QALCULATE |
|
void CalculatorRunner::powSubstitutions(QString &cmd) |
|
{ |
|
if (cmd.contains(QLatin1String("e+"), Qt::CaseInsensitive)) { |
|
cmd.replace(QLatin1String("e+"), QLatin1String("*10^"), Qt::CaseInsensitive); |
|
} |
|
|
|
if (cmd.contains(QLatin1String("e-"), Qt::CaseInsensitive)) { |
|
cmd.replace(QLatin1String("e-"), QLatin1String("*10^-"), Qt::CaseInsensitive); |
|
} |
|
|
|
// the below code is scary mainly because we have to honor priority |
|
// honor decimal numbers and parenthesis. |
|
while (cmd.contains(QLatin1Char('^'))) { |
|
int where = cmd.indexOf(QLatin1Char('^')); |
|
cmd.replace(where, 1, QLatin1Char(',')); |
|
int preIndex = where - 1; |
|
int postIndex = where + 1; |
|
int count = 0; |
|
|
|
QChar decimalSymbol = QLocale().decimalPoint(); |
|
// avoid out of range on weird commands |
|
preIndex = qMax(0, preIndex); |
|
postIndex = qMin(postIndex, cmd.length() - 1); |
|
|
|
// go backwards looking for the beginning of the number or expression |
|
while (preIndex != 0) { |
|
QChar current = cmd.at(preIndex); |
|
QChar next = cmd.at(preIndex - 1); |
|
// qDebug() << "index " << preIndex << " char " << current; |
|
if (current == QLatin1Char(')')) { |
|
count++; |
|
} else if (current == QLatin1Char('(')) { |
|
count--; |
|
} else { |
|
if (((next <= QLatin1Char('9')) && (next >= QLatin1Char('0'))) || next == decimalSymbol) { |
|
preIndex--; |
|
continue; |
|
} |
|
} |
|
if (count == 0) { |
|
// check for functions |
|
if (!((next <= QLatin1Char('z')) && (next >= QLatin1Char('a')))) { |
|
break; |
|
} |
|
} |
|
preIndex--; |
|
} |
|
|
|
// go forwards looking for the end of the number or expression |
|
count = 0; |
|
while (postIndex != cmd.size() - 1) { |
|
QChar current = cmd.at(postIndex); |
|
QChar next = cmd.at(postIndex + 1); |
|
|
|
// check for functions |
|
if ((count == 0) && (current <= QLatin1Char('z')) && (current >= QLatin1Char('a'))) { |
|
postIndex++; |
|
continue; |
|
} |
|
|
|
if (current == QLatin1Char('(')) { |
|
count++; |
|
} else if (current == QLatin1Char(')')) { |
|
count--; |
|
} else { |
|
if (((next <= QLatin1Char('9')) && (next >= QLatin1Char('0'))) || next == decimalSymbol) { |
|
postIndex++; |
|
continue; |
|
} |
|
} |
|
if (count == 0) { |
|
break; |
|
} |
|
postIndex++; |
|
} |
|
|
|
preIndex = qMax(0, preIndex); |
|
postIndex = qMin(postIndex, cmd.length()); |
|
|
|
cmd.insert(preIndex, QLatin1String("pow(")); |
|
// +1 +4 == next position to the last number after we add 4 new characters pow( |
|
cmd.insert(postIndex + 1 + 4, QLatin1Char(')')); |
|
// qDebug() << "from" << preIndex << " to " << postIndex << " got: " << cmd; |
|
} |
|
} |
|
|
|
void CalculatorRunner::hexSubstitutions(QString &cmd) |
|
{ |
|
if (cmd.contains(QLatin1String("0x"))) { |
|
// Append +0 so that the calculator can serve also as a hex converter |
|
cmd.append(QLatin1String("+0")); |
|
bool ok; |
|
int pos = 0; |
|
QString hex; |
|
|
|
while (cmd.contains(QLatin1String("0x"))) { |
|
hex.clear(); |
|
pos = cmd.indexOf(QLatin1String("0x"), pos); |
|
|
|
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 |
|
hex[q] = current; |
|
} else { |
|
break; |
|
} |
|
} |
|
cmd = cmd.replace(pos, 2 + hex.length(), QString::number(hex.toInt(&ok, 16))); // replace hex with decimal |
|
} |
|
} |
|
} |
|
#endif |
|
|
|
void CalculatorRunner::userFriendlySubstitutions(QString &cmd) |
|
{ |
|
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); |
|
} |
|
|
|
// the following substitutions are not needed with libqalculate |
|
#ifndef ENABLE_QALCULATE |
|
hexSubstitutions(cmd); |
|
powSubstitutions(cmd); |
|
|
|
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 |
|
} |
|
|
|
void CalculatorRunner::match(Plasma::RunnerContext &context) |
|
{ |
|
const QString term = context.query(); |
|
QString cmd = term; |
|
|
|
// no meanless space between friendly guys: helps simplify code |
|
cmd = cmd.trimmed().remove(QLatin1Char(' ')); |
|
|
|
if (cmd.length() < 2) { |
|
return; |
|
} |
|
|
|
if (cmd.toLower() == QLatin1String("universe") || cmd.toLower() == QLatin1String("life")) { |
|
Plasma::QueryMatch match(this); |
|
match.setType(Plasma::QueryMatch::InformationalMatch); |
|
match.setIconName(QStringLiteral("accessories-calculator")); |
|
match.setText(QStringLiteral("42")); |
|
match.setData(QStringLiteral("42")); |
|
match.setId(term); |
|
context.addMatch(match); |
|
return; |
|
} |
|
|
|
bool toHex = cmd.startsWith(QLatin1String("hex=")); |
|
bool startsWithEquals = !toHex && cmd[0] == QLatin1Char('='); |
|
const static QRegularExpression hexRegex(QStringLiteral("0x[0-9a-f]+"), QRegularExpression::CaseInsensitiveOption); |
|
const bool parseHex = cmd.contains(hexRegex); |
|
if (!parseHex) { |
|
userFriendlyMultiplication(cmd); |
|
} |
|
|
|
if (toHex || startsWithEquals) { |
|
cmd.remove(0, cmd.indexOf(QLatin1Char('=')) + 1); |
|
} else if (cmd.endsWith(QLatin1Char('='))) { |
|
cmd.chop(1); |
|
} else if (!parseHex) { |
|
bool foundDigit = false; |
|
for (int i = 0; i < cmd.length(); ++i) { |
|
QChar c = cmd.at(i); |
|
if (c.isLetter() && c != QLatin1Char('!')) { |
|
// not just numbers and symbols, so we return |
|
return; |
|
} |
|
if (c.isDigit()) { |
|
foundDigit = true; |
|
} |
|
} |
|
if (!foundDigit) { |
|
return; |
|
} |
|
} |
|
|
|
if (cmd.isEmpty()) { |
|
return; |
|
} |
|
|
|
userFriendlySubstitutions(cmd); |
|
#ifndef ENABLE_QALCULATE |
|
// needed for accessing math functions like sin(),.... |
|
cmd.replace(QRegularExpression(QStringLiteral("([a-zA-Z]+)")), QStringLiteral("Math.\\1")); |
|
#endif |
|
|
|
bool isApproximate = false; |
|
QString result = calculate(cmd, &isApproximate); |
|
if (!result.isEmpty() && (result != cmd || toHex)) { |
|
if (toHex) { |
|
result = QLatin1String("0x") + QString::number(result.toInt(), 16).toUpper(); |
|
} |
|
|
|
Plasma::QueryMatch match(this); |
|
match.setType(Plasma::QueryMatch::InformationalMatch); |
|
match.setIconName(QStringLiteral("accessories-calculator")); |
|
match.setText(result); |
|
if (isApproximate) { |
|
match.setSubtext(i18nc("The result of the calculation is only an approximation", "Approximation")); |
|
} |
|
match.setData(result); |
|
match.setId(term); |
|
match.setActions(actions().values()); |
|
context.addMatch(match); |
|
} |
|
} |
|
|
|
QString CalculatorRunner::calculate(const QString &term, bool *isApproximate) |
|
{ |
|
#ifdef ENABLE_QALCULATE |
|
QString result; |
|
|
|
try { |
|
result = m_engine->evaluate(term, isApproximate); |
|
} catch (std::exception &e) { |
|
qDebug() << "qalculate error: " << e.what(); |
|
} |
|
|
|
return result.replace(QLatin1Char('.'), QLocale().decimalPoint(), Qt::CaseInsensitive); |
|
#else |
|
Q_UNUSED(isApproximate); |
|
// qDebug() << "calculating" << term; |
|
QJSEngine eng; |
|
QJSValue result = eng.evaluate(QStringLiteral("var result = %1; result").arg(term)); |
|
|
|
if (result.isError()) { |
|
return QString(); |
|
} |
|
|
|
const QString resultString = result.toString(); |
|
if (resultString.isEmpty()) { |
|
return QString(); |
|
} |
|
|
|
if (!resultString.contains(QLatin1Char('.'))) { |
|
return resultString; |
|
} |
|
|
|
// ECMAScript has issues with the last digit in simple rational computations |
|
// This script rounds off the last digit; see bug 167986 |
|
QString roundedResultString = eng.evaluate(QStringLiteral("var exponent = 14-(1+Math.floor(Math.log(Math.abs(result))/Math.log(10)));\ |
|
var order=Math.pow(10,exponent);\ |
|
(order > 0? Math.round(result*order)/order : 0)")) |
|
.toString(); |
|
|
|
roundedResultString.replace(QLatin1Char('.'), QLocale().decimalPoint(), Qt::CaseInsensitive); |
|
|
|
return roundedResultString; |
|
#endif |
|
} |
|
|
|
void CalculatorRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match) |
|
{ |
|
Q_UNUSED(context) |
|
if (match.selectedAction()) { |
|
#ifdef ENABLE_QALCULATE |
|
m_engine->copyToClipboard(); |
|
#else |
|
QGuiApplication::clipboard()->setText(match.text()); |
|
#endif |
|
} |
|
} |
|
|
|
QMimeData *CalculatorRunner::mimeDataForMatch(const Plasma::QueryMatch &match) |
|
{ |
|
// qDebug(); |
|
QMimeData *result = new QMimeData(); |
|
result->setText(match.text()); |
|
return result; |
|
} |
|
|
|
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] = '*'; |
|
} |
|
} |
|
} |
|
} |
|
|
|
#include "calculatorrunner.moc"
|
|
|