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.
2628 lines
106 KiB
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 = ["N°", "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
|
|
}
|
|
}
|
|
|
|
}
|
|
|