From b3f07a18ccf8300d1b2792c4f67a883f5699313b Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 7 Oct 2024 11:41:33 +0200 Subject: [PATCH] add fft rules about p500+ fix sorting scores and caching in multi step groupstages fix birth date importing --- .../xcschemes/PadelClub ProdTest.xcscheme | 2 +- PadelClub/Data/GroupStage.swift | 123 +++++++++++++++--- PadelClub/Data/Match.swift | 7 +- PadelClub/Data/PlayerRegistration.swift | 2 +- PadelClub/Data/Tournament.swift | 15 ++- PadelClub/Extensions/String+Extensions.swift | 36 +++++ PadelClub/Utils/PadelRule.swift | 30 +++++ .../Views/Planning/PlanningByCourtView.swift | 48 ++++--- PadelClub/Views/Player/PlayerDetailView.swift | 4 + .../Views/Tournament/FileImportView.swift | 8 +- .../TournamentInscriptionView.swift | 31 ++++- 11 files changed, 248 insertions(+), 58 deletions(-) diff --git a/PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub ProdTest.xcscheme b/PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub ProdTest.xcscheme index a9ce218..739a24c 100644 --- a/PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub ProdTest.xcscheme +++ b/PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub ProdTest.xcscheme @@ -31,7 +31,7 @@ shouldAutocreateTestPlan = "YES"> Bool { - guard teams().count == size else { return false } let _matches = _matches() if _matches.isEmpty { return false } + //guard teams().count == size else { return false } return _matches.anySatisfy { $0.hasEnded() == false } == false } @@ -148,6 +148,8 @@ final class GroupStage: ModelObject, Storable { } func updateGroupStageState() { + clearScoreCache() + if hasEnded(), let tournament = tournamentObject() { do { let teams = teams(true) @@ -190,24 +192,24 @@ final class GroupStage: ModelObject, Storable { } } - func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? { - guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil } - let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() }) - if matches.isEmpty && nilIfEmpty { return nil } - let wins = matches.filter { $0.winningTeamId == team.id }.count - let loses = matches.filter { $0.losingTeamId == team.id }.count - let differences = matches.compactMap { $0.scoreDifference(groupStagePosition) } - let setDifference = differences.map { $0.set }.reduce(0,+) - let gameDifference = differences.map { $0.game }.reduce(0,+) - return (team, wins, loses, setDifference, gameDifference) - /* - • 2 points par rencontre gagnée - • 1 point par rencontre perdue - • -1 point en cas de rencontre perdue par disqualification (scores de 6/0 6/0 attribués aux trois matchs) - • -2 points en cas de rencontre perdu par WO (scores de 6/0 6/0 attribués aux trois matchs) - */ - } - +// func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? { +// guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil } +// let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() }) +// if matches.isEmpty && nilIfEmpty { return nil } +// let wins = matches.filter { $0.winningTeamId == team.id }.count +// let loses = matches.filter { $0.losingTeamId == team.id }.count +// let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) } +// let setDifference = differences.map { $0.set }.reduce(0,+) +// let gameDifference = differences.map { $0.game }.reduce(0,+) +// return (team, wins, loses, setDifference, gameDifference) +// /* +// • 2 points par rencontre gagnée +// • 1 point par rencontre perdue +// • -1 point en cas de rencontre perdue par disqualification (scores de 6/0 6/0 attribués aux trois matchs) +// • -2 points en cas de rencontre perdu par WO (scores de 6/0 6/0 attribués aux trois matchs) +// */ +// } +// func matches(forGroupStagePosition groupStagePosition: Int) -> [Match] { let combos = Array((0.. [TeamRegistration] { if sortedByScore { return unsortedTeams().compactMap({ team in - scores?.first(where: { $0.team.id == team.id }) ?? _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!) + // Check cache or use provided scores, otherwise calculate and store in cache + scores?.first(where: { $0.team.id == team.id }) ?? { + if let cachedScore = scoreCache[team.groupStagePositionAtStep(step)!] { + return cachedScore + } else { + let score = _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!) + if let score = score { + scoreCache[team.groupStagePositionAtStep(step)!] = score + } + return score + } + }() }).sorted { (lhs, rhs) in let predicates: [TeamScoreAreInIncreasingOrder] = [ { $0.wins < $1.wins }, @@ -379,7 +395,7 @@ final class GroupStage: ModelObject, Storable { { [self] in $0.team.groupStagePositionAtStep(self.step)! > $1.team.groupStagePositionAtStep(self.step)! } ] - for predicate in predicates { + for predicate in predicates { if !predicate(lhs, rhs) && !predicate(rhs, lhs) { continue } @@ -393,7 +409,72 @@ final class GroupStage: ModelObject, Storable { return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!) } } + + func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? { + // Check if the score for this position is already cached + if let cachedScore = scoreCache[groupStagePosition] { + return cachedScore + } + + guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil } + let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() }) + if matches.isEmpty && nilIfEmpty { return nil } + let wins = matches.filter { $0.winningTeamId == team.id }.count + let loses = matches.filter { $0.losingTeamId == team.id }.count + let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) } + let setDifference = differences.map { $0.set }.reduce(0,+) + let gameDifference = differences.map { $0.game }.reduce(0,+) + + // Calculate the score and store it in the cache + let score = (team, wins, loses, setDifference, gameDifference) + scoreCache[groupStagePosition] = score + return score + } + // Clear the cache if necessary, for example when starting a new step or when matches update + func clearScoreCache() { + scoreCache.removeAll() + } + +// func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] { +// if sortedByScore { +// return unsortedTeams().compactMap({ team in +// scores?.first(where: { $0.team.id == team.id }) ?? _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!) +// }).sorted { (lhs, rhs) in +// // Calculate intermediate values once and reuse them +// let lhsWins = lhs.wins +// let rhsWins = rhs.wins +// let lhsSetDifference = lhs.setDifference +// let rhsSetDifference = rhs.setDifference +// let lhsGameDifference = lhs.gameDifference +// let rhsGameDifference = rhs.gameDifference +// let lhsHeadToHead = self._headToHead(lhs.team, rhs.team) +// let rhsHeadToHead = self._headToHead(rhs.team, lhs.team) +// let lhsGroupStagePosition = lhs.team.groupStagePositionAtStep(self.step)! +// let rhsGroupStagePosition = rhs.team.groupStagePositionAtStep(self.step)! +// +// // Define comparison predicates in the same order +// let predicates: [(Bool, Bool)] = [ +// (lhsWins < rhsWins, lhsWins > rhsWins), +// (lhsSetDifference < rhsSetDifference, lhsSetDifference > rhsSetDifference), +// (lhsGameDifference < rhsGameDifference, lhsGameDifference > rhsGameDifference), +// (lhsHeadToHead, rhsHeadToHead), +// (lhsGroupStagePosition > rhsGroupStagePosition, lhsGroupStagePosition < rhsGroupStagePosition) +// ] +// +// // Iterate over predicates and return as soon as a valid comparison is found +// for (lhsPredicate, rhsPredicate) in predicates { +// if lhsPredicate { return true } +// if rhsPredicate { return false } +// } +// +// return false +// }.map({ $0.team }).reversed() +// } else { +// return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!) +// } +// } + func updateMatchFormat(_ updatedMatchFormat: MatchFormat) { self.matchFormat = updatedMatchFormat self.updateAllMatchesFormat() diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 11b49ec..7fee09c 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -220,6 +220,9 @@ defer { endDate = nil removeCourt() servingTeamId = nil + groupStageObject?.updateGroupStageState() + roundObject?.updateTournamentState() + currentTournament()?.updateTournamentState() } func resetScores() { @@ -757,10 +760,10 @@ defer { // return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 } } - func scoreDifference(_ teamPosition: Int) -> (set: Int, game: Int)? { + func scoreDifference(_ teamPosition: Int, atStep step: Int) -> (set: Int, game: Int)? { guard let teamScoreTeam = teamScore(.one), let teamScoreOtherTeam = teamScore(.two) else { return nil } var reverseValue = 1 - if teamPosition == team(.two)?.groupStagePosition { + if teamPosition == team(.two)?.groupStagePositionAtStep(step) { reverseValue = -1 } let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut()) diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 41b920f..92d29cf 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -82,7 +82,7 @@ final class PlayerRegistration: ModelObject, Storable { if _lastName.isEmpty && _firstName.isEmpty { return nil } lastName = _lastName firstName = _firstName - birthdate = federalData[2] + birthdate = federalData[2].formattedAsBirthdate() licenceId = federalData[3] clubName = federalData[4] let stringRank = federalData[5] diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index b710840..f5d0871 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -505,7 +505,7 @@ defer { } func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String { - let selectedSortedTeams = selectedSortedTeams() + let selectedSortedTeams = selectedSortedTeams() + waitingListSortedTeams() switch exportFormat { case .rawText: return (selectedSortedTeams.compactMap { $0.pasteData(exportFormat) } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData(exportFormat) }).joined(separator: exportFormat.newLineSeparator(2)) @@ -829,7 +829,7 @@ defer { let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending) let groupStageSpots: Int = self.groupStageSpots() - var bracketSeeds: Int = min(teamCount, _completeTeams.count) - groupStageSpots + var bracketSeeds: Int = min(teamCount, _completeTeams.count) - groupStageSpots - wcBracket.count var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count if groupStageTeamCount < 0 { groupStageTeamCount = 0 } if bracketSeeds < 0 { bracketSeeds = 0 } @@ -2373,3 +2373,14 @@ extension Tournament { } +extension Tournament { + func deadline(for type: TournamentDeadlineType) -> Date? { + guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil } + + if let date = Calendar.current.date(byAdding: .day, value: type.daysOffset, to: startDate) { + let startOfDay = Calendar.current.startOfDay(for: date) + return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay) + } + return nil + } +} diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index cce72bc..37094b9 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -214,3 +214,39 @@ extension String { extension String : @retroactive Identifiable { public var id: String { self } } + +extension String { + /// Parses the birthdate string into a `Date` based on multiple formats. + /// - Returns: A `Date` object if parsing is successful, or `nil` if the format is unrecognized. + func parseAsBirthdate() -> Date? { + let dateFormats = [ + "yyyy-MM-dd", // Format for "1993-01-31" + "dd/MM/yyyy", // Format for "27/07/1992" + "dd/MM/yy" // Format for "27/07/92" + ] + + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Ensure consistent parsing + + for format in dateFormats { + dateFormatter.dateFormat = format + + if let date = dateFormatter.date(from: self) { + return date // Return the parsed date if successful + } + } + + return nil // Return nil if no format matches + } + + /// Formats the birthdate string into "DD/MM/YYYY". + /// - Returns: A formatted birthdate string, or the original string if parsing fails. + func formattedAsBirthdate() -> String { + if let parsedDate = self.parseAsBirthdate() { + let outputFormatter = DateFormatter() + outputFormatter.dateFormat = "dd/MM/yyyy" // Desired output format + return outputFormatter.string(from: parsedDate) + } + return self // Return the original string if parsing fails + } +} diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index b313f00..7db94a5 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -1699,3 +1699,33 @@ enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable { } } } + +enum TournamentDeadlineType: String, CaseIterable { + case inscription = "Inscription" + case broadcastList = "Publication de la liste" + case wildcardRequest = "Demande de WC" + case wildcardLicensePurchase = "Prise de licence des WC" + case definitiveBroadcastList = "Publication définitive" + + var daysOffset: Int { + switch self { + case .inscription: + return -13 + case .broadcastList: + return -12 + case .wildcardRequest: + return -9 + case .wildcardLicensePurchase, .definitiveBroadcastList: + return -8 + } + } + + var timeOffset: DateComponents { + switch self { + case .broadcastList, .definitiveBroadcastList: + return DateComponents(hour: 12) + case .inscription, .wildcardRequest, .wildcardLicensePurchase: + return DateComponents(minute: -1) + } + } +} diff --git a/PadelClub/Views/Planning/PlanningByCourtView.swift b/PadelClub/Views/Planning/PlanningByCourtView.swift index 25cc794..bf8d7a4 100644 --- a/PadelClub/Views/Planning/PlanningByCourtView.swift +++ b/PadelClub/Views/Planning/PlanningByCourtView.swift @@ -13,27 +13,33 @@ struct PlanningByCourtView: View { let matches: [Match] @Binding var selectedScheduleDestination: ScheduleDestination? - @State private var timeSlots: [Date:[Match]] - @State private var days: [Date] - @State private var keys: [Date] - @State private var courts: [Int] @State private var viewByCourt: Bool = false - @State private var courtSlots: [Int:[Match]] @State private var selectedDay: Date @State private var selectedCourt: Int = 0 - + var timeSlots: [Date:[Match]] { + Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } + } + + var days: [Date] { + Set(timeSlots.keys.map { $0.startOfDay }).sorted() + } + + var keys: [Date] { + timeSlots.keys.sorted() + } + + var courts: [Int] { + courtSlots.keys.sorted() + } + + var courtSlots: [Int:[Match]] { + Dictionary(grouping: matches) { $0.courtIndex ?? Int.max } + } + init(matches: [Match], selectedScheduleDestination: Binding, startDate: Date) { self.matches = matches _selectedScheduleDestination = selectedScheduleDestination - let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } - let courtSlots = Dictionary(grouping: matches) { $0.courtIndex ?? Int.max} - _timeSlots = State(wrappedValue: timeSlots) - _courtSlots = State(wrappedValue: courtSlots) - _days = State(wrappedValue: Set(timeSlots.keys.map { $0.startOfDay }).sorted()) - _keys = State(wrappedValue: timeSlots.keys.sorted()) - _courts = State(wrappedValue: courtSlots.keys.sorted()) - _selectedDay = State(wrappedValue: startDate) } @@ -53,6 +59,13 @@ struct PlanningByCourtView: View { selectedScheduleDestination = nil } } + } else if courtSlots.isEmpty == false { + ContentUnavailableView { + Label("Aucun match plannifié", systemImage: "clock.badge.questionmark") + } description: { + Text("Aucun match n'a été plannifié sur ce terrain et au jour sélectionné") + } actions: { + } } } .navigationTitle(Text(selectedDay.formatted(.dateTime.day().weekday().month()))) @@ -113,13 +126,6 @@ struct PlanningByCourtView: View { } .headerProminence(.increased) } - } else if courtSlots.isEmpty == false { - ContentUnavailableView { - Label("Aucun match plannifié", systemImage: "clock.badge.questionmark") - } description: { - Text("Aucun match n'a été plannifié sur ce terrain et au jour sélectionné") - } actions: { - } } } diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index f80848e..982f11e 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -62,6 +62,10 @@ struct PlayerDetailView: View { } PlayerSexPickerView(player: player) + + if let birthdate = player.birthdate { + Text(birthdate) + } } Section { diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 9110942..7ea2bcc 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -490,10 +490,10 @@ struct FileImportView: View { let unfound = _getUnfound(tournament: tournament, fromTeams: filteredTeams) unfound.forEach { team in - team.resetPositions() - team.wildCardBracket = false - team.wildCardGroupStage = false - team.walkOut = true + if team.isWildCard() == false { + team.resetPositions() + team.walkOut = true + } } do { diff --git a/PadelClub/Views/Tournament/TournamentInscriptionView.swift b/PadelClub/Views/Tournament/TournamentInscriptionView.swift index fcd1a68..a40dee1 100644 --- a/PadelClub/Views/Tournament/TournamentInscriptionView.swift +++ b/PadelClub/Views/Tournament/TournamentInscriptionView.swift @@ -24,12 +24,9 @@ struct TournamentInscriptionView: View { } } } - if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false { - LabeledContent { - Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened)) - } label: { - Text("Date limite") - } + + if tournament.inscriptionClosed() == false && tournament.hasStarted() == false { + TournamentDeadlinesView(tournament: tournament) } } @@ -42,3 +39,25 @@ struct TournamentInscriptionView: View { } } + +struct TournamentDeadlinesView: View { + let tournament: Tournament + + var body: some View { + ForEach(TournamentDeadlineType.allCases, id: \.self) { deadlineType in + if let deadlineDate = tournament.deadline(for: deadlineType) { + LabeledContent { + VStack(alignment: .trailing, spacing: 2) { + Text(deadlineDate.formatted(.dateTime.hour().minute())) + .foregroundStyle(.primary) + Text(deadlineDate.formatted(.dateTime.weekday(.abbreviated).day().month())) + .foregroundStyle(.secondary) + } + } label: { + Text(deadlineType.rawValue) + Text("Date limite") + } + } + } + } +}