You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
460 lines
16 KiB
460 lines
16 KiB
//
|
|
// 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
|
|
}
|
|
|