2

My problem is to split a string which contains a logical operation. For example, here is my sample string:

var rule = "device2.temperature > 20 || device2.humidity>68 && device3.temperature >10"

I need to parse that string in a way that I can easily operate my logic and I am not sure which approach would be better.

PS: Please keep in mind that those rule strings can have 10 or more different condition combinations, like 4 ANDs and 6 ORs.

Mustafa Bereket
  • 129
  • 2
  • 13
  • Why do you want to do this? Where does the string come from? What is the logic that you want to easily operate? Do you need to handle parenthesized groupings? –  Apr 15 '16 at 18:03
  • @torazaburo since we are using a third party back-end software for IOT applications called ThingWorx, their alert functionality is limited with one device, and we dont have proper database where we can keep our own information. Anyways, for some limited capability reasons we have to keep this information in string fields and process the data within ThingWorx and that is why we have to keep it this way. – Mustafa Bereket Apr 18 '16 at 20:58
  • This could be of interest, using template strings: http://stackoverflow.com/questions/34882100/can-you-dumb-down-es6-template-strings-to-normal-strings/34883543#34883543 –  Apr 19 '16 at 05:23
  • What I meant is, when you say "operate my logic", do you just mean **evaluate**, or do you mean **parse** in order to further manipulate the expression? –  Apr 19 '16 at 05:36
  • If it is possible to evaluate immediately that would solve my problem entirely, but if that is not possible, then I can evaluate the parsed data. – Mustafa Bereket Apr 19 '16 at 23:19

4 Answers4

2

Assuming no parentheses, I might go with something like this (JavaScript code):

function f(v,op,w){
  var ops = {
    '>': function(a,b){ return a > b; },
    '<': function(a,b){ return a < b; },
    '||': function(a,b){ return a || b; },
    '&&': function(a,b){ return a && b; },
     '==': function(a,b){ return a == b;}
  }

  if (ops[op]){
    return ops[op](v,w);
  } else alert('Could not recognize the operator, "' + op + '".');
}

Now if you can manage to get a list of expressions, you can evaluate them in series:

var exps = [[6,'>',7],'||',[12,'<',22], '&&', [5,'==',5]];

var i = 0, 
    result = typeof exps[i] == 'object' ? f(exps[i][0],exps[i][1],exps[i][2]) : exps[i];

i++;

while (exps[i] !== undefined){
  var op = exps[i++],
      b = typeof exps[i] == 'object' ? f(exps[i][0],exps[i][1],exps[i][2]) : exps[i];

  result = f(result,op,b);
  i++;
}

console.log(result);
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
1

If you are absolutely sure that the input is always going to be valid JavaScript

var rule = "device2.temperature > 20 || device2.humidity>68 && device3.temperature >10"
var rulePassed = eval(rule);

Keep in mind that in most cases "eval" is "evil" and has the potential to introduce more problems than it solves.

Matt Klooster
  • 717
  • 1
  • 5
  • 13
  • So, I just receive that string from the server, and I need to create variables called device2 and device3 and I will make another request to server to retrieve actual temperature values of device2 and device3, and then finally evaluate that string. However, my problem right now is to name variables based on parsed string. So, whenever I catch "device2" from that string, I need to do "var device2;" Any idea how I could do that? – Mustafa Bereket Apr 13 '16 at 21:21
1
function parse(rule){
    return Function("ctx", "return("+rule.replace(/[a-z$_][a-z0-9$_\.]*/gi, "ctx.$&")+")");
}

a little bit better than eval, since it will most likely throw errors, when sbd. tries to inject some code. Because it will try to access these properties on the ctx-object instead of the window-object.

var rule = parse("device2.temperature > 20 || device2.humidity>68 && device3.temperature >10");
var data = {
    device2: {
        temperature: 18,
        humidity: 70
    },

    device3: {
        temperature: 15,
        humidity: 75
    }
};

console.log( rule.toString() );
console.log( rule(data) );
Thomas
  • 3,513
  • 1
  • 13
  • 10
1

Overkill:

beware, not fully tested. may still contain errors
And, code doesn't check wether syntax is valid, only throws on a few obvious errors.

var parse = (function(){    

    function parse(){
        var cache = {};

        //this may be as evil as eval, so take care how you use it.
        function raw(v){ return cache[v] || (cache[v] = Function("return " + v)) }

        //parses Strings and converts them to operator-tokens or functions
        function parseStrings(v, prop, symbol, number, string){
            if(!prop && !symbol && !number && !string){
                throw new Error("unexpected/unhandled symbol", v);
            }else{
                var w;
                switch(prop){
                    //keywords
                    case "true":
                    case "false":
                    case "null":
                        w = raw( v );
                        break;
                }
                tokens.push( 
                    w || 
                    ~unary.indexOf(prop) && v ||
                    prop && parse.fetch(v) || 
                    number && raw( number ) || 
                    string && raw( string ) ||
                    symbol
                );
            }
        }       

        var tokens = [];
        for(var i = 0; i < arguments.length; ++i){
            var arg = arguments[i];
            switch(typeof arg){
                case "number":
                case "boolean":
                    tokens.push(raw( arg ));
                    break;

                case "function":
                    tokens.push( arg );
                    break;

                case "string":
                    //abusing str.replace() as kind of a RegEx.forEach()
                    arg.replace(matchTokens, parseStrings);
                    break;
            }
        }

        for(var i = tokens.lastIndexOf("("), j; i>=0; i = tokens.lastIndexOf("(")){
            j = tokens.indexOf(")", i);
            if(j > 0){
                tokens.splice(i, j+1-i, process( tokens.slice( i+1, j ) ));
            }else{
                throw new Error("mismatching parantheses")
            }
        }
        if(tokens.indexOf(")") >= 0) throw new Error("mismatching parantheses");

        return process(tokens);
    }

    //combines tokens and functions until a single function is left
    function process(tokens){
        //unary operators like
        unary.forEach(o => {
            var i = -1;
            while((i = tokens.indexOf(o, i+1)) >= 0){
                if((o === "+" || o === "-") && typeof tokens[i-1] === "function") continue;
                tokens.splice( i, 2, parse[ unaryMapping[o] || o ]( tokens[i+1] ));
            }
        })
        //binary operators
        binary.forEach(o => {
            for(var i = tokens.lastIndexOf(o); i >= 0; i = tokens.lastIndexOf(o)){
                tokens.splice( i-1, 3, parse[ o ]( tokens[i-1], tokens[i+1] ));
            }
        })

        //ternary operator
        for(var i = tokens.lastIndexOf("?"), j; i >= 0; i = tokens.lastIndexOf("?")){
            if(tokens[i+2] === ":"){
                tokens.splice(i-1, 5, parse.ternary(tokens[i-1], tokens[i+1], tokens[i+3] ));
            }else{
                throw new Error("unexpected symbol")
            }
        }

        if(tokens.length !== 1){
            throw new Error("unparsed tokens left");
        }
        return tokens[0];
    }

    var unary = "!,~,+,-,typeof".split(",");
    var unaryMapping = {    //to avoid collisions with the binary operators
        "+": "plus",
        "-": "minus"
    }
    var binary = "**,*,/,%,+,-,<<,>>,>>>,<,<=,>,>=,==,!=,===,!==,&,^,|,&&,||".split(",");
    var matchTokens = /([a-z$_][\.a-z0-9$_]*)|([+\-*/!~^]=*|[\(\)?:]|[<>&|=]+)|(\d+(?:\.\d*)?|\.\d+)|(["](?:\\[\s\S]|[^"])+["]|['](?:\\[\s\S]|[^'])+['])|\S/gi;

    (function(){
        var def = { value: null };
        var odp = (k,v) => { def.value = v; Object.defineProperty(parse, k, def) };

        unary.forEach(o => {
            var k = unaryMapping[o] || o;
            k in parse || odp(k, Function("a", "return function(ctx){ return " + o + "(a(ctx)) }"));
        })

        //most browsers don't support this syntax yet, so I implement this manually
        odp("**", (a,b) => (ctx) => Math.pow(a(ctx), b(ctx)));
        binary.forEach(o => {
            o in parse || odp(o, Function("a,b", "return function(ctx){ return a(ctx) "+o+" b(ctx) }"));
        });

        odp("ternary", (c,t,e) => ctx => c(ctx)? t(ctx): e(ctx));

        odp("fetch", key => {
            var a = key.split(".");
            return ctx => {
                //fetches a path, like devices.2.temperature
                //does ctx["devices"][2]["temperature"];
                for(var i=0, v = ctx /*|| window*/; i<a.length; ++i){
                    if(v == null) return void 0;
                    v = v[a[i]];
                }
                return v;
            }
        });

        /* some sugar */
        var aliases = {
            "or": "||",
            "and": "&&",
            "not": "!"
        }
        for(var name in aliases) odp(name, parse[aliases[name]]);
    })();

    return parse;
})();

and your code:

var data = {
    device2: {
        temperature: 18,
        humidity: 70
    },

    device3: {
        temperature: 15,
        humidity: 75
    }
};

//you get back a function, that expects the context to work on (optional).
//aka. (in wich context/object is `device2` defined?)
var rule = parse("device2.temperature > 20 || device2.humidity>68 && device3.temperature >10");
console.log("your rule resolved:", rule(data));

sugar:

var rule1 = parse("device2.temperature > 20");
var rule2 = parse("device2.humidity>68 && device3.temperature >10");

//partials/combining rules to new ones
//only `and` (a && b), `or` (a || b), `plus` (+value), `minus` (-value) and 'not', (!value) have named aliases
var rule3 = parse.or(rule1, rule2);
//but you can access all operators like this
var rule3 = parse['||'](rule1, rule2);
//or you can combine functions and strings 
var rule3 = parse(rule1, "||", rule2);

console.log( "(", rule1(data), "||", rule2(data), ") =", rule3(data) );

//ternary operator and Strings (' and " supported)
var example = parse(rule1, "? 'device2: ' + device2.temperature : 'device3: ' + device3.temperature");
console.log( example(data) )

What else to know:

Code handles operator precedence and supports round brackets

If a Path can't be fetched, it the particular function returns undefined (no Errors thrown here)
Access to Array-keys in the paths: parse("devices.2.temperature") fetches devices[2].temperature

not implemented:

parsing Arrays and parsing function-calls and everything around value modification. This engine does some computation, it expects some Value in, and gives you a value out. No more, no less.

Thomas
  • 3,513
  • 1
  • 13
  • 10