Введение
Если Вы хотели написать калькулятор с
командной строкой, но не знали, как это сделать, или просто не собрались духом,
то эта статья для Вас.
Для начала предлагаю немного теории.
В рамках данной статьи я рассмотрю только
написание функции для расчета значения из строки. Я считаю, что написание формы
с кнопками доступно для всех, кто взялся читать эту статью.
Для начала представим себе простейший вариант
строки:
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;
Вот и все. Надеюсь, Вы быстро разобрались в этой функции.
|