LoginSignup
5
4

More than 5 years have passed since last update.

C#:逆ポーランド記法を利用した数式の計算(2) 数式をトークンに分割する

Last updated at Posted at 2017-11-06

逆ポーランド記法を利用した数式の計算(1) 逆ポーランド記法の計算
逆ポーランド記法を利用した数式の計算(2) 数式をトークンに分割する ← 当記事
逆ポーランド記法を利用した数式の計算(3) ReversePolishNotationクラスを改良する
逆ポーランド記法を利用した数式の計算(4) Contextクラスを定義する
逆ポーランド記法を利用した数式の計算(5) Interpreterパターンで数式を解析する
逆ポーランド記法を利用した数式の計算(6) 最後の仕上げ

第1回目の逆ポーランド記法の計算では、1 5 + 2 3 + *といった逆ポーランド記法の式を計算するプログラムを紹介しました。その記事の続きです。

どんなプログラムを作るか

逆ポーランド記法で表現された式を計算することができましたので、一般的な書式で書かれた数式(加減乗除を行う式)を逆ポーランド記法に変換できれば、数式の計算をするプログラムを作成できます。

つまり、(2 + 4) * (8 - 6) といった式(文字列)を逆ポーランド記法に変換し、それを計算するプログラムを作成しようということです。

ということで、以下のような感じで利用できるクラスを作成したいと思います。

    var ans = Expression.Calculate("3 * (4 + 2)");

Expression.Calculateメソッドの中は、こんな感じ

    public static decimal Calculate(string exp) {
        // 数式を逆ポーランド記法へ変換
        ReversePolishNotation rpn = ConvertToRpn(exp);
        // 逆ポーランド記法の式を計算する
        return RpnCalculator.Calculate(rpn);
    }

以下のような、インスタンスメソッドにしても良いかなとも思ったのですが、前回、RpnCalculatorクラス定義して、そこにstaticメソッドとして定義してしまったので、変更せずに、このまま行こうと思います。

return rpn.Calculate();

式をトークンに分解

ということで、上記のRpnCalculator.Calculateメソッドは、前回のコードをそのまま利用します。shiracamusさんにコメントで提示していただいた、よりスマートなコードを使っても良いですね。

また、ReversePolishNotationクラスも少し変更すれば大丈夫そうです。

問題は、ConvertToRpnメソッドです。いきなりConvertToRpnメソッドのコードを書き始めるのは、さすがに無謀です。

ということで、まずは、"(2 + 34) * (28 - 6)" といった文字列を 以下に示すようなトークンに分割するクラスを作成しようと思います。

"("
"2"
"+"
"34"
")"
"*"
"("
"28"
"-"
"6"
")"

クラス名はTokenizerとしました。公開メンバーは、MoveNextメソッドと、Currentプロパティのみです。
コンストラクタで、式(string型)を受け取ります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Rpn.App {

    // Tokenに分解する
    public class Tokenizer {
        private string _expression;

        IEnumerator<string> _ite;

        public Tokenizer(string exp) {
            _expression = exp;
            _ite = GetTokens().GetEnumerator();
        }

        public bool MoveNext() {
            return _ite.MoveNext();
        }

        public string Current {
            get { return _ite.Current; }
        }

        private IEnumerable<string> GetTokens() {
            char c = NextChar();
            var token = "";
            while (c != (char)0) {
                if (char.IsDigit(c)) {
                    token += c;
                } else if (c == '.' && token.All(x => x != '.')) {
                    token += c;
                } else {
                    if (token != "")
                        yield return token;
                    token = "";
                    if (IsSymbol(c))
                        yield return c.ToString();
                    else if (c != ' ')
                        throw new ArithmeticException("正しい式ではありません");
                }
                c = NextChar();
            }
            if (token != "")
                yield return token;
        }

        private static bool IsSymbol(char c) {
            return "+-*/()".Any(x => x == c);
        }

        private int _currentIndex = 0;

        private char NextChar() {
            if (_currentIndex < _expression.Length)
                return _expression[_currentIndex++];
            return (char)0;
        }
    }
}

GetTokenの中がちょっと汚いですが、ご容赦を。

Tokenizerクラスの動きを確認してみる

本来は、単体テストを書くべきなのですが、今回は、動作を確認するテストプログラムを書いて、動作を確認します。
Tokenizerクラスの使い方も、これで分かると思います。

    class Program {
        static void Main(string[] args) {
            TestTokenizer();
        }

        private static void TestTokenizer() {
            while (true) {
                var line = Console.ReadLine();
                var tok = new Tokenizer(line);
                while (tok.MoveNext()) {
                    var token = tok.Current;
                    Console.WriteLine($" '{token}'");
                }
            }
        }
    }

テストプログラムの実行例

実行例です。

((2+4) * (10 + -5)) / 10
 '('
 '('
 '2'
 '+'
 '4'
 ')'
 '*'
 '('
 '10'
 '+'
 '-'
 '5'
 ')'
 ')'
 '/'
 '10'

正しく動いているようです。

次回は、ReversePolishNotationクラスをすこし手直ししたいと思います。

(続く)...


この記事は、Gushwell's C# Programming Pageで公開したものを加筆・修正したものです。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4