image

Все мы помним, как в школе учили обыкновенные дроби. Числители, знаменатели, НОД и НОК, арифметические действия с дробями. Но и в реальной жизни обыкновенные дроби успешно применяются в разных сферах деятельности, в том числе юридической: например, в обыкновенных дробях могут выражаться доли участников хозяйственных обществ, доли в праве общей долевой собственности и т.д.

И вот, понадобилось как-то раз реализовать пару функций в корпоративном приложении для оперирования обыкновенными дробями. Современная реализация паскаля, будь то 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.
Поделиться с друзьями
-->

Комментарии (12)


  1. knagaev
    26.04.2017 18:21
    +1

    Даже стало интересно как коррелирует ваш ник с темой статьи.
    Но когда увидел «Юрист с хобби» всё встало на места :)


    1. java73
      26.04.2017 18:29

      Ну, видимо юристов тут не любят — карму снимают.


  1. dom1n1k
    26.04.2017 21:10

    А что означает A в начале всех операндов?


    1. java73
      26.04.2017 21:20

      Просто префикс для параметров.


      1. dom1n1k
        26.04.2017 22:00

        А смысл?


        1. pda0
          26.04.2017 22:42

          Артикул это. А смысл в том, чтобы случайно не пересечься с полем или свойством класса/записи. В принципе компилятор обычно достаточно умён, чтобы понять даже когда вы пишете типа ALeft := ALeft; :) но сам будешь мучиться: «а точно он правильно понял?», «а в следующей версии он случайно не перепутает?» и т.д.

          Конечно в такой функции это не требуется, но лучше от привычек не отступать.


        1. MacIn
          27.04.2017 16:21
          +1

          Это Borland'овский styleguide. Используется венгерская нотация по области видимости:
          A — формальный параметр
          f — поле класса

          У нас еще используют
          l — локальная
          g — глобальная переменная
          c — константа.

          Очень удобно, потому что сразу понятно, поле это, формальный параметр, глобальная переменная, property класса или что-то еще.
          Вот, например, кусок кода, с которым сейчас работаю:

            lPageContent := PostProcessFinalPage(lPageContent, AReqContext, lMessage);
            AResponse.ResponseNo := cStatusOK;
          

          По всем полям понятно, откуда они пришли с первого взгляда, никуда скролить не надо.


  1. ftdgoodluck
    27.04.2017 07:06

    А можно чуть поподробнее про use case? Дроби в корпоративном приложении?


    1. java73
      27.04.2017 07:19
      +2

      Как раз описанный в начале случай. База данных юридических лиц. В егрюл размер доли может храниться в виде денежной суммы, десятичной дроби или обыкновенной. 10000 рублей ровно на троих не разделить, поэтому по 1/3 каждому. Сложение всех долей должно давать 1 в качестве проверки.
      Как всторостепенное приложение — написал калькулятор для того, чтоб ребенок мог проверять верно ли он решает школьные задачи.


      1. Danov
        27.04.2017 10:01

        Да, в школьном приложении неплохо получается арифметика дробей:


        1. java73
          27.04.2017 10:24

          Воот, до такого и допилю со временем))


          1. Danov
            27.04.2017 11:20

            А там в основе и лежит моя реализация перегрузки операторов для дробей, только на C#, который является развитием Object Pascal, и тем же автором — Андерсом Хейлсбергом.