Laurent 2 years ago
commit 2d896e1b0f
  1. 94
      PadelClub/Data/Federal/FederalPlayer.swift
  2. 3
      PadelClub/Data/Tournament.swift
  3. 3
      PadelClub/Utils/FileImportManager.swift
  4. 28
      PadelClub/Utils/SourceFileManager.swift
  5. 6
      PadelClub/Views/Navigation/MainView.swift
  6. 67
      PadelClub/Views/Navigation/Toolbox/PadelClubView.swift
  7. 2
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift

@ -7,7 +7,7 @@
import Foundation
struct FederalPlayer {
struct FederalPlayer: Decodable {
var rank: Int
var lastName: String
var firstName: String
@ -21,6 +21,94 @@ struct FederalPlayer {
var club: String
var isMale: Bool
// MARK: - Nationnalite
struct Nationnalite: Hashable, Codable {
let code, codeFov: String
}
init(from decoder: Decoder) throws {
enum CodingKeys: String, CodingKey {
case nom
case prenom
case licence
case meilleurClassement
case nationnalite
case anneeNaissance
case codeClub
case nomClub
case nomLigue
case rang
case progression
case points
case nombreDeTournois
case assimile
}
let container = try decoder.container(keyedBy: CodingKeys.self)
isMale = (decoder.userInfo[.maleData] as? Bool) == true
let _lastName = try container.decode(String.self, forKey: .nom)
let _firstName = try container.decode(String.self, forKey: .prenom)
lastName = _lastName
firstName = _firstName
if let lic = try? container.decodeIfPresent(Int.self, forKey: .licence) {
license = String(lic)
} else {
license = ""
}
let nationnalite = try container.decode(Nationnalite.self, forKey: .nationnalite)
country = nationnalite.code
//meilleurClassement = try container.decode(Int.self, forKey: .meilleurClassement)
//anneeNaissance = try container.decode(Int.self, forKey: .anneeNaissance)
clubCode = try container.decode(String.self, forKey: .codeClub)
club = try container.decode(String.self, forKey: .nomClub)
ligue = try container.decode(String.self, forKey: .nomLigue)
rank = try container.decode(Int.self, forKey: .rang)
//progression = try? container.decodeIfPresent(Int.self, forKey: .progression)
let pointsAsInt = try? container.decodeIfPresent(Int.self, forKey: .points)
if let pointsAsInt {
points = Double(pointsAsInt)
} else {
points = nil
}
tournamentCount = try? container.decodeIfPresent(Int.self, forKey: .nombreDeTournois)
let assimile = try container.decode(Bool.self, forKey: .assimile)
assimilation = assimile ? "Oui" : "Non"
fullNameCanonical = _lastName.canonicalVersion + " " + _firstName.canonicalVersion
}
func exportToCSV() -> String {
let pointsString = points != nil ? String(Int(points!)) : ""
let tournamentCountString = tournamentCount != nil ? String(tournamentCount!) : ""
let strippedLicense = license.strippedLicense ?? ""
let line = ";\(rank);\(lastName);\(firstName);\(country);\(strippedLicense);\(pointsString);\(assimilation);\(tournamentCountString);\(ligue);\(formatNumbers(clubCode));\(club);"
return line
}
func formatNumbers(_ input: String) -> String {
// Insert spaces at appropriate positions
let formattedString = insertSeparator(input, separator: " ", every: [2, 4])
return formattedString
}
func insertSeparator(_ input: String, separator: String, every positions: [Int]) -> String {
var modifiedString = input
// Adjust for the index shift caused by inserting characters
var offset = 0
// Insert separator at specified positions
for position in positions {
let index = modifiedString.index(modifiedString.startIndex, offsetBy: position + offset)
modifiedString.insert(contentsOf: separator, at: index)
// Increase offset to adjust for the inserted character
offset += separator.count
}
return modifiedString
}
var fullNameCanonical: String
/*
@ -115,3 +203,7 @@ struct FederalPlayer {
}
}
extension CodingUserInfoKey {
static let maleData = Self(rawValue: "maleData")!
}

@ -872,11 +872,10 @@ class Tournament : ModelObject, Storable {
let lastRankMan = currentMonthData()?.maleUnrankedValue
let lastRankWoman = currentMonthData()?.femaleUnrankedValue
try await unsortedPlayers().concurrentForEach { player in
let dataURLs = SourceFileManager.shared.allFiles.filter({ $0.dateFromPath == newDate })
let sources = dataURLs.map { CSVParser(url: $0) }
try await unsortedPlayers().concurrentForEach { player in
try await player.updateRank(from: sources, lastRank: (player.sex == .female ? lastRankWoman : lastRankMan) ?? 0)
}
}

@ -6,6 +6,7 @@
//
import Foundation
import LeStorage
class FileImportManager {
static let shared = FileImportManager()
@ -147,7 +148,7 @@ class FileImportManager {
}
func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async {
for chunk in players.chunked(into: 1000) {
for chunk in players.chunked(into: 2000) {
await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate)
}
}

@ -6,6 +6,7 @@
//
import Foundation
import LeStorage
class SourceFileManager {
static let shared = SourceFileManager()
@ -60,6 +61,26 @@ class SourceFileManager {
}
}
func exportToCSV(players: [FederalPlayer], sourceFileType: SourceFile, date: Date) {
let lastDateString = URL.importDateFormatter.string(from: date)
let dateString = ["CLASSEMENT-PADEL", sourceFileType.rawValue, lastDateString].joined(separator: "-") + "." + "csv"
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)")
var csvText : String = ""
for player in players {
csvText.append(player.exportToCSV() + "\n")
}
do {
try csvText.write(to: destinationFileUrl, atomically: true, encoding: .utf8)
print("CSV file exported successfully.")
} catch {
print("Error writing CSV file:", error)
Logger.error(error)
}
}
func fetchData(fromDate current: Date) async {
let lastStringDate = URL.importDateFormatter.string(from: current)
@ -148,6 +169,13 @@ class SourceFileManager {
}
}
func jsonFiles() -> [URL] {
let allJSONFiles = try! FileManager.default.contentsOfDirectory(at: rankingSourceDirectory, includingPropertiesForKeys: nil).filter({ url in
url.pathExtension == "json"
})
return allJSONFiles
}
var allFiles: [URL] {
let allFiles = try! FileManager.default.contentsOfDirectory(at: rankingSourceDirectory, includingPropertiesForKeys: nil).filter({ url in
url.pathExtension == "csv"

@ -126,11 +126,7 @@ struct MainView: View {
Task {
let lastDataSource = await FileImportManager.shared.importDataFromFFT()
dataStore.appSettings.lastDataSource = lastDataSource
do {
try dataStore.appSettingsStorage.write()
} catch {
Logger.error(error)
}
dataStore.appSettingsStorage.write()
if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) {
await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate)
}

@ -47,6 +47,53 @@ struct PadelClubView: View {
Text("Classement mensuel utilisé")
}
}
if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable, _lastDataSourceDate.isEarlierThan(mostRecentDateAvailable) {
Section {
RowButtonView("Importer \(URL.importDateFormatter.string(from: mostRecentDateAvailable))") {
_startImporting()
}
}
}
#if targetEnvironment(simulator)
/*
["36435", "BOUNOUA", "walid", "France", "3311600", "15,00", "Non", "2", "AUVERGNE RHONE-ALPES", "50 73 0046", "CHAMBERY TC"]
["36435", "BRUL…", "Romain", "France", "2993139", "15,00", "Non", "2", "NOUVELLE AQUITAINE", "59 33 0447", "SAINT LOUBES TC"]
*/
Section {
RowButtonView("Exporter en csv") {
for fileURL in SourceFileManager.shared.jsonFiles() {
let decoder = JSONDecoder()
decoder.userInfo[.maleData] = fileURL.manData
do {
let data = try Data(contentsOf: fileURL)
let players = try decoder.decode([FederalPlayer].self, from: data)
SourceFileManager.shared.exportToCSV(players: players, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath)
} catch {
Logger.error(error)
}
}
}
}
#endif
}
if importingFiles {
ContentUnavailableView("Importation en cours", systemImage: "server.rack", description: Text("Une importation des données fédérales publiques est en cours, veuillez patienter."))
} else if (players.isEmpty || lastDataSource == nil) {
ContentUnavailableView {
Label("Aucun joueur importé", systemImage: "person.slash")
} description: {
Text("Padel Club peut importer toutes les données publiques de la FFT concernant tous les compétiteurs et compétitrices.")
} actions: {
RowButtonView("Démarrer l'importation") {
_startImporting()
}
}
}
let monthData = dataStore.monthData.sorted(by: \.creationDate).reversed()
@ -72,20 +119,6 @@ struct PadelClubView: View {
Text(monthData.monthKey)
}
}
if importingFiles {
ContentUnavailableView("Importation en cours", systemImage: "server.rack", description: Text("Une importation des données fédérales publiques est en cours, veuillez patienter."))
} else if (players.isEmpty || lastDataSource == nil) {
ContentUnavailableView {
Label("Aucun joueur importé", systemImage: "person.slash")
} description: {
Text("Padel Club peut importer toutes les données publiques de la FFT concernant tous les compétiteurs et compétitrices.")
} actions: {
RowButtonView("Démarrer l'importation") {
_startImporting()
}
}
}
}
.task {
await self._checkSourceFileAvailability()
@ -130,11 +163,7 @@ struct PadelClubView: View {
Task {
let lastDataSource = await FileImportManager.shared.importDataFromFFT()
dataStore.appSettings.lastDataSource = lastDataSource
do {
try dataStore.appSettingsStorage.write()
} catch {
Logger.error(error)
}
dataStore.appSettingsStorage.write()
if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) {
await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate)
}

@ -42,7 +42,7 @@ struct ToolboxView: View {
Label("Définir les durées moyennes", systemImage: "deskclock")
}
} footer: {
Text("Vous pouvez définir vos propores estimations de durées de match en fonction du format de jeu.")
Text("Vous pouvez définir vos propres estimations de durées de match en fonction du format de jeu.")
}
}
.navigationTitle(TabDestination.toolbox.title)

Loading…
Cancel
Save