From beecbd83bef07760e3d25acc3756f14a57b2c37a Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sun, 31 Dec 2017 16:47:51 +0100 Subject: [PATCH] basic arithmetic expressions --- CHANGELOG.md | 1 + Sources/Expression.swift | 166 +++++++++++++++++++----- Sources/ForTag.swift | 2 +- Sources/IfTag.swift | 36 ++--- Tests/StencilTests/ExpressionSpec.swift | 163 ++++++++++++++--------- 5 files changed, 262 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce60106f..9c2b355a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added support for resolving superclass properties for not-NSObject subclasses - The `{% for %}` tag can now iterate over tuples, structures and classes via their stored properties. +- Added basic aritmetic operatiosn +, -, /, * ### Bug Fixes diff --git a/Sources/Expression.swift b/Sources/Expression.swift index 1f41afee..6fe8e819 100644 --- a/Sources/Expression.swift +++ b/Sources/Expression.swift @@ -1,5 +1,5 @@ protocol Expression: CustomStringConvertible { - func evaluate(context: Context) throws -> Bool + func evaluate(context: Context) throws -> Any } @@ -20,7 +20,7 @@ final class StaticExpression: Expression, CustomStringConvertible { self.value = value } - func evaluate(context: Context) throws -> Bool { + func evaluate(context: Context) throws -> Any { return value } @@ -29,8 +29,7 @@ final class StaticExpression: Expression, CustomStringConvertible { } } - -final class VariableExpression: Expression, CustomStringConvertible { +class VariableExpression: Expression, CustomStringConvertible { let variable: Resolvable init(variable: Resolvable) { @@ -42,7 +41,7 @@ final class VariableExpression: Expression, CustomStringConvertible { } /// Resolves a variable in the given context as boolean - func resolve(context: Context, variable: Resolvable) throws -> Bool { + func evaluate(context: Context) throws -> Any { let result = try variable.resolve(context) var truthy = false @@ -62,13 +61,8 @@ final class VariableExpression: Expression, CustomStringConvertible { return truthy } - - func evaluate(context: Context) throws -> Bool { - return try resolve(context: context, variable: variable) - } } - final class NotExpression: Expression, PrefixOperator, CustomStringConvertible { let expression: Expression @@ -80,8 +74,8 @@ final class NotExpression: Expression, PrefixOperator, CustomStringConvertible { return "not \(expression)" } - func evaluate(context: Context) throws -> Bool { - return try !expression.evaluate(context: context) + func evaluate(context: Context) throws -> Any { + return try !(expression.evaluate(context: context) as! Bool) } } @@ -98,7 +92,7 @@ final class InExpression: Expression, InfixOperator, CustomStringConvertible { return "(\(lhs) in \(rhs))" } - func evaluate(context: Context) throws -> Bool { + func evaluate(context: Context) throws -> Any { if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { let lhsValue = try lhs.variable.resolve(context) let rhsValue = try rhs.variable.resolve(context) @@ -130,8 +124,8 @@ final class OrExpression: Expression, InfixOperator, CustomStringConvertible { return "(\(lhs) or \(rhs))" } - func evaluate(context: Context) throws -> Bool { - let lhs = try self.lhs.evaluate(context: context) + func evaluate(context: Context) throws -> Any { + let lhs = try self.lhs.evaluate(context: context) as! Bool if lhs { return lhs } @@ -154,8 +148,8 @@ final class AndExpression: Expression, InfixOperator, CustomStringConvertible { return "(\(lhs) and \(rhs))" } - func evaluate(context: Context) throws -> Bool { - let lhs = try self.lhs.evaluate(context: context) + func evaluate(context: Context) throws -> Any { + let lhs = try self.lhs.evaluate(context: context) as! Bool if !lhs { return lhs } @@ -178,7 +172,7 @@ class EqualityExpression: Expression, InfixOperator, CustomStringConvertible { return "(\(lhs) == \(rhs))" } - func evaluate(context: Context) throws -> Bool { + func evaluate(context: Context) throws -> Any { if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { let lhsValue = try lhs.variable.resolve(context) let rhsValue = try rhs.variable.resolve(context) @@ -214,16 +208,26 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible { return "(\(lhs) \(op) \(rhs))" } - func evaluate(context: Context) throws -> Bool { + func evaluate(context: Context) throws -> Any { if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { let lhsValue = try lhs.variable.resolve(context) let rhsValue = try rhs.variable.resolve(context) - if let lhs = lhsValue, let rhs = rhsValue { - if let lhs = toNumber(value: lhs), let rhs = toNumber(value: rhs) { - return compare(lhs: lhs, rhs: rhs) - } + guard let lhs = lhsValue else { + throw TemplateSyntaxError("left value is nil") + } + guard let lhsNumber = toNumber(value: lhs) else { + throw TemplateSyntaxError("left value is not a number") + } + + guard let rhs = rhsValue else { + throw TemplateSyntaxError("right value is nil") + } + guard let rhsNumber = toNumber(value: rhs) else { + throw TemplateSyntaxError("right value is not a number") } + + return compare(lhs: lhsNumber, rhs: rhsNumber) } return false @@ -239,7 +243,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible { } -class MoreThanExpression: NumericExpression { +final class MoreThanExpression: NumericExpression { override var op: String { return ">" } @@ -250,7 +254,7 @@ class MoreThanExpression: NumericExpression { } -class MoreThanEqualExpression: NumericExpression { +final class MoreThanEqualExpression: NumericExpression { override var op: String { return ">=" } @@ -261,7 +265,7 @@ class MoreThanEqualExpression: NumericExpression { } -class LessThanExpression: NumericExpression { +final class LessThanExpression: NumericExpression { override var op: String { return "<" } @@ -272,7 +276,7 @@ class LessThanExpression: NumericExpression { } -class LessThanEqualExpression: NumericExpression { +final class LessThanEqualExpression: NumericExpression { override var op: String { return "<=" } @@ -283,13 +287,13 @@ class LessThanEqualExpression: NumericExpression { } -class InequalityExpression: EqualityExpression { +final class InequalityExpression: EqualityExpression { override var description: String { return "(\(lhs) != \(rhs))" } - override func evaluate(context: Context) throws -> Bool { - return try !super.evaluate(context: context) + override func evaluate(context: Context) throws -> Any { + return try !(super.evaluate(context: context) as! Bool) } } @@ -329,3 +333,105 @@ func toNumber(value: Any) -> Number? { return nil } + +class ArithmeticExpression: Expression, InfixOperator, CustomStringConvertible { + let lhs: Expression + let rhs: Expression + + required init(lhs: Expression, rhs: Expression) { + self.lhs = lhs + self.rhs = rhs + } + + var description: String { + return "(\(lhs) \(op) \(rhs))" + } + + func evaluate(context: Context) throws -> Any { + let lhsResult: Number + if let lhs = lhs as? ArithmeticExpression { + lhsResult = try lhs.evaluate(context: context) as! Number + } else if let lhs = lhs as? VariableExpression { + let lhsValue = try lhs.variable.resolve(context) + + guard let lhs = lhsValue else { + throw TemplateSyntaxError("left value is nil") + } + guard let lhsNumber = toNumber(value: lhs) else { + throw TemplateSyntaxError("left value '\(lhs)' is not a number") + } + lhsResult = lhsNumber + } else { + throw TemplateSyntaxError("invalid arithmetic expression") + } + + let rhsResult: Number + if let rhs = rhs as? ArithmeticExpression { + rhsResult = try rhs.evaluate(context: context) as! Number + } else if let rhs = rhs as? VariableExpression { + let rhsValue = try rhs.variable.resolve(context) + + guard let rhs = rhsValue else { + throw TemplateSyntaxError("right value is nil") + } + guard let rhsNumber = toNumber(value: rhs) else { + throw TemplateSyntaxError("right value '\(rhs)' is not a number") + } + rhsResult = rhsNumber + } else { + throw TemplateSyntaxError("invalid arithmetic expression") + } + + return calculate(lhs: lhsResult, rhs: rhsResult) + } + + var op: String { + return "" + } + + func calculate(lhs: Number, rhs: Number) -> Number { + return 0 + } +} + +final class SumExpression: ArithmeticExpression { + override var op: String { + return "+" + } + + override func calculate(lhs: Number, rhs: Number) -> Number { + return lhs + rhs + } +} + +final class SubstractExpression: ArithmeticExpression { + override var op: String { + return "-" + } + + override func calculate(lhs: Number, rhs: Number) -> Number { + return lhs - rhs + } +} + +final class MultiplyExpression: ArithmeticExpression { + override var op: String { + return "*" + } + + override func calculate(lhs: Number, rhs: Number) -> Number { + return lhs * rhs + } +} + +final class DevideExpression: ArithmeticExpression { + override var op: String { + return "/" + } + + override func calculate(lhs: Number, rhs: Number) -> Number { + return lhs / rhs + } +} + + diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 25005e66..f36a16f5 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -117,7 +117,7 @@ class ForNode : NodeType { if let `where` = self.where { values = try values.filter({ item -> Bool in return try push(value: item, context: context) { - try `where`.evaluate(context: context) + try `where`.evaluate(context: context) as! Bool } }) } diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 8f3b0fda..6edc962a 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -14,6 +14,10 @@ enum Operator { let operators: [Operator] = [ + .infix("/", 4, DevideExpression.self), + .infix("*", 4, MultiplyExpression.self), + .infix("+", 3, SumExpression.self), + .infix("-", 3, SubstractExpression.self), .infix("in", 5, InExpression.self), .infix("or", 6, OrExpression.self), .infix("and", 7, AndExpression.self), @@ -37,8 +41,7 @@ func findOperator(name: String) -> Operator? { return nil } - -enum IfToken { +enum ExpressionToken { case infix(name: String, bindingPower: Int, op: InfixOperator.Type) case prefix(name: String, bindingPower: Int, op: PrefixOperator.Type) case variable(Resolvable) @@ -57,31 +60,31 @@ enum IfToken { } } - func nullDenotation(parser: IfExpressionParser) throws -> Expression { + func nullDenotation(parser: ExpressionParser) throws -> Expression { switch self { case .infix(let name, _, _): - throw TemplateSyntaxError("'if' expression error: infix operator '\(name)' doesn't have a left hand side") + throw TemplateSyntaxError("expression error: infix operator '\(name)' doesn't have a left hand side") case .prefix(_, let bindingPower, let op): let expression = try parser.expression(bindingPower: bindingPower) return op.init(expression: expression) case .variable(let variable): return VariableExpression(variable: variable) case .end: - throw TemplateSyntaxError("'if' expression error: end") + throw TemplateSyntaxError("expression error: end") } } - func leftDenotation(left: Expression, parser: IfExpressionParser) throws -> Expression { + func leftDenotation(left: Expression, parser: ExpressionParser) throws -> Expression { switch self { case .infix(_, let bindingPower, let op): let right = try parser.expression(bindingPower: bindingPower) return op.init(lhs: left, rhs: right) case .prefix(let name, _, _): - throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side") + throw TemplateSyntaxError("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") + throw TemplateSyntaxError("expression error: variable '\(variable)' was called with a left hand side") case .end: - throw TemplateSyntaxError("'if' expression error: end") + throw TemplateSyntaxError("expression error: end") } } @@ -96,8 +99,8 @@ enum IfToken { } -final class IfExpressionParser { - let tokens: [IfToken] +final class ExpressionParser { + let tokens: [ExpressionToken] var position: Int = 0 init(components: [String], tokenParser: TokenParser) throws { @@ -115,7 +118,7 @@ final class IfExpressionParser { } } - var currentToken: IfToken { + var currentToken: ExpressionToken { if tokens.count > position { return tokens[position] } @@ -123,7 +126,7 @@ final class IfExpressionParser { return .end } - var nextToken: IfToken { + var nextToken: ExpressionToken { position += 1 return currentToken } @@ -156,7 +159,7 @@ final class IfExpressionParser { func parseExpression(components: [String], tokenParser: TokenParser) throws -> Expression { - let parser = try IfExpressionParser(components: components, tokenParser: tokenParser) + let parser = try ExpressionParser(components: components, tokenParser: tokenParser) return try parser.parse() } @@ -250,7 +253,10 @@ class IfNode : NodeType { func render(_ context: Context) throws -> String { for condition in conditions { if let expression = condition.expression { - let truthy = try expression.evaluate(context: context) + let conditionResult = try expression.evaluate(context: context) + guard let truthy = conditionResult as? Bool else { + throw TemplateSyntaxError("\(conditionResult) is not bool") + } if truthy { return try condition.render(context) diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift index 4b7958d5..d0183832 100644 --- a/Tests/StencilTests/ExpressionSpec.swift +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -11,128 +11,128 @@ func testExpressions() { $0.it("evaluates to true when value is not nil") { let context = Context(dictionary: ["value": "known"]) - try expect(try expression.evaluate(context: context)).to.beTrue() + try expect(try expression.evaluate(context: context) as? Bool).to.beTrue() } $0.it("evaluates to false when value is unset") { let context = Context() - try expect(try expression.evaluate(context: context)).to.beFalse() + try expect(try expression.evaluate(context: context) as? Bool).to.beFalse() } $0.it("evaluates to true when array variable is not empty") { let items: [[String: Any]] = [["key":"key1","value":42],["key":"key2","value":1337]] let context = Context(dictionary: ["value": [items]]) - try expect(try expression.evaluate(context: context)).to.beTrue() + try expect(try expression.evaluate(context: context) as? Bool).to.beTrue() } $0.it("evaluates to false when array value is empty") { let emptyItems = [[String: Any]]() let context = Context(dictionary: ["value": emptyItems]) - try expect(try expression.evaluate(context: context)).to.beFalse() + try expect(try expression.evaluate(context: context) as? Bool).to.beFalse() } $0.it("evaluates to false when dictionary value is empty") { let emptyItems = [String:Any]() let context = Context(dictionary: ["value": emptyItems]) - try expect(try expression.evaluate(context: context)).to.beFalse() + try expect(try expression.evaluate(context: context) as? Bool).to.beFalse() } $0.it("evaluates to false when Array value is empty") { let context = Context(dictionary: ["value": ([] as [Any])]) - try expect(try expression.evaluate(context: context)).to.beFalse() + try expect(try expression.evaluate(context: context) as? Bool).to.beFalse() } $0.it("evaluates to true when integer value is above 0") { let context = Context(dictionary: ["value": 1]) - try expect(try expression.evaluate(context: context)).to.beTrue() + try expect(try expression.evaluate(context: context) as? Bool).to.beTrue() } $0.it("evaluates to true with string") { let context = Context(dictionary: ["value": "test"]) - try expect(try expression.evaluate(context: context)).to.beTrue() + try expect(try expression.evaluate(context: context) as? Bool).to.beTrue() } $0.it("evaluates to false when empty string") { let context = Context(dictionary: ["value": ""]) - try expect(try expression.evaluate(context: context)).to.beFalse() + try expect(try expression.evaluate(context: context) as? Bool).to.beFalse() } $0.it("evaluates to false when integer value is below 0 or below") { let context = Context(dictionary: ["value": 0]) - try expect(try expression.evaluate(context: context)).to.beFalse() + try expect(try expression.evaluate(context: context) as? Bool).to.beFalse() let negativeContext = Context(dictionary: ["value": 0]) - try expect(try expression.evaluate(context: negativeContext)).to.beFalse() + try expect(try expression.evaluate(context: negativeContext) as? Bool).to.beFalse() } $0.it("evaluates to true when float value is above 0") { let context = Context(dictionary: ["value": Float(0.5)]) - try expect(try expression.evaluate(context: context)).to.beTrue() + try expect(try expression.evaluate(context: context) as? Bool).to.beTrue() } $0.it("evaluates to false when float is 0 or below") { let context = Context(dictionary: ["value": Float(0)]) - try expect(try expression.evaluate(context: context)).to.beFalse() + try expect(try expression.evaluate(context: context) as? Bool).to.beFalse() } $0.it("evaluates to true when double value is above 0") { let context = Context(dictionary: ["value": Double(0.5)]) - try expect(try expression.evaluate(context: context)).to.beTrue() + try expect(try expression.evaluate(context: context) as? Bool).to.beTrue() } $0.it("evaluates to false when double is 0 or below") { let context = Context(dictionary: ["value": Double(0)]) - try expect(try expression.evaluate(context: context)).to.beFalse() + try expect(try expression.evaluate(context: context) as? Bool).to.beFalse() } $0.it("evaluates to false when uint is 0") { let context = Context(dictionary: ["value": UInt(0)]) - try expect(try expression.evaluate(context: context)).to.beFalse() + try expect(try expression.evaluate(context: context) as? Bool).to.beFalse() } } $0.describe("NotExpression") { $0.it("returns truthy for positive expressions") { let expression = NotExpression(expression: StaticExpression(value: true)) - try expect(expression.evaluate(context: Context())).to.beFalse() + try expect(expression.evaluate(context: Context()) as? Bool).to.beFalse() } $0.it("returns falsy for negative expressions") { let expression = NotExpression(expression: StaticExpression(value: false)) - try expect(expression.evaluate(context: Context())).to.beTrue() + try expect(expression.evaluate(context: Context()) as? Bool).to.beTrue() } } $0.describe("expression parsing") { $0.it("can parse a variable expression") { let expression = try parseExpression(components: ["value"], tokenParser: parser) - try expect(expression.evaluate(context: Context())).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue() + try expect(expression.evaluate(context: Context()) as? Bool).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["value": true])) as? Bool).to.beTrue() } $0.it("can parse a not expression") { let expression = try parseExpression(components: ["not", "value"], tokenParser: parser) - try expect(expression.evaluate(context: Context())).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse() + try expect(expression.evaluate(context: Context()) as? Bool).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["value": true])) as? Bool).to.beFalse() } $0.describe("and expression") { let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser) $0.it("evaluates to false with lhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true])) as? Bool).to.beFalse() } $0.it("evaluates to false with rhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false])) as? Bool).to.beFalse() } $0.it("evaluates to false with lhs and rhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false])) as? Bool).to.beFalse() } $0.it("evaluates to true with lhs and rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true])) as? Bool).to.beTrue() } } @@ -140,19 +140,19 @@ func testExpressions() { let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser) $0.it("evaluates to true with lhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false])) as? Bool).to.beTrue() } $0.it("evaluates to true with rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true])) as? Bool).to.beTrue() } $0.it("evaluates to true with lhs and rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true])) as? Bool).to.beTrue() } $0.it("evaluates to false with lhs and rhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false])) as? Bool).to.beFalse() } } @@ -160,35 +160,35 @@ func testExpressions() { let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser) $0.it("evaluates to true with equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"])) as? Bool).to.beTrue() } $0.it("evaluates to false with non equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"])) as? Bool).to.beFalse() } $0.it("evaluates to true with nils") { - try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: [:])) as? Bool).to.beTrue() } $0.it("evaluates to true with numbers") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0])) as? Bool).to.beTrue() } $0.it("evaluates to false with non equal numbers") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1])) as? Bool).to.beFalse() } $0.it("evaluates to true with booleans") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true])) as? Bool).to.beTrue() } $0.it("evaluates to false with falsy booleans") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false])) as? Bool).to.beFalse() } $0.it("evaluates to false with different types") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1])) as? Bool).to.beFalse() } } @@ -196,11 +196,11 @@ func testExpressions() { let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser) $0.it("evaluates to true with inequal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"])) as? Bool).to.beTrue() } $0.it("evaluates to false with equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"])) as? Bool).to.beFalse() } } @@ -208,11 +208,11 @@ func testExpressions() { let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser) $0.it("evaluates to true with lhs > rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4])) as? Bool).to.beTrue() } $0.it("evaluates to false with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0])) as? Bool).to.beFalse() } } @@ -220,11 +220,11 @@ func testExpressions() { let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser) $0.it("evaluates to true with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5])) as? Bool).to.beTrue() } $0.it("evaluates to false with lhs < rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1])) as? Bool).to.beFalse() } } @@ -232,11 +232,11 @@ func testExpressions() { let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser) $0.it("evaluates to true with lhs < rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5])) as? Bool).to.beTrue() } $0.it("evaluates to false with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0])) as? Bool).to.beFalse() } } @@ -244,11 +244,11 @@ func testExpressions() { let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser) $0.it("evaluates to true with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5])) as? Bool).to.beTrue() } $0.it("evaluates to false with lhs > rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0])) as? Bool).to.beFalse() } } @@ -256,27 +256,27 @@ func testExpressions() { let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser) $0.it("evaluates to true with one") { - try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["one": true])) as? Bool).to.beTrue() } $0.it("evaluates to true with one and three") { - try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true])) as? Bool).to.beTrue() } $0.it("evaluates to true with two") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["two": true])) as? Bool).to.beTrue() } $0.it("evaluates to false with two and three") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true])) as? Bool).to.beFalse() } $0.it("evaluates to false with two and three") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true])) as? Bool).to.beFalse() } $0.it("evaluates to false with nothing") { - try expect(expression.evaluate(context: Context())).to.beFalse() + try expect(expression.evaluate(context: Context()) as? Bool).to.beFalse() } } @@ -284,15 +284,58 @@ func testExpressions() { let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser) $0.it("evaluates to true when rhs contains lhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["a", "b", "c"]]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "abc"]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]])) as? Bool).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["a", "b", "c"]])) as? Bool).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "abc"])) as? Bool).to.beTrue() } $0.it("evaluates to false when rhs does not contain lhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [2, 3, 4]]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["b", "c", "d"]]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [2, 3, 4]])) as? Bool).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["b", "c", "d"]])) as? Bool).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"])) as? Bool).to.beFalse() + } + } + + $0.describe("arithmetic expression") { + $0.it("+ when both variables are numbers") { + let expression = try! parseExpression(components: ["lhs", "+", "rhs"], tokenParser: parser) + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 2])) as? Number) == 3 + } + $0.it("- when both variables are numbers") { + let expression = try! parseExpression(components: ["lhs", "-", "rhs"], tokenParser: parser) + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 2])) as? Number) == -1 + } + $0.it("* when both variables are numbers") { + let expression = try! parseExpression(components: ["lhs", "*", "rhs"], tokenParser: parser) + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 2, "rhs": 2])) as? Number) == 4 + } + $0.it("/ when both variables are numbers") { + let expression = try! parseExpression(components: ["lhs", "/", "rhs"], tokenParser: parser) + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 2])) as? Number) == 2 + } + $0.it("* and / have presedence over + and -") { + let expression = try! parseExpression(components: ["2", "*", "3", "/", "2", "+", "5", "-", "1"], tokenParser: parser) + try expect(expression.evaluate(context: Context(dictionary: [:])) as? Number) == 7 + } + + $0.it("throws when left side is nil") { + let expression = try! parseExpression(components: ["lhs", "+", "rhs"], tokenParser: parser) + try expect(expression.evaluate(context: Context(dictionary: ["rhs": 2])) as? Number).toThrow() + } + + $0.it("throws when left side is not a number") { + let expression = try! parseExpression(components: ["lhs", "+", "rhs"], tokenParser: parser) + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "1", "rhs": 2])) as? Number).toThrow() + } + + $0.it("throws when right side is nil") { + let expression = try! parseExpression(components: ["lhs", "+", "rhs"], tokenParser: parser) + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 2])) as? Number).toThrow() + } + + $0.it("throws when right side is not a number") { + let expression = try! parseExpression(components: ["lhs", "+", "rhs"], tokenParser: parser) + try expect(expression.evaluate(context: Context(dictionary: ["rhs": "1", "lhs": 2])) as? Number).toThrow() } } }