-
Notifications
You must be signed in to change notification settings - Fork 3
/
XScheduleParser.swift
364 lines (303 loc) · 14.9 KB
/
XScheduleParser.swift
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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
155
156
157
158
159
160
161
162
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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
//
// XScheduleParser.swift
// X Schedule
//
// Created by Nicholas Reichert on 2/18/15.
// Copyright (c) 2015 Nicholas Reichert.
//
import Foundation
open class XScheduleParser: ScheduleParser {
static let manualNotificationTriggerKeyword = "<!--send_notification-->"
open override class func storeScheduleInString(_ schedule: Schedule) -> String {
var output: String = ""
output += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
output += "<schedule>\n"
output += "<summary>\(schedule.title)</summary>\n"
output += "<description><p>"
output += "\(stringForItemsArray(schedule.items))"
output += "</p></description>\n"
output += "\(manualNotificationTrigger(schedule))\n"
output += "</schedule>"
return output
}
private class func stringForItemsArray(_ items: [ScheduleItem]) -> String {
var output: String = ""
for item in items {
if let spanItem = item as? TimeSpanScheduleItem {
output += "\(spanItem.blockName) \(timeStringForDate(spanItem.startTime as Date?))-\(timeStringForDate(spanItem.endTime as Date?))"
} else if let spanItem = item as? TimePointScheduleItem {
output += "\(spanItem.blockName) \(timeStringForDate(spanItem.time as Date?))"
} else if let spanItem = item as? DescriptionScheduleItem {
output += "\(spanItem.blockName)"
}
// Always finish each item with a line break tag <br>.
output += "<br>"
}
return output
}
private class func timeStringForDate(_ date: Date?) -> String {
var output: String = ""
let dateFormatter: DateFormatter = setUpParsingDateFormatter()
if (date != nil) {
output = dateFormatter.string(from: date!)
} else {
output = "?:??"
}
return output
}
private class func manualNotificationTrigger(_ schedule: Schedule) -> String {
if (schedule.manuallyMarkedUnusual) {
return manualNotificationTriggerKeyword
} else {
return ""
}
}
open override class func parseForSchedule(_ string: String, date: Date) -> Schedule {
let schedule = Schedule()
//Parse XML from inputted string.
let delegate: XScheduleXMLParser = parsedXMLElements(string)
storeManualNotificationTrigger(string, inSchedule: schedule)
storeTitleString(delegate.titleString, inSchedule: schedule)
storeDate(date, inSchedule: schedule)
storeScheduleBody(delegate.descriptionString, inSchedule: schedule)
removeCodeScheduleItems(inSchedule: schedule)
//Return finished schedule.
return schedule
}
private class func parsedXMLElements(_ string: String) -> XScheduleXMLParser {
//Returns the parsed XML.
let stringData = string.data(using: String.Encoding.utf8)
let xmlParser = XMLParser(data: stringData!)
let xmlParserDelegate: XMLParserDelegate = XScheduleXMLParser()
xmlParser.delegate = xmlParserDelegate
xmlParser.parse()
return xmlParser.delegate as! XScheduleXMLParser
}
private class func removeCodeScheduleItems(inSchedule schedule: Schedule) {
// Removes all elements that contain the manualNotificationTriggerKeyword.
schedule.items = schedule.items.filter( { !$0.primaryText().contains(manualNotificationTriggerKeyword) } )
}
private class func storeManualNotificationTrigger(_ string: String, inSchedule schedule: Schedule) {
if (string.contains(manualNotificationTriggerKeyword)) {
schedule.manuallyMarkedUnusual = true
}
}
private class func storeDate(_ date: Date, inSchedule schedule: Schedule) {
schedule.date = date
}
private class func storeTitleString(_ string: String, inSchedule schedule: Schedule) {
var titleString: String = string
trimWhitespaceFrom(&titleString)
schedule.title = titleString
}
private class func storeScheduleBody(_ scheduleDescription: String, inSchedule schedule: Schedule) {
var scheduleString: String = scheduleDescription
cleanUpDescriptionString(&scheduleString)
//Split string up by newlines.
let lines: [String] = separateLines(scheduleString)
for line in lines {
//Split each line into tokens.
var tokens: [String] = separateLineIntoTokens(line)
//Identify index of time token.
let singleTimeTokenIndex: Int? = indexOfSingleTimeTokenInArray(tokens)
let doubleTimeTokenIndex: Int? = indexOfDoubleTimeTokenInArray(tokens)
if (singleTimeTokenIndex == nil && doubleTimeTokenIndex == nil) {
//Only add a timeless ScheduleItem if it has a description.
if (tokens != []) {
//Make schedule item and add to schedule.
let item: DescriptionScheduleItem = DescriptionScheduleItem(blockName: stringFromTokens(tokens))
schedule.items.append(item)
}
} else if ( doubleTimeTokenIndex != nil) {
//Analyze time token.
let times: (start: Date?, end: Date?) = parseTokensForDoubleTimes(&tokens, index: doubleTimeTokenIndex!, onDate: schedule.date as Date)
//Make schedule item and add to schedule.
let item: TimeSpanScheduleItem = TimeSpanScheduleItem(blockName: stringFromTokens(tokens), startTime: times.start, endTime: times.end)
schedule.items.append(item)
} else { // singleTimeTokenIndex != nil
//Analyze time token.
let time: Date? = parseTokensForSingleTime(&tokens, index: singleTimeTokenIndex!, onDate: schedule.date as Date)
//Make schedule item and add to schedule.
let item: TimePointScheduleItem = TimePointScheduleItem(blockName: stringFromTokens(tokens), time: time)
schedule.items.append(item)
}
}
}
private class func cleanUpDescriptionString(_ scheduleString: inout String) {
trimWhitespaceFrom(&scheduleString)
removeTags(&scheduleString, tag: "p")
removeTags(&scheduleString, tag: "div")
replaceBRTagsWithNewlines(&scheduleString)
trimWhitespaceFrom(&scheduleString)
}
private class func trimWhitespaceFrom(_ string: inout String) {
string = string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}
private class func removeTags(_ string: inout String, tag: String) {
//Find tags.
let tagRangeStart = string.range(of: "<\(tag)>")
let tagRangeEnd = string.range(of: "</\(tag)>")
//Remove tag tags.
if ((tagRangeStart) != nil && (tagRangeEnd) != nil) {
let noTagRange = (tagRangeStart!.upperBound)..<(tagRangeEnd!.lowerBound)
string = String(string[noTagRange])
}
}
private class func replaceBRTagsWithNewlines(_ string: inout String) {
string = string.replacingOccurrences(of: "<br>", with: "\n")
string = string.replacingOccurrences(of: "<br />", with: "\n")
}
private class func separateLines(_ string: String) -> [String] {
let lines: [String] = string.components(separatedBy: "\n")
return lines
}
private class func separateLineIntoTokens(_ string: String) -> [String] {
//If string is empty, return empty output array. Else, separate the string by spaces.
var tokens: [String]
if (string == "") {
tokens = []
} else {
tokens = string.components(separatedBy: " ")
}
return tokens
}
private class func indexOfDoubleTimeTokenInArray(_ tokens: [String]) -> Int? {
//Search through array for time token and return it's id.
return tokens.firstIndex(where: { isStringDoubleTimeToken($0) } )
}
private class func isStringDoubleTimeToken(_ string: String) -> Bool {
let hasNums: Bool = string.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil
let hasQuestionMark: Bool = string.range(of: "?") != nil
let hasDash: Bool = string.range(of: "-") != nil
let hasColon: Bool = string.range(of: ":") != nil
let isTimeToken: Bool = (hasQuestionMark || hasNums) && hasDash && hasColon
return isTimeToken
}
private class func indexOfSingleTimeTokenInArray(_ tokens: [String]) -> Int? {
//Search through array for time token and return it's id.
return tokens.firstIndex(where: { isStringSingleTimeToken($0) } )
}
private class func isStringSingleTimeToken(_ string: String) -> Bool {
let hasNums: Bool = string.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil
let hasQuestionMark: Bool = string.range(of: "?") != nil
let hasDash: Bool = string.range(of: "-") != nil
let hasColon: Bool = string.range(of: ":") != nil
let isTimeToken: Bool = (hasQuestionMark || hasNums) && !hasDash && hasColon
return isTimeToken
}
private class func analyzeDoubleTimeToken(_ timeToken: String) -> (Date?, Date?) {
//Analyze time token.
let times: (start: String, end: String) = splitDoubleTimeToken(timeToken)
let dateFormatter: DateFormatter = setUpParsingDateFormatter()
let startTime: Date? = dateFormatter.date(from: times.start)
let endTime: Date? = dateFormatter.date(from: times.end)
return (startTime, endTime)
}
private class func analyzeSingleTimeToken(_ timeToken: String) -> Date? {
//Analyze time token.
let dateFormatter: DateFormatter = setUpParsingDateFormatter()
let time: Date? = dateFormatter.date(from: timeToken)
return time
}
private class func stringFromTokens(_ tokensArray: [String]) -> String {
//Join tokens delimited by spaces and set as desription of ScheduleItem.
let itemDescription: String = tokensArray.joined(separator: " ")
let trimmedDescription: String = itemDescription.trimmingCharacters(in: .whitespaces)
return trimmedDescription
}
private class func removeArrayItemsAfterIndex<T>(_ index: Int, array: inout [T]) {
array.removeSubrange(index+1..<(array.count))
}
private class func setUpParsingDateFormatter() -> DateFormatter {
let dateFormatter: DateFormatter = DateFormatter()
dateFormatter.dateFormat = "h:mm"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
return dateFormatter
}
private class func splitDoubleTimeToken(_ string: String) -> (String, String) {
let array: [String] = string.components(separatedBy: "-")
let tuple: (String, String) = (array.first!, array.last!)
return tuple
}
private class func combineTimeAndDate(time: Date?, date: Date) -> Date? {
var combined: Date?
if (time != nil) {
let dateComponents: DateComponents = (Calendar.current as NSCalendar).components( [.day, .month, .year, .era], from: date)
var timeComponents: DateComponents = (Calendar.current as NSCalendar).components( [.hour, .minute], from: time!)
timeComponents.day = dateComponents.day
timeComponents.month = dateComponents.month
timeComponents.year = dateComponents.year
timeComponents.era = dateComponents.era
combined = Calendar.current.date(from: timeComponents)
} else {
combined = nil
}
return combined
}
private class func assignAMPM(_ date: inout Date?) {
//Hours 12-5 are PM. Hours 6-11 are AM.
if (date != nil) {
let dateComponents: DateComponents = (Calendar.current as NSCalendar).components( .hour, from: date!)
if (dateComponents.hour!==12 || dateComponents.hour!<5) {
date = date!.addingTimeInterval(60*60*12)
}
}
}
private class func addDateInfoToTime(_ time: inout Date?, onDate scheduleDate: Date) {
time = combineTimeAndDate(time: time, date: scheduleDate)
assignAMPM(&time)
}
// Search through a tokens array with a double time token and extract the start and end times from it.
private class func parseTokensForDoubleTimes(_ tokens: inout [String], index timeTokenIndex: Int, onDate date: Date) -> (Date?, Date?) {
//Throw out any tokens after time token.
removeArrayItemsAfterIndex(timeTokenIndex, array: &tokens)
//Remove time token and transfer to string.
let timeToken: String = tokens.remove(at: timeTokenIndex)
//Analyze time token.
var times: (start: Date?, end: Date?) = analyzeDoubleTimeToken(timeToken)
//Put time tokens on the schedule date.
addDateInfoToTime(×.start, onDate: date)
addDateInfoToTime(×.end, onDate: date)
return times
}
// Search through a tokens array with a single time token and extract the time from it.
private class func parseTokensForSingleTime(_ tokens: inout [String], index timeTokenIndex: Int, onDate date: Date) -> Date? {
//Throw out any tokens after time token.
removeArrayItemsAfterIndex(timeTokenIndex, array: &tokens)
//Remove time token and transfer to string.
let timeToken: String = tokens.remove(at: timeTokenIndex)
//Analyze time token.
var time: Date? = analyzeSingleTimeToken(timeToken)
//Put time tokens on the schedule date.
addDateInfoToTime(&time, onDate: date)
return time
}
}
class XScheduleXMLParser: NSObject, XMLParserDelegate {
var descriptionString = ""
var titleString = ""
private var currentElement = ""
private var currentlyInDescription: Bool = false
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
currentElement = elementName
if (elementName == "description") {
currentlyInDescription = true
}
}
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
if (elementName == "description") {
currentlyInDescription = false
}
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
switch currentElement {
case "summary":
titleString += string
default:
break;
}
if (currentlyInDescription) {
descriptionString += string
}
}
}