Öncelikli denklem (ifade) ayrıştırıcısı?


105

İkili (+, -, |, &, *, /, vb.) Operatörleri, tekli (!) Operatörleri ve parantezleri işleyecek basit bir yığın algoritması kullanarak bir denklem ayrıştırıcısı geliştirdim.

Bununla birlikte, bu yöntemi kullanmak bana her şeyin aynı önceliğe sahip olmasını sağlar - operatörden bağımsız olarak soldan sağa değerlendirilir, ancak öncelik parantez kullanılarak zorlanabilir.

Yani şu anda "1 + 11 * 5", beklendiği gibi 56 değil, 60 değerini döndürüyor.

Bu mevcut proje için uygun olsa da, daha sonraki projeler için kullanabileceğim genel amaçlı bir rutine sahip olmak istiyorum.

Netlik sağlamak için düzenlendi:

Öncelikli denklemleri ayrıştırmak için iyi bir algoritma nedir?

Uygulanması basit bir şeyle ilgileniyorum ve mevcut kodla ilgili lisans sorunlarını önlemek için kendimi kodlayabileceğimi anlıyorum.

Dilbilgisi:

Dilbilgisi sorusunu anlamıyorum - bunu elle yazdım. YACC veya Bison'a ihtiyaç duymadığım kadar basit. Sadece "2 + 3 * (42/13)" gibi denklemlerle dizgeleri hesaplamam gerekiyor.

Dil:

Bunu C'de yapıyorum, ancak dile özgü bir çözümle değil, bir algoritmayla ilgileniyorum. C, ihtiyaç duyulduğunda başka bir dile dönüştürmenin kolay olacağı kadar düşük düzeydedir.

Kod Örneği

Yukarıda bahsettiğim basit ifade ayrıştırıcısının test kodunu gönderdim . Proje gereksinimleri değişti ve bu nedenle, projeye dahil edilmediğinden kodu performans veya alan için optimize etmeye hiç ihtiyaç duymadım. Orijinal ayrıntılı biçimde ve kolayca anlaşılabilir olmalıdır. Operatör önceliği açısından onunla daha fazla bir şey yaparsam, muhtemelen makro kesmeyi seçeceğim çünkü programın geri kalanıyla basitçe eşleşiyor. Bunu gerçek bir projede kullanırsam, daha kompakt / hızlı bir ayrıştırıcıya gideceğim.

İlgili soru

Bir matematik çözümleyicinin akıllı tasarımı?

-Adam


Blogumda C # ile bir ifade ayrıştırıcısı yazdım. Manevra sahası algoritmasında yığın olmadan postfix için infix yapar. Yalnızca bir dizi kullanır.
Guge

Anladığım kadarıyla sadece aritmetik ifadeyi çözümlemeniz gerekiyor. Ters Lehçe Gösterimi
mishadoff

Yanıtlar:


69

Zor yol

Özyinelemeli bir iniş ayrıştırıcısı istiyorsunuz .

Öncelik elde etmek için, örneğin örnek dizenizi kullanarak özyinelemeli düşünmeniz gerekir.

1+11*5

bunu elle yapmak için 1, okumanız , sonra artıyı görmeniz ve 11... ile başlayan yepyeni bir özyinelemeli ayrıştırma "oturumu" başlatmanız ve 11 * 5bunu kendi faktörüne ayırıp bir ayrıştırma ağacı oluşturduğunuzdan emin olmanız gerekir 1 + (11 * 5).

Tüm bunlar, özellikle C'nin ek güçsüzlüğüyle, açıklamaya çalışmak çok acı verici. Bakın, 11'i ayrıştırdıktan sonra, eğer * gerçekten bir + ise, bir terim oluşturma girişiminden vazgeçmeniz ve bunun yerine ayrıştırmanız gerekir. 11kendisi bir faktör olarak. Kafam zaten patlıyor. Yinelemeli düzgün strateji ile mümkündür, ancak daha iyi bir yol var ...

Kolay (doğru) yol

Bison gibi bir GPL aracı kullanıyorsanız, muhtemelen bison tarafından üretilen C kodu GPL tarafından kapsanmadığından lisans sorunları hakkında endişelenmenize gerek yoktur (IANAL ancak GPL araçlarının GPL'yi oluşturulan kod / ikili dosyalar; örneğin Apple, GCC ile Aperture gibi bir kod derler ve söz konusu kodu GPL'ye gerek kalmadan satarlar).

Bison'u (veya eşdeğer bir şey, ANTLR vb.) İndirin .

Genellikle bison çalıştırıp bu dört işlevli hesap makinesini gösteren istediğiniz C kodunu alabileceğiniz bazı örnek kodlar vardır:

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

Oluşturulan koda bakın ve bunun göründüğü kadar kolay olmadığını görün. Ayrıca, Bison gibi bir araç kullanmanın avantajları şunlardır: 1) bir şeyler öğrenirsiniz (özellikle Dragon kitabını okursanız ve gramerler hakkında bilgi alırsanız), 2) NIH'nin tekerleği yeniden icat etmeye çalışmasından kaçınırsınız . Gerçek bir ayrıştırıcı-oluşturucu aracıyla, aslında daha sonra ölçeklendirme konusunda bir ümidiniz olur ve diğer insanlara ayrıştırıcıların ayrıştırma araçlarının alanı olduğunu bildiğinizi gösterir.


Güncelleme:

Buradaki insanlar çok sağlam tavsiyeler verdiler. Ayrıştırma araçlarını atlamaya veya sadece Shunting Yard algoritmasını veya elle yuvarlanmış özyinelemeli düzgün ayrıştırıcıyı kullanmaya karşı tek uyarım, küçük oyuncak dillerin 1 bir gün işlevler (sin, cos, log) ve değişkenler, koşullar ve koşullar ile büyük gerçek dillere dönüşebileceğidir. döngüler.

Flex / Bison, küçük, basit bir yorumlayıcı için çok iyi olabilir, ancak bir defaya mahsus ayrıştırıcı + değerlendirici, değişikliklerin yapılması veya özelliklerin eklenmesi gerektiğinde hattın aşağısında sorunlara neden olabilir. Durumunuz değişecek ve muhakemenizi kullanmanız gerekecek; sadece günahlarınız için başkalarını cezalandırmayın [2] ve gereğinden az bir alet geliştirin.

Ayrıştırma için en sevdiğim araç

İş için dünyadaki en iyi araç, Haskell programlama dili ile birlikte gelen Parsec kitaplığıdır (özyinelemeli düzgün ayrıştırıcılar için). BNF'ye çok benziyor veya ayrıştırma için özel bir araç veya alana özgü bir dil gibi görünüyor (örnek kod [3]), ancak aslında Haskell'de normal bir kitaplıktır, yani diğerleriyle aynı derleme adımında derlenir. Haskell kodunuzu değiştirebilir ve rastgele Haskell kodu yazabilir ve onu ayrıştırıcınızda çağırabilir ve diğer kitaplıkları aynı kodda karıştırabilir ve eşleştirebilirsiniz . (Bunun gibi bir ayrıştırma dilini Haskell dışında bir dilde gömmek, bu arada bir sürü sözdizimsel bozukluğa neden olur. Bunu C # ile yaptım ve oldukça iyi çalışıyor ama o kadar güzel ve kısa değil.)

Notlar:

1 Richard Stallman, Neden Tcl kullanmamalısınız?

Emacs'ın temel dersi, bir uzantı dilinin yalnızca bir "uzantı dili" olmaması gerektiğidir. Önemli programları yazmak ve sürdürmek için tasarlanmış gerçek bir programlama dili olmalıdır. Çünkü insanlar bunu yapmak isteyecek!

[2] Evet, bu "dili" kullanmaktan sonsuza kadar yaralandım.

Ayrıca, bu girişi gönderdiğimde önizlemenin doğru olduğunu, ancak SO'ların ilk paragraftaki yakın bağlantı etiketimi yediğini ve ayrıştırıcıların önemsiz bir şey olmadığını kanıtladığını unutmayın, çünkü normal ifadeler kullanırsanız ve tek seferlik saldırılar yaparsanız muhtemelen ince ve küçük bir şeyi yanlış anlayacaktır .

[3] Parsec kullanan bir Haskell ayrıştırıcısının parçacığı: üsler, parantezler, çarpma için boşluklar ve sabitler (pi ve e gibi) ile genişletilmiş dört işlevli bir hesap makinesi.

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
Demek istediğimi vurgulamak için, yazımdaki işaretlemenin doğru bir şekilde ayrıştırılmadığını unutmayın (ve bu, statik olarak oluşturulan işaretleme ile WMD önizlemesinde oluşturulan arasında değişir). Düzeltmek için birkaç deneme yapıldı ama bence PARSER YANLIŞ. Herkese bir iyilik yapın ve ayrıştırmayı doğru yapın!
Jared Updike

155

Manevra yarda algoritması bunun için doğru bir araçtır. Wikipedia bu konuda gerçekten kafa karıştırıcı, ancak temelde algoritma şu şekilde çalışıyor:

Diyelim ki 1 + 2 * 3 + 4'ü değerlendirmek istiyorsunuz. Sezgisel olarak, önce 2 * 3'ü yapmanız gerektiğini "biliyorsunuz", ama bu sonucu nasıl elde edersiniz? Önemli olan, dizeyi soldan sağa tararken, onu izleyen operatör daha düşük (veya buna eşit) bir önceliğe sahip olduğunda bir operatörü değerlendireceğinizi fark etmektir. Örnek bağlamında, yapmak istediğiniz şudur:

  1. Şuna bakın: 1 + 2, hiçbir şey yapma.
  2. Şimdi 1 + 2 * 3'e bakın, yine de hiçbir şey yapmayın.
  3. Şimdi 1 + 2 * 3 + 4'e bakın, artık 2 * 3'ün değerlendirilmesi gerektiğini biliyorsunuz çünkü sonraki operatör daha düşük önceliğe sahip.

Bunu nasıl uyguluyorsunuz?

Biri sayılar ve diğeri operatörler için olmak üzere iki yığınınız olmasını istiyorsunuz. Sayıları her zaman yığına aktarırsınız. Her yeni operatörü yığının en üstünde olanla karşılaştırırsınız, yığının tepesindeki operatör daha yüksek önceliğe sahipse, operatör yığınından çıkarırsınız, işlenenleri sayı yığınından çıkarırsınız, operatörü uygular ve sonucu itersiniz sayı yığını üzerine. Şimdi karşılaştırmayı yığının tepesi operatörüyle tekrar edeceksiniz.

Örneğe dönersek, şu şekilde çalışır:

N = [] İşlemler = []

  • Okuyun 1. N = [1], Ops = []
  • + Okuyun. N = [1], Ops = [+]
  • Oku 2. N = [1 2], Ops = [+]
  • Okuyun *. N = [1 2], Ops = [+ *]
  • Oku 3. N = [1 2 3], Ops = [+ *]
  • + Okuyun. N = [1 2 3], Ops = [+ *]
    • Pop 3, 2 ve 2 *3'ü yürütün ve sonucu N'ye aktarın N = [1 6], Ops = [+]
    • +ilişkisel olarak bırakılır, bu nedenle 1, 6'yı da kapatmak ve +. N = [7], Ops = [].
    • Son olarak [+] 'yı operatör yığınının üzerine itin. N = [7], Ops = [+].
  • 4. N = [7 4] okuyun. İşlemler = [+].
  • Girişiniz bitti, bu yüzden yığınları şimdi boşaltmak istiyorsunuz. Bunun üzerine sonucu alacaksınız 11.

Orada, bu o kadar zor değil, değil mi? Ve herhangi bir gramer veya ayrıştırıcı oluşturucuya çağrı yapmaz.


6
Yığın üzerindeki ikinci şeyi tepesini patlatmadan görebildiğiniz sürece aslında iki yığına ihtiyacınız yoktur. Bunun yerine, sayıları ve operatörleri değiştiren tek bir yığın kullanabilirsiniz. Bu aslında bir LR ayrıştırıcı üretecinin (örneğin bizon) yaptığı şeye tam olarak karşılık gelir.
Chris Dodd

2
Şu anda uyguladığım algoritmanın gerçekten güzel açıklaması. Ayrıca onu postfix'e dönüştürmüyorsunuz ki bu da güzel. Parantez için destek eklemek de çok kolaydır.
Giorgi

4
Manevra yarda algoritmasının basitleştirilmiş bir versiyonu burada bulunabilir: andreinc.net/2010/10/05/… (Java ve python uygulamalarıyla)
Andrei Ciobanu

1
Bunun için teşekkürler, tam da peşinde olduğum şey!
Joe Green

Sol çağrışımdan bahsettiğiniz için çok teşekkür ederiz. Üçlü operatörle kaldım: karmaşık ifadeler iç içe geçmiş "?:" İle nasıl ayrıştırılır. Her ikisinin de '?' ve ':' aynı önceliğe sahip olmalıdır. Ve '?' sağ - ilişkisel ve ':' sol - ilişkisel olarak bu algoritma onlarla çok iyi çalışıyor. Ayrıca, 2 operatörü yalnızca ikisi de ilişkisel bırakıldığında daraltabiliriz.
Vladislav

25

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

Farklı yaklaşımların çok iyi açıklaması:

  • Özyinelemeli iniş tanıma
  • Manevra sahası algoritması
  • Klasik çözüm
  • Öncelik tırmanışı

Basit bir dille ve sözde kodla yazılmıştır.

"Öncelik tırmanışı" nı seviyorum.


Bağlantı kopmuş görünüyor. Daha iyi bir cevap olabilecek şey, her yöntemi yeniden yorumlamak olurdu, böylece bu bağlantı ortadan kalktığında, bu yararlı bilgilerin bir kısmı burada korunmuş olacaktı.
Adam White

18

Basit bir özyinelemeli ayrıştırıcıyı operatör öncelikli ayrıştırmayla birleştirmekle ilgili güzel bir makale var . Yakın zamanda ayrıştırıcılar yazıyorsanız, okumak çok ilginç ve öğretici olmalıdır.


16

Uzun zaman önce, ayrıştırmayla ilgili hiçbir kitapta bulamadığım kendi ayrıştırma algoritmamı oluşturdum (Dragon Book gibi). Shunting Yard algoritmasının işaretçilerine baktığımda, benzerliği görüyorum.

Yaklaşık 2 yıl önce, http://www.perlmonks.org/?node_id=554516 adresinde Perl kaynak koduyla tamamlanmış bir gönderi yaptım . Diğer dillere taşımak kolaydır: Yaptığım ilk uygulama Z80 assembler'daydı.

Sayılarla doğrudan hesaplama için idealdir, ancak gerekirse bir ayrıştırma ağacı oluşturmak için kullanabilirsiniz.

Güncelleme Daha fazla kişi Javascript okuyabildiğinden (veya çalıştırdığından), kod yeniden düzenlendikten sonra ayrıştırıcımı Javascript'e yeniden ekledim. Tüm ayrıştırıcı, hata raporlama ve yorumlar dahil olmak üzere 5k Javascript kodunun altındadır (ayrıştırıcı için yaklaşık 100 satır, bir sarmalayıcı işlevi için 15 satır).

Sen de canlı demosunu bulabilirsiniz 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

Şu anda ayrıştırmak için kullandığınız dilbilgisini tanımlamanız yardımcı olacaktır. Sorun orada yatıyor gibi görünüyor!

Düzenle:

Dilbilgisi sorusunu anlamamanız ve 'bunu elle yazmış olmanız' gerçeği, '1 + 11 * 5' biçimindeki ifadelerle (yani, operatör önceliği ile) neden sorun yaşadığınızı büyük olasılıkla açıklar. . Örneğin, 'aritmetik ifadeler için dilbilgisi' için Google'da arama yapmak, bazı iyi işaretçiler sağlamalıdır. Böyle bir gramerin karmaşık olması gerekmez:

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

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

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

örneğin hile yapar ve bazı daha karmaşık ifadelerle ilgilenmek için önemsiz bir şekilde artırılabilir (örneğin işlevler veya yetkiler dahil ...).

Örneğin bu konuya bir göz atmanızı öneririm .

Hemen hemen tüm gramerler / ayrıştırmaya girişler, aritmetik ifadeleri bir örnek olarak ele alır.

Bir dilbilgisi kullanmanın, belirli bir aracı ( a la Yacc, Bison, ...) kullanmak anlamına gelmediğini unutmayın . Aslında, kesinlikle şu dilbilgisini zaten kullanıyorsunuz:

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

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

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

(veya benzeri bir şey) bilmeden!


8

Boost Spirit kullanmayı düşündünüz mü ? EBNF benzeri gramerleri C ++ dilinde şu şekilde yazmanıza izin verir:

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

1
+1 Ve sonuç şu ki, her şey Boost'un bir parçası. Hesaplayıcı için gramer burada: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . Hesap makinesinin uygulaması burada: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/example/… . Ve belgeler burada: spirit.sourceforge.net/distrib/spirit_1_8_5/libs/spirit/doc/… . İnsanların neden hala orada kendi mini ayrıştırıcılarını uyguladıklarını asla anlamayacağım.
stephan

5

Sorunuzu sorduğunuz gibi, hiçbir şekilde özyinelemeye gerek yok. Cevap üç şeydir: Postfix gösterimi artı Shunting Yard algoritması artı Postfix ifade değerlendirmesi:

1). Sonek gösterimi = açık öncelik belirtimi ihtiyacını ortadan kaldırmak için icat edildi. İnternette daha fazlasını okuyun, ancak işin özü şu: infix ifadesi (1 + 2) * 3, insanlar için okuması ve işlemesi kolayken, makine aracılığıyla hesaplama için çok verimli değil. Nedir? "İfadeyi öncelikli olarak önbelleğe alarak yeniden yaz, ardından her zaman soldan sağa işle" diyen basit kural. Dolayısıyla, infix (1 + 2) * 3, 12 + 3 * sonekine dönüşür. POST çünkü operatör her zaman işlenenlerden SONRA yerleştirilir.

2). Sonek ifadesinin değerlendirilmesi. Kolay. Sonek dizesinden sayıları okuyun. Bir operatör görünene kadar bunları bir yığının üzerine itin. Operatör türünü kontrol edin - tekli? ikili? üçüncül? Bu işleci değerlendirmek için gerektiği kadar çok işlenen yığından çıkar. Değerlendirmek. Sonucu yığına geri itin! Ve neredeyse bitti. Yığın yalnızca bir girişi olana kadar devam edin = aradığınız değer.

Postfix'teki (1 + 2) * 3 yapalım "12 + 3 *". İlk sayıyı oku = 1. Yığının üzerine it. Sonraki oku. Sayı = 2. Yığının üzerine itin. Sonraki oku. Şebeke. Hangisi? +. Ne tür? Binary = iki işlenen gerektirir. Pop yığını iki kez = argright 2'dir ve argleft 1'dir. 1 + 2 3'tür. Yığın üzerine 3'ü geri itin. Postfix dizesinden sonraki bölümü okuyun. Bu bir sayı. 3. itin. Sonraki oku. Şebeke. Hangisi? *. Ne tür? Binary = iki sayıya ihtiyaç duyar -> iki kez pop yığını. Önce argright'a, ikinci kez argleft'e girin. İşlemi değerlendirin - 3 kere 3 eşittir 9. Yığın üzerine 9 tuşuna basın. Sonraki postfix karakterini okuyun. Boş. Girişin sonu. Pop yığını onec = cevabınız bu.

3). Shunting Yard, insan (kolayca) okunabilir infix ifadesini postfix ifadesine dönüştürmek için kullanılır (ayrıca bazı uygulamalardan sonra insan tarafından kolayca okunabilir). Manuel olarak kodlamak kolaydır. Yukarıdaki ve net yorumlara bakın.


4

Kullanmak istediğin bir dil var mı? ANTLR , bunu Java perspektifinden yapmanıza izin verecektir. Adrian Kuhn'un Ruby'de çalıştırılabilir bir gramerin nasıl yazılacağı konusunda mükemmel bir yazısı var; aslında, onun örneği neredeyse tam olarak sizin aritmetik ifade örneğinizdir.


Blog gönderisinde verilen örneklerimin sol yinelemeyi yanlış yaptığını kabul etmeliyim, yani a - b - c, ((a -b) - c) yerine (a - (b -c)) olarak değerlendiriyor. Aslında, bu bana blog gönderilerini düzeltmem gereken bir yapılacaklar eklemeyi hatırlattı.
akuhn

4

Ne kadar "genel" olmasını istediğinize bağlı.

Günah (4 + 5) * cos (7 ^ 3) gibi matematiksel fonksiyonları da ayrıştırabilmek gibi gerçekten genel olmasını istiyorsanız, muhtemelen bir ayrıştırma ağacına ihtiyacınız olacaktır .

Burada tam bir uygulamanın buraya yapıştırılmasının uygun olduğunu düşünmüyorum. Kötü şöhretli " Ejderha kitabı " ndan birine bakmanızı öneririm .

Ancak, yalnızca öncelik desteği istiyorsanız, bunu önce ifadeyi kopyalayıp yapıştırabileceğiniz bir algoritmanın google'da bulunması gereken postfix biçimine dönüştürerek yapabilirsiniz. veya bence bunu bir ikili programla kendiniz kodlayabilirsiniz. ağaç.

Postfix formuna sahip olduğunuzda, yığının nasıl yardımcı olduğunu zaten anladığınızdan o andan itibaren çocuk oyuncağı.


Ejderha kitabı, bir ifade değerlendiricisi için biraz aşırı olabilir - tek gereken basit bir özyinelemeli iniş ayrıştırıcısıdır, ancak derleyicilerde daha kapsamlı bir şey yapmak istiyorsanız okunması gerekir.
Eclipse

1
Vay canına - "Ejderha kitabının" hala tartışıldığını bilmek güzel. 30 yıl önce üniversitede okuduğumu ve başından sonuna kadar okuduğumu hatırlıyorum.
Schroedingers Cat

4

Hile yapmayı ve Shunting Yard Algoritmasını kullanmayı öneririm . Basit bir hesap makinesi türü ayrıştırıcı yazmanın kolay bir yoludur ve önceliği dikkate alır.

Bir şeyleri düzgün bir şekilde belirtmek ve değişkenler vb. Kullanmak istiyorsanız, devam edip burada başkaları tarafından önerildiği gibi yinelemeli bir iniş ayrıştırıcısı yazacağım, ancak yalnızca hesap makinesi tarzı bir ayrıştırıcıya ihtiyacınız varsa, bu algoritma yeterli olmalıdır :-)


4

Bunu PIClistinde Shunting Yard algoritmasıyla buldum :

Harold şöyle yazıyor:

Uzun zaman önce cebirsel ifadeleri kolay değerlendirme için RPN'ye dönüştüren bir algoritmayı okuduğumu hatırlıyorum. Her infix değeri veya operatör veya parantez, bir ray üzerindeki bir demiryolu vagonu ile temsil edildi. Bir tür araba başka bir yola ayrıldı ve diğeri dümdüz devam etti. Ayrıntıları hatırlamıyorum (tabii ki!), Ama her zaman kodlamanın ilginç olacağını düşündüm. Bu, 6800 (68000 değil) derleme kodunu yazarken geri döndü.

Bu "manevra alanı algoritmasıdır" ve çoğu makine ayrıştırıcısının kullandığı şeydir. Wikipedia'da ayrıştırma hakkındaki makaleye bakın. Şönt sahası algoritmasını kodlamanın kolay bir yolu, iki yığın kullanmaktır. Biri "itme" yığını ve diğeri "azaltma" veya "sonuç" yığınıdır. Misal:

pstack = () // boş rstack = () girdi: 1 + 2 * 3 öncelik = 10 // en düşük azalt = 0 // azaltma

start: token '1': isnumber, pstack (push) token koy '+': isoperator set precedence = 2 eğer precedence <previous_operator_precedence then less () // aşağıya bakın pstack (push) token '2' içine '+' koyun : isnumber, pstack (push) token koy '*': isoperator, set precedence = 1, put in pstack (push) // precedence as // token '3' üzerinde: isnumber, pstack (push) sonuna koy girdi, azaltılması gerekiyor (hedef boş pstack) azalt () // bitti

küçültmek, öğeleri itme yığınından çıkarmak ve sonuç yığınına koymak için, "operatör" "numara" biçimindeyse her zaman pstack'teki ilk 2 öğeyi değiştirin:

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

ifade şöyle olsaydı:

1 * 2 + 3

o zaman azaltma tetikleyicisi, zaten basılmış olan '*' işaretinden daha düşük öncülü olan '+' belirtecinin okunması olurdu, bu yüzden şöyle olurdu:

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

ve sonra "+" ve ardından "3" e basıp sonunda azaltıldı:

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

Yani kısa versiyon şudur: itme numaraları, operatörler iterken önceki operatörün önceliğini kontrol edin. Şimdi itilecek operatörden daha yüksekse, önce azaltın, ardından mevcut operatörü itin. Parantları işlemek için, 'önceki' operatörün önceliğini kaydedin ve pstack üzerine, parantez çiftinin içini çözerken azaltma algoritmasına indirgemeyi durdurmasını söyleyen bir işaret koyun. Kapanış pareni, girişin sonunda olduğu gibi bir azaltmayı tetikler ve ayrıca pstack'ten açık paren işaretini kaldırır ve 'önceki işlem' önceliğini geri yükler, böylece ayrıştırmanın kaldığı yerden kapatma parantezinden sonra devam etmesi sağlanır. Bu, özyinelemeli veya özyinelemesiz yapılabilir (ipucu: bir '(' ...) ile karşılaştığınızda önceki önceliği saklamak için bir yığın kullanın. Bunun genelleştirilmiş versiyonu, bir ayrıştırıcı üreteci uygulamalı şöntleme yarda algoritması, f.ex kullanmaktır. yacc veya bizon veya taccle (yacc'nin tcl analoğu) kullanarak.

Peter

-Adam


4

Öncelik ayrıştırması için başka bir kaynak Wikipedia'daki Operatör öncelik ayrıştırıcı girişidir. Dijkstra'nın manevra sahası algoritmasını ve bir ağaç alternatif algoritmasını kapsar, ancak daha da önemlisi, herhangi bir öncelikli cahil ayrıştırıcının önünde önemsiz bir şekilde uygulanabilen gerçekten basit bir makro değiştirme algoritmasını kapsar:

#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;
}

Şu şekilde çağırın:

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

Basitliği harika ve çok anlaşılır.


3
Oldukça hoş bir inci. Ancak genişletmek (örneğin, işlev uygulaması, örtük çarpma, önek ve sonek operatörleri, isteğe bağlı tür ek açıklamaları, herhangi bir şey) her şeyi bozacaktır. Diğer bir deyişle, zarif bir hack.
Jared Updike

Asıl noktayı görmüyorum. Tek yaptığı işleç öncelikli ayrıştırma problemini parantez öncelikli ayrıştırma problemine dönüştürmektir.
Marquis of Lorne

@EJP elbette, ancak sorudaki ayrıştırıcı parantezleri iyi işliyor, bu nedenle bu makul bir çözüm. Yine de olmayan bir ayrıştırıcınız varsa, bunun sorunu başka bir alana taşıdığı konusunda haklısınız.
Adam Davis

4

Web sitemde ultra kompakt (1 sınıf, <10 KiB) Java Matematik Değerlendiricisi için kaynak yayınladım . Bu, kabul edilen cevabın posteri için kraniyal patlamaya neden olan tipte yinelemeli bir iniş ayrıştırıcısıdır.

Tam öncelik, parantez, adlandırılmış değişkenler ve tek bağımsız değişken işlevlerini destekler.




2

Şu anda, tasarım kalıpları ve okunabilir programlama için bir öğrenme aracı olarak düzenli ifade ayrıştırıcı oluşturan bir dizi makale üzerinde çalışıyorum. Okunabilir koda bir göz atabilirsiniz . Makale, manevra yarda algoritmasının net bir kullanımını sunmaktadır.


2

F # 'da bir ifade çözümleyici yazdım ve burada blog yazdım . Yönlendirme sahası algoritmasını kullanıyor, ancak infix'ten RPN'ye dönüştürmek yerine, hesaplamaların sonuçlarını toplamak için ikinci bir yığın ekledim. Operatör önceliğini doğru bir şekilde işler, ancak tekli operatörleri desteklemez. Bunu F # öğrenmek için yazdım, yine de ifade çözümlemeyi öğrenmek için değil.


2

Pyparsing kullanan bir Python çözümü burada bulunabilir . Önceliğe sahip çeşitli işleçlerle infix gösterimini ayrıştırmak oldukça yaygındır ve bu nedenle pyparsing infixNotation(daha önce operatorPrecedence) ifade oluşturucuyu da içerir . Bununla, örneğin "AND", "OR", "NOT" kullanarak boole ifadelerini kolayca tanımlayabilirsiniz. Ya da dört işlevli aritmetiğinizi diğer operatörleri kullanmak için genişletebilirsiniz, örneğin! faktöriyel için veya modül için "%" veya permütasyonları ve kombinasyonları hesaplamak için P ve C operatörlerini ekleyin. Matris gösterimi için, '-1' veya 'T' operatörlerinin (ters çevirme ve devrik için) işlemesini içeren bir infix ayrıştırıcısı yazabilirsiniz. 4 işlevli ayrıştırıcının operatorPrecedence örneği ('!'


1

Bunun geç bir cevap olduğunu biliyorum, ancak tüm operatörlerin (önek, sonek ve sol alt, sağ-alt ve ilişkisiz) keyfi önceliğe sahip olmasına izin veren küçük bir ayrıştırıcı yazdım.

Bunu rastgele DSL desteğine sahip bir dil için genişleteceğim, ancak sadece operatör önceliği için özel ayrıştırıcılara gerek olmadığını, tablolara hiç ihtiyaç duymayan genelleştirilmiş bir ayrıştırıcı kullanılabileceğini belirtmek istedim. sadece göründüğü gibi her işlecin önceliğini arar. İnsanlar, yasa dışı girdileri kabul edebilen özel Pratt ayrıştırıcılardan veya şant yatağı ayrıştırıcılarından bahsediyorlar - bunun özelleştirilmesi gerekmiyor ve (bir hata yoksa) kötü girdileri kabul etmeyecek. Bir anlamda tam değil, algoritmayı test etmek için yazılmış ve girdisi bir miktar ön işlem gerektirecek bir formda ama onu netleştiren yorumlar var.

Bazı yaygın işleç türlerinin eksik olduğuna dikkat edin, örneğin tablo [dizin] veya bir işlev işlevini çağırmak (parametre ifadesi, ...) gibi dizinleme için kullanılan işleç türü eksik. Bunları ekleyeceğim, ancak ikisini de sonek olarak düşünün '[' ve ']' veya '(' ve ')' ayırıcıları arasına gelenlerin ifade ayrıştırıcısının farklı bir örneğiyle ayrıştırıldığı operatörler. Bunu dışarıda bıraktığım için üzgünüz, ancak sonek kısmı içeride - geri kalan kısmı eklemek muhtemelen kodun boyutunu neredeyse iki katına çıkaracaktır.

Ayrıştırıcı sadece 100 satırlık raket kodu olduğundan, belki de buraya yapıştırmalıyım, umarım bu stackoverflow'un izin verdiğinden daha uzun değildir.

Keyfi kararlarla ilgili birkaç ayrıntı:

Düşük öncelikli bir sonek operatörü, düşük öncelikli bir önek operatörü olarak aynı ek blokları için rekabet ediyorsa, önek operatörü kazanır. Çoğu dilde düşük öncelikli sonek operatörlerine sahip olmadığı için bu çoğu dilde ortaya çıkmaz. - örneğin: ((veri a) (sol 1 +) (ön 2 değil) (veri b) (sonrası 3!) (sol 1 +) (veri c)) a + değil b! + c, burada not a önek operatörü ve! sonek operatörüdür ve her ikisi de + 'dan daha düşük önceliğe sahiptir, bu nedenle uyumsuz yollarla ya (a + değil b!) + c veya a + (b! + c değil) olarak gruplamak isterler, bu durumlarda önek operatörü her zaman kazanır, bu nedenle ikincisi ayrıştırma şeklidir

İlişkisel olmayan infix operatörleri gerçekten oradadır, bu nedenle farklı türleri döndüren operatörlerin birlikte anlamlı olduklarını varsaymak zorunda kalmazsınız, ancak her biri için farklı ifade türlerine sahip olmadan bu bir kludge'dir. Bu nedenle, bu algoritmada, ilişkisel olmayan operatörler sadece kendileriyle değil, aynı önceliğe sahip herhangi bir operatörle de ilişkilendirmeyi reddederler. Bu, <= ==> = vb. Çoğu dilde birbiriyle ilişkilendirilmediğinden yaygın bir durumdur.

Farklı türdeki operatörlerin (sol, önek vb.) Öncelik konusundaki bağları nasıl kopardığı sorusu gündeme gelmemelidir, çünkü farklı türlerdeki operatörlere aynı önceliği vermek gerçekten mantıklı değildir. Bu algoritma bu durumlarda bir şeyler yapar, ama tam olarak ne olduğunu anlamaya bile zahmet etmiyorum çünkü böyle bir dilbilgisi ilk başta kötü bir fikirdir.

#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

İşte Java ile yazılmış basit bir durum özyinelemeli çözümü. Negatif sayıları işlemediğine dikkat edin, ancak isterseniz şunu ekleyebilirsiniz:

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

Algoritma, C'de özyinelemeli iniş ayrıştırıcısı olarak kolayca kodlanabilir.

#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;
}

sonraki kitaplıklar yararlı olabilir: yupana - kesinlikle aritmetik işlemler; tinyexpr - aritmetik işlemler + C matematik işlevleri + kullanıcı tarafından sağlanan bir; mpc - ayrıştırıcı birleştiricileri

Açıklama

Cebirsel ifadeyi temsil eden sembol dizisini yakalayalım. Birincisi, bir veya daha fazla kez tekrarlanan ondalık basamak olan bir sayıdır. Böyle bir notasyonu üretim kuralı olarak adlandıracağız.

number -> [0..9]+

İşlenenleri ile toplama operatörü başka bir kuraldır. Ya olduğu numbertemsil etmektedir ya da bir semboller sum "*" sumdizisi.

sum -> number | sum "+" sum

Deneyin yerine numberiçine sum "+" sumo olacak number "+" numbersırayla içine genişletilebilir hangi [0..9]+ "+" [0..9]+düşürüldü olabilir nihayet 1+8doğru bir ek ifadesi olan.

Diğer ikameler de doğru ifade üretecektir: sum "+" sum-> number "+" sum-> number "+" sum "+" sum-> number "+" sum "+" number-> number "+" number "+" number->12+3+5

Yavaş yavaş, mümkün olan tüm cebirsel ifadeleri ifade eden dilbilgisi olarak da bilinen bir dizi üretim kuralına benzeyebiliriz .

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

Operatör önceliğini kontrol etmek için üretim kuralının konumunu diğerlerine göre değiştirin. Yukarıdaki gramer ve nota bak üretim kuralı için bunu *altına yerleştirilirse +bu irade kuvveti productönce değerlendirmek sum. Uygulama, model tanımayı değerlendirmeyle birleştirir ve böylece üretim kurallarını yakından yansıtır.

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);
}

Burada termönce değerlendiririz ve *sonra karakter yoksa geri veririz , aksi takdirde üretim kuralımızda bu bırakılır - sembolleri sonra değerlendir ve geri term.value * product.value döndür bu, üretim kuralımızda doğru seçimdir, yaniterm "*" product

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.