Skip to content

Commit e10a0bc

Browse files
committed
Refactored token substitution
Added some cases for phrase tokens to TokenType. Extracted token replacement code for a String extension method. Added tests for token substitution in phrases. Simplified test code.
1 parent b7effa3 commit e10a0bc

File tree

3 files changed

+142
-84
lines changed

3 files changed

+142
-84
lines changed

OSRMTextInstructions/OSRMTextInstructions.swift

Lines changed: 73 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,61 @@ extension String {
88
public var sentenceCased: String {
99
return String(characters.prefix(1)).uppercased() + String(characters.dropFirst())
1010
}
11+
12+
/**
13+
Replaces `{tokens}` in the receiver using the given closure.
14+
*/
15+
public func replacingTokens(using interpolator: ((TokenType) -> String)) -> String {
16+
let scanner = Scanner(string: self)
17+
scanner.charactersToBeSkipped = nil
18+
var result = ""
19+
while !scanner.isAtEnd {
20+
var buffer: NSString?
21+
22+
if scanner.scanUpTo("{", into: &buffer) {
23+
result += buffer! as String
24+
}
25+
guard scanner.scanString("{", into: nil) else {
26+
continue
27+
}
28+
29+
var token: NSString?
30+
guard scanner.scanUpTo("}", into: &token) else {
31+
continue
32+
}
33+
34+
if scanner.scanString("}", into: nil) {
35+
if let tokenType = TokenType(description: token! as String) {
36+
result += interpolator(tokenType)
37+
}
38+
} else {
39+
result += token! as String
40+
}
41+
}
42+
43+
// remove excess spaces
44+
result = result.replacingOccurrences(of: "\\s\\s", with: " ", options: .regularExpression)
45+
46+
// capitalize
47+
let meta = OSRMTextInstructionsStrings["meta"] as! [String: Any]
48+
if meta["capitalizeFirstLetter"] as? Bool ?? false {
49+
result = result.sentenceCased
50+
}
51+
return result
52+
}
53+
}
54+
55+
public enum Phrase: String {
56+
case linkedInstructionsWithDistance = "two linked by distance"
57+
case linkedInstructions = "two linked"
58+
case instructionWithDistance = "one in distance"
59+
case nameWithRef = "name and ref"
60+
}
61+
62+
public struct PhraseOption {
63+
static let firstInstruction = "instruction_one"
64+
static let secondInstruction = "instruction_two"
65+
static let distance = "distance"
1166
}
1267

1368
public class OSRMInstructionFormatter: Formatter {
@@ -254,58 +309,27 @@ public class OSRMInstructionFormatter: Formatter {
254309
if step.finalHeading != nil { bearing = Int(step.finalHeading! as Double) }
255310

256311
// Replace tokens
257-
let scanner = Scanner(string: instruction)
258-
scanner.charactersToBeSkipped = nil
259-
var result = ""
260-
while !scanner.isAtEnd {
261-
var buffer: NSString?
262-
263-
if scanner.scanUpTo("{", into: &buffer) {
264-
result += buffer! as String
265-
}
266-
guard scanner.scanString("{", into: nil) else {
267-
continue
268-
}
269-
270-
var token: NSString?
271-
guard scanner.scanUpTo("}", into: &token) else {
272-
continue
312+
let result = instruction.replacingTokens { (tokenType) -> String in
313+
var replacement: String
314+
switch tokenType {
315+
case .code: replacement = step.codes?.first ?? ""
316+
case .wayName: replacement = wayName
317+
case .destination: replacement = destination
318+
case .exitCode: replacement = exitCode
319+
case .exitIndex: replacement = exitOrdinal
320+
case .rotaryName: replacement = rotaryName
321+
case .laneInstruction: replacement = laneInstruction ?? ""
322+
case .modifier: replacement = modifierConstant
323+
case .direction: replacement = directionFromDegree(degree: bearing)
324+
case .wayPoint: replacement = nthWaypoint ?? ""
325+
case .firstInstruction, .secondInstruction, .distance:
326+
fatalError("Unexpected token type \(tokenType) in individual instruction")
273327
}
274-
275-
if scanner.scanString("}", into: nil) {
276-
if let tokenType = TokenType(description: token! as String) {
277-
var replacement: String
278-
switch tokenType {
279-
case .code: replacement = step.codes?.first ?? ""
280-
case .wayName: replacement = wayName
281-
case .destination: replacement = destination
282-
case .exitCode: replacement = exitCode
283-
case .exitIndex: replacement = exitOrdinal
284-
case .rotaryName: replacement = rotaryName
285-
case .laneInstruction: replacement = laneInstruction ?? ""
286-
case .modifier: replacement = modifierConstant
287-
case .direction: replacement = directionFromDegree(degree: bearing)
288-
case .wayPoint: replacement = nthWaypoint ?? ""
289-
}
290-
if tokenType == .wayName {
291-
result += replacement // already modified above
292-
} else {
293-
result += modifyValueByKey?(tokenType, replacement) ?? replacement
294-
}
295-
}
328+
if tokenType == .wayName {
329+
return replacement // already modified above
296330
} else {
297-
result += token! as String
331+
return modifyValueByKey?(tokenType, replacement) ?? replacement
298332
}
299-
300-
}
301-
302-
// remove excess spaces
303-
result = result.replacingOccurrences(of: "\\s\\s", with: " ", options: .regularExpression)
304-
305-
// capitalize
306-
let meta = OSRMTextInstructionsStrings["meta"] as! [String: Any]
307-
if meta["capitalizeFirstLetter"] as? Bool ?? false {
308-
result = result.sentenceCased
309333
}
310334

311335
return result

OSRMTextInstructions/TokenType.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22

33
@objc(OSRMTokenType)
44
public enum TokenType: Int, CustomStringConvertible {
5-
5+
// For individual instructions
66
case wayName
77
case destination
88
case rotaryName
@@ -14,6 +14,11 @@ public enum TokenType: Int, CustomStringConvertible {
1414
case wayPoint
1515
case code
1616

17+
// For phrases
18+
case firstInstruction
19+
case secondInstruction
20+
case distance
21+
1722
public init?(description: String) {
1823
let type: TokenType
1924
switch description {
@@ -37,6 +42,12 @@ public enum TokenType: Int, CustomStringConvertible {
3742
type = .wayPoint
3843
case "ref":
3944
type = .code
45+
case "instruction_one":
46+
type = .firstInstruction
47+
case "instruction_two":
48+
type = .secondInstruction
49+
case "distance":
50+
type = .distance
4051
default:
4152
return nil
4253
}
@@ -65,6 +76,12 @@ public enum TokenType: Int, CustomStringConvertible {
6576
return "nth"
6677
case .code:
6778
return "ref"
79+
case .firstInstruction:
80+
return "instruction_one"
81+
case .secondInstruction:
82+
return "instruction_two"
83+
case .distance:
84+
return "distance"
6885
}
6986
}
7087
}

OSRMTextInstructionsTests/OSRMTextInstructionsTests.swift

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,60 @@ class OSRMTextInstructionsTests: XCTestCase {
2121
}
2222

2323
func testFixtures() {
24-
do {
25-
let bundle = Bundle(for: OSRMTextInstructionsTests.self)
26-
let url = bundle.url(forResource: "v5", withExtension: nil, subdirectory: "osrm-text-instructions/test/fixtures/")!
27-
28-
let directoryContents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
29-
for type in directoryContents {
30-
let typeDirectoryContents = try FileManager.default.contentsOfDirectory(at: type, includingPropertiesForKeys: nil, options: [])
31-
for fixture in typeDirectoryContents {
24+
let bundle = Bundle(for: OSRMTextInstructionsTests.self)
25+
let url = bundle.url(forResource: "v5", withExtension: nil, subdirectory: "osrm-text-instructions/test/fixtures/")!
26+
27+
let v5 = OSRMTextInstructionsStrings["v5"] as! [String: Any]
28+
let phrases = v5["phrase"] as! [String: String]
29+
30+
var directoryContents: [URL] = []
31+
XCTAssertNoThrow(directoryContents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: []))
32+
for type in directoryContents {
33+
var typeDirectoryContents: [URL] = []
34+
XCTAssertNoThrow(typeDirectoryContents = try FileManager.default.contentsOfDirectory(at: type, includingPropertiesForKeys: nil, options: []))
35+
for fixture in typeDirectoryContents {
36+
if type.lastPathComponent == "phrase" {
37+
var rawJSON = Data()
38+
XCTAssertNoThrow(rawJSON = try Data(contentsOf: fixture, options: []))
39+
40+
var json: [String: Any] = [:]
41+
XCTAssertNoThrow(json = try JSONSerialization.jsonObject(with: rawJSON, options: []) as! [String: Any])
42+
43+
let phrase = fixture.deletingPathExtension().lastPathComponent.replacingOccurrences(of: "_", with: " ")
44+
let fixtureOptions = json["options"] as! [String: String]
45+
46+
let expectedValue = (json["phrases"] as! [String: String])["en"]
47+
let actualValue = phrases[phrase]?.replacingTokens(using: { (tokenType) -> String in
48+
var replacement: String?
49+
switch tokenType {
50+
case .firstInstruction:
51+
replacement = fixtureOptions["instruction_one"]
52+
case .secondInstruction:
53+
replacement = fixtureOptions["instruction_two"]
54+
case .distance:
55+
replacement = fixtureOptions["distance"]
56+
default:
57+
XCTFail("Unexpected token type \(tokenType) in phrase \(phrase)")
58+
}
59+
XCTAssertNotNil(replacement, "Missing fixture option for \(tokenType)")
60+
return replacement ?? ""
61+
})
62+
XCTAssertEqual(expectedValue, actualValue, fixture.path)
63+
} else {
3264
// parse fixture
33-
guard let json = getFixture(url: fixture) else {
34-
XCTAssert(false, "invalid json")
35-
return
36-
}
65+
let json = getFixture(url: fixture)
3766
let options = json["options"] as? [String: Any]
38-
67+
3968
let step = RouteStep(json: json["step"] as! [String: Any])
4069

4170
var roadClasses: RoadClasses? = nil
4271
if let classes = options?["classes"] as? [String] {
4372
roadClasses = RoadClasses(descriptions: classes)
4473
}
45-
74+
4675
// compile instruction
4776
let instruction = instructions.string(for: step, legIndex: options?["legIndex"] as? Int, numberOfLegs: options?["legCount"] as? Int, roadClasses: roadClasses, modifyValueByKey: nil)
48-
77+
4978
// check generated instruction against fixture
5079
XCTAssertEqual(
5180
json["instruction"] as? String,
@@ -54,35 +83,23 @@ class OSRMTextInstructionsTests: XCTestCase {
5483
)
5584
}
5685
}
57-
} catch let error as NSError {
58-
print(error.localizedDescription)
59-
XCTAssert(false)
6086
}
6187
}
6288

63-
func getFixture(url: URL) -> NSMutableDictionary? {
89+
func getFixture(url: URL) -> [String: Any] {
6490
var rawJSON = Data()
65-
do {
66-
rawJSON = try Data(contentsOf: url, options: [])
67-
} catch {
68-
XCTAssert(false)
69-
}
91+
XCTAssertNoThrow(rawJSON = try Data(contentsOf: url, options: []))
7092

71-
let json: NSDictionary
72-
do {
73-
json = try JSONSerialization.jsonObject(with: rawJSON, options: []) as! NSDictionary
74-
} catch {
75-
XCTAssert(false)
76-
return nil
77-
}
93+
var json: [String: Any] = [:]
94+
XCTAssertNoThrow(json = try JSONSerialization.jsonObject(with: rawJSON, options: []) as! [String: Any])
7895

7996
// provide default values for properties that RouteStep
8097
// needs, but that our not in the fixtures
81-
let fixture: NSMutableDictionary = [:]
82-
let maneuver: NSMutableDictionary = [
98+
var fixture: [String: Any] = [:]
99+
var maneuver: [String: Any] = [
83100
"location": [ 1.0, 1.0 ]
84101
]
85-
let step: NSMutableDictionary = [
102+
var step: [String: Any] = [
86103
"mode": "driving"
87104
]
88105

0 commit comments

Comments
 (0)