Skip to content

Commit

Permalink
Introduce the idea of a “Configuration”
Browse files Browse the repository at this point in the history
The Configuration struct encapsulates all of the options used by a parser. This makes it easier to add new flags in the future without having to modify a zillion different call-sites
  • Loading branch information
davedelong committed Apr 18, 2019
1 parent c2e8248 commit 34d4438
Show file tree
Hide file tree
Showing 12 changed files with 120 additions and 73 deletions.
6 changes: 5 additions & 1 deletion DDMathParser.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
552CE1C81B938F15007B89A1 /* ExponentExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552CE1C71B938F15007B89A1 /* ExponentExtractor.swift */; };
552CE1CA1B94A1E1007B89A1 /* LocalizedNumberExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552CE1C91B94A1E0007B89A1 /* LocalizedNumberExtractor.swift */; };
5536EF5B1B9BED5200788354 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5536EF5A1B9BED5200788354 /* Double.swift */; };
55375E69226920A100AE4D29 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55375E68226920A100AE4D29 /* Configuration.swift */; };
554AF6B41CDCED3E00C2583E /* MathParserErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554AF6B31CDCED3E00C2583E /* MathParserErrors.swift */; };
557A80691B757C2C0014822A /* Operator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557A80681B757C2C0014822A /* Operator.swift */; };
557A806B1B757C870014822A /* OperatorSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557A806A1B757C870014822A /* OperatorSet.swift */; };
Expand Down Expand Up @@ -153,6 +154,7 @@
552CE1C71B938F15007B89A1 /* ExponentExtractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExponentExtractor.swift; sourceTree = "<group>"; };
552CE1C91B94A1E0007B89A1 /* LocalizedNumberExtractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizedNumberExtractor.swift; sourceTree = "<group>"; };
5536EF5A1B9BED5200788354 /* Double.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
55375E68226920A100AE4D29 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
554AF6B31CDCED3E00C2583E /* MathParserErrors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MathParserErrors.swift; sourceTree = "<group>"; };
557A80681B757C2C0014822A /* Operator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operator.swift; sourceTree = "<group>"; };
557A806A1B757C870014822A /* OperatorSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperatorSet.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -330,6 +332,7 @@
children = (
559B3EFB1B74063600000108 /* MathParser.h */,
554AF6B31CDCED3E00C2583E /* MathParserErrors.swift */,
55375E68226920A100AE4D29 /* Configuration.swift */,
552781561B752C680093B070 /* Operators */,
559B3F121B74071B00000108 /* Tokenizing */,
55E640AF1B76B67A00D7769C /* Resolving */,
Expand Down Expand Up @@ -501,7 +504,7 @@
08FB7793FE84155DC02AAC07 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0910;
LastSwiftUpdateCheck = 1020;
LastUpgradeCheck = 1000;
TargetAttributes = {
55113C031FC35AC7001E508D = {
Expand Down Expand Up @@ -601,6 +604,7 @@
552CE1C81B938F15007B89A1 /* ExponentExtractor.swift in Sources */,
079712FC1EB49A0B003D7C3C /* SubstitutionExtensions.swift in Sources */,
5527814F1B751DD40093B070 /* VariableExtractor.swift in Sources */,
55375E69226920A100AE4D29 /* Configuration.swift in Sources */,
5527814D1B751DCC0093B070 /* IdentifierExtractor.swift in Sources */,
554AF6B41CDCED3E00C2583E /* MathParserErrors.swift in Sources */,
557F8FE81B86BAB400155C9D /* Functions+Defaults.swift in Sources */,
Expand Down
30 changes: 30 additions & 0 deletions MathParser/Configuration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// Configuration.swift
// MathParser
//
// Created by Dave DeLong on 4/18/19.
//

import Foundation

public struct Configuration {

public static let `default` = Configuration()

public var operatorSet: OperatorSet
public var locale: Locale?

public var allowArgumentlessFunctions: Bool
public var allowImplicitMultiplication: Bool
public var useHighPrecedenceImplicitMultiplication: Bool

public init() {
operatorSet = OperatorSet.default
locale = nil

allowArgumentlessFunctions = true
allowImplicitMultiplication = true
useHighPrecedenceImplicitMultiplication = true
}

}
6 changes: 3 additions & 3 deletions MathParser/Expression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ public final class Expression {
public let kind: Kind
public let range: Range<Int>

public init(string: String, operatorSet: OperatorSet = OperatorSet.default, options: TokenResolverOptions = TokenResolverOptions.default, locale: Locale? = nil) throws {
let tokenizer = Tokenizer(string: string, operatorSet: operatorSet, locale: locale)
let resolver = TokenResolver(tokenizer: tokenizer, options: options)
public init(string: String, configuration: Configuration = .default) throws {
let tokenizer = Tokenizer(string: string, configuration: configuration)
let resolver = TokenResolver(tokenizer: tokenizer)
let grouper = TokenGrouper(resolver: resolver)
let expressionizer = Expressionizer(grouper: grouper)

Expand Down
47 changes: 14 additions & 33 deletions MathParser/TokenResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,12 @@

import Foundation

public struct TokenResolverOptions: OptionSet {
public let rawValue: UInt

public init(rawValue: UInt) {
self.rawValue = rawValue
}

public static let none = TokenResolverOptions(rawValue: 0)
public static let allowArgumentlessFunctions = TokenResolverOptions(rawValue: 1 << 0)
public static let allowImplicitMultiplication = TokenResolverOptions(rawValue: 1 << 1)
public static let useHighPrecedenceImplicitMultiplication = TokenResolverOptions(rawValue: 1 << 2)

public static let `default`: TokenResolverOptions = [.allowArgumentlessFunctions, .allowImplicitMultiplication, .useHighPrecedenceImplicitMultiplication]
}

public struct TokenResolver {

private let tokenizer: Tokenizer
fileprivate let options: TokenResolverOptions
fileprivate let locale: Locale?
private var configuration: Configuration { return tokenizer.configuration }
fileprivate let numberFormatters: Array<NumberFormatter>
internal var operatorSet: OperatorSet { return tokenizer.operatorSet }
internal var operatorSet: OperatorSet { return configuration.operatorSet }

private static func formattersForLocale(_ locale: Locale?) -> Array<NumberFormatter> {
guard let locale = locale else { return [] }
Expand All @@ -41,18 +25,15 @@ public struct TokenResolver {
return [decimal]
}

public init(tokenizer: Tokenizer, options: TokenResolverOptions = TokenResolverOptions.default) {
public init(tokenizer: Tokenizer) {
self.tokenizer = tokenizer
self.options = options
self.locale = tokenizer.locale
self.numberFormatters = TokenResolver.formattersForLocale(tokenizer.locale)
let locale = tokenizer.configuration.locale
self.numberFormatters = TokenResolver.formattersForLocale(locale)
}

public init(string: String, operatorSet: OperatorSet = OperatorSet.default, options: TokenResolverOptions = TokenResolverOptions.default, locale: Locale? = nil) {
self.tokenizer = Tokenizer(string: string, operatorSet: operatorSet, locale: locale)
self.options = options
self.locale = locale
self.numberFormatters = TokenResolver.formattersForLocale(locale)
public init(string: String, configuration: Configuration = .default) {
let t = Tokenizer(string: string, configuration: configuration)
self.init(tokenizer: t)
}

public func resolve() throws -> Array<ResolvedToken> {
Expand All @@ -79,7 +60,7 @@ extension TokenResolver {
guard let raw = raw else {
// this is the case where the we check for argumentless stuff
// after the last token
if options.contains(.allowArgumentlessFunctions) {
if configuration.allowArgumentlessFunctions {
return extraTokensForArgumentlessFunction(nil, previous: previous)
} else {
return []
Expand All @@ -94,13 +75,13 @@ extension TokenResolver {
var final = Array<ResolvedToken>()

// check for argumentless functions
if options.contains(.allowArgumentlessFunctions) {
if configuration.allowArgumentlessFunctions {
let extras = extraTokensForArgumentlessFunction(firstResolved, previous: previous)
final.append(contentsOf: extras)
}

// check for implicit multiplication
if options.contains(.allowImplicitMultiplication) {
if configuration.allowImplicitMultiplication {
let last = final.last ?? previous
let extras = extraTokensForImplicitMultiplication(firstResolved, previous: last)
final.append(contentsOf: extras)
Expand Down Expand Up @@ -188,8 +169,8 @@ extension TokenResolver {

resolved += [power, openParen]

let exponentTokenizer = Tokenizer(string: raw.string, operatorSet: operatorSet, locale: locale)
let exponentResolver = TokenResolver(tokenizer: exponentTokenizer, options: options)
let exponentTokenizer = Tokenizer(string: raw.string, configuration: configuration)
let exponentResolver = TokenResolver(tokenizer: exponentTokenizer)

let exponentTokens = try exponentResolver.resolve()

Expand Down Expand Up @@ -323,7 +304,7 @@ extension TokenResolver {
guard previousMatches && nextMatches else { return [] }

let multiplyOperator: Operator
if options.contains(.useHighPrecedenceImplicitMultiplication) {
if configuration.useHighPrecedenceImplicitMultiplication {
multiplyOperator = operatorSet.implicitMultiplyOperator
} else {
multiplyOperator = operatorSet.multiplyOperator
Expand Down
12 changes: 5 additions & 7 deletions MathParser/Tokenizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,20 @@ public struct Tokenizer {
internal typealias Result = Either<RawToken, MathParserError>

private let string: String
internal let locale: Locale?
internal let operatorSet: OperatorSet
internal let configuration: Configuration

private let buffer: TokenCharacterBuffer
private let extractors: Array<TokenExtractor>

public init(string: String, operatorSet: OperatorSet = OperatorSet.default, locale: Locale? = nil) {
public init(string: String, configuration: Configuration = .default) {
self.string = string
self.operatorSet = operatorSet
self.locale = locale
self.configuration = configuration

buffer = TokenCharacterBuffer(string: string)
let operatorTokens = operatorSet.operatorTokenSet
let operatorTokens = configuration.operatorSet.operatorTokenSet

let numberExtractor: TokenExtractor
if let locale = locale {
if let locale = configuration.locale {
numberExtractor = LocalizedNumberExtractor(locale: locale)
} else {
numberExtractor = DecimalNumberExtractor()
Expand Down
5 changes: 4 additions & 1 deletion MathParserTests/EvaluatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,11 @@ class EvaluatorTests: XCTestCase {
"true or false is true": 1
]

var c = Configuration.default
c.operatorSet = operatorSet

for (test, value) in tests {
guard let e = XCTAssertNoThrows(try Expression(string: test, operatorSet: operatorSet)) else { return }
guard let e = XCTAssertNoThrows(try Expression(string: test, configuration: c)) else { return }
guard let d = XCTAssertNoThrows(try Evaluator.default.evaluate(e)) else { return }
XCTAssertEqual(d, value, "Test failed: \(test)")
}
Expand Down
10 changes: 5 additions & 5 deletions MathParserTests/ExpressionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,8 @@ class ExpressionTests: XCTestCase {

func testMissingOperator() {
do {
let tokenizer = Tokenizer(string: "1 2")
let resolver = TokenResolver(tokenizer: tokenizer, options: [])
let tokenizer = Tokenizer(string: "1 2", configuration: .defaultWithEmptyOptions)
let resolver = TokenResolver(tokenizer: tokenizer)
let grouper = TokenGrouper(resolver: resolver)
let expressionizer = Expressionizer(grouper: grouper)
let _ = try expressionizer.expression()
Expand Down Expand Up @@ -335,9 +335,9 @@ class ExpressionTests: XCTestCase {
}

func testInvalidFormat() {
var options = TokenResolverOptions.default
options.remove(.allowImplicitMultiplication)
guard let e = XCTAssertNoThrows(try Expression(string: "3$x", options: options)) else { return }
var c = Configuration.default
c.allowImplicitMultiplication = false
guard let e = XCTAssertNoThrows(try Expression(string: "3$x", configuration: c)) else { return }
print("\(e)")
}

Expand Down
19 changes: 13 additions & 6 deletions MathParserTests/GithubIssues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,9 @@ class GithubIssues: XCTestCase {
}

func testIssue40() {
let operatorSet = OperatorSet(interpretsPercentSignAsModulo: false)
guard let e = XCTAssertNoThrows(try Expression(string: "7+5%", operatorSet: operatorSet)) else { return }
var c = Configuration.default
c.operatorSet = OperatorSet(interpretsPercentSignAsModulo: false)
guard let e = XCTAssertNoThrows(try Expression(string: "7+5%", configuration: c)) else { return }

let eval = Evaluator.default
guard let d = XCTAssertNoThrows(try eval.evaluate(e)) else { return }
Expand Down Expand Up @@ -213,10 +214,12 @@ class GithubIssues: XCTestCase {
}

func testIssue75() {
var c = Configuration.default
let operatorSet = OperatorSet()
operatorSet.addTokens(["and"], forOperator: Operator(builtInOperator: .logicalAnd))
c.operatorSet = operatorSet

guard let e = XCTAssertNoThrows(try Expression(string: "1 and 2", operatorSet: operatorSet)) else { return }
guard let e = XCTAssertNoThrows(try Expression(string: "1 and 2", configuration: c)) else { return }

let eval = Evaluator.default
guard let d = XCTAssertNoThrows(try eval.evaluate(e)) else { return }
Expand Down Expand Up @@ -276,10 +279,12 @@ class GithubIssues: XCTestCase {
}

func testIssue105() {
var c = Configuration.default
let operatorSet = OperatorSet()
operatorSet.addTokens(["or"], forOperator: Operator(builtInOperator: .logicalOr))
c.operatorSet = operatorSet

guard let e = XCTAssertNoThrows(try Expression(string: "cos(pi)", operatorSet: operatorSet)) else { return }
guard let e = XCTAssertNoThrows(try Expression(string: "cos(pi)", configuration: c)) else { return }

let eval = Evaluator.default
guard let d = XCTAssertNoThrows(try eval.evaluate(e)) else { return }
Expand All @@ -301,10 +306,12 @@ class GithubIssues: XCTestCase {
}

func testIssue109() {
var c = Configuration.default
let operatorSet = OperatorSet()
operatorSet.addTokens(["as"], forOperator: Operator(builtInOperator: .logicalEqual))
c.operatorSet = operatorSet

guard let e = XCTAssertNoThrows(try Expression(string: "asin(0.5)", operatorSet: operatorSet)) else { return }
guard let e = XCTAssertNoThrows(try Expression(string: "asin(0.5)", configuration: c)) else { return }

let eval = Evaluator.default
guard let d = XCTAssertNoThrows(try eval.evaluate(e)) else { return }
Expand Down Expand Up @@ -337,7 +344,7 @@ class GithubIssues: XCTestCase {

func testIssue138() {
let tokenizer = Tokenizer(string: "pow")
let resolver = TokenResolver(tokenizer: tokenizer, options: .default)
let resolver = TokenResolver(tokenizer: tokenizer)
let grouper = TokenGrouper(resolver: resolver)
let expressionizer = Expressionizer(grouper: grouper)
let expression = try! expressionizer.expression()
Expand Down
4 changes: 2 additions & 2 deletions MathParserTests/GroupingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class GroupingTests: XCTestCase {
}

func testFunctionMissingOpenParenthesis() {
let r = TokenResolver(string: "foo", options: [])
let r = TokenResolver(string: "foo", configuration: .defaultWithEmptyOptions)
let g = TokenGrouper(resolver: r)

do {
Expand All @@ -99,7 +99,7 @@ class GroupingTests: XCTestCase {
}

func testFunctionMissingCloseParenthesis() {
let r = TokenResolver(string: "foo(", options: [])
let r = TokenResolver(string: "foo(", configuration: .defaultWithEmptyOptions)
let g = TokenGrouper(resolver: r)

do {
Expand Down
Loading

0 comments on commit 34d4438

Please sign in to comment.