I'm finally reaching the point where I can parse most of the OData conventions for Uris, which is nice!
A re-cap of where we are so far.
Wowsers, talk about an accidental blog series...
Arithmetic operators
What were they again?
Add Addition /Products?$filter=Price add 5 gt 10
Sub Subtraction /Products?$filter=Price sub 5 gt 10
Mul Multiplication /Products?$filter=Price mul 2 gt 2000
Div Division /Products?$filter=Price div 2 gt 4
Mod Modulo /Products?$filter=Price mod 2 eq 0
Ah yes,
Now, these are all the same, but operator precedence is important so the order in which we want to go through them is:
This is very similar to how we implemented And/Or although I'll write a few tests to make sure I get it right.
test("/some/resource?$filterby=Price add 5 gt 10", "OData", function(result) {
it("A filter should be present", function() {
assert.notEqual(result.options.$filterby, null)
})
it("Filter should be an instance of 'gt'", function() {
assert.equal(result.options.$filterby[0], "gt")
})
it("lhr should be Price add 5", function() {
var rhs = result.options.$filterby[1]
assert.equal(rhs[0], "add")
assert.equal(rhs[1].name, "Price")
assert.equal(rhs[2], 5)
})
it("rhr should be 10", function() {
assert.equal(result.options.$filterby[2], 10)
})
})
This tells us that our 'add' operator has higher precedence than the comparisons (which makes sense). This'll mean we want to sneak it in somewhere after those comparisons. (Assuming in this scheme that And/Or have a higher precedence than add, and it seems to be that way)
FilterLogicalExpression =
FilterLogicalExpression:lhs
FilterByOperand:op
FilterAddExpression:rhs -> [op, lhs, rhs ]
| FilterAddExpression
,
FilterAddExpression =
FilterAddExpression:lhs
FilterAddOperand:op
FilterByValue:rhs -> [ op, lhs, rhs ]
| FilterByValue
,
FilterAddOperand =
spaces
(
seq("add")
| seq("sub")
):op
spaces -> op
,
Simples, we insert it in the pipeline between "LogicalExpression" and "Checking the value" (Literal values have the highest precedence because they don't require any work)
And because Mul/etc have a higher precedence than Add, this exactly the same
test("/some/resource?$filterby=Price mul 5 gt 10", "OData", function(result) {
it("A filter should be present", function() {
assert.notEqual(result.options.$filterby, null)
})
it("Filter should be an instance of 'gt'", function() {
assert.equal(result.options.$filterby[0], "gt")
})
it("lhr should be Price add 5", function() {
var lhs = result.options.$filterby[1]
assert.equal(lhs[0], "mul")
assert.equal(lhs[1].name, "Price")
assert.equal(lhs[2], 5)
})
it("rhr should be 10", function() {
assert.equal(result.options.$filterby[2], 10)
})
})
Like so
FilterAddExpression =
FilterAddExpression:lhs
FilterAddOperand:op
FilterMulExpression:rhs -> [ op, lhs, rhs ]
| FilterMulExpression
,
FilterMulExpression =
FilterMulExpression:lhs
FilterMulOperand:op
FilterByValue:rhs -> [ op, lhs, rhs ]
| FilterByValue
,
Now what I actually have to do is define operator precedence for mul/div etc independently. So I can't actually cheat and do
FilterMulOperand =
spaces
(
seq("mul")
| seq("div")
| seq("mod")
):op
spaces -> op
,
Like I have been doing, or when I write the following test, it will fail.
test("/some/resource?$filterby=Price div Price mul 5 gt 10", "OData", function(result) {
console.log(JSON.stringify(result))
it("A filter should be present", function() {
assert.notEqual(result.options.$filterby, null)
})
it("Filter should be an instance of 'gt'", function() {
assert.equal(result.options.$filterby[0], "gt")
})
var lexpr = result.options.$filterby[1]
it("should be Price div {expr}", function() {
assert.equal(lexpr[0], "div")
assert.equal(lexpr[1].name, "Price")
})
it("should be Price mul 5", function() {
assert.equal(lexpr[2][0], "mul")
assert.equal(lexpr[2][1].name, "Price")
assert.equal(lexpr[2][2], 5)
})
it("rhr should be 10", function() {
assert.equal(result.options.$filterby[2], 10)
})
})
What will happen here is we'll get
[
'gt',
[
'mul',
[
'div', 'Price', 'Price'
],
5
],
10
]
When what we clearly want is
[
'gt',
[
'div',
'Price',
[
'mul', 'Price', '5'
]
],
10
]
Or if you like
( (price / price) * 5 ) > 10
Instead of
( Price / (price * 5) ) > 10
Which is a little bit different to say the least!
So, explicit operation order is what we want, and here is how get it:
One massively explicit set of operator precedences...
FilterByOption =
seq("$filterby=")
FilterByExpression:expr -> { name: "$filterby", value: expr }
,
FilterByExpression =
FilterAndExpression
,
And is the least important in our hierarchy
FilterAndExpression =
FilterAndExpression:lhs
FilterAndOperand:op
FilterLogicalExpression:rhs -> [ op, lhs, rhs ]
| FilterLogicalExpression
,
Followed by any logical expression
FilterLogicalExpression =
FilterLogicalExpression:lhs
FilterByOperand:op
FilterAddExpression:rhs -> [op, lhs, rhs ]
| FilterAddExpression
,
Then we descend through our mathematical operators in reverse precedence order
FilterSubExpression =
FilterSubExpression:lhs
spaces seq("sub") spaces
FilterAddExpression:rhs -> [ "sub", lhs, rhs ]
| FilterAddExpression
,
FilterAddExpression =
FilterAddExpression:lhs
spaces seq("add") spaces
FilterModExpression:rhs -> [ "add", lhs, rhs ]
| FilterModExpression
,
FilterModExpression =
FilterModExpression:lhs
spaces seq("mod") spaces
FilterDivExpression:rhs -> [ "mod", lhs, rhs ]
| FilterDivExpression
,
FilterDivExpression =
FilterDivExpression:lhs
spaces seq("div") spaces
FilterMulExpression:rhs -> [ "div", lhs, rhs ]
| FilterMulExpression
,
FilterMulExpression =
FilterMulExpression:lhs
spaces seq("mul") spaces
FilterByValue:rhs -> [ "mul", lhs, rhs ]
| FilterByValue
,
FilterByValue =
FilterNegateExpression
| Number
| QuotedText
| PropertyPath
,
FilterNegateExpression =
spaces
seq("not")
spaces
(
FilterByValue
| '(' spaces FilterByExpression:expr spaces ')' -> expr
):value -> [ "not", value ]
,
How cool is that??!!? That's pretty much the whole shebang wrapped up as far as expressing parsing goes, and now I can go trigger mad with nested and/or/sub/mul/etc - with the exception of the precedence operators which I'll add next!
2020 © Rob Ashton. ALL Rights Reserved.