// // 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" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } var id: String = Store.randomId() var event: String? var name: String? var startDate: Date var endDate: Date? private(set) var creationDate: Date var isPrivate: Bool private(set) var groupStageFormat: MatchFormat? private(set) var roundFormat: MatchFormat? private(set) var loserRoundFormat: MatchFormat? var groupStageSortMode: GroupStageOrderingMode var groupStageCount: Int var rankSourceDate: Date? var dayDuration: Int var teamCount: Int var teamSorting: TeamSortingType var federalCategory: TournamentCategory var federalLevelCategory: TournamentLevel var federalAgeCategory: FederalTournamentAge var closedRegistrationDate: Date? var groupStageAdditionalQualified: Int var courtCount: Int = 2 var prioritizeClubMembers: Bool var qualifiedPerGroupStage: Int var teamsPerGroupStage: Int var entryFee: Double? var payment: TournamentPayment? = nil var additionalEstimationDuration: Int = 0 var isDeleted: Bool = false var isCanceled: Bool = false var publishTeams: Bool = false //var publishWaitingList: Bool = false var publishSummons: Bool = false var publishGroupStages: Bool = false var publishBrackets: Bool = false @ObservationIgnored var navigationPath: [Screen] = [] 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 _seedCount = "seedCount" 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" case _publishTeams = "publishTeams" //case _publishWaitingList = "publishWaitingList" case _publishSummons = "publishSummons" case _publishGroupStages = "publishGroupStages" case _publishBrackets = "publishBrackets" } internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false) { self.event = event 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 self.groupStageCount = groupStageCount self.rankSourceDate = rankSourceDate self.dayDuration = dayDuration self.teamCount = teamCount self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType self.federalCategory = federalCategory self.federalLevelCategory = federalLevelCategory self.federalAgeCategory = federalAgeCategory self.closedRegistrationDate = closedRegistrationDate self.groupStageAdditionalQualified = groupStageAdditionalQualified self.courtCount = courtCount self.prioritizeClubMembers = prioritizeClubMembers self.qualifiedPerGroupStage = qualifiedPerGroupStage self.teamsPerGroupStage = teamsPerGroupStage self.entryFee = entryFee self.additionalEstimationDuration = additionalEstimationDuration self.isDeleted = isDeleted self.publishTeams = publishTeams self.publishSummons = publishSummons self.publishBrackets = publishBrackets self.publishGroupStages = publishGroupStages } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: ._id) event = try container.decodeIfPresent(String.self, forKey: ._event) name = try container.decodeIfPresent(String.self, forKey: ._name) startDate = try container.decode(Date.self, forKey: ._startDate) endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate) creationDate = try container.decode(Date.self, forKey: ._creationDate) isPrivate = try container.decode(Bool.self, forKey: ._isPrivate) groupStageFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageFormat) roundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._roundFormat) loserRoundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserRoundFormat) groupStageSortMode = try container.decode(GroupStageOrderingMode.self, forKey: ._groupStageSortMode) groupStageCount = try container.decode(Int.self, forKey: ._groupStageCount) rankSourceDate = try container.decodeIfPresent(Date.self, forKey: ._rankSourceDate) dayDuration = try container.decode(Int.self, forKey: ._dayDuration) teamCount = try container.decode(Int.self, forKey: ._teamCount) teamSorting = try container.decode(TeamSortingType.self, forKey: ._teamSorting) federalCategory = try container.decode(TournamentCategory.self, forKey: ._federalCategory) federalLevelCategory = try container.decode(TournamentLevel.self, forKey: ._federalLevelCategory) federalAgeCategory = try container.decode(FederalTournamentAge.self, forKey: ._federalAgeCategory) closedRegistrationDate = try container.decodeIfPresent(Date.self, forKey: ._closedRegistrationDate) groupStageAdditionalQualified = try container.decode(Int.self, forKey: ._groupStageAdditionalQualified) courtCount = try container.decode(Int.self, forKey: ._courtCount) prioritizeClubMembers = try container.decode(Bool.self, forKey: ._prioritizeClubMembers) qualifiedPerGroupStage = try container.decode(Int.self, forKey: ._qualifiedPerGroupStage) teamsPerGroupStage = try container.decode(Int.self, forKey: ._teamsPerGroupStage) entryFee = try container.decodeIfPresent(Double.self, forKey: ._entryFee) payment = try Tournament._decodePayment(container: container) additionalEstimationDuration = try container.decode(Int.self, forKey: ._additionalEstimationDuration) isDeleted = try container.decode(Bool.self, forKey: ._isDeleted) isCanceled = try Tournament._decodeCanceled(container: container) publishTeams = try container.decodeIfPresent(Bool.self, forKey: ._publishTeams) ?? false publishSummons = try container.decodeIfPresent(Bool.self, forKey: ._publishSummons) ?? false publishGroupStages = try container.decodeIfPresent(Bool.self, forKey: ._publishGroupStages) ?? false publishBrackets = try container.decodeIfPresent(Bool.self, forKey: ._publishBrackets) ?? false } fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter() fileprivate static func _decodePayment(container: KeyedDecodingContainer) throws -> TournamentPayment? { let data = try container.decodeIfPresent(Data.self, forKey: ._payment) if let data { do { let decoded: String = try data.decryptData(pass: Key.pass.rawValue) let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue } return TournamentPayment(rawValue: sequence[18]) } catch { Logger.error(error) } } return nil } fileprivate static func _decodeCanceled(container: KeyedDecodingContainer) throws -> Bool { let data = try container.decodeIfPresent(Data.self, forKey: ._isCanceled) if let data { do { let decoded: String = try data.decryptData(pass: Key.pass.rawValue) let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue } return Bool.decodeInt(sequence[18]) } catch { Logger.error(error) } } return false } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: ._id) try container.encodeIfPresent(event, forKey: ._event) try container.encodeIfPresent(name, forKey: ._name) try container.encode(startDate, forKey: ._startDate) try container.encodeIfPresent(endDate, forKey: ._endDate) try container.encode(creationDate, forKey: ._creationDate) try container.encode(isPrivate, forKey: ._isPrivate) try container.encodeIfPresent(groupStageFormat, forKey: ._groupStageFormat) try container.encodeIfPresent(roundFormat, forKey: ._roundFormat) try container.encodeIfPresent(loserRoundFormat, forKey: ._loserRoundFormat) try container.encode(groupStageSortMode, forKey: ._groupStageSortMode) try container.encode(groupStageCount, forKey: ._groupStageCount) try container.encodeIfPresent(rankSourceDate, forKey: ._rankSourceDate) try container.encode(dayDuration, forKey: ._dayDuration) try container.encode(teamCount, forKey: ._teamCount) try container.encode(teamSorting, forKey: ._teamSorting) try container.encode(federalCategory, forKey: ._federalCategory) try container.encode(federalLevelCategory, forKey: ._federalLevelCategory) try container.encode(federalAgeCategory, forKey: ._federalAgeCategory) try container.encodeIfPresent(closedRegistrationDate, forKey: ._closedRegistrationDate) try container.encode(groupStageAdditionalQualified, forKey: ._groupStageAdditionalQualified) try container.encode(courtCount, forKey: ._courtCount) try container.encode(prioritizeClubMembers, forKey: ._prioritizeClubMembers) try container.encode(qualifiedPerGroupStage, forKey: ._qualifiedPerGroupStage) try container.encode(teamsPerGroupStage, forKey: ._teamsPerGroupStage) try container.encodeIfPresent(entryFee, forKey: ._entryFee) try self._encodePayment(container: &container) try container.encode(additionalEstimationDuration, forKey: ._additionalEstimationDuration) try container.encode(isDeleted, forKey: ._isDeleted) try self._encodeIsCanceled(container: &container) try container.encode(publishTeams, forKey: ._publishTeams) try container.encode(publishSummons, forKey: ._publishSummons) try container.encode(publishBrackets, forKey: ._publishBrackets) try container.encode(publishGroupStages, forKey: ._publishGroupStages) } fileprivate func _encodePayment(container: inout KeyedEncodingContainer) throws { guard let payment else { try container.encodeNil(forKey: ._payment) return } let max: Int = TournamentPayment.allCases.count var sequence = (1...18).map { _ in Int.random(in: (0..) throws { let max: Int = 9 var sequence = (1...18).map { _ in Int.random(in: (0.. Date { startDate } func areTeamsPublished() -> Bool { Date() >= startDate || publishTeams } func areSummonsPublished() -> Bool { Date() >= startDate || publishSummons } func publishedGroupStagesDate() -> Date? { if let first = groupStages().flatMap({ $0.playedMatches() }).compactMap({ $0.startDate }).sorted().first?.atEightAM() { if first.isEarlierThan(startDate) { return startDate } else { return first } } else { return nil } } func areGroupStagesPublished() -> Bool { if publishGroupStages { return true } if let publishedGroupStagesDate = publishedGroupStagesDate() { return Date() >= publishedGroupStagesDate } else { return false } } func publishedBracketsDate() -> Date? { if let first = rounds().flatMap({ $0.playedMatches() }).compactMap({ $0.startDate }).sorted().first?.atEightAM() { if first.isEarlierThan(startDate) { return startDate } else { return first } } else { return nil } } func areBracketsPublished() -> Bool { if publishBrackets { return true } if let publishedBracketsDate = publishedBracketsDate() { return Date() >= publishedBracketsDate } else { return false } } func shareURL() -> URL? { return URLs.main.url.appending(path: "tournament/\(id)") } func broadcastURL() -> URL? { return URLs.main.url.appending(path: "tournament/\(id)/broadcast") } func courtUsed() -> [Int] { let runningMatches : [Match] = Store.main.filter(isIncluded: { $0.isRunning() }).filter({ $0.tournamentId() == self.id }) return Set(runningMatches.compactMap { $0.courtIndex }).sorted() } func hasStarted() -> Bool { startDate <= Date() } func 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 self.isCanceled == true { return .canceled } if (groupStageCount > 0 && groupStages().isEmpty == false) || rounds().isEmpty == false { return .build } return .initial } func seededTeams() -> [TeamRegistration] { selectedSortedTeams().filter({ $0.bracketPosition != nil && $0.groupStagePosition == nil }) } func groupStageTeams() -> [TeamRegistration] { selectedSortedTeams().filter({ $0.bracketPosition == nil && $0.groupStagePosition != nil }) } func seeds() -> [TeamRegistration] { let selectedSortedTeams = selectedSortedTeams() let seeds = max(selectedSortedTeams.count - 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 0 } } 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)?.playedMatches().filter { $0.teams().count == 0 } ?? [] } func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] { getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 1 } ?? [] } func availableSeedGroups() -> [SeedInterval] { let seeds = seeds() var availableSeedGroup = Set() 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: 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 seedGroupAvailable(atRoundIndex roundIndex: Int) -> SeedInterval? { if let availableSeedGroup = availableSeedGroup() { return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: availableSeedGroup) } else { return nil } } func seedGroupAvailable(atRoundIndex roundIndex: Int, availableSeedGroup: SeedInterval) -> SeedInterval? { if availableSeeds().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) if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count { return availableSeedGroup } else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) && availableSeedGroup.count == availableSeedOpponentSpot.count { return availableSeedGroup } else if let chunks = availableSeedGroup.chunks() { if let chunk = chunks.first(where: { seedInterval in seedInterval.first >= self.seededTeams().count }) { return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk) } } } return nil } 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 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) } } } } 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 matchesWithSpace() -> [Match] { getActiveRound()?.playedMatches().filter({ $0.hasSpaceLeft() }) ?? [] } func getActiveRound(withSeeds: Bool = false) -> Round? { let rounds = rounds() let round = rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.last(where: { $0.hasEnded() }) ?? rounds.first if withSeeds { if round?.seeds().isEmpty == false { return round } else { return nil } } else { return round } } func allRoundMatches() -> [Match] { allRounds().flatMap { $0._matches() } } func allMatches() -> [Match] { let unsortedGroupStages : [GroupStage] = Store.main.filter { $0.tournament == self.id } let matches: [Match] = unsortedGroupStages.flatMap { $0._matches() } + allRoundMatches() return matches.filter({ $0.disabled == false }) } func _allMatchesIncludingDisabled() -> [Match] { let unsortedGroupStages : [GroupStage] = Store.main.filter { $0.tournament == self.id } return unsortedGroupStages.flatMap { $0._matches() } + allRounds().flatMap { $0._matches() } } func allRounds() -> [Round] { Store.main.filter { $0.tournament == self.id } } func rounds() -> [Round] { Store.main.filter { $0.tournament == self.id && $0.parent == nil }.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] = _defaultSorting() let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false }.prefix(teamCount).sorted(by: \.initialWeight) 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("func selectedSortedTeams", 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 selectedPlayers() -> [PlayerRegistration] { selectedSortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.computedRank) } func players() -> [PlayerRegistration] { unsortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.computedRank) } 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? { eventObject()?.clubObject()?.name } //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.computedRank <= 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.formattedLicense().isLicenseNumber == false || $0.licenceId?.isEmpty == true)) }) } func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? { guard let seedIndex else { return nil } return selectedSortedTeams()[safe: seedIndex]?.callDate } func importTeams(_ teams: [FileImportManager.TeamHolder]) { var teamsToImport = [TeamRegistration]() teams.forEach { team in if let previousTeam = team.previousTeam { previousTeam.updatePlayers(team.players, inTournamentCategory: team.tournamentCategory) teamsToImport.append(previousTeam) } else { let newTeam = addTeam(team.players, registrationDate: team.registrationDate) teamsToImport.append(newTeam) } } try? DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: teamsToImport) try? DataStore.shared.playerRegistrations.addOrUpdate(contentOfs: teams.flatMap { $0.players }) } func maximumCourtsPerGroupSage() -> Int { if teamsPerGroupStage > 1 { return min(teamsPerGroupStage / 2, courtCount) } else { return max(1, courtCount) } } func registrationIssues() -> Int { let players : [PlayerRegistration] = unsortedPlayers() let selectedTeams : [TeamRegistration] = selectedSortedTeams() let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } let duplicates : [PlayerRegistration] = duplicates(in: players) let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil }) let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players) let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players) let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) let waitingList : [TeamRegistration] = waitingListTeams(in: selectedTeams) let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil }) let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil }) return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count } func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration) -> Bool { guard let callDate = team.callDate else { return true } if let groupStageStartDate = team.groupStageObject()?.startDate { return Calendar.current.compare(callDate, to: groupStageStartDate, toGranularity: .minute) != ComparisonResult.orderedSame } else if let roundMatchStartDate = team.initialMatch()?.startDate { return Calendar.current.compare(callDate, to: roundMatchStartDate, toGranularity: .minute) != ComparisonResult.orderedSame } return true } func availableToStart(_ allMatches: [Match]) -> [Match] { let runningMatches = allMatches.filter({ $0.isRunning() && $0.isReady() }) return allMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }).sorted(by: \.computedStartDateForSorting) } func runningMatches(_ allMatches: [Match]) -> [Match] { allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) } func readyMatches(_ allMatches: [Match]) -> [Match] { return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) } func finishedMatches(_ allMatches: [Match], limit: Int? = nil) -> [Match] { let _limit = limit ?? courtCount return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(_limit)) } func finalRanking() -> [Int: [String]] { var teams: [Int: [String]] = [:] let rounds = rounds() let final = rounds.last?.playedMatches().last if let winner = final?.winningTeamId { teams[1] = [winner] } if let finalist = final?.losingTeamId { teams[2] = [finalist] } let others : [Round] = rounds.flatMap { round in round.loserRoundsAndChildren().filter { $0.isDisabled() == false && $0.hasNextRound() == false } }.compactMap({ $0 }) others.forEach { round in if let interval = round.seedInterval() { let playedMatches = round.playedMatches().filter { $0.disabled == false } let winners = playedMatches.compactMap({ $0.winningTeamId }) let losers = playedMatches.compactMap({ $0.losingTeamId }) teams[interval.first + winners.count - 1] = winners teams[interval.last] = losers } } let groupStages = groupStages() let baseRank = teamCount - teamsPerGroupStage * groupStageCount + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified groupStages.forEach { groupStage in let groupStageTeams = groupStage.teams(true) for (index, team) in groupStageTeams.enumerated() { if team.qualified == false { let groupStageWidth = max(((index == qualifiedPerGroupStage) ? teamsPerGroupStage - groupStageAdditionalQualified : teamsPerGroupStage) * (index - qualifiedPerGroupStage), 0) let _index = baseRank + groupStageWidth + 1 if let existingTeams = teams[_index] { teams[_index] = existingTeams + [team.id] } else { teams[_index] = [team.id] } } } } return teams } func lockRegistration() { closedRegistrationDate = Date() let count = selectedSortedTeams().count if teamCount != count { teamCount = count } let teams = unsortedTeams() teams.forEach { team in team.lockedWeight = 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.setComputedRank(in: self) } team.setWeight(from: players, inTournamentCategory: tournamentCategory) 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 if currentMonthData() == nil { let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate) let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate) await MainActor.run { let monthData = MonthData(monthKey: URL.importDateFormatter.string(from: newDate)) monthData.maleUnrankedValue = lastRankMan monthData.femaleUnrankedValue = lastRankWoman try? DataStore.shared.monthData.addOrUpdate(instance: monthData) } } let lastRankMan = currentMonthData()?.maleUnrankedValue let lastRankWoman = currentMonthData()?.femaleUnrankedValue let dataURLs = SourceFileManager.shared.allFiles.filter({ $0.dateFromPath == newDate }) let sources = dataURLs.map { CSVParser(url: $0) } try await unsortedPlayers().concurrentForEach { player in try await player.updateRank(from: sources, lastRank: (player.sex == .female ? lastRankWoman : lastRankMan) ?? 0) } } 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 availableQualifiedTeams() -> [TeamRegistration] { unsortedTeams().filter({ $0.qualified && $0.bracketPosition == nil }) } func qualifiedTeams() -> [TeamRegistration] { unsortedTeams().filter({ $0.qualifiedFromGroupStage() }) } 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.qualifiedFromGroupStage() == false }) } else { return [] } } func groupStagesAreOver() -> Bool { let groupStages = groupStages() guard groupStages.isEmpty == false else { return true } return groupStages.allSatisfy({ $0.hasEnded() }) //return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified } func paymentMethodMessage() -> String? { DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods } var entryFeeMessage: String { if let entryFee { return ["Inscription: " + entryFee.formatted(.currency(code: "EUR")) + " par joueur.", paymentMethodMessage()].compactMap { $0 }.joined(separator: "\n") } else { return "Inscription: gratuite." } } func umpireMail() -> [String]? { return [DataStore.shared.user.email] } func earnings() -> Double { if let entryFee { return Double(selectedPlayers().filter { $0.hasPaid() }.count) * entryFee } else { return 0.0 } } func paidCompletion() -> Double { let selectedPlayers = selectedPlayers() if selectedPlayers.isEmpty { return 0 } return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count) } typealias TournamentStatus = (label:String, completion: String) func cashierStatus() -> TournamentStatus { let selectedPlayers = selectedPlayers() let paid = selectedPlayers.filter({ $0.hasPaid() }) let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés" let completion = (Double(paid.count) / Double(selectedPlayers.count)) let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) return TournamentStatus(label: label, completion: completionLabel) } func scheduleStatus() -> TournamentStatus { let allMatches = allMatches() let ready = allMatches.filter({ $0.startDate != nil }) 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) } func callStatus() -> TournamentStatus { let selectedSortedTeams = selectedSortedTeams() let called = selectedSortedTeams.filter{ $0.called() } let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " paires convoqué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) } 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 + 1).formatted() }.joined(separator: ", ") + " en cours" } return groupStages().count.formatted() + " poule" + groupStages().count.pluralSuffix } else { return "Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { ($0.index + 1).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() { deleteStructure() deleteGroupStages() buildGroupStages() buildBracket() } func buildGroupStages() { guard groupStages().isEmpty else { return } var _groupStages = [GroupStage]() for index in 0.. Int { let bracketTeamCount = teamCount - (teamsPerGroupStage - qualifiedPerGroupStage) * groupStageCount + (groupStageAdditionalQualified * (groupStageCount > 0 ? 1 : 0)) return bracketTeamCount } func buildBracket() { guard rounds().isEmpty else { return } let roundCount = RoundRule.numberOfRounds(forTeams: bracketTeamCount()) let rounds = (0.. 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 { deleteGroupStages() buildGroupStages() } 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 indexOf(team: TeamRegistration) -> Int? { selectedSortedTeams().firstIndex(where: { $0.id == team.id }) } func labelIndexOf(team: TeamRegistration) -> String? { if let teamIndex = indexOf(team: team) { return "#" + (teamIndex + 1).formatted() } else { return nil } } func addTeam(_ players: Set, registrationDate: Date? = nil) -> TeamRegistration { let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date()) team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory) players.forEach { player in player.teamRegistration = team.id } return team } var matchFormat: MatchFormat { get { roundFormat ?? .defaultFormatForMatchType(.bracket) } set { roundFormat = newValue } } var groupStageMatchFormat: MatchFormat { get { groupStageFormat ?? .defaultFormatForMatchType(.groupStage) } set { groupStageFormat = newValue } } var loserBracketMatchFormat: MatchFormat { get { loserRoundFormat ?? .defaultFormatForMatchType(.loserBracket) } set { loserRoundFormat = newValue } } var groupStageOrderingMode: GroupStageOrderingMode { get { groupStageSortMode } set { groupStageSortMode = newValue } } var tournamentCategory: TournamentCategory { get { federalCategory } set { if federalCategory != newValue { federalCategory = newValue updateWeights() } else { federalCategory = newValue } } } var tournamentLevel: TournamentLevel { get { federalLevelCategory } set { federalLevelCategory = newValue teamSorting = newValue.defaultTeamSortingType groupStageMatchFormat = groupStageSmartMatchFormat() loserBracketMatchFormat = loserBracketSmartMatchFormat(1) matchFormat = roundSmartMatchFormat(1) } } var federalTournamentAge: FederalTournamentAge { get { federalAgeCategory } set { federalAgeCategory = newValue } } 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 setupFederalSettings() { teamSorting = tournamentLevel.defaultTeamSortingType groupStageMatchFormat = groupStageSmartMatchFormat() loserBracketMatchFormat = loserBracketSmartMatchFormat(1) matchFormat = roundSmartMatchFormat(1) } func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { let format = tournamentLevel.federalFormatForBracketRound(roundIndex) if matchFormat.rank > format.rank { return format } else { return matchFormat } } private func _defaultSorting() -> [MySortDescriptor] { switch teamSorting { case .rank: [.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.registrationDate!)] case .inscriptionDate: [.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight)] } } func isSameBuild(_ build: any TournamentBuildHolder) -> Bool { tournamentLevel == build.level && tournamentCategory == build.category && federalTournamentAge == build.age } private let _currentSelectionSorting : [MySortDescriptor] = [.keyPath(\.weight), .keyPath(\.registrationDate!)] 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()) try Store.main.deleteDependencies(items: self._matchSchedulers()) } private func _matchSchedulers() -> [MatchScheduler] { Store.main.filter(isIncluded: { $0.id == self.id }) } func matchScheduler() -> MatchScheduler? { _matchSchedulers().first } func currentMonthData() -> MonthData? { guard let rankSourceDate else { return nil } let dateString = URL.importDateFormatter.string(from: rankSourceDate) return Store.main.filter(isIncluded: { $0.monthKey == dateString }).first } var maleUnrankedValue: Int? { currentMonthData()?.maleUnrankedValue } var femaleUnrankedValue: Int? { currentMonthData()?.femaleUnrankedValue } func courtNameIfAvailable(atIndex courtIndex: Int) -> String? { club()?.courts.first(where: { $0.index == courtIndex })?.name } func courtName(atIndex courtIndex: Int) -> String { courtNameIfAvailable(atIndex: courtIndex) ?? Court.courtIndexedTitle(atIndex: courtIndex) } // MARK: - Payments & Crypto fileprivate var _currentPayment: TournamentPayment? = nil fileprivate var _currentCanceled: Bool? = nil // func setPayment(_ payment: TournamentPayment) { // // let max: Int = TournamentPayment.allCases.count // self._currentPayment = payment // var sequence = (1...18).map { _ in Int.random(in: (0.. TournamentPayment? { // if let payment { // do { // let decoded: String = try payment.decryptData(pass: Key.pass.rawValue) // let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue } // return TournamentPayment(rawValue: sequence[18]) // } catch { // Logger.error(error) // } // } // return nil // } // func setCanceled(_ canceled: Bool) { // // let max: Int = 9 // self._currentCanceled = canceled // var sequence = (1...18).map { _ in Int.random(in: (0.. Bool? { // if let isCanceled { // do { // let decoded: String = try isCanceled.decryptData(pass: Key.pass.rawValue) // let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue } // return Bool.decodeInt(sequence[18]) // } catch { // Logger.error(error) // } // } // return nil // } enum PaymentError: Error { case cantPayTournament } func payIfNecessary() throws { if let payment = Guard.main.paymentForNewTournament() { self.payment = payment try DataStore.shared.tournaments.addOrUpdate(instance: self) return } throw PaymentError.cantPayTournament } } fileprivate extension Bool { var encodedValue: Int { switch self { case true: return Int.random(in: (0...4)) case false: return Int.random(in: (5...9)) } } 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" // } //} 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 { func buildHolderTitle() -> String { tournamentTitle() } var category: TournamentCategory { tournamentCategory } var level: TournamentLevel { tournamentLevel } var age: FederalTournamentAge { federalTournamentAge } } extension Tournament { static func newEmptyInstance() -> Tournament { let lastDataSource: String? = DataStore.shared.appSettings.lastDataSource var _mostRecentDateAvailable: Date? { guard let lastDataSource else { return nil } return URL.importDateFormatter.date(from: lastDataSource) } let rankSourceDate = _mostRecentDateAvailable let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil }.sorted(by: \.startDate).reversed() let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments) let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments) let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments) //creator: DataStore.shared.user?.id return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge) } 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) } }