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.
 
 
PadelClubData/PadelClubData/Data/Tournament.swift

2628 lines
106 KiB

//
// swift
// PadelClub
//
// Created by Laurent Morvillier on 02/02/2024.
//
import Foundation
import LeStorage
import SwiftUI
@Observable
final public class Tournament: BaseTournament {
//local variable
public var refreshInProgress: Bool = false
public var lastTeamRefresh: Date?
public var refreshRanking: Bool = false
public func shouldRefreshTeams(forced: Bool) -> Bool {
if forced {
return true
}
guard let lastTeamRefresh else { return true }
return lastTeamRefresh.timeIntervalSinceNow < -600
}
@ObservationIgnored
public var navigationPath: [Screen] = []
public var tournamentStore: TournamentStore? {
return TournamentLibrary.shared.store(tournamentId: self.id)
}
public override func deleteUnusedSharedDependencies(store: Store) {
do {
let tournamentStore = try store.alternateStore(identifier: self.id)
tournamentStore.deleteUnusedSharedDependencies(type: DrawLog.self)
tournamentStore.deleteUnusedSharedDependencies(type: TeamRegistration.self)
tournamentStore.deleteUnusedSharedDependencies(type: GroupStage.self)
tournamentStore.deleteUnusedSharedDependencies(type: Round.self)
} catch {
Logger.error(error)
}
store.deleteUnusedSharedDependencies(type: Court.self) { $0.club == self.id }
}
public override func deleteDependencies(store: Store, actionOption: ActionOption) {
do {
let tournamentStore = try store.alternateStore(identifier: self.id)
tournamentStore.deleteAllDependencies(type: DrawLog.self, actionOption: actionOption)
tournamentStore.deleteAllDependencies(type: TeamRegistration.self, actionOption: actionOption)
tournamentStore.deleteAllDependencies(type: GroupStage.self, actionOption: actionOption)
tournamentStore.deleteAllDependencies(type: Round.self, actionOption: actionOption)
tournamentStore.deleteAllDependencies(type: MatchScheduler.self, actionOption: actionOption)
} catch {
Logger.error(error)
}
// guard let tournamentStore = self.tournamentStore else { return }
//
// let drawLogs = Array(tournamentStore.drawLogs)
// for drawLog in drawLogs {
// drawLog.deleteDependencies(store: tournamentStore, shouldBeSynchronized: shouldBeSynchronized)
// }
// tournamentStore.drawLogs.deleteDependencies(drawLogs, shouldBeSynchronized: shouldBeSynchronized)
//
// let teams = Array(tournamentStore.teamRegistrations)
// for team in teams {
// team.deleteDependencies(store: tournamentStore, shouldBeSynchronized: shouldBeSynchronized)
// }
// tournamentStore.teamRegistrations.deleteDependencies(teams, shouldBeSynchronized: shouldBeSynchronized)
//
// let groups = Array(tournamentStore.groupStages)
// for group in groups {
// group.deleteDependencies(store: tournamentStore, shouldBeSynchronized: shouldBeSynchronized)
// }
// tournamentStore.groupStages.deleteDependencies(groups, shouldBeSynchronized: shouldBeSynchronized)
//
// let rounds = Array(tournamentStore.rounds)
// for round in rounds {
// round.deleteDependencies(store: tournamentStore, shouldBeSynchronized: shouldBeSynchronized)
// }
// tournamentStore.rounds.deleteDependencies(rounds, shouldBeSynchronized: shouldBeSynchronized)
//
// tournamentStore.matchSchedulers.deleteDependencies(self._matchSchedulers())
}
// MARK: - Computed Dependencies
public func unsortedTeams() -> [TeamRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] }
return Array(tournamentStore.teamRegistrations)
}
public func unsortedTeamsCount() -> Int {
return self.tournamentStore?.teamRegistrations.count ?? 0
}
public func deleteGroupStage(_ groupStage: GroupStage) {
groupStage.removeAllTeams()
let index = groupStage.index
self.tournamentStore?.groupStages.delete(instance: groupStage)
self.groupStageCount -= 1
let groupStages = self.groupStages()
groupStages.filter({ $0.index > index }).forEach { gs in
gs.index -= 1
}
self.tournamentStore?.groupStages.addOrUpdate(contentOfs: groupStages)
}
public func addGroupStage() {
let groupStage = GroupStage(tournament: id, index: groupStageCount, size: teamsPerGroupStage, format: groupStageFormat)
self.tournamentStore?.groupStages.addOrUpdate(instance: groupStage)
groupStage.buildMatches(keepExistingMatches: false)
self.groupStageCount += 1
}
public func groupStages(atStep step: Int = 0) -> [GroupStage] {
guard let tournamentStore = self.tournamentStore else { return [] }
let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.step == step }
return groupStages.sorted(by: \.index)
}
public func hasGroupeStages() -> Bool {
if groupStageCount > 0 { return true }
guard let tournamentStore = self.tournamentStore else { return false }
return tournamentStore.groupStages.isEmpty == false
}
public func allGroupStages() -> [GroupStage] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.groupStages.sorted(by: \GroupStage.computedOrder)
}
public func allRounds() -> [Round] {
guard let tournamentStore = self.tournamentStore else { return [] }
return Array(tournamentStore.rounds)
}
// MARK: -
public enum State {
case initial
case build
case running
case canceled
case finished
}
public func eventLabel() -> String {
if let event = eventObject(), let name = event.name {
return name
} else {
return ""
}
}
public func publishedTournamentDate() -> Date {
return min(creationDate.tomorrowAtNine, startDate)
}
public func publishedProgDate() -> Date {
return self.startDate
}
public func publishedTeamsDate() -> Date {
return self.startDate
}
public func canBePublished() -> Bool {
switch state() {
case .build, .finished, .running:
return unsortedTeams().count > 3
default:
return false
}
}
public func isTournamentPublished() -> Bool {
return (Date() >= publishedTournamentDate()) || publishTournament
}
public func isProgPublished() -> Bool {
return (Date() >= publishedProgDate()) || publishProg
}
public func areTeamsPublished() -> Bool {
return Date() >= startDate || publishTeams
}
public func areSummonsPublished() -> Bool {
return Date() >= startDate || publishSummons
}
fileprivate func _publishedDateFromMatches(_ matches: [Match]) -> Date? {
let startDates: [Date] = matches.compactMap { $0.startDate }
let sortedDates: [Date] = startDates.sorted()
if let first: Date = sortedDates.first?.atEightAM() {
if first.isEarlierThan(startDate) {
return startDate
} else {
return first
}
} else {
return startDate
}
}
public func publishedGroupStagesDate() -> Date? {
let matches: [Match] = self.groupStages().flatMap { $0.playedMatches() }
return self._publishedDateFromMatches(matches)
}
public func areGroupStagesPublished() -> Bool {
if publishGroupStages { return true }
if let publishedGroupStagesDate = publishedGroupStagesDate() {
return Date() >= publishedGroupStagesDate
} else {
return false
}
}
public func publishedBracketsDate() -> Date? {
let matches: [Match] = self.rounds().flatMap { $0.playedMatches() }
return self._publishedDateFromMatches(matches)
}
public func areBracketsPublished() -> Bool {
if publishBrackets { return true }
if let publishedBracketsDate = publishedBracketsDate() {
return Date() >= publishedBracketsDate
} else {
return false
}
}
public func shareURL(_ pageLink: PageLink = .matches) -> URL? {
if pageLink == .clubBroadcast {
let club = club()
// print("club", club)
// print("club broadcast code", club?.broadcastCode)
if let club, let broadcastCode = club.broadcastCode {
return URLs.main.url.appending(path: "c/\(broadcastCode)")
} else {
return nil
}
}
return URLs.main.url.appending(path: "tournament/\(id)").appending(path: pageLink.path)
}
public func courtUsed(runningMatches: [Match]) -> [Int] {
#if _DEBUGING_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func courtUsed()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return Set(runningMatches.compactMap { $0.courtIndex }).sorted()
}
public func hasStarted() -> Bool {
return startDate <= Date()
}
public func eventObject() -> Event? {
guard let event else { return nil }
return Store.main.findById(event)
}
public func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> String {
let _selectedSortedTeams = selectedSortedTeams()
let selectedSortedTeams = _selectedSortedTeams + waitingListSortedTeams(selectedSortedTeams: _selectedSortedTeams)
switch exportFormat {
case .rawText:
let waitingList = waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true)
var stats = [String]()
if type == .payment, isAnimation(), minimumPlayerPerTeam == 1 {
stats += ["\(self.selectedPlayers().count.formatted()) personnes"]
} else {
stats += [selectedSortedTeams.count.formatted() + " équipes"]
}
return (stats + selectedSortedTeams.compactMap { $0.pasteData(exportFormat, type: type) } + (waitingList.isEmpty == false ? ["Liste d'attente"] : []) + waitingList.compactMap { $0.pasteData(exportFormat, type: type) }).joined(separator: exportFormat.newLineSeparator(1))
case .csv:
let headers = ["", "Nom Prénom", "rang", "Nom Prénom", "rang", "poids", "Paire"].joined(separator: exportFormat.separator())
var teamPaste = [headers]
for (index, team) in selectedSortedTeams.enumerated() {
var teamData = team.pasteData(exportFormat, type: type, index + 1)
teamData.append(exportFormat.separator())
teamData.append(team.teamLastNames().joined(separator: " / "))
teamPaste.append(teamData)
}
return teamPaste.joined(separator: exportFormat.newLineSeparator())
}
}
public func club() -> Club? {
return eventObject()?.clubObject()
}
public func locationLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if let club = club() {
switch displayStyle {
case .wide, .title:
return club.name
case .short:
return club.acronym
}
} else {
return ""
}
}
public func hasEnded() -> Bool {
return endDate != nil
}
public func state() -> State {
if self.isCanceled == true {
return .canceled
}
if self.hasEnded() { return .finished }
let isBuild = (groupStageCount > 0 && groupStages().isEmpty == false)
|| rounds().isEmpty == false
if isBuild && startDate <= Date() { return .running }
if isBuild {
return .build
}
return .initial
}
public func seededTeams() -> [TeamRegistration] {
return selectedSortedTeams().filter({ $0.bracketPosition != nil && $0.groupStagePosition == nil })
}
public func groupStageTeams() -> [TeamRegistration] {
return selectedSortedTeams().filter({ $0.groupStagePosition != nil })
}
public func groupStageSpots() -> Int {
return groupStages().map { $0.size }.reduce(0,+)
}
public func seeds() -> [TeamRegistration] {
let selectedSortedTeams = selectedSortedTeams()
let seeds = max(selectedSortedTeams.count - groupStageSpots() , 0)
return Array(selectedSortedTeams.prefix(seeds))
}
public func availableSeeds() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func availableSeeds()", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return seeds().filter { $0.isSeedable() }
}
public func lastSeedRound() -> Int {
if let last = seeds().filter({ $0.bracketPosition != nil }).last {
return RoundRule.roundIndex(fromMatchIndex: last.bracketPosition! / 2)
} else {
return 0
}
}
public func getRound(atRoundIndex roundIndex: Int) -> Round? {
return self.tournamentStore?.rounds.first(where: { $0.index == roundIndex })
// return Store.main.filter(isIncluded: { $0.tournament == id && $0.index == roundIndex }).first
}
public func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] {
return getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.isEmpty() } ?? []
}
public func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] {
return getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.hasSpaceLeft() } ?? []
}
public func availableSeedGroups(includeAll: Bool = false) -> [SeedInterval] {
let seeds = seeds()
var availableSeedGroup = Set<SeedInterval>()
for (index, seed) in seeds.enumerated() {
if seed.isSeedable(), let seedGroup = seedGroup(for: index) {
if includeAll {
if let chunks = seedGroup.chunks() {
chunksBy(in: chunks, availableSeedGroup: &availableSeedGroup)
}
} else {
availableSeedGroup.insert(seedGroup)
}
}
}
return availableSeedGroup.sorted(by: <)
}
public func generateSeedGroups(base: Int, teamCount: Int) -> [SeedInterval] {
let start = base + 1
let root = SeedInterval(first: start, last: start + teamCount - 1)
var groups: [SeedInterval] = []
func split(interval: SeedInterval) {
groups.append(interval)
if let chunks = interval.chunks() {
for chunk in chunks {
split(interval: chunk)
}
}
}
split(interval: root)
return groups.sorted(by: <)
}
public func chunksBy(in chunks: [SeedInterval], availableSeedGroup: inout Set<SeedInterval>) {
chunks.forEach { chunk in
availableSeedGroup.insert(chunk)
if let moreChunk = chunk.chunks() {
self.chunksBy(in: moreChunk, availableSeedGroup: &availableSeedGroup)
}
}
}
public func seedGroup(for alreadySetupSeeds: Int) -> SeedInterval? {
switch alreadySetupSeeds {
case 0...1:
return SeedInterval(first: 1, last: 2)
case 2...3:
return SeedInterval(first: 3, last: 4)
case 4...7:
return SeedInterval(first: 5, last: 8)
case 8...15:
return SeedInterval(first: 9, last: 16)
case 16...23:
return SeedInterval(first: 17, last: 24)
case 24...31:
return SeedInterval(first: 25, last: 32)
default:
let pow = Int(pow(2.0, ceil(log2(Double(alreadySetupSeeds)))))
return SeedInterval(first: pow + 1, last: pow * 2)
}
}
public func availableSeedGroup() -> SeedInterval? {
let seeds = seeds()
if let firstIndex = seeds.firstIndex(where: { $0.isSeedable() }) {
guard let seedGroup = seedGroup(for: firstIndex) else { return nil }
return seedGroup
}
return nil
}
public func randomSeed(fromSeedGroup seedGroup: SeedInterval) -> TeamRegistration? {
let availableSeeds = seeds(inSeedGroup: seedGroup)
return availableSeeds.randomElement()
}
public func seeds(inSeedGroup seedGroup: SeedInterval) -> [TeamRegistration] {
let availableSeedInSeedGroup = (seedGroup.last - seedGroup.first) + 1
let availableSeeds = seeds().dropFirst(seedGroup.first - 1).prefix(availableSeedInSeedGroup).filter({ $0.isSeedable() })
return availableSeeds
}
public func seedGroupAvailable(atRoundIndex roundIndex: Int) -> SeedInterval? {
if let availableSeedGroup = availableSeedGroup() {
return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: availableSeedGroup)
} else {
return nil
}
}
public func seedGroupAvailable(atRoundIndex roundIndex: Int, availableSeedGroup: SeedInterval) -> SeedInterval? {
let fullLeftSeeds = availableSeeds()
if fullLeftSeeds.isEmpty == false && roundIndex >= lastSeedRound() {
if availableSeedGroup == SeedInterval(first: 1, last: 2) { return availableSeedGroup }
let availableSeeds = seeds(inSeedGroup: availableSeedGroup)
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
let targetSpots = availableSeedSpot.isEmpty ? availableSeedOpponentSpot.count : availableSeedSpot.count
if availableSeedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 {
return availableSeedGroup
}
if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count {
return availableSeedGroup
} else if availableSeeds.count == availableSeedOpponentSpot.count && availableSeedGroup.count == availableSeedOpponentSpot.count {
return availableSeedGroup
} else if let chunks = availableSeedGroup.chunks() {
let seededTeamsCount = self.seededTeams().count
if let chunk = chunks.first(where: { seedInterval in
return seedInterval.first == seededTeamsCount
}) {
return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk)
} else if fullLeftSeeds.count > 1, targetSpots > 1, fullLeftSeeds.count >= targetSpots {
let currentSeeds = seeds()
if let firstIndex = currentSeeds.firstIndex(where: { $0.isSeedable() }) {
if firstIndex < seededTeamsCount {
return nil
} else {
let sg = SeedInterval(first: seededTeamsCount + 1, last: seededTeamsCount + targetSpots)
let futureAvailableSeeds = self.seeds(inSeedGroup: sg)
if futureAvailableSeeds.count == targetSpots {
return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: sg)
} else {
return nil
}
}
}
}
}
}
return nil
}
public func setSeeds(inRoundIndex roundIndex: Int, inSeedGroup seedGroup: SeedInterval) {
if seedGroup == SeedInterval(first: 1, last: 2) {
let seeds = seeds()
if let matches = getRound(atRoundIndex: roundIndex)?.playedMatches() {
if let lastMatch = matches.last {
seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, slot: .two, opposingSeeding: false)
}
if let firstMatch = matches.first {
seeds.prefix(2).dropFirst().first?.setSeedPosition(inSpot: firstMatch, slot: .one, opposingSeeding: false)
}
}
} else {
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
let availableSeeds = seeds(inSeedGroup: seedGroup)
if seedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 {
var spots = [Match]()
spots.append(availableSeedSpot[1])
spots.append(availableSeedSpot[4])
spots = spots.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false)
}
// } else if seedGroup == SeedInterval(first: 5, last: 6), availableSeedSpot.count == 4 {
// var spots = [Match]()
// spots.append(availableSeedSpot[1])
// spots.append(availableSeedSpot[2])
// spots = spots.shuffled()
// for (index, seed) in availableSeeds.enumerated() {
// seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false)
// }
} else {
if availableSeeds.count <= availableSeedSpot.count {
let spots = availableSeedSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false)
}
} else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) {
let spots = availableSeedOpponentSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true)
}
} else if let chunks = seedGroup.chunks() {
if let chunk = chunks.first(where: { seedInterval in
seedInterval.first >= self.seededTeams().count
}) {
setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk)
}
}
}
}
}
public func inscriptionClosed() -> Bool {
closedRegistrationDate != nil
}
public func getActiveGroupStage(atStep step: Int = 0) -> GroupStage? {
let groupStages = groupStages(atStep: step)
return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first
}
public func matchesWithSpace() -> [Match] {
getActiveRound()?.playedMatches().filter({ $0.hasSpaceLeft() }) ?? []
}
public func getActiveRound(withSeeds: Bool = false) -> Round? {
let rounds: [Round] = self.rounds()
for round in rounds {
let playedMatches = round.playedMatches()
// Optimization: If no matches have started in this round, return nil immediately
if !playedMatches.contains(where: { $0.hasStarted() }) {
return round
}
if playedMatches.contains(where: { $0.hasStarted() && !$0.hasEnded() }) {
if withSeeds {
if !round.seeds().isEmpty {
return round
} else {
return nil
}
} else {
return round
}
}
}
return nil
}
public func getActiveRoundAndStatus() -> (Round, String)? {
let rounds: [Round] = self.rounds()
for round in rounds {
let playedMatches = round.playedMatches()
// Optimization: If no matches have started in this round, return nil immediately
if !playedMatches.contains(where: { $0.hasStarted() }) {
return (round, round.roundStatus(playedMatches: playedMatches))
}
if playedMatches.contains(where: { $0.hasStarted() && !$0.hasEnded() }) {
return (round, round.roundStatus(playedMatches: playedMatches))
}
}
return nil
}
public func getPlayedMatchDateIntervals(in event: Event) -> [DateInterval] {
let allMatches: [Match] = self.allMatches().filter { $0.courtIndex != nil && $0.startDate != nil }
return allMatches.map { match in
DateInterval(event: event.id, courtIndex: match.courtIndex!, startDate: match.startDate!, endDate: match.estimatedEndDate(additionalEstimationDuration)!)
}
}
public func allRoundMatches() -> [Match] {
return allRounds().flatMap { $0._matches() }
}
public func allMatches() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter { $0.disabled == false }
}
public func _allMatchesIncludingDisabled() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return Array(tournamentStore.matches)
}
public func rounds() -> [Round] {
guard let tournamentStore = self.tournamentStore else { return [] }
let rounds: [Round] = tournamentStore.rounds.filter { $0.isUpperBracket() }
return rounds.sorted { $0.index > $1.index }
}
public func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] {
let teams = selectedSortedTeams
return teams + waitingListTeams(in: teams, includingWalkOuts: true)
}
public func waitingListSortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] {
let teams = selectedSortedTeams
return waitingListTeams(in: teams, includingWalkOuts: false)
}
public func allTeamsWithoutWalkOut() -> [TeamRegistration] {
return unsortedTeams().filter({ !$0.walkOut })
}
public func selectedSortedTeams() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
var _sortedTeams : [TeamRegistration] = []
var _teams = unsortedTeams().filter({ $0.isOutOfTournament() == false })
if let closedRegistrationDate {
_teams = _teams.filter({ team in
if let registrationDate = team.registrationDate {
return registrationDate <= closedRegistrationDate
} else {
return true
}
})
}
let defaultSorting : [MySortDescriptor<TeamRegistration>] = _defaultSorting()
let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false }.prefix(teamCount).sorted(using: [.keyPath(\.initialWeight), .keyPath(\.uniqueRandomIndex), .keyPath(\.id)], order: .ascending)
let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending)
let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending)
let groupStageSpots: Int = self.groupStageSpots()
var bracketSeeds: Int = teamCount - groupStageSpots - wcBracket.count
var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count
if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
if bracketSeeds < 0 { bracketSeeds = 0 }
let clubName = self.clubName
if prioritizeClubMembers {
var bracketTeams: [TeamRegistration] = []
bracketTeams.append(contentsOf: _completeTeams.filter { $0.hasMemberOfClub(clubName) })
let others: [TeamRegistration] = _completeTeams.filter { $0.hasMemberOfClub(clubName) == false }
let sortedOthers: [TeamRegistration] = others.sorted(using: defaultSorting, order: .ascending)
bracketTeams.append(contentsOf: sortedOthers)
bracketTeams = bracketTeams
.prefix(bracketSeeds)
.sorted(using: _currentSelectionSorting, order: .ascending)
bracketTeams.append(contentsOf: wcBracket)
// let bracketTeams: [TeamRegistration] = (_completeTeams.filter { $0.hasMemberOfClub(clubName) } + _completeTeams.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket
let groupStageTeamsNoFiltering = Set(_completeTeams).subtracting(bracketTeams)
let groupStageTeams = (groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) } + groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage
_sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending)
} else {
let bracketTeams = _completeTeams.prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket
let groupStageTeams = Set(_completeTeams).subtracting(bracketTeams).sorted(using: defaultSorting, order: .ascending).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage
_sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending)
}
return _sortedTeams
}
public func waitingListTeams(in teams: [TeamRegistration], includingWalkOuts: Bool) -> [TeamRegistration] {
let waitingList = Set(unsortedTeams()).subtracting(teams)
let waitings = waitingList.filter { $0.isOutOfTournament() == false }.sorted(using: _defaultSorting(), order: .ascending)
let walkOuts = waitingList.filter { $0.walkOut == true }.sorted(using: _defaultSorting(), order: .ascending)
if includingWalkOuts {
return waitings + walkOuts
} else {
return waitings
}
}
public func bracketCut(teamCount: Int, groupStageCut: Int) -> Int {
return self.teamCount - groupStageCut
}
public func groupStageCut() -> Int {
return groupStageSpots()
}
public func cutLabel(index: Int, teamCount: Int?) -> String {
let _teamCount = teamCount ?? selectedSortedTeams().count
let groupStageCut = groupStageCut()
let bracketCut = bracketCut(teamCount: _teamCount, groupStageCut: groupStageCut)
if index < bracketCut {
return "Tableau"
} else if index - bracketCut < groupStageCut && _teamCount > 0 {
return "Poule"
} else {
return "Attente"
}
}
public func unsortedTeamsWithoutWO() -> [TeamRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.teamRegistrations.filter { $0.isOutOfTournament() == false }
}
public func walkoutTeams() -> [TeamRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.teamRegistrations.filter { $0.walkOut == true }
// return Store.main.filter { $0.tournament == self.id && $0.walkOut == true }
}
public func duplicates(in players: [PlayerRegistration]) -> [PlayerRegistration] {
var duplicates = [PlayerRegistration]()
Set(players.compactMap({ $0.licenceId })).forEach { licenceId in
let found = players.filter({ $0.licenceId?.strippedLicense == licenceId.strippedLicense })
if found.count > 1 {
duplicates.append(found.first!)
}
}
return duplicates
}
public func unsortedPlayers() -> [PlayerRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] }
return Array(tournamentStore.playerRegistrations)
}
public func selectedPlayers() -> [PlayerRegistration] {
return self.selectedSortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.computedRank)
}
public func paidSelectedPlayers(type: PlayerPaymentType) -> Double? {
if let entryFee {
let flat = self.selectedSortedTeams().flatMap { $0.unsortedPlayers() }
let count = flat.filter { $0.paymentType == type }.count
return Double(count) * entryFee
} else {
return nil
}
}
public func players() -> [PlayerRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.playerRegistrations.sorted(by: \.computedRank)
}
public func unrankValue(for malePlayer: Bool) -> Int? {
switch tournamentCategory {
case .unlisted:
return nil
case .men:
return maleUnrankedValue
case .women:
return femaleUnrankedValue
case .mix:
return malePlayer ? maleUnrankedValue : femaleUnrankedValue
}
}
//todo
public var clubName: String? {
return self.eventObject()?.clubObject()?.name
}
//todo
public func significantPlayerCount() -> Int {
return minimumPlayerPerTeam
}
public func licenseYearValidity() -> Int {
if startDate.get(.month) > 8 {
return startDate.get(.year) + 1
} else {
return startDate.get(.year)
}
}
public func maximumCourtsPerGroupSage() -> Int {
if teamsPerGroupStage > 1 {
return min(teamsPerGroupStage / 2, courtCount)
} else {
return max(1, courtCount)
}
}
public func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration, expectedSummonDate: Date? = nil) -> Bool {
guard let summonDate = team.callDate else { return true }
let expectedSummonDate : Date? = team.expectedSummonDate() ?? expectedSummonDate
guard let expectedSummonDate else { return true }
return Calendar.current.compare(summonDate, to: expectedSummonDate, toGranularity: .minute) != ComparisonResult.orderedSame
}
public func groupStagesMatches(atStep step: Int = 0) -> [Match] {
return groupStages(atStep: step).flatMap({ $0._matches() })
// return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) })
}
static let defaultSorting : [MySortDescriptor<Match>] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.computedOrder)]
public static func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return allMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(using: defaultSorting, order: .ascending)
}
public static func runningMatches(_ allMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending)
}
public static func readyMatches(_ allMatches: [Match], runningMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
let playingTeams = runningMatches.flatMap({ $0.teams() }).map({ $0.id })
return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false && $0.containsTeamIds(playingTeams) == false }).sorted(using: defaultSorting, order: .ascending)
}
public static func matchesLeft(_ allMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending)
}
public func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? {
guard let seedIndex else { return nil }
return selectedSortedTeams()[safe: seedIndex]?.callDate
}
public static func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
if let limit {
return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(limit))
} else {
return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed())
}
}
private func _removeStrings(from dictionary: inout [Int: [String]], stringsToRemove: [String]) {
for key in dictionary.keys {
if var stringArray = dictionary[key] {
// Remove all instances of each string in stringsToRemove
stringArray.removeAll { stringsToRemove.contains($0) }
dictionary[key] = stringArray
}
}
}
public func finalRanking() async -> [Int: [String]] {
var teams: [Int: [String]] = [:]
var ids: Set<String> = Set<String>()
let rounds = rounds()
let lastStep = lastStep()
if rounds.isEmpty, lastStep > 0 {
let groupStages = groupStages(atStep: lastStep)
for groupStage in groupStages {
let groupStageTeams = groupStage.teams(true)
for teamIndex in 0..<groupStageTeams.count {
teams[groupStage.index * groupStage.size + 1 + teamIndex] = [groupStageTeams[teamIndex].id]
}
}
} else {
let final = rounds.last?.playedMatches().last
if let winner = final?.winningTeamId {
teams[1] = [winner]
ids.insert(winner)
}
if let finalist = final?.losingTeamId {
teams[2] = [finalist]
ids.insert(finalist)
}
let others: [Round] = rounds.flatMap { round in
let losers = round.losers()
let minimumFinalPosition = round.seedInterval()?.last ?? teamCount
if teams[minimumFinalPosition] == nil {
teams[minimumFinalPosition] = losers.map { $0.id }
} else {
teams[minimumFinalPosition]?.append(contentsOf: losers.map { $0.id })
}
print("round", round.roundTitle())
let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false }
print(rounds.count, rounds.map { $0.roundTitle() })
return rounds
}.compactMap({ $0 })
others.forEach { round in
print("round", round.roundTitle())
if let interval = round.seedInterval() {
print("interval", interval.localizedInterval())
let playedMatches = round.playedMatches().filter { $0.disabled == false || $0.isReady() }
print("playedMatches", playedMatches.count)
let winners = playedMatches.compactMap({ $0.winningTeamId }).filter({ ids.contains($0) == false })
print("winners", winners.count)
let losers = playedMatches.compactMap({ $0.losingTeamId }).filter({ ids.contains($0) == false })
print("losers", losers.count)
if winners.isEmpty {
let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false })
if disabledIds.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: disabledIds)
teams[interval.last] = disabledIds
let teamNames : [String] = disabledIds.compactMap {
let t : TeamRegistration? = self.tournamentStore?.teamRegistrations.findById($0)
return t
}.map { $0.canonicalName }
print("winners.isEmpty", "\(interval.last) : ", teamNames)
disabledIds.forEach {
ids.insert($0)
}
}
} else {
if winners.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: winners)
teams[interval.first + winners.count - 1] = winners
let teamNames : [String] = winners.compactMap {
let t: TeamRegistration? = tournamentStore?.teamRegistrations.findById($0)
return t
}.map { $0.canonicalName }
print("winners", "\(interval.last + winners.count - 1) : ", teamNames)
winners.forEach { ids.insert($0) }
}
if losers.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: losers)
teams[interval.first + winners.count] = losers
let loserTeamNames : [String] = losers.compactMap {
let t: TeamRegistration? = tournamentStore?.teamRegistrations.findById($0)
return t
}.map { $0.canonicalName }
print("losers", "\(interval.first + winners.count) : ", loserTeamNames)
losers.forEach { ids.insert($0) }
}
}
}
}
if let groupStageLoserBracketPlayedMatches = groupStageLoserBracket()?.playedMatches() {
groupStageLoserBracketPlayedMatches.forEach({ match in
if match.hasEnded() {
let sameMatchIndexCount = groupStageLoserBracketPlayedMatches.filter({ $0.index == match.index }).count
teams.setOrAppend(match.winningTeamId, at: match.index)
teams.setOrAppend(match.losingTeamId, at: match.index + sameMatchIndexCount)
}
})
}
let groupStages = groupStages()
var baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified
if disableRankingFederalRuling == false, baseRank > 0 {
baseRank += qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified - 1
}
let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 }))
groupStages.forEach { groupStage in
let groupStageTeams = groupStage.teams(true)
for (index, team) in groupStageTeams.enumerated() {
if groupStage.hasEnded() {
if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false {
let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0)
let _index = baseRank + groupStageWidth + 1 - (index > qualifiedPerGroupStage ? groupStageAdditionalQualified : 0)
print("finalRanking", team.teamLabel() , _index, baseRank, groupStageWidth)
if let existingTeams = teams[_index] {
teams[_index] = existingTeams + [team.id]
} else {
teams[_index] = [team.id]
}
}
}
}
}
}
return teams
}
public func setRankings(assimilationLevel: TournamentLevel? = nil, finalRanks: [Int: [String]]) async -> [Int: [TeamRegistration]] {
guard let tournamentStore = self.tournamentStore else { return [:] }
let tournamentLevel = assimilationLevel ?? tournamentLevel
var rankings: [Int: [TeamRegistration]] = [:]
finalRanks.keys.sorted().forEach { rank in
if let rankedTeamIds = finalRanks[rank] {
let teams: [TeamRegistration] = rankedTeamIds.compactMap { tournamentStore.teamRegistrations.findById($0) }
rankings[rank] = teams
}
}
rankings.keys.sorted().forEach { rank in
if let rankedTeams = rankings[rank] {
rankedTeams.forEach { team in
team.finalRanking = rank
team.pointsEarned = isAnimation() ? nil : tournamentLevel.points(for: rank - 1, count: teamCount)
}
}
}
if rankings.isEmpty == false {
let teams = unsortedTeams()
self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
if self.publishRankings == false {
self.publishRankings = true
}
DataStore.shared.tournaments.addOrUpdate(instance: self)
}
return rankings
}
public func refreshPointsEarned(assimilationLevel: TournamentLevel? = nil) {
guard let tournamentStore = self.tournamentStore else { return }
let tournamentLevel = assimilationLevel ?? tournamentLevel
let unsortedTeams = unsortedTeams()
unsortedTeams.forEach { team in
if let finalRanking = team.finalRanking {
team.pointsEarned = isAnimation() ? nil : tournamentLevel.points(for: finalRanking - 1, count: teamCount)
}
}
tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams)
}
public func lockRegistration() {
closedRegistrationDate = Date()
let count = selectedSortedTeams().count
if teamCount != count {
teamCount = count
}
let teams = unsortedTeams()
teams.forEach { team in
team.lockedWeight = team.weight
}
self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
}
public func unlockRegistration() {
closedRegistrationDate = nil
let teams = unsortedTeams()
teams.forEach { team in
team.lockedWeight = nil
}
self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
}
public func updateWeights() {
let teams = self.unsortedTeams()
teams.forEach { team in
let players = team.unsortedPlayers()
players.forEach { $0.setComputedRank(in: self) }
team.setWeight(from: players, inTournamentCategory: tournamentCategory)
self.tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players)
}
self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
}
public func missingUnrankedValue() -> Bool {
return maleUnrankedValue == nil || femaleUnrankedValue == nil
}
public func findTeam(_ players: [PlayerRegistration]) -> TeamRegistration? {
return unsortedTeams().first(where: { $0.includes(players: players) })
}
public func tournamentTitle(_ displayStyle: DisplayStyle = .wide, hideSenior: Bool = false) -> String {
if tournamentLevel == .unlisted {
if let name {
return name
} else if displayStyle == .title {
return tournamentLevel.localizedLevelLabel(.title)
}
}
let displayStyleCategory = hideSenior ? .short : displayStyle
var levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedCategoryLabel(displayStyle, ageCategory: federalAgeCategory)]
if displayStyle == .short {
levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle) + tournamentCategory.localizedCategoryLabel(displayStyle, ageCategory: federalAgeCategory)]
}
let array = levelCategory + [federalTournamentAge.localizedFederalAgeLabel(displayStyleCategory)]
let title: String = array.filter({ $0.isEmpty == false }).joined(separator: " ")
if displayStyle == .wide, let name {
return [title, name].joined(separator: " - ")
} else {
return title
}
}
public func localizedTournamentType() -> String {
switch tournamentLevel {
case .unlisted, .championship:
return tournamentLevel.localizedLevelLabel(.short)
default:
return tournamentLevel.localizedLevelLabel(.short) + tournamentCategory.localizedCategoryLabel(.short, ageCategory: federalAgeCategory)
}
}
public func hideWeight() -> Bool {
return hideTeamsWeight
}
public func isAnimation() -> Bool {
federalLevelCategory.isAnimation()
}
public func subtitle(_ displayStyle: DisplayStyle = .wide) -> String {
return name ?? ""
}
public func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String {
startDate.formattedDate(displayStyle)
}
public func qualifiedFromGroupStage() -> Int {
return groupStageCount * qualifiedPerGroupStage
}
public func availableQualifiedTeams() -> [TeamRegistration] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func availableQualifiedTeams()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return unsortedTeams().filter({ $0.qualified && $0.bracketPosition == nil })
}
public func qualifiedTeams() -> [TeamRegistration] {
return unsortedTeams().filter({ $0.qualified })
}
public func moreQualifiedToDraw() -> Int {
return max((qualifiedFromGroupStage() + groupStageAdditionalQualified) - qualifiedTeams().count, 0)
}
public func missingQualifiedFromGroupStages() -> [TeamRegistration] {
if groupStageAdditionalQualified > 0 && groupStagesAreOver() {
return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in
groupStage.teams(true)[safe: qualifiedPerGroupStage]
}
.filter({ $0.qualified == false })
} else {
return []
}
}
public func groupStagesAreOver(atStep: Int = 0) -> Bool {
let groupStages = groupStages(atStep: atStep)
guard groupStages.isEmpty == false else {
return true
}
return groupStages.allSatisfy({ $0.hasEnded() })
//return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
}
public func groupStageLoserBracketAreOver() -> Bool {
guard let groupStageLoserBracket = groupStageLoserBracket() else {
return true
}
return groupStageLoserBracket.hasEnded()
}
fileprivate func _paymentMethodMessage() -> String? {
return DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods
}
public var entryFeeMessage: String {
if let entryFee {
let message: String = "Inscription : \(entryFee.formatted(.currency(code: self.defaultCurrency()))) par joueur."
return [message, self._paymentMethodMessage()].compactMap { $0 }.joined(separator: "\n")
} else {
return "Inscription : gratuite."
}
}
public func umpireMail() -> [String]? {
return [umpireCustomMail ?? DataStore.shared.user.email]
}
public func earnings() -> Double {
return selectedPlayers().compactMap { $0.paidAmount(self) }.reduce(0.0, +)
}
public func remainingAmount() -> Double {
return selectedPlayers().compactMap { $0.remainingAmount(self) }.reduce(0.0, +)
}
public func totalIncome() -> Double {
if let entryFee {
return Double(teamCount) * entryFee * 2.0
} else {
return 0.0
}
}
public func paidCompletion() -> Double {
let selectedPlayers = selectedPlayers()
if selectedPlayers.isEmpty { return 0 }
return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count)
}
public func presenceStatus() -> Double {
let selectedPlayers = selectedPlayers()
if selectedPlayers.isEmpty { return 0 }
return Double(selectedPlayers.filter { $0.hasArrived }.count) / Double(selectedPlayers.count)
}
public typealias TournamentStatus = (label:String, completion: String)
public func cashierStatus() async -> TournamentStatus {
let selectedPlayers = selectedPlayers()
var filteredPlayers = [PlayerRegistration]()
var wording = ""
if isFree() {
wording = "présent"
filteredPlayers = selectedPlayers.filter({ $0.hasArrived })
} else {
wording = "encaissé"
filteredPlayers = selectedPlayers.filter({ $0.hasPaid() })
}
// let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés"
let label = "\(filteredPlayers.count.formatted()) / \(selectedPlayers.count.formatted()) joueurs \(wording)\(filteredPlayers.count.pluralSuffix)"
let completion = (Double(filteredPlayers.count) / Double(selectedPlayers.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
}
public func scheduleStatus() async -> TournamentStatus {
let allMatches = allMatches()
let ready = allMatches.filter({ $0.startDate != nil })
// let label = ready.count.formatted() + " / " + allMatches.count.formatted() + " matchs programmés"
let label = "\(ready.count.formatted()) / \(allMatches.count.formatted()) matchs programmés"
let completion = (Double(ready.count) / Double(allMatches.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
}
public func callStatus() async -> TournamentStatus {
let selectedSortedTeams = selectedSortedTeams()
let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false }
let justCalled = selectedSortedTeams.filter { $0.called() }
let label = "\(justCalled.count.formatted()) / \(selectedSortedTeams.count.formatted()) (\(called.count.formatted()) au bon horaire)"
let completion = (Double(called.count) / Double(selectedSortedTeams.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
}
public func confirmedSummonStatus() async -> TournamentStatus {
let selectedSortedTeams = selectedSortedTeams()
let called = selectedSortedTeams.filter { $0.confirmationDate != nil }
let label = "\(called.count.formatted()) / \(selectedSortedTeams.count.formatted()) confirmées"
let completion = (Double(called.count) / Double(selectedSortedTeams.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
}
public func bracketStatus() async -> (status: String, description: String?, cut: TeamRegistration.TeamRange?) {
let availableSeeds = availableSeeds()
var description: String? = nil
if availableSeeds.isEmpty == false {
description = "placer \(availableSeeds.count) équipe\(availableSeeds.count.pluralSuffix)"
}
if description == nil {
let availableQualifiedTeams = availableQualifiedTeams()
if availableQualifiedTeams.isEmpty == false {
description = "placer \(availableQualifiedTeams.count) qualifié" + availableQualifiedTeams.count.pluralSuffix
}
}
var cut: TeamRegistration.TeamRange? = nil
if description == nil && isAnimation() == false {
cut = TeamRegistration.TeamRange(availableSeeds.first, availableSeeds.last)
}
if let roundAndStatus = getActiveRoundAndStatus() {
return ([roundAndStatus.0.roundTitle(.short), roundAndStatus.1].joined(separator: " ").lowercased(), description, cut)
} else {
return ("", description, nil)
}
}
public func groupStageStatus() async -> (status: String, cut: TeamRegistration.TeamRange?) {
let groupStageTeams = groupStageTeams()
let groupStageTeamsCount = groupStageTeams.count
if groupStageTeamsCount == 0 || groupStageTeamsCount != groupStageSpots() {
return ("à compléter", nil)
}
let cut : TeamRegistration.TeamRange? = isAnimation() ? nil : TeamRegistration.TeamRange(groupStageTeams.first, groupStageTeams.last)
if groupStagesAreOver() { return ("terminées", cut) }
let groupStages = groupStages()
let runningGroupStages = groupStages.filter({ $0.isRunning() })
if runningGroupStages.isEmpty {
let ongoingGroupStages = runningGroupStages.filter({ $0.hasStarted() && $0.hasEnded() == false })
if ongoingGroupStages.isEmpty == false {
return ("Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours", cut)
}
return (groupStages.count.formatted() + " poule" + groupStages.count.pluralSuffix, cut)
} else {
return ("Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours", cut)
}
}
public func settingsDescriptionLocalizedLabel() -> String {
[courtCount.formatted() + " piste\(courtCount.pluralSuffix)", entryFeeMessage].joined(separator: ", ")
}
public func structureDescriptionLocalizedLabel() -> String {
let groupStageLabel: String? = groupStageCount > 0 ? groupStageCount.formatted() + " poule\(groupStageCount.pluralSuffix)" : nil
return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ")
}
public func deleteAndBuildEverything(preset: PadelTournamentStructurePreset = .manual) {
resetBracketPosition()
deleteStructure()
deleteGroupStages()
switch preset {
case .doubleGroupStage:
buildGroupStages()
addNewGroupStageStep()
qualifiedPerGroupStage = 0
groupStageAdditionalQualified = 0
default:
buildGroupStages()
buildBracket()
}
}
public func addEmptyTeamRegistration(_ count: Int) {
guard let tournamentStore = self.tournamentStore else { return }
let teams = (0..<count).map { _ in
let team = TeamRegistration(tournament: id, registrationDate: Date())
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
return team
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
} catch {
Logger.error(error)
}
}
public func buildGroupStages() {
guard groupStages().isEmpty, let tournamentStore = self.tournamentStore else {
return
}
var _groupStages = [GroupStage]()
for index in 0..<groupStageCount {
let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, format: groupStageSmartMatchFormat())
_groupStages.append(groupStage)
}
tournamentStore.groupStages.addOrUpdate(contentOfs: _groupStages)
refreshGroupStages()
}
public func bracketTeamCount() -> Int {
let bracketTeamCount = teamCount - (teamsPerGroupStage - qualifiedPerGroupStage) * groupStageCount + (groupStageAdditionalQualified * (groupStageCount > 0 ? 1 : 0))
return bracketTeamCount
}
public func buildBracket(minimalBracketTeamCount: Int? = nil) {
guard rounds().isEmpty else { return }
let roundCount = RoundRule.numberOfRounds(forTeams: minimalBracketTeamCount ?? bracketTeamCount())
let matchCount = RoundRule.numberOfMatches(forTeams: minimalBracketTeamCount ?? bracketTeamCount())
let rounds = (0..<roundCount).map { //index 0 is the final
return Round(tournament: id, index: $0, format: roundSmartMatchFormat($0), loserBracketMode: loserBracketMode)
}
if rounds.isEmpty {
return
}
do {
try self.tournamentStore?.rounds.addOrUpdate(contentOfs: rounds)
} catch {
Logger.error(error)
}
let matches = (0..<matchCount).map { //0 is final match
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
let round = rounds[roundIndex]
return Match(round: round.id, index: $0, format: round.matchFormat, name: Match.setServerTitle(upperRound: round, matchIndex: RoundRule.matchIndexWithinRound(fromMatchIndex: $0)))
}
print(matches.map {
(RoundRule.roundName(fromMatchIndex: $0.index), RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index))
})
self.tournamentStore?.matches.addOrUpdate(contentOfs: matches)
rounds.forEach { round in
round.buildLoserBracket()
}
}
public func match(for bracketPosition: Int?) -> Match? {
guard let bracketPosition else { return nil }
let matchIndex = bracketPosition / 2
let roundIndex = RoundRule.roundIndex(fromMatchIndex: matchIndex)
if let round: Round = self.getRound(atRoundIndex: roundIndex) {
return self.tournamentStore?.matches.first(where: { $0.round == round.id && $0.index == matchIndex })
// return Store.main.filter(isIncluded: { $0.round == round.id && $0.index == matchIndex }).first
}
return nil
}
public func resetTeamScores(in matchOfBracketPosition: Int?, outsideOf: [TeamScore] = []) {
guard let match = match(for: matchOfBracketPosition) else { return }
match.resetTeamScores(outsideOf: outsideOf)
}
public func updateTeamScores(in matchOfBracketPosition: Int?) {
guard let match = match(for: matchOfBracketPosition) else { return }
match.updateTeamScores()
}
public func deleteStructure() {
self.tournamentStore?.rounds.delete(contentOfs: rounds())
}
public func resetBracketPosition() {
unsortedTeams().forEach({ $0.bracketPosition = nil })
}
public func deleteGroupStages() {
self.tournamentStore?.groupStages.delete(contentOfs: allGroupStages())
if let gs = self.groupStageLoserBracket() {
self.tournamentStore?.rounds.delete(instance: gs)
}
}
public func refreshGroupStages(keepExistingMatches: Bool = false) {
unsortedTeams().forEach { team in
team.groupStage = nil
team.groupStagePosition = nil
}
if groupStageCount > 0 {
switch groupStageOrderingMode {
case .random:
setGroupStage(randomize: true, keepExistingMatches: keepExistingMatches)
case .snake:
setGroupStage(randomize: false, keepExistingMatches: keepExistingMatches)
case .swiss:
setGroupStage(randomize: true, keepExistingMatches: keepExistingMatches)
}
}
}
public func setGroupStage(randomize: Bool, keepExistingMatches: Bool = false) {
let groupStages = groupStages()
let numberOfBracketsAsInt = groupStages.count
// let teamsPerBracket = teamsPerBracket
if groupStageCount != numberOfBracketsAsInt {
deleteGroupStages()
buildGroupStages()
} else {
setGroupStageTeams(randomize: randomize)
groupStages.forEach { $0.buildMatches(keepExistingMatches: keepExistingMatches) }
}
}
public func removeWildCards() {
let wcs = unsortedTeams().filter({ $0.isWildCard() && $0.unsortedPlayers().isEmpty })
do {
try tournamentStore?.teamRegistrations.delete(contentOfs: wcs)
} catch {
Logger.error(error)
}
}
public func setGroupStageTeams(randomize: Bool) {
let groupStages = groupStages()
let max = groupStages.map { $0.size }.reduce(0,+)
var chunks = selectedSortedTeams().filter({ $0.wildCardBracket == false }).suffix(max).chunked(into: groupStageCount)
for (index, _) in chunks.enumerated() {
if randomize {
chunks[index].shuffle()
} else if index % 2 != 0 {
chunks[index].reverse()
}
print("Equipes \(chunks[index].map { $0.weight })")
for (jIndex, _) in chunks[index].enumerated() {
print("Position \(index + 1) Poule \(groupStages[jIndex].index)")
chunks[index][jIndex].groupStage = groupStages[jIndex].id
chunks[index][jIndex].groupStagePosition = index
}
}
self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
}
public func isFree() -> Bool {
return entryFee == nil || entryFee == 0
}
public func indexOf(team: TeamRegistration) -> Int? {
return selectedSortedTeams().firstIndex(where: { $0.id == team.id })
}
public func labelIndexOf(team: TeamRegistration) -> String? {
if let teamIndex = indexOf(team: team) {
return "Tête de série #" + (teamIndex + 1).formatted()
} else {
return nil
}
}
public var matchFormat: MatchFormat {
get {
roundFormat ?? .defaultFormatForMatchType(.bracket)
}
set {
roundFormat = newValue
}
}
public var groupStageMatchFormat: MatchFormat {
get {
groupStageFormat ?? .defaultFormatForMatchType(.groupStage)
}
set {
groupStageFormat = newValue
}
}
public var loserBracketMatchFormat: MatchFormat {
get {
loserRoundFormat ?? .defaultFormatForMatchType(.loserBracket)
}
set {
loserRoundFormat = newValue
}
}
public var groupStageOrderingMode: GroupStageOrderingMode {
get {
groupStageSortMode
}
set {
groupStageSortMode = newValue
}
}
public var tournamentCategory: TournamentCategory {
get {
federalCategory
}
set {
if federalCategory != newValue {
federalCategory = newValue
updateWeights()
} else {
federalCategory = newValue
}
}
}
public var tournamentLevel: TournamentLevel {
get {
federalLevelCategory
}
set {
federalLevelCategory = newValue
teamSorting = newValue.defaultTeamSortingType
groupStageMatchFormat = DataStore.shared.user.groupStageMatchFormatPreference ?? groupStageSmartMatchFormat()
loserBracketMatchFormat = DataStore.shared.user.loserBracketMatchFormatPreference ?? loserBracketSmartMatchFormat()
matchFormat = DataStore.shared.user.bracketMatchFormatPreference ?? roundSmartMatchFormat(5)
}
}
public var federalTournamentAge: FederalTournamentAge {
get {
federalAgeCategory
}
set {
federalAgeCategory = newValue
}
}
public func loserBracketSmartMatchFormat() -> MatchFormat {
let format = tournamentLevel.federalFormatForLoserBracketRound()
if tournamentLevel == .p25 { return .superTie }
if format.rank < loserBracketMatchFormat.rank {
return format
} else {
return loserBracketMatchFormat
}
}
public func groupStageSmartMatchFormat() -> MatchFormat {
let format = tournamentLevel.federalFormatForGroupStage()
if format.rank < groupStageMatchFormat.rank {
return format
} else {
return groupStageMatchFormat
}
}
public func initSettings(templateTournament: Tournament?, overrideTeamCount: Bool = true) {
courtCount = eventObject()?.clubObject()?.courtCount ?? 2
setupDefaultPrivateSettings(templateTournament: templateTournament)
setupUmpireSettings(defaultTournament: nil) //default is not template, default is for event sharing settings
if let templateTournament {
setupRegistrationSettings(templateTournament: templateTournament, overrideTeamCount: overrideTeamCount)
}
setupFederalSettings()
customizeUsingPreferences()
}
public func setupFederalSettings() {
teamSorting = tournamentLevel.defaultTeamSortingType
groupStageMatchFormat = DataStore.shared.user.groupStageMatchFormatPreference ?? groupStageSmartMatchFormat()
loserBracketMatchFormat = DataStore.shared.user.loserBracketMatchFormatPreference ?? loserBracketSmartMatchFormat()
matchFormat = DataStore.shared.user.bracketMatchFormatPreference ?? roundSmartMatchFormat(5)
entryFee = tournamentLevel.entryFee
registrationDateLimit = deadline(for: .inscription)
if enableOnlineRegistration, isAnimation() == false {
accountIsRequired = true
licenseIsRequired = true
}
}
public func customizeUsingPreferences() {
guard let lastTournamentWithSameBuild = DataStore.shared.tournaments.filter({ tournament in
tournament.tournamentLevel == self.tournamentLevel
&& tournament.tournamentCategory == self.tournamentCategory
&& tournament.federalTournamentAge == self.federalTournamentAge
&& tournament.hasEnded() == true
&& tournament.isCanceled == false
&& tournament.isDeleted == false
}).sorted(by: \.endDate!, order: .descending).first else {
return
}
self.entryFee = lastTournamentWithSameBuild.entryFee
self.clubMemberFeeDeduction = lastTournamentWithSameBuild.clubMemberFeeDeduction
}
public func deadline(for type: TournamentDeadlineType) -> Date? {
guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil }
let daysOffset = type.daysOffset(level: tournamentLevel)
if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) {
let startOfDay = Calendar.current.startOfDay(for: date)
return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay)
}
return nil
}
public func setupDefaultPrivateSettings(templateTournament: Tournament?) {
#if DEBUG
self.isPrivate = false
self.publishTeams = true
self.publishSummons = true
self.publishBrackets = true
self.publishGroupStages = true
self.publishRankings = true
self.publishTournament = true
self.publishProg = true
#else
var shouldBePrivate = templateTournament?.isPrivate ?? true
if Guard.main.currentPlan == .monthlyUnlimited {
shouldBePrivate = false
} else if Guard.main.purchasedTransactions.isEmpty == false {
shouldBePrivate = false
}
self.isPrivate = shouldBePrivate
#endif
}
public func setupUmpireSettings(defaultTournament: Tournament? = nil) {
if let defaultTournament {
self.umpireCustomMail = defaultTournament.umpireCustomMail
self.umpireCustomPhone = defaultTournament.umpireCustomPhone
self.umpireCustomContact = defaultTournament.umpireCustomContact
self.hideUmpireMail = defaultTournament.hideUmpireMail
self.hideUmpirePhone = defaultTournament.hideUmpirePhone
self.disableRankingFederalRuling = defaultTournament.disableRankingFederalRuling
self.loserBracketMode = defaultTournament.loserBracketMode
} else {
let user = DataStore.shared.user
self.umpireCustomMail = user.umpireCustomMail
self.umpireCustomPhone = user.umpireCustomPhone
self.umpireCustomContact = user.umpireCustomContact
self.hideUmpireMail = user.hideUmpireMail
self.hideUmpirePhone = user.hideUmpirePhone
self.disableRankingFederalRuling = user.disableRankingFederalRuling
self.loserBracketMode = user.loserBracketMode
}
}
public func setupRegistrationSettings(templateTournament: Tournament, overrideTeamCount: Bool = true) {
self.enableOnlineRegistration = templateTournament.enableOnlineRegistration
self.unregisterDeltaInHours = templateTournament.unregisterDeltaInHours
self.accountIsRequired = templateTournament.accountIsRequired
self.licenseIsRequired = templateTournament.licenseIsRequired
self.minimumPlayerPerTeam = templateTournament.minimumPlayerPerTeam
self.maximumPlayerPerTeam = templateTournament.maximumPlayerPerTeam
self.waitingListLimit = templateTournament.waitingListLimit
self.teamCountLimit = templateTournament.teamCountLimit
if overrideTeamCount {
self.teamCount = templateTournament.teamCount
}
self.enableOnlinePayment = templateTournament.enableOnlinePayment
self.onlinePaymentIsMandatory = templateTournament.onlinePaymentIsMandatory
self.enableOnlinePaymentRefund = templateTournament.enableOnlinePaymentRefund
self.stripeAccountId = templateTournament.stripeAccountId
self.enableTimeToConfirm = templateTournament.enableTimeToConfirm
self.isCorporateTournament = templateTournament.isCorporateTournament
self.clubMemberFeeDeduction = templateTournament.clubMemberFeeDeduction
self.unregisterDeltaInHours = templateTournament.unregisterDeltaInHours
if self.registrationDateLimit == nil, templateTournament.registrationDateLimit != nil {
self.registrationDateLimit = startDate.truncateMinutesAndSeconds()
}
self.openingRegistrationDate = templateTournament.openingRegistrationDate != nil ? creationDate.truncateMinutesAndSeconds() : nil
self.refundDateLimit = templateTournament.enableOnlinePaymentRefund ? startDate.truncateMinutesAndSeconds() : nil
}
public func onlineRegistrationCanBeEnabled() -> Bool {
true
// isAnimation() == false
}
public func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
let format = tournamentLevel.federalFormatForBracketRound(roundIndex)
if format.rank < matchFormat.rank {
return format
} else {
return matchFormat
}
}
private func _defaultSorting() -> [MySortDescriptor<TeamRegistration>] {
switch teamSorting {
case .rank:
[.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.uniqueRandomIndex), .keyPath(\TeamRegistration.id)]
case .inscriptionDate:
[.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.uniqueRandomIndex), .keyPath(\TeamRegistration.id)]
}
}
public func isSameBuild(_ build: any TournamentBuildHolder) -> Bool {
tournamentLevel == build.level
&& tournamentCategory == build.category
&& federalTournamentAge == build.age
}
private let _currentSelectionSorting : [MySortDescriptor<TeamRegistration>] = [.keyPath(\.weight), .keyPath(\.uniqueRandomIndex), .keyPath(\.id)]
private func _matchSchedulers() -> [MatchScheduler] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matchSchedulers.filter { $0.tournament == self.id }
// DataStore.shared.matchSchedulers.filter(isIncluded: { $0.tournament == self.id })
}
public func matchScheduler() -> MatchScheduler? {
return self._matchSchedulers().first
}
public func courtsAvailable() -> [Int] {
(0..<courtCount).map { $0 }
}
public func currentMonthData() -> MonthData? {
guard let rankSourceDate else { return nil }
let dateString = URL.importDateFormatter.string(from: rankSourceDate)
return DataStore.shared.monthData.first(where: { $0.monthKey == dateString })
}
public var maleUnrankedValue: Int? {
return currentMonthData()?.maleUnrankedValue
}
public var femaleUnrankedValue: Int? {
return currentMonthData()?.femaleUnrankedValue
}
public func courtNameIfAvailable(atIndex courtIndex: Int) -> String? {
return club()?.customizedCourts.first(where: { $0.index == courtIndex })?.name
}
public func courtName(atIndex courtIndex: Int) -> String {
return courtNameIfAvailable(atIndex: courtIndex) ?? Court.courtIndexedTitle(atIndex: courtIndex)
}
public func tournamentWinner() -> TeamRegistration? {
let finals: Round? = self.tournamentStore?.rounds.first(where: { $0.index == 0 && $0.isUpperBracket() })
return finals?.playedMatches().first?.winner()
}
public func getGroupStageChunkValue() -> Int {
if groupStageCount > 0 && teamsPerGroupStage >= 2 {
let result = courtCount / (teamsPerGroupStage / 2)
let remainder = courtCount % (teamsPerGroupStage / 2)
let value = remainder == 0 ? result : result + 1
return min(groupStageCount, value)
} else {
return 1
}
}
public func replacementRangeExtended(groupStagePosition: Int) -> TeamRegistration.TeamRange? {
let selectedSortedTeams = selectedSortedTeams()
var left: TeamRegistration? = nil
if groupStagePosition == 0 {
left = seeds().last
} else {
let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition - 1 }).sorted(by: \.weight)
left = previousHat.last
}
var right: TeamRegistration? = nil
if groupStagePosition == teamsPerGroupStage - 1 {
right = nil
} else {
let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition + 1 }).sorted(by: \.weight)
right = previousHat.first
}
return (left: left, right: right)
}
public typealias TeamPlacementIssue = (shouldBeInIt: [String], shouldNotBeInIt: [String])
public func groupStageTeamPlacementIssue() -> TeamPlacementIssue {
let selected = selectedSortedTeams()
let allTeams = unsortedTeams()
let newGroup = selected.suffix(groupStageSpots())
let currentGroup = allTeams.filter({ $0.groupStagePosition != nil })
let selectedIds = newGroup.map { $0.id }
let groupIds = currentGroup.map { $0.id }
let shouldBeInIt = Set(selectedIds).subtracting(groupIds)
let shouldNotBeInIt = Set(groupIds).subtracting(selectedIds)
return (Array(shouldBeInIt), Array(shouldNotBeInIt))
}
public func bracketTeamPlacementIssue() -> TeamPlacementIssue {
let selected = selectedSortedTeams()
let allTeams = unsortedTeams()
let seedCount = max(selected.count - groupStageSpots(), 0)
let newGroup = selected.prefix(seedCount) + selected.filter({ $0.qualified })
let currentGroup = allTeams.filter({ $0.bracketPosition != nil })
let selectedIds = newGroup.map { $0.id }
let groupStageTeamsInBracket = selected.filter({ $0.qualified == false && $0.inGroupStage() && $0.inRound() })
let groupIds = currentGroup.map { $0.id } + groupStageTeamsInBracket.map { $0.id }
let shouldBeInIt = Set(selectedIds).subtracting(groupIds)
let shouldNotBeInIt = Set(groupIds).subtracting(selectedIds)
return (Array(shouldBeInIt), Array(shouldNotBeInIt))
}
public func groupStageLoserBracket() -> Round? {
self.tournamentStore?.rounds.first(where: { $0.isGroupStageLoserBracket() })
}
public func groupStageLoserBracketsInitialPlace() -> Int {
return teamCount - teamsPerGroupStage * groupStageCount + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified + 1
}
public func addNewGroupStageStep() {
let lastStep = lastStep() + 1
for i in 0..<teamsPerGroupStage {
let gs = GroupStage(tournament: id, index: i, size: groupStageCount, step: lastStep)
self.tournamentStore?.groupStages.addOrUpdate(instance: gs)
}
groupStages(atStep: 1).forEach { $0.buildMatches() }
}
public func lastStep() -> Int {
self.tournamentStore?.groupStages.sorted(by: \.step).last?.step ?? 0
}
public func generateSmartLoserGroupStageBracket() {
guard let groupStageLoserBracket = groupStageLoserBracket() else { return }
for i in qualifiedPerGroupStage..<teamsPerGroupStage {
groupStages().chunked(into: 2).forEach { gss in
let placeCount = i * 2 + 1
let match = Match(round: groupStageLoserBracket.id, index: placeCount, format: groupStageLoserBracket.matchFormat)
match.setMatchName("\(placeCount)\(placeCount.ordinalFormattedSuffix(feminine: true)) place")
do {
try tournamentStore?.matches.addOrUpdate(instance: match)
} catch {
Logger.error(error)
}
if let gs1 = gss.first, let gs2 = gss.last, let score1 = gs1.teams(true)[safe: i], let score2 = gs2.teams(true)[safe: i] {
print("rang \(i)")
print(score1.teamLabel(.short), "vs", score2.teamLabel(.short))
match.setLuckyLoser(team: score1, teamPosition: .one)
match.setLuckyLoser(team: score2, teamPosition: .two)
}
}
}
}
public func updateTournamentState() {
Task {
let fr = await finalRanking()
_ = await setRankings(finalRanks: fr)
}
}
public func allLoserRoundMatches() -> [Match] {
rounds().flatMap { $0.allLoserRoundMatches() }
}
public func seedsCount() -> Int {
selectedSortedTeams().count - groupStageSpots()
}
public func lastDrawnDate() -> Date? {
drawLogs().last?.drawDate
}
public func drawLogs() -> [DrawLog] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.drawLogs.sorted(by: \.drawDate)
}
public func seedSpotsLeft() -> Bool {
let alreadySeededRounds = rounds().filter({ $0.seeds().isEmpty == false })
if alreadySeededRounds.isEmpty { return true }
let spotsLeft = alreadySeededRounds.flatMap({ $0.playedMatches() }).filter { $0.isEmpty() || $0.isValidSpot() }
return spotsLeft.isEmpty == false
}
public func isRoundValidForSeeding(roundIndex: Int) -> Bool {
if let lastRoundWithSeeds = rounds().last(where: { $0.seeds().isEmpty == false }) {
return roundIndex >= lastRoundWithSeeds.index
} else {
return true
}
}
public func updateSeedsBracketPosition() async {
await removeAllSeeds(saveTeamsAtTheEnd: false)
let drawLogs = drawLogs().reversed()
let seeds = seeds()
await MainActor.run {
for (index, seed) in seeds.enumerated() {
if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) {
drawLog.updateTeamBracketPosition(seed)
}
}
}
do {
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: seeds)
} catch {
Logger.error(error)
}
}
public func removeAllSeeds(saveTeamsAtTheEnd: Bool) async {
let teams = unsortedTeams()
teams.forEach({ team in
team.bracketPosition = nil
team._cachedRestingTime = nil
team.finalRanking = nil
team.pointsEarned = nil
})
let allMatches = allRoundMatches()
let ts = allMatches.flatMap { match in
match.teamScores
}
allMatches.forEach { match in
match.disabled = false
match.losingTeamId = nil
match.winningTeamId = nil
match.endDate = nil
match.removeCourt()
match.servingTeamId = nil
}
do {
try tournamentStore?.teamScores.delete(contentOfs: ts)
} catch {
Logger.error(error)
}
if saveTeamsAtTheEnd {
do {
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
} catch {
Logger.error(error)
}
}
}
public func removeRound(_ round: Round) async {
await MainActor.run {
let teams = round.seeds()
teams.forEach { team in
team.resetBracketPosition()
}
tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
tournamentStore?.rounds.delete(instance: round)
}
}
public func addNewRound(_ roundIndex: Int) async {
await MainActor.run {
let round = Round(tournament: id, index: roundIndex, format: matchFormat)
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
let nextRound = round.nextRound()
let tournamentStore = self.tournamentStore
var currentIndex = 0
let matches = (0..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, format: round.matchFormat)
if let nextRound, let followingMatch = tournamentStore?.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
return match
}
tournamentStore?.rounds.addOrUpdate(instance: round)
tournamentStore?.matches.addOrUpdate(contentOfs: matches)
if round.index < 5 {
round.buildLoserBracket()
round.loserRounds().forEach { loserRound in
loserRound.disableUnplayedLoserBracketMatches()
}
}
}
}
public func exportedDrawLogs() -> String {
var logs : [String] = ["Journal des tirages\n\n"]
logs.append(drawLogs().map { $0.exportedDrawLog() }.joined(separator: "\n\n"))
return logs.joined()
}
public func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool {
guard let source = eventObject()?.courtsUnavailability else { return false }
let courtLockedSchedule = source.filter({ $0.courtIndex == courtIndex })
return courtLockedSchedule.anySatisfy({ dateInterval in
let range = startDate..<endDate
return dateInterval.range.overlaps(range)
})
}
public func getOnlineRegistrationStatus() -> OnlineRegistrationStatus {
if hasStarted() {
return .inProgress
}
if closedRegistrationDate != nil {
return .ended
}
if endDate != nil {
return .endedWithResults
}
let now = Date()
if let openingRegistrationDate = openingRegistrationDate {
let timezonedDateTime = openingRegistrationDate // Assuming dates are already in local timezone
if now < timezonedDateTime {
return .notStarted
}
}
if let registrationDateLimit = registrationDateLimit {
let timezonedDateTime = registrationDateLimit // Assuming dates are already in local timezone
if now > timezonedDateTime {
return .ended
}
}
let currentTeamCount = unsortedTeamsWithoutWO().count
if currentTeamCount >= teamCount {
if let waitingListLimit = waitingListLimit {
let waitingListCount = currentTeamCount - teamCount
if waitingListCount >= waitingListLimit {
return .waitingListFull
}
}
return .waitingListPossible
}
return .open
}
public func getPaymentStatus() -> PaymentStatus {
if enableOnlinePayment == false {
return .notEnabled
}
// 1. Check if Stripe account is configured. This is the most fundamental requirement.
if (stripeAccountId == nil || stripeAccountId!.isEmpty) && isCorporateTournament == false {
return .notConfigured
}
// 2. Check if online payment is mandatory. This determines the main branch of statuses.
if onlinePaymentIsMandatory {
// Payment is mandatory. Now check refund options.
if enableOnlinePaymentRefund {
let now = Date()
if let refundDate = refundDateLimit {
// Refund is enabled and a date limit is set. Check if the date has passed.
if now < refundDate {
return .mandatoryRefundEnabled // Mandatory, refund is currently possible
} else {
return .mandatoryRefundEnded // Mandatory, refund period has ended
}
} else {
// Refund is enabled but no specific date limit is set. Assume it's currently enabled.
return .mandatoryRefundEnabled
}
} else {
// Payment is mandatory, but refunds are explicitly not enabled.
return .mandatoryNoRefund
}
} else {
// Payment is not mandatory, meaning it's optional.
// Features like `clubMemberFeeDeduction` or `enableTimeToConfirm`
// would apply to this optional payment, but don't change its
// overall 'optionalPayment' status for this summary function.
return .optionalPayment
}
}
// MARK: - Status
public func shouldTournamentBeOver() async -> Bool {
if hasEnded() {
return true
}
if hasStarted() == false {
return false
}
if hasStarted(), self.startDate.timeIntervalSinceNow > -3600*24 {
return false
}
if tournamentStore?.store.fileCollectionsAllLoaded() == false {
return false
}
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func shouldTournamentBeOver()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
if isDeleted == false && hasEnded() == false && hasStarted() {
let allMatches = allMatches()
let remainingMatches = allMatches.filter({ $0.hasEnded() == false && $0.startDate != nil })
let calendar = Calendar.current
let anyTomorrow = remainingMatches.anySatisfy({ calendar.isDateInTomorrow($0.startDate!) })
if anyTomorrow == false, let endDate = allMatches.filter({ $0.hasEnded() }).sorted(by: \.endDate!, order: .ascending).last?.endDate, endDate.timeIntervalSinceNow <= -2 * 3600 {
return true
}
}
return false
}
public func rankSourceShouldBeRefreshed() -> Date? {
if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate = rankSourceDate, currentRankSourceDate < mostRecentDate, hasEnded() == false {
return mostRecentDate
} else {
return nil
}
}
public func onlineTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.hasRegisteredOnline() })
}
public func paidOnlineTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.hasPaidOnline() })
}
public func shouldWarnOnlineRegistrationUpdates() -> Bool {
enableOnlineRegistration && onlineTeams().isEmpty == false && hasEnded() == false && hasStarted() == false
}
public func refreshTeamList(forced: Bool) async {
guard StoreCenter.main.isAuthenticated else { return }
guard tournamentStore?.store.fileCollectionsAllLoaded() == true else { return }
guard shouldRefreshTeams(forced: forced), refreshInProgress == false else { return }
if forced == false {
guard enableOnlineRegistration, hasEnded() == false else {
return
}
}
refreshInProgress = true
do {
try await self.tournamentStore?.playerRegistrations.loadDataFromServerIfAllowed(clear: true)
//try await self.tournamentStore?.teamScores.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore?.teamRegistrations.loadDataFromServerIfAllowed(clear: true)
refreshInProgress = false
lastTeamRefresh = Date()
} catch {
Logger.error(error)
refreshInProgress = false
lastTeamRefresh = Date()
}
}
public func mailSubject() -> String {
let subject = [tournamentTitle(hideSenior: true), formattedDate(.short), customClubName ?? clubName].compactMap({ $0 }).joined(separator: " | ")
return subject
}
public func groupStageLosingPositions() -> [Int] {
guard let maxSize = groupStages().map({ $0.size }).max() else {
return []
}
let leftInterval = qualifiedPerGroupStage + 1
return Array(leftInterval...maxSize)
}
public func groupMatchesByDay(matches: [Match]) -> [Date: [Match]] {
var matchesByDay = [Date: [Match]]()
let calendar = Calendar.current
for match in matches {
// Extract day/month/year and create a date with only these components
let components = calendar.dateComponents([.year, .month, .day], from: match.computedStartDateForSorting)
let strippedDate = calendar.date(from: components)!
// Group matches by the strippedDate (only day/month/year)
if matchesByDay[strippedDate] == nil {
matchesByDay[strippedDate] = []
}
let shouldIncludeMatch: Bool
switch match.matchType {
case .groupStage:
shouldIncludeMatch = !matchesByDay[strippedDate]!.filter { $0.groupStage != nil }.compactMap { $0.groupStage }.contains(match.groupStage!)
case .bracket:
shouldIncludeMatch = !matchesByDay[strippedDate]!.filter { $0.round != nil }.compactMap { $0.round }.contains(match.round!)
case .loserBracket:
shouldIncludeMatch = true
}
if shouldIncludeMatch {
matchesByDay[strippedDate]!.append(match)
}
}
return matchesByDay
}
public func matchCountPerDay(matchesByDay: [Date: [Match]]) -> [Date: NSCountedSet] {
let days = matchesByDay.keys
var matchCountPerDay = [Date: NSCountedSet]()
for day in days {
if let matches = matchesByDay[day] {
var groupStageCount = 0
let countedSet = NSCountedSet()
for match in matches {
switch match.matchType {
case .groupStage:
if let groupStage = match.groupStageObject {
if groupStageCount < groupStage.size - 1 {
groupStageCount = groupStage.size - 1
}
}
case .bracket:
countedSet.add(match.matchFormat)
case .loserBracket:
break
}
}
if groupStageCount > 0 {
for _ in 0..<groupStageCount {
countedSet.add(groupStageMatchFormat)
}
}
if let loserRounds = matches.filter({ $0.round != nil }).filter({ $0.roundObject?.parent == nil }).sorted(by: \.computedStartDateForSorting).last?.roundObject?.loserRounds() {
let ids = matches.map { $0.id }
for loserRound in loserRounds {
if let first = loserRound.playedMatches().first {
if ids.contains(first.id) {
countedSet.add(first.matchFormat)
}
}
}
}
matchCountPerDay[day] = countedSet
}
}
return matchCountPerDay
}
public func groupStageStartDate() -> Date? {
groupStages().sorted(by: \.computedStartDateForSorting).first?.startDate
}
public func defaultCurrency() -> String {
if let currencyCode = self.currencyCode {
return currencyCode
} else {
return Locale.defaultCurrency()
}
}
public func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int {
if tournamentCategory != .men {
return 0
}
switch playerRank {
case 0: return 0
case womanMax: return manMax - womanMax
case manMax: return 0
default:
return TournamentCategory.femaleInMaleAssimilationAddition(playerRank, seasonYear: self.startDate.seasonYear())
}
}
public func coachingIsAuthorized() -> Bool {
switch startDate.seasonYear() {
case 2026:
return true
default:
return tournamentLevel.coachingIsAuthorized
}
}
public func minimumNumberOfTeams() -> Int {
return federalTournamentAge.minimumNumberOfTeams(inCategory: tournamentCategory, andInLevel: tournamentLevel)
}
public func removeAllDates() {
let allMatches = allMatches()
allMatches.forEach({
$0.startDate = nil
$0.confirmed = false
})
self.tournamentStore?.matches.addOrUpdate(contentOfs: allMatches)
let allGroupStages = groupStages()
allGroupStages.forEach({ $0.startDate = nil })
self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages)
let allRounds = allRounds()
allRounds.forEach({ $0.startDate = nil })
self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds)
}
public func formatSummary() -> String {
var label = [String]()
if groupStageCount > 0 {
label.append("Poules " + groupStageMatchFormat.format)
}
label.append("Tableau " + matchFormat.format)
label.append("Classement " + loserBracketMatchFormat.format)
return label.joined(separator: ", ")
}
// MARK: -
func insertOnServer() throws {
DataStore.shared.tournaments.writeChangeAndInsertOnServer(instance: self)
if let teamRegistrations = self.tournamentStore?.teamRegistrations {
for teamRegistration in teamRegistrations {
teamRegistration.insertOnServer()
}
}
if let groupStages = self.tournamentStore?.groupStages {
for groupStage in groupStages {
groupStage.insertOnServer()
}
}
if let rounds = self.tournamentStore?.rounds {
for round in rounds {
round.insertOnServer()
}
}
}
// MARK: - Payments & Crypto
public enum PaymentError: Error {
case cantPayTournament
}
}
extension Bool {
var encodedValue: Int {
switch self {
case true:
return Int.random(in: (0...4))
case false:
return Int.random(in: (5...9))
}
}
public static func decodeInt(_ int: Int) -> Bool {
switch int {
case (0...4):
return true
default:
return false
}
}
}
//extension Tournament {
// enum CodingKeys: String, CodingKey {
// case _id = "id"
// case _event = "event"
// case _name = "name"
// case _startDate = "startDate"
// case _endDate = "endDate"
// case _creationDate = "creationDate"
// case _isPrivate = "isPrivate"
// case _groupStageFormat = "groupStageFormat"
// case _roundFormat = "roundFormat"
// case _loserRoundFormat = "loserRoundFormat"
// case _groupStageSortMode = "groupStageSortMode"
// case _groupStageCount = "groupStageCount"
// case _rankSourceDate = "rankSourceDate"
// case _dayDuration = "dayDuration"
// case _teamCount = "teamCount"
// case _teamSorting = "teamSorting"
// case _federalCategory = "federalCategory"
// case _federalLevelCategory = "federalLevelCategory"
// case _federalAgeCategory = "federalAgeCategory"
// case _groupStageCourtCount = "groupStageCourtCount"
// case _closedRegistrationDate = "closedRegistrationDate"
// case _groupStageAdditionalQualified = "groupStageAdditionalQualified"
// case _courtCount = "courtCount"
// case _prioritizeClubMembers = "prioritizeClubMembers"
// case _qualifiedPerGroupStage = "qualifiedPerGroupStage"
// case _teamsPerGroupStage = "teamsPerGroupStage"
// case _entryFee = "entryFee"
// case _additionalEstimationDuration = "additionalEstimationDuration"
// case _isDeleted = "isDeleted"
// case _isCanceled = "localId"
// case _payment = "globalId"
// }
//}
public extension Tournament {
static func getTemplateTournament() -> Tournament? {
return DataStore.shared.tournaments.filter { $0.isTemplate && $0.isDeleted == false }.sorted(by: \.startDate, order: .descending).first
}
static func fake() -> Tournament {
return Tournament(event: "Roland Garros", name: "Magic P100", startDate: Date(), endDate: Date(), creationDate: Date(), isPrivate: false, groupStageFormat: .nineGames, roundFormat: nil, loserRoundFormat: nil, groupStageSortMode: .snake, groupStageCount: 4, rankSourceDate: nil, dayDuration: 2, teamCount: 24, teamSorting: .rank, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .a45, closedRegistrationDate: nil, groupStageAdditionalQualified: 0, courtCount: 4, prioritizeClubMembers: false, qualifiedPerGroupStage: 2, teamsPerGroupStage: 4, entryFee: nil)
}
}
/// Warning: if the enum has more than 10 cases, the payment algo is broken
public enum TournamentPayment: Int, CaseIterable {
case free, unit, subscriptionUnit, unlimited
var isSubscription: Bool {
switch self {
case .subscriptionUnit, .unlimited:
return true
default:
return false
}
}
}