2013-12-04 2 views
1

Я хотел разобрать строку, которая имеет оператор if и оценить его вывод.Операторы анализа и оценки их в JavaScript

Я могу получить оценочную часть, но другая становится слишком сложной для меня.

Возьмите следующие примеры:

  • if $a == 10 && ($b == '5' || $c == 'test')
  • if $x != 10 || $y == false
  • if $z < 10

Я хочу, чтобы получить их в массив - то есть вывод, что я хочу для соответствующего примером может служить:

  • [ [ '$a', '==', '10' ], '&&', [ [ '$b', '==', '5' ], '||', [ '$c', '==', 'test' ] ] ]
  • [ [ '$x', '!=', '10' ], '||', [ '$y', '==', 'false' ] ]
  • [ '$z', '<', '10' ]

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

+0

Вы хотите что-то вроде: для первого условия возьмите два массива (один для условия и другой для оператора «&&», «||») и добавьте этот массив в список массивов. .. и то же самое делать для других запросов (второй и третий запрос)? Пожалуйста посоветуй. –

+1

Какой ваш вариант использования? Я думаю, что есть более простые альтернативы тому, что вы пытаетесь сделать. –

+0

Я хочу преобразовать оператор if в массив с помощью javascript –

ответ

5

Если вы просто хотите, чтобы разобрать вашу строку, то есть много JavaScript разбора библиотеки, которые могут сделать это для вас. Например, вы можете разобрать действительный Код JavaScript, используя acorn в Mozilla AST. Вы также можете преобразовать его обратно в строку, используя escodegen.

К сожалению, ваша строка не является действительным кодом JavaScript, но если вы удалите if в начале каждой строки, вы можете определенно проанализировать строку, используя acorn. Выход будет AST, который не является тем, что вы ищете, но вы можете легко преобразовать его в желаемый формат.

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

function read(expression) { 
    var variables = expression.match(/\$\w+/g); 
    var length = variables.length; 
    var uniqueVariables = []; 
    var index = 0; 

    while (index < length) { 
     var variable = variables[index++]; 
     if (uniqueVariables.indexOf(variable) < 0) 
      uniqueVariables.push(variable); 
    } 

    return Function.apply(null, uniqueVariables.concat("return " + expression)); 
} 

Эта read функция позволяет читать выражения следующим образом:

var condition = read("$a == 10 && ($b == '5' || $c == 'test')"); 

Теперь вы можете использовать condition функции следующим образом:

alert(condition(10, "10", "test")); // true 
alert(condition(5, "10", "test")); // false 

Смотрите демо для себя: http://jsfiddle.net/ZnUh2/

Конечно, вам нужно удалить if в начале всех ваших строк, чтобы их прочитать.Это можно легко сделать, используя string.slice(2), чтобы удалить if.

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

var lexer = new Lexer; 

lexer.addRule(/\s+/, function() { /* skip whitespace */ }); 

lexer.addRule(/if\b/g, function() { /* skip the if keyword */ }); 

// match opening parentheses 
lexer.addRule(/\(/, function() { return "("; }); 

// match closing parentheses 
lexer.addRule(/\)/, function() { return ")"; }); 

// match any other token 
lexer.addRule(/[^\s\(\)]+/, function (lexeme) { return lexeme; }); 

Обратите внимание, что этот лексический ожидает каждый маркер (кроме скобок), чтобы иметь пробелы между ними. Например, $a==10 будет считаться одним токеном, но $a == 10 будет считаться 3 жетонами.

Следующее, что вам нужно, это рудиментарный парсер. Вы можете реализовать один за другим, но было бы больно писать все правила приоритета оператора самостоятельно. Вместо этого я предлагаю использовать следующие parser на основе Dijkstra's shunting yard algorithm.

Теперь мы можем создать парсер следующим образом:

var relational = { 
    precedence: 3, 
    associativity: "left" 
}; 

var equality = { 
    precedence: 2, 
    associativity: "left" 
}; 

var parser = new Parser({ 
    "<": relational, 
    "<=": relational, 
    ">": relational, 
    ">=": relational, 
    "==": equality, 
    "!=": equality, 
    "&&": { 
     precedence: 1, 
     associativity: "right" 
    }, 
    "||": { 
     precedence: 0, 
     associativity: "right" 
    } 
}); 

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

function parse(string) { 
    lexer.setInput(string); 

    var tokens = [], token; 

    while (token = lexer.lex()) tokens.push(token); 
    tokens = parser.parse(tokens); 

    var stack = [], length = tokens.length, index = 0; 

    while (index < length) { 
     token = tokens[index++]; 

     switch (token) { 
     case "<": 
     case "<=": 
     case ">": 
     case ">=": 
     case "==": 
     case "!=": 
     case "&&": 
     case "||": 
      var b = stack.pop(); 
      var a = stack.pop(); 
      stack.push([a, token, b]); 
      break; 
     default: 
      stack.push(token); 
     } 
    } 

    return stack.length && stack[0]; 
} 

Вот и все. Теперь вы можете разобрать вашу строку в массив следующим образом:

var array = parse("if $a == 10 && ($b == '5' || $c == 'test')"); 

Чтобы увидеть результат, вы можете использовать JSON.stringify. Посмотрите демо для себя: http://jsfiddle.net/d2UYZ/3/

+0

Отличный ответ, что лексер и парсер тоже выглядят хорошо. Я не понимаю, почему парсер берет и возвращает строку токенов, разделенных пробелами, вместо массива, хотя ... только с учетом какого-то другого кода? –

+0

@DaggNabbit Действительно. Я написал код для парсера 2 месяца назад для демонстрации. Следовательно, имеет смысл взять строку вместо массива токенов и вернуть строку вместо массива токенов. Поскольку код уже был опубликован как gist на GitHub, я решил не изменять его и просто «присоединить»/«разделить» вход и выход функции 'parser.parse'. Было бы лучше удалить этот ненужный бокс/unboxing вообще, но я был ленив, чтобы сделать это для простого ответа. –

+0

@DaggNabbit Я обновил суть, jsfiddle и мой ответ и удалил ненужное 'split' /' join'. –

1
var operators = ['==', '||', '&&']; // fill the list 
var str = "$a == 10 && ($b == '5' || $c == 'test') "; 
var expectedArr = [['$a','==','$b'], '&&', [['$b', '==', '5'], '||', ['$c', '==', 'test']]]; 

function parseStr(str) 
{ 
    var output = []; 
    var openParenthesis = str.indexOf('('); 
    var toAppend = []; 
    if (openParenthesis > -1) { 
     var closeParenthesis = str.lastIndexOf(')'); 
     var contents = str.substring(openParenthesis + 1, closeParenthesis - 1); 
     toAppend = parseStr(contents); 
    } 
    else { 
     openParenthesis = str.length; 
    } 

    var prefix = str.substring(0, openParenthesis); 
    var regexp = makeOperatorsRegex(); 
    var res; 
    while (res = regexp.exec(prefix)) { 
     console.log(res); 
     output.push(res[1].trim()); 
     output.push(res[2].trim()); 
    } 
    var lastIdx = output.length - 1; 
    if (operators.indexOf(output[lastIdx]) > -1) { 
     var tmpOutput = [[output.slice(0, lastIdx)], output[lastIdx]]; 
     output = tmpOutput; 
    } 
    for (var i in toAppend) { 
     output.push(toAppend[i]); 
    } 
    return output; 
} 

function makeOperatorsRegex() 
{ 
    var opRegex = '([a-zA-Z0-9\\$ ]*)('; 
    var first = true; 
    for (var i in operators) { 
     if (! first) { 
      opRegex += '|'; 
     } 
     opRegex += '(' + RegExp.quote(operators[i]) + ')'; 
     first = false; 
    } 
    opRegex += ')'; 

    return new RegExp(opRegex, 'g'); 
} 

RegExp.quote = function(str) { 
    return (str+'').replace(/([\=.?*+^$[\]\\(){}|-])/g, "\\$1"); 
}; 

function quote(value) 
{ 
    if (value[0] == '$') { 
     return value; 
    } 
    for (var i in operators) { 
     if (value == operators[i]) { 
      return value; 
     } 
    } 
    return '\'' + value + '\''; 
} 

function buildExpr(arr, values) { 
    var expr = ''; 
    for (var i in arr) { 
     if(Object.prototype.toString.call(arr[i]) === '[object Array]') { 
      expr += '(' + buildExpr(arr[i]) + ')'; 
     } 
     else { 
      expr += quote(arr[i]); 
     } 
    } 
    for (var k in values) { 
     expr = expr.replace(new RegExp('\\$' + k, 'g'), quote(values[k])); 
     expr = expr.replace(new RegExp('\\$' + k, 'g'), values[k]); 
    } 
    return expr 
} 

console.log(parseStr(str)); 

var values = { 
    'a': 5, 
    'b': 5, 
    'c': 'test' 
}; 
console.log(buildExpr(expectedArr, values)); 
console.log(eval(buildExpr(expectedArr, values))); 

Это почти работает. Вам просто нужно полировать здесь и там, и так будет. Когда your'e сделано, вы бы даже быть в состоянии сделать кругооборот, как это:

var str = "$a == 10 && ($b == '5' || $c == 'test') "; 
var array = parseStr(str); 
var newStr = buildExpr(array); 
if (newStr == str) { 
    console.log('ok'); 
} 
+3

Я думаю, что OP спрашивал, как разобрать это выражение на ' arr' в первую очередь? –

+0

Хорошо, вы правы, я обновлю мгновение. –

+0

Исправлено + предыдущий код можно использовать для кругового отключения. –

Смежные вопросы