Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic numeric expresion handling #86

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 67 additions & 5 deletions Sources/Variable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import Foundation

typealias Number = Float

public protocol GenericVariable: Equatable {
var variable: String { get }
}

public func ==<T: GenericVariable>(lhs: T, rhs: T) -> Bool {
return lhs.variable == rhs.variable
}


class FilterExpression : Resolvable {
let filters: [(FilterType, [Variable])]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
68 changes: 68 additions & 0 deletions Tests/StencilTests/VariableSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}