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.
516 lines
23 KiB
516 lines
23 KiB
//
|
|
// FileImportManager.swift
|
|
// PadelClub
|
|
//
|
|
// Created by Razmig Sarkissian on 01/03/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import LeStorage
|
|
import SwiftUI
|
|
|
|
enum FileImportManagerError: LocalizedError {
|
|
case unknownFormat
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .unknownFormat:
|
|
return "Format non reconnu"
|
|
}
|
|
}
|
|
}
|
|
|
|
@Observable
|
|
class ImportObserver {
|
|
var checkingFilesAttempt: Int = 0
|
|
var checkingFiles: Bool = false
|
|
var willCheckDataIntegrity: Bool = false
|
|
|
|
func currentlyImportingLabel() -> String {
|
|
guard let currentImportDate else { return "import en cours" }
|
|
if URL.importDateFormatter.string(from: currentImportDate) == "07-2024" {
|
|
return "consolidation des données"
|
|
}
|
|
return "import " + currentImportDate.monthYearFormatted
|
|
}
|
|
|
|
var currentImportDate: Date? = nil
|
|
|
|
func isImportingFile() -> Bool {
|
|
currentImportDate != nil
|
|
}
|
|
}
|
|
|
|
class FileImportManager {
|
|
static let shared = FileImportManager()
|
|
|
|
|
|
func updatePlayers(isMale: Bool, players: inout [FederalPlayer]) {
|
|
let replacements: [(Character, Character)] = [("Á", "ç"), ("‡", "à"), ("Ù", "ô"), ("Ë", "è"), ("Ó", "î"), ("Î", "ë"), ("…", "É"), ("Ô", "ï"), ("È", "é"), ("«", "Ç"), ("»", "È")]
|
|
|
|
var playersLeft = players
|
|
SourceFileManager.shared.allFilesSortedByDate(isMale).forEach({ url in
|
|
if playersLeft.isEmpty == false {
|
|
let federalPlayers = readCSV(inputFile: url)
|
|
let replacementsCharacters = url.dateFromPath.monthYearFormatted != "04-2024" ? [] : replacements
|
|
|
|
playersLeft.forEach { importedPlayer in
|
|
if let federalPlayer = federalPlayers.first(where: { $0.license == importedPlayer.license }) {
|
|
var lastName = federalPlayer.lastName
|
|
lastName.replace(characters: replacementsCharacters)
|
|
var firstName = federalPlayer.firstName
|
|
firstName.replace(characters: replacementsCharacters)
|
|
importedPlayer.lastName = lastName.trimmed.uppercased()
|
|
importedPlayer.firstName = firstName.trimmed.capitalized
|
|
}
|
|
}
|
|
playersLeft.removeAll(where: { $0.lastName.isEmpty == false })
|
|
}
|
|
})
|
|
}
|
|
|
|
func foundInWomenData(license: String?) -> Bool {
|
|
guard let license = license?.strippedLicense else {
|
|
return false
|
|
}
|
|
do {
|
|
return try SourceFileManager.shared.allFilesSortedByDate(false).first(where: {
|
|
let fileContent = try String(contentsOf: $0)
|
|
return fileContent.contains(";\(license);")
|
|
}) != nil
|
|
} catch {
|
|
print("history", error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
func foundInMenData(license: String?) -> Bool {
|
|
guard let license = license?.strippedLicense else {
|
|
return false
|
|
}
|
|
do {
|
|
return try SourceFileManager.shared.allFilesSortedByDate(true).first(where: {
|
|
let fileContent = try String(contentsOf: $0)
|
|
return fileContent.contains(";\(license);")
|
|
}) != nil
|
|
} catch {
|
|
print("history", error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
enum FileProvider: CaseIterable, Identifiable {
|
|
var id: Self { self }
|
|
|
|
case frenchFederation
|
|
case padelClub
|
|
case custom
|
|
case customAutoSearch
|
|
|
|
var localizedLabel: String {
|
|
switch self {
|
|
case .padelClub:
|
|
return "Padel Club"
|
|
case .frenchFederation:
|
|
return "FFT"
|
|
case .custom:
|
|
return "Personnalisé"
|
|
case .customAutoSearch:
|
|
return "Personnalisé (avec recherche)"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct TeamHolder: Identifiable {
|
|
let id: UUID = UUID()
|
|
let players: Set<PlayerRegistration>
|
|
let weight: Int
|
|
let tournamentCategory: TournamentCategory
|
|
let tournamentAgeCategory: FederalTournamentAge
|
|
let previousTeam: TeamRegistration?
|
|
var registrationDate: Date? = nil
|
|
var name: String? = nil
|
|
|
|
init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, tournamentAgeCategory: FederalTournamentAge, previousTeam: TeamRegistration?, registrationDate: Date? = nil, name: String? = nil, tournament: Tournament) {
|
|
self.players = Set(players)
|
|
self.tournamentCategory = tournamentCategory
|
|
self.tournamentAgeCategory = tournamentAgeCategory
|
|
self.name = name
|
|
self.previousTeam = previousTeam
|
|
if players.count < 2 {
|
|
let s = players.compactMap { $0.sex?.rawValue }
|
|
var missing = tournamentCategory.mandatoryPlayerType()
|
|
s.forEach { i in
|
|
if let index = missing.firstIndex(of: i) {
|
|
missing.remove(at: index)
|
|
}
|
|
}
|
|
let significantPlayerCount = 2
|
|
let pl = players.prefix(significantPlayerCount).map { $0.computedRank }
|
|
let missingPl = (missing.map { tournament.unrankValue(for: $0 == 1 ? true : false ) ?? ($0 == 1 ? 70_000 : 10_000) }).prefix(significantPlayerCount)
|
|
self.weight = pl.reduce(0,+) + missingPl.reduce(0,+)
|
|
} else {
|
|
self.weight = players.map { $0.computedRank }.reduce(0,+)
|
|
}
|
|
self.registrationDate = registrationDate
|
|
}
|
|
|
|
func index(in teams: [TeamHolder]) -> Int? {
|
|
teams.firstIndex(where: { $0.id == id })
|
|
}
|
|
|
|
func formattedSeedIndex(index: Int?) -> String {
|
|
if let index {
|
|
return "#\(index + 1)"
|
|
} else {
|
|
return "###"
|
|
}
|
|
}
|
|
|
|
func formattedSeed(in teams: [TeamHolder]) -> String {
|
|
if let index = index(in: teams) {
|
|
return "#\(index + 1)"
|
|
} else {
|
|
return "###"
|
|
}
|
|
}
|
|
}
|
|
|
|
static let FFT_ASSIMILATION_WOMAN_IN_MAN = "A calculer selon la pondération en vigueur"
|
|
|
|
func createTeams(from fileContent: String, tournament: Tournament, fileProvider: FileProvider = .frenchFederation, checkingCategoryDisabled: Bool, chunkByParameter: Bool) async throws -> [TeamHolder] {
|
|
|
|
switch fileProvider {
|
|
case .frenchFederation:
|
|
return try await _getFederalTeams(from: fileContent, tournament: tournament, checkingCategoryDisabled: checkingCategoryDisabled)
|
|
case .padelClub:
|
|
return await _getPadelClubTeams(from: fileContent, tournament: tournament)
|
|
case .custom:
|
|
return await _getPadelBusinessLeagueTeams(from: fileContent, chunkByParameter: chunkByParameter, autoSearch: false, tournament: tournament)
|
|
case .customAutoSearch:
|
|
return await _getPadelBusinessLeagueTeams(from: fileContent, chunkByParameter: chunkByParameter, autoSearch: true, tournament: tournament)
|
|
}
|
|
}
|
|
|
|
func importDataFromFFT(importingDate: Date) async -> String? {
|
|
for source in SourceFile.allCases {
|
|
for fileURL in source.currentURLs(importingDate: importingDate) {
|
|
let p = readCSV(inputFile: fileURL)
|
|
await importingChunkOfPlayers(p, importingDate: importingDate)
|
|
}
|
|
}
|
|
return URL.importDateFormatter.string(from: importingDate)
|
|
}
|
|
|
|
|
|
func readCSV(inputFile: URL) -> [FederalPlayer] {
|
|
do {
|
|
let fileContent = try String(contentsOf: inputFile)
|
|
return loadFromCSV(fileContent: fileContent, isMale: inputFile.manData)
|
|
} catch {
|
|
print("error: \(error)") // to do deal with errors
|
|
}
|
|
return []
|
|
}
|
|
|
|
func loadFromCSV(fileContent: String, isMale: Bool) -> [FederalPlayer] {
|
|
let lines = fileContent.components(separatedBy: "\n")
|
|
return lines.compactMap { line in
|
|
if line.components(separatedBy: ";").count < 10 {
|
|
} else {
|
|
let data = line.components(separatedBy: ";").joined(separator: "\n")
|
|
return FederalPlayer(data, isMale: isMale)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async {
|
|
for chunk in players.chunked(into: 2000) {
|
|
await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate)
|
|
}
|
|
}
|
|
|
|
private func _getFederalTeams(from fileContent: String, tournament: Tournament, checkingCategoryDisabled: Bool = false) async throws -> [TeamHolder] {
|
|
let lines = fileContent.components(separatedBy: "\n")
|
|
guard let firstLine = lines.first else { return [] }
|
|
var separator = ","
|
|
if firstLine.contains(";") {
|
|
separator = ";"
|
|
}
|
|
let headerCount = firstLine.components(separatedBy: separator).count
|
|
|
|
guard headerCount > 12 else {
|
|
throw FileImportManagerError.unknownFormat
|
|
}
|
|
|
|
var results: [TeamHolder] = []
|
|
if headerCount <= 18 {
|
|
Array(lines.dropFirst()).chunked(into: 2).forEach { teamLines in
|
|
if teamLines.count == 2 {
|
|
let dataOne = teamLines[0].replacingOccurrences(of: "\"", with: "").components(separatedBy: separator)
|
|
var dataTwo = teamLines[1].replacingOccurrences(of: "\"", with: "").components(separatedBy: separator)
|
|
if dataOne[11] != dataTwo[3] || dataOne[12] != dataTwo[4] {
|
|
if let found = lines.map({ $0.replacingOccurrences(of: "\"", with: "").components(separatedBy: separator) }).first(where: { components in
|
|
return dataOne[11] == components[3] && dataOne[12] == components[4]
|
|
}) {
|
|
dataTwo = found
|
|
}
|
|
}
|
|
if dataOne.count == dataTwo.count {
|
|
let category = dataOne[0]
|
|
|
|
var tournamentCategory: TournamentCategory {
|
|
switch category {
|
|
case "Double Messieurs":
|
|
return .men
|
|
case "Double Dames":
|
|
return .women
|
|
case "Double Mixte":
|
|
return .mix
|
|
default:
|
|
return .men
|
|
}
|
|
}
|
|
|
|
let ageCategory = dataOne[1]
|
|
var tournamentAgeCategory: FederalTournamentAge {
|
|
FederalTournamentAge.allCases.first(where: { $0.importingRawValue.canonicalVersion == ageCategory.canonicalVersion }) ?? .senior
|
|
}
|
|
|
|
let resultOne = Array(dataOne.dropFirst(3).dropLast(3))
|
|
let resultTwo = Array(dataTwo.dropFirst(3).dropLast(3))
|
|
let sexUnknown: Bool = (dataOne.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) || (dataTwo.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true)
|
|
|
|
var sexPlayerOne : Int {
|
|
switch tournamentCategory {
|
|
case .unlisted: return 1
|
|
case .men: return 1
|
|
case .women: return 0
|
|
case .mix: return 0
|
|
}
|
|
}
|
|
|
|
var sexPlayerTwo : Int {
|
|
switch tournamentCategory {
|
|
case .unlisted: return 1
|
|
case .men: return 1
|
|
case .women: return 0
|
|
case .mix: return 1
|
|
}
|
|
}
|
|
if (tournamentCategory == tournament.tournamentCategory && tournamentAgeCategory == tournament.federalTournamentAge) || checkingCategoryDisabled {
|
|
let playerOne = PlayerRegistration(federalData: Array(resultOne[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown)
|
|
playerOne?.setComputedRank(in: tournament)
|
|
let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
|
|
playerTwo?.setComputedRank(in: tournament)
|
|
|
|
let players = [playerOne, playerTwo].compactMap({ $0 })
|
|
if players.isEmpty == false {
|
|
let team = TeamHolder(players: players, tournamentCategory: tournamentCategory, tournamentAgeCategory: tournamentAgeCategory, previousTeam: tournament.findTeam(players), tournament: tournament)
|
|
results.append(team)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return results
|
|
} else {
|
|
lines.dropFirst().forEach { line in
|
|
let data = line.components(separatedBy: separator)
|
|
if data.count > 18 {
|
|
let category = data[0]
|
|
let sexUnknown: Bool = (data.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true)
|
|
var tournamentCategory: TournamentCategory {
|
|
switch category {
|
|
case "Double Messieurs":
|
|
return .men
|
|
case "Double Dames":
|
|
return .women
|
|
case "Double Mixte":
|
|
return .mix
|
|
default:
|
|
return .men
|
|
}
|
|
}
|
|
|
|
|
|
let ageCategory = data[1]
|
|
var tournamentAgeCategory: FederalTournamentAge {
|
|
FederalTournamentAge.allCases.first(where: { $0.importingRawValue.canonicalVersion == ageCategory.canonicalVersion }) ?? .senior
|
|
}
|
|
|
|
if (tournamentCategory == tournament.tournamentCategory && tournamentAgeCategory == tournament.federalTournamentAge) || checkingCategoryDisabled {
|
|
let result = Array(data.dropFirst(3).dropLast())
|
|
|
|
var sexPlayerOne : Int {
|
|
switch tournamentCategory {
|
|
case .unlisted: return 1
|
|
case .men: return 1
|
|
case .women: return 0
|
|
case .mix: return 1
|
|
}
|
|
}
|
|
|
|
var sexPlayerTwo : Int {
|
|
switch tournamentCategory {
|
|
case .unlisted: return 1
|
|
case .men: return 1
|
|
case .women: return 0
|
|
case .mix: return 0
|
|
}
|
|
}
|
|
|
|
let playerOne = PlayerRegistration(federalData: Array(result[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown)
|
|
playerOne?.setComputedRank(in: tournament)
|
|
let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
|
|
playerTwo?.setComputedRank(in: tournament)
|
|
|
|
let players = [playerOne, playerTwo].compactMap({ $0 })
|
|
if players.isEmpty == false {
|
|
let team = TeamHolder(players: players, tournamentCategory: tournamentCategory, tournamentAgeCategory: tournamentAgeCategory, previousTeam: tournament.findTeam(players), tournament: tournament)
|
|
results.append(team)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
}
|
|
|
|
private func _getPadelClubTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] {
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.locale = Locale(identifier: "fr_FR")
|
|
|
|
// Set the date format to match the input string
|
|
dateFormatter.dateFormat = "EEE dd MMM yyyy 'à' HH:mm"
|
|
|
|
|
|
var lines = fileContent.components(separatedBy: "\n\n")
|
|
var results: [TeamHolder] = []
|
|
let fetchRequest = ImportedPlayer.fetchRequest()
|
|
let federalContext = PersistenceController.shared.localContainer.viewContext
|
|
lines.removeAll(where: { $0.contains("Liste d'attente")})
|
|
lines.forEach { team in
|
|
let data = team.components(separatedBy: "\n")
|
|
let players = team.licencesFound()
|
|
fetchRequest.predicate = NSPredicate(format: "license IN %@", players)
|
|
let found = try? federalContext.fetch(fetchRequest)
|
|
let registeredPlayers = found?.map({ importedPlayer in
|
|
let player = PlayerRegistration(importedPlayer: importedPlayer)
|
|
player.setComputedRank(in: tournament)
|
|
return player
|
|
})
|
|
if let registeredPlayers, registeredPlayers.isEmpty == false {
|
|
var registrationDate: Date? {
|
|
if let registrationDateData = data[safe:2]?.replacingOccurrences(of: "Inscrit le ", with: "") {
|
|
return dateFormatter.date(from: registrationDateData)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var name: String? {
|
|
if let name = data[safe:3] {
|
|
return name
|
|
}
|
|
return nil
|
|
}
|
|
|
|
let team = TeamHolder(players: registeredPlayers, tournamentCategory: tournament.tournamentCategory, tournamentAgeCategory: tournament.federalTournamentAge, previousTeam: tournament.findTeam(registeredPlayers), registrationDate: registrationDate, tournament: tournament)
|
|
results.append(team)
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
private func _getPadelBusinessLeagueTeams(from fileContent: String, chunkByParameter: Bool, autoSearch: Bool, tournament: Tournament) async -> [TeamHolder] {
|
|
let lines = fileContent.replacingOccurrences(of: "\"", with: "").components(separatedBy: "\n")
|
|
guard let firstLine = lines.first else { return [] }
|
|
var separator = ","
|
|
if firstLine.contains(";") {
|
|
separator = ";"
|
|
}
|
|
let fetchRequest = ImportedPlayer.fetchRequest()
|
|
let federalContext = PersistenceController.shared.localContainer.viewContext
|
|
|
|
var chunks: [[String]] = []
|
|
if chunkByParameter {
|
|
chunks = lines.chunked(byParameterAt: 1)
|
|
} else {
|
|
chunks = lines.chunked(into: 2)
|
|
}
|
|
let results = chunks.map { team in
|
|
var teamName: String? = nil
|
|
let players = team.map { player in
|
|
let data = player.components(separatedBy: separator)
|
|
let lastName : String = data[safe: 2]?.prefixTrimmed(50) ?? ""
|
|
let firstName : String = data[safe: 3]?.prefixTrimmed(50) ?? ""
|
|
let sex: PlayerRegistration.PlayerSexType = data[safe: 0] == "f" ? PlayerRegistration.PlayerSexType.female : PlayerRegistration.PlayerSexType.male
|
|
if data[safe: 1]?.trimmed != nil {
|
|
teamName = data[safe: 1]?.trimmed
|
|
}
|
|
let phoneNumber : String? = data[safe: 4]?.trimmed.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).prefixTrimmed(50)
|
|
let email : String? = data[safe: 5]?.prefixTrimmed(50)
|
|
let rank : Int? = data[safe: 6]?.trimmed.toInt()
|
|
let licenceId : String? = data[safe: 7]?.prefixTrimmed(50)
|
|
let club : String? = data[safe: 8]?.prefixTrimmed(200)
|
|
let predicate = NSPredicate(format: "firstName like[cd] %@ && lastName like[cd] %@", firstName, lastName)
|
|
fetchRequest.predicate = predicate
|
|
let found = try? federalContext.fetch(fetchRequest).first
|
|
if let found, autoSearch {
|
|
let player = PlayerRegistration(importedPlayer: found)
|
|
player.setComputedRank(in: tournament)
|
|
player.email = email
|
|
player.phoneNumber = phoneNumber
|
|
return player
|
|
} else {
|
|
let player = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: licenceId, rank: rank, sex: sex, clubName: club, phoneNumber: phoneNumber, email: email)
|
|
if rank == nil, autoSearch {
|
|
player.setComputedRank(in: tournament)
|
|
} else {
|
|
player.computedRank = rank ?? 0
|
|
}
|
|
return player
|
|
}
|
|
}
|
|
|
|
return TeamHolder(players: players, tournamentCategory: tournament.tournamentCategory, tournamentAgeCategory: tournament.federalTournamentAge, previousTeam: nil, name: teamName, tournament: tournament)
|
|
}
|
|
return results
|
|
}
|
|
}
|
|
|
|
extension Array where Element == String {
|
|
/// Groups the array of CSV lines based on the same value at the specified column index.
|
|
/// If no key is found, it defaults to chunking the array into groups of 2 lines.
|
|
/// - Parameter index: The index of the CSV column to group by.
|
|
/// - Returns: An array of arrays, where each inner array contains lines grouped by the CSV parameter or by default chunks of 2.
|
|
func chunked(byParameterAt index: Int) -> [[String]] {
|
|
var groups: [String: [String]] = [:]
|
|
|
|
for line in self {
|
|
let columns = line.split(separator: ";", omittingEmptySubsequences: false).map { String($0) }
|
|
if index < columns.count {
|
|
let key = columns[index]
|
|
|
|
if groups[key] == nil {
|
|
groups[key] = []
|
|
}
|
|
groups[key]?.append(line)
|
|
} else {
|
|
// Handle out-of-bounds by continuing
|
|
print("Warning: Index \(index) out of bounds for line: \(line)")
|
|
}
|
|
}
|
|
|
|
// If no valid groups found, chunk into groups of 2 lines
|
|
if groups.isEmpty {
|
|
return self.chunked(into: 2)
|
|
} else {
|
|
// Append groups by parameter value, converting groups.values into an array of arrays
|
|
return groups.map { $0.value }
|
|
}
|
|
}
|
|
}
|
|
|
|
|