// // Tournament.swift // PadelClub // // Created by Laurent Morvillier on 02/02/2024. // import Foundation import LeStorage import SwiftUI @Observable final class Tournament : ModelObject, Storable { static func resourceName() -> String { "tournaments" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return false } static var relationshipNames: [String] = [] 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 var shouldVerifyGroupStage: Bool = false var shouldVerifyBracket: Bool = false var hideTeamsWeight: Bool = false var publishTournament: Bool = false var hidePointsEarned: Bool = false var publishRankings: Bool = false var loserBracketMode: LoserBracketMode = .automatic @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" case _shouldVerifyGroupStage = "shouldVerifyGroupStage" case _shouldVerifyBracket = "shouldVerifyBracket" case _hideTeamsWeight = "hideTeamsWeight" case _publishTournament = "publishTournament" case _hidePointsEarned = "hidePointsEarned" case _publishRankings = "publishRankings" case _loserBracketMode = "loserBracketMode" } 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, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic) { self.event = event self.name = name self.startDate = startDate self.endDate = endDate self.creationDate = creationDate self.isPrivate = Guard.main.purchasedTransactions.isEmpty 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 self.shouldVerifyBracket = shouldVerifyBracket self.shouldVerifyGroupStage = shouldVerifyGroupStage self.hideTeamsWeight = hideTeamsWeight self.publishTournament = publishTournament self.hidePointsEarned = hidePointsEarned self.publishRankings = publishRankings self.loserBracketMode = loserBracketMode } 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 shouldVerifyBracket = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyBracket) ?? false shouldVerifyGroupStage = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyGroupStage) ?? false hideTeamsWeight = try container.decodeIfPresent(Bool.self, forKey: ._hideTeamsWeight) ?? false publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic } 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: CryptoKey.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: CryptoKey.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) if let event { try container.encode(event, forKey: ._event) } else { try container.encodeNil(forKey: ._event) } if let name { try container.encode(name, forKey: ._name) } else { try container.encodeNil(forKey: ._name) } try container.encode(startDate, forKey: ._startDate) if let endDate { try container.encode(endDate, forKey: ._endDate) } else { try container.encodeNil(forKey: ._endDate) } try container.encode(creationDate, forKey: ._creationDate) try container.encode(isPrivate, forKey: ._isPrivate) if let groupStageFormat { try container.encode(groupStageFormat, forKey: ._groupStageFormat) } else { try container.encodeNil(forKey: ._groupStageFormat) } if let roundFormat { try container.encode(roundFormat, forKey: ._roundFormat) } else { try container.encodeNil(forKey: ._roundFormat) } if let loserRoundFormat { try container.encode(loserRoundFormat, forKey: ._loserRoundFormat) } else { try container.encodeNil(forKey: ._loserRoundFormat) } try container.encode(groupStageSortMode, forKey: ._groupStageSortMode) try container.encode(groupStageCount, forKey: ._groupStageCount) if let rankSourceDate { try container.encode(rankSourceDate, forKey: ._rankSourceDate) } else { try container.encodeNil(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) if let closedRegistrationDate { try container.encode(closedRegistrationDate, forKey: ._closedRegistrationDate) } else { try container.encodeNil(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) if let entryFee { try container.encode(entryFee, forKey: ._entryFee) } else { try container.encodeNil(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) try container.encode(shouldVerifyBracket, forKey: ._shouldVerifyBracket) try container.encode(shouldVerifyGroupStage, forKey: ._shouldVerifyGroupStage) try container.encode(hideTeamsWeight, forKey: ._hideTeamsWeight) try container.encode(publishTournament, forKey: ._publishTournament) try container.encode(hidePointsEarned, forKey: ._hidePointsEarned) try container.encode(publishRankings, forKey: ._publishRankings) } 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...max)) } sequence.append(self.isCanceled.encodedValue) sequence.append(contentsOf: (1...13).map { _ in Int.random(in: (0...max ))} ) let stringCombo: [String] = sequence.map { $0.formatted() } let joined: String = stringCombo.joined(separator: "") if let data = joined.data(using: .utf8) { let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue) try container.encode(encryped, forKey: ._isCanceled) } } var tournamentStore: TournamentStore { return TournamentStore.instance(tournamentId: self.id) } override func deleteDependencies() throws { let store = self.tournamentStore let teams = self.tournamentStore.teamRegistrations for team in teams { try team.deleteDependencies() } store.teamRegistrations.deleteDependencies(teams) let groups = self.tournamentStore.groupStages for group in groups { try group.deleteDependencies() } store.groupStages.deleteDependencies(groups) let rounds = self.self.tournamentStore.rounds for round in rounds { try round.deleteDependencies() } store.rounds.deleteDependencies(rounds) store.matchSchedulers.deleteDependencies(self._matchSchedulers()) } // MARK: - Computed Dependencies func unsortedTeams() -> [TeamRegistration] { return Array(self.tournamentStore.teamRegistrations) } func groupStages() -> [GroupStage] { let groupStages: [GroupStage] = self.tournamentStore.groupStages.filter { $0.tournament == self.id } return groupStages.sorted(by: \.index) } func allGroupStages() -> [GroupStage] { return Array(self.tournamentStore.groupStages) } func allRounds() -> [Round] { return Array(self.tournamentStore.rounds) } // MARK: - /// Warning: if the enum has more than 10 cases, the payment algo is broken enum TournamentPayment: Int, CaseIterable { case free, unit, subscriptionUnit, unlimited var isSubscription: Bool { switch self { case .subscriptionUnit, .unlimited: return true default: return false } } } enum State { case initial case build case running case canceled case finished } func eventLabel() -> String { if let event = eventObject(), let name = event.name { return name } else { return "" } } func publishedTournamentDate() -> Date { return min(creationDate.tomorrowAtNine, startDate) } func publishedTeamsDate() -> Date { return self.startDate } func canBePublished() -> Bool { switch state() { case .build, .finished, .running: return unsortedTeams().count > 3 default: return false } } func isTournamentPublished() -> Bool { return (Date() >= publishedTournamentDate()) || publishTournament } func areTeamsPublished() -> Bool { return Date() >= startDate || publishTeams } func areSummonsPublished() -> Bool { return Date() >= startDate || publishSummons } fileprivate func _publishedDateFromMatches(_ matches: [Match]) -> Date? { let startDates: [Date] = matches.compactMap { $0.startDate } let sortedDates: [Date] = startDates.sorted() if let first: Date = sortedDates.first?.atEightAM() { if first.isEarlierThan(startDate) { return startDate } else { return first } } else { return startDate } } func publishedGroupStagesDate() -> Date? { let matches: [Match] = self.groupStages().flatMap { $0.playedMatches() } return self._publishedDateFromMatches(matches) } func areGroupStagesPublished() -> Bool { if publishGroupStages { return true } if let publishedGroupStagesDate = publishedGroupStagesDate() { return Date() >= publishedGroupStagesDate } else { return false } } func publishedBracketsDate() -> Date? { let matches: [Match] = self.rounds().flatMap { $0.playedMatches() } return self._publishedDateFromMatches(matches) } func areBracketsPublished() -> Bool { if publishBrackets { return true } if let publishedBracketsDate = publishedBracketsDate() { return Date() >= publishedBracketsDate } else { return false } } func shareURL(_ pageLink: PageLink = .matches) -> URL? { if pageLink == .clubBroadcast { let club = club() print("club", club) print("club broadcast code", club?.broadcastCode) if let club, let broadcastCode = club.broadcastCode { return URLs.main.url.appending(path: "c/\(broadcastCode)") } else { return nil } } return URLs.main.url.appending(path: "tournament/\(id)").appending(path: pageLink.path) } func courtUsed() -> [Int] { #if DEBUG //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func courtUsed()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif let runningMatches: [Match] = self.tournamentStore.matches.filter { $0.isRunning() } return Set(runningMatches.compactMap { $0.courtIndex }).sorted() } func hasStarted() -> Bool { return startDate <= Date() } func eventObject() -> Event? { guard let event else { return nil } return Store.main.findById(event) } func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String { let selectedSortedTeams = selectedSortedTeams() switch exportFormat { case .rawText: return (selectedSortedTeams.compactMap { $0.pasteData(exportFormat) } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData(exportFormat) }).joined(separator: exportFormat.newLineSeparator(2)) case .csv: let headers = ["N°", "Nom Prénom", "rang", "Nom Prénom", "rang", "poids"].joined(separator: exportFormat.separator()) var teamPaste = [headers] for (index, team) in selectedSortedTeams.enumerated() { teamPaste.append(team.pasteData(exportFormat, index + 1)) } return teamPaste.joined(separator: exportFormat.newLineSeparator()) } } func club() -> Club? { return eventObject()?.clubObject() } func locationLabel(_ displayStyle: DisplayStyle = .wide) -> String { if let club = club() { switch displayStyle { case .wide, .title: return club.name case .short: return club.acronym } } else { return "" } } func hasEnded() -> Bool { return endDate != nil } func state() -> Tournament.State { if self.isCanceled == true { return .canceled } if self.hasEnded() { return .finished } let isBuild = (groupStageCount > 0 && groupStages().isEmpty == false) || rounds().isEmpty == false if isBuild && startDate <= Date() { return .running } if isBuild { return .build } return .initial } func seededTeams() -> [TeamRegistration] { return selectedSortedTeams().filter({ $0.bracketPosition != nil && $0.groupStagePosition == nil }) } func groupStageTeams() -> [TeamRegistration] { return selectedSortedTeams().filter({ $0.groupStagePosition != nil }) } func groupStageSpots() -> Int { return groupStages().map { $0.size }.reduce(0,+) } func seeds() -> [TeamRegistration] { let selectedSortedTeams = selectedSortedTeams() let seeds = max(selectedSortedTeams.count - groupStageSpots() , 0) return Array(selectedSortedTeams.prefix(seeds)) } func availableSeeds() -> [TeamRegistration] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func availableSeeds()", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif return seeds().filter { $0.isSeedable() } } 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? { return self.tournamentStore.rounds.first(where: { $0.index == roundIndex }) // return Store.main.filter(isIncluded: { $0.tournament == id && $0.index == roundIndex }).first } func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] { return getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.isEmpty() } ?? [] } func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] { return getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.hasSpaceLeft() } ?? [] } 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 && 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 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: [Round] = self.rounds() let unfinishedRounds: [Round] = rounds.filter { $0.hasStarted() && $0.hasEnded() == false } let sortedRounds: [Round] = unfinishedRounds.sorted(by: \.index).reversed() let round = sortedRounds.first ?? rounds.last(where: { $0.hasEnded() }) ?? rounds.first if withSeeds { if round?.seeds().isEmpty == false { return round } else { return nil } } else { return round } } func getPlayedMatchDateIntervals(in event: Event) -> [DateInterval] { let allMatches: [Match] = self.allMatches().filter { $0.courtIndex != nil && $0.startDate != nil } return allMatches.map { match in DateInterval(event: event.id, courtIndex: match.courtIndex!, startDate: match.startDate!, endDate: match.estimatedEndDate(additionalEstimationDuration)!) } } func allRoundMatches() -> [Match] { return allRounds().flatMap { $0._matches() } } func allMatches() -> [Match] { return self.tournamentStore.matches.filter { $0.disabled == false } } func _allMatchesIncludingDisabled() -> [Match] { return Array(self.tournamentStore.matches) } func rounds() -> [Round] { let rounds: [Round] = self.tournamentStore.rounds.filter { $0.isUpperBracket() } return rounds.sorted(by: \.index).reversed() } func sortedTeams() -> [TeamRegistration] { let teams = selectedSortedTeams() return teams + waitingListTeams(in: teams, includingWalkOuts: true) } func waitingListSortedTeams() -> [TeamRegistration] { let teams = selectedSortedTeams() return waitingListTeams(in: teams, includingWalkOuts: false) } func selectedSortedTeams() -> [TeamRegistration] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif var _sortedTeams : [TeamRegistration] = [] var _teams = unsortedTeams().filter({ $0.walkOut == false }) if let closedRegistrationDate { _teams = _teams.filter({ team in if let registrationDate = team.registrationDate { return registrationDate <= closedRegistrationDate } else { return true } }) } 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) let groupStageSpots: Int = self.groupStageSpots() var bracketSeeds: Int = min(teamCount, _completeTeams.count) - groupStageSpots - wcBracket.count var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count if groupStageTeamCount < 0 { groupStageTeamCount = 0 } if bracketSeeds < 0 { bracketSeeds = 0 } if prioritizeClubMembers { var bracketTeams: [TeamRegistration] = [] bracketTeams.append(contentsOf: _completeTeams.filter { $0.hasMemberOfClub(clubName) }) let others: [TeamRegistration] = _completeTeams.filter { $0.hasMemberOfClub(clubName) == false } let sortedOthers: [TeamRegistration] = others.sorted(using: defaultSorting, order: .ascending) bracketTeams.append(contentsOf: sortedOthers) bracketTeams = bracketTeams .prefix(bracketSeeds) .sorted(using: _currentSelectionSorting, order: .ascending) bracketTeams.append(contentsOf: wcBracket) // let bracketTeams: [TeamRegistration] = (_completeTeams.filter { $0.hasMemberOfClub(clubName) } + _completeTeams.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket let groupStageTeamsNoFiltering = Set(_completeTeams).subtracting(bracketTeams) let groupStageTeams = (groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) } + groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage _sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending) } else { let bracketTeams = _completeTeams.prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket let groupStageTeams = Set(_completeTeams).subtracting(bracketTeams).sorted(using: defaultSorting, order: .ascending).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage _sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending) } return _sortedTeams } func waitingListTeams(in teams: [TeamRegistration], includingWalkOuts: Bool) -> [TeamRegistration] { let waitingList = Set(unsortedTeams()).subtracting(teams) let waitings = waitingList.filter { $0.walkOut == false }.sorted(using: _defaultSorting(), order: .ascending) let walkOuts = waitingList.filter { $0.walkOut == true }.sorted(using: _defaultSorting(), order: .ascending) if includingWalkOuts { return waitings + walkOuts } else { return waitings } } func bracketCut(teamCount: Int) -> Int { return max(0, teamCount - groupStageCut()) } func groupStageCut() -> Int { return groupStageSpots() } func cutLabel(index: Int, teamCount: Int?) -> String { let _teamCount = teamCount ?? selectedSortedTeams().count let bracketCut = bracketCut(teamCount: _teamCount) if index < bracketCut { return "Tableau" } else if index - bracketCut < groupStageCut() && _teamCount > 0 { return "Poule" } else { return "Attente" } } func cutLabelColor(index: Int?, teamCount: Int?) -> Color { guard let index else { return Color.gray } let _teamCount = teamCount ?? selectedSortedTeams().count let bracketCut = bracketCut(teamCount: _teamCount) if index < bracketCut { return Color.mint } else if index - bracketCut < groupStageCut() && _teamCount > 0 { return Color.cyan } else { return Color.gray } } func unsortedTeamsWithoutWO() -> [TeamRegistration] { return self.tournamentStore.teamRegistrations.filter { $0.walkOut == false } // return Store.main.filter { $0.tournament == self.id && $0.walkOut == false } } func walkoutTeams() -> [TeamRegistration] { return self.tournamentStore.teamRegistrations.filter { $0.walkOut == true } // return Store.main.filter { $0.tournament == self.id && $0.walkOut == true } } 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 homonyms(in players: [PlayerRegistration]) -> [PlayerRegistration] { players.filter({ $0.hasHomonym() }) } func unsortedPlayers() -> [PlayerRegistration] { return Array(self.tournamentStore.playerRegistrations) } func selectedPlayers() -> [PlayerRegistration] { return self.selectedSortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.computedRank) } func paidSelectedPlayers(type: PlayerRegistration.PlayerPaymentType) -> Double? { if let entryFee { return Double(self.selectedSortedTeams().flatMap { $0.unsortedPlayers() }.filter { $0.paymentType == type }.count) * entryFee } else { return nil } } func players() -> [PlayerRegistration] { return self.tournamentStore.playerRegistrations.sorted(by: \.computedRank) } func unrankValue(for malePlayer: Bool) -> Int? { switch tournamentCategory { case .unlisted: return nil case .men: return maleUnrankedValue case .women: return femaleUnrankedValue case .mix: return malePlayer ? maleUnrankedValue : femaleUnrankedValue } } //todo var clubName: String? { return self.eventObject()?.clubObject()?.name } //todo func significantPlayerCount() -> Int { return 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: -13, 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 = self.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]() let players = players().filter { $0.licenceId != nil } teams.forEach { team in if let previousTeam = team.previousTeam { previousTeam.updatePlayers(team.players, inTournamentCategory: team.tournamentCategory) teamsToImport.append(previousTeam) } else { var registrationDate = team.registrationDate if let previousPlayer = players.first(where: { player in let ids = team.players.compactMap({ $0.licenceId }) return ids.contains(player.licenceId!) }), let previousTeamRegistrationDate = previousPlayer.team()?.registrationDate { registrationDate = previousTeamRegistrationDate } let newTeam = addTeam(team.players, registrationDate: registrationDate, name: team.name) teamsToImport.append(newTeam) } } do { try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teamsToImport) } catch { Logger.error(error) } do { try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: teams.flatMap { $0.players }) } catch { Logger.error(error) } if state() == .build && groupStageCount > 0 && groupStageTeams().isEmpty { setGroupStage(randomize: groupStageSortMode == .random) } } 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, includingWalkOuts: true) 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 summonDate = team.callDate else { return true } guard let expectedSummonDate = team.expectedSummonDate() else { return true } return Calendar.current.compare(summonDate, to: expectedSummonDate, toGranularity: .minute) != ComparisonResult.orderedSame } func groupStagesMatches() -> [Match] { return self.tournamentStore.matches.filter { $0.groupStage != nil } // return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) } func availableToStart(_ allMatches: [Match], in runningMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif return allMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }).sorted(by: \.computedStartDateForSorting) } func runningMatches(_ allMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) } func readyMatches(_ allMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) } func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func tournament finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if let limit { return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(limit)) } else { return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed()) } } func teamsRanked() -> [TeamRegistration] { let selected = selectedSortedTeams().filter({ $0.finalRanking != nil }) return selected.sorted(by: \.finalRanking!, order: .ascending) } private func _removeStrings(from dictionary: inout [Int: [String]], stringsToRemove: [String]) { for key in dictionary.keys { if var stringArray = dictionary[key] { // Remove all instances of each string in stringsToRemove stringArray.removeAll { stringsToRemove.contains($0) } dictionary[key] = stringArray } } } func finalRanking() async -> [Int: [String]] { var teams: [Int: [String]] = [:] var ids: Set = Set() let rounds = rounds() let final = rounds.last?.playedMatches().last if let winner = final?.winningTeamId { teams[1] = [winner] ids.insert(winner) } if let finalist = final?.losingTeamId { teams[2] = [finalist] ids.insert(finalist) } let others: [Round] = rounds.flatMap { round in let losers = round.losers() let minimumFinalPosition = round.seedInterval()?.last ?? teamCount if teams[minimumFinalPosition] == nil { teams[minimumFinalPosition] = losers.map { $0.id } } else { teams[minimumFinalPosition]?.append(contentsOf: losers.map { $0.id }) } print("round", round.roundTitle()) let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false } print(rounds.count, rounds.map { $0.roundTitle() }) return rounds }.compactMap({ $0 }) others.forEach { round in print("round", round.roundTitle()) if let interval = round.seedInterval() { print("interval", interval.localizedInterval()) let playedMatches = round.playedMatches().filter { $0.disabled == false || $0.isReady() } print("playedMatches", playedMatches.count) let winners = playedMatches.compactMap({ $0.winningTeamId }).filter({ ids.contains($0) == false }) print("winners", winners.count) let losers = playedMatches.compactMap({ $0.losingTeamId }).filter({ ids.contains($0) == false }) print("losers", losers.count) if winners.isEmpty { let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false }) if disabledIds.isEmpty == false { _removeStrings(from: &teams, stringsToRemove: disabledIds) teams[interval.last] = disabledIds let teamNames : [String] = disabledIds.compactMap { let t : TeamRegistration? = tournamentStore.teamRegistrations.findById($0) return t }.map { $0.canonicalName } print("winners.isEmpty", "\(interval.last) : ", teamNames) disabledIds.forEach { ids.insert($0) } } } else { if winners.isEmpty == false { _removeStrings(from: &teams, stringsToRemove: winners) teams[interval.first + winners.count - 1] = winners let teamNames : [String] = winners.compactMap { let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) return t }.map { $0.canonicalName } print("winners", "\(interval.last + winners.count - 1) : ", teamNames) winners.forEach { ids.insert($0) } } if losers.isEmpty == false { _removeStrings(from: &teams, stringsToRemove: losers) teams[interval.first + winners.count] = losers let loserTeamNames : [String] = losers.compactMap { let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0) return t }.map { $0.canonicalName } print("losers", "\(interval.first + winners.count) : ", loserTeamNames) losers.forEach { ids.insert($0) } } } } } if let groupStageLoserBracketPlayedMatches = groupStageLoserBracket()?.playedMatches() { groupStageLoserBracketPlayedMatches.forEach({ match in if match.hasEnded() { let sameMatchIndexCount = groupStageLoserBracketPlayedMatches.filter({ $0.index == match.index }).count teams.setOrAppend(match.winningTeamId, at: match.index) teams.setOrAppend(match.losingTeamId, at: match.index + sameMatchIndexCount) } }) } let groupStages = groupStages() let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 })) groupStages.forEach { groupStage in let groupStageTeams = groupStage.teams(true) for (index, team) in groupStageTeams.enumerated() { if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false { let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0) let _index = baseRank + groupStageWidth + 1 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 } do { try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) } catch { Logger.error(error) } } func unlockRegistration() { closedRegistrationDate = nil let teams = unsortedTeams() teams.forEach { team in team.lockedWeight = nil } do { try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) } catch { Logger.error(error) } } 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) do { try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) } catch { Logger.error(error) } } do { try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) } catch { Logger.error(error) } } 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 formatted: String = URL.importDateFormatter.string(from: newDate) let monthData: MonthData = MonthData(monthKey: formatted) monthData.maleUnrankedValue = lastRankMan monthData.femaleUnrankedValue = lastRankWoman do { try DataStore.shared.monthData.addOrUpdate(instance: monthData) } catch { Logger.error(error) } } } 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 { return maleUnrankedValue == nil || femaleUnrankedValue == nil } func findTeam(_ players: [PlayerRegistration]) -> TeamRegistration? { return unsortedTeams().first(where: { $0.includes(players: players) }) } func tournamentTitle(_ displayStyle: DisplayStyle = .wide) -> String { if tournamentLevel == .unlisted, displayStyle == .title, let name { return name } let title: String = [tournamentLevel.localizedLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedLabel(displayStyle)].joined(separator: " ") if displayStyle == .wide, let name { return [title, name].joined(separator: " - ") } else { return title } } func localizedTournamentType() -> String { switch tournamentLevel { case .unlisted: return tournamentLevel.localizedLabel(.short) default: return tournamentLevel.localizedLabel(.short) + tournamentCategory.localizedLabel(.short) } } func hideWeight() -> Bool { return tournamentLevel.hideWeight() } func isAnimation() -> Bool { federalLevelCategory == .unlisted } func subtitle(_ displayStyle: DisplayStyle = .wide) -> String { return name ?? "" } func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String { switch displayStyle { case .wide, .title: startDate.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted) case .short: startDate.formatted(date: .numeric, time: .omitted) } } func qualifiedFromGroupStage() -> Int { return groupStageCount * qualifiedPerGroupStage } func availableQualifiedTeams() -> [TeamRegistration] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func availableQualifiedTeams()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif return unsortedTeams().filter({ $0.qualified && $0.bracketPosition == nil }) } func qualifiedTeams() -> [TeamRegistration] { return unsortedTeams().filter({ $0.qualified }) } func moreQualifiedToDraw() -> Int { return max((qualifiedFromGroupStage() + groupStageAdditionalQualified) - qualifiedTeams().count, 0) } func missingQualifiedFromGroupStages() -> [TeamRegistration] { if groupStageAdditionalQualified > 0 && groupStagesAreOver() { return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in groupStage.teams(true)[safe: qualifiedPerGroupStage] } .filter({ $0.qualified == false }) } else { return [] } } func groupStagesAreOver() -> Bool { let groupStages = groupStages() guard groupStages.isEmpty == false else { return true } return groupStages.allSatisfy({ $0.hasEnded() }) //return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified } fileprivate func _paymentMethodMessage() -> String? { return DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods } var entryFeeMessage: String { if let entryFee { let message: String = "Inscription: \(entryFee.formatted(.currency(code: "EUR"))) par joueur." return [message, self._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() async -> TournamentStatus { let selectedPlayers = selectedPlayers() let paid = selectedPlayers.filter({ $0.hasPaid() }) // let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés" 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() async -> TournamentStatus { let allMatches = allMatches() let ready = allMatches.filter({ $0.startDate != nil }) // let label = ready.count.formatted() + " / " + allMatches.count.formatted() + " matchs programmés" let label = "\(ready.count.formatted()) / \(allMatches.count.formatted()) matchs programmés" let completion = (Double(ready.count) / Double(allMatches.count)) let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) return TournamentStatus(label: label, completion: completionLabel) } func callStatus() async -> TournamentStatus { let selectedSortedTeams = selectedSortedTeams() let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false } let label = "\(called.count.formatted()) / \(selectedSortedTeams.count.formatted()) convoquées au bon horaire" let completion = (Double(called.count) / Double(selectedSortedTeams.count)) let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) return TournamentStatus(label: label, completion: completionLabel) } func confirmedSummonStatus() async -> TournamentStatus { let selectedSortedTeams = selectedSortedTeams() let called = selectedSortedTeams.filter { $0.confirmationDate != nil } let label = "\(called.count.formatted()) / \(selectedSortedTeams.count.formatted()) confirmées" let completion = (Double(called.count) / Double(selectedSortedTeams.count)) let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) return TournamentStatus(label: label, completion: completionLabel) } func bracketStatus() async -> (status: String, description: String?, cut: TeamRegistration.TeamRange?) { let availableSeeds = availableSeeds() var description: String? = nil if availableSeeds.isEmpty == false { description = "placer \(availableSeeds.count) équipe\(availableSeeds.count.pluralSuffix)" } if description == nil { let availableQualifiedTeams = availableQualifiedTeams() if availableQualifiedTeams.isEmpty == false { description = "placer \(availableQualifiedTeams.count) qualifié" + availableQualifiedTeams.count.pluralSuffix } } var cut: TeamRegistration.TeamRange? = nil if description == nil && isAnimation() == false { cut = TeamRegistration.TeamRange(availableSeeds.first, availableSeeds.last) } if let round = getActiveRound() { return ([round.roundTitle(.short), round.roundStatus()].joined(separator: " ").lowercased(), description, cut) } else { return ("", description, nil) } } func groupStageStatus() async -> (status: String, cut: TeamRegistration.TeamRange?) { let groupStageTeams = groupStageTeams() let groupStageTeamsCount = groupStageTeams.count if groupStageTeamsCount == 0 || groupStageTeamsCount != groupStageSpots() { return ("à compléter", nil) } let cut : TeamRegistration.TeamRange? = isAnimation() ? nil : TeamRegistration.TeamRange(groupStageTeams.first, groupStageTeams.last) let runningGroupStages = groupStages().filter({ $0.isRunning() }) if groupStagesAreOver() { return ("terminées", cut) } if runningGroupStages.isEmpty { let ongoingGroupStages = runningGroupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }) if ongoingGroupStages.isEmpty == false { return ("Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours", cut) } return (groupStages().count.formatted() + " poule" + groupStages().count.pluralSuffix, cut) } else { return ("Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours", cut) } } func settingsDescriptionLocalizedLabel() -> String { [courtCount.formatted() + " terrain\(courtCount.pluralSuffix)", entryFeeMessage].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 deleteAndBuildEverything() { resetBracketPosition() 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.. Match? { guard let bracketPosition else { return nil } let matchIndex = bracketPosition / 2 let roundIndex = RoundRule.roundIndex(fromMatchIndex: matchIndex) if let round: Round = self.getRound(atRoundIndex: roundIndex) { return self.tournamentStore.matches.first(where: { $0.round == round.id && $0.index == matchIndex }) // return Store.main.filter(isIncluded: { $0.round == round.id && $0.index == matchIndex }).first } return nil } func resetTeamScores(in matchOfBracketPosition: Int?, outsideOf: [TeamScore] = []) { guard let match = match(for: matchOfBracketPosition) else { return } match.resetTeamScores(outsideOf: outsideOf) } func updateTeamScores(in matchOfBracketPosition: Int?) { guard let match = match(for: matchOfBracketPosition) else { return } match.updateTeamScores() } func deleteStructure() { do { try self.tournamentStore.rounds.delete(contentOfs: rounds()) } catch { Logger.error(error) } } func resetBracketPosition() { unsortedTeams().forEach({ $0.bracketPosition = nil }) } func deleteGroupStages() { do { try self.tournamentStore.groupStages.delete(contentOfs: groupStages()) } catch { Logger.error(error) } } 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 { deleteGroupStages() buildGroupStages() } else { setGroupStageTeams(randomize: randomize) groupStages.forEach { $0.buildMatches() } } } func setGroupStageTeams(randomize: Bool) { let groupStages = groupStages() let max = groupStages.map { $0.size }.reduce(0,+) var chunks = selectedSortedTeams().suffix(max).chunked(into: groupStageCount) for (index, _) in chunks.enumerated() { if randomize { chunks[index].shuffle() } else if index % 2 != 0 { chunks[index].reverse() } print("Equipes \(chunks[index].map { $0.weight })") for (jIndex, _) in chunks[index].enumerated() { print("Position \(index + 1) Poule \(groupStages[jIndex].index)") chunks[index][jIndex].groupStage = groupStages[jIndex].id chunks[index][jIndex].groupStagePosition = index } } do { try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) } catch { Logger.error(error) } } func isFree() -> Bool { return entryFee == nil || entryFee == 0 } func indexOf(team: TeamRegistration) -> Int? { return 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, name: String? = nil) -> TeamRegistration { let date: Date = registrationDate ?? Date() let team = TeamRegistration(tournament: id, registrationDate: date, name: name) 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(5) } } var federalTournamentAge: FederalTournamentAge { get { federalAgeCategory } set { federalAgeCategory = newValue } } func loserBracketSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { let format = tournamentLevel.federalFormatForLoserBracketRound(roundIndex) if tournamentLevel == .p25 { return .superTie } if format.rank < loserBracketMatchFormat.rank { return format } else { return loserBracketMatchFormat } } func groupStageSmartMatchFormat() -> MatchFormat { let format = tournamentLevel.federalFormatForGroupStage() if tournamentLevel == .p25 { return .superTie } if format.rank < groupStageMatchFormat.rank { return format } else { return groupStageMatchFormat } } func setupFederalSettings() { teamSorting = tournamentLevel.defaultTeamSortingType groupStageMatchFormat = groupStageSmartMatchFormat() loserBracketMatchFormat = loserBracketSmartMatchFormat(5) matchFormat = roundSmartMatchFormat(5) } func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { let format = tournamentLevel.federalFormatForBracketRound(roundIndex) if tournamentLevel == .p25 { return .superTie } if format.rank < matchFormat.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!)] private func _matchSchedulers() -> [MatchScheduler] { return self.tournamentStore.matchSchedulers.filter { $0.tournament == self.id } // DataStore.shared.matchSchedulers.filter(isIncluded: { $0.tournament == self.id }) } func matchScheduler() -> MatchScheduler? { return self._matchSchedulers().first } func currentMonthData() -> MonthData? { guard let rankSourceDate else { return nil } let dateString = URL.importDateFormatter.string(from: rankSourceDate) return DataStore.shared.monthData.first(where: { $0.monthKey == dateString }) } var maleUnrankedValue: Int? { return currentMonthData()?.maleUnrankedValue } var femaleUnrankedValue: Int? { return currentMonthData()?.femaleUnrankedValue } func courtNameIfAvailable(atIndex courtIndex: Int) -> String? { return club()?.customizedCourts.first(where: { $0.index == courtIndex })?.name } func courtName(atIndex courtIndex: Int) -> String { return courtNameIfAvailable(atIndex: courtIndex) ?? Court.courtIndexedTitle(atIndex: courtIndex) } func tournamentWinner() -> TeamRegistration? { let finals: Round? = self.tournamentStore.rounds.first(where: { $0.index == 0 && $0.isUpperBracket() }) return finals?.playedMatches().first?.winner() } func getGroupStageChunkValue() -> Int { if groupStageCount > 0 && teamsPerGroupStage >= 2 { let result = courtCount / (teamsPerGroupStage / 2) let remainder = courtCount % (teamsPerGroupStage / 2) let value = remainder == 0 ? result : result + 1 return min(groupStageCount, value) } else { return 1 } } func replacementRangeExtended(groupStagePosition: Int) -> TeamRegistration.TeamRange? { let selectedSortedTeams = selectedSortedTeams() var left: TeamRegistration? = nil if groupStagePosition == 0 { left = seeds().last } else { let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition - 1 }).sorted(by: \.weight) left = previousHat.last } var right: TeamRegistration? = nil if groupStagePosition == teamsPerGroupStage - 1 { right = nil } else { let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition + 1 }).sorted(by: \.weight) right = previousHat.first } return (left: left, right: right) } typealias TeamPlacementIssue = (shouldBeInIt: [String], shouldNotBeInIt: [String]) func groupStageTeamPlacementIssue() -> TeamPlacementIssue { let selected = selectedSortedTeams() let allTeams = unsortedTeams() let newGroup = selected.suffix(groupStageSpots()) let currentGroup = allTeams.filter({ $0.groupStagePosition != nil }) let selectedIds = newGroup.map { $0.id } let groupIds = currentGroup.map { $0.id } let shouldBeInIt = Set(selectedIds).subtracting(groupIds) let shouldNotBeInIt = Set(groupIds).subtracting(selectedIds) return (Array(shouldBeInIt), Array(shouldNotBeInIt)) } func bracketTeamPlacementIssue() -> TeamPlacementIssue { let selected = selectedSortedTeams() let allTeams = unsortedTeams() let seedCount = max(selected.count - groupStageSpots(), 0) let newGroup = selected.prefix(seedCount) + selected.filter({ $0.qualified }) let currentGroup = allTeams.filter({ $0.bracketPosition != nil }) let selectedIds = newGroup.map { $0.id } let groupIds = currentGroup.map { $0.id } let shouldBeInIt = Set(selectedIds).subtracting(groupIds) let shouldNotBeInIt = Set(groupIds).subtracting(selectedIds) return (Array(shouldBeInIt), Array(shouldNotBeInIt)) } func groupStageLoserBracket() -> Round? { tournamentStore.rounds.first(where: { $0.groupStageLoserBracket }) } func groupStageLoserBracketsInitialPlace() -> Int { return teamCount - teamsPerGroupStage * groupStageCount + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified + 1 } // MARK: - func insertOnServer() throws { DataStore.shared.tournaments.writeChangeAndInsertOnServer(instance: self) for teamRegistration in self.tournamentStore.teamRegistrations { teamRegistration.insertOnServer() } for groupStage in self.tournamentStore.groupStages { groupStage.insertOnServer() } for round in self.tournamentStore.rounds { round.insertOnServer() } } // MARK: - Payments & Crypto enum PaymentError: Error { case cantPayTournament } func payIfNecessary() throws { if self.payment != nil { return } 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 codeClub: String? { club()?.code } var holderId: String { id } func clubLabel() -> String { locationLabel() } func subtitleLabel() -> String { subtitle() } var tournaments: [any TournamentBuildHolder] { [ self ] } var dayPeriod: DayPeriod { let day = startDate.get(.weekday) switch day { case 2...6: return .week default: return .weekend } } } extension Tournament: TournamentBuildHolder { func buildHolderTitle() -> String { tournamentTitle(.short) } 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 && $0.isDeleted == false } 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, loserBracketMode: DataStore.shared.user.loserBracketMode) } 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) } }