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.
708 lines
28 KiB
708 lines
28 KiB
//
|
|
// GroupStage.swift
|
|
// Padel Tournament
|
|
//
|
|
// Created by razmig on 10/03/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import LeStorage
|
|
import Algorithms
|
|
import SwiftUI
|
|
|
|
@Observable
|
|
final class GroupStage: ModelObject, Storable {
|
|
static func resourceName() -> String { "group-stages" }
|
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
|
|
static func filterByStoreIdentifier() -> Bool { return true }
|
|
static var relationshipNames: [String] = []
|
|
|
|
var id: String = Store.randomId()
|
|
var tournament: String
|
|
var index: Int
|
|
var size: Int
|
|
private var format: MatchFormat?
|
|
var startDate: Date?
|
|
var name: String?
|
|
var step: Int = 0
|
|
|
|
var matchFormat: MatchFormat {
|
|
get {
|
|
format ?? .defaultFormatForMatchType(.groupStage)
|
|
}
|
|
set {
|
|
format = newValue
|
|
}
|
|
}
|
|
|
|
internal init(tournament: String, index: Int, size: Int, matchFormat: MatchFormat? = nil, startDate: Date? = nil, name: String? = nil, step: Int = 0) {
|
|
self.tournament = tournament
|
|
self.index = index
|
|
self.size = size
|
|
self.format = matchFormat
|
|
self.startDate = startDate
|
|
self.name = name
|
|
self.step = step
|
|
}
|
|
|
|
var tournamentStore: TournamentStore {
|
|
return TournamentStore.instance(tournamentId: self.tournament)
|
|
}
|
|
|
|
// MARK: - Computed dependencies
|
|
|
|
func _matches() -> [Match] {
|
|
return self.tournamentStore.matches.filter { $0.groupStage == self.id }.sorted(by: \.index)
|
|
// Store.main.filter { $0.groupStage == self.id }
|
|
}
|
|
|
|
func tournamentObject() -> Tournament? {
|
|
Store.main.findById(self.tournament)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
func teamAt(groupStagePosition: Int) -> TeamRegistration? {
|
|
if step > 0 {
|
|
return teams().first(where: { $0.groupStagePositionAtStep(step) == groupStagePosition })
|
|
}
|
|
return teams().first(where: { $0.groupStagePosition == groupStagePosition })
|
|
}
|
|
|
|
func groupStageTitle(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
if let name { return name }
|
|
|
|
var stepLabel = ""
|
|
if step > 0 {
|
|
stepLabel = " (" + (step + 1).ordinalFormatted(feminine: true) + " phase)"
|
|
}
|
|
|
|
switch displayStyle {
|
|
case .title:
|
|
return "Poule \(index + 1)" + stepLabel
|
|
case .wide:
|
|
return "Poule \(index + 1)"
|
|
case .short:
|
|
return "#\(index + 1)"
|
|
}
|
|
}
|
|
|
|
var computedOrder: Int {
|
|
index + step * 100
|
|
}
|
|
|
|
func isRunning() -> Bool { // at least a match has started
|
|
_matches().anySatisfy({ $0.isRunning() })
|
|
}
|
|
|
|
func hasStarted() -> Bool { // meaning at least one match is over
|
|
_matches().filter { $0.hasEnded() }.isEmpty == false
|
|
}
|
|
|
|
func hasEnded() -> Bool {
|
|
let _matches = _matches()
|
|
if _matches.isEmpty { return false }
|
|
//guard teams().count == size else { return false }
|
|
return _matches.anySatisfy { $0.hasEnded() == false } == false
|
|
}
|
|
|
|
fileprivate func _createMatch(index: Int) -> Match {
|
|
let match: Match = Match(groupStage: self.id,
|
|
index: index,
|
|
matchFormat: self.matchFormat,
|
|
name: self.localizedMatchUpLabel(for: index))
|
|
match.store = self.store
|
|
print("_createMatch(index)", index)
|
|
return match
|
|
}
|
|
|
|
func removeReturnMatches(onlyLast: Bool = false) {
|
|
|
|
var returnMatches = _matches().filter({ $0.index >= matchCount })
|
|
if onlyLast {
|
|
let matchPhaseCount = matchPhaseCount - 1
|
|
returnMatches = returnMatches.filter({ $0.index >= matchCount * matchPhaseCount })
|
|
}
|
|
do {
|
|
try self.tournamentStore.matches.delete(contentOfs: returnMatches)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
var matchPhaseCount: Int {
|
|
let count = _matches().count
|
|
if matchCount > 0 {
|
|
return count / matchCount
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func addReturnMatches() {
|
|
var teamScores = [TeamScore]()
|
|
var matches = [Match]()
|
|
let matchPhaseCount = matchPhaseCount
|
|
for i in 0..<_numberOfMatchesToBuild() {
|
|
let newMatch = self._createMatch(index: i + matchCount * matchPhaseCount)
|
|
// let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i))
|
|
teamScores.append(contentsOf: newMatch.createTeamScores())
|
|
matches.append(newMatch)
|
|
}
|
|
|
|
do {
|
|
try self.tournamentStore.matches.addOrUpdate(contentOfs: matches)
|
|
try self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func buildMatches(keepExistingMatches: Bool = false) {
|
|
var teamScores = [TeamScore]()
|
|
var matches = [Match]()
|
|
clearScoreCache()
|
|
|
|
if keepExistingMatches == false {
|
|
_removeMatches()
|
|
|
|
for i in 0..<_numberOfMatchesToBuild() {
|
|
let newMatch = self._createMatch(index: i)
|
|
// let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i))
|
|
teamScores.append(contentsOf: newMatch.createTeamScores())
|
|
matches.append(newMatch)
|
|
}
|
|
} else {
|
|
for match in _matches() {
|
|
match.resetTeamScores(outsideOf: [])
|
|
teamScores.append(contentsOf: match.createTeamScores())
|
|
}
|
|
}
|
|
|
|
do {
|
|
try self.tournamentStore.matches.addOrUpdate(contentOfs: matches)
|
|
try self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func playedMatches() -> [Match] {
|
|
let ordered = _matches()
|
|
let order = _matchOrder()
|
|
let matchCount = max(1, matchCount)
|
|
let count = ordered.count / matchCount
|
|
if ordered.isEmpty == false && ordered.count % order.count == 0 {
|
|
let repeatedArray = (0..<count).flatMap { i in
|
|
order.map { $0 + i * order.count }
|
|
}
|
|
let result = repeatedArray.map { ordered[$0] }
|
|
return result
|
|
} else {
|
|
return ordered
|
|
}
|
|
}
|
|
|
|
func orderedIndexOfMatch(_ match: Match) -> Int {
|
|
_matchOrder()[safe: match.index] ?? match.index
|
|
}
|
|
|
|
func updateGroupStageState() {
|
|
clearScoreCache()
|
|
|
|
if hasEnded(), let tournament = tournamentObject() {
|
|
do {
|
|
let teams = teams(true)
|
|
for (index, team) in teams.enumerated() {
|
|
team.qualified = index < tournament.qualifiedPerGroupStage
|
|
if team.bracketPosition != nil && team.qualified == false {
|
|
tournamentObject()?.shouldVerifyBracket = true
|
|
}
|
|
}
|
|
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
|
|
if let tournamentObject = tournamentObject() {
|
|
try DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject)
|
|
}
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
|
|
let groupStagesAreOverAtFirstStep = tournament.groupStagesAreOver(atStep: 0)
|
|
let nextStepGroupStages = tournament.groupStages(atStep: 1)
|
|
let groupStagesAreOverAtSecondStep = tournament.groupStagesAreOver(atStep: 1)
|
|
|
|
if groupStagesAreOverAtFirstStep, nextStepGroupStages.isEmpty || groupStagesAreOverAtSecondStep == true, tournament.groupStageLoserBracketAreOver(), tournament.rounds().isEmpty {
|
|
tournament.endDate = Date()
|
|
do {
|
|
try DataStore.shared.tournaments.addOrUpdate(instance: tournament)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func scoreLabel(forGroupStagePosition groupStagePosition: Int, score: TeamGroupStageScore? = nil) -> (wins: String, losses: String, setsDifference: String?, gamesDifference: String?)? {
|
|
if let scoreData = (score ?? _score(forGroupStagePosition: groupStagePosition, nilIfEmpty: true)) {
|
|
let hideSetDifference = matchFormat.setsToWin == 1
|
|
let setDifference = scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: true))) + " set" + scoreData.setDifference.pluralSuffix
|
|
let gameDifference = scoreData.gameDifference.formatted(.number.sign(strategy: .always(includingZero: true))) + " jeu" + scoreData.gameDifference.localizedPluralSuffix("x")
|
|
return (wins: scoreData.wins.formatted(), losses: scoreData.loses.formatted(), setsDifference: hideSetDifference ? nil : setDifference, gamesDifference: gameDifference)
|
|
// return "\(scoreData.wins)/\(scoreData.loses) " + differenceAsString
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? {
|
|
// guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil }
|
|
// let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() })
|
|
// if matches.isEmpty && nilIfEmpty { return nil }
|
|
// let wins = matches.filter { $0.winningTeamId == team.id }.count
|
|
// let loses = matches.filter { $0.losingTeamId == team.id }.count
|
|
// let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) }
|
|
// let setDifference = differences.map { $0.set }.reduce(0,+)
|
|
// let gameDifference = differences.map { $0.game }.reduce(0,+)
|
|
// return (team, wins, loses, setDifference, gameDifference)
|
|
// /*
|
|
// • 2 points par rencontre gagnée
|
|
// • 1 point par rencontre perdue
|
|
// • -1 point en cas de rencontre perdue par disqualification (scores de 6/0 6/0 attribués aux trois matchs)
|
|
// • -2 points en cas de rencontre perdu par WO (scores de 6/0 6/0 attribués aux trois matchs)
|
|
// */
|
|
// }
|
|
//
|
|
func matches(forGroupStagePosition groupStagePosition: Int) -> [Match] {
|
|
let combos = Array((0..<size).combinations(ofCount: 2))
|
|
var matchIndexes = [Int]()
|
|
for (index, combo) in combos.enumerated() {
|
|
if combo.contains(groupStagePosition) { //team is playing
|
|
matchIndexes.append(index)
|
|
}
|
|
}
|
|
return _matches().filter { matchIndexes.contains($0.index%matchCount) }
|
|
}
|
|
|
|
func initialStartDate(forTeam team: TeamRegistration) -> Date? {
|
|
guard let groupStagePosition = team.groupStagePositionAtStep(step) else { return nil }
|
|
return matches(forGroupStagePosition: groupStagePosition).compactMap({ $0.startDate }).sorted().first ?? startDate
|
|
}
|
|
|
|
func matchPlayed(by groupStagePosition: Int, againstPosition: Int) -> Match? {
|
|
if groupStagePosition == againstPosition { return nil }
|
|
let combos = Array((0..<size).combinations(ofCount: 2))
|
|
var matchIndexes = [Int]()
|
|
for (index, combo) in combos.enumerated() {
|
|
if combo.contains(groupStagePosition) && combo.contains(againstPosition) { //teams are playing
|
|
matchIndexes.append(index)
|
|
}
|
|
}
|
|
return _matches().first(where: { matchIndexes.contains($0.index) })
|
|
}
|
|
|
|
func availableToStart(playedMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] {
|
|
#if DEBUG //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func group stage availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
return playedMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(by: \.computedStartDateForSorting)
|
|
}
|
|
|
|
func runningMatches(playedMatches: [Match]) -> [Match] {
|
|
#if DEBUG //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func group stage runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting)
|
|
}
|
|
|
|
func readyMatches(playedMatches: [Match]) -> [Match] {
|
|
#if DEBUG //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func group stage readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false })
|
|
}
|
|
|
|
func finishedMatches(playedMatches: [Match]) -> [Match] {
|
|
#if DEBUG //DEBUGING TIME
|
|
let start = Date()
|
|
defer {
|
|
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
|
|
print("func group stage finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
|
|
}
|
|
#endif
|
|
return playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed()
|
|
}
|
|
|
|
func isReturnMatchEnabled() -> Bool {
|
|
_matches().count > matchCount
|
|
}
|
|
|
|
private func _matchOrder() -> [Int] {
|
|
var order: [Int]
|
|
|
|
switch size {
|
|
case 3:
|
|
order = [1, 2, 0]
|
|
case 4:
|
|
order = [2, 3, 1, 4, 5, 0]
|
|
case 5:
|
|
order = [3, 5, 8, 2, 6, 1, 9, 4, 7, 0]
|
|
case 6:
|
|
order = [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0]
|
|
default:
|
|
order = []
|
|
}
|
|
|
|
return order
|
|
}
|
|
|
|
|
|
func indexOf(_ matchIndex: Int) -> Int {
|
|
_matchOrder().firstIndex(of: matchIndex) ?? matchIndex
|
|
}
|
|
|
|
func _matchUp(for matchIndex: Int) -> [Int] {
|
|
let combinations = Array((0..<size).combinations(ofCount: 2))
|
|
return combinations[safe: matchIndex%matchCount] ?? []
|
|
}
|
|
|
|
func returnMatchesSuffix(for matchIndex: Int) -> String {
|
|
if matchCount > 0 {
|
|
let count = _matches().count
|
|
if count > matchCount * 2 {
|
|
return " - vague \((matchIndex / matchCount) + 1)"
|
|
}
|
|
|
|
if matchIndex >= matchCount {
|
|
return " - retour"
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func localizedMatchUpLabel(for matchIndex: Int) -> String {
|
|
let matchUp = _matchUp(for: matchIndex)
|
|
if let index = matchUp.first, let index2 = matchUp.last {
|
|
return "#\(index + 1) vs #\(index2 + 1)" + returnMatchesSuffix(for: matchIndex)
|
|
} else {
|
|
return "--"
|
|
}
|
|
}
|
|
|
|
var matchCount: Int {
|
|
(size * (size - 1)) / 2
|
|
}
|
|
|
|
func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
|
|
let _teams = _teams(for: matchIndex)
|
|
switch team {
|
|
case .one:
|
|
return _teams.first ?? nil
|
|
case .two:
|
|
return _teams.last ?? nil
|
|
}
|
|
}
|
|
|
|
private func _teams(for matchIndex: Int) -> [TeamRegistration?] {
|
|
let combinations = Array(0..<size).combinations(ofCount: 2).map {$0}
|
|
return combinations[safe: matchIndex%matchCount]?.map { teamAt(groupStagePosition: $0) } ?? []
|
|
}
|
|
|
|
private func _removeMatches() {
|
|
do {
|
|
try self.tournamentStore.matches.delete(contentOfs: _matches())
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
private func _numberOfMatchesToBuild() -> Int {
|
|
(size * (size - 1)) / 2
|
|
}
|
|
|
|
func unsortedPlayers() -> [PlayerRegistration] {
|
|
unsortedTeams().flatMap({ $0.unsortedPlayers() })
|
|
}
|
|
|
|
fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool
|
|
|
|
typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int)
|
|
|
|
fileprivate func _headToHead(_ teamPosition: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool {
|
|
let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted()
|
|
let combos = Array((0..<size).combinations(ofCount: 2))
|
|
let matchIndexes = combos.enumerated().compactMap { $0.element == indexes ? $0.offset : nil }
|
|
let matches = _matches().filter { matchIndexes.contains($0.index) }
|
|
if matches.count > 1 {
|
|
let scoreA = calculateScore(for: teamPosition, matches: matches, groupStagePosition: teamPosition.groupStagePosition!)
|
|
let scoreB = calculateScore(for: otherTeam, matches: matches, groupStagePosition: otherTeam.groupStagePosition!)
|
|
|
|
let teamsSorted = [scoreA, scoreB].sorted { (lhs, rhs) in
|
|
let predicates: [TeamScoreAreInIncreasingOrder] = [
|
|
{ $0.wins < $1.wins },
|
|
{ $0.setDifference < $1.setDifference },
|
|
{ $0.gameDifference < $1.gameDifference},
|
|
{ [self] in $0.team.groupStagePositionAtStep(self.step)! > $1.team.groupStagePositionAtStep(self.step)! }
|
|
]
|
|
|
|
for predicate in predicates {
|
|
if !predicate(lhs, rhs) && !predicate(rhs, lhs) {
|
|
continue
|
|
}
|
|
|
|
return predicate(lhs, rhs)
|
|
}
|
|
|
|
return false
|
|
}.map({ $0.team })
|
|
|
|
return teamsSorted.first == teamPosition
|
|
} else {
|
|
|
|
if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) {
|
|
return teamPosition.id == match.losingTeamId
|
|
} else {
|
|
return false
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func unsortedTeams() -> [TeamRegistration] {
|
|
if step > 0 {
|
|
return self.tournamentStore.groupStages.filter({ $0.step == step - 1 }).compactMap({ $0.teams(true)[safe: index] })
|
|
}
|
|
return self.tournamentStore.teamRegistrations.filter { $0.groupStage == self.id && $0.groupStagePosition != nil }
|
|
}
|
|
|
|
|
|
var scoreCache: [Int: TeamGroupStageScore] = [:]
|
|
|
|
func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] {
|
|
if sortedByScore {
|
|
return unsortedTeams().compactMap({ team in
|
|
// Check cache or use provided scores, otherwise calculate and store in cache
|
|
scores?.first(where: { $0.team.id == team.id }) ?? {
|
|
if let cachedScore = scoreCache[team.groupStagePositionAtStep(step)!] {
|
|
return cachedScore
|
|
} else {
|
|
let score = _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!)
|
|
if let score = score {
|
|
scoreCache[team.groupStagePositionAtStep(step)!] = score
|
|
}
|
|
return score
|
|
}
|
|
}()
|
|
}).sorted { (lhs, rhs) in
|
|
let predicates: [TeamScoreAreInIncreasingOrder] = [
|
|
{ $0.wins < $1.wins },
|
|
{ $0.setDifference < $1.setDifference },
|
|
{ $0.gameDifference < $1.gameDifference},
|
|
{ self._headToHead($0.team, $1.team) },
|
|
{ [self] in $0.team.groupStagePositionAtStep(self.step)! > $1.team.groupStagePositionAtStep(self.step)! }
|
|
]
|
|
|
|
for predicate in predicates {
|
|
if !predicate(lhs, rhs) && !predicate(rhs, lhs) {
|
|
continue
|
|
}
|
|
|
|
return predicate(lhs, rhs)
|
|
}
|
|
|
|
return false
|
|
}.map({ $0.team }).reversed()
|
|
} else {
|
|
return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!)
|
|
}
|
|
}
|
|
|
|
func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? {
|
|
// Check if the score for this position is already cached
|
|
if let cachedScore = scoreCache[groupStagePosition] {
|
|
return cachedScore
|
|
}
|
|
|
|
guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil }
|
|
let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() })
|
|
if matches.isEmpty && nilIfEmpty { return nil }
|
|
let score = calculateScore(for: team, matches: matches, groupStagePosition: groupStagePosition)
|
|
scoreCache[groupStagePosition] = score
|
|
return score
|
|
}
|
|
|
|
private func calculateScore(for team: TeamRegistration, matches: [Match], groupStagePosition: Int) -> TeamGroupStageScore {
|
|
let wins = matches.filter { $0.winningTeamId == team.id }.count
|
|
let loses = matches.filter { $0.losingTeamId == team.id }.count
|
|
let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) }
|
|
let setDifference = differences.map { $0.set }.reduce(0,+)
|
|
let gameDifference = differences.map { $0.game }.reduce(0,+)
|
|
|
|
return (team, wins, loses, setDifference, gameDifference)
|
|
}
|
|
|
|
// Clear the cache if necessary, for example when starting a new step or when matches update
|
|
func clearScoreCache() {
|
|
scoreCache.removeAll()
|
|
}
|
|
|
|
// func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] {
|
|
// if sortedByScore {
|
|
// return unsortedTeams().compactMap({ team in
|
|
// scores?.first(where: { $0.team.id == team.id }) ?? _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!)
|
|
// }).sorted { (lhs, rhs) in
|
|
// // Calculate intermediate values once and reuse them
|
|
// let lhsWins = lhs.wins
|
|
// let rhsWins = rhs.wins
|
|
// let lhsSetDifference = lhs.setDifference
|
|
// let rhsSetDifference = rhs.setDifference
|
|
// let lhsGameDifference = lhs.gameDifference
|
|
// let rhsGameDifference = rhs.gameDifference
|
|
// let lhsHeadToHead = self._headToHead(lhs.team, rhs.team)
|
|
// let rhsHeadToHead = self._headToHead(rhs.team, lhs.team)
|
|
// let lhsGroupStagePosition = lhs.team.groupStagePositionAtStep(self.step)!
|
|
// let rhsGroupStagePosition = rhs.team.groupStagePositionAtStep(self.step)!
|
|
//
|
|
// // Define comparison predicates in the same order
|
|
// let predicates: [(Bool, Bool)] = [
|
|
// (lhsWins < rhsWins, lhsWins > rhsWins),
|
|
// (lhsSetDifference < rhsSetDifference, lhsSetDifference > rhsSetDifference),
|
|
// (lhsGameDifference < rhsGameDifference, lhsGameDifference > rhsGameDifference),
|
|
// (lhsHeadToHead, rhsHeadToHead),
|
|
// (lhsGroupStagePosition > rhsGroupStagePosition, lhsGroupStagePosition < rhsGroupStagePosition)
|
|
// ]
|
|
//
|
|
// // Iterate over predicates and return as soon as a valid comparison is found
|
|
// for (lhsPredicate, rhsPredicate) in predicates {
|
|
// if lhsPredicate { return true }
|
|
// if rhsPredicate { return false }
|
|
// }
|
|
//
|
|
// return false
|
|
// }.map({ $0.team }).reversed()
|
|
// } else {
|
|
// return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!)
|
|
// }
|
|
// }
|
|
|
|
func updateMatchFormat(_ updatedMatchFormat: MatchFormat) {
|
|
self.matchFormat = updatedMatchFormat
|
|
self.updateAllMatchesFormat()
|
|
}
|
|
|
|
func updateAllMatchesFormat() {
|
|
let playedMatches = playedMatches()
|
|
playedMatches.forEach { match in
|
|
match.matchFormat = matchFormat
|
|
}
|
|
do {
|
|
try self.tournamentStore.matches.addOrUpdate(contentOfs: playedMatches)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func pasteData() -> String {
|
|
var data: [String] = []
|
|
data.append(self.groupStageTitle())
|
|
teams().forEach { team in
|
|
data.append(team.teamLabelRanked(displayRank: true, displayTeamName: true))
|
|
}
|
|
|
|
return data.joined(separator: "\n")
|
|
}
|
|
|
|
func finalPosition(ofTeam team: TeamRegistration) -> Int? {
|
|
guard hasEnded() else { return nil }
|
|
return teams(true).firstIndex(of: team)
|
|
}
|
|
|
|
override func deleteDependencies() throws {
|
|
let matches = self._matches()
|
|
for match in matches {
|
|
try match.deleteDependencies()
|
|
}
|
|
self.tournamentStore.matches.deleteDependencies(matches)
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
id = try container.decode(String.self, forKey: ._id)
|
|
tournament = try container.decode(String.self, forKey: ._tournament)
|
|
index = try container.decode(Int.self, forKey: ._index)
|
|
size = try container.decode(Int.self, forKey: ._size)
|
|
format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format)
|
|
startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate)
|
|
name = try container.decodeIfPresent(String.self, forKey: ._name)
|
|
step = try container.decodeIfPresent(Int.self, forKey: ._step) ?? 0
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
try container.encode(id, forKey: ._id)
|
|
try container.encode(tournament, forKey: ._tournament)
|
|
try container.encode(index, forKey: ._index)
|
|
try container.encode(size, forKey: ._size)
|
|
try container.encode(format, forKey: ._format)
|
|
try container.encode(startDate, forKey: ._startDate)
|
|
try container.encode(name, forKey: ._name)
|
|
try container.encode(step, forKey: ._step)
|
|
}
|
|
|
|
func insertOnServer() {
|
|
self.tournamentStore.groupStages.writeChangeAndInsertOnServer(instance: self)
|
|
for match in self._matches() {
|
|
match.insertOnServer()
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension GroupStage {
|
|
enum CodingKeys: String, CodingKey {
|
|
case _id = "id"
|
|
case _tournament = "tournament"
|
|
case _index = "index"
|
|
case _size = "size"
|
|
case _format = "format"
|
|
case _startDate = "startDate"
|
|
case _name = "name"
|
|
case _step = "step"
|
|
}
|
|
}
|
|
|
|
extension GroupStage: Selectable {
|
|
func selectionLabel(index: Int) -> String {
|
|
groupStageTitle()
|
|
}
|
|
|
|
func badgeValue() -> Int? {
|
|
return runningMatches(playedMatches: _matches()).count
|
|
}
|
|
|
|
func badgeValueColor() -> Color? {
|
|
return nil
|
|
}
|
|
|
|
func badgeImage() -> Badge? {
|
|
if teams().count < size {
|
|
return .xmark
|
|
} else {
|
|
return hasEnded() ? .checkmark : nil
|
|
}
|
|
}
|
|
}
|
|
|