Skip to content

Latest commit

 

History

History
311 lines (230 loc) · 30 KB

File metadata and controls

311 lines (230 loc) · 30 KB

PhpSimpleExpression

Это парсер и вычислетель для простых выражений. Может быть использован для вычисления формул или строк с подстановками, вводимых пользователем.

Требует PHP 5.3 или выше

Использует замыкания функций (Closure, которые были представлены в PHP 5.3) чтобы вычислять выражения быстро, но, в то же время, не используя eval, безопасно изолируя введённый пользователем код от среды исполнения.

Реализует модель: "компилируется однажды - вычисляется многократно". Одно выражение может быть вычислено с разными значениями переменных сотни тысяч раз за секунду.

Работает в двух режимах:

Режим одиночного выражения

Это режим по-умолчнию. В нём исходная строка воспринимается как одно выражение. Например: x ^ 2 + sqrt(y) * 4

  $expression = "x ^ 2 + sqrt(y) * 4"; 
  $se = new SimpleExpression($expression); // Компилирование выражения с использованием контекста по-умолчанию
  ...
 
  $vars = array('x' => 20, 'y' => 16); // Список значений переменных
  $result = $se->run($vars); // Вычисление выражение с использованием данного списка
  
  print $result . PHP_EOL; // Выводит результат: 416

Режим подстановок

В этом режиме исходная строка рассматривается как простой текст, в котором встречаются выражения-подстановоки, заключённые в квадратные скобки. Например: У меня есть [num_of_apple] яблок[num_of_apple % 100 >= 10 & num_of_apple % 100 < 20 | num_of_apple % 10 >= 5 | num_of_apple % 10 = 0 ? '' : num_of_apple > 1 ? 'а' : 'о']

  $expression = "У меня есть [num_of_carrot] морков[num_of_carrot % 100 >= 10 & num_of_carrot % 100 < 20 | num_of_carrot % 10 >= 5 | num_of_carrot % 10 = 0 ? 'ок' : num_of_carrot > 1 ? 'ки' : 'ка'], [num_of_apple] яблок[num_of_apple % 100 >= 10 & num_of_apple % 100 < 20 | num_of_apple % 10 >= 5 | num_of_apple % 10 = 0 ? '' : num_of_apple > 1 ? 'а' : 'о'], [num_of_banana] банан[num_of_banana % 100 >= 10 & num_of_banana % 100 < 20 | num_of_banana % 10 >= 5 | num_of_banana % 10 = 0 ? 'ов' : num_of_banana > 1 ? 'а']"; 
  $se = new SimpleExpression($expression, true, true); // Выражение компилируется с использованеим контекста по-умолчанию
  // `true` во втором параметре означает использование контекста по-умолчанию. В третьем параметре `true` выбирает режим подстановок

  $vars = array(
    'num_of_carrot' => 20,
    'num_of_apple' => 1,
    'num_of_banana' => 3
   );

  print $se->run($vars) . PHP_EOL; // У меня есть 20 морковок, 1 яблоко, 3 банана

Чтобы пользователю не приходилось вводить подобные длинные формулы, более правильным вариантом было бы изготовление соответствующей функции

Контексты (класс SimpleContext)

Класс SimpleContext содержит информацию о функциях и именованных констнантах, которые могут быть использованы в выражениях Контексты могут быть объединены в иерархическую структуру, в этом случае, если функция или константа не найдена в конкретном контексте, поиск продолжается в родительском и так далее.

Существует созданный в единственном экземпляре контекст по-умолчанию, который описывает несколько полезных функций и константу PI. Его можно получить следующим образом: SimpleContext::getDefaultContext()

Добавление или удаление функций и констант в контексте по-умолчанию, затронет все последующие компиляции выраженый, которые используют контекст по-умолчанию. Чтобы использовать все представленные по-умолчанию функции, но не затрагивая сам контекст, можно создать новый контекст, указав контекст по-умолчанию в качестве родительского. Это может быть сделано несколькими способами.

$default_context = SimpleContext::getDefaultContext();
$my_context = new SimpleContext($default_context); // Родительский контекст может быть передан как параметр конструктора SimpleContext.

$my_context = new SimpleContext(true); // Если передано булево значение `true`, то в качестве родительского будет использован контекст по-умолчанию.

$default_context = SimpleContext::getDefaultContext();
$my_context = $default_context->derive(); // Или же может быть использован метод `derive` контекста, который создаст новый контекст, с текущим контекстом в качестве родительского

Чтобы создать корневой контекст, вызовите конструктор без параметров или передав NULL: $my_context = new SimpleContext();

Константы

Чтобы зарегистрировать константу, используйте методы registerConstant(name, value) или registerConstants(array).

$my_context->registerConstant('THETA', 0.000001); // метод registerConstant позволяет зарегистрировать одну константу

$my_context->registerConstants(array('MAX_WIDTH' => 128, 'MAX_HEIGHT' => 64)); // registerConstants принимает массив для массовой регистрации констант

Чтобы удалить константу, используйте метод unregisterConstant. Передайте имя удаляемой константы, либо массив имён, либо '*' для удаления всех констант из текущего контекста. Эта операция не затрагивает родительские контексты.

ВНИМАНИЕ: константа со значением NULL может быть зарегистрирована в контексте, но во время компиляции выражения, такая константа будет считаться необъявленной и её имя будет использовано для обращения к переменной. NULL можно использовать, чтобы заглушить какую-либо константу, объявленную в родительском контексте, если такое имя требуется для переменной.

Функции

Можно объявить функции, которые будут использованы в выражениях.

Чтобы зарегистрировать функцию, используйте метод registerFunction(function[, alias[, is_volatile]]). function может быть именем функции или замыканием. alias позволяет задать альтернативное имя, под которым функция будет вызываться из выражения. Установите is_volatile в true, если функция имеет побочные эффекты, т.е. может возвращать разные значения при одинаковом наборе параметров. rand, date - пример таких функций. В противном случае, если на вход функции переданы константы, то её значение будет вычисленно на этапе компиляции и функция будет заменена полученным выражением.

  function my_func($a, $b) {
    return $a - $b * 2;
  }
  $my_context->registerFunction('my_func'); // Проще всего зарегистрировать функцию, передав её имя в `registerFunction`;

  // Передача замыкания
  $f = function($x) { 
    return $x * $x * 2; 
  }; 
  $my_context->registerFunction($f, 'second_func'); // Регистрирует функцию под указанным именем

  $my_context->registerFunction('time', 'get_time', true); // Регистрирует volatile функцию под альтернативным именем.

Также можно использовать метод registerFunctions(array[, is_volatile]), чтобы зарегистрировать множество функций одновременно. Если ключ элемента массива не числовой, то он используется как alias.

Чтобы удалить зарегистрированную функцию, используйте метод unregisterFunction, передав имя (alias), массив имён или '*', чтобы удалить всё.

ВНИМАНИЕ: В процессе компиляции все именованные константы заменяются на их значения, а все вызовы функций замещаются ссылкой на ReflectionFunction. Таким образом, никакая информация об использованном контексте не сохраняется, и изменение контекста после компиляции выражения никак не повлияет на скомпилированное выражение.

Контекст по-умолчанию

Контекст по-умолчанию содержит константу PI, а также определяет несколько функций:

Имя функции Комментарий
sin(x) Синус
cos(x) Косинус
asin(x) Арксинус
acos(x) Арккосинус
tan(x) Тангенс
atan(x) Арктангенс
atan2(x, y) Арктангенс от y / x в корректном квадранте (см.)
deg2rad(x) Преобразование градусов в радианы
rad2deg(x) Преобразование радиан в градусы
abs(x) Абсолютное значение
floor(x) Округление до целого вниз
ceil(x) Округление до целого вверх
round(x[, precision]) Округление до ближайшего значения (см.)
exp(x) Экспонента (e ^ x)
sqrt(x) Квадратный корень
hypot(x, y) Гипотенуза (Квадратный корень суммы квадратов)
ln(x) Натуральный логарифм (см. примечания)
log(x, base) Логарифм по произвольному основанию (см. примечания)
lg(x), log10(x) Логарифм по основанию 10 (см.)
min(...) Меньшее значение из списка аргументов
max(...) Большее значение из списка аргументов
substr(string, start[, length]) Подстрока (см.)
strlen(string) Длина строки
upper(string) Приведение строки к верхнему регистру (см.)
lower(string) Приведение строки к нижнему регистру (см.)
replace(search, replace, subject) Замена вхождений search на replace в строке subject (см.)
regexp(pattern, subject) Выполняет сопоставление регулярному выражению (Использует функцию php preg_match)
regexp_replace(pattern, replacement, subject[, limit]) Выполняет замену по регулярному выражению (Использует функцию php preg_replace)
number_format(number[, decimals[, dec_point, thousands_sep]]) Форматирует число (см.)
format(format, ...) Форматирует строку (Использует функцию php sprintf)
random([a[, b]]) (volatile) Возвращает случайное число: 1) если без параметров, то не меньше нуля, но меньше единицы; 2) если с одним параметром, то целое, меньше указанного числа, но не меньше нуля; 3) если оба параметра указаны, то целое число в диапазоне между первым и вторым включительно. Если второй параметр меньше первого, то первые возвращается.
date(format[, timestamp]) (volatile) форматирует дату или время в строку (см.)

NOTES:

  • Некоторые функции зарегистрированы под другими именами, не так, как они обявлены в php. Это сделано чтобы сохранить одинаковые имена функций использующихся в подобных парсерах, написанных на разных языках.
  • Функции ln и log - обе псевдонимы для функции php log, и, следовательно, имеют одинаковый функционал и необязательный второй параметр. Но, для совместимости, рекомендуется использовать ln для вычисления натуральных логарифмов и log - с произвольным основанием.

Синтаксис выражений

Поддерживаются:

  • Одноместные операции + (преобразование к числу), - (отрицание), ! (логическая инверсия)
  • Математические операции: '+' (сложение), '-' (вычитание), * (умножение), / (деление), '%' (остаток), ^ or ** (возведение в степень);
  • Логические операции: & (логическое И), | (логическое ИЛИ), ^^ (логичсеское ИСКЛЮЧАЮЩЕЕ ИЛИ)
  • Сравнения: = (равно), <> или != (не равно), > (больше), >= (больше или равно), < (меньше), <= (меньше или равно)
  • Трёхместный оператор сравнения <условие> ? <выражение_если_истина> [: <выражение_если_ложь>] (Если последняя часть опущена, то на её месте подразумевается пустая строка)
  • Операция строковой конкатенации #. А также доступен включаемый в настройках режим неявной строковая конкатенация (выражения без оператора между ними рассматриваютися как конкатенация их строковых значений)
  • Скобки
  • Вызовы функций
  • Именованные константы
  • Обращения к переменным

ВНИМАНИЕ: операции деления и взятия остатка, в случае если делитель равен нулю, обрабатываются особым образом, возвращая INF, -INF или NAN. Это сделано, чтобы избежать предупреждений PHP в случае появления деления на ноль.

Числовые константы начинаются с цифры и могут содержать точку в качестве десятичного разделителя. Строковые константы заключаются в одиночные или двойные кавычки (' or "). Чтобы отобразить используемую кавычку в строке её следует напечатать дважды.

Идентификаторы состоят из букв и символа подчёркивания, и могут содержать цифры (кроме первой позиции)

Приоритеты операций (от большего к меньшему):

  1. Выражения в скобках вычисляются в первую очередь;
  2. Одноместные операции;
  3. Неявная строковая конкатенация, если включена в настройках (когда между выражениями нет оператора);
  4. Возведение в степень (^ или **);
  5. Умножение, деление, взятие остатка (*, /, %);
  6. Сложение и вычитание (+, -)
  7. Строковая конкатенация (#)
  8. Сравнение (=, <> or !=, >, >=, <, <=)
  9. Логическое И (&)
  10. Логическое ИЛИ (|)
  11. Логическое ИСКЛЮЧАЮЩЕЕ ИЛИ (^^)
  12. Трёхместный условный оператор

ВНИМАНИЕ: Оператор сравнения обладает наименьшим приоритетом. Это значит, что всё выражение слева от ? будет воспринято, как условие, а всё выражение справа от : - как блок иначе. Это позволяет объединять несколько условных операторов, образуя селекторы: условие1 ? вариант1 : условие2 ? вариант2 : ... условиеN ? вариантN : иначе

Использование

На этапе компиляции может быть выброшено исключение SimpleExpressionParseError, метод getPosition() которого позволяет узнать позицию в исходной строке, на которой возникла ошибка

  $expression = "x ^ 2 + sqrt(y) * 4"; 
  try {
    $se = new SimpleExpression($expression); // Компилирование выражения с использованием контекста по-умолчанию

    $vars = array('x' => 0, 'y' => 0);

    // Когда выражение скомпилировано, его можно вычислять многократно, используя разные значения переменных
    for ($x = 10 ; $x < 100 ; $x += 40) {
      for ($y = 10 ; $y < 100 ; $y += 40) { // Запускаем многократно
        $vars['x'] = $x;
        $vars['y'] = $y;

        $result = $se->run($vars);

        print "Для x = $x, y = $y, результат $result" . PHP_EOL;
      }
    }

  } catch (SimpleExpressionParseError $e) {
    print "Ошибка разбора выражения на позиции {$e->getPosition()}: {$e->getMessage()}" . PHP_EOL;

    print $expression . PHP_EOL;
    print str_repeat(' ', $e->getPosition()) . '^' . PHP_EOL; 
  }

Неявная конкатенация строк (опционально)

Настройка может быть включена вызовом ->implicitConcatenation(true) на объекте SimpleContext перед компиляцией выражения.

Когда настройка включена, любые идущие подряд выражения без оператора между ними, трактуются как конкатенация их строковых значений. Например:

    $my_context = new SimpleContext(true); // Если передано булево значние `true`, контекст по-умолчанию будет использован в качестве родительского
    $my_context->implicitConcatenation(true); // Включении настройки
    $expression = "'Здравствуй, ' obj '!'";
    $se = new SimpleExpression($expression, $my_context); // Компиляция выражения с использованием предоставленного контекста
    $vars = array('obj' => 'Мир');
    print $se->run($vars); // Prints "Здравствуй, Мир!";

В версии 1.0 это был единственный способ конкатенации строк, поэтому настройка была включена изначально. В версии 1.1 появился явный оператор конкатенации #, и неявная конкатенация по-умолчанию отключена.

Неявное обращение к переменным

NOTE: Все неизвестные идентификаторы на этапе компиляции будут трактованы как обращения к переменным. Во время исполнения все неизвестные переменные будут без предупреждений трактованы как NULL. Это может привести к нежелательным ситуациям.

Рассмотрим два выражения: "sin(x)" и "sinus(x)".

Первое (предполагая, что функция sin объявлена в контексте) будет скомпилировано как вызов функции, которой передаётся значение переменной x в качестве параметра. Второе (предполагая что sinus не объявлено, а неявная конкатенация включена) будет без ошибок скомпилировано в строковую конкатенацию переменных sinus и x.

Чтобы избежать подобных ошибок, можно проверить все имена переменных, использованные в выражении. Получить их список можно вызвав метод getVars() объекта SimpleExpression. Он вернёт массив, в котором ключи представляют имена переменных, а значения - позиции, на которой каждая переменная впервые встретилась в исходном выражении. Но чтобы упростить процесс проверки, можно просто вызвать метод checkVars(array), передав в качестве параметра массив, в котором ключи перечисляют все допустимые имена переменных Если выражение использует переменную отличную от перечисленных, то будет выброшено исключение SimpleExpressionParseError .

    $expression = 'sinus(x)';
    $se = new SimpleExpression($expression); // Компиляция выражения с использованием контекста по-умолчанию
    $vars = array('x' => 0, 'y' => 0);
    $se->checkVars($vars); // Исключение будет выброшено, поскольку `sinus` - неизвестная переменная

Нечувствительность к регистру

ВНИМАНИЕ: выражения нечувствительны к регистру. Имена всех переменных приводятся к нижнему регистру, поэтому имена ключей в массиве значений переменных, передаваемых методу run, должны быть в нижнем регистре

Оптимизации

В момент компиляции над выражением производятся некоторые оптимизации

Одна и наиболее важная оптимизация - это предвычисление константных выражений. Например в выражении sin(PI / 2) (подразумевая использование контекста по-умолчанию) PI будет заменена на значение константы, затем вычсилено PI / 2 и затем вычислено значение функции sin от констатного аргумента. Возвращенное значение будет подставлено как константное выражение на место функции, тем самым избегая повторного вычисления одного и того же выражения каждый раз.

Также некоторые менее очевидные оптимизации могут быть произведены. Такие как объединение вложеных операций конкатенации в одну последовательную, или комбинирование математических операндов (например (x * 4) / 2 => (x * 2) и т.п.)

Чтобы проверить как были выполнены оптимизации, можно использовать метод debugDump() у объекта SimpleExpression. Он вернёт текстовое представление дерева вычисления.

  $s = "(PI > 3) ? sin(x * 2 * PI) : sqrt(y)"; 
  $e = new SimpleExpression($s);
  print $e->debugDump();

Выведет @sin(({x} * (const 6.2831853071796)))

В этой строке @ означает вызов функции; {...} - обращение к переменной; (const ...) - константный элемент.

В данном выражение, PI - это константа, которая всегда больше 3, поэтому весь условный оператор заменяется его частью "если истинна", в которой именованная константа замещается значением, а две последовательные операции с константным выражением в правой части ... * 2 * PI объединяются в одну.

Что нового

Версия 1.1.0

  • Добавлен оператор # (явная строковая конкатенация)
  • Неявная строковая конкатенация (без операторов) тепрь по-умолчанию отключена, но может быть включена в SimpleContex (см. метод SimpleContext::implicitConcatenation())
  • Изменена логика условий и булевых операций. Теперь строка '0' (а также пустой массив) расматривается как 'истина'. NULL, false, 0, 0.0, '' по прежнему 'ложь'
  • & (и), | (или) and ^^ (исключающее или) более не "булевы" операторы. Тип их результата зависит от типа операндов: A | B эквивалентно A ? A : B A & B эквивалентно A ? B : A A ^^ B эквивалентно !A ? B : (!B ? A : '')
  • движок поддерживает обработчки ИЛИ-цепочки, которая используется, когда обнаружена конструкция вида A | B | C ..., она возвращает первый операнд, чьё значение приводится к булевому "истина", а если таковой не найден, то возвращает значение последнего в цепочке операнда.
  • Добавлены некоторые новые оптимизации.
  • Оптимизация (X * 0) => 0 удалена, для того, чтобы правильным образом обрабатывать значения NAN и INF выражения X