import Foundation // MARK: - Models public enum AgeGroup: Int, CustomStringConvertible, CaseIterable { case junior = 1 case adult = 2 case senior = 3 public var description: String { switch self { case .junior: return "Junior" case .adult: return "Adult" case .senior: return "Senior" } } } public struct Runner: Equatable { public let name: String public let ageGroup: AgeGroup public let time: String public let number: Int public init(name: String, ageGroup: AgeGroup, time: String, number: Int) { self.name = name self.ageGroup = ageGroup self.time = time self.number = number } } public enum SortKey: String, CaseIterable { case name case ageGroup case time case number public func sort(_ lhs: Runner, _ rhs: Runner) -> Bool { switch self { case .name: return lhs.name < rhs.name case .ageGroup: return lhs.ageGroup.rawValue < rhs.ageGroup.rawValue case .time: return lhs.time < rhs.time case .number: return lhs.number < rhs.number } } } // MARK: - IO public func readFile(_ path: String) -> String { (try? String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8)) ?? "" } // MARK: - Parsing public func parseResults(_ content: String, separator: String) -> [Int: String] { let pairs = content .components(separatedBy: .newlines) .compactMap { line -> (Int, String)? in let cols = line.components(separatedBy: separator) guard cols.count >= 2, let key = Int(cols[0]) else { return nil } return (key, cols[1]) } return Dictionary(pairs, uniquingKeysWith: { _, last in last }) } public func parseStarters(_ content: String, separator: String) -> [(nr: Int, ageGroup: Int, name: String)] { content .components(separatedBy: .newlines) .compactMap { line in let cols = line.components(separatedBy: separator) guard cols.count >= 3, let nr = Int(cols[0]), let ageGroup = Int(cols[1]) else { return nil } return (nr: nr, ageGroup: ageGroup, name: cols[2]) } } public func assembleRunners( starters: [(nr: Int, ageGroup: Int, name: String)], results: [Int: String] ) -> [Runner] { starters.compactMap { starter in guard let time = results[starter.nr], let group = AgeGroup(rawValue: starter.ageGroup) else { return nil } return Runner(name: starter.name, ageGroup: group, time: time, number: starter.nr) } } public func nonParticipants( starters: [(nr: Int, ageGroup: Int, name: String)], results: [Int: String] ) -> [Runner] { starters.compactMap { starter in guard results[starter.nr] == nil, let group = AgeGroup(rawValue: starter.ageGroup) else { return nil } return Runner(name: starter.name, ageGroup: group, time: "DNS", number: starter.nr) } } // MARK: - Transforms public func visibleRunners( _ runners: [Runner], query: String, includedGroups: Set, sortKey: SortKey ) -> [Runner] { let needle = query.lowercased() return runners .filter { includedGroups.contains($0.ageGroup) } .filter { needle.isEmpty || $0.name.lowercased().contains(needle) } .sorted(by: sortKey.sort) } public func toggling(_ groups: Set, _ group: AgeGroup) -> Set { groups.contains(group) ? groups.subtracting([group]) : groups.union([group]) } // MARK: - Formatting public func menuLabel(_ text: String, on: Bool) -> String { on ? "[\(text)]" : " \(text) " } public func formatRow(_ nr: String, _ name: String, _ group: String, _ time: String) -> String { nr.padding(toLength: 6, withPad: " ", startingAt: 0) + name.padding(toLength: 22, withPad: " ", startingAt: 0) + group.padding(toLength: 9, withPad: " ", startingAt: 0) + time }