Автор оригинала: FreeCodeCamp Community Member.
Шалва
Некоторое время назад я вдохновил, чтобы построить приложение для решения конкретных видов математических проблем. Я обнаружил, что я должен был разбирать выражение в Абстрактное синтаксическое дерево Поэтому я решил построить прототип в JavaScript. Работая на парсере, я понял, что токенизатор должен был быть построен первым. Я пойду на тебя, как сделать один сам. (Предупреждение: его проще, чем оно выглядит сначала.)
Что такое токенизатор?
А Токенизатор это программа, которая разбивает выражение в единицы под названием токены Отказ Например, если у нас есть выражение, такое как «я большой жирный разработчик», мы могли бы ослабить его по-разному, например:
Используя слова как токены,
0 => I'm1 => a2 => big3 => fat4 => developer
Использование персонажей без пробела в качестве токенов,
0 => I1 => '2 => m3 => a4 => b…16 => p17 => e18 => r
Мы могли бы также рассмотреть все персонажи как токены, чтобы получить
0 => I1 => '2 => m3 => (space)4 => a5 => (space)6 => b…20 => p21 => e22 => r
Вы получаете идею, верно?
Токенизаторы (Также называется лексерами) используются в разработке компиляторов для языков программирования. Они помогают компилятору сделать структурный смысл из того, что вы пытаетесь сказать. В этом случае мы строим один для математических выражений.
Токены
Действительное выражение математики состоит из математически действительных токенов, которые для целей настоящего проекта могут быть Литералы , Переменные , Операторы, функции или Функциональные сепараторы аргумента Отказ Несколько замечаний по вышеуказанному:
- Литерал – это необычное имя для числа (в этом случае). Мы позвольте номера всего или только десятичной формы.
- Переменная – это вид, который вы используете в математике: A, B, C, X, Y, Z. Для этого проекта все переменные ограничены односторонними именами (поэтому ничего не похоже на var1 или цена ). Это так, что мы можем сделать выражение, как мА Как продукт переменных м и А , а не одна единая переменная мА Отказ
- Операторы действуют по литералам и переменным и результатам функций. Мы позволим операторам +, -, *,/и ^.
- Функции являются «более продвинутыми» операциями. Они включают в себя такие вещи, как SIN (), COS (), TAN (), MIN (), MAX () ETC
- Разделитель аргумента функции – это просто воплощение для запятой, используется в контексте, как это: Макс (4, 5) (максимум один из двух значений). Мы называем это сепаратором аргумента функция, потому что он, ну разделяет аргументы функций (для функций, которые принимают два или более аргументов, таких как Max и min ).
Мы также добавим два токена, которые обычно не считают токенами, но помогут нам с ясностью: Левый и Правые скобки Отказ Вы знаете, что это.
Несколько соображений
Неявное умножение
Неявное умножение просто означает позволяя пользователю писать «сокращение» мультипликаций, таких как 5x вместо 5 * х Отказ Принимая его шаг дальше, он также позволяет делать это с функциями ( 5SIN (x) = 5 * SIN (X) ).
Далее он позволяет 5 (x) и 5 (SIN (X)). У нас есть возможность разрешить это или нет. Компромиссы? Не позволяя ему на самом деле сделать токенизацию проще и позволило бы для многобуквенных имен переменной (имена, такие как цена
). Позволяя сделать платформу более интуитивно понятной для пользователя, и хорошо предоставляет дополнительную проблему для преодоления. Я решил позволить этому.
Синтаксис
Хотя мы не создаем языку программирования, нам нужно иметь некоторые правила о том, что делает допустимое выражение, поэтому пользователи знают, что вводить, и мы знаем, что планировать. Точные условия, Математические токены должны быть объединены в соответствии с этими синтаксическими правилами для эффективного выражения. Вот мои правила:
- Токены могут быть разделены 0 или более персонажами пробелов
2+3, 2 +3, 2 + 3, 2 + 3 are all OK 5 x - 22, 5x-22, 5x- 22 are all OK
Другими словами, Интервал не имеет значения (За исключением литерала 22).
2. Функциональные аргументы должны быть в скобках ( Sin (Y) , COS (45) , а не Sin y , Cos 45 ). (Почему? Мы будем удалять все пробелы со строки, поэтому мы хотим знать, где функция начинается и заканчивается без необходимости делать некоторые «гимнастики».)
3. Неявное умножение разрешено только между Литералы и переменные или Литералы и функции В этом порядке (то есть литералы всегда приходят первыми) и могут быть с или без скобок. Это означает:
- A (4) будет рассматриваться как функциональный звонок, а не A * 4.
- A4 не разрешено
- 4А и 4 (а) ОК
Теперь давайте добраться до работы.
Моделирование данных
Это помогает образец выражения в вашей голове, чтобы проверить это. Начнем с чего-то Базового: 2y + 1.
То, что мы ожидаем, это массив, который перечисляет различные токены в выражении, наряду с их типами и значениями. Так что для этого случая мы ожидаем:
0 => Literal (2)1 => Variable (y)2 => Operator (+)3 => Literal (1)
Во-первых, мы определим класс токена, чтобы облегчить вещи:
function Token(type, value) { this.type = type; this.value = value}
Алгоритм
Далее давайте построим скелет нашего функции токенизатора.
Наш токенизатор пройдет через каждый символ утра
Массив и строить токены на основе значения, находящегося.
[Обратите внимание, что мы предполагаем, что пользователь дает нам действительное выражение, поэтому мы пропустим любую форму проверки во всем этом проекте.]
function tokenize(str) { var result=[]; //array of tokens // remove spaces; remember they don't matter? str.replace(/\s+/g, "");
// convert to array of characters str=str.split("");
str.forEach(function (char, idx) { if(isDigit(char)) { result.push(new Token("Literal", char)); } else if (isLetter(char)) { result.push(new Token("Variable", char)); } else if (isOperator(char)) { result.push(new Token("Operator", char)); } else if (isLeftParenthesis(char)) { result.push(new Token("Left Parenthesis", char)); } else if (isRightParenthesis(char)) { result.push(new Token("Right Parenthesis", char)); } else if (isComma(char)) { result.push(new Token("Function Argument Separator", char)); } });
return result;}
Код выше довольно простой. Для справки, помощники Isdigit ()
, Лислет ()
, Isoperator ()
, Isleftparentcesis ()
и ISRightParenthesis ()
Определены следующим образом (не боитесь символами – это называется Regex , и это действительно потрясающе):
function isComma(ch) { return (ch === ",");}
function isDigit(ch) { return /\d/.test(ch);}
function isLetter(ch) { return /[a-z]/i.test(ch);}
function isOperator(ch) { return /\+|-|\*|\/|\^/.test(ch);}
function isLeftParenthesis(ch) { return (ch === "(");}
function isRightParenthesis(ch) { return (ch == ")");}
[Обратите внимание, что нет Isfunction () , isliteral () или Isvariable () функции, потому что мы проверяем персонажи индивидуально.]
Так что теперь наш парсер на самом деле работает. Попробуйте по этим выражениям: 2 + 3, 4A + 1, 5x + (2y), 11 + SIN (20.4).
Все хорошо?
Не совсем.
Вы будете наблюдать, что для последнего выражения, 11 сообщается как Два Буквальные токены вместо одного. Также грех
сообщается как Три токены вместо одного. Почему это?
Давайте паузом на мгновение и подумайте об этом. Мы тонкризовали персонаж массива по символу, но на самом деле некоторые из наших токенов могут содержать несколько символов. Например, литералы могут быть 5, 7,9, .5. Функции могут быть грех, COS и т. Д. Переменные являются только одно символами, но могут происходить вместе в неявном умножении. Как мы решаем это?
Буферы
Мы можем исправить это, реализовав буфер. Два, на самом деле. Мы будем использовать один буфер для удержания буквальных символов (чисел и десятичную точку) и один для букв (которые охватывают как переменные, так и функции).
Как работают буферы? Когда токенизатор сталкивается с числом/десятичной точкой или буквой, он подталкивает его в соответствующий буфер, и продолжает делать это, пока он не входит в другой тип оператора. Его действия будут варьироваться в зависимости от оператора.
Например, в выражении 456.7xy + 6Sin (7.04x) – Min (A, 7) он должен идти по этим строкам:
read 4 => numberBuffer read 5 => numberBuffer read 6 => numberBuffer read . => numberBuffer read 7 => numberBuffer x is a letter, so put all the contents of numberbuffer together as a Literal 456.7 => result read x => letterBuffer read y => letterBuffer + is an Operator, so remove all the contents of letterbuffer separately as Variables x => result, y => result + => result read 6 => numberBuffer s is a letter, so put all the contents of numberbuffer together as a Literal 6 => result read s => letterBuffer read i => letterBuffer read n => letterBuffer ( is a Left Parenthesis, so put all the contents of letterbuffer together as a function sin => result read 7 => numberBuffer read . => numberBuffer read 0 => numberBuffer read 4 => numberBuffer x is a letter, so put all the contents of numberbuffer together as a Literal 7.04 => result read x => letterBuffer ) is a Right Parenthesis, so remove all the contents of letterbuffer separately as Variables x => result - is an Operator, but both buffers are empty, so there's nothing to remove read m => letterBuffer read i => letterBuffer read n => letterBuffer ( is a Left Parenthesis, so put all the contents of letterbuffer together as a function min => result read a=> letterBuffer , is a comma, so put all the contents of letterbuffer together as a Variable a => result, then push , as a Function Arg Separator => result read 7=> numberBuffer ) is a Right Parenthesis, so put all the contents of numberbuffer together as a Literal 7 => result
Полный. Вы получаете это сейчас, верно?
Мы получаем там, всего несколько случаев для обработки.
Это точка, где вы садитесь и глубоко подумаете о своем алгоритме и моделировании данных. Что произойдет, если мой текущий персонаж является оператором, и номер убийства не пустым? Можно ли оба буфера когда-либо одновременно быть не пустыми?
Вместе все вместе, вот то, что мы придумываем (значения слева от стрелки изображают наш текущий тип символа (CH) ,,, скобка, скобка, скобка
loop through the array: what type is ch?
digit => push ch to NB decimal point => push ch to NB letter => join NB contents as one Literal and push to result, then push ch to LB operator => join NB contents as one Literal and push to result OR push LB contents separately as Variables, then push ch to result LP => join LB contents as one Function and push to result OR (join NB contents as one Literal and push to result, push Operator * to result), then push ch to result RP => join NB contents as one Literal and push to result, push LB contents separately as Variables, then push ch to result comma => join NB contents as one Literal and push to result, push LB contents separately as Variables, then push ch to result
end loop
join NB contents as one Literal and push to result, push LB contents separately as Variables,
Две вещи, чтобы отметить.
- Уведомление, где я добавил «Push Operator * для результата»? Это нас преобразует неявное умножение на явную. Кроме того, при опустении содержимого LB отдельно в качестве переменных мы должны не забывать вставлять оператор умножения между ними.
- В конце цикла функций нам нужно помнить, чтобы опустошить все, что мы оставили в буферах.
Перевод его в код
Вместе все вместе, ваша функция Tokenize должна выглядеть так:
Мы можем запустить немного демо:
var tokens = tokenize("89sin(45) + 2.2x/7");tokens.forEach(function(token, index) { console.log(index + "=> " + token.type + "(" + token.value + ")":});
Упаковывать его вверх
Это точка, в которой вы проаналируете свою функцию и измерить, что это делает против того, что вы хотите сделать. Задайте себе вопросы, такие как: «Работает ли функция как предназначена?» и “я покрыл все краевые случаи?”
Краевые чехлы для этого могут включать отрицательные числа и тому подобное. Вы также запускаете тесты на функцию. Если в конце вы довольны, вы можете затем начать искать, как вы можете улучшить его.
Спасибо за прочтение. Пожалуйста, нажмите на маленькое сердце, чтобы порекомендовать эту статью и поделиться, если вам это наслаждается! И если вы пробовали еще один подход для построения математического токена, дайте мне знать в комментариях.