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

1551 lines
62 KiB

//
// Tournament.swift
// PadelClub
//
// Created by Laurent Morvillier on 02/02/2024.
//
import Foundation
import LeStorage
@Observable
class Tournament : ModelObject, Storable {
static func resourceName() -> String { "tournaments" }
var id: String = Store.randomId()
var event: String?
var 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 groupStageCourtCount: Int?
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
@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 _groupStageCourtCount = "groupStageCourtCount"
case _seedCount = "seedCount"
case _closedRegistrationDate = "closedRegistrationDate"
case _groupStageAdditionalQualified = "groupStageAdditionalQualified"
case _courtCount = "courtCount"
case _prioritizeClubMembers = "prioritizeClubMembers"
case _qualifiedPerGroupStage = "qualifiedPerGroupStage"
case _teamsPerGroupStage = "teamsPerGroupStage"
case _entryFee = "entryFee"
case _additionalEstimationDuration = "additionalEstimationDuration"
case _isDeleted = "isDeleted"
case _isCanceled = "localId"
case _payment = "globalId"
}
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, 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, groupStageCourtCount: Int? = nil, 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) {
self.event = event
self.name = name
self.startDate = startDate
self.endDate = endDate
self.creationDate = creationDate
self.isPrivate = isPrivate
self.groupStageFormat = groupStageFormat
self.roundFormat = roundFormat
self.loserRoundFormat = loserRoundFormat
self.groupStageSortMode = groupStageSortMode
self.groupStageCount = groupStageCount
self.rankSourceDate = rankSourceDate
self.dayDuration = dayDuration
self.teamCount = teamCount
self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType
self.federalCategory = federalCategory
self.federalLevelCategory = federalLevelCategory
self.federalAgeCategory = federalAgeCategory
self.groupStageCourtCount = groupStageCourtCount
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
}
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)
groupStageCourtCount = try container.decodeIfPresent(Int.self, forKey: ._groupStageCourtCount)
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)
}
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: Key.pass.rawValue)
let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue }
return TournamentPayment(rawValue: sequence[18])
} catch {
Logger.error(error)
}
}
return nil
}
fileprivate static func _decodeCanceled(container: KeyedDecodingContainer<CodingKeys>) throws -> Bool {
let data = try container.decodeIfPresent(Data.self, forKey: ._isCanceled)
if let data {
do {
let decoded: String = try data.decryptData(pass: Key.pass.rawValue)
let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue }
return Bool.decodeInt(sequence[18])
} catch {
Logger.error(error)
}
}
return false
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: ._id)
try container.encodeIfPresent(event, forKey: ._event)
try container.encodeIfPresent(name, forKey: ._name)
try container.encode(startDate, forKey: ._startDate)
try container.encodeIfPresent(endDate, forKey: ._endDate)
try container.encode(creationDate, forKey: ._creationDate)
try container.encode(isPrivate, forKey: ._isPrivate)
try container.encodeIfPresent(groupStageFormat, forKey: ._groupStageFormat)
try container.encodeIfPresent(roundFormat, forKey: ._roundFormat)
try container.encodeIfPresent(loserRoundFormat, forKey: ._loserRoundFormat)
try container.encode(groupStageSortMode, forKey: ._groupStageSortMode)
try container.encode(groupStageCount, forKey: ._groupStageCount)
try container.encodeIfPresent(rankSourceDate, forKey: ._rankSourceDate)
try container.encode(dayDuration, forKey: ._dayDuration)
try container.encode(teamCount, forKey: ._teamCount)
try container.encode(teamSorting, forKey: ._teamSorting)
try container.encode(federalCategory, forKey: ._federalCategory)
try container.encode(federalLevelCategory, forKey: ._federalLevelCategory)
try container.encode(federalAgeCategory, forKey: ._federalAgeCategory)
try container.encodeIfPresent(groupStageCourtCount, forKey: ._groupStageCourtCount)
try container.encodeIfPresent(closedRegistrationDate, forKey: ._closedRegistrationDate)
try container.encode(groupStageAdditionalQualified, forKey: ._groupStageAdditionalQualified)
try container.encode(courtCount, forKey: ._courtCount)
try container.encode(prioritizeClubMembers, forKey: ._prioritizeClubMembers)
try container.encode(qualifiedPerGroupStage, forKey: ._qualifiedPerGroupStage)
try container.encode(teamsPerGroupStage, forKey: ._teamsPerGroupStage)
try container.encodeIfPresent(entryFee, forKey: ._entryFee)
try self._encodePayment(container: &container)
try container.encode(additionalEstimationDuration, forKey: ._additionalEstimationDuration)
try container.encode(isDeleted, forKey: ._isDeleted)
try self._encodeIsCanceled(container: &container)
}
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: Key.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: Key.pass.rawValue)
try container.encode(encryped, forKey: ._isCanceled)
}
}
/// 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 canceled
}
func shareURL() -> URL? {
return URLs.main.url.appending(path: "tournament/\(id)")
}
func broadcastURL() -> URL? {
return URLs.main.url.appending(path: "tournament/\(id)/broadcast")
}
func courtUsed() -> [Int] {
let runningMatches : [Match] = Store.main.filter(isIncluded: { $0.isRunning() }).filter({ $0.tournamentId() == self.id })
return Set(runningMatches.compactMap { $0.courtIndex }).sorted()
}
func hasStarted() -> Bool {
startDate <= Date()
}
func eventObject() -> Event? {
guard let event else { return nil }
return Store.main.findById(event)
}
func pasteDataForImporting() -> String {
let selectedSortedTeams = selectedSortedTeams()
return (selectedSortedTeams.compactMap { $0.pasteData() } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams).compactMap { $0.pasteData() }).joined(separator: "\n\n")
}
func club() -> Club? {
eventObject()?.clubObject()
}
func locationLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if let club = club() {
switch displayStyle {
case .wide:
return club.name
case .short:
return club.acronym
}
} else {
return ""
}
}
func hasEnded() -> Bool {
endDate != nil
}
func state() -> Tournament.State {
if self.isCanceled == true {
return .canceled
}
if (groupStageCount > 0 && groupStages().isEmpty == false)
|| rounds().isEmpty == false {
return .build
}
return .initial
}
func seededTeams() -> [TeamRegistration] {
selectedSortedTeams().filter({ $0.bracketPosition != nil && $0.groupStagePosition == nil })
}
func groupStageTeams() -> [TeamRegistration] {
selectedSortedTeams().filter({ $0.bracketPosition == nil && $0.groupStagePosition != nil })
}
func seeds() -> [TeamRegistration] {
let seeds = max(teamCount - groupStageCount * teamsPerGroupStage, 0)
return Array(selectedSortedTeams().prefix(seeds))
}
func availableSeeds() -> [TeamRegistration] {
return seeds().filter { $0.isSeedable() }
}
func lastSeedRound() -> Int {
if let last = seeds().filter({ $0.bracketPosition != nil }).last {
return RoundRule.roundIndex(fromMatchIndex: last.bracketPosition! / 2)
} else {
return 0
}
}
func getRound(atRoundIndex roundIndex: Int) -> Round? {
Store.main.filter(isIncluded: { $0.tournament == id && $0.index == roundIndex }).first
}
func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] {
getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 0 } ?? []
}
func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] {
getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 1 } ?? []
}
func availableSeedGroups() -> [SeedInterval] {
let seeds = seeds()
var availableSeedGroup = Set<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 availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count {
return availableSeedGroup
} else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) && availableSeedGroup.count == availableSeedOpponentSpot.count {
return availableSeedGroup
} else if let chunks = availableSeedGroup.chunks() {
if let chunk = chunks.first(where: { seedInterval in
seedInterval.first >= self.seededTeams().count
}) {
return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk)
}
}
}
return nil
}
func setSeeds(inRoundIndex roundIndex: Int, inSeedGroup seedGroup: SeedInterval) {
if seedGroup == SeedInterval(first: 1, last: 2) {
let seeds = seeds()
if let matches = getRound(atRoundIndex: roundIndex)?.playedMatches() {
if let lastMatch = matches.last {
seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, slot: .two, opposingSeeding: false)
}
if let firstMatch = matches.first {
seeds.prefix(2).dropFirst().first?.setSeedPosition(inSpot: firstMatch, slot: .one, opposingSeeding: false)
}
}
} else {
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
let availableSeeds = seeds(inSeedGroup: seedGroup)
if availableSeeds.count <= availableSeedSpot.count {
let spots = availableSeedSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false)
}
} else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) {
let spots = availableSeedOpponentSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true)
}
} else if let chunks = seedGroup.chunks() {
if let chunk = chunks.first(where: { seedInterval in
seedInterval.first >= self.seededTeams().count
}) {
setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk)
}
}
}
}
func inscriptionClosed() -> Bool {
closedRegistrationDate != nil
}
func groupStages() -> [GroupStage] {
Store.main.filter { $0.tournament == self.id }.sorted(by: \.index)
}
func getActiveGroupStage() -> GroupStage? {
let groupStages = groupStages()
return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first
}
func matchesWithSpace() -> [Match] {
getActiveRound()?.playedMatches().filter({ $0.hasSpaceLeft() }) ?? []
}
func getActiveRound(withSeeds: Bool = false) -> Round? {
let rounds = rounds()
let round = rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.last(where: { $0.hasEnded() }) ?? rounds.first
if withSeeds {
if round?.seeds().isEmpty == false {
return round
} else {
return nil
}
} else {
return round
}
}
func allRoundMatches() -> [Match] {
allRounds().flatMap { $0._matches() }
}
func allMatches() -> [Match] {
let unsortedGroupStages : [GroupStage] = Store.main.filter { $0.tournament == self.id }
let matches: [Match] = unsortedGroupStages.flatMap { $0._matches() } + allRoundMatches()
return matches.filter({ $0.disabled == false })
}
func _allMatchesIncludingDisabled() -> [Match] {
let unsortedGroupStages : [GroupStage] = Store.main.filter { $0.tournament == self.id }
return unsortedGroupStages.flatMap { $0._matches() } + allRounds().flatMap { $0._matches() }
}
func allRounds() -> [Round] {
Store.main.filter { $0.tournament == self.id }
}
func rounds() -> [Round] {
Store.main.filter { $0.tournament == self.id && $0.parent == nil }.sorted(by: \.index).reversed()
}
func sortedTeams() -> [TeamRegistration] {
let teams = selectedSortedTeams()
return teams + waitingListTeams(in: teams)
}
func selectedSortedTeams() -> [TeamRegistration] {
//let start = Date()
var _sortedTeams : [TeamRegistration] = []
let _teams = unsortedTeams().filter({ $0.walkOut == false })
let defaultSorting : [MySortDescriptor<TeamRegistration>] = _defaultSorting()
let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false}
let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending)
let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending)
var bracketSeeds = min(teamCount, _completeTeams.count) - groupStageCount * teamsPerGroupStage - wcBracket.count
var groupStageTeamCount = groupStageCount * teamsPerGroupStage - wcGroupStage.count
if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
if bracketSeeds < 0 { bracketSeeds = 0 }
if prioritizeClubMembers {
let bracketTeams = (_completeTeams.filter { $0.hasMemberOfClub(clubName) } + _completeTeams.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket
let groupStageTeamsNoFiltering = Set(_completeTeams).subtracting(bracketTeams)
let groupStageTeams = (groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) } + groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage
_sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending)
} else {
let bracketTeams = _completeTeams.prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket
let groupStageTeams = Set(_completeTeams).subtracting(bracketTeams).sorted(using: defaultSorting, order: .ascending).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage
_sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending)
}
//let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
//print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
return _sortedTeams
}
func waitingListTeams(in teams: [TeamRegistration]) -> [TeamRegistration] {
let waitingList = Set(unsortedTeams()).subtracting(teams)
return waitingList.filter { $0.walkOut == false }.sorted(using: _defaultSorting(), order: .ascending) + waitingList.filter { $0.walkOut == true }.sorted(using: _defaultSorting(), order: .ascending)
}
func bracketCut() -> Int {
max(0, teamCount - groupStageCut())
}
func groupStageCut() -> Int {
groupStageCount * teamsPerGroupStage
}
func cutLabel(index: Int) -> String {
if index < bracketCut() {
return "Tableau"
} else if index - bracketCut() < groupStageCut() {
return "Poule"
} else {
return "Liste d'attente"
}
}
func unsortedTeams() -> [TeamRegistration] {
Store.main.filter { $0.tournament == self.id }
}
func duplicates(in players: [PlayerRegistration]) -> [PlayerRegistration] {
var duplicates = [PlayerRegistration]()
Set(players.compactMap({ $0.licenceId })).forEach { licenceId in
let found = players.filter({ $0.licenceId == licenceId })
if found.count > 1 {
duplicates.append(found.first!)
}
}
return duplicates
}
func unsortedPlayers() -> [PlayerRegistration] {
unsortedTeams().flatMap { $0.unsortedPlayers() }
}
func selectedPlayers() -> [PlayerRegistration] {
selectedSortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.computedRank)
}
func players() -> [PlayerRegistration] {
unsortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.computedRank)
}
func femalePlayers() -> [PlayerRegistration] {
unsortedPlayers().filter({ $0.isMalePlayer() == false })
}
func unrankValue(for malePlayer: Bool) -> Int? {
switch tournamentCategory {
case .men:
return maleUnrankedValue
case .women:
return femaleUnrankedValue
case .mix:
return malePlayer ? maleUnrankedValue : femaleUnrankedValue
}
}
//todo
var clubName: String? {
eventObject()?.clubObject()?.name
}
//todo
func significantPlayerCount() -> Int {
2
}
func inadequatePlayers(in players: [PlayerRegistration]) -> [PlayerRegistration] {
if startDate.isInCurrentYear() == false {
return []
}
return players.filter { player in
if player.rank == nil { return false }
if player.computedRank <= tournamentLevel.minimumPlayerRank(category: tournamentCategory, ageCategory: federalTournamentAge) {
return true
} else {
return false
}
}
}
func mandatoryRegistrationCloseDate() -> Date? {
switch tournamentLevel {
case .p500, .p1000, .p1500, .p2000:
if let date = Calendar.current.date(byAdding: .day, value: -6, to: startDate) {
let startOfDay = Calendar.current.startOfDay(for: date)
return Calendar.current.date(byAdding: .minute, value: -1, to: startOfDay)
}
default:
break
}
return nil
}
func licenseYearValidity() -> Int {
if startDate.get(.month) > 8 {
return startDate.get(.year) + 1
} else {
return startDate.get(.year)
}
}
func playersWithoutValidLicense(in players: [PlayerRegistration]) -> [PlayerRegistration] {
let licenseYearValidity = licenseYearValidity()
return players.filter({
($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.formattedLicense().isLicenseNumber == false || $0.licenceId?.isEmpty == true))
})
}
func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? {
guard let seedIndex else { return nil }
return selectedSortedTeams()[safe: seedIndex]?.callDate
}
func importTeams(_ teams: [FileImportManager.TeamHolder]) {
var teamsToImport = [TeamRegistration]()
teams.forEach { team in
if let previousTeam = team.previousTeam {
previousTeam.updatePlayers(team.players, inTournamentCategory: team.tournamentCategory)
teamsToImport.append(previousTeam)
} else {
let newTeam = addTeam(team.players, registrationDate: team.registrationDate)
teamsToImport.append(newTeam)
}
}
try? DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: teamsToImport)
try? DataStore.shared.playerRegistrations.addOrUpdate(contentOfs: teams.flatMap { $0.players })
}
func maximumCourtsPerGroupSage() -> Int {
if teamsPerGroupStage > 1 {
return min(teamsPerGroupStage / 2, courtCount)
} else {
return max(1, courtCount)
}
}
func registrationIssues() -> Int {
let players : [PlayerRegistration] = unsortedPlayers()
let selectedTeams : [TeamRegistration] = selectedSortedTeams()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
let duplicates : [PlayerRegistration] = duplicates(in: players)
let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil })
let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players)
let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players)
let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 })
let waitingList : [TeamRegistration] = waitingListTeams(in: selectedTeams)
let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil })
let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil })
return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count
}
func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration) -> Bool {
guard let callDate = team.callDate else { return true }
if let groupStageStartDate = team.groupStageObject()?.startDate {
return Calendar.current.compare(callDate, to: groupStageStartDate, toGranularity: .minute) != ComparisonResult.orderedSame
} else if let roundMatchStartDate = team.initialMatch()?.startDate {
return Calendar.current.compare(callDate, to: roundMatchStartDate, toGranularity: .minute) != ComparisonResult.orderedSame
}
return true
}
func availableToStart(_ allMatches: [Match]) -> [Match] {
let runningMatches = allMatches.filter({ $0.isRunning() && $0.isReady() })
return allMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }).sorted(by: \.computedStartDateForSorting)
}
func runningMatches(_ allMatches: [Match]) -> [Match] {
allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting)
}
func readyMatches(_ allMatches: [Match]) -> [Match] {
return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting)
}
func finishedMatches(_ allMatches: [Match], limit: Int? = nil) -> [Match] {
let _limit = limit ?? courtCount
return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(_limit))
}
func finalRanking() -> [Int: [String]] {
var teams: [Int: [String]] = [:]
let rounds = rounds()
let final = rounds.last?.playedMatches().last
if let winner = final?.winningTeamId {
teams[1] = [winner]
}
if let finalist = final?.losingTeamId {
teams[2] = [finalist]
}
let others : [Round] = rounds.flatMap { round in
round.loserRoundsAndChildren().filter { $0.isDisabled() == false && $0.hasNextRound() == false }
}.compactMap({ $0 })
others.forEach { round in
if let interval = round.seedInterval() {
let playedMatches = round.playedMatches().filter { $0.disabled == false }
let winners = playedMatches.compactMap({ $0.winningTeamId })
let losers = playedMatches.compactMap({ $0.losingTeamId })
teams[interval.first + winners.count - 1] = winners
teams[interval.last] = losers
}
}
let groupStages = groupStages()
let baseRank = teamCount - teamsPerGroupStage * groupStageCount + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified
groupStages.forEach { groupStage in
let groupStageTeams = groupStage.teams(true)
for (index, team) in groupStageTeams.enumerated() {
if team.qualified == false {
let groupStageWidth = max(((index == qualifiedPerGroupStage) ? teamsPerGroupStage - groupStageAdditionalQualified : teamsPerGroupStage) * (index - qualifiedPerGroupStage), 0)
let _index = baseRank + groupStageWidth + 1
if let existingTeams = teams[_index] {
teams[_index] = existingTeams + [team.id]
} else {
teams[_index] = [team.id]
}
}
}
}
return teams
}
func lockRegistration() {
closedRegistrationDate = Date()
let count = selectedSortedTeams().count
if teamCount != count {
teamCount = count
}
let teams = unsortedTeams()
teams.forEach { team in
team.lockWeight = team.weight
}
try? DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: teams)
}
func updateWeights() {
let teams = self.unsortedTeams()
teams.forEach { team in
let players = team.unsortedPlayers()
players.forEach { $0.setComputedRank(in: self) }
team.setWeight(from: players, inTournamentCategory: tournamentCategory)
try? DataStore.shared.playerRegistrations.addOrUpdate(contentOfs: players)
}
try? DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: teams)
}
func updateRank(to newDate: Date?) async throws {
guard let newDate else { return }
rankSourceDate = newDate
if currentMonthData() == nil {
let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate)
let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate)
await MainActor.run {
let monthData = MonthData(monthKey: URL.importDateFormatter.string(from: newDate))
monthData.maleUnrankedValue = lastRankMan
monthData.femaleUnrankedValue = lastRankWoman
try? DataStore.shared.monthData.addOrUpdate(instance: monthData)
}
}
let lastRankMan = currentMonthData()?.maleUnrankedValue
let lastRankWoman = currentMonthData()?.femaleUnrankedValue
let dataURLs = SourceFileManager.shared.allFiles.filter({ $0.dateFromPath == newDate })
let sources = dataURLs.map { CSVParser(url: $0) }
try await unsortedPlayers().concurrentForEach { player in
try await player.updateRank(from: sources, lastRank: (player.sex == .female ? lastRankWoman : lastRankMan) ?? 0)
}
}
func missingUnrankedValue() -> Bool {
maleUnrankedValue == nil || femaleUnrankedValue == nil
}
func findTeam(_ players: [PlayerRegistration]) -> TeamRegistration? {
unsortedTeams().first(where: { $0.includes(players) })
}
func tournamentTitle(_ displayStyle: DisplayStyle = .wide) -> String {
[tournamentLevel.localizedLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), name].compactMap({ $0 }).joined(separator: " ")
}
func subtitle(_ displayStyle: DisplayStyle = .wide) -> String {
name ?? ""
}
func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .wide:
startDate.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted)
case .short:
startDate.formatted(date: .numeric, time: .omitted)
}
}
func qualifiedFromGroupStage() -> Int {
groupStageCount * qualifiedPerGroupStage
}
func availableQualifiedTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.qualified && $0.bracketPosition == nil })
}
func qualifiedTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.qualifiedFromGroupStage() })
}
func moreQualifiedToDraw() -> Int {
max(qualifiedTeams().count - (qualifiedFromGroupStage() + groupStageAdditionalQualified), 0)
}
func missingQualifiedFromGroupStages() -> [TeamRegistration] {
if groupStageAdditionalQualified > 0 {
return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in
groupStage.teams()[qualifiedPerGroupStage]
}
.filter({ $0.qualifiedFromGroupStage() == false })
} else {
return []
}
}
func groupStagesAreOver() -> Bool {
let groupStages = groupStages()
guard groupStages.isEmpty == false else {
return true
}
return groupStages.allSatisfy({ $0.hasEnded() })
//return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
}
var entryFeeMessage: String {
if let entryFee {
return "Inscription: " + entryFee.formatted(.currency(code: "EUR")) + "."
} else {
return "Inscription: gratuite."
}
}
func umpireMail() -> [String]? {
return [DataStore.shared.user.email]
}
func earnings() -> Double {
if let entryFee {
return Double(selectedPlayers().filter { $0.hasPaid() }.count) * entryFee
} else {
return 0.0
}
}
func paidCompletion() -> Double {
let selectedPlayers = selectedPlayers()
if selectedPlayers.isEmpty { return 0 }
return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count)
}
typealias TournamentStatus = (label:String, completion: String)
func cashierStatus() -> TournamentStatus {
let selectedPlayers = selectedPlayers()
let paid = selectedPlayers.filter({ $0.hasPaid() })
let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés"
let completion = (Double(paid.count) / Double(selectedPlayers.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
}
func scheduleStatus() -> TournamentStatus {
let allMatches = allMatches()
let ready = allMatches.filter({ $0.startDate != nil })
let label = ready.count.formatted() + " / " + allMatches.count.formatted() + " matchs programmés"
let completion = (Double(ready.count) / Double(allMatches.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
}
func callStatus() -> TournamentStatus {
let selectedSortedTeams = selectedSortedTeams()
let called = selectedSortedTeams.filter{ $0.called() }
let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " paires convoquées"
let completion = (Double(called.count) / Double(selectedSortedTeams.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
}
func bracketStatus() -> String {
if let round = getActiveRound() {
return [round.roundTitle(), round.roundStatus()].joined(separator: " ")
} else {
return "à construire"
}
}
func groupStageStatus() -> String {
let runningGroupStages = groupStages().filter({ $0.isRunning() })
if groupStagesAreOver() { return "terminées" }
if runningGroupStages.isEmpty {
let ongoingGroupStages = runningGroupStages.filter({ $0.hasStarted() && $0.hasEnded() == false })
if ongoingGroupStages.isEmpty == false {
return "Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours"
}
return groupStages().count.formatted() + " poule" + groupStages().count.pluralSuffix
} else {
return "Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { ($0.index + 1).formatted() }.joined(separator: ", ") + " en cours"
}
}
func settingsDescriptionLocalizedLabel() -> String {
[dayDuration.formatted() + " jour\(dayDuration.pluralSuffix)", courtCount.formatted() + " terrain\(courtCount.pluralSuffix)"].joined(separator: ", ")
}
func structureDescriptionLocalizedLabel() -> String {
let groupStageLabel: String? = groupStageCount > 0 ? groupStageCount.formatted() + " poule\(groupStageCount.pluralSuffix)" : nil
return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ")
}
func buildStructure() {
deleteStructure()
deleteGroupStages()
buildGroupStages()
buildBracket()
}
func buildGroupStages() {
guard groupStages().isEmpty else {
return
}
var _groupStages = [GroupStage]()
for index in 0..<groupStageCount {
let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, matchFormat: groupStageMatchFormat)
_groupStages.append(groupStage)
}
try? DataStore.shared.groupStages.addOrUpdate(contentOfs: _groupStages)
groupStages().forEach { $0.buildMatches() }
refreshGroupStages()
}
func bracketTeamCount() -> Int {
let bracketTeamCount = teamCount - (teamsPerGroupStage - qualifiedPerGroupStage) * groupStageCount + (groupStageAdditionalQualified * (groupStageCount > 0 ? 1 : 0))
return bracketTeamCount
}
func buildBracket() {
guard rounds().isEmpty else { return }
let roundCount = RoundRule.numberOfRounds(forTeams: bracketTeamCount())
let rounds = (0..<roundCount).map { //index 0 is the final
Round(tournament: id, index: $0)
}
try? DataStore.shared.rounds.addOrUpdate(contentOfs: rounds)
let matchCount = RoundRule.numberOfMatches(forTeams: bracketTeamCount())
let matches = (0..<matchCount).map { //0 is final match
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
let round = rounds[roundIndex]
return Match(round: round.id, index: $0, matchFormat: matchFormat)
}
print(matches.map {
(RoundRule.roundName(fromMatchIndex: $0.index), RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index))
})
try? DataStore.shared.matches.addOrUpdate(contentOfs: matches)
self.rounds().forEach { round in
round.buildLoserBracket()
}
}
func deleteStructure() {
try? DataStore.shared.rounds.delete(contentOfs: rounds())
unsortedTeams().forEach({ $0.bracketPosition = nil })
try? DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
}
func deleteGroupStages() {
try? DataStore.shared.groupStages.delete(contentOfs: groupStages())
unsortedTeams().forEach({
$0.groupStage = nil
$0.groupStagePosition = nil
})
try? DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
}
func refreshGroupStages() {
unsortedTeams().forEach { team in
team.groupStage = nil
team.groupStagePosition = nil
}
if groupStageCount > 0 {
switch groupStageOrderingMode {
case .random:
setGroupStage(randomize: true)
case .snake:
setGroupStage(randomize: false)
case .swiss:
setGroupStage(randomize: true)
}
}
}
func setGroupStage(randomize: Bool) {
let groupStages = groupStages()
let numberOfBracketsAsInt = groupStages.count
// let teamsPerBracket = teamsPerBracket
if groupStageCount != numberOfBracketsAsInt {
deleteGroupStages()
buildGroupStages()
}
let max = groupStages.map { $0.size }.reduce(0,+)
var chunks = selectedSortedTeams().suffix(max).chunked(into: numberOfBracketsAsInt)
for (index, _) in chunks.enumerated() {
if randomize {
chunks[index].shuffle()
} else if index % 2 != 0 {
chunks[index].reverse()
}
print("Equipes \(chunks[index].map { $0.weight })")
for (jIndex, _) in chunks[index].enumerated() {
print("Position \(index+1) Poule \(groupStages[jIndex].index)")
chunks[index][jIndex].groupStage = groupStages[jIndex].id
chunks[index][jIndex].groupStagePosition = index
try? DataStore.shared.teamRegistrations.addOrUpdate(instance: chunks[index][jIndex])
}
}
}
func isFree() -> Bool {
entryFee == nil || entryFee == 0
}
func indexOf(team: TeamRegistration) -> Int? {
selectedSortedTeams().firstIndex(where: { $0.id == team.id })
}
func labelIndexOf(team: TeamRegistration) -> String? {
if let teamIndex = indexOf(team: team) {
return "#" + (teamIndex + 1).formatted()
} else {
return nil
}
}
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil) -> TeamRegistration {
let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date())
team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
players.forEach { player in
player.teamRegistration = team.id
}
return team
}
var matchFormat: MatchFormat {
get {
roundFormat ?? .defaultFormatForMatchType(.bracket)
}
set {
roundFormat = newValue
}
}
var groupStageMatchFormat: MatchFormat {
get {
groupStageFormat ?? .defaultFormatForMatchType(.groupStage)
}
set {
groupStageFormat = newValue
}
}
var loserBracketMatchFormat: MatchFormat {
get {
loserRoundFormat ?? .defaultFormatForMatchType(.loserBracket)
}
set {
loserRoundFormat = newValue
}
}
var groupStageOrderingMode: GroupStageOrderingMode {
get {
groupStageSortMode
}
set {
groupStageSortMode = newValue
}
}
var tournamentCategory: TournamentCategory {
get {
federalCategory
}
set {
if federalCategory != newValue {
federalCategory = newValue
updateWeights()
} else {
federalCategory = newValue
}
}
}
var tournamentLevel: TournamentLevel {
get {
federalLevelCategory
}
set {
federalLevelCategory = newValue
teamSorting = newValue.defaultTeamSortingType
groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(1)
matchFormat = roundSmartMatchFormat(1)
}
}
var federalTournamentAge: FederalTournamentAge {
get {
federalAgeCategory
}
set {
federalAgeCategory = newValue
}
}
func loserBracketSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
let format = tournamentLevel.federalFormatForLoserBracketRound(roundIndex)
if loserBracketMatchFormat.rank > format.rank {
return format
} else {
return loserBracketMatchFormat
}
}
func groupStageSmartMatchFormat() -> MatchFormat {
let format = tournamentLevel.federalFormatForGroupStage()
if groupStageMatchFormat.rank > format.rank {
return format
} else {
return groupStageMatchFormat
}
}
func setupFederalSettings() {
teamSorting = tournamentLevel.defaultTeamSortingType
groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(1)
matchFormat = roundSmartMatchFormat(1)
}
func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
let format = tournamentLevel.federalFormatForBracketRound(roundIndex)
if matchFormat.rank > format.rank {
return format
} else {
return matchFormat
}
}
private func _defaultSorting() -> [MySortDescriptor<TeamRegistration>] {
switch teamSorting {
case .rank:
[.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.registrationDate!), .keyPath(\.canonicalName)]
case .inscriptionDate:
[.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\.canonicalName)]
}
}
func isSameBuild(_ build: any TournamentBuildHolder) -> Bool {
tournamentLevel == build.level
&& tournamentCategory == build.category
&& federalTournamentAge == build.age
}
private let _currentSelectionSorting : [MySortDescriptor<TeamRegistration>] = [.keyPath(\.weight), .keyPath(\.registrationDate!), .keyPath(\.canonicalName)]
override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: self.unsortedTeams())
try Store.main.deleteDependencies(items: self.groupStages())
try Store.main.deleteDependencies(items: self.rounds())
}
func currentMonthData() -> MonthData? {
guard let rankSourceDate else { return nil }
let dateString = URL.importDateFormatter.string(from: rankSourceDate)
return Store.main.filter(isIncluded: { $0.monthKey == dateString }).first
}
var maleUnrankedValue: Int? {
currentMonthData()?.maleUnrankedValue
}
var femaleUnrankedValue: Int? {
currentMonthData()?.femaleUnrankedValue
}
func courtNameIfAvailable(atIndex courtIndex: Int) -> String? {
club()?.courts.first(where: { $0.index == courtIndex })?.name
}
func courtName(atIndex courtIndex: Int) -> String {
courtNameIfAvailable(atIndex: courtIndex) ?? Court.courtIndexedTitle(atIndex: courtIndex)
}
// MARK: - Payments & Crypto
fileprivate var _currentPayment: TournamentPayment? = nil
fileprivate var _currentCanceled: Bool? = nil
// func setPayment(_ payment: TournamentPayment) {
//
// let max: Int = TournamentPayment.allCases.count
// self._currentPayment = payment
// var sequence = (1...18).map { _ in Int.random(in: (0..<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) {
// do {
// self.payment = try data.encrypt(pass: Key.pass.rawValue)
// } catch {
// Logger.error(error)
// }
// }
// }
// var currentPayment: TournamentPayment? {
// if let current = self._currentPayment {
// return current
// }
// self._currentPayment = self.decryptPayment()
// return self._currentPayment
// }
// func decryptPayment() -> TournamentPayment? {
// if let payment {
// do {
// let decoded: String = try payment.decryptData(pass: Key.pass.rawValue)
// let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue }
// return TournamentPayment(rawValue: sequence[18])
// } catch {
// Logger.error(error)
// }
// }
// return nil
// }
// func setCanceled(_ canceled: Bool) {
//
// let max: Int = 9
// self._currentCanceled = canceled
// var sequence = (1...18).map { _ in Int.random(in: (0..<max)) }
// sequence.append(canceled.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) {
// do {
// self.isCanceled = try data.encrypt(pass: Key.pass.rawValue)
// } catch {
// Logger.error(error)
// }
// }
// }
// var currentCanceled: Bool? {
// if let current = self._currentCanceled {
// return current
// }
// self._currentCanceled = self.decryptCanceled()
// return self._currentCanceled
// }
// func decryptCanceled() -> Bool? {
// if let isCanceled {
// do {
// let decoded: String = try isCanceled.decryptData(pass: Key.pass.rawValue)
// let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue }
// return Bool.decodeInt(sequence[18])
// } catch {
// Logger.error(error)
// }
// }
// return nil
// }
enum PaymentError: Error {
case cantPayTournament
}
func payIfNecessary() throws {
if let payment = Guard.main.paymentForNewTournament() {
self.payment = payment
try DataStore.shared.tournaments.addOrUpdate(instance: self)
return
}
throw PaymentError.cantPayTournament
}
}
fileprivate extension Bool {
var encodedValue: Int {
switch self {
case true:
return Int.random(in: (0...4))
case false:
return Int.random(in: (5...9))
}
}
static func decodeInt(_ int: Int) -> Bool {
switch int {
case (0...4):
return true
default:
return false
}
}
}
//extension Tournament {
// enum CodingKeys: String, CodingKey {
// case _id = "id"
// case _event = "event"
// case _name = "name"
// case _startDate = "startDate"
// case _endDate = "endDate"
// case _creationDate = "creationDate"
// case _isPrivate = "isPrivate"
// case _groupStageFormat = "groupStageFormat"
// case _roundFormat = "roundFormat"
// case _loserRoundFormat = "loserRoundFormat"
// case _groupStageSortMode = "groupStageSortMode"
// case _groupStageCount = "groupStageCount"
// case _rankSourceDate = "rankSourceDate"
// case _dayDuration = "dayDuration"
// case _teamCount = "teamCount"
// case _teamSorting = "teamSorting"
// case _federalCategory = "federalCategory"
// case _federalLevelCategory = "federalLevelCategory"
// case _federalAgeCategory = "federalAgeCategory"
// case _groupStageCourtCount = "groupStageCourtCount"
// case _closedRegistrationDate = "closedRegistrationDate"
// case _groupStageAdditionalQualified = "groupStageAdditionalQualified"
// case _courtCount = "courtCount"
// case _prioritizeClubMembers = "prioritizeClubMembers"
// case _qualifiedPerGroupStage = "qualifiedPerGroupStage"
// case _teamsPerGroupStage = "teamsPerGroupStage"
// case _entryFee = "entryFee"
// case _additionalEstimationDuration = "additionalEstimationDuration"
// case _isDeleted = "isDeleted"
// case _isCanceled = "localId"
// case _payment = "globalId"
// }
//}
extension Tournament: Hashable {
static func == (lhs: Tournament, rhs: Tournament) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
extension Tournament: FederalTournamentHolder {
var holderId: String { id }
func clubLabel() -> String {
locationLabel()
}
func subtitleLabel() -> String {
subtitle()
}
var tournaments: [any TournamentBuildHolder] {
[
self
]
}
}
extension Tournament: TournamentBuildHolder {
func buildHolderTitle() -> String {
tournamentTitle()
}
var category: TournamentCategory {
tournamentCategory
}
var level: TournamentLevel {
tournamentLevel
}
var age: FederalTournamentAge {
federalTournamentAge
}
}
extension Tournament {
static func newEmptyInstance() -> Tournament {
let lastDataSource: String? = DataStore.shared.appSettings.lastDataSource
var _mostRecentDateAvailable: Date? {
guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource)
}
let rankSourceDate = _mostRecentDateAvailable
let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil }.sorted(by: \.startDate).reversed()
let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments)
let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments)
let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments)
//creator: DataStore.shared.user?.id
return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge)
}
static func fake() -> Tournament {
return Tournament(event: "Roland Garros", name: "Magic P100", startDate: Date(), endDate: Date(), creationDate: Date(), isPrivate: false, groupStageFormat: .nineGames, roundFormat: nil, loserRoundFormat: nil, groupStageSortMode: .snake, groupStageCount: 4, rankSourceDate: nil, dayDuration: 2, teamCount: 24, teamSorting: .rank, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .a45, groupStageCourtCount: nil, closedRegistrationDate: nil, groupStageAdditionalQualified: 0, courtCount: 4, prioritizeClubMembers: false, qualifiedPerGroupStage: 2, teamsPerGroupStage: 4, entryFee: nil)
}
}