File indexing completed on 2024-05-19 04:56:01

0001 /**
0002  * \file expressionparser.h
0003  * Simple parser for expressions.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 23 Jan 2008
0008  *
0009  * Copyright (C) 2008-2024  Urs Fleisch
0010  *
0011  * This file is part of Kid3.
0012  *
0013  * Kid3 is free software; you can redistribute it and/or modify
0014  * it under the terms of the GNU General Public License as published by
0015  * the Free Software Foundation; either version 2 of the License, or
0016  * (at your option) any later version.
0017  *
0018  * Kid3 is distributed in the hope that it will be useful,
0019  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0020  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021  * GNU General Public License for more details.
0022  *
0023  * You should have received a copy of the GNU General Public License
0024  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0025  *
0026  * The RPN tokenizer is based on ExprEvaluator,
0027  * Copyright (C) 2004 the VideoLAN team, under the same license.
0028  */
0029 
0030 #include "expressionparser.h"
0031 
0032 namespace {
0033 
0034 /**
0035  * Convert a string to a boolean.
0036  *
0037  * @param str string
0038  * @param b   the boolean is returned here
0039  *
0040  * @return true if ok.
0041  */
0042 bool stringToBool(const QString& str, bool& b)
0043 {
0044   if (str == QLatin1String("1") || str == QLatin1String("true") ||
0045       str == QLatin1String("on") || str == QLatin1String("yes")) {
0046     b = true;
0047     return true;
0048   }
0049   if (str == QLatin1String("0") || str == QLatin1String("false") ||
0050       str == QLatin1String("off") || str == QLatin1String("no")) {
0051     b = false;
0052     return true;
0053   }
0054   return false;
0055 }
0056 
0057 /**
0058  * Convert a boolean to a string.
0059  *
0060  * @param b boolean to convert
0061  *
0062  * @return "1" or "0".
0063  */
0064 QString boolToString(bool b)
0065 {
0066   return b ? QLatin1String("1") : QLatin1String("0");
0067 }
0068 
0069 }
0070 
0071 /**
0072  * Constructor.
0073  *
0074  * @param operators additional operators (besides not, and, or),
0075  *                  highest priority first
0076  */
0077 ExpressionParser::ExpressionParser(QStringList operators) : // clazy:exclude=function-args-by-ref
0078   m_operators(operators << QLatin1String("not") << QLatin1String("and")
0079                         << QLatin1String("or")),
0080   m_error(false)
0081 {
0082 }
0083 
0084 /**
0085  * Compare operator priority.
0086  *
0087  * @return true if op1 has less priority than op2.
0088  */
0089 bool ExpressionParser::lessPriority(const QString& op1,
0090                                     const QString& op2) const
0091 {
0092   int index1 = m_operators.indexOf(op1);
0093   int index2 = m_operators.indexOf(op2);
0094   if (op1 == QLatin1String("(")) return true;
0095   if (index1 >= 0 && index2 >= 0) return index1 >= index2;
0096   return false;
0097 }
0098 
0099 /**
0100  * Tokenize an expression in reverse polish notation.
0101  *
0102  * @param expr with strings, operators, not, and, or, (, ).
0103  */
0104 void ExpressionParser::tokenizeRpn(const QString& expr)
0105 {
0106   m_rpnStack.clear();
0107 
0108   QStringList operatorStack;
0109   QString token;
0110   int begin = 0, end = 0, len = expr.length();
0111   while (begin < len) {
0112     // skip spaces
0113     while (expr[begin] == QLatin1Char(' ')) {
0114       ++begin;
0115     }
0116 
0117     if (expr[begin] == QLatin1Char('(')) {
0118       // push '(' on operator stack and continue
0119       operatorStack.push_back(QLatin1String("("));
0120       ++begin;
0121     } else if (expr[begin] == QLatin1Char(')')) {
0122       // after ')', pop operator stack until '(' is found
0123       while (!operatorStack.empty()) {
0124         QString lastOp = operatorStack.back();
0125         operatorStack.pop_back();
0126         if (lastOp == QLatin1String("(")) {
0127           break;
0128         }
0129         m_rpnStack.push_back(lastOp);
0130       }
0131       ++begin;
0132     } else {
0133       if (expr[begin] == QLatin1Char('"')) {
0134         // skip quoted string
0135         end = begin + 1;
0136         while (end < len &&
0137                !(expr[end] == QLatin1Char('"') &&
0138                  (end <= 0 || expr[end - 1] != QLatin1Char('\\')))) {
0139           ++end;
0140         }
0141         token = expr.mid(begin + 1, end - begin - 1);
0142         token.replace(QLatin1String("\\\""), QLatin1String("\""));
0143         begin = end + 1;
0144       } else {
0145         // skip spaces
0146         end = begin;
0147         while (end < len && expr[end] != QLatin1Char(' ') &&
0148                expr[end] != QLatin1Char(')')) {
0149           ++end;
0150         }
0151         token = expr.mid(begin, end - begin);
0152         begin = end;
0153       }
0154       if (m_operators.contains(token)) {
0155         // pop the operator stack while the token has lower priority
0156         while (!operatorStack.empty() &&
0157                lessPriority(token, operatorStack.back())) {
0158           QString lastOp = operatorStack.back();
0159           operatorStack.pop_back();
0160           m_rpnStack.push_back(lastOp);
0161         }
0162         operatorStack.push_back(token);
0163       } else {
0164         m_rpnStack.push_back(token);
0165       }
0166     }
0167   }
0168   // pop operator stack
0169   while (!operatorStack.empty()) {
0170     QString lastOp = operatorStack.back();
0171     operatorStack.pop_back();
0172     m_rpnStack.push_back(lastOp);
0173   }
0174   m_rpnIterator = m_rpnStack.constBegin();
0175 }
0176 
0177 /**
0178  * Clear the variable stack before restarting an evaluation.
0179  */
0180 void ExpressionParser::clearEvaluation()
0181 {
0182   m_rpnIterator = m_rpnStack.constBegin();
0183   m_varStack.clear();
0184   m_error = false;
0185 }
0186 
0187 /**
0188  * Pop a boolean from the variable stack.
0189  * Can be used to get the result after evaluate() returns false and
0190  * no error occurred.
0191  *
0192  * @param var the boolean is returned here
0193  *
0194  * @return true if ok.
0195  */
0196 bool ExpressionParser::popBool(bool& var)
0197 {
0198   if (m_varStack.empty() || !stringToBool(m_varStack.back(), var)) {
0199     return false;
0200   }
0201   m_varStack.pop_back();
0202   return true;
0203 }
0204 
0205 /**
0206  * Push a boolean to the variable stack.
0207  * Can be used to push the result of the operation returned by
0208  * evaluate() back onto the variable stack.
0209  *
0210  * @param var boolean to  push
0211  */
0212 void ExpressionParser::pushBool(bool var)
0213 {
0214   m_varStack.push_back(boolToString(var));
0215 }
0216 
0217 /**
0218  * Pop two booleans from the variable stack.
0219  *
0220  * @param var1 first boolean
0221  * @param var2 second boolean
0222  *
0223  * @return true if ok.
0224  */
0225 bool ExpressionParser::popTwoBools(bool& var1, bool& var2)
0226 {
0227   if (m_varStack.empty() || !stringToBool(m_varStack.back(), var1)) {
0228     return false;
0229   }
0230   m_varStack.pop_back();
0231   if (m_varStack.empty() || !stringToBool(m_varStack.back(), var2)) {
0232     return false;
0233   }
0234   m_varStack.pop_back();
0235   return true;
0236 }
0237 
0238 /**
0239  * Evaluate the RPN stack.
0240  * Boolean operations and, or, not are performed automatically. If another
0241  * operation has to be performed, the method stops and returns operator
0242  * and variables. The result can then be pushed onto the stack using
0243  * pushBool() and then the method can be called again.
0244  *
0245  * @param op the operator is returned here
0246  * @param var1 the first variable is returned here
0247  * @param var2 the second variable is returned here
0248  *
0249  * @return true if the RPN stack has more to evaluate,
0250  *         if false, the evaluation is finished.
0251  */
0252 bool ExpressionParser::evaluate(QString& op, QString& var1, QString& var2)
0253 {
0254   while (m_rpnIterator != m_rpnStack.constEnd()) {
0255     if (QString token = *m_rpnIterator++; token == QLatin1String("and")) {
0256       bool b1, b2;
0257       if (!popTwoBools(b1, b2)) {
0258         m_error = true;
0259         break;
0260       }
0261       pushBool(b1 && b2);
0262     } else if (token == QLatin1String("or")) {
0263       bool b1, b2;
0264       if (!popTwoBools(b1, b2)) {
0265         m_error = true;
0266         break;
0267       }
0268       pushBool(b1 || b2);
0269     } else if (token == QLatin1String("not")) {
0270       bool var;
0271       if (!popBool(var)) {
0272         m_error = true;
0273         break;
0274       }
0275       pushBool(!var);
0276     } else if (m_operators.contains(token)) {
0277       if (m_varStack.empty()) {
0278         m_error = true;
0279         break;
0280       }
0281       var1 = m_varStack.back();
0282       m_varStack.pop_back();
0283       if (m_varStack.empty()) {
0284         m_error = true;
0285         break;
0286       }
0287       var2 = m_varStack.back();
0288       m_varStack.pop_back();
0289       op = token;
0290       return true;
0291     } else {
0292       m_varStack.push_back(token);
0293     }
0294   }
0295   return false;
0296 }