Skip to content

Commit

Permalink
added support for brackets in boolean expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyapuchka committed Dec 25, 2017
1 parent 1223efb commit 943e064
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Enhancements

- Added support for resolving superclass properties for not-NSObject subclasses
- You can now use brackets in boolean expressions to change operators precedence

### Bug Fixes

Expand Down
69 changes: 59 additions & 10 deletions Sources/IfTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ func findOperator(name: String) -> Operator? {
}


enum IfToken {
indirect enum IfToken {
case infix(name: String, bindingPower: Int, op: InfixOperator.Type)
case prefix(name: String, bindingPower: Int, op: PrefixOperator.Type)
case variable(Resolvable)
case subExpression(IfExpressionParser)
case end

var bindingPower: Int {
Expand All @@ -52,6 +53,8 @@ enum IfToken {
return bindingPower
case .variable(_):
return 0
case .subExpression(_):
return 0
case .end:
return 0
}
Expand All @@ -66,6 +69,8 @@ enum IfToken {
return op.init(expression: expression)
case .variable(let variable):
return VariableExpression(variable: variable)
case .subExpression(let parser):
return try parser.expression()
case .end:
throw TemplateSyntaxError("'if' expression error: end")
}
Expand All @@ -80,6 +85,8 @@ enum IfToken {
throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side")
case .variable(let variable):
throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side")
case .subExpression(_):
throw TemplateSyntaxError("'if' expression error: sub expression was called with a left hand side")
case .end:
throw TemplateSyntaxError("'if' expression error: end")
}
Expand All @@ -101,17 +108,59 @@ final class IfExpressionParser {
var position: Int = 0

init(components: [String], tokenParser: TokenParser) throws {
self.tokens = try components.map { component in
if let op = findOperator(name: component) {
switch op {
case .infix(let name, let bindingPower, let cls):
return .infix(name: name, bindingPower: bindingPower, op: cls)
case .prefix(let name, let bindingPower, let cls):
return .prefix(name: name, bindingPower: bindingPower, op: cls)
guard try components.reduce(0, {
var balance = $0
if $1 == "(" { balance += 1 }
else if $1 == ")" { balance -= 1 }
if balance < 0 { throw TemplateSyntaxError("unbalanced brackets") }
return balance
}) == 0 else {
throw TemplateSyntaxError("unbalanced brackets")
}

var parsedComponents = [String]()

func parseSubExpression(from index: Int, components: [String], tokenParser: TokenParser) throws -> (IfExpressionParser, [String]) {
var bracketsBalance = 1
let subComponents = Array(components
.suffix(from: index)
.prefix(while: {
if $0 == "(" { bracketsBalance += 1 }
else if $0 == ")" { bracketsBalance -= 1 }
return bracketsBalance != 0
}))

let expression = try IfExpressionParser(components: subComponents, tokenParser: tokenParser)
return (expression, ["("] + subComponents + [")"])
}

self.tokens = try components.enumerated().flatMap { index, component in
if component == "(" {
let (expression, parsed) = try parseSubExpression(
from: index + 1,
components: components,
tokenParser: tokenParser
)
parsedComponents.append(contentsOf: parsed)
return .subExpression(expression)
}
else if component == ")" {
parsedComponents.append(component)
return nil
} else if index >= parsedComponents.count {
parsedComponents.append(component)
if let op = findOperator(name: component) {
switch op {
case .infix(let name, let bindingPower, let cls):
return .infix(name: name, bindingPower: bindingPower, op: cls)
case .prefix(let name, let bindingPower, let cls):
return .prefix(name: name, bindingPower: bindingPower, op: cls)
}
}
return .variable(try tokenParser.compileFilter(component))
} else {
return nil
}

return .variable(try tokenParser.compileFilter(component))
}
}

Expand Down
29 changes: 29 additions & 0 deletions Tests/StencilTests/ExpressionSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,35 @@ func testExpressions() {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"]))).to.beFalse()
}
}

$0.describe("sub expression") {
$0.it("evaluates correctly") {
let context = Context(dictionary: ["one": false, "two": false, "three": true])

let expression = try! parseExpression(components: ["one", "and", "two", "or", "three"], tokenParser: parser)
let expressionWithBrackets = try! parseExpression(components: ["one", "and", "(", "two", "or", "three", ")"], tokenParser: parser)

try expect(expression.evaluate(context: context)).to.beTrue()
try expect(expressionWithBrackets.evaluate(context: context)).to.beFalse()

let notExpression = try! parseExpression(components: ["not", "one", "or", "three"], tokenParser: parser)
let notExpressionWithBrackets = try! parseExpression(components: ["not", "(", "one", "or", "three", ")"], tokenParser: parser)

try expect(notExpression.evaluate(context: context)).to.beTrue()
try expect(notExpressionWithBrackets.evaluate(context: context)).to.beFalse()
}

$0.it("fails when brackets are not balanced") {
try expect(parseExpression(components: ["(", "lhs", "and", "rhs"], tokenParser: parser)).toThrow()
try expect(parseExpression(components: [")", "lhs", "and", "rhs"], tokenParser: parser)).toThrow()
try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", "("], tokenParser: parser)).toThrow()
try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", ")"], tokenParser: parser)).toThrow()
try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", ")"], tokenParser: parser)).toThrow()
try expect(parseExpression(components: ["(", "lhs", "and", ")"], tokenParser: parser)).toThrow()
try expect(parseExpression(components: ["(", "and", "rhs", ")"], tokenParser: parser)).toThrow()
}
}

}
}
}

0 comments on commit 943e064

Please sign in to comment.