diff --git a/Sources/Variable.swift b/Sources/Variable.swift index ea0353ae..db7440a3 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -3,6 +3,14 @@ import Foundation typealias Number = Float +public protocol GenericVariable: Equatable { + var variable: String { get } +} + +public func ==(lhs: T, rhs: T) -> Bool { + return lhs.variable == rhs.variable +} + class FilterExpression : Resolvable { let filters: [(FilterType, [Variable])] @@ -41,8 +49,66 @@ class FilterExpression : Resolvable { } } +// A structure used to represent a template variable or expression, resolved in +// a given context +public struct CompoundVariable : GenericVariable, Resolvable { + public let variable: String + + /// Create a variable with a string representing the variable + public init(_ variable: String) { + self.variable = variable + } + + /// Resolve the variable in the given context, first as a normal variable, then + /// as an expression (more expensive) + public func resolve(_ context: Context) throws -> Any? { + var result = try Variable(variable).resolve(context) + + if result == nil { + result = try expressionResolve(context) + } + + return result + } + + private func expressionResolve(_ context: Context) throws -> Any? { + var components = explode(expression: variable, operators: " +-*/()") + + // try to resolve each individual component + components = try components.map { + if let resolved = try Variable($0).resolve(context) { + return stringify(resolved) + } else { + return $0 + } + } + + let expression = NSExpression(format: components.joined()) + return expression.expressionValue(with: nil, context: nil) + } + + private func explode(expression: String, operators: String) -> [String] { + let set = CharacterSet(charactersIn: operators) + var result = [String]() + + var current = "" + for character in expression.unicodeScalars { + if !set.contains(character) { + current += String(character) + } else { + result.append(current) + result.append(String(character)) + current = "" + } + } + result.append(current) + + return result.filter { !$0.isEmpty } + } +} + /// A structure used to represent a template variable, and to resolve it in a given context. -public struct Variable : Equatable, Resolvable { +public struct Variable : GenericVariable, Resolvable { public let variable: String /// Create a variable with a string representing the variable @@ -117,10 +183,6 @@ public struct Variable : Equatable, Resolvable { } } -public func ==(lhs: Variable, rhs: Variable) -> Bool { - return lhs.variable == rhs.variable -} - func normalize(_ current: Any?) -> Any? { if let current = current as? Normalizable { diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index d66dc1a4..1a680b82 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -115,4 +115,72 @@ func testVariable() { } #endif } + + describe("CompoundVariable") { + let context = Context(dictionary: [ + "name": "Kyle", + "a": 3, + "x": 20 + ]) + + $0.it("Falls back to default behaviour for strings") { + let variable = CompoundVariable("\"name\"") + let result = try variable.resolve(context) as? String + try expect(result) == "name" + } + + $0.it("Falls back to default behaviour for numbers") { + let variable = CompoundVariable("5") + let result = try variable.resolve(context) as? Number + try expect(result) == 5 + } + + $0.it("Falls back to default behaviour for resolvables") { + let variable = CompoundVariable("name") + let result = try variable.resolve(context) as? String + try expect(result) == "Kyle" + } + + $0.it("Unresolvables still produce nil") { + let variable = CompoundVariable("something") + let result = try variable.resolve(context) + try expect(result).to.beNil() + } + + $0.it("Can add two numbers") { + let variable = CompoundVariable("1 + 2") + let result = try variable.resolve(context) as? Number + try expect(result) == 3 + } + + $0.it("Can substract two numbers") { + let variable = CompoundVariable("2 - 4") + let result = try variable.resolve(context) as? Number + try expect(result) == -2 + } + + $0.it("Can multiply two numbers") { + let variable = CompoundVariable("-2 * -4") + let result = try variable.resolve(context) as? Number + try expect(result) == 8 + } + + $0.it("Can divide two numbers") { + let variable = CompoundVariable("4 / 2") + let result = try variable.resolve(context) as? Number + try expect(result) == 2 + } + + $0.it("Can resolve variables") { + let variable = CompoundVariable("a * x") + let result = try variable.resolve(context) as? Number + try expect(result) == 60 + } + + $0.it("Can process a complex expression") { + let variable = CompoundVariable("1 + 2 * 3 - (4 + 6) / 5") + let result = try variable.resolve(context) as? Number + try expect(result) == 5 + } + } }