Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Qt Widget-Based Calculator: UI Validation, Infix Parsing, RPN Conversion, and Evaluation

Tech 1

User interface uses QWidget as the top-level window, QLineEdit to show and edit the expression/result, and QPushButton for keypad input.

  • Normalize user input as it’s typed to simplify later evaluation.
  • Parentheses are tracked sothat a closing parenthesis is only allowed when a matching opening parenthesis exists.

Input validation rules

  • Parentheses must be balanced and properly ordered

    • On "(": increase balance
    • On ")": valid only if balance > 0, then decrease balance
  • Digits cannot immediately follow a right parenthesis

    • Example: 10+3)5*2 is invalid because digit 5 follows ')'
  • Decimal point

    • A dot must have a digit before it
    • Each contiguous number may contain at most one dot
    • Example: 1.23.45 is invalid
  • Plus/Minus

    • Cannot follow a decimal point
    • Avoid two consecutive binary operators (+ - * /) directly preceding another operator
    • Example:
      • 7*-+10 is invalid (two operators before the number)
      • 7.+ is invalid (dot before plus)
      • 7-(--5) is invalid (double minus before a number inside parentheses)
  • Multiply/Divide cannot follow: empty, '(', '.', '+', '-', '*', '/'

    • Example: *1+(/5+10 is invalid
  • Left parenthesis '('

    • Cannot follow ')', a digit, or '.'
    • Avoid two consecutive operators right before '('
    • Example: ()+10(11+10 is invalid (no operand before '(' appropriately)
  • Right parenthesis ')'

    • Cannot follow empty string, operator (+ - * /), '.', or '('
    • Must have a matching '('
    • Example: )+(10+5.) is invalid

Expression engine

Tokenization (infix splitting)

  • Scan the expression and split into numbers and operators.
  • Distinguish unary +/− from binary +/−:
    • At the beginning of the expression, treat +/− as a sign for the following number
    • Immediately following '(', '+', '-', '*', or '/', treat +/− as a sign
    • Otherwise, +/− is an operator

Example: +9+(-3--1)*-5 splits into

  • "+9", "+", "(", "-3", "-", "-1", ")", "*", "-5"

Converting infix tokens to RPN (postfix)

  • Use a stack for operators and an output queue for the final postfix sequence.
  • Algorithm
    • If token is a number: push to output
    • If token is '+' or '-': pop operators from the stack until hitting '(' or stack is empty; then push the operator
    • If token is '' or '/': pop while the top of stack is '' or '/' (and not '('); then push the operator
    • If token is '(': push too stack
    • If token is ')': pop operators to output until '(' is found; discard the '('
    • After all tokens processed: pop remaining operators to output

Example postfix for +9+(-3--1)*-5

  • +9, -3, -1, -, -5, *, +

Evaluating RPN

  • Use a value stack.
  • For a number: push it
  • For an operator: pop right then left, compute, then push result
    • Division needs explicit zero check
  • At the end: the single stack value is the result

Code: Expression engine (header)

#ifndef EXPRENGINE_H
#define EXPRENGINE_H

#include <QString>
#include <QVector>
#include <QStack>

class ExprEngine {
public:
    QString result(const QString& expr);

private:
    QVector<QString> lex(const QString& s);
    QVector<QString> toRpn(const QVector<QString>& tokens);
    QString evalRpn(const QVector<QString>& rpn);
    QString trimNumber(const QString& s);
    bool isOperator(const QString& t) const;
    int precedence(const QString& op) const;
};

#endif // EXPRENGINE_H

Code: Expression engine (implementation)

#include "ExprEngine.h"

static bool isDigitOrDot(QChar c) {
    return (c >= '0' && c <= '9') || c == '.';
}

bool ExprEngine::isOperator(const QString& t) const {
    return t == "+" || t == "-" || t == "*" || t == "/";
}

int ExprEngine::precedence(const QString& op) const {
    if (op == "+" || op == "-") return 1;
    if (op == "*" || op == "/") return 2;
    return 0;
}

QVector<QString> ExprEngine::lex(const QString& s) {
    QVector<QString> out;
    QString num;

    auto flushNum = [&]() {
        if (!num.isEmpty()) {
            out.push_back(num);
            num.clear();
        }
    };

    for (int i = 0; i < s.size(); ++i) {
        const QChar ch = s[i];
        if (isDigitOrDot(ch)) {
            num.append(ch);
            continue;
        }

        if (ch == '(' || ch == ')' || ch == '*' || ch == '/') {
            flushNum();
            out.push_back(QString(ch));
            continue;
        }

        if (ch == '+' || ch == '-') {
            const bool atStart = (i == 0);
            const QChar prev = atStart ? '\0' : s[i - 1];
            const bool prevIsOp = (prev == '(' || prev == '+' || prev == '-' || prev == '*' || prev == '/');
            if (atStart || prevIsOp) {
                // unary sign becomes part of the number token
                num.append(ch);
            } else {
                flushNum();
                out.push_back(QString(ch));
            }
            continue;
        }
    }
    flushNum();
    return out;
}

QVector<QString> ExprEngine::toRpn(const QVector<QString>& tokens) {
    QVector<QString> out;
    QStack<QString> ops;

    for (const QString& t : tokens) {
        bool ok = false;
        t.toDouble(&ok);
        if (ok) {
            out.push_back(t);
            continue;
        }
        if (t == "(") { ops.push(t); continue; }
        if (t == ")") {
            while (!ops.isEmpty() && ops.top() != "(") {
                out.push_back(ops.pop());
            }
            if (!ops.isEmpty() && ops.top() == "(") ops.pop();
            continue;
        }
        if (isOperator(t)) {
            while (!ops.isEmpty() && isOperator(ops.top()) && precedence(ops.top()) >= precedence(t)) {
                out.push_back(ops.pop());
            }
            ops.push(t);
            continue;
        }
    }
    while (!ops.isEmpty()) {
        out.push_back(ops.pop());
    }
    return out;
}

QString ExprEngine::trimNumber(const QString& s) {
    if (!s.contains('.')) return s;
    QString t = s;
    // remove trailing zeros
    while (t.size() > 1 && t.endsWith('0')) t.chop(1);
    if (t.endsWith('.')) t.chop(1);
    return t;
}

QString ExprEngine::evalRpn(const QVector<QString>& rpn) {
    QStack<QString> st;

    auto apply = [&](const QString& a, const QString& op, const QString& b) -> QString {
        const double left = a.toDouble();
        const double right = b.toDouble();
        double res = 0.0;
        if (op == "+") res = left + right;
        else if (op == "-") res = left - right;
        else if (op == "*") res = left * right;
        else if (op == "/") {
            if (qAbs(right) < 1e-15) return QStringLiteral("DIV_ZERO");
            res = left / right;
        }
        QString out = QString::number(res, 'f', 12);
        return trimNumber(out);
    };

    for (const QString& t : rpn) {
        bool ok = false;
        t.toDouble(&ok);
        if (ok) {
            st.push(t);
            continue;
        }
        if (isOperator(t)) {
            if (st.size() < 2) return QStringLiteral("ERR");
            const QString right = st.pop();
            const QString left = st.pop();
            QString r = apply(left, t, right);
            if (r == "DIV_ZERO") return r;
            st.push(r);
        }
    }
    if (st.size() != 1) return QStringLiteral("ERR");
    return st.pop();
}

QString ExprEngine::result(const QString& expr) {
    const auto tokens = lex(expr);
    const auto rpn = toRpn(tokens);
    return evalRpn(rpn);
}

Code: Calculator window (header)

#ifndef CALCWINDOW_H
#define CALCWINDOW_H

#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QVector>
#include <QString>
#include "ExprEngine.h"

class CalcWindow : public QWidget {
    Q_OBJECT
public:
    static CalcWindow* create(QWidget* parent = nullptr);
    void showFixed();

private:
    explicit CalcWindow(QWidget* parent = nullptr);
    void buildUi();
    void wireUp();
    void handleKey(const QString& key);
    bool canAppendDigit(const QString& line) const;
    bool canAppendDot(const QString& line) const;
    bool canAppendPlusMinus(const QString& line) const;
    bool canAppendMulDiv(const QString& line) const;
    bool canAppendLParen(const QString& line) const;
    bool canAppendRParen(const QString& line) const;
    int currentParenBalance(const QString& line) const;

private slots:
    void onKey();

private:
    ExprEngine engine_;
    QLineEdit* display_ { nullptr };
    QVector<QPushButton*> keys_;
    bool clearOnNext_ { false };
};

#endif // CALCWINDOW_H

Code: Calculator window (implementation)

#include "CalcWindow.h"
#include <QGridLayout>
#include <QHBoxLayout>
#include <QVBoxLayout>

CalcWindow* CalcWindow::create(QWidget* parent) {
    return new CalcWindow(parent);
}

CalcWindow::CalcWindow(QWidget* parent) : QWidget(parent) {
    buildUi();
    wireUp();
}

void CalcWindow::buildUi() {
    setWindowTitle("Calculator");

    display_ = new QLineEdit(this);
    display_->setAlignment(Qt::AlignRight);
    display_->setReadOnly(true);

    auto grid = new QGridLayout();

    // Layout map similar to: <-, CE, digits, operators, parentheses, equals
    const QVector<QString> rows[] = {
        { "<-", "CE" },
        { "7", "8", "9", "+", "(" },
        { "4", "5", "6", "-", ")" },
        { "1", "2", "3", "*", "=" },
        { "0", ".", "/" }
    };

    int r = 0;
    for (const auto& row : rows) {
        int c = 0;
        for (const auto& label : row) {
            auto* btn = new QPushButton(label, this);
            keys_.push_back(btn);
            if (label == "CE") {
                grid->addWidget(btn, r, c, 1, 2);
                c += 2;
                continue;
            }
            if (label == "=") {
                grid->addWidget(btn, r, c, 2, 1); // span to next row for emphasis
                // next items in the same column may skip placement
                ++c;
                continue;
            }
            if (label == "0") {
                grid->addWidget(btn, r, c, 1, 2);
                c += 2;
                continue;
            }
            grid->addWidget(btn, r, c);
            ++c;
        }
        ++r;
    }

    auto vbox = new QVBoxLayout(this);
    vbox->addWidget(display_);
    vbox->addLayout(grid);
    setLayout(vbox);
}

void CalcWindow::wireUp() {
    for (auto* b : keys_) {
        connect(b, &QPushButton::clicked, this, &CalcWindow::onKey);
    }
}

int CalcWindow::currentParenBalance(const QString& line) const {
    int bal = 0;
    for (const auto ch : line) {
        if (ch == '(') ++bal;
        else if (ch == ')') --bal;
    }
    return bal;
}

bool CalcWindow::canAppendDigit(const QString& line) const {
    if (!line.isEmpty()) {
        const QChar last = line.back();
        if (last == ')') return false;
    }
    return true;
}

bool CalcWindow::canAppendDot(const QString& line) const {
    if (line.isEmpty()) return false; // dot cannot start a number
    const QChar last = line.back();
    if (!(last >= '0' && last <= '9')) return false; // dot must follow a digit
    // ensure current numeric run has no dot
    for (int i = line.size() - 1; i >= 0; --i) {
        const QChar ch = line[i];
        if (ch == '.' ) return false; // already has a dot
        if (!(ch >= '0' && ch <= '9')) break;
    }
    return true;
}

bool CalcWindow::canAppendPlusMinus(const QString& line) const {
    if (!line.isEmpty() && line.back() == '.') return false;
    if (line.size() >= 2) {
        const QChar a = line[line.size() - 2];
        const QChar b = line[line.size() - 1];
        const auto isOp = [](QChar c){ return c=='+'||c=='-'||c=='*'||c=='/'; };
        if ((a=='(' || isOp(a)) && isOp(b)) return false;
    }
    return true;
}

bool CalcWindow::canAppendMulDiv(const QString& line) const {
    if (line.isEmpty()) return false;
    const QChar last = line.back();
    if (last=='(' || last=='.' || last=='+' || last=='-' || last=='*' || last=='/') return false;
    return true;
}

bool CalcWindow::canAppendLParen(const QString& line) const {
    if (line.isEmpty()) return true;
    const QChar last = line.back();
    if (last==')' || (last>='0'&&last<='9') || last=='.') return false;
    if (line.size() >= 2) {
        const QChar a = line[line.size() - 2];
        const QChar b = line[line.size() - 1];
        const auto isOp = [](QChar c){ return c=='+'||c=='-'||c=='*'||c=='/'; };
        if (isOp(a) && isOp(b)) return false;
    }
    return true;
}

bool CalcWindow::canAppendRParen(const QString& line) const {
    if (line.isEmpty()) return false;
    if (currentParenBalance(line) <= 0) return false;
    const QChar last = line.back();
    if (last=='+'||last=='-'||last=='*'||last=='/'||last=='.'||last=='(') return false;
    return true;
}

void CalcWindow::handleKey(const QString& key) {
    QString line = display_->text();

    if (clearOnNext_) {
        line.clear();
        clearOnNext_ = false;
    }

    if (key.size() == 1 && key[0] >= '0' && key[0] <= '9') {
        if (!canAppendDigit(line)) return;
        line += key;
    } else if (key == ".") {
        if (!canAppendDot(line)) return;
        line += key;
    } else if (key == "+" || key == "-") {
        if (!canAppendPlusMinus(line)) return;
        line += key;
    } else if (key == "*" || key == "/") {
        if (!canAppendMulDiv(line)) return;
        line += key;
    } else if (key == "(") {
        if (!canAppendLParen(line)) return;
        line += key;
    } else if (key == ")") {
        if (!canAppendRParen(line)) return;
        line += key;
    } else if (key == "<-") {
        if (!line.isEmpty()) line.chop(1);
    } else if (key == "CE") {
        line.clear();
    } else if (key == "=") {
        if (line.isEmpty()) {
            display_->setText(line);
            return;
        }
        const QString res = engine_.result(line);
        if (res == "DIV_ZERO") {
            line += " : Division by zero";
        } else if (res == "ERR") {
            line += " : Syntax error";
        } else {
            line += "=" + res;
        }
        clearOnNext_ = true;
    }

    display_->setText(line);
}

void CalcWindow::onKey() {
    if (auto* btn = qobject_cast<QPushButton*>(sender())) {
        handleKey(btn->text());
    }
}

void CalcWindow::showFixed() {
    QWidget::show();
    setFixedSize(width(), height());
}

Code: main.cpp

#include <QApplication>
#include "CalcWindow.h"

int main(int argc, char* argv[]) {
    QApplication app(argc, argv);

    CalcWindow* w = CalcWindow::create();
    w->showFixed();

    return app.exec();
}
Tags: qtC++widgets

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.