Прежде чем мы перейдем к числам с плавающей запятой, напомню о натуральных дробях. Иногда с их помощью можно решить задачу, не прибегая к флоатам.

Натуральная дробь — это отношение двух целых чисел, например 1/2, 1/3, 4/9 и так далее. Их достоинство в том, что иногда из них можно выйти обратно к целым числам. Например, кто-то меняет коров на лошадей по курсу 2/3, то есть за две коровы — три лошади. В случае с флоатами курс был бы 0.666666… или 0.(6) в периоде. Далее нам дают 6 лошадей. Умножаем 6 на 0.66666… и получаем 3.999996 коровы. Округляем до четырех и совершаем обмен. Но если учесть, что:

  • коров и лошадей может быть много;
  • операции совершаются часто;
  • операции строятся в цепочку: коровы на лошадей, лошади на кур и все это по разным курсам,

то погрешность рано или поздно даст о себе знать.

Если хранить курс обмена в виде натуральной дроби 2/3, то умножение на 6 даст целое число:

2   6   12
- * - = -- = 4
3   1    3

Легко написать класс для работы с натуральными дробями. Это пара целых чисел. Операции над парой повторяют школьную программу. Чтобы сложить две дроби, их проводят к общему знаменателю. Например, чтобы сложить 1/6 и 1/12, мы:

  • приводим 1/6 к знаменателю 12 и получаем 2/12;
  • складываем числители;
  • сокращаем дробь.
1    1    2    1    3   1
- + -- = -- + -- = -- = -
6   12   12   12   12   4

Вычитание — то же самое, только меняется знак. Умножение несложное: перемножаем числители и знаменатели, затем сокращаем результат. Деление аналогично, только вторая дробь переворачивается.

Для всех этих действий нужны две вещи: наименьшее общее кратное (НОК) и наибольший общий делитель (НОД). Принцип их поиска был найден еще греками.

В Кложе из коробки идет тип Ratio, который устроен как пара чисел BigInteger. Если я поделю одно целое на другое, и результат дает остаток, то получу дробь:

(/ 2 3)
2/3 ;; clоure.lang.Ratio

Умножив дробь на целое, которое сокращает знаменатель, я получу целое:

(* (/ 2 3) 6)
4 ;; clоjure.lang.BigInt

Никаких 3.999996 коровы, все честно. Здесь Рич Хикки ничего не придумал, а взял реализацию из Common Lisp, который еще до вашего рождения считал натуральные дроби из коробки.

Понимаю, что примеры с лошадьми звучат забавно. Но есть область, где натуральные дроби в высшей степени оправданы — недвижимость. Доли собственников хранятся в натуральных дробях, например у Васи — 1/2, у Пети — 1/3, у Маши — 1/6. В сумме они дают единицу. Предположим, Вася поделил свою долю 1/2 между Колей и Жорой: каждый получил по 1/4 от общей площади. Сумма долей по-прежнему дает единицу.

В недвижимости бывают и более сложные доли. Скажем, недавно я участвовал в сделке, где моя доля была 49/125. Так получилось в результате наследства, выкупа долей у других собственников и так далее.

Приступая к делению, подумайте: можно ли выразить его натуральной дробью. Не всегда это возможно, но если все-таки да, вы обезопасите себя от проблем, связанных с флотами. Об этих проблемах мы поговорим в следующих заметках.