Математика в PHP

Математические операции это то, для чего изначально были созданы компьютеры и языки программирования. Сегодня акцент сместился в сторону котиков и фотографий еды, но периодически все-таки надо что-то посчитать. Какие возможности для работы с математикой есть в PHP?

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

Основы

Для начала стоит рассмотреть операторы и встроенный модуль Math. Собственно тут никаких сюрпризов. В PHP математические операторы ничем не отличаются от таковых в мейнстримных языках за исключением, пожалуй, того, что “+” не перегружен, как это часто бывает. Есть побитовые операторы, на которых в некоторых ситуациях можно делать очень эффективную и быструю логику. В языке есть 2 типа числовых данных int и float и они автоматически приводятся к более общему.

Для тех, у кого в языках много типов чисел специальное замечание: int и float в PHP это на самом деле long и double

Так что возникают ситуации так любимые программистами на JS с результатами вычислений вроде 5.000000001. JavaScript я, кстати, упомянул не случайно – он куда чаще оказывается героем историй с округлением из-за меньшей точности float. Просто попробуйте выполнить в консоли 0.1 + 0.2.

// PHP
var_dump(0.1 + 0.2);    //  float(0.3)

// JS
console.log(0.1 + 0.2); //  0.30000000000000004

Но вернемся к PHP. Модуль Math дополнительно определяет ряд функций для работы с этими числами. Весь список приводить смысла нет, он есть на php.net, поэтому дам лишь качественную характеристику тому, что там есть. Здесь полный набор базовых тригонометрических функций, различные функции округления, логарифмические функции и несколько методов для работы со случайными числами. Что касается последнего, то случайность там весьма условна и на нее не стоит полагаться, если нужно по настоящему случайное число.

Также в модуле есть некоторое количество предопределенных констант. Это, разумеется, специальные числа, такие как PI (привет людям, у которых свое собственное ПИ), число Эйлера (e), log_2 e, lg e, ln 2 и многие другие. Подробнее можно ознакомиться здесь. Что интересно, есть даже определенные константы для корня из 2 и корня из 3. Действительно для многих математических функций используются эти числа и каждый раз вызывать sqrt(2) было бы очень дорого, поэтому можно просто обратиться к константе M_SQRT2.

Собственно это покрывает примерно 95% потребностей в математике при работе с PHP. Случаев, когда стандартная библиотека не подходит действительно немного, но они есть. Либо это специфические расчеты (физика, статистика), формулы которых можно реализовать с помощью того, что описано выше, но это будет громоздко и скорее всего приведет к ошибкам. Также проблематично будет работать с финансовыми и любыми другими операциями, требующими абсолютной точности. Поэтому в языке есть несколько модулей для более хитрой математики и ниже я их рассмотрю.

BC Math

Первый из таких полезных модулей – это BC Math. Он может использоваться для вычислений произвольной точности, вплоть до 2147483647 десятичных знаков, при этом числа представляются в виде строк. В виде чисел их также можно передавать, но тогда они будут порезаны при больших размерах. Данное расширение дает возможность избежать врожденного порока чисел с плавающей запятой и одновременно снимает с нас ограничения по максимально возможному используемому числу. Точнее ограничение все-таки есть, но оно ощутимо больше количества атомов во Вселенной и едва ли помешает на практике.

Давайте посмотрим, что можно сделать с помощью BC Math:

<?php
bcscale(10);

$number = 36 + (-35.99);
var_dump($number);

$bc_number = bcadd('36', '-35.99');
var_dump($bc_number);

//  9223372036854775807 - PHP_INT_MAX: все что больше, 
//  будет записываться экспоненциальной записью
$bigNumber = 9223372036854775807 * 78.99;
var_dump($bigNumber);

$bcBigNumber = bcmul('9223372036854775807', '78.99');
var_dump($bcBigNumber);

Сначала мы установим необходимое количество знаков после запятой с помощью функции bcscale(). По умолчанию это число равно 0 – и это эквивалентно использованию целых чисел. Также установить значение scale можно при вызове любой функции модуля для конкретного выражения, передав ее в качестве параметра. Наконец, это делается глобально с помощью настройки bcmath.scale в php.ini. После установки точности сами вычисления. В первом примере bcadd() справился с точным вычислением дробной части, а во второй вернул в результате удобно отформатированное число, вместо экспоненты.

Но это всего лишь форматирование. В конце концов PHP все-таки может работать с такими числами, просто “прячет” их от пользователя. Чтобы продемонстрировать всю мощь BC Math нужны действительно большие числа. Например такое:

178999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999

В экспоненциальной записи оно выглядит вот так: float(1.79E+308). Это лишь немного меньше PHP_FLOAT_MAX, которое равно float(1.7976931348623E+308). Добавление еще одного знака в хвост этого числа приведет к тому, что оно будет отображаться как INF – то есть по мнению php станет бесконечным. При этом при таких масштабах точность сохранения чисел во float удручающе низкая: если отнять от нашего великана единицу или даже тысячу, то новое число будет считаться равным исходному.

По правде говоря все настолько плохо, что можно отнять даже 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999900000000000000000000000000000000000000000000000000000000000000000000000000000000000000999999999999999999999999999999999999999999999999 и разность все равно будет равна изначальному числу 🙂

Винить PHP тут не в чем, это стандарт IEEE 754 и он используется везде, где в программировании есть дроби.

Как же справится с такими числами BC Math? Я отнял от суперчисла выше единицу, а также умножил его на 25000. Не буду утомлять перепечаткой огромных простыней символов, просто скажу, что отработало это корректно. Функции bc* способны переварить и большие числа.

Официального ОО-интейфейса для BC Math нет, только функции, которые реализованы для всех основных математических действий (сложение, вычитание, умножение, etc). Подробно ознакомиться с функционалом данного модуля можно в документации

GNU Multiple Precision

Данное расширение позволяет работать с целыми числами произвольной точности. То есть делать все то же самое, что рассматривалось в предыдущем абзаце. А вот с дробями работать не выйдет. Возникает вопрос: зачем нужен GMP, если есть BC Math, который имеет даже больший функционал?

Дьявол, как обычно, кроется в деталях. В GMP используется собственное представление чисел, благодаря чему он работает гораздо быстрее. GMP нацелена на то, чтобы быть самой быстрой библиотекой для работы с большими числами. Вот старый бенчмарк, но алгоритмы, лежащие в основе библиотек с тех пор едва ли поменялись, а значит и соотношение производительности скорее всего примерно такое же.
Также введен специальный класс GMP, с которым работают функции данного модуля – и это одно из немногих мест в php, где реализована перегрузка операторов. Подробнее почитать про перегрузку можно в этом RFC. А ознакомиться с документацией к модулю на php.net.

Что еще есть в PHP на эту тему?

Статистика https://www.php.net/manual/ru/book.stats.php. С помощью этого модуля можно сделать то, что нынче обычно поручают питонистам. Квадратичное отклонение, распределение Коши, дисперсия – вы наверняка не захотите делать это на php, но если придется, то инструмент в наличии.

Technical Analysis for Tradershttps://www.php.net/manual/ru/book.trader.php. Это расширение стало для меня настоящим открытием. Все, что было выше, могло использоваться для решения задач широкого профиля как инструмент. Круг же решаемых задач модуля “Trader” крайне специфичен, только посмотрите на названия методов: “Три черных ворона”, “Брошенное дитя”, “Висельник”, “Прилипший сэндвич”. У трейдеров богатая фантазия.

Lapack https://www.php.net/manual/ru/book.lapack.php. По этому расширению информации практически нет. Используется для решения задач по линейной алгебре.

Что в итоге?

PHP обладает базовым набором расширений для реализации достаточно сложной логики, связанной с математическими вычислениями. Конечно если выполнение таких операций главная цель приложения, стоит задуматься над другими ЯП. Даже если это не так, сложная математика может быть вынесена в отдельный сервис. Однако принципиальная возможность решить любую задачу подобного плана с помощью PHP имеется.