Skip to content

Commit 1d999b5

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 1d999b5

File tree

3 files changed

+141
-86
lines changed

3 files changed

+141
-86
lines changed

OSRMTextInstructions/OSRMTextInstructions.swift

Lines changed: 72 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,48 @@ 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+
}
1153
}
1254

1355
public class OSRMInstructionFormatter: Formatter {
@@ -192,7 +234,18 @@ public class OSRMInstructionFormatter: Formatter {
192234
let isMotorway = roadClasses?.contains(.motorway) ?? false
193235

194236
if let name = name, let ref = ref, name != ref, !isMotorway {
195-
wayName = modifyValueByKey != nil ? "\(modifyValueByKey!(.wayName, name)) (\(modifyValueByKey!(.code, ref)))" : "\(name) (\(ref))"
237+
let phrases = instructions["phrase"] as! [String: String]
238+
let phrase = phrases["name and ref"]!
239+
wayName = phrase.replacingTokens(using: { (tokenType) -> String in
240+
switch tokenType {
241+
case .wayName:
242+
return modifyValueByKey?(.wayName, name) ?? name
243+
case .code:
244+
return modifyValueByKey?(.code, ref) ?? ref
245+
default:
246+
fatalError("Unexpected token type \(tokenType) in name-and-ref phrase")
247+
}
248+
})
196249
} else if let ref = ref, isMotorway, let decimalRange = ref.rangeOfCharacter(from: .decimalDigits), !decimalRange.isEmpty {
197250
wayName = modifyValueByKey != nil ? "\(modifyValueByKey!(.code, ref))" : ref
198251
} else if name == nil, let ref = ref {
@@ -254,58 +307,27 @@ public class OSRMInstructionFormatter: Formatter {
254307
if step.finalHeading != nil { bearing = Int(step.finalHeading! as Double) }
255308

256309
// 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
310+
let result = instruction.replacingTokens { (tokenType) -> String in
311+
var replacement: String
312+
switch tokenType {
313+
case .code: replacement = step.codes?.first ?? ""
314+
case .wayName: replacement = wayName
315+
case .destination: replacement = destination
316+
case .exitCode: replacement = exitCode
317+
case .exitIndex: replacement = exitOrdinal
318+
case .rotaryName: replacement = rotaryName
319+
case .laneInstruction: replacement = laneInstruction ?? ""
320+
case .modifier: replacement = modifierConstant
321+
case .direction: replacement = directionFromDegree(degree: bearing)
322+
case .wayPoint: replacement = nthWaypoint ?? ""
323+
case .firstInstruction, .secondInstruction, .distance:
324+
fatalError("Unexpected token type \(tokenType) in individual instruction")
273325
}
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-
}
326+
if tokenType == .wayName {
327+
return replacement // already modified above
296328
} else {
297-
result += token! as String
329+
return modifyValueByKey?(tokenType, replacement) ?? replacement
298330
}
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
309331
}
310332

311333
return result

OSRMTextInstructions/TokenType.swift

Lines changed: 19 additions & 2 deletions
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,10 +14,15 @@ 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 {
20-
case "way_name":
25+
case "way_name", "name":
2126
type = .wayName
2227
case "destination":
2328
type = .destination
@@ -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: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,59 @@ 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 phrases = instructions.instructions["phrase"] as! [String: String]
28+
29+
var directoryContents: [URL] = []
30+
XCTAssertNoThrow(directoryContents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: []))
31+
for type in directoryContents {
32+
var typeDirectoryContents: [URL] = []
33+
XCTAssertNoThrow(typeDirectoryContents = try FileManager.default.contentsOfDirectory(at: type, includingPropertiesForKeys: nil, options: []))
34+
for fixture in typeDirectoryContents {
35+
if type.lastPathComponent == "phrase" {
36+
var rawJSON = Data()
37+
XCTAssertNoThrow(rawJSON = try Data(contentsOf: fixture, options: []))
38+
39+
var json: [String: Any] = [:]
40+
XCTAssertNoThrow(json = try JSONSerialization.jsonObject(with: rawJSON, options: []) as! [String: Any])
41+
42+
let phrase = fixture.deletingPathExtension().lastPathComponent.replacingOccurrences(of: "_", with: " ")
43+
let fixtureOptions = json["options"] as! [String: String]
44+
45+
let expectedValue = (json["phrases"] as! [String: String])["en"]
46+
let actualValue = phrases[phrase]?.replacingTokens(using: { (tokenType) -> String in
47+
var replacement: String?
48+
switch tokenType {
49+
case .firstInstruction:
50+
replacement = fixtureOptions["instruction_one"]
51+
case .secondInstruction:
52+
replacement = fixtureOptions["instruction_two"]
53+
case .distance:
54+
replacement = fixtureOptions["distance"]
55+
default:
56+
XCTFail("Unexpected token type \(tokenType) in phrase \(phrase)")
57+
}
58+
XCTAssertNotNil(replacement, "Missing fixture option for \(tokenType)")
59+
return replacement ?? ""
60+
})
61+
XCTAssertEqual(expectedValue, actualValue, fixture.path)
62+
} else {
3263
// parse fixture
33-
guard let json = getFixture(url: fixture) else {
34-
XCTAssert(false, "invalid json")
35-
return
36-
}
64+
let json = getFixture(url: fixture)
3765
let options = json["options"] as? [String: Any]
38-
66+
3967
let step = RouteStep(json: json["step"] as! [String: Any])
4068

4169
var roadClasses: RoadClasses? = nil
4270
if let classes = options?["classes"] as? [String] {
4371
roadClasses = RoadClasses(descriptions: classes)
4472
}
45-
73+
4674
// compile instruction
4775
let instruction = instructions.string(for: step, legIndex: options?["legIndex"] as? Int, numberOfLegs: options?["legCount"] as? Int, roadClasses: roadClasses, modifyValueByKey: nil)
48-
76+
4977
// check generated instruction against fixture
5078
XCTAssertEqual(
5179
json["instruction"] as? String,
@@ -54,35 +82,23 @@ class OSRMTextInstructionsTests: XCTestCase {
5482
)
5583
}
5684
}
57-
} catch let error as NSError {
58-
print(error.localizedDescription)
59-
XCTAssert(false)
6085
}
6186
}
6287

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

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-
}
92+
var json: [String: Any] = [:]
93+
XCTAssertNoThrow(json = try JSONSerialization.jsonObject(with: rawJSON, options: []) as! [String: Any])
7894

7995
// provide default values for properties that RouteStep
8096
// needs, but that our not in the fixtures
81-
let fixture: NSMutableDictionary = [:]
82-
let maneuver: NSMutableDictionary = [
97+
var fixture: [String: Any] = [:]
98+
var maneuver: [String: Any] = [
8399
"location": [ 1.0, 1.0 ]
84100
]
85-
let step: NSMutableDictionary = [
101+
var step: [String: Any] = [
86102
"mode": "driving"
87103
]
88104

0 commit comments

Comments
 (0)