Все мы помним, как в школе учили обыкновенные дроби. Числители, знаменатели, НОД и НОК, арифметические действия с дробями. Но и в реальной жизни обыкновенные дроби успешно применяются в разных сферах деятельности, в том числе юридической: например, в обыкновенных дробях могут выражаться доли участников хозяйственных обществ, доли в праве общей долевой собственности и т.д.
И вот, понадобилось как-то раз реализовать пару функций в корпоративном приложении для оперирования обыкновенными дробями. Современная реализация паскаля, будь то delphi или freepascal, предлагает удобные средства для этого.
Получившийся в итоге модуль вырос в процессе изучения перегрузки операторов.
Когда модуль уже был готов, я увидел, что год назад участники англоязычного форума freepascal разработали аналогичный, правда, коллективно утяжелили его массой функций и различных реализаций одного и того же. Ну, будем считать сделанное своеобразным импортозамещением.
Основным типом данных будет следующая структура:
TFraction = record
Numerator: longint;
Denumerator: longint;
function Create(ANum, ADenum: longint): TFraction;
function toStr: string;
function toFloat: extended;
end;
Не забываем включать нужную директиву компилятора — {$MODESWITCH ADVANCEDRECORDS}
Структура имеет два поля — целочисленные числитель и знаменатель, функцию-конструктор для присвоения значения одной строкой и пару функций конвертации обыкновенной дроби в строку и десятичную дробь.
Вспомогательные функции модуля (могут использоваться и самостоятельно):
// приведение к общему знаменателю
procedure SetEqualDenum(var ALeftFr, ARightFr: TFraction);
// расширение дроби - умножение на целое число
function ExpandFraction(AFraction: TFraction; Factor: longint): TFraction;
// наибольший общий делитель
function gcd(ALeftDenum, ARightDenum: longint): longint;
// наименьшее общее кратное
function lcm(ALeftDenum, ARightDenum: longint): longint;
// сокращение дроби - делением на целое число, константа toGCD по умолчанию подразумевает приведение дроби к несократимой
function CollapseFraction(AFraction: TFraction; Divider: longint = toGCD): TFraction;
// функция сравнения двух дробей
function CompareFractions(ALeftFr, ARightFr: TFraction): TfrCompareResult;
// возвращает обратную дробь, то есть меняет местами числитель и знаменатель
function ReverseFraction(AFraction: TFraction): TFraction;
Главные же в модуле — перегруженные операторы для сложения, вычитания, умножения, деления, присваивания и сравнения дробей:
// сложение двух дробей
operator +(ALeftFr, ARightFr: TFraction) r: TFraction;
// сложение с целым числом
operator +(ALeftFr: TFraction; const Term: longint) r: TFraction;
// вычитание дробей
operator -(ALeftFr, ARightFr: TFraction) r: TFraction;
// вычитание целого числа
operator -(ALeftFr: TFraction; const Sub: longint) r: TFraction;
// умножение двух дробей
operator * (ALeftFr, ARightFr: TFraction) r: TFraction;
// умножение на целое число
operator * (AFraction: TFraction; const Multiplier: longint) r: TFraction;
operator * (const Multiplier: longint; AFraction: TFraction) r: TFraction;
// деление двух дробей
operator / (ALeftFr, ARightFr: TFraction) r: TFraction;
// деление на целое число
operator / (AFraction: TFraction; const Divider: longint) r: TFraction;
// проверяет на равеноство
operator = (ALeftFr, ARightFr: TFraction) r: boolean;
// проверяет, больше ли левая дробь
operator > (ALeftFr, ARightFr: TFraction) r: boolean;
// проверяет, меньше ли левая дробь
operator < (ALeftFr, ARightFr: TFraction) r: boolean;
// преобразование дроби из целого числа (знаменатель = 1)
operator := (const AIntegerPart: longint) r: TFraction;
// преобразование строки вида Ч/З в дробь
operator := (const AStringFr: string) r: TFraction;
К сожалению, в freepascal невозможно передать в качестве присваемого значения перечисление целых чисел (словарь, множество, называйте как угодно, смысл в том, что так нельзя: А := (1,2); или так B := [1,2]), поэтому инициирование дроби идет через функцию-конструктор или строковое значение, хотя ничто не мешает просто задать значения двум полям, но я хотел сделать как можно проще.
operator+(ALeftFr, ARightFr: TFraction)r: TFraction;
begin
SetEqualDenum(ALeftFr, ARightFr);
r.Numerator := ALeftFr.Numerator + ARightFr.Numerator;
r.Denumerator := ALeftFr.Denumerator;
r := CollapseFraction(r, toGCD);
end;
...
operator/(ALeftFr, ARightFr: TFraction)r: TFraction;
begin
r := ALeftFr * ReverseFraction(ARightFr);
end;
...
operator:=(const AStringFr: string)r: TFraction;
var
i: integer;
begin
i := PosEx(char(SolidorSym), AStringFr);
if not TryStrToInt(LeftStr(AStringFr, i - 1), r.Numerator) then
raise Exception.Create('Numerator is not integer!');
if not TryStrToInt(RightStr(AStringFr, Length(AStringFr) - i), r.Denumerator) then
raise Exception.Create('Denumerator is not integer!');
end;
...
operator=(ALeftFr, ARightFr: TFraction)r: boolean;
begin
Result := CompareFractions(ALeftFr, ARightFr) = crEqual;
end;
operator>(ALeftFr, ARightFr: TFraction)r: boolean;
begin
Result := CompareFractions(ALeftFr, ARightFr) = crLeft;
end;
operator<(ALeftFr, ARightFr: TFraction)r: boolean;
begin
Result := CompareFractions(ALeftFr, ARightFr) = crRight;
end;
Повторюсь, в модуле “конкурентов” больше функций и перегруженных операторов, так они дополнительно перегрузили >=, <=, **, а также ввели присваивание через десятичную дробь и преобразование в строку с выдачей “правильной” дроби, последнее для математических выражений совершенно не нужно.
Для вычисления НОД я выбрал самый простой рекурсивный алгоритм:
function gcd(ALeftDenum, ARightDenum: longint): longint;
begin
if ARightDenum = 0 then
Result := abs(ALeftDenum)
else
Result := abs(gcd(ARightDenum, ALeftDenum mod ARightDenum));
end;
НОД нам нужен для сокращения дробей и вычисления НОК:
function lcm(ALeftDenum, ARightDenum: longint): longint;
begin
Result := abs(ALeftDenum * ARightDenum) div gcd(ALeftDenum, ARightDenum);
end;
НОК в свою очередь используем для приведения дробей к общему знаменателю:
procedure SetEqualDenum(var ALeftFr, ARightFr: TFraction);
var
tDenum: longint;
begin
if ALeftFr.Denumerator = ARightFr.Denumerator then
exit;
tDenum := lcm(ALeftFr.Denumerator, ARightFr.Denumerator);
ALeftFr := ExpandFraction(ALeftFr, tDenum div ALeftFr.Denumerator);
ARightFr := ExpandFraction(ARightFr, tDenum div ARightFr.Denumerator);
end;
А уж эта функция и используется в итоге в перегруженных операторах сложения, вычитания и сравнения.
Перегрузка операторов позволяет писать такие простые присваивания:
Fr1, Fr2: TFraction;
...
Fr1 := 12; // (получится дробь 12/1)
Fr2 := ‘3/5’; // (преобразование строки в дробь)
// ну или при необходимости
Fr3 := TFraction.Create(22,7); // 22/7
Становится проще записывать операции с дробями и неравенства:
Fr3 := Fr1+ Fr2;
Fr3 := Fr1 * Fr2;
Fr2 := Fr1 - 1;
Fr2 := Fr1 / 3;
Fr3 := Fr1 / Fr2;
if Fr1 > Fr2 …
Сработают и комбинированные операторы присваивания:
Fr1 += Fr2;
Fr2 -= 1;
Fr3 *= ‘1/2’;
Допустимы даже такие выражения:
if Fr1 > ‘2/3’ ...
while Fr2 < 1 ...
Эти неравенства отлично скомпилируются и дадут верный логический результат.
В стандартной поставке freepascal есть пара аналогичных модулей для работы с математическими матрицами и комплексными числами, можно посмотреть на их реализацию в качестве примеров.
По моему мнению, перегрузка операторов приносит такое же упрощение в чистоте и наглядности кода, как дженерики (особенно любимы мною списки из fgl), конечно, если вы не перегружаете оператор плюс методом деления. Компилятор всегда (ну почти) вас остановит, если вы забудете, что оператор перегружен или наоборот (из-за несоответствия типов данных).
Описанный модуль далек от совершенства, нужно еще добавить обработку результата в случаях получения нуля при математических операциях, деления на ноль, также можно добавить преобразования десятичных дробей в обыкновенные и наоборот.
→ Полный текст модуля приведен здесь.
→ Модуль с форума freepascal.org.
Автор: java73