You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
PadelClub/PadelClub/Data/Tournament.swift

2781 lines
113 KiB

//
// 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
var initialSeedRound: Int = 0
var initialSeedCount: Int = 0
var enableOnlineRegistration: Bool = false
var registrationDateLimit: Date? = nil
var openingRegistrationDate: Date? = nil
var waitingListLimit: Int? = nil
var accountIsRequired: Bool = true
var licenseIsRequired: Bool = true
var minimumPlayerPerTeam: Int = 2
var maximumPlayerPerTeam: Int = 2
var information: String? = nil
@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"
case _initialSeedRound = "initialSeedRound"
case _initialSeedCount = "initialSeedCount"
case _enableOnlineRegistration = "enableOnlineRegistration"
case _registrationDateLimit = "registrationDateLimit"
case _openingRegistrationDate = "openingRegistrationDate"
case _waitingListLimit = "waitingListLimit"
case _accountIsRequired = "accountIsRequired"
case _licenseIsRequired = "licenseIsRequired"
case _minimumPlayerPerTeam = "minimumPlayerPerTeam"
case _maximumPlayerPerTeam = "maximumPlayerPerTeam"
case _information = "information"
}
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, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil) {
self.event = event
self.name = name
self.startDate = startDate
self.endDate = endDate
self.creationDate = creationDate
#if DEBUG
self.isPrivate = false
#else
if Guard.main.currentPlan == .monthlyUnlimited {
self.isPrivate = true
} else {
self.isPrivate = Guard.main.purchasedTransactions.isEmpty
}
#endif
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
#if DEBUG
self.publishTeams = true
self.publishSummons = true
self.publishBrackets = true
self.publishGroupStages = true
self.publishRankings = true
self.publishTournament = true
#else
self.publishTeams = publishTeams
self.publishSummons = publishSummons
self.publishBrackets = publishBrackets
self.publishGroupStages = publishGroupStages
self.publishRankings = publishRankings
self.publishTournament = publishTournament
#endif
self.shouldVerifyBracket = shouldVerifyBracket
self.shouldVerifyGroupStage = shouldVerifyGroupStage
self.hideTeamsWeight = hideTeamsWeight
self.hidePointsEarned = hidePointsEarned
self.loserBracketMode = loserBracketMode
self.initialSeedRound = initialSeedRound
self.initialSeedCount = initialSeedCount
self.enableOnlineRegistration = enableOnlineRegistration
self.registrationDateLimit = registrationDateLimit
self.openingRegistrationDate = openingRegistrationDate
self.waitingListLimit = waitingListLimit
self.accountIsRequired = accountIsRequired
self.licenseIsRequired = licenseIsRequired
self.minimumPlayerPerTeam = minimumPlayerPerTeam
self.maximumPlayerPerTeam = maximumPlayerPerTeam
self.information = information
}
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
initialSeedRound = try container.decodeIfPresent(Int.self, forKey: ._initialSeedRound) ?? 0
initialSeedCount = try container.decodeIfPresent(Int.self, forKey: ._initialSeedCount) ?? 0
enableOnlineRegistration = try container.decodeIfPresent(Bool.self, forKey: ._enableOnlineRegistration) ?? false
registrationDateLimit = try container.decodeIfPresent(Date.self, forKey: ._registrationDateLimit)
openingRegistrationDate = try container.decodeIfPresent(Date.self, forKey: ._openingRegistrationDate)
waitingListLimit = try container.decodeIfPresent(Int.self, forKey: ._waitingListLimit)
accountIsRequired = try container.decodeIfPresent(Bool.self, forKey: ._accountIsRequired) ?? true
licenseIsRequired = try container.decodeIfPresent(Bool.self, forKey: ._licenseIsRequired) ?? true
minimumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._minimumPlayerPerTeam) ?? 2
maximumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._maximumPlayerPerTeam) ?? 2
information = try container.decodeIfPresent(String.self, forKey: ._information)
}
fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter()
fileprivate static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) 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<CodingKeys>) 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)
try container.encode(event, forKey: ._event)
try container.encode(name, forKey: ._name)
try container.encode(startDate, forKey: ._startDate)
try container.encode(endDate, forKey: ._endDate)
try container.encode(creationDate, forKey: ._creationDate)
try container.encode(isPrivate, forKey: ._isPrivate)
try container.encode(groupStageFormat, forKey: ._groupStageFormat)
try container.encode(roundFormat, forKey: ._roundFormat)
try container.encode(loserRoundFormat, forKey: ._loserRoundFormat)
try container.encode(groupStageSortMode, forKey: ._groupStageSortMode)
try container.encode(groupStageCount, forKey: ._groupStageCount)
try container.encode(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.encode(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.encode(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)
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)
try container.encode(loserBracketMode, forKey: ._loserBracketMode)
try container.encode(initialSeedRound, forKey: ._initialSeedRound)
try container.encode(initialSeedCount, forKey: ._initialSeedCount)
try container.encode(enableOnlineRegistration, forKey: ._enableOnlineRegistration)
try container.encode(registrationDateLimit, forKey: ._registrationDateLimit)
try container.encode(openingRegistrationDate, forKey: ._openingRegistrationDate)
try container.encode(waitingListLimit, forKey: ._waitingListLimit)
try container.encode(accountIsRequired, forKey: ._accountIsRequired)
try container.encode(licenseIsRequired, forKey: ._licenseIsRequired)
try container.encode(minimumPlayerPerTeam, forKey: ._minimumPlayerPerTeam)
try container.encode(maximumPlayerPerTeam, forKey: ._maximumPlayerPerTeam)
try container.encode(information, forKey: ._information)
}
fileprivate func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) 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..<max)) }
sequence.append(payment.rawValue)
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.encodeIfPresent(encryped, forKey: ._payment)
}
}
func _encodeIsCanceled(container: inout KeyedEncodingContainer<CodingKeys>) 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 drawLogs = self.tournamentStore.drawLogs
for drawLog in drawLogs {
try drawLog.deleteDependencies()
}
store.drawLogs.deleteDependencies(drawLogs)
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.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(atStep step: Int = 0) -> [GroupStage] {
let groupStages: [GroupStage] = self.tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step }
return groupStages.sorted(by: \.index)
}
func allGroupStages() -> [GroupStage] {
return self.tournamentStore.groupStages.sorted(by: \GroupStage.computedOrder)
}
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(runningMatches: [Match]) -> [Int] {
#if _DEBUGING_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func courtUsed()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return Set(runningMatches.compactMap { $0.courtIndex }).sorted()
}
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() + waitingListSortedTeams()
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 = ["", "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() -> 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<SeedInterval>()
for (index, seed) in seeds.enumerated() {
if seed.isSeedable(), let seedGroup = seedGroup(for: index) {
availableSeedGroup.insert(seedGroup)
}
}
return availableSeedGroup.sorted(by: <)
}
func seedGroup(for alreadySetupSeeds: Int) -> SeedInterval? {
switch alreadySetupSeeds {
case 0...1:
return SeedInterval(first: 1, last: 2)
case 2...3:
return SeedInterval(first: 3, last: 4)
case 4...7:
return SeedInterval(first: 5, last: 8)
case 8...15:
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 availableSeedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 {
print("availableSeedGroup == SeedInterval(first: 3, last: 4)")
return availableSeedGroup
}
if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count {
return availableSeedGroup
} else if availableSeeds.count == availableSeedOpponentSpot.count && availableSeedGroup.count == availableSeedOpponentSpot.count {
return availableSeedGroup
} else if let chunks = availableSeedGroup.chunks() {
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 seedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 {
var spots = [Match]()
spots.append(availableSeedSpot[1])
spots.append(availableSeedSpot[4])
spots = spots.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false)
}
} else {
if 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(atStep step: Int = 0) -> GroupStage? {
let groupStages = groupStages(atStep: step)
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.isOutOfTournament() == false })
if let closedRegistrationDate {
_teams = _teams.filter({ team in
if let registrationDate = team.registrationDate {
return registrationDate <= closedRegistrationDate
} else {
return true
}
})
}
let defaultSorting : [MySortDescriptor<TeamRegistration>] = _defaultSorting()
let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false }.prefix(teamCount).sorted(using: [.keyPath(\.initialWeight), .keyPath(\.id)], order: .ascending)
let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending)
let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending)
let groupStageSpots: Int = self.groupStageSpots()
var bracketSeeds: Int = min(teamCount, _teams.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.isOutOfTournament() == false }.sorted(using: _defaultSorting(), order: .ascending)
let walkOuts = waitingList.filter { $0.walkOut == true }.sorted(using: _defaultSorting(), order: .ascending)
if includingWalkOuts {
return waitings + walkOuts
} else {
return waitings
}
}
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.isOutOfTournament() == 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 minimumPlayerPerTeam
}
func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
if startDate.isInCurrentYear() == false {
return []
}
return players.filter { player in
return isPlayerRankInadequate(player: player)
}
}
func isPlayerRankInadequate(player: PlayerHolder) -> Bool {
guard let rank = player.getRank() else { return false }
let _rank = player.male ? rank : rank + PlayerRegistration.addon(for: rank, manMax: maleUnrankedValue ?? 0, womanMax: femaleUnrankedValue ?? 0)
if _rank <= tournamentLevel.minimumPlayerRank(category: tournamentCategory, ageCategory: federalTournamentAge) {
return true
} else {
return false
}
}
func ageInadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
if startDate.isInCurrentYear() == false {
return []
}
return players.filter { player in
return isPlayerAgeInadequate(player: player)
}
}
func isPlayerAgeInadequate(player: PlayerHolder) -> Bool {
guard let computedAge = player.computedAge else { return false }
if federalTournamentAge.isAgeValid(age: computedAge) == false {
return true
} else {
return false
}
}
func licenseYearValidity() -> Int {
if startDate.get(.month) > 8 {
return startDate.get(.year) + 1
} else {
return startDate.get(.year)
}
}
func playersWithoutValidLicense(in players: [PlayerRegistration], isImported: Bool) -> [PlayerRegistration] {
let licenseYearValidity = self.licenseYearValidity()
return players.filter({ player in
if player.isImported() {
// Player is marked as imported: check if the license is valid
return !player.isValidLicenseNumber(year: licenseYearValidity)
} else {
// Player is not imported: validate license and handle `isImported` flag for non-imported players
let noLicenseId = player.licenceId == nil || player.licenceId?.isEmpty == true
let invalidFormattedLicense = player.formattedLicense().isLicenseNumber == false
// If global `isImported` is true, check license number as well
let invalidLicenseForImportedFlag = isImported && !player.isValidLicenseNumber(year: licenseYearValidity)
return noLicenseId || invalidFormattedLicense || invalidLicenseForImportedFlag
}
})
}
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)
if isAnimation() {
if newTeam.weight == 0 {
newTeam.weight = team.index(in: teams) ?? 0
}
}
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 homonyms = homonyms(in: players)
let ageInadequatePlayers = ageInadequatePlayers(in: players)
let isImported = players.anySatisfy({ $0.isImported() })
let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players, isImported: isImported)
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 + ageInadequatePlayers.count + homonyms.count
}
func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration, expectedSummonDate: Date? = nil) -> Bool {
guard let summonDate = team.callDate else { return true }
let expectedSummonDate : Date? = team.expectedSummonDate() ?? expectedSummonDate
guard let expectedSummonDate else { return true }
return Calendar.current.compare(summonDate, to: expectedSummonDate, toGranularity: .minute) != ComparisonResult.orderedSame
}
func groupStagesMatches(atStep step: Int = 0) -> [Match] {
return groupStages(atStep: step).flatMap({ $0._matches() })
// return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) })
}
static let defaultSorting : [MySortDescriptor<Match>] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.index)]
static func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return allMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(using: defaultSorting, order: .ascending)
}
static func runningMatches(_ allMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending)
}
static 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(using: defaultSorting, order: .ascending)
}
static func matchesLeft(_ allMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending)
}
static func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
if let limit {
return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(limit))
} else {
return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed())
}
}
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<String> = Set<String>()
let rounds = rounds()
let lastStep = lastStep()
if rounds.isEmpty, lastStep > 0 {
let groupStages = groupStages(atStep: lastStep)
for groupStage in groupStages {
let groupStageTeams = groupStage.teams(true)
for teamIndex in 0..<groupStageTeams.count {
teams[groupStage.index * groupStage.size + 1 + teamIndex] = [groupStageTeams[teamIndex].id]
}
}
} else {
let final = rounds.last?.playedMatches().last
if let winner = final?.winningTeamId {
teams[1] = [winner]
ids.insert(winner)
}
if let finalist = final?.losingTeamId {
teams[2] = [finalist]
ids.insert(finalist)
}
let others: [Round] = rounds.flatMap { round in
let losers = round.losers()
let minimumFinalPosition = round.seedInterval()?.last ?? teamCount
if teams[minimumFinalPosition] == nil {
teams[minimumFinalPosition] = losers.map { $0.id }
} else {
teams[minimumFinalPosition]?.append(contentsOf: losers.map { $0.id })
}
print("round", round.roundTitle())
let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false }
print(rounds.count, rounds.map { $0.roundTitle() })
return rounds
}.compactMap({ $0 })
others.forEach { round in
print("round", round.roundTitle())
if let interval = round.seedInterval() {
print("interval", interval.localizedInterval())
let playedMatches = round.playedMatches().filter { $0.disabled == false || $0.isReady() }
print("playedMatches", playedMatches.count)
let winners = playedMatches.compactMap({ $0.winningTeamId }).filter({ ids.contains($0) == false })
print("winners", winners.count)
let losers = playedMatches.compactMap({ $0.losingTeamId }).filter({ ids.contains($0) == false })
print("losers", losers.count)
if winners.isEmpty {
let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false })
if disabledIds.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: disabledIds)
teams[interval.last] = disabledIds
let teamNames : [String] = disabledIds.compactMap {
let t : TeamRegistration? = 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 - (index > qualifiedPerGroupStage ? groupStageAdditionalQualified : 0)
if let existingTeams = teams[_index] {
teams[_index] = existingTeams + [team.id]
} else {
teams[_index] = [team.id]
}
}
}
}
}
return teams
}
func setRankings(finalRanks: [Int: [String]]) async -> [Int: [TeamRegistration]] {
var rankings: [Int: [TeamRegistration]] = [:]
finalRanks.keys.sorted().forEach { rank in
if let rankedTeamIds = finalRanks[rank] {
let teams: [TeamRegistration] = rankedTeamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) }
rankings[rank] = teams
}
}
rankings.keys.sorted().forEach { rank in
if let rankedTeams = rankings[rank] {
rankedTeams.forEach { team in
team.finalRanking = rank
team.pointsEarned = isAnimation() ? nil : tournamentLevel.points(for: rank - 1, count: teamCount)
}
}
}
do {
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
} catch {
Logger.error(error)
}
if self.publishRankings == false {
self.publishRankings = true
do {
try DataStore.shared.tournaments.addOrUpdate(instance: self)
} catch {
Logger.error(error)
}
}
return rankings
}
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 {
#if DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
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) }
let players = unsortedPlayers()
try await players.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, hideSenior: Bool = false) -> String {
if tournamentLevel == .unlisted, displayStyle == .title {
if let name {
return name
} else {
return tournamentLevel.localizedLevelLabel(.title)
}
}
let displayStyleCategory = hideSenior ? .short : displayStyle
var levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle)]
if displayStyle == .short {
levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle) + tournamentCategory.localizedLabel(displayStyle)]
}
let array = levelCategory + [federalTournamentAge.localizedFederalAgeLabel(displayStyleCategory)]
let title: String = array.filter({ $0.isEmpty == false }).joined(separator: " ")
if displayStyle == .wide, let name {
return [title, name].joined(separator: " - ")
} else {
return title
}
}
func localizedTournamentType() -> String {
switch tournamentLevel {
case .unlisted:
return tournamentLevel.localizedLevelLabel(.short)
default:
return tournamentLevel.localizedLevelLabel(.short) + tournamentCategory.localizedLabel(.short)
}
}
func hideWeight() -> Bool {
return hideTeamsWeight || tournamentLevel.hideWeight()
}
func isAnimation() -> Bool {
federalLevelCategory == .unlisted
}
func subtitle(_ displayStyle: DisplayStyle = .wide) -> String {
return name ?? ""
}
func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .title:
startDate.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).year())
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 {
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(atStep: Int = 0) -> Bool {
let groupStages = groupStages(atStep: atStep)
guard groupStages.isEmpty == false else {
return true
}
return groupStages.allSatisfy({ $0.hasEnded() })
//return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
}
func groupStageLoserBracketAreOver() -> Bool {
guard let groupStageLoserBracket = groupStageLoserBracket() else {
return true
}
return groupStageLoserBracket.hasEnded()
}
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: Locale.defaultCurrency()))) 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)
}
func presenceStatus() -> Double {
let selectedPlayers = selectedPlayers()
if selectedPlayers.isEmpty { return 0 }
return Double(selectedPlayers.filter { $0.hasArrived }.count) / Double(selectedPlayers.count)
}
typealias TournamentStatus = (label:String, completion: String)
func cashierStatus() async -> TournamentStatus {
let selectedPlayers = selectedPlayers()
var filteredPlayers = [PlayerRegistration]()
var wording = ""
if isFree() {
wording = "présent"
filteredPlayers = selectedPlayers.filter({ $0.hasArrived })
} else {
wording = "encaissé"
filteredPlayers = selectedPlayers.filter({ $0.hasPaid() })
}
// let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés"
let label = "\(filteredPlayers.count.formatted()) / \(selectedPlayers.count.formatted()) joueurs \(wording)\(filteredPlayers.count.pluralSuffix)"
let completion = (Double(filteredPlayers.count) / Double(selectedPlayers.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
}
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 justCalled = selectedSortedTeams.filter { $0.called() }
let label = "\(justCalled.count.formatted()) / \(selectedSortedTeams.count.formatted()) (\(called.count.formatted()) au bon horaire)"
let completion = (Double(called.count) / Double(selectedSortedTeams.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
}
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(preset: PadelTournamentStructurePreset = .manual) {
resetBracketPosition()
deleteStructure()
deleteGroupStages()
switch preset {
case .doubleGroupStage:
buildGroupStages()
addNewGroupStageStep()
qualifiedPerGroupStage = 0
groupStageAdditionalQualified = 0
default:
buildGroupStages()
buildBracket()
}
}
func addWildCardIfNeeded(_ count: Int, _ type: MatchType) {
let currentCount = selectedSortedTeams().filter({
if type == .bracket {
return $0.wildCardBracket
} else {
return $0.wildCardGroupStage
}
}).count
if currentCount < count {
let _diff = count - currentCount
addWildCard(_diff, type)
}
}
func addWildCard(_ count: Int, _ type: MatchType) {
let wcs = (0..<count).map { _ in
let team = TeamRegistration(tournament: id)
if type == .bracket {
team.wildCardBracket = true
} else {
team.wildCardGroupStage = true
}
return team
}
do {
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: wcs)
} catch {
Logger.error(error)
}
}
func addEmptyTeamRegistration(_ count: Int) {
let teams = (0..<count).map { _ in
let team = TeamRegistration(tournament: id)
return team
}
do {
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
} catch {
Logger.error(error)
}
}
func buildGroupStages() {
guard groupStages().isEmpty else {
return
}
var _groupStages = [GroupStage]()
for index in 0..<groupStageCount {
let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, matchFormat: groupStageSmartMatchFormat())
_groupStages.append(groupStage)
}
do {
try self.tournamentStore.groupStages.addOrUpdate(contentOfs: _groupStages)
} catch {
Logger.error(error)
}
refreshGroupStages()
}
func bracketTeamCount() -> 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..<roundCount).map { //index 0 is the final
return Round(tournament: id, index: $0, matchFormat: roundSmartMatchFormat($0), loserBracketMode: loserBracketMode)
}
if rounds.isEmpty {
return
}
do {
try self.tournamentStore.rounds.addOrUpdate(contentOfs: rounds)
} catch {
Logger.error(error)
}
let matchCount = RoundRule.numberOfMatches(forTeams: bracketTeamCount())
let matches = (0..<matchCount).map { //0 is final match
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
let round = rounds[roundIndex]
return Match(round: round.id, index: $0, matchFormat: round.matchFormat, name: Match.setServerTitle(upperRound: round, matchIndex: RoundRule.matchIndexWithinRound(fromMatchIndex: $0)))
}
print(matches.map {
(RoundRule.roundName(fromMatchIndex: $0.index), RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index))
})
do {
try self.tournamentStore.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
rounds.forEach { round in
round.buildLoserBracket()
}
}
func match(for bracketPosition: Int?) -> Match? {
guard let bracketPosition else { return nil }
let matchIndex = bracketPosition / 2
let roundIndex = RoundRule.roundIndex(fromMatchIndex: matchIndex)
if let round: Round = self.getRound(atRoundIndex: roundIndex) {
return self.tournamentStore.matches.first(where: { $0.round == round.id && $0.index == matchIndex })
// return Store.main.filter(isIncluded: { $0.round == round.id && $0.index == matchIndex }).first
}
return nil
}
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: allGroupStages())
} catch {
Logger.error(error)
}
}
func refreshGroupStages(keepExistingMatches: Bool = false) {
unsortedTeams().forEach { team in
team.groupStage = nil
team.groupStagePosition = nil
}
if groupStageCount > 0 {
switch groupStageOrderingMode {
case .random:
setGroupStage(randomize: true, keepExistingMatches: keepExistingMatches)
case .snake:
setGroupStage(randomize: false, keepExistingMatches: keepExistingMatches)
case .swiss:
setGroupStage(randomize: true, keepExistingMatches: keepExistingMatches)
}
}
}
func setGroupStage(randomize: Bool, keepExistingMatches: Bool = false) {
let groupStages = groupStages()
let numberOfBracketsAsInt = groupStages.count
// let teamsPerBracket = teamsPerBracket
if groupStageCount != numberOfBracketsAsInt {
deleteGroupStages()
buildGroupStages()
} else {
setGroupStageTeams(randomize: randomize)
groupStages.forEach { $0.buildMatches(keepExistingMatches: keepExistingMatches) }
}
}
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 "Tête de série #" + (teamIndex + 1).formatted()
} else {
return nil
}
}
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration {
let team = TeamRegistration(tournament: id, registrationDate: registrationDate, name: name)
team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
players.forEach { player in
player.teamRegistration = team.id
}
if isAnimation() {
if team.weight == 0 {
team.weight = unsortedTeams().count
}
}
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(fromEvent event: Event?) {
teamSorting = tournamentLevel.defaultTeamSortingType
groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(5)
matchFormat = roundSmartMatchFormat(5)
entryFee = tournamentLevel.entryFee
if event?.tenupId != nil {
enableOnlineRegistration = true
registrationDateLimit = deadline(for: .inscription)
}
}
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<TeamRegistration>] {
switch teamSorting {
case .rank:
[.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)]
case .inscriptionDate:
[.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)]
}
}
func isSameBuild(_ build: any TournamentBuildHolder) -> Bool {
tournamentLevel == build.level
&& tournamentCategory == build.category
&& federalTournamentAge == build.age
}
private let _currentSelectionSorting : [MySortDescriptor<TeamRegistration>] = [.keyPath(\.weight), .keyPath(\.id)]
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 courtsAvailable() -> [Int] {
(0..<courtCount).map { $0 }
}
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 groupStageTeamsInBracket = selected.filter({ $0.qualified == false && $0.inGroupStage() && $0.inRound() })
let groupIds = currentGroup.map { $0.id } + groupStageTeamsInBracket.map { $0.id }
let shouldBeInIt = Set(selectedIds).subtracting(groupIds)
let shouldNotBeInIt = Set(groupIds).subtracting(selectedIds)
return (Array(shouldBeInIt), Array(shouldNotBeInIt))
}
func groupStageLoserBracket() -> Round? {
tournamentStore.rounds.first(where: { $0.groupStageLoserBracket })
}
func groupStageLoserBracketsInitialPlace() -> Int {
return teamCount - teamsPerGroupStage * groupStageCount + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified + 1
}
func addNewGroupStageStep() {
let lastStep = lastStep() + 1
for i in 0..<teamsPerGroupStage {
let gs = GroupStage(tournament: id, index: i, size: groupStageCount, step: lastStep)
do {
try tournamentStore.groupStages.addOrUpdate(instance: gs)
} catch {
Logger.error(error)
}
}
groupStages(atStep: 1).forEach { $0.buildMatches() }
}
func lastStep() -> Int {
self.tournamentStore.groupStages.sorted(by: \.step).last?.step ?? 0
}
func generateSmartLoserGroupStageBracket() {
guard let groupStageLoserBracket = groupStageLoserBracket() else { return }
for i in qualifiedPerGroupStage..<teamsPerGroupStage {
groupStages().chunked(into: 2).forEach { gss in
let placeCount = i * 2 + 1
let match = Match(round: groupStageLoserBracket.id, index: placeCount, matchFormat: groupStageLoserBracket.matchFormat)
match.setMatchName("\(placeCount)\(placeCount.ordinalFormattedSuffix(feminine: true)) place")
do {
try tournamentStore.matches.addOrUpdate(instance: match)
} catch {
Logger.error(error)
}
if let gs1 = gss.first, let gs2 = gss.last, let score1 = gs1.teams(true)[safe: i], let score2 = gs2.teams(true)[safe: i] {
print("rang \(i)")
print(score1.teamLabel(.short), "vs", score2.teamLabel(.short))
match.setLuckyLoser(team: score1, teamPosition: .one)
match.setLuckyLoser(team: score2, teamPosition: .two)
}
}
}
}
func updateTournamentState() {
Task {
if hasEnded() {
let fr = await finalRanking()
_ = await setRankings(finalRanks: fr)
}
}
}
func allLoserRoundMatches() -> [Match] {
rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) }
}
func seedsCount() -> Int {
selectedSortedTeams().count - groupStageSpots()
}
func lastDrawnDate() -> Date? {
drawLogs().last?.drawDate
}
func drawLogs() -> [DrawLog] {
self.tournamentStore.drawLogs.sorted(by: \.drawDate)
}
func seedSpotsLeft() -> Bool {
let alreadySeededRounds = rounds().filter({ $0.seeds().isEmpty == false })
if alreadySeededRounds.isEmpty { return true }
let spotsLeft = alreadySeededRounds.flatMap({ $0.playedMatches() }).filter { $0.isEmpty() || $0.isValidSpot() }
return spotsLeft.isEmpty == false
}
func isRoundValidForSeeding(roundIndex: Int) -> Bool {
if let lastRoundWithSeeds = rounds().last(where: { $0.seeds().isEmpty == false }) {
return roundIndex >= lastRoundWithSeeds.index
} else {
return true
}
}
func updateSeedsBracketPosition() async {
await removeAllSeeds()
let drawLogs = drawLogs().reversed()
let seeds = seeds()
for (index, seed) in seeds.enumerated() {
if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) {
drawLog.updateTeamBracketPosition(seed)
}
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: seeds)
} catch {
Logger.error(error)
}
}
func removeAllSeeds() async {
unsortedTeams().forEach({ team in
team.bracketPosition = nil
})
let ts = allRoundMatches().flatMap { match in
match.teamScores
}
do {
try tournamentStore.teamScores.delete(contentOfs: ts)
} catch {
Logger.error(error)
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
} catch {
Logger.error(error)
}
allRounds().forEach({ round in
round.enableRound()
})
}
func addNewRound(_ roundIndex: Int) async {
let round = Round(tournament: id, index: roundIndex, matchFormat: matchFormat)
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
let nextRound = round.nextRound()
var currentIndex = 0
let matches = (0..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, matchFormat: round.matchFormat)
if let nextRound, let followingMatch = self.tournamentStore.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
return match
}
do {
try tournamentStore.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
do {
try tournamentStore.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
round.buildLoserBracket()
matches.filter { $0.disabled }.forEach {
$0._toggleLoserMatchDisableState(true)
}
}
func exportedDrawLogs() -> String {
var logs : [String] = ["Journal des tirages\n\n"]
logs.append(drawLogs().map { $0.exportedDrawLog() }.joined(separator: "\n\n"))
return logs.joined()
}
func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool {
guard let source = eventObject()?.courtsUnavailability else { return false }
let courtLockedSchedule = source.filter({ $0.courtIndex == courtIndex })
return courtLockedSchedule.anySatisfy({ dateInterval in
let range = startDate..<endDate
return dateInterval.range.overlaps(range)
})
}
func getOnlineRegistrationStatus() -> OnlineRegistrationStatus {
if hasStarted() {
return .inProgress
}
if closedRegistrationDate != nil {
return .ended
}
if endDate != nil {
return .endedWithResults
}
let now = Date()
if let openingRegistrationDate = openingRegistrationDate {
let timezonedDateTime = openingRegistrationDate // Assuming dates are already in local timezone
if now < timezonedDateTime {
return .notStarted
}
}
if let registrationDateLimit = registrationDateLimit {
let timezonedDateTime = registrationDateLimit // Assuming dates are already in local timezone
if now > timezonedDateTime {
return .ended
}
}
let currentTeamCount = unsortedTeamsWithoutWO().count
if currentTeamCount >= teamCount {
if let waitingListLimit = waitingListLimit {
let waitingListCount = currentTeamCount - teamCount
if waitingListCount >= waitingListLimit {
return .waitingListFull
}
}
return .waitingListPossible
}
return .open
}
// MARK: - Status
func shouldTournamentBeOver() -> Bool {
#if _DEBUGING_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func shouldTournamentBeOver()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
if isDeleted == false && hasEnded() == false && hasStarted() {
let allMatches = allMatches()
let remainingMatches = allMatches.filter({ $0.hasEnded() == false && $0.startDate != nil })
let calendar = Calendar.current
let anyTomorrow = remainingMatches.anySatisfy({ calendar.isDateInTomorrow($0.startDate!) })
if anyTomorrow == false, let endDate = allMatches.filter({ $0.hasEnded() }).sorted(by: \.endDate!, order: .ascending).last?.endDate, endDate.timeIntervalSinceNow <= -2 * 3600 {
return true
}
}
return false
}
// 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 {
func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String {
if isAnimation() {
if let name {
return name.trunc(length: DeviceHelper.charLength())
} else if build.age == .unlisted, build.category == .unlisted {
return build.level.localizedLevelLabel(.title)
} else {
return build.level.localizedLevelLabel(displayStyle)
}
}
return build.level.localizedLevelLabel(displayStyle)
}
var codeClub: String? {
club()?.code
}
var holderId: String { id }
func clubLabel() -> String {
locationLabel()
}
func subtitleLabel(forBuild build: any TournamentBuildHolder) -> String {
if isAnimation() {
if displayAgeAndCategory(forBuild: build) == false {
return [build.category.localizedLabel(), build.age.localizedFederalAgeLabel()].filter({ $0.isEmpty == false }).joined(separator: " ")
} else if name != nil {
return build.level.localizedLevelLabel(.title)
} else {
return ""
}
} else {
return subtitle()
}
}
var tournaments: [any TournamentBuildHolder] {
[
self
]
}
var dayPeriod: DayPeriod {
let day = startDate.get(.weekday)
switch day {
case 2...6:
return .week
default:
return .weekend
}
}
func displayAgeAndCategory(forBuild build: any TournamentBuildHolder) -> Bool {
if isAnimation() {
if let name, name.count < DeviceHelper.maxCharacter() {
return true
} else if build.age == .unlisted, build.category == .unlisted {
return true
} else {
return DeviceHelper.isBigScreen()
}
}
return true
}
}
extension Tournament: TournamentBuildHolder {
func buildHolderTitle(_ displayStyle: DisplayStyle) -> 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)
}
}
extension Tournament {
func deadline(for type: TournamentDeadlineType) -> Date? {
guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil }
var daysOffset = type.daysOffset
if tournamentLevel == .p500 {
daysOffset += 7
}
if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) {
let startOfDay = Calendar.current.startOfDay(for: date)
return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay)
}
return nil
}
}