├── .github └── workflows │ └── build.yml ├── .gitignore ├── .swift-version ├── .swiftformat ├── Benchmarks ├── BenchmarkUtils.swift ├── Benchmarks.swift └── PerformanceTests.swift ├── CHANGELOG.md ├── Examples ├── Benchmark │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── ViewController.swift ├── Calculator │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── ViewController.swift ├── Colors │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── ColorLabel.swift │ ├── Info.plist │ ├── UIColor+Expression.swift │ └── ViewController.swift ├── Layout │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── View+Layout.swift │ └── ViewController.swift └── REPL │ └── main.swift ├── Expression.podspec.json ├── Expression.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ ├── xcbaselines │ ├── 01DD38BB1ED630CB00F17F46.xcbaseline │ │ ├── A544E583-C966-4A17-B7DE-008EFD1B4FC2.plist │ │ └── Info.plist │ └── 01DD38D51ED63B1E00F17F46.xcbaseline │ │ ├── ECC5D8A7-1C11-4DDD-9972-84D3DA20302D.plist │ │ └── Info.plist │ └── xcschemes │ ├── Benchmark.xcscheme │ ├── Calculator.xcscheme │ ├── Colors.xcscheme │ ├── DebugPerformance.xcscheme │ ├── Expression (Mac).xcscheme │ ├── Expression (iOS).xcscheme │ ├── Layout.xcscheme │ └── ReleasePerformance.xcscheme ├── LICENSE.md ├── Package.swift ├── README.md ├── Sources ├── AnyExpression.swift ├── Expression.h ├── Expression.swift └── Info.plist └── Tests ├── AnyExpressionTests.swift ├── ExpressionTests.swift ├── Info.plist └── MetadataTests.swift /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | jobs: 7 | macos: 8 | runs-on: macos-latest 9 | strategy: 10 | matrix: 11 | xcode: ["15.3", "14.3.1"] 12 | steps: 13 | - uses: maxim-lobanov/setup-xcode@v1 14 | with: 15 | xcode-version: ${{ matrix.xcode }} 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | - name: Build and Test 19 | run: 20 | xcodebuild -scheme "Expression (Mac)" -sdk macosx clean build test 21 | - name: Codecov 22 | uses: codecov/codecov-action@v2 23 | with: 24 | # the token is optional for a public repo, but including it anyway 25 | token: f835f552-0734-4266-b5c6-0c184c368bd9 26 | env_vars: MD_APPLE_SDK_ROOT,RUNNER_OS,RUNNER_ARCH 27 | linux: 28 | runs-on: ubuntu-latest 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | swiftver: 33 | - swift:5.2 34 | - swift:5.9 35 | swiftos: 36 | - focal 37 | container: 38 | image: ${{ format('{0}-{1}', matrix.swiftver, matrix.swiftos) }} 39 | options: --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --security-opt apparmor=unconfined 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v3 43 | - name: Build and Test 44 | run: swift test --enable-test-discovery 45 | 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | html/ 7 | *.pbxuser 8 | !default.pbxuser 9 | *.mode1v3 10 | !default.mode1v3 11 | *.mode2v3 12 | !default.mode2v3 13 | *.perspectivev3 14 | !default.perspectivev3 15 | xcuserdata 16 | *.xccheckout 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | *.dot 22 | /.build 23 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.2 -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --hexgrouping ignore 2 | --decimalgrouping ignore 3 | --enable isEmpty 4 | --disable blankLineAfterImports 5 | 6 | --exclude Tests/XCTestManifests.swift -------------------------------------------------------------------------------- /Benchmarks/BenchmarkUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BenchmarkUtils.swift 3 | // Expression 4 | // 5 | // Created by Nick Lockwood on 13/02/2018. 6 | // Copyright © 2018 Nick Lockwood. All rights reserved. 7 | // 8 | 9 | import Expression 10 | import Foundation 11 | 12 | #if os(iOS) || os(macOS) 13 | import JavaScriptCore 14 | #endif 15 | 16 | let symbols: [Expression.Symbol: Expression.SymbolEvaluator] = [ 17 | .variable("a"): { _ in 5 }, 18 | .variable("b"): { _ in 6 }, 19 | .variable("c"): { _ in 7 }, 20 | .variable("hello"): { _ in -5 }, 21 | .variable("world"): { _ in -3 }, 22 | .function("foo", arity: 0): { _ in .pi }, 23 | .function("foo", arity: 2): { $0[0] - $0[1] }, 24 | .function("bar", arity: 1): { $0[0] - 2 }, 25 | ] 26 | 27 | let anySymbols: [AnyExpression.Symbol: AnyExpression.SymbolEvaluator] = { 28 | var anySymbols = [AnyExpression.Symbol: AnyExpression.SymbolEvaluator]() 29 | for (symbol, fn) in symbols { 30 | anySymbols[symbol] = { args in 31 | try fn(args.map { 32 | guard let arg = $0 as? Double else { 33 | throw AnyExpression.Error.message("Type mismatch") 34 | } 35 | return arg 36 | }) 37 | } 38 | } 39 | return anySymbols 40 | }() 41 | 42 | let shortExpressions = [ 43 | "5", 44 | "a", 45 | "foo()", 46 | "hello", 47 | "67", 48 | "3.5", 49 | "pi", 50 | ] 51 | 52 | let mediumExpressions = [ 53 | "5 + 7", 54 | "a + b", 55 | "foo(5, 6)", 56 | "hello + world", 57 | "67 * 2", 58 | "3.5 / 6", 59 | "pi + 15", 60 | ] 61 | 62 | let longExpressions = [ 63 | "5 + min(a, b * 10)", 64 | "max(a + b, b + c)", 65 | "foo(5, 6 + bar(6))", 66 | "hello + world", 67 | "(67 * 2) + (68 * 3)", 68 | "3.5 / 6 + 1234 * 54", 69 | "pi * -56.4 + (5 + 4)", 70 | ] 71 | 72 | let reallyLongExpression: String = { 73 | var parts = [String]() 74 | for i in 0 ..< 100 { 75 | parts.append("\(i)") 76 | } 77 | return "foo(" + parts.joined(separator: "+") + " + bar(5), a) + b" 78 | }() 79 | 80 | let booleanExpressions = [ 81 | "true && false", 82 | "a == b", 83 | "foo(5, 6) != foo(5, 6)", 84 | "a ? hello : world", 85 | "false || true", 86 | "pi > 3", 87 | ] 88 | 89 | // MARK: Expression support 90 | 91 | func buildExpressions(_ expressions: [String]) -> [Expression] { 92 | return expressions.map { 93 | let parsedExpression = Expression.parse($0, usingCache: false) 94 | return Expression(parsedExpression, options: .pureSymbols, symbols: symbols) 95 | } 96 | } 97 | 98 | func evaluateExpressions(_ expressions: [Expression]) -> Double? { 99 | var result: Double? 100 | for _ in 0 ..< evalRepetitions { 101 | for expression in expressions { 102 | result = try! expression.evaluate() 103 | } 104 | } 105 | return result 106 | } 107 | 108 | func evaluateExpressions(_ expressions: [String]) -> Double? { 109 | var result: Double? 110 | for _ in 0 ..< parseRepetitions { 111 | for exp in expressions { 112 | let parsedExpression = Expression.parse(exp, usingCache: false) 113 | let expression = Expression(parsedExpression, options: .pureSymbols, symbols: symbols) 114 | result = try! expression.evaluate() 115 | } 116 | } 117 | return result 118 | } 119 | 120 | func buildAnyExpressions(_ expressions: [String]) -> [AnyExpression] { 121 | return expressions.map { 122 | let parsedExpression = Expression.parse($0, usingCache: false) 123 | return AnyExpression(parsedExpression, options: .pureSymbols, symbols: anySymbols) 124 | } 125 | } 126 | 127 | func evaluateAnyExpressions(_ expressions: [AnyExpression]) -> Double? { 128 | var result: Any? 129 | for _ in 0 ..< evalRepetitions { 130 | for expression in expressions { 131 | result = try! expression.evaluate() 132 | } 133 | } 134 | return (result as? NSNumber).map(Double.init(truncating:)) 135 | } 136 | 137 | func evaluateAnyExpressions(_ expressions: [String]) -> Double? { 138 | var result: Any? 139 | for _ in 0 ..< parseRepetitions { 140 | for exp in expressions { 141 | let parsedExpression = Expression.parse(exp, usingCache: false) 142 | let expression = AnyExpression(parsedExpression, options: .noOptimize, symbols: anySymbols) 143 | result = try! expression.evaluate() 144 | } 145 | } 146 | return (result as? NSNumber).map(Double.init(truncating:)) 147 | } 148 | 149 | // MARK: NSExpression support 150 | 151 | let shortNSExpressions: [String] = shortExpressions.map { 152 | switch $0 { 153 | case "foo()": 154 | return "FUNCTION(0, 'foo')" 155 | default: 156 | return $0 157 | } 158 | } 159 | 160 | let mediumNSExpressions: [String] = mediumExpressions.map { 161 | switch $0 { 162 | case "foo(5, 6)": 163 | return "FUNCTION(5, 'foo:', 6)" 164 | default: 165 | return $0 166 | } 167 | } 168 | 169 | let longNSExpressions: [String] = longExpressions.map { 170 | switch $0 { 171 | case "5 + min(a, b * 10)": 172 | return "5 + FUNCTION(a, 'min:', b * 10)" 173 | case "max(a + b, b + c)": 174 | return "FUNCTION(a + b, 'max:', b + c)" 175 | case "foo(5, 6 + bar(6))": 176 | return "FUNCTION(5, 'foo:', 6 + FUNCTION(6, 'bar'))" 177 | default: 178 | return $0 179 | } 180 | } 181 | 182 | let nsSymbols = [ 183 | "pi": Double.pi, 184 | "a": 5, 185 | "b": 6, 186 | "c": 7, 187 | "hello": -5, 188 | "world": -3, 189 | ] 190 | 191 | extension NSNumber { 192 | @objc func foo() -> NSNumber { 193 | return Double.pi as NSNumber 194 | } 195 | 196 | @objc func foo(_ other: NSNumber) -> NSNumber { 197 | return (Double(truncating: self) + Double(truncating: other)) as NSNumber 198 | } 199 | 200 | @objc func bar() -> NSNumber { 201 | return (Double(truncating: self) + 2) as NSNumber 202 | } 203 | 204 | @objc func min(_ other: NSNumber) -> NSNumber { 205 | return Swift.min(Double(truncating: self), Double(truncating: other)) as NSNumber 206 | } 207 | 208 | @objc func max(_ other: NSNumber) -> NSNumber { 209 | return Swift.max(Double(truncating: self), Double(truncating: other)) as NSNumber 210 | } 211 | } 212 | 213 | func buildNSExpressions(_ expressions: [String]) -> [NSExpression] { 214 | return expressions.map { NSExpression(format: $0) } 215 | } 216 | 217 | func evaluateNSExpressions(_ expressions: [NSExpression]) -> NSNumber? { 218 | var result: NSNumber? 219 | for _ in 0 ..< evalRepetitions { 220 | for expression in expressions { 221 | result = expression.expressionValue(with: nsSymbols, context: nil) as? NSNumber 222 | } 223 | } 224 | return result 225 | } 226 | 227 | func evaluateNSExpressions(_ expressions: [String]) -> NSNumber? { 228 | var result: NSNumber? 229 | for _ in 0 ..< parseRepetitions { 230 | for exp in expressions { 231 | let expression = NSExpression(format: exp) 232 | result = expression.expressionValue(with: nsSymbols, context: nil) as? NSNumber 233 | } 234 | } 235 | return result 236 | } 237 | 238 | #if os(iOS) || os(macOS) 239 | 240 | // MARK: JS symbols 241 | 242 | private let foo: @convention(block) (Double, Double) -> Double = { a, b in 243 | if a.isNaN { 244 | return Double.pi 245 | } 246 | return a + b 247 | } 248 | 249 | private let bar: @convention(block) (Double) -> Double = { 250 | $0 - 2 251 | } 252 | 253 | let jsSymbols: [String: Any] = [ 254 | "pi": Double.pi, 255 | "a": 5, 256 | "b": 6, 257 | "c": 7, 258 | "hello": -5, 259 | "world": -3, 260 | "foo": foo, 261 | "bar": bar, 262 | ] 263 | 264 | func makeJSContext(symbols: [String: Any]) -> JSContext { 265 | let context = JSContext()! 266 | for (key, value) in symbols { 267 | context.globalObject.setValue(value, forProperty: key) 268 | } 269 | return context 270 | } 271 | 272 | func buildJSExpressions(_ expressions: [String]) -> [() -> JSValue] { 273 | return expressions.map { exp -> (() -> JSValue) in 274 | let context = makeJSContext(symbols: jsSymbols) 275 | // Note: it may seem unfair to be evaluating the script inside the block 276 | // however, I tried wrapping the script as a function and storing the 277 | // evaluated result in a JSValue to be executed in the block, and that 278 | // was at least an order of magnitude slower than this approac 279 | return { context.evaluateScript(exp) } 280 | } 281 | } 282 | 283 | func evaluateJSExpressions(_ expressions: [() -> JSValue]) -> JSValue? { 284 | var result: JSValue? 285 | for _ in 0 ..< evalRepetitions { 286 | for expression in expressions { 287 | result = expression() 288 | } 289 | } 290 | return result 291 | } 292 | 293 | func evaluateJSExpressions(_ expressions: [String]) -> JSValue? { 294 | var result: JSValue? 295 | for _ in 0 ..< parseRepetitions { 296 | let context = makeJSContext(symbols: jsSymbols) 297 | for exp in expressions { 298 | result = context.evaluateScript(exp) 299 | } 300 | } 301 | return result 302 | } 303 | 304 | #endif 305 | -------------------------------------------------------------------------------- /Benchmarks/Benchmarks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Benchmarks.swift 3 | // ExpressionTests 4 | // 5 | // Created by Nick Lockwood on 13/02/2018. 6 | // Copyright © 2018 Nick Lockwood. All rights reserved. 7 | // 8 | 9 | import Expression 10 | import XCTest 11 | 12 | #if os(iOS) || os(macOS) 13 | import JavaScriptCore 14 | #endif 15 | 16 | let parseRepetitions = 50 17 | let evalRepetitions = 500 18 | 19 | class Benchmarks: XCTestCase { 20 | // MARK: End-to-end 21 | 22 | func testEndToEndShortExpressions() { 23 | var result: Double? 24 | measure { 25 | result = evaluateExpressions(shortExpressions) 26 | } 27 | XCTAssertEqual(result, Double.pi) 28 | } 29 | 30 | func testEndToEndShortAnyExpressions() { 31 | var result: Double? 32 | measure { 33 | result = evaluateAnyExpressions(shortExpressions) 34 | } 35 | XCTAssertEqual(result, Double.pi) 36 | } 37 | 38 | func testEndToEndShortNSExpressions() { 39 | var result: NSNumber? 40 | measure { 41 | result = evaluateNSExpressions(shortNSExpressions) 42 | } 43 | XCTAssertEqual(result, Double.pi as NSNumber) 44 | } 45 | 46 | #if os(iOS) || os(macOS) 47 | func testEndToEndShortJSExpressions() { 48 | var result: JSValue? 49 | measure { 50 | result = evaluateJSExpressions(shortExpressions) 51 | } 52 | XCTAssertEqual(result?.toNumber(), Double.pi as NSNumber) 53 | } 54 | #endif 55 | 56 | func testEndToEndMediumExpressions() { 57 | var result: Double? 58 | measure { 59 | result = evaluateExpressions(mediumExpressions) 60 | } 61 | XCTAssertEqual(result, Double.pi + 15) 62 | } 63 | 64 | func testEndToEndMediumAnyExpressions() { 65 | var result: Double? 66 | measure { 67 | result = evaluateAnyExpressions(mediumExpressions) 68 | } 69 | XCTAssertEqual(result, Double.pi + 15) 70 | } 71 | 72 | func testEndToEndMediumNSExpressions() { 73 | var result: NSNumber? 74 | measure { 75 | result = evaluateNSExpressions(mediumNSExpressions) 76 | } 77 | XCTAssertEqual(result, (Double.pi + 15) as NSNumber) 78 | } 79 | 80 | #if os(iOS) || os(macOS) 81 | func testEndToEndMediumJSExpressions() { 82 | var result: JSValue? 83 | measure { 84 | result = evaluateJSExpressions(mediumExpressions) 85 | } 86 | XCTAssertEqual(result?.toNumber(), (Double.pi + 15) as NSNumber) 87 | } 88 | #endif 89 | 90 | func testEndToEndLongExpressions() { 91 | var result: Double? 92 | measure { 93 | result = evaluateExpressions(longExpressions) 94 | } 95 | XCTAssertEqual(result, Double.pi * -56.4 + 9) 96 | } 97 | 98 | func testEndToEndLongAnyExpressions() { 99 | var result: Double? 100 | measure { 101 | result = evaluateAnyExpressions(longExpressions) 102 | } 103 | XCTAssertEqual(result, Double.pi * -56.4 + 9) 104 | } 105 | 106 | func testEndToEndLongNSExpressions() { 107 | var result: NSNumber? 108 | measure { 109 | result = evaluateNSExpressions(longNSExpressions) 110 | } 111 | XCTAssertEqual(result, (Double.pi * -56.4 + 9) as NSNumber) 112 | } 113 | 114 | #if os(iOS) || os(macOS) 115 | func testEndToEndLongJSExpressions() { 116 | var result: JSValue? 117 | measure { 118 | result = evaluateJSExpressions(longExpressions) 119 | } 120 | XCTAssertEqual(result?.toNumber(), (Double.pi * -56.4 + 9) as NSNumber) 121 | } 122 | #endif 123 | 124 | // MARK: Evaluation 125 | 126 | func testEvaluateShortExpressions() { 127 | let expressions = buildExpressions(shortExpressions) 128 | var result: Double? 129 | measure { 130 | result = evaluateExpressions(expressions) 131 | } 132 | XCTAssertEqual(result, Double.pi) 133 | } 134 | 135 | func testEvaluateShortAnyExpressions() { 136 | let expressions = buildAnyExpressions(shortExpressions) 137 | var result: Double? 138 | measure { 139 | result = evaluateAnyExpressions(expressions) 140 | } 141 | XCTAssertEqual(result, Double.pi) 142 | } 143 | 144 | func testEvaluateShortNSExpressions() { 145 | let expressions = buildNSExpressions(shortNSExpressions) 146 | var result: NSNumber? 147 | measure { 148 | result = evaluateNSExpressions(expressions) 149 | } 150 | XCTAssertEqual(result, Double.pi as NSNumber) 151 | } 152 | 153 | #if os(iOS) || os(macOS) 154 | func testEvaluateShortJSExpressions() { 155 | let expressions = buildJSExpressions(shortExpressions) 156 | var result: JSValue? 157 | measure { 158 | result = evaluateJSExpressions(expressions) 159 | } 160 | XCTAssertEqual(result?.toNumber(), Double.pi as NSNumber) 161 | } 162 | #endif 163 | 164 | func testEvaluateMediumExpressions() { 165 | let expressions = buildExpressions(mediumExpressions) 166 | var result: Double? 167 | measure { 168 | result = evaluateExpressions(expressions) 169 | } 170 | XCTAssertEqual(result, Double.pi + 15) 171 | } 172 | 173 | func testEvaluateMediumAnyExpressions() { 174 | let expressions = buildAnyExpressions(mediumExpressions) 175 | var result: Double? 176 | measure { 177 | result = evaluateAnyExpressions(expressions) 178 | } 179 | XCTAssertEqual(result, Double.pi + 15) 180 | } 181 | 182 | func testEvaluateMediumNSExpressions() { 183 | let expressions = buildNSExpressions(mediumNSExpressions) 184 | var result: NSNumber? 185 | measure { 186 | result = evaluateNSExpressions(expressions) 187 | } 188 | XCTAssertEqual(result, (Double.pi + 15) as NSNumber) 189 | } 190 | 191 | #if os(iOS) || os(macOS) 192 | func testEvaluateMediumJSExpressions() { 193 | let expressions = buildJSExpressions(mediumExpressions) 194 | var result: JSValue? 195 | measure { 196 | result = evaluateJSExpressions(expressions) 197 | } 198 | XCTAssertEqual(result?.toNumber(), (Double.pi + 15) as NSNumber) 199 | } 200 | #endif 201 | 202 | func testEvaluateLongExpressions() { 203 | let expressions = buildExpressions(longExpressions) 204 | var result: Double? 205 | measure { 206 | result = evaluateExpressions(expressions) 207 | } 208 | XCTAssertEqual(result, Double.pi * -56.4 + 9) 209 | } 210 | 211 | func testEvaluateLongAnyExpressions() { 212 | let expressions = buildAnyExpressions(longExpressions) 213 | var result: Double? 214 | measure { 215 | result = evaluateAnyExpressions(expressions) 216 | } 217 | XCTAssertEqual(result, Double.pi * -56.4 + 9) 218 | } 219 | 220 | func testEvaluateLongNSExpressions() { 221 | let expressions = buildNSExpressions(longNSExpressions) 222 | var result: NSNumber? 223 | measure { 224 | result = evaluateNSExpressions(expressions) 225 | } 226 | XCTAssertEqual(result, (Double.pi * -56.4 + 9) as NSNumber) 227 | } 228 | 229 | #if os(iOS) || os(macOS) 230 | func testEvaluateLongJSExpressions() { 231 | let expressions = buildJSExpressions(longExpressions) 232 | var result: JSValue? 233 | measure { 234 | result = evaluateJSExpressions(expressions) 235 | } 236 | XCTAssertEqual(result?.toNumber(), (Double.pi * -56.4 + 9) as NSNumber) 237 | } 238 | #endif 239 | } 240 | -------------------------------------------------------------------------------- /Benchmarks/PerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerformanceTests.swift 3 | // ExpressionTests 4 | // 5 | // Created by Nick Lockwood on 24/05/2017. 6 | // Copyright © 2017 Nick Lockwood. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/nicklockwood/Expression 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Expression 33 | import XCTest 34 | 35 | class PerformanceTests: XCTestCase { 36 | private let parseRepetitions = 500 37 | private let evalRepetitions = 5000 38 | 39 | // MARK: parsing 40 | 41 | func testParsingShortExpressions() { 42 | measure { parseExpressions(shortExpressions) } 43 | } 44 | 45 | func testParsingMediumExpressions() { 46 | measure { parseExpressions(mediumExpressions) } 47 | } 48 | 49 | func testParsingLongExpressions() { 50 | measure { parseExpressions(longExpressions) } 51 | } 52 | 53 | func testParsingReallyLongExpressions() { 54 | measure { parseExpressions([reallyLongExpression]) } 55 | } 56 | 57 | func testParsingBooleanExpressions() { 58 | measure { parseExpressions(booleanExpressions) } 59 | } 60 | 61 | // MARK: optimizing 62 | 63 | func testOptimizingShortExpressions() { 64 | let expressions = shortExpressions.map { Expression.parse($0, usingCache: false) } 65 | measure { optimizeExpressions(expressions) } 66 | } 67 | 68 | func testOptimizingShortExpressionsWithNewInitializer() { 69 | let expressions = shortExpressions.map { Expression.parse($0, usingCache: false) } 70 | measure { optimizeExpressionsWithNewInitializer(expressions) } 71 | } 72 | 73 | func testOptimizingShortAnyExpressions() { 74 | let expressions = shortExpressions.map { Expression.parse($0, usingCache: false) } 75 | measure { optimizeAnyExpressions(expressions) } 76 | } 77 | 78 | func testOptimizingShortAnyExpressionsWithNewInitializer() { 79 | let expressions = shortExpressions.map { Expression.parse($0, usingCache: false) } 80 | measure { optimizeAnyExpressionsWithNewInitializer(expressions) } 81 | } 82 | 83 | func testOptimizingMediumExpressions() { 84 | let expressions = mediumExpressions.map { Expression.parse($0, usingCache: false) } 85 | measure { optimizeExpressions(expressions) } 86 | } 87 | 88 | func testOptimizingMediumExpressionsWithNewInitializer() { 89 | let expressions = mediumExpressions.map { Expression.parse($0, usingCache: false) } 90 | measure { optimizeExpressionsWithNewInitializer(expressions) } 91 | } 92 | 93 | func testOptimizingMediumAnyExpressions() { 94 | let expressions = mediumExpressions.map { Expression.parse($0, usingCache: false) } 95 | measure { optimizeAnyExpressions(expressions) } 96 | } 97 | 98 | func testOptimizingMediumAnyExpressionsWithNewInitializer() { 99 | let expressions = mediumExpressions.map { Expression.parse($0, usingCache: false) } 100 | measure { optimizeExpressionsWithNewInitializer(expressions) } 101 | } 102 | 103 | func testOptimizingLongExpressions() { 104 | let expressions = longExpressions.map { Expression.parse($0, usingCache: false) } 105 | measure { optimizeExpressions(expressions) } 106 | } 107 | 108 | func testOptimizingLongExpressionsWithNewInitializer() { 109 | let expressions = longExpressions.map { Expression.parse($0, usingCache: false) } 110 | measure { optimizeExpressionsWithNewInitializer(expressions) } 111 | } 112 | 113 | func testOptimizingLongAnyExpressions() { 114 | let expressions = longExpressions.map { Expression.parse($0, usingCache: false) } 115 | measure { optimizeAnyExpressions(expressions) } 116 | } 117 | 118 | func testOptimizingLongAnyExpressionsWithNewInitializer() { 119 | let expressions = longExpressions.map { Expression.parse($0, usingCache: false) } 120 | measure { optimizeAnyExpressionsWithNewInitializer(expressions) } 121 | } 122 | 123 | func testOptimizingReallyLongExpression() { 124 | let exp = Expression.parse(reallyLongExpression, usingCache: false) 125 | measure { optimizeExpressions([exp]) } 126 | } 127 | 128 | func testOptimizinReallyLongExpressionWithNewInitializer() { 129 | let exp = Expression.parse(reallyLongExpression, usingCache: false) 130 | measure { optimizeExpressionsWithNewInitializer([exp]) } 131 | } 132 | 133 | func testOptimizingReallyLongAnyExpression() { 134 | let exp = Expression.parse(reallyLongExpression, usingCache: false) 135 | measure { optimizeAnyExpressions([exp]) } 136 | } 137 | 138 | func testOptimizingReallyLongAnyExpressionWithNewInitializer() { 139 | let exp = Expression.parse(reallyLongExpression, usingCache: false) 140 | measure { optimizeAnyExpressionsWithNewInitializer([exp]) } 141 | } 142 | 143 | func testOptimizingBooleanExpressions() { 144 | let expressions = booleanExpressions.map { Expression.parse($0, usingCache: false) } 145 | measure { optimizeExpressions(expressions) } 146 | } 147 | 148 | func testOptimizingBooleanExpressionsWithNewInitializer() { 149 | let expressions = booleanExpressions.map { Expression.parse($0, usingCache: false) } 150 | measure { optimizeExpressionsWithNewInitializer(expressions) } 151 | } 152 | 153 | func testOptimizingBooleanAnyExpressions() { 154 | let expressions = booleanExpressions.map { Expression.parse($0, usingCache: false) } 155 | measure { optimizeAnyExpressions(expressions) } 156 | } 157 | 158 | func testOptimizingBooleanAnyExpressionsWithNewInitializer() { 159 | let expressions = booleanExpressions.map { Expression.parse($0, usingCache: false) } 160 | measure { optimizeAnyExpressionsWithNewInitializer(expressions) } 161 | } 162 | 163 | // MARK: evaluating 164 | 165 | func testEvaluatingShortExpressions() { 166 | let expressions = shortExpressions.map { Expression($0, options: .pureSymbols, symbols: symbols) } 167 | measure { evaluateExpressions(expressions) } 168 | } 169 | 170 | func testEvaluatingShortAnyExpressions() { 171 | let expressions = shortExpressions.map { AnyExpression($0, options: .pureSymbols, symbols: anySymbols) } 172 | measure { evaluateExpressions(expressions) } 173 | } 174 | 175 | func testEvaluatingMediumExpressions() { 176 | let expressions = mediumExpressions.map { Expression($0, options: .pureSymbols, symbols: symbols) } 177 | measure { evaluateExpressions(expressions) } 178 | } 179 | 180 | func testEvaluatingMediumAnyExpressions() { 181 | let expressions = mediumExpressions.map { AnyExpression($0, options: .pureSymbols, symbols: anySymbols) } 182 | measure { evaluateExpressions(expressions) } 183 | } 184 | 185 | func testEvaluatingLongExpressions() { 186 | let expressions = mediumExpressions.map { Expression($0, options: .pureSymbols, symbols: symbols) } 187 | measure { evaluateExpressions(expressions) } 188 | } 189 | 190 | func testEvaluatingLongAnyExpressions() { 191 | let expressions = mediumExpressions.map { AnyExpression($0, options: .pureSymbols, symbols: anySymbols) } 192 | measure { evaluateExpressions(expressions) } 193 | } 194 | 195 | func testEvaluatingReallyLongExpression() { 196 | let exp = Expression(reallyLongExpression, options: .pureSymbols, symbols: symbols) 197 | measure { evaluateExpressions([exp]) } 198 | } 199 | 200 | func testEvaluatingReallyLongAnyExpression() { 201 | let exp = AnyExpression(reallyLongExpression, options: .pureSymbols, symbols: anySymbols) 202 | measure { evaluateExpressions([exp]) } 203 | } 204 | 205 | func testEvaluatingBooleanExpressions() { 206 | let expressions = booleanExpressions.map { Expression($0, options: [.boolSymbols, .pureSymbols], symbols: symbols) } 207 | measure { evaluateExpressions(expressions) } 208 | } 209 | 210 | func testEvaluatingBooleanAnyExpressions() { 211 | let expressions = booleanExpressions.map { AnyExpression($0, options: [.boolSymbols, .pureSymbols], symbols: anySymbols) } 212 | measure { evaluateExpressions(expressions) } 213 | } 214 | 215 | // MARK: == performance 216 | 217 | func testEquateDoubles() { 218 | let symbols: [AnyExpression.Symbol: AnyExpression.SymbolEvaluator] = [ 219 | .variable("a"): { _ in 5 }, 220 | .variable("b"): { _ in 6 }, 221 | ] 222 | let equalExpression = AnyExpression("a == a", symbols: symbols) 223 | let unequalExpression = AnyExpression("a == b", symbols: symbols) 224 | measure { evaluateExpressions([equalExpression, unequalExpression]) } 225 | XCTAssertTrue(try equalExpression.evaluate()) 226 | XCTAssertFalse(try unequalExpression.evaluate()) 227 | } 228 | 229 | func testEquateBools() { 230 | let symbols: [AnyExpression.Symbol: AnyExpression.SymbolEvaluator] = [ 231 | .variable("a"): { _ in true }, 232 | .variable("b"): { _ in false }, 233 | ] 234 | let equalExpression = AnyExpression("a == a", symbols: symbols) 235 | let unequalExpression = AnyExpression("a == b", symbols: symbols) 236 | measure { evaluateExpressions([equalExpression, unequalExpression]) } 237 | XCTAssertTrue(try equalExpression.evaluate()) 238 | XCTAssertFalse(try unequalExpression.evaluate()) 239 | } 240 | 241 | func testEquateStrings() { 242 | let symbols: [AnyExpression.Symbol: AnyExpression.SymbolEvaluator] = [ 243 | .variable("a"): { _ in "a" }, 244 | .variable("b"): { _ in "b" }, 245 | ] 246 | let equalExpression = AnyExpression("a == a", symbols: symbols) 247 | let unequalExpression = AnyExpression("a == b", symbols: symbols) 248 | measure { evaluateExpressions([equalExpression, unequalExpression]) } 249 | XCTAssertTrue(try equalExpression.evaluate()) 250 | XCTAssertFalse(try unequalExpression.evaluate()) 251 | } 252 | 253 | func testEquateNSObjects() { 254 | let objectA = NSObject() 255 | let symbols: [AnyExpression.Symbol: AnyExpression.SymbolEvaluator] = [ 256 | .variable("a"): { _ in objectA }, 257 | .variable("b"): { _ in NSObject() }, 258 | ] 259 | let equalExpression = AnyExpression("a == a", symbols: symbols) 260 | let unequalExpression = AnyExpression("a == b", symbols: symbols) 261 | measure { evaluateExpressions([equalExpression, unequalExpression]) } 262 | XCTAssertTrue(try equalExpression.evaluate()) 263 | XCTAssertFalse(try unequalExpression.evaluate()) 264 | } 265 | 266 | func testEquateArrays() { 267 | let symbols: [AnyExpression.Symbol: AnyExpression.SymbolEvaluator] = [ 268 | .variable("a"): { _ in ["hello"] }, 269 | .variable("b"): { _ in ["goodbye"] }, 270 | ] 271 | let equalExpression = AnyExpression("a == a", symbols: symbols) 272 | let unequalExpression = AnyExpression("a == b", symbols: symbols) 273 | measure { evaluateExpressions([equalExpression, unequalExpression]) } 274 | XCTAssertTrue(try equalExpression.evaluate()) 275 | XCTAssertFalse(try unequalExpression.evaluate()) 276 | } 277 | 278 | func testEquateHashables() { 279 | let symbols: [AnyExpression.Symbol: AnyExpression.SymbolEvaluator] = [ 280 | .variable("a"): { _ in HashableStruct(foo: 5) }, 281 | .variable("b"): { _ in HashableStruct(foo: 6) }, 282 | ] 283 | let equalExpression = AnyExpression("a == a", symbols: symbols) 284 | let unequalExpression = AnyExpression("a == b", symbols: symbols) 285 | measure { evaluateExpressions([equalExpression, unequalExpression]) } 286 | XCTAssertTrue(try equalExpression.evaluate()) 287 | XCTAssertFalse(try unequalExpression.evaluate()) 288 | } 289 | 290 | func testCompareAgainstNil() { 291 | let symbols: [AnyExpression.Symbol: AnyExpression.SymbolEvaluator] = [ 292 | .variable("a"): { _ in NSNull() }, 293 | .variable("b"): { _ in 5 }, 294 | ] 295 | let equalExpression = AnyExpression("a == a", symbols: symbols) 296 | let unequalExpression = AnyExpression("a == b", symbols: symbols) 297 | measure { evaluateExpressions([equalExpression, unequalExpression]) } 298 | XCTAssertTrue(try equalExpression.evaluate()) 299 | XCTAssertFalse(try unequalExpression.evaluate()) 300 | } 301 | 302 | // MARK: Utility functions 303 | 304 | private func parseExpressions(_ expressions: [String]) { 305 | for _ in 0 ..< parseRepetitions { 306 | for exp in expressions { 307 | _ = Expression.parse(exp, usingCache: false) 308 | } 309 | } 310 | } 311 | 312 | private func optimizeExpressions(_ expressions: [ParsedExpression]) { 313 | for _ in 0 ..< parseRepetitions { 314 | for exp in expressions { 315 | _ = Expression(exp, options: [.pureSymbols, .boolSymbols], symbols: symbols) 316 | } 317 | } 318 | } 319 | 320 | private func optimizeExpressionsWithNewInitializer(_ expressions: [ParsedExpression]) { 321 | for _ in 0 ..< parseRepetitions { 322 | for exp in expressions { 323 | _ = Expression(exp, pureSymbols: { symbols[$0] }) 324 | } 325 | } 326 | } 327 | 328 | private func optimizeAnyExpressions(_ expressions: [ParsedExpression]) { 329 | for _ in 0 ..< parseRepetitions { 330 | for exp in expressions { 331 | _ = AnyExpression(exp, options: [.pureSymbols, .boolSymbols], symbols: anySymbols) 332 | } 333 | } 334 | } 335 | 336 | private func optimizeAnyExpressionsWithNewInitializer(_ expressions: [ParsedExpression]) { 337 | for _ in 0 ..< parseRepetitions { 338 | for exp in expressions { 339 | _ = AnyExpression(exp, pureSymbols: { anySymbols[$0] }) 340 | } 341 | } 342 | } 343 | 344 | private func evaluateExpressions(_ expressions: [Expression]) { 345 | for _ in 0 ..< evalRepetitions { 346 | for exp in expressions { 347 | _ = try! exp.evaluate() 348 | } 349 | } 350 | } 351 | 352 | private func evaluateExpressions(_ expressions: [AnyExpression]) { 353 | for _ in 0 ..< evalRepetitions { 354 | for exp in expressions { 355 | _ = try! exp.evaluate() as Any 356 | } 357 | } 358 | } 359 | 360 | private struct HashableStruct: Hashable { 361 | let foo: Int 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.13.9](https://github.com/nicklockwood/Expression/releases/tag/0.13.9) (2024-07-14) 4 | 5 | - Added `NumericExpression` typealias for Xcode 16 6 | 7 | ## [0.13.8](https://github.com/nicklockwood/Expression/releases/tag/0.13.8) (2024-01-04) 8 | 9 | - Fixed precedence for (^) exponent operator 10 | 11 | ## [0.13.7](https://github.com/nicklockwood/Expression/releases/tag/0.13.7) (2023-02-21) 12 | 13 | - Fixed String.Index crash on macOS 13 / Swift 5.7 14 | - Added log() function to standard library 15 | 16 | ## [0.13.6](https://github.com/nicklockwood/Expression/releases/tag/0.13.6) (2022-03-26) 17 | 18 | - Fixed crash when running with Swift 5.6 / Xcode 13.3 19 | - Fixed warnings and build failures in Xcode 13.3 20 | 21 | ## [0.13.5](https://github.com/nicklockwood/Expression/releases/tag/0.13.5) (2021-10-16) 22 | 23 | - Fixed build failure in benchmark example on Xcode 13 24 | 25 | ## [0.13.4](https://github.com/nicklockwood/Expression/releases/tag/0.13.4) (2021-07-29) 26 | 27 | - Fixed precedence bug where postfix operator was incorrectly applied to entire preceding expression 28 | 29 | ## [0.13.3](https://github.com/nicklockwood/Expression/releases/tag/0.13.3) (2021-07-24) 30 | 31 | - Fixed parsing error with string literals that begin with a double backslash 32 | 33 | ## [0.13.2](https://github.com/nicklockwood/Expression/releases/tag/0.13.2) (2019-06-30) 34 | 35 | - AnyExpression now supports concurrent evaluation on Linux 36 | - Fixed NSString bug on Linux 37 | 38 | ## [0.13.1](https://github.com/nicklockwood/Expression/releases/tag/0.13.1) (2019-06-05) 39 | 40 | - Fixed error in Swift 5.1 beta 41 | 42 | ## [0.13.0](https://github.com/nicklockwood/Expression/releases/tag/0.13.0) (2019-05-10) 43 | 44 | - Fixed Xcode 10.2 warnings 45 | - Expression now requires a minimum Swift version of 4.2 46 | 47 | ## [0.12.12](https://github.com/nicklockwood/Expression/releases/tag/0.12.12) (2019-03-26) 48 | 49 | - Expression now builds correctly on Linux, including the test suite 50 | - Fixed bug where whitespace around operators could affect the precedence 51 | - Fixed a bug where numeric values could incorrectly be printed as a boolean 52 | 53 | ## [0.12.11](https://github.com/nicklockwood/Expression/releases/tag/0.12.11) (2018-06-15) 54 | 55 | - Fixed all warnings in Xcode 10 beta 56 | - Expression now requires Xcode 9.3 or higher 57 | 58 | ## [0.12.10](https://github.com/nicklockwood/Expression/releases/tag/0.12.10) (2018-06-05) 59 | 60 | - Fixed compilation errors in Xcode 10 beta (but not warnings, yet) 61 | - Fixed slow-compiling unit tests 62 | 63 | ## [0.12.9](https://github.com/nicklockwood/Expression/releases/tag/0.12.9) (2018-03-07) 64 | 65 | - AnyExpression now works on Linux, with a couple of caveats (see README for details) 66 | - Fixed Swift Package Manager integration 67 | 68 | ## [0.12.8](https://github.com/nicklockwood/Expression/releases/tag/0.12.8) (2018-02-26) 69 | 70 | - Pure function symbols now take precedence over impure SymbolEvaluator symbols of the same name 71 | 72 | ## [0.12.7](https://github.com/nicklockwood/Expression/releases/tag/0.12.7) (2018-02-26) 73 | 74 | - Anonymous function syntax now works with all symbol types 75 | - AnyExpression `evaluate()` now supports CGFloat values 76 | - AnyExpression now correctly handles `NSArray` and `NSDictionary` values 77 | - Improved AnyExpression stringifying of array, dictionary and partial range values 78 | - Improved error messages for invalid range and type mismatch 79 | - Added AnyExpression REPL example project 80 | 81 | ## [0.12.6](https://github.com/nicklockwood/Expression/releases/tag/0.12.6) (2018-02-22) 82 | 83 | - AnyExpression now supports calling functions stored in a constant or variable 84 | - AnyExpression now supports calling anonymous functions returned by a sub-expression 85 | - Fixed some bugs with array constants incorrectly shadowing symbols 86 | 87 | ## [0.12.5](https://github.com/nicklockwood/Expression/releases/tag/0.12.5) (2018-02-13) 88 | 89 | - Added Benchmark app for comparing Expression performance against NSExpression and JavaScriptCore 90 | - Added support for partial ranges (e.g. `array[startIndex...]`, `string[...upperBound]`, etc) 91 | - Fixed crash when accessing string with an out-of-bounds range 92 | 93 | ## [0.12.4](https://github.com/nicklockwood/Expression/releases/tag/0.12.4) (2018-02-12) 94 | 95 | - Array subscripting operator can now be used with array literals and the result of expressions 96 | - Added support for subscripting strings using either `String.Index` or `Int` character offsets 97 | - AnyExpression now supports range literals using the `...` and `..<` operators 98 | - You can now create substrings or sub-arrays using subscript syntax with range values 99 | - Added automatic casting between `String` and `Substring` within expressions 100 | - AnyExpression's + operator can now concatenate arrays as well as strings 101 | 102 | ## [0.12.3](https://github.com/nicklockwood/Expression/releases/tag/0.12.3) (2018-02-06) 103 | 104 | - AnyExpression now supports array literals like `[1,2,3]` and `["hello", "world"]` 105 | - AnyExpression can now automatically cast between numeric arrays of different types 106 | - Improved messaging for function arity errors and some types of syntax error 107 | - Fixed Swift 3.2 compatibility issues 108 | 109 | ## [0.12.2](https://github.com/nicklockwood/Expression/releases/tag/0.12.2) (2018-02-05) 110 | 111 | - AnyExpression now supports subscripting of `ArraySlice` and `Dictionary` values 112 | - AnyExpression's `evaluate()` type casting now supports almost all numeric types 113 | - Improved AnyExpression error messaging, especially array subscripting errors 114 | 115 | ## [0.12.1](https://github.com/nicklockwood/Expression/releases/tag/0.12.1) (2018-01-31) 116 | 117 | - Reduced initialization time for `Expression` and `AnyExpression` instances 118 | 119 | ## [0.12.0](https://github.com/nicklockwood/Expression/releases/tag/0.12.0) (2018-01-25) 120 | 121 | - An `AnyExpression` instance can now be evaluated concurrently on multiple threads (`Expression` instances were already thread-safe) 122 | - Using the AnyExpression == operator with unsupported types now throws an error instead of returning false 123 | - Significantly reduced initialization time for Expression and AnyExpression instances 124 | - The `boolSymbols` and `noOptimize` options now work correctly for AnyExpression 125 | - Removed all deprecated APIs and deprecation warnings 126 | 127 | ## [0.11.4](https://github.com/nicklockwood/Expression/releases/tag/0.11.4) (2018-01-24) 128 | 129 | - Improved AnyExpression == operator implementation, now supports equating tuples and dictionaries 130 | - Array symbols implemented using `symbols` dictionary are no longer inlined by optimizer, in accordance with documentation 131 | - Deprecated the `Evaluator` function and provided alternative initializers for `Expression` and `AnyExpression` 132 | - Removed deferred optimization. Expressions using a custom `Evaluator` function may now run slower as a result 133 | 134 | ## [0.11.3](https://github.com/nicklockwood/Expression/releases/tag/0.11.3) (2018-01-22) 135 | 136 | - Added new initializers for Expression and AnyExpression to simplify and improve performance when using advanced features 137 | - Attempting to index an array with a non-numeric type in AnyExpression now produces a more meaningful error message 138 | - Fixed optimization bug when using the built-in `pi` constant 139 | 140 | ## [0.11.2](https://github.com/nicklockwood/Expression/releases/tag/0.11.2) (2018-01-18) 141 | 142 | - Significantly improved AnyExpression evaluation performance 143 | - The `pureSymbols` option is now taken into account when optimizing custom AnyExpression symbols 144 | - Added `noDeferredOptimize` option to disable additional optimization of expressions during first evaluation 145 | - Updated performance tests to include tests for boolean expressions and AnyExpression 146 | 147 | ## [0.11.1](https://github.com/nicklockwood/Expression/releases/tag/0.11.1) (2018-01-17) 148 | 149 | - Fixed optimization bug where custom symbols could unexpectedly produce NaN output in AnyExpression 150 | - The `pureSymbols` option now has no effect for AnyExpression (regular Expression is unaffected) 151 | 152 | ## [0.11.0](https://github.com/nicklockwood/Expression/releases/tag/0.11.0) (2018-01-16) 153 | 154 | - Added `AnyExpression` extension for dealing with arbitrary data types 155 | - Renamed `Symbol.Evaluator` to `SymbolEvaluator` (the old name is now deprecated) 156 | - Improved error messages for missing function arguments 157 | 158 | ## [0.10.0](https://github.com/nicklockwood/Expression/releases/tag/0.10.0) (2017-12-28) 159 | 160 | - Added support for variadic functions. This may cause minor breaking changes to custom Evaluator functions 161 | - The built-in `min()` and `max()` functions now both support more than two arguments (using the new variadics support) 162 | 163 | ## [0.9.3](https://github.com/nicklockwood/Expression/releases/tag/0.9.3) (2017-12-18) 164 | 165 | - Hyphens are now only permitted at the start of an operator, which solves an ambiguity with unary minus 166 | - Dots are now only permitted at the start of an operator, which solves an ambiguity with float literals 167 | 168 | ## [0.9.2](https://github.com/nicklockwood/Expression/releases/tag/0.9.2) (2017-12-15) 169 | 170 | - A dot followed by a digit is now treated as a floating point literal instead of an identifier 171 | - Parens are no longer stripped around function arguments containing a comma operator (tuples) 172 | - Fixed edge case when printing description for operators containing special characters 173 | - Refactored parser implementation to removed unreachable code and improve test coverage 174 | - Improved error message when trying to pass multiple arguments to an array subscript 175 | 176 | ## [0.9.1](https://github.com/nicklockwood/Expression/releases/tag/0.9.1) (2017-12-04) 177 | 178 | - Expression description now correctly escapes unprintable characters in quoted symbols 179 | - Expression description no longer adds unnecessary parens around sub-expressions 180 | - More helpful error messages are now generated for various syntax mistakes 181 | - Improved test coverage and fixed many other minor bugs 182 | 183 | ## [0.9.0](https://github.com/nicklockwood/Expression/releases/tag/0.9.0) (2017-12-01) 184 | 185 | - Switched to a more conventional MIT license 186 | - Added support for array symbols, so expressions like `foo[5]` and `bar[x + 1]` are now possible 187 | - Enabled trailing apostrophe in symbol names, so you can use symbols like `x'` 188 | - Added `isValidIdentifier()` and `isValidOperator()` methods for validating symbol names 189 | - Fixed warnings in Xcode 9.1 and dropped support for Swift 3.1 190 | - Improved cache performance 191 | 192 | ## [0.8.5](https://github.com/nicklockwood/Expression/releases/tag/0.8.5) (2017-09-04) 193 | 194 | - Improved expression parsing performance in Swift 3.2 and 4.0 195 | - Fixed some bugs in parsing of identifiers containing dots 196 | 197 | ## [0.8.4](https://github.com/nicklockwood/Expression/releases/tag/0.8.4) (2017-08-22) 198 | 199 | - Fixed spurious parsing errors when expressions have leading whitespace 200 | - The `parse(_: String.UnicodeScalarView)` method now accepts an optional list of terminating delimiters 201 | 202 | ## [0.8.3](https://github.com/nicklockwood/Expression/releases/tag/0.8.3) (2017-08-16) 203 | 204 | - Fixed crash when parsing a malformed expression that contains just a single operator 205 | - Internal `mathSymbols` and `boolSymbols` dictionaries are now public, so you can filter them from symbols array 206 | 207 | ## [0.8.2](https://github.com/nicklockwood/Expression/releases/tag/0.8.2) (2017-08-08) 208 | 209 | - Xcode 9b5 compatibility fixes 210 | 211 | ## [0.8.1](https://github.com/nicklockwood/Expression/releases/tag/0.8.1) (2017-07-25) 212 | 213 | - Now marks the correct token as unexpected when attempting to chain function calls (e.g. `foo(5)(6)`) 214 | - Now produces a clearer error for empty expressions 215 | 216 | ## [0.8.0](https://github.com/nicklockwood/Expression/releases/tag/0.8.0) (2017-07-07) 217 | 218 | - Added `parse(_: String.UnicodeScalarView)` method for parsing expressions embedded in an interpolated string 219 | - Improved parsing of expressions containing ambiguous whitespace around operators 220 | - Fixed some more bugs in the expression description logic 221 | - Removed the deprecated `noCache` option 222 | 223 | ## [0.7.1](https://github.com/nicklockwood/Expression/releases/tag/0.7.1) (2017-07-05) 224 | 225 | - Made `clearCache()` method public (was previously left internal by accident) 226 | - Added additional hard-coded precedence for common operator types and names 227 | - Now supports right-associativity for assignment and comparison operators 228 | - Improved description logic, now correctly handles nested prefix/postfix operators 229 | - Added support for infix alphanumeric operators, in addition to postfix 230 | - Fixed bug when parsing a binary `?:` operator 231 | - Swift 4 compatibility fixes 232 | 233 | ## [0.7.0](https://github.com/nicklockwood/Expression/releases/tag/0.7.0) (2017-06-03) 234 | 235 | - Significantly improved evaluation performance of by storing functions inline inside the parsed expression 236 | - Expressions can now contain quoted string literals, which are treated as identifiers (variable names) 237 | - Added `pureSymbols` optimization option, allowing custom functions and operators to be inlined where possible 238 | - Added deferred optimization, allowing functions that use a custom evaluator take advantage of optimization 239 | - Added `parse(_, usingCache:)` method for fine-grained control of caching and pre-parsing 240 | - The `clearCache()` method now optionally accepts a specific expression to be cleared 241 | - Deprecated the `noCache` option. Use the new `parse(_, usingCache:)` method instead 242 | - Added optimization guide to the README file 243 | 244 | ## [0.6.1](https://github.com/nicklockwood/Expression/releases/tag/0.6.1) (2017-05-28) 245 | 246 | - Fixed bug where optimizer stopped as soon as it encountered a custom symbol in the expression 247 | 248 | ## [0.6.0](https://github.com/nicklockwood/Expression/releases/tag/0.6.0) (2017-05-27) 249 | 250 | - BREAKING CHANGE: `constant` symbols have now been renamed to `variable` to more accurately reflect their behavior 251 | - Minor breaking change: `description` now returns the optimized description 252 | - Added built-in symbol library for boolean operations 253 | - Added thread-safe in-memory caching of previously parsed expressions 254 | - Improved optimizer - now pre-evaluates subexpressions with constant arguments 255 | - Added configuration options for enabling/disabling optimizations and boolean arguments 256 | - Added modulo `%` operator to the standard math symbol library 257 | - Added support for hexadecimal literals 258 | 259 | ## [0.5.0](https://github.com/nicklockwood/Expression/releases/tag/0.5.0) (2017-04-12) 260 | 261 | - Added support for multi-character operators, and precedence rules for most standard operators 262 | - Added special-case support for implementing a ternary `?:` operator with 3 arguments 263 | - Static constants are now replaced by their literal values at initialization time, reducing lookup overhead 264 | - Constant expressions are now computed once at initialization time and then cached 265 | - Numeric literals are now stored as Doubles instead of Strings, avoiding conversion overhead 266 | - Fixed bug where printing an expression omitted the parens around sub-expressions 267 | - Fixed crash when parsing a trailing postfix operator preceded by a space 268 | - Fixed bug in Colors example when running on 32-bit 269 | 270 | ## [0.4.0](https://github.com/nicklockwood/Expression/releases/tag/0.4.0) (2017-03-27) 271 | 272 | - You can now get all symbols used by an expression via the `symbols` property 273 | - Fixed crash with postfix operators followed by comma or closing paren 274 | 275 | ## [0.3](https://github.com/nicklockwood/Expression/releases/tag/0.3) (2017-01-04) 276 | 277 | - Fixed crash when processing malformed expression 278 | - Added support for Swift Package Manager and Linux 279 | - Updated for latest Xcode version 280 | 281 | ## [0.2](https://github.com/nicklockwood/Expression/releases/tag/0.2) (2016-10-15) 282 | 283 | - `Expression.init` no longer throws. The expression will still be compiled on init, but errors won't be thrown until first evaluation 284 | - Added optional `constants` and `symbols` arguments to `Expression.init` for simpler setup of custom functions and operators 285 | - Removed the `constants` param from the `evaluate()` function - this can now be provided in `Expression.init` 286 | - Added automatic error reporting for custom functions called with the wrong arity 287 | - Improved evaluation performance for built-in symbols 288 | 289 | ## [0.1](https://github.com/nicklockwood/Expression/releases/tag/0.1) (2016-10-01) 290 | 291 | - First release 292 | -------------------------------------------------------------------------------- /Examples/Benchmark/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Benchmark 4 | // 5 | // Created by Nick Lockwood on 13/02/2018. 6 | // Copyright © 2018 Nick Lockwood. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | } 15 | -------------------------------------------------------------------------------- /Examples/Benchmark/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Examples/Benchmark/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Examples/Benchmark/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Examples/Benchmark/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Examples/Benchmark/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Benchmark 4 | // 5 | // Created by Nick Lockwood on 13/02/2018. 6 | // Copyright © 2018 Nick Lockwood. All rights reserved. 7 | // 8 | 9 | import Expression 10 | import JavaScriptCore 11 | import UIKit 12 | 13 | let parseRepetitions = 50 14 | let evalRepetitions = 50 15 | 16 | private func time(_ block: () -> Void) -> Double { 17 | let start = CFAbsoluteTimeGetCurrent() 18 | block() 19 | return CFAbsoluteTimeGetCurrent() - start 20 | } 21 | 22 | private func time(_ setup: () -> Any, _ block: (Any) -> Void) -> Double { 23 | let value = setup() 24 | let start = CFAbsoluteTimeGetCurrent() 25 | block(value) 26 | return CFAbsoluteTimeGetCurrent() - start 27 | } 28 | 29 | let formatter: NumberFormatter = { 30 | let formatter = NumberFormatter() 31 | formatter.groupingSeparator = "," 32 | formatter.numberStyle = .decimal 33 | return formatter 34 | }() 35 | 36 | class ViewController: UITableViewController { 37 | var results: [(String, [[(String, Double)]])] = [] 38 | 39 | @objc func update() { 40 | results = [ 41 | ("End-to-end (x\(parseRepetitions))", [ 42 | [ 43 | ("Short Expressions", time { 44 | _ = evaluateExpressions(shortExpressions) 45 | }), 46 | ("Short AnyExpressions", time { 47 | _ = evaluateAnyExpressions(shortExpressions) 48 | }), 49 | ("Short NSExpressions", time { 50 | _ = evaluateNSExpressions(shortNSExpressions) 51 | }), 52 | ("Short JS Expressions", time { 53 | _ = evaluateJSExpressions(shortExpressions) 54 | }), 55 | ], 56 | [ 57 | ("Medium Expressions", time { 58 | _ = evaluateExpressions(mediumExpressions) 59 | }), 60 | ("Medium AnyExpressions", time { 61 | _ = evaluateAnyExpressions(mediumExpressions) 62 | }), 63 | ("Medium NSExpressions", time { 64 | _ = evaluateNSExpressions(mediumNSExpressions) 65 | }), 66 | ("Medium JS Expressions", time { 67 | _ = evaluateJSExpressions(mediumExpressions) 68 | }), 69 | ], 70 | [ 71 | ("Long Expressions", time { 72 | _ = evaluateExpressions(longExpressions) 73 | }), 74 | ("Long AnyExpressions", time { 75 | _ = evaluateAnyExpressions(longExpressions) 76 | }), 77 | ("Long NSExpressions", time { 78 | _ = evaluateNSExpressions(longNSExpressions) 79 | }), 80 | ("Long JS Expressions", time { 81 | _ = evaluateJSExpressions(longExpressions) 82 | }), 83 | ], 84 | ]), 85 | ("Setup (x\(parseRepetitions))", [ 86 | [ 87 | ("Short Expressions", time { 88 | _ = buildExpressions(shortExpressions) 89 | }), 90 | ("Short AnyExpressions", time { 91 | _ = buildAnyExpressions(shortExpressions) 92 | }), 93 | ("Short NSExpressions", time { 94 | _ = buildNSExpressions(shortNSExpressions) 95 | }), 96 | ("Short JS Expressions", time { 97 | _ = buildJSExpressions(shortExpressions) 98 | }), 99 | ], 100 | [ 101 | ("Medium Expressions", time { 102 | _ = buildExpressions(mediumExpressions) 103 | }), 104 | ("Medium AnyExpressions", time { 105 | _ = buildAnyExpressions(mediumExpressions) 106 | }), 107 | ("Medium NSExpressions", time { 108 | _ = buildNSExpressions(mediumNSExpressions) 109 | }), 110 | ("Medium JS Expressions", time { 111 | _ = buildJSExpressions(mediumExpressions) 112 | }), 113 | ], 114 | [ 115 | ("Long Expressions", time { 116 | _ = buildExpressions(longExpressions) 117 | }), 118 | ("Long AnyExpressions", time { 119 | _ = buildAnyExpressions(longExpressions) 120 | }), 121 | ("Long NSExpressions", time { 122 | _ = buildNSExpressions(longNSExpressions) 123 | }), 124 | ("Long JS Expressions", time { 125 | _ = buildJSExpressions(longExpressions) 126 | }), 127 | ], 128 | ]), 129 | ("Evaluation (x\(evalRepetitions))", [ 130 | [ 131 | ("Short Expressions", time( 132 | { buildExpressions(shortExpressions) }, 133 | { _ = evaluateExpressions($0 as! [Expression]) } 134 | )), 135 | ("Short AnyExpressions", time( 136 | { buildAnyExpressions(shortExpressions) }, 137 | { _ = evaluateAnyExpressions($0 as! [AnyExpression]) } 138 | )), 139 | ("Short NSExpressions", time( 140 | { buildNSExpressions(shortNSExpressions) }, 141 | { _ = evaluateNSExpressions($0 as! [NSExpression]) } 142 | )), 143 | ("Short JS Expressions", time( 144 | { buildJSExpressions(shortExpressions) }, 145 | { _ = evaluateJSExpressions($0 as! [() -> JSValue]) } 146 | )), 147 | ], 148 | [ 149 | ("Medium Expressions", time( 150 | { buildExpressions(mediumExpressions) }, 151 | { _ = evaluateExpressions($0 as! [Expression]) } 152 | )), 153 | ("Medium AnyExpressions", time( 154 | { buildAnyExpressions(mediumExpressions) }, 155 | { _ = evaluateAnyExpressions($0 as! [AnyExpression]) } 156 | )), 157 | ("Medium NSExpressions", time( 158 | { buildNSExpressions(mediumNSExpressions) }, 159 | { _ = evaluateNSExpressions($0 as! [NSExpression]) } 160 | )), 161 | ("Medium JS Expressions", time( 162 | { buildJSExpressions(mediumExpressions) }, 163 | { _ = evaluateJSExpressions($0 as! [() -> JSValue]) } 164 | )), 165 | ], 166 | [ 167 | ("Long Expressions", time( 168 | { buildExpressions(longExpressions) }, 169 | { _ = evaluateExpressions($0 as! [Expression]) } 170 | )), 171 | ("Long AnyExpressions", time( 172 | { buildAnyExpressions(longExpressions) }, 173 | { _ = evaluateAnyExpressions($0 as! [AnyExpression]) } 174 | )), 175 | ("Long NSExpressions", time( 176 | { buildNSExpressions(longNSExpressions) }, 177 | { _ = evaluateNSExpressions($0 as! [NSExpression]) } 178 | )), 179 | ("Long JS Expressions", time( 180 | { buildJSExpressions(longExpressions) }, 181 | { _ = evaluateJSExpressions($0 as! [() -> JSValue]) } 182 | )), 183 | ], 184 | ]), 185 | ] 186 | tableView.reloadData() 187 | refreshControl?.endRefreshing() 188 | } 189 | 190 | override func viewDidLoad() { 191 | super.viewDidLoad() 192 | refreshControl = UIRefreshControl() 193 | refreshControl?.addTarget(self, action: #selector(update), for: .valueChanged) 194 | refreshControl?.beginRefreshing() 195 | update() 196 | } 197 | 198 | override func numberOfSections(in _: UITableView) -> Int { 199 | return results.count 200 | } 201 | 202 | override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { 203 | return results[section].1.flatMap { $0 }.count 204 | } 205 | 206 | override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { 207 | return results[section].0 208 | } 209 | 210 | override func tableView(_: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 211 | let subsections = results[indexPath.section].1 212 | let row = subsections.flatMap { $0 }[indexPath.row] 213 | let cell = UITableViewCell(style: .value1, reuseIdentifier: "cell") 214 | 215 | let subsection = subsections[Int(indexPath.row / subsections[0].count)] 216 | let useMS = !subsection.contains(where: { $0.1 < 0.001 }) 217 | 218 | cell.textLabel?.text = row.0 219 | cell.detailTextLabel?.text = useMS ? 220 | "\(formatter.string(from: Int(row.1 * 1000) as NSNumber)!)ms" : 221 | "\(formatter.string(from: Int(row.1 * 1000000) as NSNumber)!)µs" 222 | 223 | cell.detailTextLabel?.textColor = { 224 | if !subsection.contains(where: { $0.1 > row.1 }) { 225 | return .red // worst 226 | } else if !subsection.contains(where: { $0.1 < row.1 }) { 227 | return UIColor(red: 0, green: 0.75, blue: 0, alpha: 1) // best 228 | } else { 229 | return .gray 230 | } 231 | }() 232 | return cell 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /Examples/Calculator/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Calculator 4 | // 5 | // Created by Nick Lockwood on 17/09/2016. 6 | // Copyright © 2016 Nick Lockwood. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/nicklockwood/Expression 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import UIKit 33 | 34 | @UIApplicationMain 35 | class AppDelegate: UIResponder, UIApplicationDelegate { 36 | var window: UIWindow? 37 | } 38 | -------------------------------------------------------------------------------- /Examples/Calculator/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Examples/Calculator/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Examples/Calculator/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /Examples/Calculator/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Examples/Calculator/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Calculator 4 | // 5 | // Created by Nick Lockwood on 17/09/2016. 6 | // Copyright © 2016 Nick Lockwood. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/nicklockwood/Expression 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Expression 33 | import UIKit 34 | 35 | class ViewController: UIViewController, UITextFieldDelegate { 36 | @IBOutlet private var inputField: UITextField! 37 | @IBOutlet private var outputView: UITextView! 38 | 39 | private var output = NSMutableAttributedString() 40 | 41 | private func addOutput(_ string: String, color: UIColor) { 42 | let text = NSAttributedString(string: string + "\n\n", attributes: [ 43 | NSAttributedStringKey.foregroundColor: color, 44 | NSAttributedStringKey.font: outputView.font!, 45 | ]) 46 | 47 | output.replaceCharacters(in: NSMakeRange(0, 0), with: text) 48 | outputView.attributedText = output 49 | } 50 | 51 | override func viewDidLoad() { 52 | super.viewDidLoad() 53 | output.append(outputView.attributedText) 54 | } 55 | 56 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 57 | if let text = textField.text, text != "" { 58 | do { 59 | let result = try Expression(text).evaluate() 60 | addOutput(String(format: "= %g", result), color: .black) 61 | } catch { 62 | addOutput("\(error)", color: .red) 63 | } 64 | } 65 | return false 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Examples/Colors/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Colors 4 | // 5 | // Created by Nick Lockwood on 30/09/2016. 6 | // Copyright © 2016 Nick Lockwood. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | } 15 | -------------------------------------------------------------------------------- /Examples/Colors/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Examples/Colors/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Examples/Colors/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 32 | 37 | 44 | 51 | 58 | 65 | 72 | 79 | 86 | 93 | 100 | 107 | 114 | 121 | 128 | 135 | 142 | 149 | 156 | 163 | 170 | 177 | 184 | 191 | 198 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /Examples/Colors/ColorLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorLabel.swift 3 | // Colors 4 | // 5 | // Created by Nick Lockwood on 30/09/2016. 6 | // Copyright © 2016 Nick Lockwood. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ColorLabel: UILabel { 12 | private func updateColor() { 13 | do { 14 | backgroundColor = .clear 15 | textColor = try UIColor(expression: text ?? "") 16 | } catch { 17 | backgroundColor = .red 18 | textColor = .black 19 | } 20 | } 21 | 22 | override var text: String? { 23 | get { return super.text } 24 | set { 25 | super.text = newValue 26 | updateColor() 27 | } 28 | } 29 | 30 | required init?(coder aDecoder: NSCoder) { 31 | super.init(coder: aDecoder) 32 | updateColor() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Examples/Colors/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeRight 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Examples/Colors/UIColor+Expression.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Expression.swift 3 | // Colors 4 | // 5 | // Created by Nick Lockwood on 30/09/2016. 6 | // Copyright © 2016 Nick Lockwood. All rights reserved. 7 | // 8 | 9 | import Expression 10 | import UIKit 11 | 12 | private let colors: [String: UIColor] = [ 13 | "red": .red, 14 | "green": .green, 15 | "blue": .blue, 16 | "yellow": .yellow, 17 | "purple": .purple, 18 | "cyan": .cyan, 19 | "pink": UIColor(rgba: 0xFF7F7FFF), 20 | "orange": .orange, 21 | "gray": .gray, 22 | "black": .black, 23 | "white": .white, 24 | ] 25 | 26 | private let functions: [AnyExpression.Symbol: AnyExpression.SymbolEvaluator] = [ 27 | .function("rgb", arity: 3): { args in 28 | guard let r = args[0] as? Double, 29 | let g = args[1] as? Double, 30 | let b = args[2] as? Double 31 | else { 32 | throw AnyExpression.Error.message("Type mismatch") 33 | } 34 | return UIColor( 35 | red: CGFloat(r / 255), 36 | green: CGFloat(g / 255), 37 | blue: CGFloat(b / 255), 38 | alpha: 1 39 | ) 40 | }, 41 | .function("rgba", arity: 4): { args in 42 | guard let r = args[0] as? Double, 43 | let g = args[1] as? Double, 44 | let b = args[2] as? Double, 45 | let a = args[3] as? Double 46 | else { 47 | throw Expression.Error.message("Type mismatch") 48 | } 49 | return UIColor( 50 | red: CGFloat(r / 255), 51 | green: CGFloat(g / 255), 52 | blue: CGFloat(b / 255), 53 | alpha: CGFloat(a) 54 | ) 55 | }, 56 | ] 57 | 58 | public extension UIColor { 59 | convenience init(rgba: UInt32) { 60 | let red = CGFloat((rgba & 0xFF000000) >> 24) / 255 61 | let green = CGFloat((rgba & 0x00FF0000) >> 16) / 255 62 | let blue = CGFloat((rgba & 0x0000FF00) >> 8) / 255 63 | let alpha = CGFloat((rgba & 0x000000FF) >> 0) / 255 64 | self.init(red: red, green: green, blue: blue, alpha: alpha) 65 | } 66 | 67 | convenience init(expression: String) throws { 68 | let parsedExpression = Expression.parse(expression) 69 | var constants = [String: Any]() 70 | 71 | for symbol in parsedExpression.symbols { 72 | if case let .variable(name) = symbol { 73 | if name.hasPrefix("#") { 74 | var string = String(name.dropFirst()) 75 | switch string.count { 76 | case 3: 77 | string += "f" 78 | fallthrough 79 | case 4: 80 | let chars = Array(string) 81 | let red = chars[0] 82 | let green = chars[1] 83 | let blue = chars[2] 84 | let alpha = chars[3] 85 | string = "\(red)\(red)\(green)\(green)\(blue)\(blue)\(alpha)\(alpha)" 86 | case 6: 87 | string += "ff" 88 | case 8: 89 | break 90 | default: 91 | // unsupported format 92 | continue 93 | } 94 | guard let rgba = Double("0x" + string).flatMap({ UInt32(exactly: $0) }) else { 95 | throw Expression.Error.message("Unsupported color format") 96 | } 97 | constants[name] = UIColor(rgba: rgba) 98 | } else if let color = colors[name.lowercased()] { 99 | constants[name] = color 100 | } 101 | } 102 | } 103 | let expression = AnyExpression( 104 | expression, 105 | constants: constants, 106 | symbols: functions 107 | ) 108 | let color: UIColor = try expression.evaluate() 109 | self.init(cgColor: color.cgColor) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Examples/Colors/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Colors 4 | // 5 | // Created by Nick Lockwood on 30/09/2016. 6 | // Copyright © 2016 Nick Lockwood. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | // Do any additional setup after loading the view, typically from a nib. 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Examples/Layout/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Layout 4 | // 5 | // Created by Nick Lockwood on 21/09/2016. 6 | // Copyright © 2016 Nick Lockwood. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/nicklockwood/Expression 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import UIKit 33 | 34 | @UIApplicationMain 35 | class AppDelegate: UIResponder, UIApplicationDelegate { 36 | var window: UIWindow? 37 | } 38 | -------------------------------------------------------------------------------- /Examples/Layout/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Examples/Layout/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Examples/Layout/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /Examples/Layout/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Examples/Layout/View+Layout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Layout.swift 3 | // Layout 4 | // 5 | // Created by Nick Lockwood on 21/09/2016. 6 | // Copyright © 2016 Nick Lockwood. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/nicklockwood/Expression 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import Expression 33 | import UIKit 34 | 35 | private class LayoutData: NSObject { 36 | private weak var view: UIView! 37 | private var inProgress = Set() 38 | 39 | func computedValue(forKey key: String) throws -> Double { 40 | if inProgress.contains(key) { 41 | throw Expression.Error.message("Circular reference: \(key) depends on itself") 42 | } 43 | defer { inProgress.remove(key) } 44 | inProgress.insert(key) 45 | 46 | if let expression = props[key] { 47 | return try expression.evaluate() 48 | } 49 | switch key { 50 | case "right": 51 | return try computedValue(forKey: "left") + computedValue(forKey: "width") 52 | case "bottom": 53 | return try computedValue(forKey: "top") + computedValue(forKey: "height") 54 | default: 55 | throw Expression.Error.undefinedSymbol(.variable(key)) 56 | } 57 | } 58 | 59 | private func common(_ symbol: Expression.Symbol) -> Expression.SymbolEvaluator? { 60 | switch symbol { 61 | case .variable("auto"): 62 | return { _ in throw Expression.Error.message("`auto` can only be used for width or height") } 63 | case let .variable(name): 64 | let parts = name.components(separatedBy: ".") 65 | if parts.count == 2 { 66 | return { [unowned self] _ in 67 | if let sublayout = self.view.window?.subview(forKey: parts[0])?.layout { 68 | return try sublayout.computedValue(forKey: parts[1]) 69 | } 70 | throw Expression.Error.message("No view found for key `\(parts[0])`") 71 | } 72 | } 73 | return { [unowned self] _ in 74 | try self.computedValue(forKey: parts[0]) 75 | } 76 | default: 77 | return nil 78 | } 79 | } 80 | 81 | var key: String? 82 | var left: String? { 83 | didSet { 84 | props["left"] = Expression( 85 | Expression.parse(left ?? "0"), 86 | impureSymbols: { symbol in 87 | switch symbol { 88 | case .postfix("%"): 89 | return { [unowned self] args in 90 | self.view.superview.map { Double($0.frame.width) / 100 * args[0] } ?? 0 91 | } 92 | default: 93 | return self.common(symbol) 94 | } 95 | } 96 | ) 97 | } 98 | } 99 | 100 | var top: String? { 101 | didSet { 102 | props["top"] = Expression( 103 | Expression.parse(top ?? "0"), 104 | impureSymbols: { symbol in 105 | switch symbol { 106 | case .postfix("%"): 107 | return { [unowned self] args in 108 | self.view.superview.map { Double($0.frame.height) / 100 * args[0] } ?? 0 109 | } 110 | default: 111 | return self.common(symbol) 112 | } 113 | } 114 | ) 115 | } 116 | } 117 | 118 | var width: String? { 119 | didSet { 120 | props["width"] = Expression( 121 | Expression.parse(width ?? "100%"), 122 | impureSymbols: { symbol in 123 | switch symbol { 124 | case .postfix("%"): 125 | return { [unowned self] args in 126 | self.view.superview.map { Double($0.frame.width) / 100 * args[0] } ?? 0 127 | } 128 | case .variable("auto"): 129 | return { [unowned self] _ in 130 | self.view.superview.map { superview in 131 | Double(self.view.systemLayoutSizeFitting(superview.frame.size).width) 132 | } ?? 0 133 | } 134 | default: 135 | return self.common(symbol) 136 | } 137 | } 138 | ) 139 | } 140 | } 141 | 142 | var height: String? { 143 | didSet { 144 | props["height"] = Expression( 145 | Expression.parse(height ?? "100%"), 146 | impureSymbols: { symbol in 147 | switch symbol { 148 | case .postfix("%"): 149 | return { [unowned self] args in 150 | self.view.superview.map { Double($0.frame.height) / 100 * args[0] } ?? 0 151 | } 152 | case .variable("auto"): 153 | return { [unowned self] _ in 154 | try self.view.superview.map { superview in 155 | var size = superview.frame.size 156 | size.width = try CGFloat(self.computedValue(forKey: "width")) 157 | return Double(self.view.systemLayoutSizeFitting(size).height) 158 | } ?? 0 159 | } 160 | default: 161 | return self.common(symbol) 162 | } 163 | } 164 | ) 165 | } 166 | } 167 | 168 | private var props: [String: Expression] = [:] 169 | 170 | init(_ view: UIView) { 171 | self.view = view 172 | left = nil 173 | top = nil 174 | width = nil 175 | height = nil 176 | } 177 | } 178 | 179 | @IBDesignable 180 | public extension UIView { 181 | fileprivate var layout: LayoutData? { 182 | return layout(create: false) 183 | } 184 | 185 | private func layout(create: Bool) -> LayoutData! { 186 | let layout = layer.value(forKey: "layout") as? LayoutData 187 | if layout == nil, create { 188 | let layout = LayoutData(self) 189 | layer.setValue(layout, forKey: "layout") 190 | return layout 191 | } 192 | return layout 193 | } 194 | 195 | @IBInspectable var key: String? { 196 | get { return layout?.key } 197 | set { layout(create: true).key = newValue } 198 | } 199 | 200 | @IBInspectable var left: String? { 201 | get { return layout?.left } 202 | set { layout(create: true).left = newValue } 203 | } 204 | 205 | @IBInspectable var top: String? { 206 | get { return layout?.top } 207 | set { layout(create: true).top = newValue } 208 | } 209 | 210 | @IBInspectable var width: String? { 211 | get { return layout?.width } 212 | set { layout(create: true).width = newValue } 213 | } 214 | 215 | @IBInspectable var height: String? { 216 | get { return layout?.height } 217 | set { layout(create: true).height = newValue } 218 | } 219 | 220 | fileprivate func subview(forKey key: String) -> UIView? { 221 | if self.key == key { 222 | return self 223 | } 224 | for view in subviews { 225 | if let match = view.subview(forKey: key) { 226 | return match 227 | } 228 | } 229 | return nil 230 | } 231 | 232 | func updateLayout() throws { 233 | guard let layout = layout(create: true) else { 234 | return 235 | } 236 | frame = try CGRect(x: layout.computedValue(forKey: "left"), 237 | y: layout.computedValue(forKey: "top"), 238 | width: layout.computedValue(forKey: "width"), 239 | height: layout.computedValue(forKey: "height")) 240 | 241 | for view in subviews { 242 | try view.updateLayout() 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /Examples/Layout/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Layout 4 | // 5 | // Created by Nick Lockwood on 21/09/2016. 6 | // Copyright © 2016 Nick Lockwood. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/nicklockwood/Expression 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | import UIKit 33 | 34 | class ViewController: UIViewController, UITextFieldDelegate { 35 | @IBOutlet private var leftField: UITextField! 36 | @IBOutlet private var topField: UITextField! 37 | @IBOutlet private var widthField: UITextField! 38 | @IBOutlet private var heightField: UITextField! 39 | @IBOutlet private var errorLabel: UILabel! 40 | @IBOutlet private var layoutView: UIView! 41 | 42 | var selectedView: UIView? { 43 | didSet { 44 | oldValue?.layer.borderWidth = 0 45 | selectedView?.layer.borderWidth = 2 46 | selectedView?.layer.borderColor = UIColor.black.cgColor 47 | leftField.isEnabled = true 48 | leftField.text = selectedView?.left 49 | topField.isEnabled = true 50 | topField.text = selectedView?.top 51 | widthField.isEnabled = true 52 | widthField.text = selectedView?.width 53 | heightField.isEnabled = true 54 | heightField.text = selectedView?.height 55 | } 56 | } 57 | 58 | @IBAction func didTap(sender: UITapGestureRecognizer) { 59 | let point = sender.location(in: layoutView) 60 | if let view = layoutView.hitTest(point, with: nil), view != layoutView { 61 | selectedView = view 62 | } 63 | } 64 | 65 | override func viewDidLayoutSubviews() { 66 | super.viewDidLayoutSubviews() 67 | updateLayout() 68 | } 69 | 70 | func updateLayout() { 71 | do { 72 | for view in layoutView.subviews { 73 | try view.updateLayout() 74 | } 75 | errorLabel.text = nil 76 | } catch { 77 | errorLabel.text = "\(error)" 78 | } 79 | } 80 | 81 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 82 | textField.resignFirstResponder() 83 | return true 84 | } 85 | 86 | func textFieldDidEndEditing(_: UITextField) { 87 | selectedView?.left = leftField.text 88 | selectedView?.top = topField.text 89 | selectedView?.width = widthField.text 90 | selectedView?.height = heightField.text 91 | updateLayout() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Examples/REPL/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // REPL 4 | // 5 | // Created by Nick Lockwood on 23/02/2018. 6 | // Copyright © 2018 Nick Lockwood. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Prevent control characters confusing expression 12 | private let start = UnicodeScalar(63232)! 13 | private let end = UnicodeScalar(63235)! 14 | private let cursorCharacters = CharacterSet(charactersIn: start ... end) 15 | 16 | // Previously defined variables 17 | private var variables = [String: Any]() 18 | 19 | func evaluate(_ parsed: ParsedExpression) throws -> Any { 20 | let expression = AnyExpression(parsed, constants: variables) 21 | return try expression.evaluate() 22 | } 23 | 24 | while true { 25 | print("> ", terminator: "") 26 | guard var input = readLine() else { break } 27 | input = String(input.unicodeScalars.filter { !cursorCharacters.contains($0) }) 28 | do { 29 | var parsed = Expression.parse(input) 30 | if parsed.symbols.contains(where: { $0 == .infix("=") || $0 == .prefix("=") }) { 31 | let range = input.range(of: " = ") ?? input.range(of: "= ") ?? input.range(of: "=")! 32 | parsed = Expression.parse(String(input[range.upperBound...])) 33 | let identifier = input[.. Any in 42 | try expression.evaluate() 43 | } 44 | default: 45 | print("error: Invalid variable name '\(identifier)'") 46 | } 47 | } else { 48 | print("error: Invalid left side for = expression: '\(identifier)'") 49 | } 50 | } else { 51 | try print(AnyExpression.stringify(evaluate(parsed))) 52 | } 53 | } catch { 54 | print("error: \(error)") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Expression.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Expression", 3 | "version": "0.13.9", 4 | "license": { 5 | "type": "MIT", 6 | "file": "LICENSE.md" 7 | }, 8 | "summary": "Mac and iOS library for evaluating numeric expressions at runtime.", 9 | "homepage": "https://github.com/nicklockwood/Expression", 10 | "authors": "Nick Lockwood", 11 | "source": { 12 | "git": "https://github.com/nicklockwood/Expression.git", 13 | "tag": "0.13.9" 14 | }, 15 | "source_files": "Sources", 16 | "requires_arc": true, 17 | "platforms": { 18 | "ios": "11.0", 19 | "osx": "10.13", 20 | "tvos": "11.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Expression.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Expression.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Expression.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Expression.xcodeproj/xcshareddata/xcbaselines/01DD38BB1ED630CB00F17F46.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | A544E583-C966-4A17-B7DE-008EFD1B4FC2 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i7 17 | cpuSpeedInMHz 18 | 2500 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookPro11,5 23 | physicalCPUCoresPerPackage 24 | 4 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | targetDevice 31 | 32 | modelCode 33 | iPhone10,5 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Expression.xcodeproj/xcshareddata/xcbaselines/01DD38D51ED63B1E00F17F46.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | ECC5D8A7-1C11-4DDD-9972-84D3DA20302D 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i7 17 | cpuSpeedInMHz 18 | 2500 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookPro11,5 23 | physicalCPUCoresPerPackage 24 | 4 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | targetDevice 31 | 32 | modelCode 33 | iPhone10,5 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Expression.xcodeproj/xcshareddata/xcschemes/Benchmark.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Expression.xcodeproj/xcshareddata/xcschemes/Calculator.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Expression.xcodeproj/xcshareddata/xcschemes/Colors.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Expression.xcodeproj/xcshareddata/xcschemes/DebugPerformance.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 42 | 43 | 49 | 50 | 52 | 53 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Expression.xcodeproj/xcshareddata/xcschemes/Expression (Mac).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /Expression.xcodeproj/xcshareddata/xcschemes/Expression (iOS).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /Expression.xcodeproj/xcshareddata/xcschemes/Layout.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Expression.xcodeproj/xcshareddata/xcschemes/ReleasePerformance.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Nick Lockwood 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Expression", 6 | products: [ 7 | .library(name: "Expression", targets: ["Expression"]), 8 | ], 9 | targets: [ 10 | .target(name: "Expression", path: "Sources"), 11 | .testTarget(name: "ExpressionTests", dependencies: ["Expression"], path: "Tests"), 12 | ] 13 | ) 14 | -------------------------------------------------------------------------------- /Sources/Expression.h: -------------------------------------------------------------------------------- 1 | // 2 | // Expression.h 3 | // Expression 4 | // 5 | // Created by Nick Lockwood on 15/09/2016. 6 | // Copyright © 2016 Nick Lockwood. All rights reserved. 7 | // 8 | // Distributed under the permissive MIT license 9 | // Get the latest version from here: 10 | // 11 | // https://github.com/nicklockwood/Expression 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in all 21 | // copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | // SOFTWARE. 30 | // 31 | 32 | #import 33 | 34 | //! Project version number for Expression. 35 | FOUNDATION_EXPORT double ExpressionVersionNumber; 36 | 37 | //! Project version string for Expression. 38 | FOUNDATION_EXPORT const unsigned char ExpressionVersionString[]; 39 | 40 | // In this header, you should import all the public headers of your framework using statements like #import 41 | 42 | 43 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2016 Nick Lockwood. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleVersion 18 | 1 19 | 20 | 21 | -------------------------------------------------------------------------------- /Tests/MetadataTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetadataTests.swift 3 | // EuclidTests 4 | // 5 | // Created by Nick Lockwood on 04/07/2021. 6 | // Copyright © 2019 Nick Lockwood. All rights reserved. 7 | // 8 | 9 | @testable import Expression 10 | import XCTest 11 | 12 | private let projectDirectory = URL(fileURLWithPath: #file) 13 | .deletingLastPathComponent().deletingLastPathComponent() 14 | 15 | private let changelogURL = projectDirectory 16 | .appendingPathComponent("CHANGELOG.md") 17 | 18 | private let podspecURL = projectDirectory 19 | .appendingPathComponent("Expression.podspec.json") 20 | 21 | private let projectURL = projectDirectory 22 | .appendingPathComponent("Expression.xcodeproj") 23 | .appendingPathComponent("project.pbxproj") 24 | 25 | private let expressionVersion: String = { 26 | let string = try! String(contentsOf: projectURL) 27 | let start = string.range(of: "MARKETING_VERSION = ")!.upperBound 28 | let end = string.range(of: ";", range: start ..< string.endIndex)!.lowerBound 29 | return String(string[start ..< end]) 30 | }() 31 | 32 | class MetadataTests: XCTestCase { 33 | // MARK: releases 34 | 35 | func testLatestVersionInChangelog() { 36 | let changelog = try! String(contentsOf: changelogURL, encoding: .utf8) 37 | XCTAssertTrue(changelog.contains("[\(expressionVersion)]"), "CHANGELOG.md does not mention latest release") 38 | XCTAssertTrue( 39 | changelog.contains("(https://github.com/nicklockwood/Expression/releases/tag/\(expressionVersion))"), 40 | "CHANGELOG.md does not include correct link for latest release" 41 | ) 42 | } 43 | 44 | func testLatestVersionInPodspec() { 45 | let podspec = try! String(contentsOf: podspecURL, encoding: .utf8) 46 | XCTAssertTrue( 47 | podspec.contains("\"version\": \"\(expressionVersion)\""), 48 | "Podspec version does not match latest release" 49 | ) 50 | XCTAssertTrue( 51 | podspec.contains("\"tag\": \"\(expressionVersion)\""), 52 | "Podspec tag does not match latest release" 53 | ) 54 | } 55 | } 56 | --------------------------------------------------------------------------------