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

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