m323/Sources/MiniProject/RunnerCore.swift
cediackermann 13876d51c2
test(MiniProject): extract RunnerCore library with Swift Testing suite
Split UI-free logic into a RunnerCore swift_library and cover it with a
swift_test suite using the Swift Testing library (import Testing / @Test /
#expect), matching Tests/m323Tests. Runs via `bazel test`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 08:47:33 +02:00

135 lines
3.9 KiB
Swift

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<AgeGroup>,
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<AgeGroup>, _ group: AgeGroup) -> Set<AgeGroup> {
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
}