add fft rules about p500+

fix sorting scores and caching in multi step groupstages
fix birth date importing
sync2
Raz 1 year ago
parent b823a10bf5
commit b3f07a18cc
  1. 2
      PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub ProdTest.xcscheme
  2. 121
      PadelClub/Data/GroupStage.swift
  3. 7
      PadelClub/Data/Match.swift
  4. 2
      PadelClub/Data/PlayerRegistration.swift
  5. 15
      PadelClub/Data/Tournament.swift
  6. 36
      PadelClub/Extensions/String+Extensions.swift
  7. 30
      PadelClub/Utils/PadelRule.swift
  8. 46
      PadelClub/Views/Planning/PlanningByCourtView.swift
  9. 4
      PadelClub/Views/Player/PlayerDetailView.swift
  10. 8
      PadelClub/Views/Tournament/FileImportView.swift
  11. 31
      PadelClub/Views/Tournament/TournamentInscriptionView.swift

@ -31,7 +31,7 @@
shouldAutocreateTestPlan = "YES"> shouldAutocreateTestPlan = "YES">
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "0"

@ -100,9 +100,9 @@ final class GroupStage: ModelObject, Storable {
} }
func hasEnded() -> Bool { func hasEnded() -> Bool {
guard teams().count == size else { return false }
let _matches = _matches() let _matches = _matches()
if _matches.isEmpty { return false } if _matches.isEmpty { return false }
//guard teams().count == size else { return false }
return _matches.anySatisfy { $0.hasEnded() == false } == false return _matches.anySatisfy { $0.hasEnded() == false } == false
} }
@ -148,6 +148,8 @@ final class GroupStage: ModelObject, Storable {
} }
func updateGroupStageState() { func updateGroupStageState() {
clearScoreCache()
if hasEnded(), let tournament = tournamentObject() { if hasEnded(), let tournament = tournamentObject() {
do { do {
let teams = teams(true) let teams = teams(true)
@ -190,24 +192,24 @@ final class GroupStage: ModelObject, Storable {
} }
} }
func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? { // func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? {
guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil } // guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil }
let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() }) // let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() })
if matches.isEmpty && nilIfEmpty { return nil } // if matches.isEmpty && nilIfEmpty { return nil }
let wins = matches.filter { $0.winningTeamId == team.id }.count // let wins = matches.filter { $0.winningTeamId == team.id }.count
let loses = matches.filter { $0.losingTeamId == team.id }.count // let loses = matches.filter { $0.losingTeamId == team.id }.count
let differences = matches.compactMap { $0.scoreDifference(groupStagePosition) } // let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) }
let setDifference = differences.map { $0.set }.reduce(0,+) // let setDifference = differences.map { $0.set }.reduce(0,+)
let gameDifference = differences.map { $0.game }.reduce(0,+) // let gameDifference = differences.map { $0.game }.reduce(0,+)
return (team, wins, loses, setDifference, gameDifference) // return (team, wins, loses, setDifference, gameDifference)
/* // /*
2 points par rencontre gagnée // 2 points par rencontre gagnée
1 point par rencontre perdue // 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) // -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) // -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] { func matches(forGroupStagePosition groupStagePosition: Int) -> [Match] {
let combos = Array((0..<size).combinations(ofCount: 2)) let combos = Array((0..<size).combinations(ofCount: 2))
var matchIndexes = [Int]() var matchIndexes = [Int]()
@ -366,10 +368,24 @@ final class GroupStage: ModelObject, Storable {
return self.tournamentStore.teamRegistrations.filter { $0.groupStage == self.id && $0.groupStagePosition != nil } return self.tournamentStore.teamRegistrations.filter { $0.groupStage == self.id && $0.groupStagePosition != nil }
} }
var scoreCache: [Int: TeamGroupStageScore] = [:]
func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] { func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] {
if sortedByScore { if sortedByScore {
return unsortedTeams().compactMap({ team in 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 }).sorted { (lhs, rhs) in
let predicates: [TeamScoreAreInIncreasingOrder] = [ let predicates: [TeamScoreAreInIncreasingOrder] = [
{ $0.wins < $1.wins }, { $0.wins < $1.wins },
@ -394,6 +410,71 @@ final class GroupStage: ModelObject, Storable {
} }
} }
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) { func updateMatchFormat(_ updatedMatchFormat: MatchFormat) {
self.matchFormat = updatedMatchFormat self.matchFormat = updatedMatchFormat
self.updateAllMatchesFormat() self.updateAllMatchesFormat()

@ -220,6 +220,9 @@ defer {
endDate = nil endDate = nil
removeCourt() removeCourt()
servingTeamId = nil servingTeamId = nil
groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState()
} }
func resetScores() { func resetScores() {
@ -757,10 +760,10 @@ defer {
// return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 } // 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 } guard let teamScoreTeam = teamScore(.one), let teamScoreOtherTeam = teamScore(.two) else { return nil }
var reverseValue = 1 var reverseValue = 1
if teamPosition == team(.two)?.groupStagePosition { if teamPosition == team(.two)?.groupStagePositionAtStep(step) {
reverseValue = -1 reverseValue = -1
} }
let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut()) let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut())

@ -82,7 +82,7 @@ final class PlayerRegistration: ModelObject, Storable {
if _lastName.isEmpty && _firstName.isEmpty { return nil } if _lastName.isEmpty && _firstName.isEmpty { return nil }
lastName = _lastName lastName = _lastName
firstName = _firstName firstName = _firstName
birthdate = federalData[2] birthdate = federalData[2].formattedAsBirthdate()
licenceId = federalData[3] licenceId = federalData[3]
clubName = federalData[4] clubName = federalData[4]
let stringRank = federalData[5] let stringRank = federalData[5]

@ -505,7 +505,7 @@ defer {
} }
func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String { func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String {
let selectedSortedTeams = selectedSortedTeams() let selectedSortedTeams = selectedSortedTeams() + waitingListSortedTeams()
switch exportFormat { switch exportFormat {
case .rawText: 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)) 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 wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending)
let groupStageSpots: Int = self.groupStageSpots() 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 var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count
if groupStageTeamCount < 0 { groupStageTeamCount = 0 } if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
if bracketSeeds < 0 { bracketSeeds = 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
}
}

@ -214,3 +214,39 @@ extension String {
extension String : @retroactive Identifiable { extension String : @retroactive Identifiable {
public var id: String { self } 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
}
}

@ -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)
}
}
}

@ -13,27 +13,33 @@ struct PlanningByCourtView: View {
let matches: [Match] let matches: [Match]
@Binding var selectedScheduleDestination: ScheduleDestination? @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 viewByCourt: Bool = false
@State private var courtSlots: [Int:[Match]]
@State private var selectedDay: Date @State private var selectedDay: Date
@State private var selectedCourt: Int = 0 @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<ScheduleDestination?>, startDate: Date) { init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>, startDate: Date) {
self.matches = matches self.matches = matches
_selectedScheduleDestination = selectedScheduleDestination _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) _selectedDay = State(wrappedValue: startDate)
} }
@ -53,6 +59,13 @@ struct PlanningByCourtView: View {
selectedScheduleDestination = nil 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()))) .navigationTitle(Text(selectedDay.formatted(.dateTime.day().weekday().month())))
@ -113,13 +126,6 @@ struct PlanningByCourtView: View {
} }
.headerProminence(.increased) .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: {
}
} }
} }

@ -62,6 +62,10 @@ struct PlayerDetailView: View {
} }
PlayerSexPickerView(player: player) PlayerSexPickerView(player: player)
if let birthdate = player.birthdate {
Text(birthdate)
}
} }
Section { Section {

@ -490,10 +490,10 @@ struct FileImportView: View {
let unfound = _getUnfound(tournament: tournament, fromTeams: filteredTeams) let unfound = _getUnfound(tournament: tournament, fromTeams: filteredTeams)
unfound.forEach { team in unfound.forEach { team in
team.resetPositions() if team.isWildCard() == false {
team.wildCardBracket = false team.resetPositions()
team.wildCardGroupStage = false team.walkOut = true
team.walkOut = true }
} }
do { do {

@ -24,12 +24,9 @@ struct TournamentInscriptionView: View {
} }
} }
} }
if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false {
LabeledContent { if tournament.inscriptionClosed() == false && tournament.hasStarted() == false {
Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened)) TournamentDeadlinesView(tournament: tournament)
} label: {
Text("Date limite")
}
} }
} }
@ -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")
}
}
}
}
}

Loading…
Cancel
Save