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 (2*cos(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
Oraz Dispose(Z1); bo by byl wyciek
przy dzieleniu warto dodać dzielenie przez zero :D
oraz w funkcji oblicz dac elsy.. tak to super :)
super kalkulator naprawde, a prosty....<ort>zalerzy</ort> dla kogo... ;)
oceniam na 6
hehe widzialem prostrze kalkulatory... :)
fajne a
Ś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!
Prosty kalkulator... :) nic dodać nic ująć :) po prostu Kuba :) prosty... no spoko :)