優先順位のある式(式)パーサー?


103

バイナリ(+、-、|、&、*、/など)演算子、単項(!)演算子、および括弧を処理する単純なスタックアルゴリズムを使用して、方程式パーサーを開発しました。

ただし、この方法を使用すると、すべてに同じ優先順位が残ります。演算子に関係なく左から右に評価されますが、括弧を使用して優先順位を強制できます。

そのため、現在のところ、「1 + 11 * 5」は56でなく、60を返します。

これは現在のプロジェクトに適していますが、後のプロジェクトで使用できる汎用ルーチンが欲しいです。

明確にするために編集:

優先順位のある方程式を解析するための良いアルゴリズムは何ですか?

私は実装が簡単なものに興味があり、利用可能なコードのライセンス問題を回避するために自分でコーディングできることを理解しています。

文法:

文法の質問が理解できません-これを手で書きました。YACCやBisonの必要性がわからないほど簡単です。「2 + 3 *(42/13)」のような方程式で文字列を計算するだけです。

言語:

私はCでこれを行っていますが、言語固有のソリューションではなく、アルゴリズムに興味があります。Cは十分に低いレベルなので、必要に応じて別の言語に簡単に変換できます。

コード例

上記で説明した単純式パーサーテストコードを投稿しました。プロジェクトの要件が変更されたため、コードがプロジェクトに組み込まれていなかったため、パフォーマンスやスペースを最適化する必要がありませんでした。元の詳細形式であり、すぐに理解できるはずです。演算子の優先順位の観点からそれをさらに行う場合は、マクロハックを選択することになります。マクロハックは、プログラムの他の部分と簡単に一致するためです。ただし、これを実際のプロジェクトで使用する場合は、よりコンパクトで高速なパーサーを使用します。

関連する質問

数学パーサーのスマートなデザイン?

-アダム


私は書かれているC#で表現パーサーを自分のブログに。シャンティングヤードアルゴリズムでは、スタックなしでポストフィックスにインフィックスを行います。配列のみを使用します。
なGuge

私が理解しているように、算術式のみを解析する必要があります。使用逆ポーランド記法
mishadoff

回答:


69

難しい方法

再帰的な下降パーサーが必要です。

優先順位を取得するには、たとえば、サンプル文字列を使用して再帰的に考える必要があります。

1+11*5

これを手動で行うには1、を読み取ってからプラスを表示し、11...で始まるまったく新しい再帰解析「セッション」を開始し11 * 5、を独自の要素に解析して、で解析ツリーを生成する必要があり1 + (11 * 5)ます。

これは、特にCに追加された無力性を使用して、説明しようとしても非常に痛みを感じます。11を解析した後、*が実際に+だった場合は、用語の作成を中止して、代わりに11要因としてそれ自体。私の頭はすでに爆発しています。それは再帰的なまともな戦略で可能ですが、より良い方法があります...

簡単な(正しい)方法

BisonのようなGPLツールを使用する場合、bisonによって生成されたCコードはGPLによってカバーされないため(IANALですが、GPLツールがGPLを強制しないので、ライセンスの問題を心配する必要はないでしょう)生成されたコード/バイナリ;たとえば、AppleはGCCを使用してApertureなどのコードをコンパイルし、GPLのコードを使用せずにそれを販売します)。

Bison(または同等のもの、ANTLRなど)をダウンロードします。

通常、bisonを実行して、この4つの関数計算機を示す目的のCコードを取得できるいくつかのサンプルコードがあります。

http://www.gnu.org/software/bison/manual/html_node/Infix-Calc.html

生成されたコードを見て、これが思ったほど簡単ではないことを確認してください。また、Bisonのようなツールを使用することの利点は、1)何かを学ぶことです(特にドラゴンの本を読んで文法について学ぶ場合)、2)NIHがホイールを再発明しようとするのを避けます。実際のパーサージェネレーターツールを使用すると、実際に後で拡張して、パーサーが解析ツールのドメインであることを他の人に示すことができます。


更新:

ここの人々は多くの健全なアドバイスを提供しています。解析ツールのスキップ、またはシャンティングヤードアルゴリズムまたは手動の再帰的な適切なパーサーの使用に対する私の唯一の警告は、小さなおもちゃの言語1がいつか、関数(sin、cos、log)と変数、条件を備えた大きな実際の言語に変わる可能性があることです。ループ。

Flex / Bisonは、小さくて単純なインタープリターにとってはやりすぎかもしれませんが、パーサーとエバリュエーターが1回限りであれば、変更を加える必要がある場合や機能を追加する必要がある場合に問題が発生する可能性があります。あなたの状況はさまざまであり、あなたの判断を使用する必要があります。他人をあなたの罪で罰せ ないでください[2]、適切ではないツールを構築してください

私のお気に入りの解析ツール

この仕事に最適なツールは、プログラミング言語Haskellに付属するParsecライブラリー(再帰的なまともなパーサー用)です。これはBNFや、解析用の特殊なツールやドメイン固有の言語(サンプルコード[3])によく似ていますが、実際にはHaskellの通常のライブラリにすぎません。つまり、残りのビルドステップと同じビルドステップでコンパイルされます。 Haskellコードの一部であり、任意のHaskellコードを記述してパーサー内で呼び出すことができ、同じコード内で他のライブラリをすべて組み合わせて一致させることができます。(Haskell以外の言語でこのような構文解析言語を埋め込むと、ところで構文の乱れが発生します。私はC#でこれを実行しましたが、非常にうまく機能しますが、それほどきれいで簡潔ではありません。)

ノート:

1 Richard Stallmanは、「Tclを使用すべきでない理由」で述べています。

Emacsの主要な教訓は、拡張機能の言語は単なる「拡張機能言語」であってはならないということです。実質的なプログラムを作成して維持するために設計された、実際のプログラミング言語である必要があります。人々はそうしたいと思うでしょうから!

[2]はい、私はその「言語」を使用することから永遠に傷つきます。

また、このエントリを送信したときのプレビューは正しかったが、SOが適切なパーサーではないため、最初の段落のクローズアンカータグが食べられなかったことに注意してください。おそらく、微妙で小さな間違いが発生します。

[3] Parsecを使用したHaskellパーサーのスニペット:指数、括弧、乗算用の空白、定数(piやeなど)で拡張された4つの関数計算機。

aexpr   =   expr `chainl1` toOp
expr    =   optChainl1 term addop (toScalar 0)
term    =   factor `chainl1` mulop
factor  =   sexpr  `chainr1` powop
sexpr   =   parens aexpr
        <|> scalar
        <|> ident

powop   =   sym "^" >>= return . (B Pow)
        <|> sym "^-" >>= return . (\x y -> B Pow x (B Sub (toScalar 0) y))

toOp    =   sym "->" >>= return . (B To)

mulop   =   sym "*" >>= return . (B Mul)
        <|> sym "/" >>= return . (B Div)
        <|> sym "%" >>= return . (B Mod)
        <|>             return . (B Mul)

addop   =   sym "+" >>= return . (B Add) 
        <|> sym "-" >>= return . (B Sub)

scalar = number >>= return . toScalar

ident  = literal >>= return . Lit

parens p = do
             lparen
             result <- p
             rparen
             return result

9
私のポイントを強調するために、私の投稿のマークアップが正しく解析されていないことに注意してください(これは静的にレンダリングされたマークアップとWMDプレビューでレンダリングされたマークアップの間で異なります)。それを修正するためにいくつかの試みがありましたが、パーサーは間違っていると思います。みんなにお願いして、正しく解析してください!
Jared Updike、

155

操車場のアルゴリズムは、このための適切なツールです。ウィキペディアはこれについて本当に混乱していますが、基本的にアルゴリズムは次のように機能します:

たとえば、1 + 2 * 3 + 4を評価したいとします。直感的には、最初に2 * 3を実行する必要があることを「知っています」が、どのようにこの結果を得るのでしょうか。重要なのは、文字列を左から右にスキャンしているときに、後続の演算子の優先順位が低い(または等しい)場合に演算子を評価することを認識することです。この例のコンテキストでは、次のことを行います。

  1. 見てください:1 + 2、何もしないでください。
  2. 1 + 2 * 3を見てください。それでも何もしません。
  3. 1 + 2 * 3 + 4を見ると、次の演算子の優先順位が低いため、2 * 3を評価する必要があることがわかります。

これをどのように実装しますか?

数値用と演算子用の2つのスタックが必要です。あなたはいつもスタックに数字をプッシュします。新しい各演算子をスタックの一番上の演算子と比較します。スタックの一番上の演算子の方が優先順位が高い場合は、演算子を演算子スタックからポップし、オペランドを数値スタックからポップして、演算子を適用して結果をプッシュします。番号スタックに。次に、top of stack演算子を使用して比較を繰り返します。

例に戻ると、次のように機能します。

N = [] Ops = []

  • 読み取り1. N = [1]、Ops = []
  • +を読んでください。N = [1]、Ops = [+]
  • 読み取り2. N = [1 2]、Ops = [+]
  • をお読みください*。N = [1 2]、Ops = [+ *]
  • 読み取り3. N = [1 2 3]、Ops = [+ *]
  • +を読んでください。N = [1 2 3]、Ops = [+ *]
    • 3、2をポップし、2 3を実行して*、結果をNにプッシュします。N= [1 6]、Ops = [+]
    • +は関連付けられたままなので、1、6もポップして+を実行します。N = [7]、Ops = []。
    • 最後に、[+]を演算子スタックにプッシュします。N = [7]、Ops = [+]。
  • 4. N = [7 4]を読み取ります。Ops = [+]。
  • 入力が不足しているため、今すぐスタックを空にしたいとします。その結果、結果が得られます11。

そこは、それほど難しいことではありませんか。また、文法やパーサージェネレーターは呼び出されません。


6
スタックを上に出さずにスタックの2番目のものを見ることができる限り、実際には2つのスタックは必要ありません。代わりに、数値と演算子を交互に使用する単一のスタックを使用できます。これは実際には、LRパーサージェネレーター(bisonなど)の動作に正確に対応しています。
クリス・ドッド

2
私が今実装したアルゴリズムの本当に素晴らしい説明。また、あなたはそれをpostfixに変換していません。これもいいことです。括弧のサポートを追加することも非常に簡単です。
Giorgi

4
シャンティングヤードアルゴリズムの簡略化されたバージョンは、ここにあります:andreinc.net/2010/10/05/…(Javaおよびpythonでの実装を使用)
Andrei Ciobanu

1
これをありがとう、まさに私が求めているものです!
ジョーグリーン

結合について-左について言及していただきありがとうございます。私は三項演算子にこだわっています。「?:」がネストされた複雑な式を解析する方法。どちらも「?」と ':'は同じ優先順位でなければなりません。そして、「?」を解釈すると 右-連想、左:-連想このアルゴリズムは連想的です。また、2つの演算子を折りたたむことができるのは、それらの両方が残っている場合のみです-結合性。
ウラジスラフ

25

http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm

さまざまなアプローチの非常に良い説明:

  • 再帰下降認識
  • 分路ヤードアルゴリズム
  • 古典的なソリューション
  • 優先クライミング

シンプルな言語と疑似コードで書かれています。

「優先登山」が好きです。


リンクが壊れているようです。より適切な答えは、各メソッドを言い換えて、リンクが消えたときに、その有用な情報の一部がここに保持されるようにすることでした。
アダム・ホワイト

18

単純な再帰下降パーサーと演算子優先順位の構文解析の組み合わせについては、ここに素晴らしい記事があります。あなたが最近パーサーを書いているなら、読むことは非常に興味深くそして有益であるべきです。


16

昔、私は独自の構文解析アルゴリズムを作成しましたが、それは構文解析に関する本(Dragon Bookなど)にはありませんでした。シャンティングヤードアルゴリズムへのポインターを見ると、私は類似点を確認しています。

約2年前、http://www.perlmonks.org/?node_id = 554516に、Perlのソースコードを完備した投稿を投稿しました。他の言語への移植は簡単です。私が最初に行った実装はZ80アセンブラでした。

数値を使用した直接計算には理想的ですが、必要に応じてこれを使用して解析ツリーを作成できます。

更新より多くの人がJavascriptを読み取る(または実行する)ことができるため、コードが再編成された後、パーサーをJavascriptに再実装しました。パーサー全体は、エラー報告とコメントを含む5kのJavascriptコード(パーサーの場合は約100行、ラッパー関数の場合は15行)未満です。

ライブデモはhttp://users.telenet.be/bartl/expressionParser/expressionParser.htmlにあります。

// operator table
var ops = {
   '+'  : {op: '+', precedence: 10, assoc: 'L', exec: function(l,r) { return l+r; } },
   '-'  : {op: '-', precedence: 10, assoc: 'L', exec: function(l,r) { return l-r; } },
   '*'  : {op: '*', precedence: 20, assoc: 'L', exec: function(l,r) { return l*r; } },
   '/'  : {op: '/', precedence: 20, assoc: 'L', exec: function(l,r) { return l/r; } },
   '**' : {op: '**', precedence: 30, assoc: 'R', exec: function(l,r) { return Math.pow(l,r); } }
};

// constants or variables
var vars = { e: Math.exp(1), pi: Math.atan2(1,1)*4 };

// input for parsing
// var r = { string: '123.45+33*8', offset: 0 };
// r is passed by reference: any change in r.offset is returned to the caller
// functions return the parsed/calculated value
function parseVal(r) {
    var startOffset = r.offset;
    var value;
    var m;
    // floating point number
    // example of parsing ("lexing") without aid of regular expressions
    value = 0;
    while("0123456789".indexOf(r.string.substr(r.offset, 1)) >= 0 && r.offset < r.string.length) r.offset++;
    if(r.string.substr(r.offset, 1) == ".") {
        r.offset++;
        while("0123456789".indexOf(r.string.substr(r.offset, 1)) >= 0 && r.offset < r.string.length) r.offset++;
    }
    if(r.offset > startOffset) {  // did that work?
        // OK, so I'm lazy...
        return parseFloat(r.string.substr(startOffset, r.offset-startOffset));
    } else if(r.string.substr(r.offset, 1) == "+") {  // unary plus
        r.offset++;
        return parseVal(r);
    } else if(r.string.substr(r.offset, 1) == "-") {  // unary minus
        r.offset++;
        return negate(parseVal(r));
    } else if(r.string.substr(r.offset, 1) == "(") {  // expression in parens
        r.offset++;   // eat "("
        value = parseExpr(r);
        if(r.string.substr(r.offset, 1) == ")") {
            r.offset++;
            return value;
        }
        r.error = "Parsing error: ')' expected";
        throw 'parseError';
    } else if(m = /^[a-z_][a-z0-9_]*/i.exec(r.string.substr(r.offset))) {  // variable/constant name        
        // sorry for the regular expression, but I'm too lazy to manually build a varname lexer
        var name = m[0];  // matched string
        r.offset += name.length;
        if(name in vars) return vars[name];  // I know that thing!
        r.error = "Semantic error: unknown variable '" + name + "'";
        throw 'unknownVar';        
    } else {
        if(r.string.length == r.offset) {
            r.error = 'Parsing error at end of string: value expected';
            throw 'valueMissing';
        } else  {
            r.error = "Parsing error: unrecognized value";
            throw 'valueNotParsed';
        }
    }
}

function negate (value) {
    return -value;
}

function parseOp(r) {
    if(r.string.substr(r.offset,2) == '**') {
        r.offset += 2;
        return ops['**'];
    }
    if("+-*/".indexOf(r.string.substr(r.offset,1)) >= 0)
        return ops[r.string.substr(r.offset++, 1)];
    return null;
}

function parseExpr(r) {
    var stack = [{precedence: 0, assoc: 'L'}];
    var op;
    var value = parseVal(r);  // first value on the left
    for(;;){
        op = parseOp(r) || {precedence: 0, assoc: 'L'}; 
        while(op.precedence < stack[stack.length-1].precedence ||
              (op.precedence == stack[stack.length-1].precedence && op.assoc == 'L')) {  
            // precedence op is too low, calculate with what we've got on the left, first
            var tos = stack.pop();
            if(!tos.exec) return value;  // end  reached
            // do the calculation ("reduce"), producing a new value
            value = tos.exec(tos.value, value);
        }
        // store on stack and continue parsing ("shift")
        stack.push({op: op.op, precedence: op.precedence, assoc: op.assoc, exec: op.exec, value: value});
        value = parseVal(r);  // value on the right
    }
}

function parse (string) {   // wrapper
    var r = {string: string, offset: 0};
    try {
        var value = parseExpr(r);
        if(r.offset < r.string.length){
          r.error = 'Syntax error: junk found at offset ' + r.offset;
            throw 'trailingJunk';
        }
        return value;
    } catch(e) {
        alert(r.error + ' (' + e + '):\n' + r.string.substr(0, r.offset) + '<*>' + r.string.substr(r.offset));
        return;
    }    
}

11

現在解析に使用している文法を説明できれば助かります。問題があるように聞こえるかもしれません!

編集:

文法の質問を理解していないという事実と、「これを手作業で書いた」という事実は、「1 + 11 * 5」という形式の式(つまり、演算子の優先順位)に問題がある理由を説明している可能性が高いです。たとえば、「算術式の文法」をグーグルすると、いくつかの良いポインタが得られるはずです。そのような文法は複雑である必要はありません:

<Exp> ::= <Exp> + <Term> |
          <Exp> - <Term> |
          <Term>

<Term> ::= <Term> * <Factor> |
           <Term> / <Factor> |
           <Factor>

<Factor> ::= x | y | ... |
             ( <Exp> ) |
             - <Factor> |
             <Number>

たとえば、トリックを実行し、いくつかのより複雑な式(たとえば、関数やパワーなど)を処理するために簡単に拡張できます。

たとえば、このスレッドをご覧になることをお勧めします。

文法/構文解析のほとんどすべての導入では、算術式を例として扱います。

文法の使用は、特定のツール(la Yacc、Bisonなど)の使用を意味するものではないことに注意してください。実際、あなたは間違いなくすでに次の文法を使用しています。

<Exp>  :: <Leaf> | <Exp> <Op> <Leaf>

<Op>   :: + | - | * | /

<Leaf> :: <Number> | (<Exp>)

(またはそのようなもの)それを知らずに!


8

ブーストスピリットの使用を考えましたか?これにより、EBNFのような文法をC ++で次のように記述できます。

group       = '(' >> expression >> ')';
factor      = integer | group;
term        = factor >> *(('*' >> factor) | ('/' >> factor));
expression  = term >> *(('+' >> term) | ('-' >> term));

1
+1そして結果は、すべてがブーストの一部です。計算機の文法はここにあります:spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/…。計算機の実装はここにあります:spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/…。そして、ドキュメントはここにあります:spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/doc/…。なぜ人々がまだ独自のミニパーサーを実装しているのか理解できません。
ステファン、2009年

5

あなたが質問をするとき、再帰はまったく必要ありません。答えは3つです。Postfix表記とシャンティングヤードアルゴリズムとPostfix式の評価:

1)。Postfix表記=明示的な優先指定の必要性を排除するために発明されました。ネットで詳細を読むが、その要点は次のとおりです。インフィックス式(1 + 2)* 3人間が読みやすく、機械による計算ではあまり効率的ではない処理。とは?「式を優先してキャッシングして式を書き直し、常に左から右に処理する」という単純なルール。したがって、下付き(1 + 2)* 3は、後置12 + 3 *になります。演算子は常にオペランドの後に配置されるため、POST。

2)。postfix式を評価しています。簡単です。後置文字列から数値を読み取ります。オペレーターが見えるまでそれらをスタックにプッシュします。演算子のタイプをチェック-単項?バイナリ?三次?この演算子を評価するために必要な数のオペランドをスタックからポップします。評価する。結果をスタックに戻します!そして、あなたはほとんど終わりました。スタックに1つのエントリ=探している値urしかなくなるまで、これを続けます。

やってみましょう(1 + 2)* 3は接尾辞にある "12 + 3 *" 最初の数値= 1を読み取ります。それをスタックにプッシュします。次をお読みください。数値=2。スタックにプッシュします。次をお読みください。オペレーター。どれ?+。何?Binary =には2つのオペランドが必要です。スタックを2回ポップ= argrightは2、argleftは1。1+ 2は3。スタックに3を押し戻す。次をpostfix文字列から読み取ります。その数。3.押す。次をお読みください。オペレーター。どれ?*。何?Binary = 2つの数値が必要->スタックを2回ポップ。最初にargrightにポップし、次にargleftにポップします。演算を評価-3の3は9です。9をスタックにプッシュします。次の接尾文字を読みます。それはnullです。入力の終わり。ポップスタックonec =それがあなたの答えです。

3)。シャンティングヤードは、人間の(簡単に)読み取り可能な中置式を後置式に変換するために使用されます(これも、何らかの練習をすれば人間が容易に読み取れる)。手動で簡単にコーディングできます。上記のコメントとネットを参照してください。


4

使いたい言語はありますか? ANTLRを使用すると、Javaの観点からこれを行うことができます。Adrian Kuhnは、Rubyで実行可能な文法を書く方法について優れた記事を書いています。実際、彼の例はほぼ正確にあなたの算術式の例です。


私はブログの投稿で与えられた私の例が左再帰の誤りを起こしていることを認めなければなりません。すなわち、a-b-cは((a -b)-c)ではなく(a-(b -c))に評価されます。実際、ブログの投稿を修正する必要があるというToDoを追加することを思い出します。
akuhn 2009年

4

それはあなたがそれがいかに「一般的」になりたいかに依存します。

sin(4 + 5)* cos(7 ^ 3)のように数学関数を解析できるなど、本当に一般的なものにしたい場合は、おそらく解析ツリーが必要になります。

ここでは、完全な実装をここに貼り付けるのは適切だとは思いません。悪名高い「ドラゴンブック」をチェックすることをお勧めします。

しかし、優先順位のサポートだけが必要な場合は、最初に式をpostfix形式に変換して、コピーアンドペーストできるアルゴリズムをgoogleから利用できるようにするか、バイナリで自分でコーディングできると思います木。

スタックがどのように役立つかをすでに理解しているので、それをpostfix形式で持っていると、それ以降はそれは簡単なことです。


ドラゴンブックは式エバリュエーターにとって少し過剰かもしれません-必要なのは単純な再帰降下パーサーだけですが、コンパイラーでもっと広範囲なことをしたい場合は必読です。
Eclipse

1
うわー-「ドラゴンブック」がまだ議論されているのを知ってうれしいです。私はそれを30年前の大学で勉強し、それをすべて読んだことを覚えています。
Schroedingers Cat

4

私は、シャンティングヤードアルゴリズムをだまして使用することをお勧めします。これは、単純な計算機タイプのパーサーを作成する簡単な方法であり、優先順位が考慮されます。

物事を適切にトークン化し、変数などを使用したい場合は、ここで他の人が提案する再帰降下パーサーを作成しますが、計算機スタイルのパーサーが必要な場合は、このアルゴリズムで十分です:-)


4

これは、シャンティングヤードアルゴリズムに関するPIClistで見つかりました。

ハロルドは書いています:

評価を簡単にするために代数式をRPNに変換するアルゴリズムをずっと昔に読んだことを覚えています。各中置値または演算子または括弧は、線路上の鉄道車両によって表されました。あるタイプの車は別のトラックに分かれ、もう一方はまっすぐ進みました。詳細は思い出せませんが(明らかに!)、コードを書くのは面白いといつも思っていました。これは、6800(ではなく68000)のアセンブリコードを書いたときに戻ってきました。

これは「回避ヤードのアルゴリズム」であり、ほとんどのマシンパーサーで使用されています。Wikipediaの解析に関する記事を参照してください。分路ヤードのアルゴリズムをコーディングする簡単な方法は、2つのスタックを使用することです。1つは「プッシュ」スタックで、もう1つは「削減」または「結果」スタックです。例:

pstack =()//空のrstack =()入力:1 + 2 * 3 precedence = 10 //最小の削減= 0 //削減しない

start:token '1':isnumber、put in pstack(push)token '+':isoperator set precedence = 2 if precedence <previous_operator_precedence then reduce()//下記を参照してください '+' in pstack(push)token '2' :isnumber、put in pstack(push)token '*':isoperator、put precedence = 1、put in pstack(push)// check precedence as // above token '3':isnumber、put in pstack(push)end of of入力、削減する必要があります(目標は空のpstack)reduce()//完了

要素を削減し、プッシュスタックからポップして結果スタックに配置するには、 'operator' 'number'の形式の場合、常にpstackの上位2項目を交換します。

pstack: '1' '+' '2' ' ' '3' rstack:()... pstack:()rstack: '3' '2' ' ' '1' '+'

式が次のようになった場合:

1 * 2 + 3

次に、reduceトリガーは、すでにプッシュされている「*」よりも優先順位が低いトークン「+」を読み取ることになるため、次のようになります。

pstack: '1' ' ' '2' rstack:()... pstack:()rstack: '1' '2' ' '

そして「+」を押し、次に「3」を押して、最後に削減します:

pstack: '+' '3' rstack: '1' '2' ' ' ... pstack:()rstack: '1' '2' ' ' '3' '+'

したがって、短いバージョンは次のとおりです。プッシュ番号。プッシュ演算子は、前の演算子の優先順位を確認します。現在プッシュされている演算子よりも高かった場合は、まず削減してから、現在の演算子をプッシュします。括弧を処理するには、単純に「前の」演算子の優先順位を保存し、括弧ペアの内側を解決するときに削減アルゴリズムが削減を停止するように指示するマークをpstackに付けます。閉じ括弧は、入力の終了と同様に削減をトリガーし、開いた括弧マークをpstackから削除し、「前の操作」の優先順位を復元して、閉じた括弧の後に解析を続行できるようにします。これは、再帰を使用して、または使用せずに実行できます(ヒント:スタックを使用して、 '(' ... これの一般化されたバージョンは、パーサージェネレーターを実装した分路ヤードのアルゴリズムf.exを使用することです。yaccまたはbisonまたはtaccle(yaccのtclアナログ)を使用します。

ピーター

-アダム


4

優先順位解析のもう1つのリソースは、WikipediaのOperator-precedence parserエントリです。ダイクストラの回避ヤードアルゴリズムと代替ツリーアルゴリズムをカバーしますが、特に、優先度を知らないパーサーの前で簡単に実装できる非常に単純なマクロ置換アルゴリズムをカバーします。

#include <stdio.h>
int main(int argc, char *argv[]){
  printf("((((");
  for(int i=1;i!=argc;i++){
    if(argv[i] && !argv[i][1]){
      switch(argv[i]){
      case '^': printf(")^("); continue;
      case '*': printf("))*(("); continue;
      case '/': printf("))/(("); continue;
      case '+': printf(")))+((("); continue;
      case '-': printf(")))-((("); continue;
      }
    }
    printf("%s", argv[i]);
  }
  printf("))))\n");
  return 0;
}

次のように呼び出します。

$ cc -o parenthesise parenthesise.c
$ ./parenthesise a \* b + c ^ d / e
((((a))*((b)))+(((c)^(d))/((e))))

これは、その単純さの点で優れており、非常に理解できます。


3
それはとても素敵な小さな真珠です。しかし、それを拡張すると(たとえば、関数の適用、暗黙的な乗算、前置演算子と後置演算子、オプションの型注釈など)、全体が壊れてしまいます。つまり、エレガントなハックです。
Jared Updike、

要点はわかりません。これは、演算子優先順位解析の問題をかっこ優先順位解析の問題に変更するだけです。
ローン侯爵、2015年

@EJPは確かですが、問題のパーサーはかっこを問題なく処理するため、これは合理的な解決策です。ただし、パーサーがない場合は、問題が別の領域に移動するだけです。
アダムデービス

4

超コンパクト(1クラス、<10 KiB)Java Math Evaluatorのソースを自分のWebサイトに投稿しました。これは、受け入れられた回答のポスターの頭蓋爆発を引き起こしたタイプの再帰降下パーサーです。

完全な優先順位、括弧、名前付き変数、単一引数関数をサポートしています。




2

私は現在、一連の記事に取り組んでおり、デザインパターンと読みやすいプログラミングの学習ツールとして正規表現パーサーを構築しています。あなたはreadablecodeを見ることができます。記事では、シャンティングヤードアルゴリズムの明確な使用法を紹介しています。


2

私は、F#で式パーサーを作成し、それについてここでブログに投稿しました。シャンティングヤードアルゴリズムを使用しますが、インフィックスからRPNに変換する代わりに、計算結果を蓄積するために2つ目のスタックを追加しました。演算子の優先順位を正しく処理しますが、単項演算子はサポートしていません。これは、F#を学ぶために書いたもので、式の構文解析を学ぶためのものではありません。


2

pyparsingを使用したPythonソリューションはここにあります。優先順位の高いさまざまな演算子を使用した中置表記法の解析はかなり一般的であるため、pyparsingにはinfixNotation(以前のoperatorPrecedence)式ビルダーも含まれています。これを使用すると、「AND」、「OR」、「NOT」などを使用してブール式を簡単に定義できます。または、4関数演算を拡張して、!などの他の演算子を使用することもできます。階乗の場合、または係数の場合は '%'、または順列と組み合わせを計算するためにPおよびC演算子を追加します。'-1'または 'T'演算子(反転および転置用)の処理を含む、マトリックス表記用のインフィックスパーサーを作成できます。4関数パーサーのoperatorPrecedenceの例( '!'


1

私はこれが遅い答えであることを知っていますが、すべての演算子(prefix、postfix、infix-left、infix-right、nonassociative)が任意の優先順位を持つことを可能にする小さなパーサーを書いたところです。

これを任意のDSLサポートを備えた言語に拡張するつもりですが、演算子の優先順位にカスタムパーサーが不要で、テーブルをまったく必要としない汎用パーサーを使用できることを指摘したいと思います。表示される各演算子の優先順位を調べるだけです。人々は、違法な入力を受け入れることができるカスタムプラットパーサーまたはシャンティングヤードパーサーについて言及しています。これはカスタマイズする必要がなく、(バグがない限り)悪い入力を受け入れません。ある意味では完全ではありません。アルゴリズムをテストするために作成され、その入力は前処理が必要な形式ですが、それを明確にするコメントがあります。

いくつかの一般的な種類の演算子が欠落していることに注意してください。たとえば、インデックス付けに使用される演算子の種類、つまりtable [index]または関数function(parameter-expression、...)を呼び出します。これらを追加しますが、両方とも後置と考えます区切り文字「[」と「]」または「(」と「)」の間にあるものは、式パーサーの別のインスタンスで解析されます。省略して申し訳ありませんが、後置部分が含まれています-残りを追加すると、おそらくコードのサイズがほぼ2倍になります。

パーサーは100行のラケットコードなので、おそらくここに貼り付けるだけでよいので、stackoverflowが許可する長さより長くないことを願っています。

任意の決定に関するいくつかの詳細:

優先順位の低い後置演算子が優先順位の低い前置演算子と同じ中置ブロックを求めて競合している場合、前置演算子が優先されます。ほとんどの言語では優先順位の低い後置演算子がないため、これはほとんどの言語では発生しません。-例えば:((データa)(左1 +)(前2なし)(データb)(投稿3!)(左1 +)(データc))はa + not b!+ cであり、ここでnotはa前置演算子と!は後置演算子であり、どちらも+よりも優先順位が低いため、(a + not b!)+ cまたはa +(not b!+ c)として互換性のない方法でグループ化したい場合は、前置演算子が常に優先されるため、 2番目はそれが解析する方法です

非連想的な中置演算子は実際に存在するため、意味のある型とは異なる型を返す演算子を一緒に解釈する必要はありませんが、それぞれに異なる式の型がないと簡単ではありません。そのため、このアルゴリズムでは、非結合演算子は、それ自体だけでなく、同じ優先順位の演算子との関連付けを拒否します。<<= ==> =などはほとんどの言語で互いに関連付けられないため、これは一般的なケースです。

異なる種類の演算子(左、接頭辞など)がどのようにして優先順位に関係を破るのかという問題は、異なるタイプの演算子に同じ優先順位を与えることは実際には意味がないため、出てはなりません。このアルゴリズムはそれらの場合に何かをします、しかし、そのような文法はそもそも悪い考えなので、私は正確に何を理解することさえ気にしません。

#lang racket
;cool the algorithm fits in 100 lines!
(define MIN-PREC -10000)
;format (pre prec name) (left prec name) (right prec name) (nonassoc prec name) (post prec name) (data name) (grouped exp)
;for example "not a*-7+5 < b*b or c >= 4"
;which groups as: not ((((a*(-7))+5) < (b*b)) or (c >= 4))"
;is represented as '((pre 0 not)(data a)(left 4 *)(pre 5 -)(data 7)(left 3 +)(data 5)(nonassoc 2 <)(data b)(left 4 *)(data b)(right 1 or)(data c)(nonassoc 2 >=)(data 4)) 
;higher numbers are higher precedence
;"(a+b)*c" is represented as ((grouped (data a)(left 3 +)(data b))(left 4 *)(data c))

(struct prec-parse ([data-stack #:mutable #:auto]
                    [op-stack #:mutable #:auto])
  #:auto-value '())

(define (pop-data stacks)
  (let [(data (car (prec-parse-data-stack stacks)))]
    (set-prec-parse-data-stack! stacks (cdr (prec-parse-data-stack stacks)))
    data))

(define (pop-op stacks)
  (let [(op (car (prec-parse-op-stack stacks)))]
    (set-prec-parse-op-stack! stacks (cdr (prec-parse-op-stack stacks)))
    op))

(define (push-data! stacks data)
    (set-prec-parse-data-stack! stacks (cons data (prec-parse-data-stack stacks))))

(define (push-op! stacks op)
    (set-prec-parse-op-stack! stacks (cons op (prec-parse-op-stack stacks))))

(define (process-prec min-prec stacks)
  (let [(op-stack (prec-parse-op-stack stacks))]
    (cond ((not (null? op-stack))
           (let [(op (car op-stack))]
             (cond ((>= (cadr op) min-prec) 
                    (apply-op op stacks)
                    (set-prec-parse-op-stack! stacks (cdr op-stack))
                    (process-prec min-prec stacks))))))))

(define (process-nonassoc min-prec stacks)
  (let [(op-stack (prec-parse-op-stack stacks))]
    (cond ((not (null? op-stack))
           (let [(op (car op-stack))]
             (cond ((> (cadr op) min-prec) 
                    (apply-op op stacks)
                    (set-prec-parse-op-stack! stacks (cdr op-stack))
                    (process-nonassoc min-prec stacks))
                   ((= (cadr op) min-prec) (error "multiply applied non-associative operator"))
                   ))))))

(define (apply-op op stacks)
  (let [(op-type (car op))]
    (cond ((eq? op-type 'post)
           (push-data! stacks `(,op ,(pop-data stacks) )))
          (else ;assume infix
           (let [(tos (pop-data stacks))]
             (push-data! stacks `(,op ,(pop-data stacks) ,tos))))))) 

(define (finish input min-prec stacks)
  (process-prec min-prec stacks)
  input
  )

(define (post input min-prec stacks)
  (if (null? input) (finish input min-prec stacks)
      (let* [(cur (car input))
             (input-type (car cur))]
        (cond ((eq? input-type 'post)
               (cond ((< (cadr cur) min-prec)
                      (finish input min-prec stacks))
                     (else 
                      (process-prec (cadr cur)stacks)
                      (push-data! stacks (cons cur (list (pop-data stacks))))
                      (post (cdr input) min-prec stacks))))
              (else (let [(handle-infix (lambda (proc-fn inc)
                                          (cond ((< (cadr cur) min-prec)
                                                 (finish input min-prec stacks))
                                                (else 
                                                 (proc-fn (+ inc (cadr cur)) stacks)
                                                 (push-op! stacks cur)
                                                 (start (cdr input) min-prec stacks)))))]
                      (cond ((eq? input-type 'left) (handle-infix process-prec 0))
                            ((eq? input-type 'right) (handle-infix process-prec 1))
                            ((eq? input-type 'nonassoc) (handle-infix process-nonassoc 0))
                            (else error "post op, infix op or end of expression expected here"))))))))

;alters the stacks and returns the input
(define (start input min-prec stacks)
  (if (null? input) (error "expression expected")
      (let* [(cur (car input))
             (input-type (car cur))]
        (set! input (cdr input))
        ;pre could clearly work with new stacks, but could it reuse the current one?
        (cond ((eq? input-type 'pre)
               (let [(new-stack (prec-parse))]
                 (set! input (start input (cadr cur) new-stack))
                 (push-data! stacks 
                             (cons cur (list (pop-data new-stack))))
                 ;we might want to assert here that the cdr of the new stack is null
                 (post input min-prec stacks)))
              ((eq? input-type 'data)
               (push-data! stacks cur)
               (post input min-prec stacks))
              ((eq? input-type 'grouped)
               (let [(new-stack (prec-parse))]
                 (start (cdr cur) MIN-PREC new-stack)
                 (push-data! stacks (pop-data new-stack)))
               ;we might want to assert here that the cdr of the new stack is null
               (post input min-prec stacks))
              (else (error "bad input"))))))

(define (op-parse input)
  (let [(stacks (prec-parse))]
    (start input MIN-PREC stacks)
    (pop-data stacks)))

(define (main)
  (op-parse (read)))

(main)

1

これは、Javaで記述された単純なケース再帰ソリューションです。負の数は処理しませんが、必要に応じて追加できます。

public class ExpressionParser {

public double eval(String exp){
    int bracketCounter = 0;
    int operatorIndex = -1;

    for(int i=0; i<exp.length(); i++){
        char c = exp.charAt(i);
        if(c == '(') bracketCounter++;
        else if(c == ')') bracketCounter--;
        else if((c == '+' || c == '-') && bracketCounter == 0){
            operatorIndex = i;
            break;
        }
        else if((c == '*' || c == '/') && bracketCounter == 0 && operatorIndex < 0){
            operatorIndex = i;
        }
    }
    if(operatorIndex < 0){
        exp = exp.trim();
        if(exp.charAt(0) == '(' && exp.charAt(exp.length()-1) == ')')
            return eval(exp.substring(1, exp.length()-1));
        else
            return Double.parseDouble(exp);
    }
    else{
        switch(exp.charAt(operatorIndex)){
            case '+':
                return eval(exp.substring(0, operatorIndex)) + eval(exp.substring(operatorIndex+1));
            case '-':
                return eval(exp.substring(0, operatorIndex)) - eval(exp.substring(operatorIndex+1));
            case '*':
                return eval(exp.substring(0, operatorIndex)) * eval(exp.substring(operatorIndex+1));
            case '/':
                return eval(exp.substring(0, operatorIndex)) / eval(exp.substring(operatorIndex+1));
        }
    }
    return 0;
}

}


1

アルゴリズムは、再帰的降下パーサーとしてCで簡単にエンコードできます。

#include <stdio.h>
#include <ctype.h>

/*
 *  expression -> sum
 *  sum -> product | product "+" sum
 *  product -> term | term "*" product
 *  term -> number | expression
 *  number -> [0..9]+
 */

typedef struct {
    int value;
    const char* context;
} expression_t;

expression_t expression(int value, const char* context) {
    return (expression_t) { value, context };
}

/* begin: parsers */

expression_t eval_expression(const char* symbols);

expression_t eval_number(const char* symbols) {
    // number -> [0..9]+
    double number = 0;        
    while (isdigit(*symbols)) {
        number = 10 * number + (*symbols - '0');
        symbols++;
    }
    return expression(number, symbols);
}

expression_t eval_term(const char* symbols) {
    // term -> number | expression
    expression_t number = eval_number(symbols);
    return number.context != symbols ? number : eval_expression(symbols);
}

expression_t eval_product(const char* symbols) {
    // product -> term | term "*" product
    expression_t term = eval_term(symbols);
    if (*term.context != '*')
        return term;

    expression_t product = eval_product(term.context + 1);
    return expression(term.value * product.value, product.context);
}

expression_t eval_sum(const char* symbols) {
    // sum -> product | product "+" sum
    expression_t product = eval_product(symbols);
    if (*product.context != '+')
        return product;

    expression_t sum = eval_sum(product.context + 1);
    return expression(product.value + sum.value, sum.context);
}

expression_t eval_expression(const char* symbols) {
    // expression -> sum
    return eval_sum(symbols);
}

/* end: parsers */

int main() {
    const char* expression = "1+11*5";
    printf("eval(\"%s\") == %d\n", expression, eval_expression(expression).value);

    return 0;
}

次のライブラリは便利かもしれません: yupana-厳密に算術演算; tinyexpr-算術演算+ C数学関数+ユーザーが提供する関数。 mpc-パーサーコンビネーター

説明

代数式を表す記号のシーケンスをキャプチャしてみましょう。最初の1つは数値です。これは10進数で1回以上繰り返されます。 そのような表記をプロダクションルールと呼びます。

number -> [0..9]+

オペランド付きの加算演算子は別のルールです。シーケンスnumberを表すのはいずれかまたは任意の記号ですsum "*" sum

sum -> number | sum "+" sum

代わりnumberに拡張して、最終的に正しい加算式に縮小sum "+" sumできるnumber "+" numberものに置き換えてみてください。[0..9]+ "+" [0..9]+1+8

他の置換でも正しい式が生成されます:sum "+" sum-> number "+" sum-> number "+" sum "+" sum-> number "+" sum "+" number-> number "+" number "+" number->12+3+5

少しずつ、可能なすべての代数式を表現する一連のプロダクションルール(別名文法)に似ています。

expression -> sum
sum -> difference | difference "+" sum
difference -> product | difference "-" product
product -> fraction | fraction "*" product
fraction -> term | fraction "/" term
term -> "(" expression ")" | number
number -> digit+                                                                    

演算子の優先順位を制御するには、その生成規則の位置を他のものに対して変更します。上記の文法やノートを見ては、生産ルールがためという*の下に配置され+、この意志力productの前に評価しますsum。実装では、パターン認識と評価を組み合わせるだけなので、生産ルールを厳密に反映します。

expression_t eval_product(const char* symbols) {
    // product -> term | term "*" product
    expression_t term = eval_term(symbols);
    if (*term.context != '*')
        return term;

    expression_t product = eval_product(term.context + 1);
    return expression(term.value * product.value, product.context);
}

ここで、term最初に評価し、プロダクションルールで選択されたまま*になっている文字がない場合はそれを返します。それ以外の場合は、その後にシンボルを評価し、プロダクションルールで選択された文字を返しterm.value * product.value ます。term "*" product

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.