Создание калькулятора с командной строкой в Delphi
Введение
Если Вы хотели написать калькулятор с командной строкой, но не знали, как это сделать, или просто не собрались духом, то эта статья для Вас.
Для начала предлагаю немного теории.
В рамках данной статьи я рассмотрю только написание функции для расчета значения из строки. Я считаю, что написание формы с кнопками доступно для всех, кто взялся читать эту статью.
Для начала представим себе простейший вариант строки:
1*2+3*4
Если быть совсем честным, это нужно делать через стеки, но поскольку не все знают что это такое, я предлагаю сделать через стандартный класс TStringList. Для расчета такой строки нам нужно создать два экземпляра класса TStringList. В одном мы будем хранить числа, а в другом знаки операций.
Вот что у нас получилось. Пройдемся по строке, как только мы нашли число, добавляем его в первый список. А если мы нашли знак операции, добавляем его во второй список.
Далее Вы спросите: «А зачем, собственно, мы это делали?». Так просто очень легко считать.
Привожу алгоритм подсчета значения строки, разделенной таким способом на два списка.
Ищем во втором списке операции, приоритет которых выше. То есть, знаки умножения или деления.
Если нашли, то вынимаем этот знак. Вынимаем из первого списка число с таким же номером, как и у знака, и следующее. Это и будут наши операнды. Выполняем с ними соответствующее действие, и записываем результат в первый список на то место, с которого выдернули первый операнд.
Повторяем пункты 1 и 2 до тех пор, пока во втором списке не останется ни одного такого знака.
Повторяем пункты 1-3 со знаками сложения и вычитания.
А теперь давайте прогоним через этот алгоритм наш пример.
Ищем в правом столбце знак умножения или деления. Нашли – он стоит на первой позиции. Далее нам нужны числа, которые надо умножить. Они стоят во втором списке под номерами 1, и 1+1, то есть 2. В нашем примере это цифры 1 и 2. Вынимаем их из списка и умножаем. Получилась двойка. Записываем её в первый список на то место, откуда выдернули первый операнд, в нашем случае на первое место. И удаляем из второго списка знак умножения. Вот что должно получиться:
2.Далее продолжаем искать знаки умножения или деления. Нашли. Знак умножения стоит на второй позиции. Нам опять же нужны операнды. Они находятся на втором и третьем месте в первом списке. Это цифры 3 и 4. Умножаем их и удаляем из первого списка. Результат, число 12, заносим в первый список под номером 2.
На этом знаки умножения и деления у нас закончились, займемся поисками знаков сложения и вычитания. В нашем случае есть знак умножения, стоящий на первом месте. Выбираем из первого списка то, что будем складывать (это элементы под номерами 1 и два), удаляем их из списка, а результат сложения заносим обратно в список. У нас получилось следующее:
Так как список операций пуст, то мы выполнили все действия, а результатом является число, оставшееся в списке чисел.
Как Вы видите, на данном этапе все довольно легко. Давайте теперь напишем функцию, которая вычисляет значение уже разделенной строки. В качестве параметра у неё будут два списка: список чисел и список операций. Вот полный код этой функции. Я думаю, в ней нет ничего сложного. А если Вы дочитали до этого момента, то алгоритм Вам уже ясен.
function CalculateLists (s1, s2: TStringList): real;
var
i: integer;
a,b,r1: real;
c: char;
begin
r1 := 0;
// Ищем знаки умножения или деления
i := 0;
if s2.Count>0 then
while (s2.Find('*', i)or(s2.Find('/', i))) do
begin
c := s2[i][1];
a := strtofloat(s1[i]);
b := strtofloat(s1[i+1]);
case c of
'*': r1 := a*b;
'/': r1 := a/b;
end;
s1.Delete(i);
s1.Delete(i);
s1.Insert(i, floattostr(r1));
s2.Delete(i);
end;
// Сложение и вычитание ///
if s2.Count>0 then
repeat
c := s2[0][1];
a := strtofloat(s1[0]);
b := strtofloat(s1[1]);
case c of
'+': r1 := a+b;
'-': r1 := a-b;
end;
s1.Delete(0);
s1.Delete(0);
s1.Insert(0, floattostr(r1));
s2.Delete(0);
if s1.Count = 1 then break;
if s2.Count = 0 then break;
until false;
result := strtofloat(s1[0]);
end;
Сразу хочу заметить, что элементы TStringList имеют строковый тип. Поэтому приходится преобразовывать типы туда сюда.
Ну а теперь давайте займемся самой сложной частью всего задания: разделение строки на два списка. Давайте, прежде чем включать Delphi и начинать лупить клавиатуру, немного разберемся, чего мы от этой функции хотим. Главная её задача состоит в том, чтобы корректно разделить строку на два списка и передать эти списки на вычисление функции CalculateLists, которую мы только что написали. А что, если мы наткнемся на неверный символ? Для того, чтобы в Вашей основной программе Вы смогли верно определить какая произошла ошибка и на каком символе, я предлагаю создать свой класс-исключение. И возбуждать это исключение при каждой ошибке обработки строки. Этот класс самый простой, просто чтобы не загромождать проект. Вы можете изменить его по Вашему желанию.
type ECalcError = class (Exception)
end;
Пойдем дальше. Давайте добавим еще возможность написания функций. Для определения функций давайте создадим два массива. В одном мы будем хранить строковые представления функций, а в другом ссылки на реальные функции. Так же для облегчения процесса редактирования предлагаю создать константу, которая будет отвечать за количество поддерживаемых функций. Также, для описания массива реальных функций, нам понадобиться тип-функция. А для облегчения поиска цифр и знаков операций создадим два множества.
const Sign: set of char = ['+', '-', '*', '/'];
var Digits: set of char = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
type TFunc = function (x: real): real;
const MaxFunctionID = 2; // - Количество обрабатываемых функций
Так как использовать ссылки на стандартные процедуры нельзя, или я просто не знаю как. Поэтому нам придется переопределить парочку функций.
function _sin(x: real):real;
begin
result := sin(x);
end;
function _cos(x: real):real;
begin
result := cos(x);
end;
А теперь можно и определять два массива функций:
const sfunc: array [1..MaxFunctionID] of string[7]= ('sin', 'cos');
const ffunc: array [1..MaxFunctionID] of TFunc = (_sin, _cos);
А теперь, для правильной работы с такими массивами я предлагаю написать парочку функций:
Для нахождения функции
Для подсчета значения функции.
Напишем функцию, для проверки, есть ли в строке поддерживаемая функция. Я считаю, что она довольно простая, поэтому сразу приведу её код:
function GetFunction (Line: string; index: integer): integer;
var i: integer;
begin
for i:= 1 to MaxFunctionID do
if sfunc[i] = copy(Line, index, length(sfunc[i])) then
begin
result := i;
exit;
end;
result := 0;
end;
Как Вы видите, функция получает строку, и индекс символа, в котором возможно появление функции. Если наша функция определила, что в строке иметься поддерживаемая математическая функция, то она возвращает её номер, а если таковой нет, то возвращает ноль.
Функция для подсчета значения выглядит еще проще:
function CalculateFunction (Fid: integer; x: real): real;
begin
result := ffunc[Fid](x);
end;
Я предлагаю написать еще одну простенькую функция, которая после облегчит нам жизнь. Она будет считать длину строкового написания математической функции.
function GetFunctionNameLength (Fid: integer): integer;
begin
result := length (sfunc[Fid]);
end;
Вот мы и закончили писать подготовительные функции. Давайте разберем примерный алгоритм функции разбора строки. Мы будем просматривать строку по символам. Если очередной символ есть цифра, то заносим эту цифру в строку-число. Если символ – знак операции, то записываем в список операций эту операцию, и записываем строку-число в список чисел. Если нам попалась открывающая скобка, то мы должны найти её закрывающую, независимо есть ли в этих скобках вложенные скобки и передать строку, которая находиться в этих скобках функции разделения строк, и записать в массив чисел то, что она вернет. Этот пример называется рекурсией. Идем дальше, если мы нашли символ, не являющийся скобкой, цифрой или знаком операции. Это должно быть функция. Вот здесь нам и пригодится функция для проверки, начинается ли с этой позиции поддерживаемая функция. Если да, то ищем после записи этой функции открывающую скобку, так как параметр любой функции должен идти в скобках. Если скобки нет, то можно вызвать ошибку. Если скобка есть, то действуем по намеченному алгоритму обработки скобки, только в список чисел надо вносить не результат выполнения функции разбиения, а значение функции с этим результатом.
Если Вы досюда дочитали, то я готов Вас поздравить, потому что дальше я предлагаю полный код функции разделения строки на два списка и подсчета значений этого списка.
function Calculate (Line1: string): real;
var z, d: TStringList; // - z –список знаков; d – список чисел
i, j, c: integer; // счетчики
w, l, Line: string; // begw – переменная, отвечающая за начало числа
begw, ok: boolean;
res: real; // - результат
e: ECalcError; // - ошибка
id : integer; // - номер функции
begin
w := '';
Line := Line1;
begw := FALSE;
ok := false;
z := TStringList.Create;
d := TStringList.Create;
//// Разбиение строки на два списка ////
i := 1;
repeat
//// Если знак операции ////
if Line[i] in Sign then
begin
z.Add(Line[i]);
if begw then d.Add(w);
w := '';
begw := TRUE;
end
//// Если цифра ////
else if Line[i] in digits then
begin
begw := true;
w := w + Line[i];
end
//// Если скобка ////
else if Line[i]='(' then
begin
c := 1;
for j := i+1 to length (Line) do
begin
if Line[j]='(' then c := c + 1;
if Line[j]=')' then c := c - 1;
if c=0 then
begin
ok := true;
break;
end;
end;
if not ok then
begin
e := ECalcError.Create('Не найдена закрывающая скобка к символу ' + inttostr(i));
raise e;
e.Free;
end;
l := copy (Line, i+1, j-i-1);
d.Add(floattostr(Calculate(l)));
delete (Line, i, j-i+1);
i := i - 1;
end
/// Проверка на функцию
else if (GetFunction (Line, i)<>0) then
begin
id := GetFunction (Line, i);
if Line[i+GetFunctionNameLength(id)]<>'(' then {Если после функции нет скобки}
begin
e := ECalcError.Create('Не найдена скобка после функции в символе '+ inttostr(i));
raise e;
e.Free;
end;
{----Если есть скобка----------}
c := 1;
for j := i+GetFunctionNameLength(id)+1 to length (Line) do
begin
if Line[j]='(' then c := c + 1;
if Line[j]=')' then c := c - 1;
if c=0 then
begin
ok := true;
break;
end;
end;
if not ok then
begin
e := ECalcError.Create('Не найдена закрывающая скобка к символу' + inttostr(i));
raise e;
e.Free;
end;
l := copy (Line, i+GetFunctionNameLength(id)+1, j-i-GetFunctionNameLength(id)-1);
d.Add(floattostr(CalculateFunction(id, Calculate(l))));
delete (Line, i, j-i+1);
i := i - 1;
end
//// Если неизвестный символ ////
else
begin
e := ECalcError.Create('Неизвестный символ : '+inttostr(i));
raise e;
e.Free;
end;
i := i + 1;
j := Length (Line);
if i>J then break;
until false;
if w<>'' then d.Add(w);
res := (CalculateLists(d, z));
z.Free;
d.Free;
result := res;
end;
Вот и все. Надеюсь, Вы быстро разобрались в этой функции.