MASA BLOG

Qt6で電卓を作るその3

この記事では、C++とQtを使用して電卓アプリケーションに括弧と関数(例としてsin)を導入し、逆ポーランド記法(RPN)を用いて計算を行う方法を解説します。

2024-11-13

Qt6で電卓を作るその3

はじめに

    前回の記事(その1その2)では、Qtを使用して基本的な電卓アプリケーションを作成し、四則演算の実装について解説しました。今回は、括弧を使用した複雑な数式の計算や、関数(例としてsin)の導入を行い、電卓の機能を拡張します。これには、数式を解析して計算するために逆ポーランド記法(RPN)とShunting Yardアルゴリズムを使用します。

    Shunting Yardアルゴリズムによる中置記法からRPNへの変換

    Shunting Yardアルゴリズムは、エドガー・ダイクストラが開発したアルゴリズムで、中置記法の数式をRPNに変換するために使用されます。このアルゴリズムでは、演算子の優先順位と括弧の対応を考慮して、数式をトークン化し、適切な順序で出力します。

    逆ポーランド記法(RPN)とは

    • *逆ポーランド記法(Reverse Polish Notation, RPN)**は、数式を後置記法で表現する方法です。通常の中置記法とは異なり、演算子がオペランドの後に来ます。例えば:
    • 中置記法: 3 + 4
    • RPN: 3 4 +

    RPNの利点は、括弧や演算子の優先順位を明示的に扱う必要がなく、スタックを使用して計算を容易に行える点です。

    電卓アプリケーションへの括弧の導入

    トークン化の実装

    数式を解析する最初のステップは、トークン化です。これは、数式を構成する数字、演算子、括弧、関数などを個々の要素(トークン)に分割するプロセスです。

    cpp
    コードをコピーする
    // 数式をトークンに分割する関数
    QList<QString> MainWindow::tokenize(const QString &expression)
    {
        QList<QString> tokens;
        QString numberBuffer;
    
        for(int i = 0; i < expression.length(); ++i){
            QChar c = expression[i];
    
            if(c.isDigit() || c == '.'){
                numberBuffer += c;
            }
            else{
                if(!numberBuffer.isEmpty()){
                    tokens.append(numberBuffer);
                    numberBuffer.clear();
                }
    
                if(c == '+' || c == '-' || c == '*' || c == '/' || c == '^' ||
                   c == '(' || c == ')'){
                    tokens.append(QString(c));
                }
                else{
                    // その他の文字は無視
                }
            }
        }
    
        if(!numberBuffer.isEmpty()){
            tokens.append(numberBuffer);
        }
    
        return tokens;
    }
    
    

    ポイント:

    • 数字と小数点をnumberBufferに蓄積し、演算子や括弧が出てきたらバッファをクリアしてトークンに追加します。
    • 演算子や括弧はそれぞれ単一のトークンとして追加します。

    Shunting Yardアルゴリズムの実装

    トークン化された数式をRPNに変換します。

    cpp
    コードをコピーする
    // Shunting YardアルゴリズムでトークンをRPNに変換する関数
    QList<QString> MainWindow::shuntingYard(const QList<QString> &tokens)
    {
        QList<QString> outputQueue;
        QStack<QString> operatorStack;
    
        for(const QString &token : tokens){
            bool isNumber;
            token.toDouble(&isNumber);
            if(isNumber){
                outputQueue.append(token);
            }
            else if(token == "+" || token == "-" || token == "*" || token == "/" || token == "^"){
                while(!operatorStack.isEmpty() &&
                      getPrecedence(operatorStack.top()) >= getPrecedence(token)){
                    outputQueue.append(operatorStack.pop());
                }
                operatorStack.push(token);
            }
            else if(token == "("){
                operatorStack.push(token);
            }
            else if(token == ")"){
                while(!operatorStack.isEmpty() && operatorStack.top() != "("){
                    outputQueue.append(operatorStack.pop());
                }
                if(!operatorStack.isEmpty()){
                    operatorStack.pop(); // '(' を取り除く
                }
                else{
                    // 括弧の対応が取れていない場合
                    qDebug() << "Error: Mismatched parentheses";
                    outputQueue.clear();
                    return outputQueue;
                }
            }
        }
    
        while(!operatorStack.isEmpty()){
            QString op = operatorStack.pop();
            if(op == "(" || op == ")"){
                qDebug() << "Error: Mismatched parentheses";
                outputQueue.clear();
                return outputQueue;
            }
            outputQueue.append(op);
        }
    
        return outputQueue;
    }
    
    

    ポイント:

    • 数字の場合outputQueueに追加します。
    • 演算子の場合:スタックトップの演算子と優先順位を比較し、必要に応じてoutputQueueに移動します。
    • 左括弧の場合operatorStackにプッシュします。
    • 右括弧の場合:左括弧が見つかるまでスタックから演算子をoutputQueueに移動します。

    演算子の優先順位を決定する関数:

    cpp
    コードをコピーする
    // 演算子の優先順位を返す関数
    int MainWindow::getPrecedence(const QString &op)
    {
        if(op == "+" || op == "-") {
            return 1;
        }
        if(op == "*" || op == "/") {
            return 2;
        }
        if(op == "^") {
            return 3; // べき乗は高い優先順位
        }
        return 0;
    }
    
    

    関数sinの追加

    関数トークンの処理

    トークン化の際に関数名を認識し、適切に処理します。

    cpp
    コードをコピーする
    // 数式をトークンに分割する関数(関数対応)
    QList<QString> MainWindow::tokenize(const QString &expression)
    {
        QList<QString> tokens;
        QString numberBuffer;
        QString functionBuffer;
    
        for(int i = 0; i < expression.length(); ++i){
            QChar c = expression[i];
    
            if(c.isLetter()){
                functionBuffer += c;
                if(functionBuffer == "sin"){
                    tokens.append(functionBuffer);
                    functionBuffer.clear();
                }
            }
            else if(c.isDigit() || c == '.'){
                if(!functionBuffer.isEmpty()){
                    tokens.append(functionBuffer);
                    functionBuffer.clear();
                }
                numberBuffer += c;
            }
            else{
                if(!functionBuffer.isEmpty()){
                    tokens.append(functionBuffer);
                    functionBuffer.clear();
                }
                if(!numberBuffer.isEmpty()){
                    tokens.append(numberBuffer);
                    numberBuffer.clear();
                }
    
                if(c == '+' || c == '-' || c == '*' || c == '/' || c == '^' ||
                   c == '(' || c == ')'){
                    tokens.append(QString(c));
                }
            }
        }
    
        if(!functionBuffer.isEmpty()){
            tokens.append(functionBuffer);
        }
        if(!numberBuffer.isEmpty()){
            tokens.append(numberBuffer);
        }
    
        return tokens;
    }
    
    

    ポイント:

    • functionBufferを使用して関数名(例:sin)を蓄積し、トークンとして追加します。
    • 関数名が認識されたら、バッファをクリアして次のトークンを処理します。

    RPNでの関数の評価

    Shunting Yardアルゴリズムで関数を適切に処理し、RPNで評価します。

    cpp
    コードをコピーする
    // Shunting Yardアルゴリズムでの関数処理
    else if(token == "sin"){
        operatorStack.push(token);
    }
    
    
    cpp
    コードをコピーする
    // RPNを評価する関数での関数処理
    else if(token == "sin"){
        if(evalStack.isEmpty()){
            success = false;
            qDebug() << "Error: Insufficient values in stack for function" << token;
            return 0.0;
        }
        double operand = evalStack.pop();
        double result = std::sin(operand); // ラジアンで計算
        evalStack.push(result);
    }
    
    

    ポイント:

    • Shunting Yardアルゴリズムでは、関数名を演算子スタックにプッシュします。
    • RPNの評価では、関数に必要なオペランドをスタックからポップし、結果を計算してスタックにプッシュします。

    イコールボタンの処理と計算の実行

    イコールボタンがクリックされたときに、数式を評価します。

    cpp
    コードをコピーする
    // イコールボタンの処理
    void MainWindow::on_pushButton_Equal_clicked()
    {
        QString expression = ui->textBrowser->toPlainText();
    
        // トークン化
        QList<QString> tokens = tokenize(expression);
        if(tokens.isEmpty()){
            ui->textBrowser->setText("Error");
            return;
        }
    
        // RPNに変換
        QList<QString> rpn = shuntingYard(tokens);
        if(rpn.isEmpty()){
            ui->textBrowser->setText("Error");
            return;
        }
    
        // RPNを評価
        bool success;
        double result = evaluateRPN(rpn, success);
        if(!success){
            ui->textBrowser->setText("Error");
            return;
        }
    
        // 結果を表示
        ui->textBrowser->setText(QString::number(result, 'g', 15));
    
        // 状態をリセット
        currentOperator = "";
        isOperatorClicked = false;
    }
    
    

    ポイント:

    • 入力された数式をトークン化し、RPNに変換します。
    • RPNを評価し、結果をディスプレイに表示します。
    • エラーが発生した場合は"Error"と表示します。

    まとめ

    今回は、C++とQtを使用して電卓アプリケーションに**括弧と関数(sin)**を導入し、**逆ポーランド記法(RPN)**を用いて計算を行う方法を解説しました。これにより、複雑な数式の計算や関数の評価が可能になりました。

    ポイントのおさらい:

    • 逆ポーランド記法を使用することで、括弧や演算子の優先順位を適切に処理できます。
    • Shunting Yardアルゴリズムを実装して、中置記法からRPNへの変換を行います。
    • トークン化の際に関数名を正しく認識し、RPNの評価で関数を適切に計算します。

    これで、より高度な電卓アプリケーションを作成する基盤が整いました。今後は、さらなる関数の追加やユーザーインターフェースの改善など、機能拡張を行うことができます。

    注意:この記事では、sin関数を例に取り上げましたが、他の関数を追加する際も同様の手順で実装できます。ただし、適切なエラーチェックや入力検証を行うことが重要です。

    他の関数など実装した例はhttps://github.com/masa-1234tf/Calculator mainwindow.cppをご覧ください。

    参考書

     

    Product Image
    VisualC++2022パーフェクトマスター (Perfect Master 188)

    https://amzn.to/4hCPURK

    Visual C++は、無料で配布されているオブジェクト指向に対応した強力なプログラミング言語です。本書は、Windowsアプリを開発したい方に向けて、標準C++およびC++/CXの基礎から主要機能、...続きを読む

    Product Image
    VisualC++2022パーフェクトマスター (Perfect Master 188)

    https://amzn.to/4hCPURK

    Visual C++は、無料で配布されているオブジェクト指向に対応した強力なプログラミング言語です。本書は、Windowsアプリを開発したい方に向けて、標準C++およびC++/CXの基礎から主要機能、...続きを読む

    Product Image
    Qt5/Qt6入門 C++編 技術の泉シリーズ (技術の泉シリーズ(NextPublishing))

    https://amzn.to/3YHMWCZ

    本書はクロスプラットフォームの開発フレームワーク「Qt」について、Qt5とQt6の両方に対応した入門書です。インストールから始め、C++でコードを書き、画面はQt Widgetsベースのアプリについて...続きを読む

    自己紹介

    Masa Blogへようこそ、Masaです。ここではプログラミングを初めDTMやギターなど音楽のことを発信していきます。