Рубрики
Без рубрики

Разбор математических выражений с JavaScript

Shalvah разбавляет математические выражения с JavaScript, когда я писал о том, что токенизация математического выражения, с JavaScript как язык выбора. Токенизатор, который я построен в этой статье, был первым компонентом моего стремления для рендеринга и решения математических выражений, используя JavaScript или любой другой язык. В

от Шалвы

Данное назад я писал о Токенизация математического выражения, с JavaScript как язык выбора. Токенизатор, который я построен в этой статье, был первым компонентом моего стремления для рендеринга и решения математических выражений, используя JavaScript или любой другой язык. В этой статье я прогулюсь, как построить следующий компонент: парсер.

Какова работа парсера? Довольно просто. Это анализирует выражение. (Дух.) Хорошо, на самом деле, Википедия Хороший ответ:

Итак, по сути, это то, что мы пытаемся достичь:

math expression => [parser] => some data structure (we'll get to this in a bit)

Давайте пропустим немного немного: «… Парсер часто предшествует отдельный лексический анализатор, который создает токены из последовательности входных символов». Это говорит о токенкезе, который мы построили ранее. Таким образом, наш парсер не будет получать необработанное математическое выражение, а скорее массив токенов. Так что теперь у нас есть:

math expression => [tokenizer] => list of tokens => [parser] => some data structure

Для токенизатора мы должны были придумать алгоритм вручную. Для парсера мы будем реализовывать уже существующий алгоритм, алгоритм шунтирования. Помните «какую-то структуру данных» выше? С помощью этого алгоритма наш парсер может дать нам структуру данных, называемую абстрактным синтаксическим деревом (AST) или альтернативным представлением выражения, известного как обратную польскую запись (RPN).

Обратный польский обозначение

Я начну с RPN. Снова из Википедия RPN является «математической записью, в которой каждый оператор следует всем его операндам ». Вместо того, чтобы иметь, сказать, 3 + 4, RPN будет 3 4+. Странно, я знаю. Но правило заключается в том, что оператор должен прийти после все его операнды.

Сохраняйте это правило, поскольку мы взглянем на некоторые сложные примеры. Также помните, что операнд для одной операции может быть результатом более ранней операции).

Algebraic: 3 - 4                        RPN: 3 4 -
Algebraic: 3 - 4 + 5                    RPN: 3 4 - 5 +
Algebraic: 2^3                          RPN: 2 3 ^
Algebraic: 5 + ((1 + 2) × 4) − 3        RPN: 5 1 2 + 4 * + 3 -
Algebraic: sin(45)                      RPN: 45 sin
Algebraic: tan(x^2 + 2*x + 6)           RPN: x 2 ^ 2 x * + 6 + tan

Потому что оператор должен прийти после его операндов, RPN также известен как Постфикс Обозначения И наша «регулярная» алгебраическая обозначение называется инфикс .

Как вы оцениваете выражение в RPN? Там простой алгоритм, который я использую:

(Это упрощенный алгоритм, который предполагает, что выражение действителен. Пара индикаторов, что выражение недействительна, есть ли у вас более одного токена, оставленного в конце, или если последний токен слева – это оператор/функция.)

Итак, для чего-то вроде 5 1 2 + 4 * + 3 -:

read 5read 1read 2read +. + is an operator which takes 2 args, so calculate 1+2 and replace with the result (3). The expression is now 5 3 4 * + 3 -read 4read *. * is an operator which takes two args, so calculate 3*4 and replace with the result, 12. The expression is reduced to 5 12 + 3 -read +. + is an operator which takes two args, so calculate 5+12, replace by the result, 17. Now, we have 17 3 -read 3read -. - is an operator which takes two args, so calculate 17-3. The result is 14.

Надеюсь, вы сделали в моем маленьком курсе в RPN. Ты сделал? Хорошо, давайте перейдем дальше.

Абстрактные синтаксические деревья

Определение Википедии здесь может быть не слишком полезно для многих из нас: «Представление дерева абстрактной синтаксической структуры исходного кода, написанного на языке программирования. «Для этого случая использования мы можем подумать о AST как структуру данных, которая представляет математическую структуру выражения. Это лучше, чем сказано, так что давайте нарисуем грубую диаграмму. Я начну с AST для простого выражения 3 + 4:

  [+] /   \[3] [4]

Каждый [] представляет узел в дереве. Таким образом, вы можете увидеть с первого взгляда, что два жетона объединяются оператором +.

Более сложное выражение, 5 + ((1 + 2) * 4) – 3:

           [-]          /   \        [+]    \___[3]          /  \ [5]__/   [*]         /   \        [+]  [4]       /   \       [1]  [2]

Ах, прекрасное маленькое синтаксическое дерево. Он отлично связывает все токены и операторы. Вы можете видеть, что оценка этого выражения намного проще – просто следуйте дереву.

Итак, почему это полезно? Это поможет вам правильно представлять логику и структуру выражения, что облегчает оценку выражения. Например, чтобы оценить вышеуказанное выражение, на нашей бэкэнде мы могли сделать что-то вроде этого:

result = binaryoperation(+, 1, 2)result = binaryoperation(*, result, 4)result = binaryoperation(+, 5, result)result = binaryoperation(-, result, 3)return result

Другими словами, для каждого оператора (или функционального) узла, который встречает оценщик/компилятор/интерпретатор, он проверяет, сколько ветвей есть, а затем оценивает результаты всех этих ветвей с оператором.

Хорошо, круговой курс закончен, теперь вернулся к нашему парсеру. Наш парсер преобразует (токенизированное) выражение для RPN, а затем к AUT. Так что давайте начнем реализовать это.

Алгоритм шунтирования

Вот версия RPN полного алгоритма ( от нашего друга Википедии ) и модифицирована, чтобы соответствовать нашему токенизатору:

(Побочная заметка: в случае, если вы прочитаете более раннюю статью, я ue Обновил список распознанных токенов, чтобы включить функциональный аргумент сепаратора, aka comma).

Алгоритм выше предполагает, что выражение действительна. Я сделал это таким образом, чтобы легко понятно в контексте статьи. Вы можете просмотреть полный алгоритм На Википедии Отказ

Вы будете наблюдать пару вещей:

  • Нам нужны две структуры данных: A стек Удерживать функции и операторы, а очередь для вывода. Если вы не знакомы с этими двумя структурами данных, вот праймер для вас: если вы хотите получить значение из стека, вы начинаете с последнего, который вы ввели, тогда как для очереди вы начинаете с первого вы Путин.
// we'll use arrays to represent both of themvar outQueue=[];var opStack=[];
  • Нам нужно знать Ассоциативность операторов. Ассоциативность Проще означает, в каком порядке выражение, содержащее несколько операций того же рода, сгруппированы в отсутствие скобок. Например, 2 + 3 + 4 канонически оценивается слева направо (2+, затем 5 +), поэтому + имеет левую ассоциативность. Сравните это до 2 ^ 3 ^ 4, что оценивается как 2 ^ 81, а не 8 ^ 4. Таким образом, имеет правильную ассоциативность. Мы посылаем Associativitivity of O операторов хорошо встреча в JavaScriptbject:
var assoc = {  "^" : "right",  "*" : "left",  "/" : "left",  "+" : "left",  "-" : "left" };
  • Нам также нужно знать Приоритет операторов. Приоритет Это своего рода рейтинг, присвоенный операторам, поэтому мы можем знать, в каком порядке их следует оценивать, если они появляются в том же выражении. Операторы с более высоким приоритетом оцениваются в первую очередь. Например, * имеет более высокий приоритет, чем +, так что 2 + 3 * 4 оцениваются как 2 + 12, а не 5 * 4, если скобки не используются. + И – имеют одинаковый приоритет, поэтому 3 + 5 – 2 можно оценить как 8-2 или 3 + 3. Опять же, мы упаковываем о целях оператора в объекте:
var prec = {  "^" : 4,  "*" : 3,  "/" : 3,  "+" : 2,  "-" : 2 };

Теперь давайте обновим наш Токен Класс, чтобы мы могли легко получить доступ к приоритету и ассоциативности через методы:

Token.prototype.precedence = function() {  return prec[this.value]; };  Token.prototype.associativity = function() {  return assoc[this.value]; };
  • Нам нужен метод, который позволяет нам заглянуть в стеке (чтобы проверить элемент сверху, не удаляя его), и тот, который позволяет нам поп из стека (извлеките и удалите элемент сверху). К счастью, массивы JavaScript уже имеют поп () Метод, поэтому все, что нам нужно сделать, это реализовать PEEK () метод. (Помните, для стеках элемент в верхней части, который мы добавили в последний.)
Array.prototype.peek = function() {  return this.slice(-1)[0]; //retrieve the last element of the array };

Так вот что у нас есть:

function tokenize(expr) {  ...   // just paste the tokenizer code here}
function parse(inp){ var outQueue=[]; var opStack=[];
Array.prototype.peek = function() {  return this.slice(-1)[0]; };
var assoc = {  "^" : "right",  "*" : "left",  "/" : "left",  "+" : "left",  "-" : "left" };
var prec = {  "^" : 4,  "*" : 3,  "/" : 3,  "+" : 2,  "-" : 2 };
Token.prototype.precedence = function() {  return prec[this.value]; };  Token.prototype.associativity = function() {  return assoc[this.value]; };
 //tokenize var tokens=tokenize(inp);
 tokens.forEach(function(v) {   ...   //apply the algorithm here });
 return outQueue.concat(opStack.reverse());  // list of tokens in RPN}

Я не буду углубиться в реализацию алгоритма, поэтому я не охватил вас. Это довольно простая задача, практически перевод алгоритма алгоритма Word-для Word для кода, поэтому в конце дня вот что у нас есть:

TOSTRING Функция просто форматирует наш RPN список токенов в читаемый формат.

И мы можем проверить наш анализатор Infix-To-Postfix:

var rpn = parse("3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3");console.log(toString(rpn));

Выход:

3 4 2 * 1 5 - 2 3 ^ ^ / +

RPN !!

Время посадить дерево

Теперь давайте изменим наш парсер, чтобы он возвращает AST.

Чтобы генерировать AST вместо RPN, нам нужно сделать несколько модификаций:

  • Мы создадим объект для представления узла в нашем AST. Каждый узел имеет значение и две ветви (которые могут быть NULL ):
function ASTNode(token, leftChildNode, rightChildNode) {   this.token = token.value;   this.leftChildNode = leftChildNode;   this.rightChildNode = rightChildNode;}
  • Второе, что мы будем делать, это изменение нашей структуры вывода данных в стек. В то время как фактический код для этого просто для изменения линии var wurneue = [] к var outstack =. [] (Поскольку оно остается массивом), изменение ключа находится в нашем понимании и лечении этого массива.

Теперь, как наш алгоритм Infix-AST будет работать? В основном тот же алгоритм, с несколькими модификациями:

  1. Вместо того, чтобы толкать буквальный или переменный токен на наше Вперед , мы нажимаем новый узел, значение которого является токен, и чьи филиалы являются null на наше outstack
  2. Вместо того, чтобы выскакивать оператор/токен функции из Опстак мы заменяем два верхних узла на OutStack С одним узлом, значение которого является токеном, и у этого есть те два в качестве ветвей. Давайте создадим функцию, которая делает это:
Array.prototype.addNode = function (operatorToken) {  rightChildNode = this.pop();  leftChildNode = this.pop();  this.push(new ASTNode(operatorToken, leftChildNode, rightChildNode)); }

3. Теперь наш парсер должен вернуть один узел, узел в верхней части нашей AST. Его две ветви будут содержать два дочерних узла, филиалы которых будут содержать своих детей и так далее, в рекурсивном порядке. Например, для выражения, такого как 3 + 4 * 2/(1-5) ^ 2 ^ 3, мы ожидаем, что структура нашего выходного узла будет такой (в горизонтальной форме):

+ => 3 => null       => null  => / => * => 4 => null                 => null            => 2 => null                 => null       => ^ => - => 1 => null                      => null                 => 5 => null                      => null            => ^ => 2 => null                      => null                 => 3 => null                      => null

На диаграмме выше, то => представляют ветви узла (верхний узел – левая ветвь, нижняя часть – это правильная ветвь). Каждый узел имеет две ветви, а узлы в конце дерева имеют их указывание до n ул

Итак, если мы поставим все это вместе, вот код, который мы придумали:

А также Если мы демонстрируем это:

//a little hack I put together so it prints out in a readable formASTNode.prototype.toString = function(count) {   if (!this.leftChildNode && !this.rightChildNode)     return this.token + "\t=>null\n" + Array(count+1).join("\t") + "=>null";   var count = count || 1;   count++;   return this.token + "\t=>" + this.leftChildNode.toString(count) + "\n" + Array(count).join("\t") + "=>" + this.rightChildNode.toString(count);};
var ast = parse("3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3");console.log("" + ast);

И результат:

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

Концепции, которые мы охватываем в этой статье, и предыдущие, очень интересные темы в области компьютерной науки и теории языка. У меня все еще есть много, чтобы узнать о них, и я призываю вас идти вперед и исследовать их, если они вас интересуют. И Оставьте мне линию Чтобы дать мне знать о вашем прогрессе. Мир!

Оригинал: “https://www.freecodecamp.org/news/parsing-math-expressions-with-javascript-7e8f5572276e/”