Prosty kalkulator

Dryobates

Od razu ostrzegam, że pisałem ten kalkulator jedynie kilka godzin i jest naprawdę skromny. Powinien jedynie stanowić punkt wyjścia dla waszych projektów. Do napisania go skłoniły mnie ostatnio pojawiajace się na forum posty o tematyce pobliskiej zagadnieniom wyliczania wartości.
Nie tłumaczę tutaj na razi działania tego. Program nie jest odporny na błędy.

Zaczynamy:
Najpierw dodaj do uses moduły:
uses Contnrs, Math

Pierwszy z nich umożliwi nam korzystanie z bardzo wygodnych klas: stosu oraz kolejki. Drugi zawiera m. in. wykorzystywaną tutaj funkcję potęgowania.

Zadeklarujmy sobie stałą:

const
  toOp = Char(5);

Będzie nam określała element listy, którym jest operator. Przy czym nawiasy traktuję tutaj podobnie do operatorów (aby uprościć kod).

Następnie zadeklarujmy sobie typy.

type
  PElement = ^TElement;
  TElement = record
    case Typ: Char of
      toInteger: (Int: Integer);
      toFloat: (Float: Single);
      toOp: (Op: string[5]);
    end;

TElement jest elementem naszego wyrażenia. Stałe toFloat i toInteger są zadeklarowane w module Classes. Są one używane przez nieudokumentowaną klasę TParser. Mogłaby ona być użyta do pisania kalkulatora, jednak dla nas wygodniej jest napisać samemu tą część analizy składniowej, którą przeprowadza TParser.

Drugim typem jaki zadeklarujemy to typ opisujący nasze operatory:

  TOp = record
    Priorytet: Byte; // Priorytet operatora
    PrawoAsoc: Boolean; // Czy operator prawo asocjacyjny czy lewo
    Symbol: string[20]; // Symbol naszego operatora
  end;

Operatorami prawoasocjacyjnymi są np. operator potęgowania. Np.
234 jest wykonywane w kolejności 2(34).

Jeszcze zadeklarujmy kilka zmiennych

var
  Wejscie: string; // To będzie nasz łańcuch z wyrażeniem
  Poz: Integer; // To jest pozycja odczytu z łańcucha
  WY, POM: TStack; // Dwa stosy pomocne przy zamianie wyrażenia z postaci infiksowej do postfiksowej, a także przy wyliczaniu wartości
  WE: TQueue; // Ta kolejka ułatwi nam rozbicie ciągu wejściowego na pojedyncze elementy: liczby, operatory, nawiasy
  Priorytety: array [0..10] of TOp; // Tablica priorytetów.

Teraz w części implementation piszemy funkcje:

procedure PrzeskoczZnBiale;
// Przeskakuje znaki białe. Wprowadzając obsługę błędów, można np. dodać zliczanie nr lini itp. Ale to już bardziej przy kompilatorach przydatne. Na razie zostawmy to jako taką prostą funckję
begin
  while True do
  begin
    if Wejscie[Poz] in [#33..#255] then
      Exit;
    Inc(Poz);
  end;
end;
procedure AnSyn;
{Ta procedura zajmuje się prostą analizą syntaktyczną. Jest bardzo wrażliwa na błędy. Przyjmuje jedynie poprawne wyrażenia i nie informuje o błędach.}
var
  El: PElement;
  Pom: string;
  i: Integer;
begin
  while Poz <= Length(Wejscie) do
  begin
    PrzeskoczZnBiale;
    case Wejscie[Poz] of
    '(', ')':
      begin
        New(El);
        El^.Typ := toOp;
        El^.Op := Wejscie[Poz];
        WE.Push(El);
        Inc(Poz);
      end;
'*', '/':
      begin
        New(El);
        El^.Typ := toOp;
        El^.Op := Wejscie[Poz];
        WE.Push(El);
        Inc(Poz);
      end;
    '+', '-':
      begin
        // Czy operator unarny czy binarny?
        i := Poz-1;
        while (i >0) and (Wejscie[i] < #33) do
          Inc(i);
        if (Wejscie[i] in ['0'..'9', ')']) then
        // Jeżeli operator binarny
        begin
          New(El);
          El^.Typ := toOp;
          El^.Op := Wejscie[Poz];
          WE.Push(El);
          Inc(Poz);
        end
        else
        // Jeżeli unarny
        begin
          Pom := Wejscie[Poz];
          Inc(Poz);
          while Wejscie[Poz] in ['0'..'9', '.'] do
          begin
            Pom := Pom + Wejscie[Poz];
            Inc(Poz);
          end;
          New(El);
          El^.Typ := toFloat;
          El^.Float := StrToFloat(Pom);
          WE.Push(El);
        end;
      end;
    '^':
      begin
        New(El);
        El^.Typ := toOp;
        El^.Op := Wejscie[Poz];
        WE.Push(El);
        Inc(Poz);
      end;
    'A'..'Z', 'a'..'z':
      begin
        Pom := '';
        while Wejscie[Poz] in ['A'..'Z', 'a'..'z'] do
        begin
          Pom := Pom + Wejscie[Poz];
          Inc(Poz);
        end;
        New(El);
        El^.Typ := toOp;
        El^.Op := Pom;
        WE.Push(El);
      end;
    '0'..'9':
      begin
        Pom := '';
        while Wejscie[Poz] in ['0'..'9', '.'] do
        begin
          Pom := Pom + Wejscie[Poz];
          Inc(Poz);
        end;
        New(El);
        El^.Typ := toFloat;
        El^.Float := StrToFloat(Pom);
        WE.Push(El);
      end;
    end;
  end;
end;
 
function Priorytet(a, b: PElement): Boolean;
// Ta funkcja określa nam względny priorytet operatorów
var
  i: Byte;
  Op1, Op2: TOp;
begin
  i := 0;
  while Priorytety[i].Symbol <> a^.Op do
    Inc(i);
  Op1 := Priorytety[i];
  i := 0;
  while Priorytety[i].Symbol <> b^.Op do
    Inc(i);
  Op2 := Priorytety[i];
  if Op1.Priorytet = Op2.Priorytet then
  { Jeżeli priorytety są równe to przy operatory prawoasocjacyjnych muszą się zachowywać tak, jakgdyby drugi miał większy priorytet niż pierwszy }
    Result := Op1.PrawoAsoc
  else
    Result := Op1.Priorytet > Op2.Priorytet;
end;
 
procedure InToPost;
{ Zamienia z notacji infixowej do notacji postfixowej (odwrotnej notacji polskiej, notacji łukasiewiczowskiej) }
var
  Znak: PElement;
begin
  while WE.Count > 0 do
  begin
    Znak := WE.Pop;
    case Znak^.Typ of
      toInteger, toFloat:
        WY.Push(Znak);
      toOP:
        if Znak^.Op = '(' then
          POM.Push(Znak)
        else
          if Znak^.Op = ')' then
          begin
            while PElement(POM.Peek)^.Op <> '(' do
              WY.Push(POM.Pop);
            Dispose(POM.Pop);
          end
          else
            if (POM.Count=0) or (Priorytet(Znak, POM.Peek)) then
              POM.Push(Znak)
            else
            begin
              while ((POM.Count)>0) and not Priorytet(Znak, POM.Peek) do
                WY.Push(POM.Pop);
              POM.Push(Znak);
            end;
    end;
  end;
  while WY.Count > 0 do
    POM.Push(WY.Pop);
end;
 
function Oblicz: Single;
// Oblicza wartość wyrażenia
var
  Znak, Z1, Z2: PElement;
begin
  while POM.Count > 0 do
  begin
    Znak := POM.Pop;
    case Znak^.Typ of
      toFloat:
        WY.Push(Znak);
      toOp:
        begin
          if Znak^.Op = '+' then
          begin
            Z1 := PElement(WY.Pop);
            Z2 := PElement(WY.Pop);
            Z1^.Float := Z2^.Float + Z1^.Float;
            Dispose(Z2);
            WY.Push(Z1);
            Dispose(Znak);
          end;
          if Znak^.Op = '-' then
          begin
            Z1 := PElement(WY.Pop);
            Z2 := PElement(WY.Pop);
            Z1^.Float := Z2^.Float - Z1^.Float;
            Dispose(Z2);
            WY.Push(Z1);
            Dispose(Znak);
          end;
          if Znak^.Op = 

'*'

 then
          begin
            Z1 := PElement(WY.Pop);
            Z2 := PElement(WY.Pop);
            Z1^.Float := Z2^.Float * Z1^.Float;
            Dispose(Z2);
            WY.Push(Z1);
            Dispose(Znak);
          end;
          if Znak^.Op = '/' then
          begin
            Z1 := PElement(WY.Pop);
            Z2 := PElement(WY.Pop);
            Z1^.Float := Z2^.Float / Z1^.Float;
            Dispose(Z2);
            WY.Push(Z1);
            Dispose(Znak);
          end;
          if Znak^.Op = '^' then
          begin
            Z1 := PElement(WY.Pop);
            Z2 := PElement(WY.Pop);
            Z1^.Float := Power(Z2^.Float, Z1^.Float);
            Dispose(Z2);
            WY.Push(Z1);
            Dispose(Znak);
          end;
          if Znak^.Op = 'sin' then
          begin
            Z1 := PElement(WY.Pop);
            Z1^.Float := Sin(Z1^.Float);
            WY.Push(Z1);
            Dispose(Znak);
          end;
          if Znak^.Op = 'cos' then
          begin
            Z1 := PElement(WY.Pop);
            Z1^.Float := Cos(Z1^.Float);
            WY.Push(Z1);
            Dispose(Znak);
          end;
        end;
    end;
  end;
  Znak := WY.Pop;
  Result := Znak^.Float;
  Dispose(Znak);
end;

Wrzućmy jeszcze na formę TEdit oraz TButton. Pod przycisk podepnijmy następującą procedurę:

procedure TForm1.Button1Click(Sender: TObject);
begin
  //Najpierw ustalmy priorytety operatorów
  with Priorytety[0] do
  begin
    Priorytet := 1;
    PrawoAsoc := False;
    Symbol := '+';
  end;
  with Priorytety[1] do
  begin
    Priorytet := 1;
    PrawoAsoc := False;
    Symbol := '-';
  end;
  with Priorytety[2] do
  begin
    Priorytet := 2;
    PrawoAsoc := False;
    Symbol := '*';
  end;
  with Priorytety[3] do
  begin
    Priorytet := 2;
    PrawoAsoc := False;
    Symbol := '/';
  end;
  with Priorytety[4] do
  begin
    Priorytet := 3;
    PrawoAsoc := True;
    Symbol := '^';
  end;
  with Priorytety[5] do
  begin
    Priorytet := 4;
    PrawoAsoc := False;
    Symbol := 'sin';
  end;
  with Priorytety[6] do
  begin
    Priorytet := 4;
    PrawoAsoc := False;
    Symbol := 'cos';
  end;
  { Nawiasy traktujemy jako pewnego rodzaju operatory. To upraszcza troszkę kod. Jednak odwrotnie niż w rzeczywistości tutaj musimy nadać im najniższy priorytet }
  with Priorytety[7] do
  begin
    Priorytet := 0;
    PrawoAsoc := True;
    Symbol := '(';
  end;
  with Priorytety[8] do
  begin
    Priorytet := 0;
    PrawoAsoc := True;
    Symbol := ')';
  end;
// Tworzymy nasze stosy i kolejkę
  WE := TQueue.Create;
  WY := TStack.Create;
  POM := TStack.Create;
//Ustawiamy nasz ciąg wejściowy
  Wejscie := Edit1.Text;
//oraz ustawiamy odczyt na początek
  Poz := 1;
//Tutaj dzielimy wyrażenie na atomy
  AnSyn;
//Zamieniamy na notację postfixową
  InToPost;
//i obliczamy
  ShowMessage(FloatToStr(Oblicz));
  WE.Free;
  WY.Free;
  POM.Free;
end;

Weźmy sobie do rozważenia wyrażenie (2cos(3/(1+2)-1)-5)^(3--1).
Najpierw jest ono dzielone na atomy przez funkcję AnSyn:
( | 2 |
| cos | ( | 3 | / | ( | 1 | + | 2 | ) | - | 1 | ) | - | 5 | ) | ^ | ( | 3 |- |-1 | )

Następnie jest konwertowane na notację postfixową:
POM ^ - -1 3 - 5 * cos - 1 / + 2 1 3 2

Kolejny etap to wyliczanie wartości przez funkcję Oblicz:
WY 81

8 komentarzy

Oraz Dispose(Z1); bo by byl wyciek

przy dzieleniu warto dodać dzielenie przez zero :D
oraz w funkcji oblicz dac elsy.. tak to super :)

        if(Z1^.Float = 0) then
        begin
          Dispose(Z2);
          Dispose(Znak);
          Result := '';
          exit;
        end
        else
        begin
          Z1<sup>.Float := Z2</sup>.Float / Z1^.Float;
          Dispose(Z2);
          WY.Push(Z1);
          Dispose(Znak);
        end;

super kalkulator naprawde, a prosty....<ort>zalerzy</ort> dla kogo... ;)
oceniam na 6

hehe widzialem prostrze kalkulatory... :)

Świetny gotowiec! przyda się na 100%. Oceniam na 6.

Pod adresem www.icpnet.pl/~pinio/evaluator.exe można znaleźć naprawdę rozbudowany kalkulator (adres dzięki Piniolowi, thnx!). Jest prosty w użyciu, a jednak FULL wypas!

<u>Prosty</u> kalkulator... :) nic dodać nic ująć :) po prostu Kuba :) prosty... no spoko :)