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.
 
 
PadelClub/PadelClub/Data/PlayerRegistration.swift

464 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)
}
required public init() {
super.init()
}
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
}