File indexing completed on 2024-05-12 05:38:19

0001 /*
0002     SPDX-FileCopyrightText: 2010 Matteo Agostinelli <agostinelli@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "qalculate_engine.h"
0008 
0009 #include <libqalculate/Calculator.h>
0010 #include <libqalculate/ExpressionItem.h>
0011 #include <libqalculate/Function.h>
0012 #include <libqalculate/Prefix.h>
0013 #include <libqalculate/Unit.h>
0014 #include <libqalculate/Variable.h>
0015 
0016 #include <QDebug>
0017 #include <QFile>
0018 #include <QMutex>
0019 #include <QUrl>
0020 
0021 #include <KIO/FileCopyJob>
0022 #include <KLocalizedString>
0023 
0024 constexpr int evaluationTimeout = 10000;
0025 
0026 // Synchronization lock that ensures that
0027 // a) only one evaluation is running at a time
0028 // b) abortion and preemption of evaluation is synchronized
0029 class QalculateLock
0030 {
0031 public:
0032     QalculateLock()
0033     {
0034         QMutexLocker ctrlLocker(&s_ctrlLock);
0035         CALCULATOR->abort();
0036         s_evalLock.lock();
0037         CALCULATOR->startControl(evaluationTimeout);
0038     }
0039 
0040     ~QalculateLock()
0041     {
0042         CALCULATOR->stopControl();
0043         s_evalLock.unlock();
0044     }
0045 
0046 private:
0047     static QMutex s_ctrlLock;
0048     static QMutex s_evalLock;
0049 };
0050 
0051 QMutex QalculateLock::s_ctrlLock;
0052 QMutex QalculateLock::s_evalLock;
0053 
0054 // This mutex protects construction and destruction of the global calculator
0055 // instance CALCULATOR
0056 QMutex s_initMutex;
0057 QAtomicInt QalculateEngine::s_counter;
0058 
0059 QalculateEngine::QalculateEngine(QObject *parent)
0060     : QObject(parent)
0061 {
0062     QMutexLocker lock(&s_initMutex);
0063     s_counter.ref();
0064     if (!CALCULATOR) {
0065         new Calculator();
0066         CALCULATOR->terminateThreads();
0067         CALCULATOR->loadGlobalDefinitions();
0068         CALCULATOR->loadLocalDefinitions();
0069         CALCULATOR->loadGlobalCurrencies();
0070         CALCULATOR->loadExchangeRates();
0071     }
0072 }
0073 
0074 QalculateEngine::~QalculateEngine()
0075 {
0076     QMutexLocker lock(&s_initMutex);
0077     if (s_counter.deref()) {
0078         delete CALCULATOR;
0079         CALCULATOR = nullptr;
0080     }
0081 }
0082 
0083 void QalculateEngine::updateExchangeRates()
0084 {
0085     QUrl source = QUrl("http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml");
0086     QUrl dest = QUrl::fromLocalFile(QFile::decodeName(CALCULATOR->getExchangeRatesFileName().c_str()));
0087 
0088     KIO::Job *getJob = KIO::file_copy(source, dest, -1, KIO::Overwrite | KIO::HideProgressInfo);
0089     connect(getJob, &KJob::result, this, &QalculateEngine::updateResult);
0090 }
0091 
0092 void QalculateEngine::updateResult(KJob *job)
0093 {
0094     if (job->error()) {
0095         qDebug() << "The exchange rates could not be updated. The following error has been reported:" << job->errorString();
0096     } else {
0097         // the exchange rates have been successfully updated, now load them
0098         CALCULATOR->loadExchangeRates();
0099     }
0100 }
0101 
0102 #if QALCULATE_MAJOR_VERSION > 2 || QALCULATE_MINOR_VERSION > 6
0103 bool has_error()
0104 {
0105     while (CALCULATOR->message()) {
0106         if (CALCULATOR->message()->type() == MESSAGE_ERROR) {
0107             CALCULATOR->clearMessages();
0108             return true;
0109         }
0110         CALCULATOR->nextMessage();
0111     }
0112     return false;
0113 }
0114 
0115 bool check_valid_before(const std::string &expression, const EvaluationOptions &search_eo)
0116 {
0117     bool b_valid = false;
0118     if (!b_valid)
0119         b_valid = (expression.find_first_of(OPERATORS NUMBERS PARENTHESISS) != std::string::npos);
0120     if (!b_valid)
0121         b_valid = CALCULATOR->hasToExpression(expression, false, search_eo);
0122     if (!b_valid) {
0123         std::string str = expression;
0124         CALCULATOR->parseSigns(str);
0125         b_valid = (str.find_first_of(OPERATORS NUMBERS PARENTHESISS) != std::string::npos);
0126         if (!b_valid) {
0127             size_t i = str.find_first_of(SPACES);
0128             MathStructure m;
0129             if (!b_valid) {
0130                 CALCULATOR->parse(&m, str, search_eo.parse_options);
0131                 if (!has_error() && (m.isUnit() || m.isFunction() || (m.isVariable() && (i != std::string::npos || m.variable()->isKnown()))))
0132                     b_valid = true;
0133             }
0134         }
0135     }
0136     return b_valid;
0137 }
0138 #endif
0139 
0140 QString QalculateEngine::evaluate(const QString &expression, bool *isApproximate, int base, const QString &customBase)
0141 {
0142     if (expression.isEmpty()) {
0143         return QString();
0144     }
0145 
0146     QString input = expression;
0147     // Make sure to use toLocal8Bit, the expression can contain non-latin1 characters
0148     QByteArray ba = input.replace(QChar(0xA3), "GBP").replace(QChar(0xA5), "JPY").replace('$', "USD").replace(QChar(0x20AC), "EUR").toLocal8Bit();
0149     const char *ctext = ba.data();
0150 
0151     QalculateLock qalculateLock;
0152 
0153     EvaluationOptions eo;
0154 
0155     eo.auto_post_conversion = POST_CONVERSION_BEST;
0156     eo.keep_zero_units = false;
0157 
0158     eo.parse_options.angle_unit = ANGLE_UNIT_RADIANS;
0159     eo.structuring = STRUCTURING_SIMPLIFY;
0160 
0161     // suggested in https://github.com/Qalculate/libqalculate/issues/16
0162     // to avoid memory overflow for seemingly innocent calculations (Bug 277011)
0163     eo.approximation = APPROXIMATION_APPROXIMATE;
0164 
0165 #if QALCULATE_MAJOR_VERSION > 2 || QALCULATE_MINOR_VERSION > 6
0166     if (!check_valid_before(expression.toStdString(), eo)) {
0167         return QString(); // See https://github.com/Qalculate/libqalculate/issues/442
0168     }
0169 #endif
0170 
0171     CALCULATOR->setPrecision(16);
0172 #ifdef BASE_CUSTOM // v3.3.0 has setCustomOutputBase
0173     if (base == BASE_CUSTOM) {
0174         EvaluationOptions eo;
0175         eo.parse_options.base = 10;
0176         eo.approximation = APPROXIMATION_TRY_EXACT;
0177 
0178         MathStructure m = CALCULATOR->calculate(customBase.toStdString(), eo);
0179 
0180         if (m.isNumber() && (m.number().isPositive() || m.number().isInteger()) && (m.number().isGreaterThan(1) || m.number().isLessThan(-1))) {
0181             CALCULATOR->setCustomOutputBase(m.number());
0182         } else {
0183             base = BASE_DECIMAL;
0184         }
0185     }
0186 #endif
0187 
0188     MathStructure result = CALCULATOR->calculate(ctext, eo);
0189 
0190     PrintOptions po;
0191     po.base = base;
0192     po.number_fraction_format = FRACTION_DECIMAL;
0193     po.indicate_infinite_series = false;
0194     po.use_all_prefixes = false;
0195     po.use_denominator_prefix = true;
0196     po.negative_exponents = false;
0197     po.lower_case_e = true;
0198     po.base_display = BASE_DISPLAY_NORMAL;
0199 #if defined(QALCULATE_MAJOR_VERSION) && defined(QALCULATE_MINOR_VERSION)                                                                                       \
0200     && (QALCULATE_MAJOR_VERSION > 2 || (QALCULATE_MAJOR_VERSION == 2 && QALCULATE_MINOR_VERSION >= 2))
0201     po.interval_display = INTERVAL_DISPLAY_SIGNIFICANT_DIGITS;
0202 #endif
0203 
0204     result.format(po);
0205 
0206     m_lastResult = QString::fromStdString(result.print(po));
0207 
0208     if (isApproximate) {
0209         *isApproximate = result.isApproximate();
0210     }
0211 
0212     return m_lastResult;
0213 }
0214 
0215 static const QMap<QString, int> s_commonBaseMappings = {
0216     {QStringLiteral("roman"), BASE_ROMAN_NUMERALS},
0217     {QStringLiteral("time"), BASE_TIME},
0218 
0219     {QStringLiteral("bin"), BASE_BINARY},
0220     {QStringLiteral("oct"), BASE_OCTAL},
0221     {QStringLiteral("dec"), BASE_DECIMAL},
0222     {QStringLiteral("duo"), 12},
0223     {QStringLiteral("hex"), BASE_HEXADECIMAL},
0224     {QStringLiteral("sexa"), BASE_SEXAGESIMAL},
0225 
0226 #ifdef BASE_CUSTOM // v3.3.0
0227     {QStringLiteral("pi"), BASE_PI},
0228     {QStringLiteral("e"), BASE_E},
0229     {QStringLiteral("sqrt2"), BASE_SQRT2},
0230 
0231     {QStringLiteral("golden"), BASE_GOLDEN_RATIO},
0232     {QStringLiteral("supergolden"), BASE_SUPER_GOLDEN_RATIO},
0233 
0234     {QStringLiteral("unicode"), BASE_UNICODE},
0235 #endif // v3.3.0
0236 
0237 #ifdef BASE_BIJECTIVE_26 // v3.5.0
0238     {QStringLiteral("bijective"), BASE_BIJECTIVE_26},
0239     {QStringLiteral("b26"), BASE_BIJECTIVE_26},
0240 #endif // v3.5.0
0241 
0242 #ifdef BASE_FP16 // v3.8.0
0243     {QStringLiteral("fp16"), BASE_FP16},
0244     {QStringLiteral("fp32"), BASE_FP32},
0245     {QStringLiteral("fp64"), BASE_FP64},
0246     {QStringLiteral("fp80"), BASE_FP80},
0247     {QStringLiteral("fp128"), BASE_FP128},
0248 #endif // v3.8.0
0249 
0250 #ifdef BASE_SEXAGESIMAL_2 // v3.18.0
0251     {QStringLiteral("sexa2"), BASE_SEXAGESIMAL_2},
0252     {QStringLiteral("sexa3"), BASE_SEXAGESIMAL_3},
0253 
0254     {QStringLiteral("latitude"), BASE_LATITUDE},
0255     {QStringLiteral("latitude2"), BASE_LATITUDE_2},
0256     {QStringLiteral("longitude"), BASE_LONGITUDE},
0257     {QStringLiteral("longitude2"), BASE_LONGITUDE_2},
0258 #endif
0259 
0260 #ifdef BASE_BINARY_DECIMAL // v4.2.0
0261     {QStringLiteral("bcd"), BASE_BINARY_DECIMAL},
0262 #endif
0263 };
0264 
0265 bool QalculateEngine::findPrefix(QString basePrefix, int *base, QString *customBase)
0266 {
0267     if (basePrefix.isEmpty()) {
0268         return true;
0269     }
0270 
0271     basePrefix = basePrefix.toLower();
0272     if (s_commonBaseMappings.contains(basePrefix)) {
0273         *base = s_commonBaseMappings[basePrefix];
0274         return true;
0275     }
0276 #ifdef BASE_CUSTOM // v3.3.0
0277     if (basePrefix.startsWith("base")) {
0278         *base = BASE_CUSTOM;
0279         *customBase = basePrefix.mid(4);
0280 
0281         return true;
0282     }
0283 #endif
0284     return false;
0285 }
0286 
0287 bool QalculateEngine::isKnownFunction(const QString &str)
0288 {
0289     return CALCULATOR->getFunction(str.toLocal8Bit().constData()) != nullptr;
0290 }