// // PlayerRegistration.swift // Padel Tournament // // Created by razmig on 10/03/2024. // import Foundation import LeStorage @Observable final class PlayerRegistration: BasePlayerRegistration, SideStorable { func localizedSourceLabel() -> String { switch source { case .frenchFederation: return "base fédérale" case .beachPadel: return "beach-padel" case nil: if registeredOnline { return "à vérifier vous-même" } else { return "créé par vous-même" } } } init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, paymentType: PlayerPaymentType? = nil, sex: PlayerSexType? = nil, tournamentPlayed: Int? = nil, points: Double? = nil, clubName: String? = nil, ligueName: String? = nil, assimilation: String? = nil, phoneNumber: String? = nil, email: String? = nil, birthdate: String? = nil, computedRank: Int = 0, source: PlayerRegistration.PlayerDataSource? = nil, hasArrived: Bool = false) { super.init() self.teamRegistration = teamRegistration self.firstName = firstName self.lastName = lastName self.licenceId = licenceId self.rank = rank self.paymentType = paymentType self.sex = sex self.tournamentPlayed = tournamentPlayed self.points = points self.clubName = clubName self.ligueName = ligueName self.assimilation = assimilation self.phoneNumber = phoneNumber self.email = email self.birthdate = birthdate self.computedRank = computedRank self.source = source self.hasArrived = hasArrived } internal init(importedPlayer: ImportedPlayer) { super.init() self.teamRegistration = "" self.firstName = (importedPlayer.firstName ?? "").prefixTrimmed(50).capitalized self.lastName = (importedPlayer.lastName ?? "").prefixTrimmed(50).uppercased() self.licenceId = importedPlayer.license?.prefixTrimmed(50) ?? nil self.rank = Int(importedPlayer.rank) self.sex = importedPlayer.male ? .male : .female self.tournamentPlayed = importedPlayer.tournamentPlayed self.points = importedPlayer.getPoints() self.clubName = importedPlayer.clubName?.prefixTrimmed(200) self.ligueName = importedPlayer.ligueName?.prefixTrimmed(200) self.assimilation = importedPlayer.assimilation?.prefixTrimmed(50) self.source = .frenchFederation self.birthdate = importedPlayer.birthYear?.prefixTrimmed(50) } internal init?(federalData: [String], sex: Int, sexUnknown: Bool) { super.init() let _lastName = federalData[0].trimmed.uppercased() let _firstName = federalData[1].trimmed.capitalized if _lastName.isEmpty && _firstName.isEmpty { return nil } lastName = _lastName.prefixTrimmed(50) firstName = _firstName.prefixTrimmed(50) birthdate = federalData[2].formattedAsBirthdate().prefixTrimmed(50) licenceId = federalData[3].prefixTrimmed(50) clubName = federalData[4].prefixTrimmed(200) let stringRank = federalData[5] if stringRank.isEmpty { rank = nil } else { rank = Int(stringRank) } let _email = federalData[6] if _email.isEmpty == false { self.email = _email.prefixTrimmed(50) } let _phoneNumber = federalData[7] if _phoneNumber.isEmpty == false { self.phoneNumber = _phoneNumber.prefixTrimmed(50) } source = .beachPadel if sexUnknown { if sex == 1 && FileImportManager.shared.foundInWomenData(license: federalData[3]) { self.sex = .female } else if FileImportManager.shared.foundInMenData(license: federalData[3]) { self.sex = .male } else { self.sex = nil } } else { self.sex = PlayerSexType(rawValue: sex) } } required init(from decoder: any Decoder) throws { try super.init(from: decoder) } var tournamentStore: TournamentStore? { guard let storeId else { fatalError("missing store id for \(String(describing: type(of: self)))") } return TournamentLibrary.shared.store(tournamentId: storeId) // if let store = self.store as? TournamentStore { // return store // } } var computedAge: Int? { if let birthdate { let components = birthdate.components(separatedBy: "/") if let age = components.last, let ageInt = Int(age) { let year = Calendar.current.getSportAge() if age.count == 2 { //si l'année est sur 2 chiffres dans le fichier if ageInt < 23 { return year - 2000 - ageInt } else { return year - 2000 + 100 - ageInt } } else { //si l'année est représenté sur 4 chiffres return year - ageInt } } } return nil } func pasteData(_ exportFormat: ExportFormat = .rawText) -> String { switch exportFormat { case .rawText: return [firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: exportFormat.separator()) case .csv: return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator()) } } func isPlaying() -> Bool { team()?.isPlaying() == true } func contains(_ searchField: String) -> Bool { let nameComponents = searchField.canonicalVersion.split(separator: " ") if nameComponents.count > 1 { let pairs = nameComponents.pairs() return pairs.contains(where: { (firstName.canonicalVersion.localizedCaseInsensitiveContains(String($0)) && lastName.canonicalVersion.localizedCaseInsensitiveContains(String($1))) || (firstName.canonicalVersion.localizedCaseInsensitiveContains(String($1)) && lastName.canonicalVersion.localizedCaseInsensitiveContains(String($0))) }) } else { return nameComponents.contains { component in firstName.canonicalVersion.localizedCaseInsensitiveContains(component) || lastName.canonicalVersion.localizedCaseInsensitiveContains(component) } } } func isSameAs(_ player: PlayerRegistration) -> Bool { firstName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.firstName.trimmedMultiline.canonicalVersion) == .orderedSame && lastName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.lastName.trimmedMultiline.canonicalVersion) == .orderedSame } func tournament() -> Tournament? { guard let tournament = team()?.tournament else { return nil } return Store.main.findById(tournament) } func team() -> TeamRegistration? { guard let teamRegistration else { return nil } return self.tournamentStore?.teamRegistrations.findById(teamRegistration) } func isHere() -> Bool { hasArrived } func hasPaid() -> Bool { paymentType != nil } func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch displayStyle { case .wide, .title: return lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized case .short: let names = lastName.components(separatedBy: .whitespaces) if lastName.components(separatedBy: .whitespaces).count > 1 { if let firstLongWord = names.first(where: { $0.count > 3 }) { return firstLongWord.trimmed.capitalized.trunc(length: 10) + " " + firstName.trimmed.prefix(1).capitalized + "." } } return lastName.trimmed.capitalized.trunc(length: 10) + " " + firstName.trimmed.prefix(1).capitalized + "." } } func isImported() -> Bool { source == .beachPadel } func unrankedOrUnknown() -> Bool { source == nil } func isValidLicenseNumber(year: Int) -> Bool { guard let licenceId else { return false } guard licenceId.isLicenseNumber else { return false } guard licenceId.suffix(6) == "(\(year))" else { return false } return true } @objc var canonicalName: String { playerLabel().folding(options: .diacriticInsensitive, locale: .current).lowercased() } func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String { if let rank, rank > 0 { if rank != computedRank { return computedRank.formatted() + " (" + rank.formatted() + ")" } else { return rank.formatted() } } else { return "non classé" + (isMalePlayer() ? "" : "e") } } func updateRank(from sources: [CSVParser], lastRank: Int?) async throws { #if DEBUG_TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if let dataFound = try await history(from: sources) { rank = dataFound.rankValue?.toInt() points = dataFound.points tournamentPlayed = dataFound.tournamentCountValue?.toInt() } else if let dataFound = try await historyFromName(from: sources) { rank = dataFound.rankValue?.toInt() points = dataFound.points tournamentPlayed = dataFound.tournamentCountValue?.toInt() } else { rank = lastRank } } func history(from sources: [CSVParser]) async throws -> Line? { #if DEBUG_TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func history()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif guard let license = licenceId?.strippedLicense else { return nil // Do NOT call historyFromName here, let updateRank handle it } let filteredSources = sources.filter { $0.maleData == isMalePlayer() } return await withTaskGroup(of: Line?.self) { group in for source in filteredSources { group.addTask { guard !Task.isCancelled else { return nil } return try? await source.first { $0.rawValue.contains(";\(license);") } } } for await result in group { if let result { group.cancelAll() // Stop other tasks as soon as we find a match return result } } return nil } } func historyFromName(from sources: [CSVParser]) async throws -> Line? { #if DEBUG let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func historyFromName()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif let filteredSources = sources.filter { $0.maleData == isMalePlayer() } let normalizedLastName = lastName.canonicalVersionWithPunctuation let normalizedFirstName = firstName.canonicalVersionWithPunctuation return await withTaskGroup(of: Line?.self) { group in for source in filteredSources { group.addTask { guard !Task.isCancelled else { print("Cancelled"); return nil } return try? await source.first { let lineValue = $0.rawValue.canonicalVersionWithPunctuation return lineValue.contains(";\(normalizedLastName);\(normalizedFirstName);") } } } for await result in group { if let result { group.cancelAll() // Stop other tasks as soon as we find a match return result } } return nil } } func setComputedRank(in tournament: Tournament) { let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 90_000 switch tournament.tournamentCategory { case .men: computedRank = isMalePlayer() ? currentRank : currentRank + PlayerRegistration.addon(for: currentRank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0) default: computedRank = currentRank } } func isMalePlayer() -> Bool { sex == .male } func validateLicenceId(_ year: Int) { if let currentLicenceId = licenceId { if currentLicenceId.trimmed.hasSuffix("(\(year-1))") { self.licenceId = currentLicenceId.replacingOccurrences(of: "\(year-1)", with: "\(year)") } else if let computedLicense = currentLicenceId.strippedLicense?.computedLicense { self.licenceId = computedLicense + " (\(year))" } } } func hasHomonym() -> Bool { let federalContext = PersistenceController.shared.localContainer.viewContext let fetchRequest = ImportedPlayer.fetchRequest() let predicate = NSPredicate(format: "firstName == %@ && lastName == %@", firstName, lastName) fetchRequest.predicate = predicate do { let count = try federalContext.count(for: fetchRequest) return count > 1 } catch { } return false } enum PlayerDataSource: Int, Codable { case frenchFederation = 0 case beachPadel = 1 } static func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int { switch playerRank { case 0: return 0 case womanMax: return manMax - womanMax case manMax: return 0 default: return TournamentCategory.femaleInMaleAssimilationAddition(playerRank) } } func insertOnServer() { self.tournamentStore?.playerRegistrations.writeChangeAndInsertOnServer(instance: self) } } extension PlayerRegistration: PlayerHolder { func getAssimilatedAsMaleRank() -> Int? { nil } func getFirstName() -> String { firstName } func getLastName() -> String { lastName } func getPoints() -> Double? { self.points } func getRank() -> Int? { rank } func isUnranked() -> Bool { rank == nil } func formattedRank() -> String { self.rankLabel() } func formattedLicense() -> String { if let licenceId { return licenceId.computedLicense } return "aucune licence" } var male: Bool { isMalePlayer() } func getBirthYear() -> Int? { nil } func getProgression() -> Int { 0 } func getComputedRank() -> Int? { computedRank } } enum PlayerDataSource: Int, Codable { case frenchFederation = 0 case beachPadel = 1 } enum PlayerSexType: Int, Hashable, CaseIterable, Identifiable, Codable { init?(rawValue: Int?) { guard let value = rawValue else { return nil } self.init(rawValue: value) } var id: Self { self } case female = 0 case male = 1 }