import Foundation import SwiftTUI // MARK: - Models enum AgeGroup: Int, CustomStringConvertible, CaseIterable { case junior = 1 case adult = 2 case senior = 3 var description: String { switch self { case .junior: return "Junior" case .adult: return "Adult" case .senior: return "Senior" } } } struct Runner { let name: String let ageGroup: AgeGroup let time: String let number: Int } enum SortKey: String, CaseIterable { case name case ageGroup case time case number 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 func readFile(_ path: String) -> String { (try? String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8)) ?? "" } // MARK: - Parsing 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 }) } 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]) } } 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) } } 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 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) } func toggling(_ groups: Set, _ group: AgeGroup) -> Set { groups.contains(group) ? groups.subtracting([group]) : groups.union([group]) } // MARK: - Formatting func menuLabel(_ text: String, on: Bool) -> String { on ? "[\(text)]" : " \(text) " } 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 } // MARK: - Configuration let resultsURL = "/Users/cediackermann/Projects/m323/Sources/MiniProject/data/results.txt" let startListURL = "/Users/cediackermann/Projects/m323/Sources/MiniProject/data/startList.txt" // MARK: - Entry Point let results = parseResults(readFile(resultsURL), separator: "\t") let starters = parseStarters(readFile(startListURL), separator: "\t") let finished = assembleRunners(starters: starters, results: results) let absent = nonParticipants(starters: starters, results: results) Application(rootView: RunnerBrowser(finished: finished, absent: absent)).start() // MARK: - TUI struct RunnerBrowser: View { let finished: [Runner] let absent: [Runner] @State private var query: String = "" @State private var sortKey: SortKey = .number @State private var includedGroups: Set = Set(AgeGroup.allCases) @State private var showAbsent: Bool = false private var source: [Runner] { showAbsent ? absent : finished } private var visible: [Runner] { visibleRunners(source, query: query, includedGroups: includedGroups, sortKey: sortKey) } var body: some View { VStack(alignment: .leading) { Text("Stadt-Rundlauf - \(visible.count)/\(source.count) runners") .bold() HStack { Text("Show:") Button(menuLabel("Finished", on: !showAbsent)) { showAbsent = false } Button(menuLabel("Did not start", on: showAbsent)) { showAbsent = true } } HStack { Text("Sort:") ForEach(SortKey.allCases, id: \.rawValue) { key in Button(menuLabel(key.rawValue, on: key == sortKey)) { sortKey = key } } } HStack { Text("Groups:") ForEach(AgeGroup.allCases, id: \.rawValue) { group in Button(menuLabel("\(group)", on: includedGroups.contains(group))) { includedGroups = toggling(includedGroups, group) } } } TextField(placeholder: "Search by name…") { value in query = value } Divider() Text(formatRow("Nr", "Name", "Group", "Time")).bold() ScrollView { ForEach(visible, id: \.number) { runner in Text(formatRow("#\(runner.number)", runner.name, "\(runner.ageGroup)", runner.time)) } } } .padding() } }