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.
894 lines
32 KiB
894 lines
32 KiB
//
|
|
// Tournament.swift
|
|
// PadelClub
|
|
//
|
|
// Created by Laurent Morvillier on 02/02/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import LeStorage
|
|
|
|
@Observable
|
|
class Tournament : ModelObject, Storable {
|
|
static func resourceName() -> String { "tournaments" }
|
|
|
|
var id: String = Store.randomId()
|
|
var event: String?
|
|
var creator: String?
|
|
var name: String?
|
|
var startDate: Date
|
|
var endDate: Date?
|
|
private(set) var creationDate: Date
|
|
var isPrivate: Bool
|
|
var groupStageFormat: Int?
|
|
var roundFormat: Int?
|
|
var loserRoundFormat: Int?
|
|
var groupStageSortMode: Int
|
|
var groupStageCount: Int
|
|
var rankSourceDate: Date?
|
|
var dayDuration: Int
|
|
var teamCount: Int
|
|
var teamSorting: TeamSortingType
|
|
var federalCategory: Int
|
|
var federalLevelCategory: Int
|
|
var federalAgeCategory: Int
|
|
var groupStageCourtCount: Int?
|
|
var seedCount: Int
|
|
var closedRegistrationDate: Date?
|
|
var groupStageAdditionalQualified: Int
|
|
var courtCount: Int = 2
|
|
var prioritizeClubMembers: Bool
|
|
var qualifiedPerGroupStage: Int
|
|
var teamsPerGroupStage: Int
|
|
var entryFee: Double?
|
|
var maleUnrankedValue: Int?
|
|
var femaleUnrankedValue: Int?
|
|
|
|
@ObservationIgnored
|
|
var navigationPath: [Screen] = []
|
|
|
|
internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, maleUnrankedValue: Int? = nil, femaleUnrankedValue: Int? = nil) {
|
|
self.event = event
|
|
self.creator = creator
|
|
self.name = name
|
|
self.startDate = startDate
|
|
self.endDate = endDate
|
|
self.creationDate = creationDate
|
|
self.isPrivate = isPrivate
|
|
self.groupStageFormat = groupStageFormat
|
|
self.roundFormat = roundFormat
|
|
self.loserRoundFormat = loserRoundFormat
|
|
self.groupStageSortMode = groupStageSortMode.rawValue
|
|
self.groupStageCount = groupStageCount
|
|
self.rankSourceDate = rankSourceDate
|
|
self.dayDuration = dayDuration
|
|
self.teamCount = teamCount
|
|
//self.teamSorting = teamSorting.rawValue
|
|
self.federalCategory = federalCategory.rawValue
|
|
self.federalLevelCategory = federalLevelCategory.rawValue
|
|
self.federalAgeCategory = federalAgeCategory.rawValue
|
|
self.groupStageCourtCount = groupStageCourtCount
|
|
self.seedCount = seedCount
|
|
self.closedRegistrationDate = closedRegistrationDate
|
|
self.groupStageAdditionalQualified = groupStageAdditionalQualified
|
|
self.courtCount = courtCount
|
|
self.prioritizeClubMembers = prioritizeClubMembers
|
|
self.qualifiedPerGroupStage = qualifiedPerGroupStage
|
|
self.teamsPerGroupStage = teamsPerGroupStage
|
|
self.entryFee = entryFee
|
|
self.maleUnrankedValue = maleUnrankedValue
|
|
self.femaleUnrankedValue = femaleUnrankedValue
|
|
self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType
|
|
}
|
|
|
|
enum State {
|
|
case initial
|
|
case build
|
|
}
|
|
|
|
func hasStarted() -> Bool {
|
|
startDate <= Date()
|
|
}
|
|
|
|
var eventObject: Event? {
|
|
guard let event else { return nil }
|
|
return Store.main.findById(event)
|
|
}
|
|
|
|
func pasteDataForImporting() -> String {
|
|
let selectedSortedTeams = selectedSortedTeams()
|
|
return (selectedSortedTeams.compactMap { $0.pasteData() } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams).compactMap { $0.pasteData() }).joined(separator: "\n\n")
|
|
}
|
|
|
|
func club() -> Club? {
|
|
eventObject?.clubObject
|
|
}
|
|
|
|
func locationLabel(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
if let club = club() {
|
|
switch displayStyle {
|
|
case .wide:
|
|
return club.name
|
|
case .short:
|
|
return club.acronym
|
|
}
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func hasEnded() -> Bool {
|
|
endDate != nil
|
|
}
|
|
|
|
func state() -> Tournament.State {
|
|
if groupStageCount > 0 && groupStages().isEmpty == false {
|
|
return .build
|
|
}
|
|
return .initial
|
|
}
|
|
|
|
|
|
func seeds() -> [TeamRegistration] {
|
|
let seeds = max(teamCount - groupStageCount * teamsPerGroupStage, 0)
|
|
return Array(selectedSortedTeams().prefix(seeds))
|
|
}
|
|
|
|
func availableSeeds() -> [TeamRegistration] {
|
|
return seeds().filter { $0.isSeedable() }
|
|
}
|
|
|
|
func lastSeedRound() -> Int? {
|
|
if let last = seeds().filter({ $0.bracketPosition != nil }).last {
|
|
return RoundRule.roundIndex(fromMatchIndex: last.bracketPosition! / 2)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func getRound(atRoundIndex roundIndex: Int) -> Round? {
|
|
Store.main.filter(isIncluded: { $0.tournament == id && $0.index == roundIndex }).first
|
|
}
|
|
|
|
func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] {
|
|
getRound(atRoundIndex: roundIndex)?.matches.filter { $0.teams().count == 0 } ?? []
|
|
}
|
|
|
|
func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] {
|
|
getRound(atRoundIndex: roundIndex)?.matches.filter { $0.teams().count == 1 } ?? []
|
|
}
|
|
|
|
func availableSeedGroups() -> [SeedInterval] {
|
|
let seeds = seeds()
|
|
var availableSeedGroup = Set<SeedInterval>()
|
|
for (index, seed) in seeds.enumerated() {
|
|
if seed.isSeedable(), let seedGroup = seedGroup(for: index) {
|
|
availableSeedGroup.insert(seedGroup)
|
|
}
|
|
}
|
|
return availableSeedGroup.sorted(by: <)
|
|
}
|
|
|
|
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:
|
|
// if 16 - 9 > availableSeeds().count {
|
|
// switch alreadySetupSeeds {
|
|
// case 8...15:
|
|
// return SeedInterval(first: 5, last: 8)
|
|
// case 8...15:
|
|
// return SeedInterval(first: 5, last: 8)
|
|
// }
|
|
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:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func randomSeed(fromSeedGroup seedGroup: SeedInterval) -> TeamRegistration? {
|
|
let availableSeeds = seeds(inSeedGroup: seedGroup)
|
|
return availableSeeds.randomElement()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func setSeeds(inRoundIndex roundIndex: Int, inSeedGroup seedGroup: SeedInterval) {
|
|
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
|
|
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
|
|
let availableSeeds = seeds(inSeedGroup: seedGroup)
|
|
|
|
if availableSeeds.count <= availableSeedSpot.count {
|
|
let spots = availableSeedSpot.shuffled()
|
|
for (index, seed) in availableSeeds.enumerated() {
|
|
seed.setSeedPosition(inSpot: spots[index], upperBranch: 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], upperBranch: nil, opposingSeeding: true)
|
|
}
|
|
} else if let chunk = seedGroup.chunk() {
|
|
setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk)
|
|
}
|
|
}
|
|
|
|
|
|
func inscriptionClosed() -> Bool {
|
|
closedRegistrationDate != nil
|
|
}
|
|
|
|
func groupStages() -> [GroupStage] {
|
|
Store.main.filter { $0.tournament == self.id }.sorted(by: \.index)
|
|
}
|
|
|
|
func getActiveGroupStage() -> GroupStage? {
|
|
let groupStages = groupStages()
|
|
return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first
|
|
}
|
|
|
|
func getActiveRound() -> Round? {
|
|
let rounds = rounds()
|
|
return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first
|
|
}
|
|
|
|
func rounds() -> [Round] {
|
|
Store.main.filter { $0.tournament == self.id }.sorted(by: \.index).reversed()
|
|
}
|
|
|
|
func sortedTeams() -> [TeamRegistration] {
|
|
let teams = selectedSortedTeams()
|
|
return teams + waitingListTeams(in: teams)
|
|
}
|
|
|
|
func selectedSortedTeams() -> [TeamRegistration] {
|
|
let start = Date()
|
|
var _sortedTeams : [TeamRegistration] = []
|
|
let _teams = unsortedTeams().filter({ $0.walkOut == false })
|
|
|
|
let defaultSorting : [MySortDescriptor<TeamRegistration>] = _defaultSorting()
|
|
|
|
let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false}
|
|
|
|
let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending)
|
|
|
|
let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending)
|
|
|
|
var bracketSeeds = min(teamCount, _completeTeams.count) - groupStageCount * teamsPerGroupStage - wcBracket.count
|
|
var groupStageTeamCount = groupStageCount * teamsPerGroupStage - wcGroupStage.count
|
|
if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
|
|
if bracketSeeds < 0 { bracketSeeds = 0 }
|
|
|
|
if prioritizeClubMembers {
|
|
|
|
let bracketTeams = (_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)
|
|
}
|
|
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print(id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
return _sortedTeams
|
|
}
|
|
|
|
func waitingListTeams(in teams: [TeamRegistration]) -> [TeamRegistration] {
|
|
let waitingList = Set(unsortedTeams()).subtracting(teams)
|
|
return waitingList.filter { $0.walkOut == false }.sorted(using: _defaultSorting(), order: .ascending) + waitingList.filter { $0.walkOut == true }.sorted(using: _defaultSorting(), order: .ascending)
|
|
}
|
|
|
|
func bracketCut() -> Int {
|
|
max(0, teamCount - groupStageCut())
|
|
}
|
|
|
|
func groupStageCut() -> Int {
|
|
groupStageCount * teamsPerGroupStage
|
|
}
|
|
|
|
func cutLabel(index: Int) -> String {
|
|
if index < bracketCut() {
|
|
return "Tableau"
|
|
} else if index - bracketCut() < groupStageCut() {
|
|
return "Poule"
|
|
} else {
|
|
return "Liste d'attente"
|
|
}
|
|
}
|
|
|
|
func unsortedTeams() -> [TeamRegistration] {
|
|
Store.main.filter { $0.tournament == self.id }
|
|
}
|
|
|
|
func duplicates(in players: [PlayerRegistration]) -> [PlayerRegistration] {
|
|
var duplicates = [PlayerRegistration]()
|
|
Set(players.compactMap({ $0.licenceId })).forEach { licenceId in
|
|
let found = players.filter({ $0.licenceId == licenceId })
|
|
if found.count > 1 {
|
|
duplicates.append(found.first!)
|
|
}
|
|
}
|
|
return duplicates
|
|
}
|
|
|
|
func unsortedPlayers() -> [PlayerRegistration] {
|
|
unsortedTeams().flatMap { $0.unsortedPlayers() }
|
|
}
|
|
|
|
func players() -> [PlayerRegistration] {
|
|
unsortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.weight)
|
|
}
|
|
|
|
func femalePlayers() -> [PlayerRegistration] {
|
|
unsortedPlayers().filter({ $0.isMalePlayer() == false })
|
|
}
|
|
|
|
func unrankValue(for malePlayer: Bool) -> Int? {
|
|
switch tournamentCategory {
|
|
case .men:
|
|
return maleUnrankedValue
|
|
case .women:
|
|
return femaleUnrankedValue
|
|
case .mix:
|
|
return malePlayer ? maleUnrankedValue : femaleUnrankedValue
|
|
}
|
|
}
|
|
|
|
//todo
|
|
var clubName: String? {
|
|
nil
|
|
}
|
|
|
|
//todo
|
|
func significantPlayerCount() -> Int {
|
|
2
|
|
}
|
|
|
|
func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
|
|
if startDate.isInCurrentYear() == false {
|
|
return []
|
|
}
|
|
return players.filter { player in
|
|
if player.rank == nil { return false }
|
|
if player.weight <= tournamentLevel.minimumPlayerRank(category: tournamentCategory, ageCategory: federalTournamentAge) {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
func mandatoryRegistrationCloseDate() -> Date? {
|
|
switch tournamentLevel {
|
|
case .p500, .p1000, .p1500, .p2000:
|
|
if let date = Calendar.current.date(byAdding: .day, value: -6, to: startDate) {
|
|
let startOfDay = Calendar.current.startOfDay(for: date)
|
|
return Calendar.current.date(byAdding: .minute, value: -1, to: startOfDay)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func licenseYearValidity() -> Int {
|
|
if startDate.get(.month) > 8 {
|
|
return startDate.get(.year) + 1
|
|
} else {
|
|
return startDate.get(.year)
|
|
}
|
|
}
|
|
|
|
func playersWithoutValidLicense(in players: [PlayerRegistration]) -> [PlayerRegistration] {
|
|
let licenseYearValidity = licenseYearValidity()
|
|
return players.filter({ ($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.licenceId?.isLicenseNumber == false || $0.licenceId?.isEmpty == true)) })
|
|
}
|
|
|
|
func importTeams(_ teams: [FileImportManager.TeamHolder]) {
|
|
var teamsToImport = [TeamRegistration]()
|
|
teams.forEach { team in
|
|
if let previousTeam = team.previousTeam {
|
|
previousTeam.updatePlayers(team.players)
|
|
teamsToImport.append(previousTeam)
|
|
} else {
|
|
let newTeam = addTeam(team.players)
|
|
teamsToImport.append(newTeam)
|
|
}
|
|
}
|
|
|
|
try? DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: teamsToImport)
|
|
try? DataStore.shared.playerRegistrations.addOrUpdate(contentOfs: teams.flatMap { $0.players })
|
|
|
|
}
|
|
|
|
func lockRegistration() {
|
|
closedRegistrationDate = Date()
|
|
let count = selectedSortedTeams().count
|
|
if teamCount != count {
|
|
teamCount = count
|
|
}
|
|
let teams = unsortedTeams()
|
|
teams.forEach { team in
|
|
team.lockWeight = team.weight
|
|
}
|
|
try? DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: teams)
|
|
}
|
|
|
|
func updateWeights() {
|
|
let teams = self.unsortedTeams()
|
|
teams.forEach { team in
|
|
let players = team.unsortedPlayers()
|
|
players.forEach { $0.setWeight(in: self) }
|
|
team.setWeight(from: players)
|
|
try? DataStore.shared.playerRegistrations.addOrUpdate(contentOfs: players)
|
|
}
|
|
try? DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: teams)
|
|
}
|
|
|
|
func updateRank(to newDate: Date?) async throws {
|
|
guard let newDate else { return }
|
|
rankSourceDate = newDate
|
|
|
|
let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate)
|
|
let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate)
|
|
|
|
|
|
await unsortedPlayers().concurrentForEach { player in
|
|
let dataURLs = SourceFileManager.shared.allFiles.filter({ $0.dateFromPath == newDate })
|
|
let sources = dataURLs.map { CSVParser(url: $0) }
|
|
|
|
try? await player.updateRank(from: sources, lastRank: (player.sex == 0 ? lastRankWoman : lastRankMan) ?? 0)
|
|
}
|
|
|
|
await MainActor.run {
|
|
self.maleUnrankedValue = lastRankMan
|
|
self.femaleUnrankedValue = lastRankWoman
|
|
}
|
|
}
|
|
|
|
func missingUnrankedValue() -> Bool {
|
|
maleUnrankedValue == nil || femaleUnrankedValue == nil
|
|
}
|
|
|
|
func findTeam(_ players: [PlayerRegistration]) -> TeamRegistration? {
|
|
unsortedTeams().first(where: { $0.includes(players) })
|
|
}
|
|
|
|
func tournamentTitle(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
[tournamentLevel.localizedLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), name].compactMap({ $0 }).joined(separator: " ")
|
|
}
|
|
|
|
func subtitle(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
name ?? ""
|
|
}
|
|
|
|
func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
switch displayStyle {
|
|
case .wide:
|
|
startDate.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted)
|
|
case .short:
|
|
startDate.formatted(date: .numeric, time: .omitted)
|
|
}
|
|
}
|
|
|
|
func qualifiedFromGroupStage() -> Int {
|
|
groupStageCount * qualifiedPerGroupStage
|
|
}
|
|
|
|
|
|
func qualifiedTeams() -> [TeamRegistration] {
|
|
unsortedTeams().filter({ $0.qualified() })
|
|
}
|
|
|
|
func moreQualifiedToDraw() -> Int {
|
|
max(qualifiedTeams().count - (qualifiedFromGroupStage() + groupStageAdditionalQualified), 0)
|
|
}
|
|
|
|
func missingQualifiedFromGroupStages() -> [TeamRegistration] {
|
|
if groupStageAdditionalQualified > 0 {
|
|
return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in
|
|
groupStage.teams()[qualifiedPerGroupStage]
|
|
}
|
|
.filter({ $0.qualified() == false })
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
|
|
func groupStagesAreOver() -> Bool {
|
|
guard groupStages().isEmpty == false else {
|
|
return true
|
|
}
|
|
return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
|
|
}
|
|
|
|
|
|
func bracketStatus() -> String {
|
|
if let round = getActiveRound() {
|
|
return [round.roundTitle(), round.roundStatus()].joined(separator: " ")
|
|
} else {
|
|
return "à construire"
|
|
}
|
|
}
|
|
|
|
func groupStageStatus() -> String {
|
|
let runningGroupStages = groupStages().filter({ $0.isRunning() })
|
|
if groupStagesAreOver() { return "terminées" }
|
|
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.formatted() }.joined(separator: ", ") + " en cours"
|
|
}
|
|
return groupStages().count.formatted() + " poule" + groupStages().count.pluralSuffix
|
|
} else {
|
|
return "Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { $0.index.formatted() }.joined(separator: ", ") + " en cours"
|
|
}
|
|
}
|
|
|
|
func settingsDescriptionLocalizedLabel() -> String {
|
|
[dayDuration.formatted() + " jour\(dayDuration.pluralSuffix)", courtCount.formatted() + " terrain\(courtCount.pluralSuffix)"].joined(separator: ", ")
|
|
}
|
|
|
|
func structureDescriptionLocalizedLabel() -> String {
|
|
let groupStageLabel: String? = groupStageCount > 0 ? groupStageCount.formatted() + " poule\(groupStageCount.pluralSuffix)" : nil
|
|
return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ")
|
|
}
|
|
|
|
func buildStructure() {
|
|
buildGroupStages()
|
|
buildBracket()
|
|
}
|
|
|
|
func buildGroupStages() {
|
|
groupStages().forEach { groupStage in
|
|
try? DataStore.shared.groupStages.delete(instance: groupStage)
|
|
}
|
|
|
|
var _groupStages = [GroupStage]()
|
|
for index in 0..<groupStageCount {
|
|
let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, matchFormat: groupStageMatchFormat)
|
|
_groupStages.append(groupStage)
|
|
}
|
|
|
|
try? DataStore.shared.groupStages.addOrUpdate(contentOfs: _groupStages)
|
|
|
|
groupStages().forEach { $0.buildMatches() }
|
|
refreshGroupStages()
|
|
}
|
|
|
|
func bracketTeamCount() -> Int {
|
|
let bracketTeamCount = teamCount - (teamsPerGroupStage - qualifiedPerGroupStage) * groupStageCount + (groupStageAdditionalQualified * (groupStageCount > 0 ? 1 : 0))
|
|
return bracketTeamCount
|
|
}
|
|
|
|
func buildBracket() {
|
|
try? DataStore.shared.rounds.delete(contentOfs: rounds())
|
|
|
|
// if let loserBrackets {
|
|
// removeFromLoserBrackets(loserBrackets)
|
|
// }
|
|
|
|
unsortedTeams().forEach({ $0.bracketPosition = nil })
|
|
let roundCount = RoundRule.numberOfRounds(forTeams: bracketTeamCount())
|
|
|
|
let rounds = (0..<roundCount).map { //index 0 is the final
|
|
Round(tournament: id, index: $0)
|
|
}
|
|
|
|
try? DataStore.shared.rounds.addOrUpdate(contentOfs: rounds)
|
|
let matchCount = RoundRule.numberOfMatches(forTeams: bracketTeamCount())
|
|
|
|
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, matchFormat: matchFormat)
|
|
}
|
|
|
|
print(matches.map {
|
|
(RoundRule.roundName(fromMatchIndex: $0.index), RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index))
|
|
})
|
|
|
|
try? DataStore.shared.matches.addOrUpdate(contentOfs: matches)
|
|
}
|
|
|
|
func resetStructure() {
|
|
|
|
}
|
|
|
|
func resetGroupStages() {
|
|
|
|
}
|
|
|
|
func refreshGroupStages() {
|
|
unsortedTeams().forEach { team in
|
|
team.groupStage = nil
|
|
team.groupStagePosition = nil
|
|
}
|
|
|
|
if groupStageCount > 0 {
|
|
switch groupStageOrderingMode {
|
|
case .random:
|
|
setGroupStage(randomize: true)
|
|
case .snake:
|
|
setGroupStage(randomize: false)
|
|
case .swiss:
|
|
setGroupStage(randomize: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setGroupStage(randomize: Bool) {
|
|
let groupStages = groupStages()
|
|
let numberOfBracketsAsInt = groupStages.count
|
|
// let teamsPerBracket = teamsPerBracket
|
|
if groupStageCount != numberOfBracketsAsInt {
|
|
buildGroupStages()
|
|
return
|
|
}
|
|
let max = groupStages.map { $0.size }.reduce(0,+)
|
|
var chunks = selectedSortedTeams().suffix(max).chunked(into: numberOfBracketsAsInt)
|
|
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
|
|
|
|
try? DataStore.shared.teamRegistrations.addOrUpdate(instance: chunks[index][jIndex])
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
func isFree() -> Bool {
|
|
entryFee == nil || entryFee == 0
|
|
}
|
|
|
|
func addTeam(_ players: Set<PlayerRegistration>) -> TeamRegistration {
|
|
let team = TeamRegistration(tournament: id, registrationDate: Date())
|
|
team.tournamentCategory = tournamentCategory
|
|
team.setWeight(from: Array(players))
|
|
players.forEach { player in
|
|
player.teamRegistration = team.id
|
|
}
|
|
return team
|
|
}
|
|
|
|
var matchFormat: MatchFormat {
|
|
get {
|
|
MatchFormat(rawValue: roundFormat) ?? .defaultFormatForMatchType(.bracket)
|
|
}
|
|
set {
|
|
roundFormat = newValue.rawValue
|
|
}
|
|
}
|
|
|
|
var groupStageMatchFormat: MatchFormat {
|
|
get {
|
|
MatchFormat(rawValue: groupStageFormat) ?? .defaultFormatForMatchType(.groupStage)
|
|
}
|
|
set {
|
|
groupStageFormat = newValue.rawValue
|
|
}
|
|
}
|
|
|
|
var loserBracketMatchFormat: MatchFormat {
|
|
get {
|
|
MatchFormat(rawValue: loserRoundFormat) ?? .defaultFormatForMatchType(.loserBracket)
|
|
}
|
|
set {
|
|
loserRoundFormat = newValue.rawValue
|
|
}
|
|
}
|
|
|
|
var groupStageOrderingMode: GroupStageOrderingMode {
|
|
get {
|
|
GroupStageOrderingMode(rawValue: groupStageSortMode) ?? .random
|
|
}
|
|
set {
|
|
groupStageSortMode = newValue.rawValue
|
|
}
|
|
}
|
|
|
|
var tournamentCategory: TournamentCategory {
|
|
get {
|
|
TournamentCategory(rawValue: federalCategory) ?? .men
|
|
}
|
|
set {
|
|
if federalCategory != newValue.rawValue {
|
|
federalCategory = newValue.rawValue
|
|
updateWeights()
|
|
} else {
|
|
federalCategory = newValue.rawValue
|
|
}
|
|
}
|
|
}
|
|
|
|
var tournamentLevel: TournamentLevel {
|
|
get {
|
|
TournamentLevel(rawValue: federalLevelCategory) ?? .p100
|
|
}
|
|
set {
|
|
federalLevelCategory = newValue.rawValue
|
|
teamSorting = newValue.defaultTeamSortingType
|
|
groupStageMatchFormat = groupStageSmartMatchFormat()
|
|
loserBracketMatchFormat = loserBracketSmartMatchFormat(1)
|
|
matchFormat = roundSmartMatchFormat(1)
|
|
|
|
}
|
|
}
|
|
|
|
var federalTournamentAge: FederalTournamentAge {
|
|
get {
|
|
FederalTournamentAge(rawValue: federalAgeCategory) ?? .senior
|
|
}
|
|
set {
|
|
federalAgeCategory = newValue.rawValue
|
|
}
|
|
}
|
|
|
|
func loserBracketSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
|
|
let format = tournamentLevel.federalFormatForLoserBracketRound(roundIndex)
|
|
if loserBracketMatchFormat.rank > format.rank {
|
|
return format
|
|
} else {
|
|
return loserBracketMatchFormat
|
|
}
|
|
}
|
|
|
|
func groupStageSmartMatchFormat() -> MatchFormat {
|
|
let format = tournamentLevel.federalFormatForGroupStage()
|
|
if groupStageMatchFormat.rank > format.rank {
|
|
return format
|
|
} else {
|
|
return groupStageMatchFormat
|
|
}
|
|
}
|
|
|
|
func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
|
|
let format = tournamentLevel.federalFormatForBracketRound(roundIndex)
|
|
if matchFormat.rank > format.rank {
|
|
return format
|
|
} else {
|
|
return matchFormat
|
|
}
|
|
}
|
|
|
|
private func _defaultSorting() -> [MySortDescriptor<TeamRegistration>] {
|
|
switch teamSorting {
|
|
case .rank:
|
|
[.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.registrationDate!), .keyPath(\.canonicalName)]
|
|
case .inscriptionDate:
|
|
[.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\.canonicalName)]
|
|
}
|
|
}
|
|
|
|
func isSameBuild(_ build: any TournamentBuildHolder) -> Bool {
|
|
tournamentLevel == build.level
|
|
&& tournamentCategory == build.category
|
|
&& federalTournamentAge == build.age
|
|
}
|
|
|
|
private let _currentSelectionSorting : [MySortDescriptor<TeamRegistration>] = [.keyPath(\.weight), .keyPath(\.registrationDate!), .keyPath(\.canonicalName)]
|
|
|
|
override func deleteDependencies() throws {
|
|
try Store.main.deleteDependencies(items: self.unsortedTeams())
|
|
try Store.main.deleteDependencies(items: self.groupStages())
|
|
try Store.main.deleteDependencies(items: self.rounds())
|
|
}
|
|
}
|
|
|
|
extension Tournament {
|
|
enum CodingKeys: String, CodingKey {
|
|
case _id = "id"
|
|
case _event = "event"
|
|
case _creator = "creator"
|
|
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 _seedCount = "seedCount"
|
|
case _closedRegistrationDate = "closedRegistrationDate"
|
|
case _groupStageAdditionalQualified = "groupStageAdditionalQualified"
|
|
case _courtCount = "courtCount"
|
|
case _prioritizeClubMembers = "prioritizeClubMembers"
|
|
case _qualifiedPerGroupStage = "qualifiedPerGroupStage"
|
|
case _teamsPerGroupStage = "teamsPerGroupStage"
|
|
case _entryFee = "entryFee"
|
|
case _maleUnrankedValue = "maleUnrankedValue"
|
|
case _femaleUnrankedValue = "femaleUnrankedValue"
|
|
}
|
|
}
|
|
|
|
extension Tournament: Hashable {
|
|
static func == (lhs: Tournament, rhs: Tournament) -> Bool {
|
|
lhs.id == rhs.id
|
|
}
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
hasher.combine(id)
|
|
}
|
|
}
|
|
|
|
extension Tournament: FederalTournamentHolder {
|
|
var holderId: String { id }
|
|
|
|
func clubLabel() -> String {
|
|
locationLabel()
|
|
}
|
|
|
|
func subtitleLabel() -> String {
|
|
subtitle()
|
|
}
|
|
|
|
var tournaments: [any TournamentBuildHolder] {
|
|
[
|
|
self
|
|
]
|
|
}
|
|
}
|
|
|
|
extension Tournament: TournamentBuildHolder {
|
|
var category: TournamentCategory {
|
|
tournamentCategory
|
|
}
|
|
|
|
var level: TournamentLevel {
|
|
tournamentLevel
|
|
}
|
|
|
|
var age: FederalTournamentAge {
|
|
federalTournamentAge
|
|
}
|
|
|
|
|
|
}
|
|
|