Adding arithmetic operators to our OData parser

Published on 2013-4-17

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:

• Mul/Div/Mod

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[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 ]
,

FilterByValue:rhs -> [ op, lhs, rhs ]
| FilterByValue
,
spaces
(
| 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 =
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
``````

``````( 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 ]
,
``````

Then we descend through our mathematical operators in reverse precedence order

``````FilterSubExpression =
FilterSubExpression:lhs
spaces seq("sub") spaces
FilterAddExpression:rhs -> [ "sub", lhs, rhs ]
,

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!

This used to ask if you wanted to hire me

But chances are I'm not available, as I'm busy shipping stuff.

I am available for conference speaking, I like talking and will do so for just T&E (and perhaps a bottle of wine or two).

I have done soft keynotes, and usually entertain/rile people in equal proportion, but prefer to talk tech as that's what I do

Email me :)