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.
1789 lines
71 KiB
1789 lines
71 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" }
|
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
|
|
|
|
var id: String = Store.randomId()
|
|
var event: String?
|
|
var name: String?
|
|
var startDate: Date
|
|
var endDate: Date?
|
|
private(set) var creationDate: Date
|
|
var isPrivate: Bool
|
|
private(set) var groupStageFormat: MatchFormat?
|
|
private(set) var roundFormat: MatchFormat?
|
|
private(set) var loserRoundFormat: MatchFormat?
|
|
var groupStageSortMode: GroupStageOrderingMode
|
|
var groupStageCount: Int
|
|
var rankSourceDate: Date?
|
|
var dayDuration: Int
|
|
var teamCount: Int
|
|
var teamSorting: TeamSortingType
|
|
var federalCategory: TournamentCategory
|
|
var federalLevelCategory: TournamentLevel
|
|
var federalAgeCategory: FederalTournamentAge
|
|
var closedRegistrationDate: Date?
|
|
var groupStageAdditionalQualified: Int
|
|
var courtCount: Int = 2
|
|
var prioritizeClubMembers: Bool
|
|
var qualifiedPerGroupStage: Int
|
|
var teamsPerGroupStage: Int
|
|
var entryFee: Double?
|
|
var payment: TournamentPayment? = nil
|
|
var additionalEstimationDuration: Int = 0
|
|
var isDeleted: Bool = false
|
|
var isCanceled: Bool = false
|
|
var publishTeams: Bool = false
|
|
//var publishWaitingList: Bool = false
|
|
var publishSummons: Bool = false
|
|
var publishGroupStages: Bool = false
|
|
var publishBrackets: Bool = false
|
|
var shouldVerifyGroupStage: Bool = false
|
|
var shouldVerifyBracket: Bool = false
|
|
|
|
@ObservationIgnored
|
|
var navigationPath: [Screen] = []
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case _id = "id"
|
|
case _event = "event"
|
|
case _creator = "creator"
|
|
case _name = "name"
|
|
case _startDate = "startDate"
|
|
case _endDate = "endDate"
|
|
case _creationDate = "creationDate"
|
|
case _isPrivate = "isPrivate"
|
|
case _groupStageFormat = "groupStageFormat"
|
|
case _roundFormat = "roundFormat"
|
|
case _loserRoundFormat = "loserRoundFormat"
|
|
case _groupStageSortMode = "groupStageSortMode"
|
|
case _groupStageCount = "groupStageCount"
|
|
case _rankSourceDate = "rankSourceDate"
|
|
case _dayDuration = "dayDuration"
|
|
case _teamCount = "teamCount"
|
|
case _teamSorting = "teamSorting"
|
|
case _federalCategory = "federalCategory"
|
|
case _federalLevelCategory = "federalLevelCategory"
|
|
case _federalAgeCategory = "federalAgeCategory"
|
|
case _seedCount = "seedCount"
|
|
case _closedRegistrationDate = "closedRegistrationDate"
|
|
case _groupStageAdditionalQualified = "groupStageAdditionalQualified"
|
|
case _courtCount = "courtCount"
|
|
case _prioritizeClubMembers = "prioritizeClubMembers"
|
|
case _qualifiedPerGroupStage = "qualifiedPerGroupStage"
|
|
case _teamsPerGroupStage = "teamsPerGroupStage"
|
|
case _entryFee = "entryFee"
|
|
case _additionalEstimationDuration = "additionalEstimationDuration"
|
|
case _isDeleted = "isDeleted"
|
|
case _isCanceled = "localId"
|
|
case _payment = "globalId"
|
|
case _publishTeams = "publishTeams"
|
|
//case _publishWaitingList = "publishWaitingList"
|
|
case _publishSummons = "publishSummons"
|
|
case _publishGroupStages = "publishGroupStages"
|
|
case _publishBrackets = "publishBrackets"
|
|
case _shouldVerifyGroupStage = "shouldVerifyGroupStage"
|
|
case _shouldVerifyBracket = "shouldVerifyBracket"
|
|
}
|
|
|
|
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) {
|
|
self.event = event
|
|
self.name = name
|
|
self.startDate = startDate
|
|
self.endDate = endDate
|
|
self.creationDate = creationDate
|
|
self.isPrivate = isPrivate
|
|
self.groupStageFormat = groupStageFormat
|
|
self.roundFormat = roundFormat
|
|
self.loserRoundFormat = loserRoundFormat
|
|
self.groupStageSortMode = groupStageSortMode
|
|
self.groupStageCount = groupStageCount
|
|
self.rankSourceDate = rankSourceDate
|
|
self.dayDuration = dayDuration
|
|
self.teamCount = teamCount
|
|
self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType
|
|
self.federalCategory = federalCategory
|
|
self.federalLevelCategory = federalLevelCategory
|
|
self.federalAgeCategory = federalAgeCategory
|
|
self.closedRegistrationDate = closedRegistrationDate
|
|
self.groupStageAdditionalQualified = groupStageAdditionalQualified
|
|
self.courtCount = courtCount
|
|
self.prioritizeClubMembers = prioritizeClubMembers
|
|
self.qualifiedPerGroupStage = qualifiedPerGroupStage
|
|
self.teamsPerGroupStage = teamsPerGroupStage
|
|
self.entryFee = entryFee
|
|
self.additionalEstimationDuration = additionalEstimationDuration
|
|
self.isDeleted = isDeleted
|
|
self.publishTeams = publishTeams
|
|
self.publishSummons = publishSummons
|
|
self.publishBrackets = publishBrackets
|
|
self.publishGroupStages = publishGroupStages
|
|
self.shouldVerifyBracket = shouldVerifyBracket
|
|
self.shouldVerifyGroupStage = shouldVerifyGroupStage
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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)
|
|
if let event {
|
|
try container.encode(event, forKey: ._event)
|
|
} else {
|
|
try container.encodeNil(forKey: ._event)
|
|
}
|
|
if let name {
|
|
try container.encode(name, forKey: ._name)
|
|
} else {
|
|
try container.encodeNil(forKey: ._name)
|
|
}
|
|
try container.encode(startDate, forKey: ._startDate)
|
|
if let endDate {
|
|
try container.encode(endDate, forKey: ._endDate)
|
|
} else {
|
|
try container.encodeNil(forKey: ._endDate)
|
|
}
|
|
|
|
try container.encode(creationDate, forKey: ._creationDate)
|
|
try container.encode(isPrivate, forKey: ._isPrivate)
|
|
if let groupStageFormat {
|
|
try container.encode(groupStageFormat, forKey: ._groupStageFormat)
|
|
} else {
|
|
try container.encodeNil(forKey: ._groupStageFormat)
|
|
}
|
|
if let roundFormat {
|
|
try container.encode(roundFormat, forKey: ._roundFormat)
|
|
} else {
|
|
try container.encodeNil(forKey: ._roundFormat)
|
|
}
|
|
if let loserRoundFormat {
|
|
try container.encode(loserRoundFormat, forKey: ._loserRoundFormat)
|
|
} else {
|
|
try container.encodeNil(forKey: ._loserRoundFormat)
|
|
}
|
|
try container.encode(groupStageSortMode, forKey: ._groupStageSortMode)
|
|
try container.encode(groupStageCount, forKey: ._groupStageCount)
|
|
if let rankSourceDate {
|
|
try container.encode(rankSourceDate, forKey: ._rankSourceDate)
|
|
} else {
|
|
try container.encodeNil(forKey: ._rankSourceDate)
|
|
}
|
|
try container.encode(dayDuration, forKey: ._dayDuration)
|
|
try container.encode(teamCount, forKey: ._teamCount)
|
|
try container.encode(teamSorting, forKey: ._teamSorting)
|
|
try container.encode(federalCategory, forKey: ._federalCategory)
|
|
try container.encode(federalLevelCategory, forKey: ._federalLevelCategory)
|
|
try container.encode(federalAgeCategory, forKey: ._federalAgeCategory)
|
|
if let closedRegistrationDate {
|
|
try container.encode(closedRegistrationDate, forKey: ._closedRegistrationDate)
|
|
} else {
|
|
try container.encodeNil(forKey: ._closedRegistrationDate)
|
|
}
|
|
|
|
try container.encode(groupStageAdditionalQualified, forKey: ._groupStageAdditionalQualified)
|
|
try container.encode(courtCount, forKey: ._courtCount)
|
|
try container.encode(prioritizeClubMembers, forKey: ._prioritizeClubMembers)
|
|
try container.encode(qualifiedPerGroupStage, forKey: ._qualifiedPerGroupStage)
|
|
try container.encode(teamsPerGroupStage, forKey: ._teamsPerGroupStage)
|
|
if let entryFee {
|
|
try container.encode(entryFee, forKey: ._entryFee)
|
|
} else {
|
|
try container.encodeNil(forKey: ._entryFee)
|
|
}
|
|
|
|
try self._encodePayment(container: &container)
|
|
try container.encode(additionalEstimationDuration, forKey: ._additionalEstimationDuration)
|
|
try container.encode(isDeleted, forKey: ._isDeleted)
|
|
try self._encodeIsCanceled(container: &container)
|
|
try container.encode(publishTeams, forKey: ._publishTeams)
|
|
try container.encode(publishSummons, forKey: ._publishSummons)
|
|
try container.encode(publishBrackets, forKey: ._publishBrackets)
|
|
try container.encode(publishGroupStages, forKey: ._publishGroupStages)
|
|
try container.encode(shouldVerifyBracket, forKey: ._shouldVerifyBracket)
|
|
try container.encode(shouldVerifyGroupStage, forKey: ._shouldVerifyGroupStage)
|
|
}
|
|
|
|
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 running
|
|
case canceled
|
|
case finished
|
|
}
|
|
|
|
func eventLabel() -> String {
|
|
if let event = eventObject(), let name = event.name {
|
|
return name
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func publishedTeamsDate() -> Date {
|
|
startDate
|
|
}
|
|
|
|
func areTeamsPublished() -> Bool {
|
|
Date() >= startDate || publishTeams
|
|
}
|
|
|
|
func areSummonsPublished() -> Bool {
|
|
Date() >= startDate || publishSummons
|
|
}
|
|
|
|
func publishedGroupStagesDate() -> Date? {
|
|
if let first = groupStages().flatMap({ $0.playedMatches() }).compactMap({ $0.startDate }).sorted().first?.atEightAM() {
|
|
if first.isEarlierThan(startDate) {
|
|
return startDate
|
|
} else {
|
|
return first
|
|
}
|
|
} else {
|
|
return startDate
|
|
}
|
|
}
|
|
|
|
func areGroupStagesPublished() -> Bool {
|
|
if publishGroupStages { return true }
|
|
if let publishedGroupStagesDate = publishedGroupStagesDate() {
|
|
return Date() >= publishedGroupStagesDate
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func publishedBracketsDate() -> Date? {
|
|
if let first = rounds().flatMap({ $0.playedMatches() }).compactMap({ $0.startDate }).sorted().first?.atEightAM() {
|
|
if first.isEarlierThan(startDate) {
|
|
return startDate
|
|
} else {
|
|
return first
|
|
}
|
|
} else {
|
|
return startDate
|
|
}
|
|
}
|
|
|
|
func areBracketsPublished() -> Bool {
|
|
if publishBrackets { return true }
|
|
if let publishedBracketsDate = publishedBracketsDate() {
|
|
return Date() >= publishedBracketsDate
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
|
|
func shareURL() -> URL? {
|
|
return URLs.main.url.appending(path: "tournament/\(id)")
|
|
}
|
|
|
|
func broadcastURL() -> URL? {
|
|
return URLs.main.url.appending(path: "tournament/\(id)/broadcast")
|
|
}
|
|
|
|
func courtUsed() -> [Int] {
|
|
let runningMatches : [Match] = Store.main.filter(isIncluded: { $0.isRunning() }).filter({ $0.tournamentId() == self.id })
|
|
return Set(runningMatches.compactMap { $0.courtIndex }).sorted()
|
|
}
|
|
|
|
func hasStarted() -> Bool {
|
|
startDate <= Date()
|
|
}
|
|
|
|
func eventObject() -> Event? {
|
|
guard let event else { return nil }
|
|
return Store.main.findById(event)
|
|
}
|
|
|
|
func pasteDataForImporting() -> String {
|
|
let selectedSortedTeams = selectedSortedTeams()
|
|
return (selectedSortedTeams.compactMap { $0.pasteData() } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams).compactMap { $0.pasteData() }).joined(separator: "\n\n")
|
|
}
|
|
|
|
func club() -> Club? {
|
|
eventObject()?.clubObject()
|
|
}
|
|
|
|
func locationLabel(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
if let club = club() {
|
|
switch displayStyle {
|
|
case .wide:
|
|
return club.name
|
|
case .short:
|
|
return club.acronym
|
|
}
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func hasEnded() -> Bool {
|
|
endDate != nil
|
|
}
|
|
|
|
func state() -> Tournament.State {
|
|
if self.isCanceled == true {
|
|
return .canceled
|
|
}
|
|
|
|
if 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] {
|
|
selectedSortedTeams().filter({ $0.bracketPosition != nil && $0.groupStagePosition == nil })
|
|
}
|
|
|
|
func groupStageTeams() -> [TeamRegistration] {
|
|
selectedSortedTeams().filter({ $0.groupStagePosition != nil })
|
|
}
|
|
|
|
func seeds() -> [TeamRegistration] {
|
|
let selectedSortedTeams = selectedSortedTeams()
|
|
let seeds = max(selectedSortedTeams.count - groupStageCount * teamsPerGroupStage, 0)
|
|
return Array(selectedSortedTeams.prefix(seeds))
|
|
}
|
|
|
|
func availableSeeds() -> [TeamRegistration] {
|
|
return seeds().filter { $0.isSeedable() }
|
|
}
|
|
|
|
func lastSeedRound() -> Int {
|
|
if let last = seeds().filter({ $0.bracketPosition != nil }).last {
|
|
return RoundRule.roundIndex(fromMatchIndex: last.bracketPosition! / 2)
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func getRound(atRoundIndex roundIndex: Int) -> Round? {
|
|
Store.main.filter(isIncluded: { $0.tournament == id && $0.index == roundIndex }).first
|
|
}
|
|
|
|
func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] {
|
|
getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 0 } ?? []
|
|
}
|
|
|
|
func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] {
|
|
getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 1 } ?? []
|
|
}
|
|
|
|
func availableSeedGroups() -> [SeedInterval] {
|
|
let seeds = seeds()
|
|
var availableSeedGroup = Set<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 getPlayedMatchDateIntervals(in event: Event) -> [DateInterval] {
|
|
allMatches().filter { $0.courtIndex != nil && $0.startDate != nil }.map { match in
|
|
DateInterval(event: event.id, courtIndex: match.courtIndex!, startDate: match.startDate!, endDate: match.estimatedEndDate(additionalEstimationDuration)!)
|
|
}
|
|
}
|
|
|
|
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 }.prefix(teamCount).sorted(by: \.initialWeight)
|
|
|
|
let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending)
|
|
|
|
let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending)
|
|
|
|
var bracketSeeds = min(teamCount, _completeTeams.count) - groupStageCount * teamsPerGroupStage - wcBracket.count
|
|
var groupStageTeamCount = groupStageCount * teamsPerGroupStage - wcGroupStage.count
|
|
if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
|
|
if bracketSeeds < 0 { bracketSeeds = 0 }
|
|
|
|
if prioritizeClubMembers {
|
|
|
|
let bracketTeams = (_completeTeams.filter { $0.hasMemberOfClub(clubName) } + _completeTeams.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket
|
|
|
|
let groupStageTeamsNoFiltering = Set(_completeTeams).subtracting(bracketTeams)
|
|
let groupStageTeams = (groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) } + groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage
|
|
|
|
_sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending)
|
|
} else {
|
|
let bracketTeams = _completeTeams.prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket
|
|
let groupStageTeams = Set(_completeTeams).subtracting(bracketTeams).sorted(using: defaultSorting, order: .ascending).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage
|
|
_sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending)
|
|
}
|
|
|
|
//let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
//print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
return _sortedTeams
|
|
}
|
|
|
|
func waitingListTeams(in teams: [TeamRegistration]) -> [TeamRegistration] {
|
|
let waitingList = Set(unsortedTeams()).subtracting(teams)
|
|
return waitingList.filter { $0.walkOut == false }.sorted(using: _defaultSorting(), order: .ascending) + waitingList.filter { $0.walkOut == true }.sorted(using: _defaultSorting(), order: .ascending)
|
|
}
|
|
|
|
func bracketCut() -> Int {
|
|
max(0, teamCount - groupStageCut())
|
|
}
|
|
|
|
func groupStageCut() -> Int {
|
|
groupStageCount * teamsPerGroupStage
|
|
}
|
|
|
|
func cutLabel(index: Int) -> String {
|
|
if index < bracketCut() {
|
|
return "Tableau"
|
|
} else if index - bracketCut() < groupStageCut() {
|
|
return "Poule"
|
|
} else {
|
|
return "Liste d'attente"
|
|
}
|
|
}
|
|
|
|
func unsortedTeams() -> [TeamRegistration] {
|
|
Store.main.filter { $0.tournament == self.id }
|
|
}
|
|
|
|
func unsortedTeamsWithoutWO() -> [TeamRegistration] {
|
|
Store.main.filter { $0.tournament == self.id && $0.walkOut == false }
|
|
}
|
|
|
|
func walkoutTeams() -> [TeamRegistration] {
|
|
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 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]()
|
|
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)
|
|
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 summonDate = team.callDate else { return true }
|
|
guard let expectedSummonDate = team.expectedSummonDate() else { return true }
|
|
return Calendar.current.compare(summonDate, to: expectedSummonDate, toGranularity: .minute) != ComparisonResult.orderedSame
|
|
}
|
|
|
|
func 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) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0)
|
|
|
|
let _index = baseRank + groupStageWidth + 1
|
|
if let existingTeams = teams[_index] {
|
|
teams[_index] = existingTeams + [team.id]
|
|
} else {
|
|
teams[_index] = [team.id]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return teams
|
|
}
|
|
|
|
func lockRegistration() {
|
|
closedRegistrationDate = Date()
|
|
let count = selectedSortedTeams().count
|
|
if teamCount != count {
|
|
teamCount = count
|
|
}
|
|
let teams = unsortedTeams()
|
|
teams.forEach { team in
|
|
team.lockedWeight = team.weight
|
|
}
|
|
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(.wide) + " " + tournamentCategory.localizedLabel(.wide), displayStyle == .wide ? name : nil].compactMap({ $0 }).joined(separator: " - ")
|
|
}
|
|
|
|
func subtitle(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
name ?? ""
|
|
}
|
|
|
|
func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
switch displayStyle {
|
|
case .wide:
|
|
startDate.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted)
|
|
case .short:
|
|
startDate.formatted(date: .numeric, time: .omitted)
|
|
}
|
|
}
|
|
|
|
func qualifiedFromGroupStage() -> Int {
|
|
groupStageCount * qualifiedPerGroupStage
|
|
}
|
|
|
|
|
|
func availableQualifiedTeams() -> [TeamRegistration] {
|
|
unsortedTeams().filter({ $0.qualified && $0.bracketPosition == nil })
|
|
}
|
|
|
|
func qualifiedTeams() -> [TeamRegistration] {
|
|
unsortedTeams().filter({ $0.qualifiedFromGroupStage() })
|
|
}
|
|
|
|
func moreQualifiedToDraw() -> Int {
|
|
max(qualifiedTeams().count - (qualifiedFromGroupStage() + groupStageAdditionalQualified), 0)
|
|
}
|
|
|
|
func missingQualifiedFromGroupStages() -> [TeamRegistration] {
|
|
if groupStageAdditionalQualified > 0 {
|
|
return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in
|
|
groupStage.teams()[qualifiedPerGroupStage]
|
|
}
|
|
.filter({ $0.qualifiedFromGroupStage() == false })
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
|
|
func groupStagesAreOver() -> Bool {
|
|
let groupStages = groupStages()
|
|
guard groupStages.isEmpty == false else {
|
|
return true
|
|
}
|
|
return groupStages.allSatisfy({ $0.hasEnded() })
|
|
//return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
|
|
}
|
|
|
|
func paymentMethodMessage() -> String? {
|
|
DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods
|
|
}
|
|
|
|
var entryFeeMessage: String {
|
|
if let entryFee {
|
|
return ["Inscription: " + entryFee.formatted(.currency(code: "EUR")) + " par joueur.", paymentMethodMessage()].compactMap { $0 }.joined(separator: "\n")
|
|
} else {
|
|
return "Inscription: gratuite."
|
|
}
|
|
}
|
|
|
|
func umpireMail() -> [String]? {
|
|
return [DataStore.shared.user.email]
|
|
}
|
|
|
|
func earnings() -> Double {
|
|
if let entryFee {
|
|
return Double(selectedPlayers().filter { $0.hasPaid() }.count) * entryFee
|
|
} else {
|
|
return 0.0
|
|
}
|
|
}
|
|
|
|
func paidCompletion() -> Double {
|
|
let selectedPlayers = selectedPlayers()
|
|
if selectedPlayers.isEmpty { return 0 }
|
|
return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count)
|
|
}
|
|
|
|
typealias TournamentStatus = (label:String, completion: String)
|
|
func cashierStatus() -> TournamentStatus {
|
|
let selectedPlayers = selectedPlayers()
|
|
let paid = selectedPlayers.filter({ $0.hasPaid() })
|
|
let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés"
|
|
let completion = (Double(paid.count) / Double(selectedPlayers.count))
|
|
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
|
|
return TournamentStatus(label: label, completion: completionLabel)
|
|
}
|
|
|
|
func scheduleStatus() -> TournamentStatus {
|
|
let allMatches = allMatches()
|
|
let ready = allMatches.filter({ $0.startDate != nil })
|
|
let label = ready.count.formatted() + " / " + allMatches.count.formatted() + " matchs programmés"
|
|
let completion = (Double(ready.count) / Double(allMatches.count))
|
|
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
|
|
return TournamentStatus(label: label, completion: completionLabel)
|
|
}
|
|
|
|
func callStatus() -> TournamentStatus {
|
|
let selectedSortedTeams = selectedSortedTeams()
|
|
let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false }
|
|
let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " convoquées au bon horaire"
|
|
let completion = (Double(called.count) / Double(selectedSortedTeams.count))
|
|
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
|
|
return TournamentStatus(label: label, completion: completionLabel)
|
|
}
|
|
|
|
func confirmedSummonStatus() -> 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() -> String {
|
|
let availableSeeds = availableSeeds()
|
|
if availableSeeds.isEmpty == false {
|
|
return "placer \(availableSeeds.count) tête\(availableSeeds.count.pluralSuffix) de série"
|
|
}
|
|
let availableQualifiedTeams = availableQualifiedTeams()
|
|
if availableQualifiedTeams.isEmpty == false {
|
|
return "placer \(availableQualifiedTeams.count) qualifié" + availableQualifiedTeams.count.pluralSuffix
|
|
}
|
|
if let round = getActiveRound() {
|
|
return [round.roundTitle(.short), round.roundStatus()].joined(separator: " ")
|
|
} else {
|
|
return "à construire"
|
|
}
|
|
}
|
|
|
|
func groupStageStatus() -> String {
|
|
let groupStageTeamsCount = groupStageTeams().count
|
|
if groupStageTeamsCount == 0 || groupStageTeamsCount != teamsPerGroupStage * groupStageCount {
|
|
return "à faire"
|
|
}
|
|
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 {
|
|
if state() == .initial {
|
|
return "à valider"
|
|
} else {
|
|
let groupStageLabel: String? = groupStageCount > 0 ? groupStageCount.formatted() + " poule\(groupStageCount.pluralSuffix)" : nil
|
|
return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ")
|
|
}
|
|
}
|
|
|
|
func deleteAndBuildEverything() {
|
|
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)
|
|
|
|
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, name: Match.setServerTitle(upperRound: round, matchIndex: RoundRule.matchIndexWithinRound(fromMatchIndex: $0)))
|
|
}
|
|
|
|
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 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 = Store.main.filter(isIncluded: { $0.tournament == id && $0.index == roundIndex }).first {
|
|
return Store.main.filter(isIncluded: { $0.round == round.id && $0.index == matchIndex }).first
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resetTeamScores(in matchOfBracketPosition: Int?) {
|
|
guard let match = match(for: matchOfBracketPosition) else { return }
|
|
match.resetTeamScores()
|
|
}
|
|
|
|
func updateTeamScores(in matchOfBracketPosition: Int?) {
|
|
guard let match = match(for: matchOfBracketPosition) else { return }
|
|
match.updateTeamScores()
|
|
}
|
|
|
|
func deleteStructure() {
|
|
do {
|
|
try DataStore.shared.rounds.delete(contentOfs: rounds())
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
unsortedTeams().forEach({ $0.bracketPosition = nil })
|
|
do {
|
|
try DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func deleteGroupStages() {
|
|
do {
|
|
try DataStore.shared.groupStages.delete(contentOfs: groupStages())
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
|
|
do {
|
|
unsortedTeams().forEach({
|
|
$0.groupStage = nil
|
|
$0.groupStagePosition = nil
|
|
})
|
|
try DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func refreshGroupStages() {
|
|
unsortedTeams().forEach { team in
|
|
team.groupStage = nil
|
|
team.groupStagePosition = nil
|
|
}
|
|
|
|
if groupStageCount > 0 {
|
|
switch groupStageOrderingMode {
|
|
case .random:
|
|
setGroupStage(randomize: true)
|
|
case .snake:
|
|
setGroupStage(randomize: false)
|
|
case .swiss:
|
|
setGroupStage(randomize: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setGroupStage(randomize: Bool) {
|
|
let groupStages = groupStages()
|
|
let numberOfBracketsAsInt = groupStages.count
|
|
// let teamsPerBracket = teamsPerBracket
|
|
if groupStageCount != numberOfBracketsAsInt {
|
|
deleteGroupStages()
|
|
buildGroupStages()
|
|
}
|
|
|
|
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
|
|
|
|
try? DataStore.shared.teamRegistrations.addOrUpdate(instance: chunks[index][jIndex])
|
|
}
|
|
}
|
|
|
|
groupStages.forEach { $0.buildMatches() }
|
|
}
|
|
|
|
|
|
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!)]
|
|
case .inscriptionDate:
|
|
[.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight)]
|
|
}
|
|
}
|
|
|
|
func isSameBuild(_ build: any TournamentBuildHolder) -> Bool {
|
|
tournamentLevel == build.level
|
|
&& tournamentCategory == build.category
|
|
&& federalTournamentAge == build.age
|
|
}
|
|
|
|
private let _currentSelectionSorting : [MySortDescriptor<TeamRegistration>] = [.keyPath(\.weight), .keyPath(\.registrationDate!)]
|
|
|
|
override func deleteDependencies() throws {
|
|
try Store.main.deleteDependencies(items: self.unsortedTeams())
|
|
try Store.main.deleteDependencies(items: self.groupStages())
|
|
try Store.main.deleteDependencies(items: self.rounds())
|
|
try Store.main.deleteDependencies(items: self._matchSchedulers())
|
|
}
|
|
|
|
private func _matchSchedulers() -> [MatchScheduler] {
|
|
Store.main.filter(isIncluded: { $0.tournament == self.id })
|
|
}
|
|
|
|
func matchScheduler() -> MatchScheduler? {
|
|
_matchSchedulers().first
|
|
}
|
|
|
|
func currentMonthData() -> MonthData? {
|
|
guard let rankSourceDate else { return nil }
|
|
let dateString = URL.importDateFormatter.string(from: rankSourceDate)
|
|
return Store.main.filter(isIncluded: { $0.monthKey == dateString }).first
|
|
}
|
|
|
|
var maleUnrankedValue: Int? {
|
|
currentMonthData()?.maleUnrankedValue
|
|
}
|
|
|
|
var femaleUnrankedValue: Int? {
|
|
currentMonthData()?.femaleUnrankedValue
|
|
}
|
|
|
|
func courtNameIfAvailable(atIndex courtIndex: Int) -> String? {
|
|
club()?.customizedCourts.first(where: { $0.index == courtIndex })?.name
|
|
}
|
|
|
|
func courtName(atIndex courtIndex: Int) -> String {
|
|
courtNameIfAvailable(atIndex: courtIndex) ?? Court.courtIndexedTitle(atIndex: courtIndex)
|
|
}
|
|
|
|
func tournamentWinner() -> TeamRegistration? {
|
|
let final : Round? = Store.main.filter(isIncluded: { $0.index == 0 && $0.tournament == id && $0.parent == nil }).first
|
|
return final?.playedMatches().first?.winner()
|
|
}
|
|
|
|
// 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(.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 }.sorted(by: \.startDate).reversed()
|
|
let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments)
|
|
let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments)
|
|
let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments)
|
|
//creator: DataStore.shared.user?.id
|
|
return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge)
|
|
}
|
|
|
|
static func fake() -> Tournament {
|
|
return Tournament(event: "Roland Garros", name: "Magic P100", startDate: Date(), endDate: Date(), creationDate: Date(), isPrivate: false, groupStageFormat: .nineGames, roundFormat: nil, loserRoundFormat: nil, groupStageSortMode: .snake, groupStageCount: 4, rankSourceDate: nil, dayDuration: 2, teamCount: 24, teamSorting: .rank, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .a45, closedRegistrationDate: nil, groupStageAdditionalQualified: 0, courtCount: 4, prioritizeClubMembers: false, qualifiedPerGroupStage: 2, teamsPerGroupStage: 4, entryFee: nil)
|
|
}
|
|
|
|
}
|
|
|