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.
681 lines
32 KiB
681 lines
32 KiB
//
|
|
// MatchScheduler.swift
|
|
// PadelClub
|
|
//
|
|
// Created by Razmig Sarkissian on 08/04/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import LeStorage
|
|
import SwiftUI
|
|
|
|
@Observable
|
|
class MatchScheduler : ModelObject, Storable {
|
|
|
|
static func resourceName() -> String { return "match-scheduler" }
|
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
|
|
|
|
private(set) var id: String = Store.randomId()
|
|
var tournament: String
|
|
var timeDifferenceLimit: Int
|
|
var loserBracketRotationDifference: Int
|
|
var upperBracketRotationDifference: Int
|
|
var accountUpperBracketBreakTime: Bool
|
|
var accountLoserBracketBreakTime: Bool
|
|
var randomizeCourts: Bool
|
|
var rotationDifferenceIsImportant: Bool
|
|
var shouldHandleUpperRoundSlice: Bool
|
|
var shouldEndRoundBeforeStartingNext: Bool
|
|
var groupStageChunkCount: Int?
|
|
|
|
init(tournament: String,
|
|
timeDifferenceLimit: Int = 5,
|
|
loserBracketRotationDifference: Int = 0,
|
|
upperBracketRotationDifference: Int = 1,
|
|
accountUpperBracketBreakTime: Bool = true,
|
|
accountLoserBracketBreakTime: Bool = false,
|
|
randomizeCourts: Bool = true,
|
|
rotationDifferenceIsImportant: Bool = false,
|
|
shouldHandleUpperRoundSlice: Bool = true,
|
|
shouldEndRoundBeforeStartingNext: Bool = true,
|
|
groupStageChunkCount: Int? = nil) {
|
|
self.tournament = tournament
|
|
self.timeDifferenceLimit = timeDifferenceLimit
|
|
self.loserBracketRotationDifference = loserBracketRotationDifference
|
|
self.upperBracketRotationDifference = upperBracketRotationDifference
|
|
self.accountUpperBracketBreakTime = accountUpperBracketBreakTime
|
|
self.accountLoserBracketBreakTime = accountLoserBracketBreakTime
|
|
self.randomizeCourts = randomizeCourts
|
|
self.rotationDifferenceIsImportant = rotationDifferenceIsImportant
|
|
self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice
|
|
self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext
|
|
self.groupStageChunkCount = groupStageChunkCount
|
|
}
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case _id = "id"
|
|
case _tournament = "tournament"
|
|
case _timeDifferenceLimit = "timeDifferenceLimit"
|
|
case _loserBracketRotationDifference = "loserBracketRotationDifference"
|
|
case _upperBracketRotationDifference = "upperBracketRotationDifference"
|
|
case _accountUpperBracketBreakTime = "accountUpperBracketBreakTime"
|
|
case _accountLoserBracketBreakTime = "accountLoserBracketBreakTime"
|
|
case _randomizeCourts = "randomizeCourts"
|
|
case _rotationDifferenceIsImportant = "rotationDifferenceIsImportant"
|
|
case _shouldHandleUpperRoundSlice = "shouldHandleUpperRoundSlice"
|
|
case _shouldEndRoundBeforeStartingNext = "shouldEndRoundBeforeStartingNext"
|
|
case _groupStageChunkCount = "groupStageChunkCount"
|
|
}
|
|
|
|
var courtsUnavailability: [DateInterval]? {
|
|
guard let event = tournamentObject()?.eventObject() else { return nil }
|
|
return event.courtsUnavailability + event.tournamentsCourtsUsed()
|
|
}
|
|
|
|
var additionalEstimationDuration : Int {
|
|
tournamentObject()?.additionalEstimationDuration ?? 0
|
|
}
|
|
|
|
func tournamentObject() -> Tournament? {
|
|
Store.main.findById(tournament)
|
|
}
|
|
|
|
@discardableResult
|
|
func updateGroupStageSchedule(tournament: Tournament) -> Date {
|
|
let computedGroupStageChunkCount = groupStageChunkCount ?? 1
|
|
let groupStages = tournament.groupStages()
|
|
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
|
|
|
|
let matches = groupStages.flatMap({ $0._matches() })
|
|
matches.forEach({
|
|
$0.removeCourt()
|
|
$0.startDate = nil
|
|
})
|
|
|
|
var lastDate : Date = tournament.startDate
|
|
let times = Set(groupStages.compactMap { $0.startDate }).sorted()
|
|
|
|
if let first = times.first {
|
|
if first.isEarlierThan(tournament.startDate) {
|
|
tournament.startDate = first
|
|
try? DataStore.shared.tournaments.addOrUpdate(instance: tournament)
|
|
}
|
|
}
|
|
|
|
times.forEach({ time in
|
|
lastDate = time
|
|
let groups = groupStages.filter({ $0.startDate == time })
|
|
let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate)
|
|
|
|
dispatch.timedMatches.forEach { matchSchedule in
|
|
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
|
|
let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
|
|
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60
|
|
if let startDate = match.groupStageObject?.startDate {
|
|
let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd)
|
|
match.startDate = matchStartDate
|
|
lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60)
|
|
}
|
|
match.setCourt(matchSchedule.courtIndex)
|
|
}
|
|
}
|
|
})
|
|
|
|
groupStages.filter({ $0.startDate == nil || times.contains($0.startDate!) == false }).chunked(into: computedGroupStageChunkCount).forEach { groups in
|
|
groups.forEach({ $0.startDate = lastDate })
|
|
try? DataStore.shared.groupStages.addOrUpdate(contentOfs: groups)
|
|
|
|
let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate)
|
|
|
|
dispatch.timedMatches.forEach { matchSchedule in
|
|
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
|
|
let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
|
|
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60
|
|
if let startDate = match.groupStageObject?.startDate {
|
|
let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd)
|
|
match.startDate = matchStartDate
|
|
lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60)
|
|
}
|
|
match.setCourt(matchSchedule.courtIndex)
|
|
}
|
|
}
|
|
}
|
|
try? DataStore.shared.matches.addOrUpdate(contentOfs: matches)
|
|
return lastDate
|
|
}
|
|
|
|
func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher {
|
|
|
|
let _groupStages = groupStages
|
|
|
|
// Get the maximum count of matches in any group
|
|
let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0
|
|
|
|
// Use zip and flatMap to flatten matches in the desired order
|
|
let flattenedMatches = (0..<maxMatchesCount).flatMap { index in
|
|
_groupStages.compactMap { group in
|
|
// Use optional subscript to safely access matches
|
|
let playedMatches = group.playedMatches()
|
|
return playedMatches.indices.contains(index) ? playedMatches[index] : nil
|
|
}
|
|
}
|
|
|
|
var slots = [GroupStageTimeMatch]()
|
|
var availableMatchs = flattenedMatches
|
|
var rotationIndex = 0
|
|
var teamsPerRotation = [Int: [String]]()
|
|
var freeCourtPerRotation = [Int: [Int]]()
|
|
var groupLastRotation = [Int: Int]()
|
|
let courtsUnavailability = courtsUnavailability
|
|
|
|
while slots.count < flattenedMatches.count {
|
|
teamsPerRotation[rotationIndex] = []
|
|
freeCourtPerRotation[rotationIndex] = []
|
|
let previousRotationBracketIndexes = slots.filter { $0.rotationIndex == rotationIndex - 1 }.map { ($0.groupIndex, 1) }
|
|
let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +)
|
|
var rotationMatches = Array(availableMatchs.filter({ match in
|
|
teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true
|
|
}).prefix(numberOfCourtsAvailablePerRotation))
|
|
|
|
if rotationIndex > 0 {
|
|
rotationMatches = rotationMatches.sorted(by: {
|
|
if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 {
|
|
return $0.groupStageObject!.index < $1.groupStageObject!.index
|
|
} else {
|
|
return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0
|
|
}
|
|
})
|
|
}
|
|
|
|
(0..<numberOfCourtsAvailablePerRotation).forEach { courtIndex in
|
|
//print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) })
|
|
if let first = rotationMatches.first(where: { match in
|
|
let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration)
|
|
let timeIntervalToAdd = (Double(rotationIndex)) * Double(estimatedDuration) * 60
|
|
|
|
let rotationStartDate: Date = startingDate.addingTimeInterval(timeIntervalToAdd)
|
|
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), courtsUnavailability: courtsUnavailability)
|
|
if courtIndex >= numberOfCourtsAvailablePerRotation - courtsUnavailable {
|
|
return false
|
|
} else {
|
|
return teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true
|
|
}
|
|
}) {
|
|
let timeMatch = GroupStageTimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.groupStageObject!.index)
|
|
slots.append(timeMatch)
|
|
teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds())
|
|
rotationMatches.removeAll(where: { $0.id == first.id })
|
|
availableMatchs.removeAll(where: { $0.id == first.id })
|
|
if let index = first.groupStageObject?.index {
|
|
groupLastRotation[index] = rotationIndex
|
|
}
|
|
} else {
|
|
freeCourtPerRotation[rotationIndex]!.append(courtIndex)
|
|
}
|
|
}
|
|
|
|
rotationIndex += 1
|
|
}
|
|
|
|
var organizedSlots = [GroupStageTimeMatch]()
|
|
for i in 0..<rotationIndex {
|
|
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted()
|
|
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
|
|
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex))
|
|
|
|
for j in 0..<matches.count {
|
|
matches[j].courtIndex = courts[j]
|
|
organizedSlots.append(matches[j])
|
|
}
|
|
}
|
|
|
|
|
|
return GroupStageMatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation)
|
|
}
|
|
|
|
func rotationDifference(loserBracket: Bool) -> Int {
|
|
if loserBracket {
|
|
return loserBracketRotationDifference
|
|
} else {
|
|
return upperBracketRotationDifference
|
|
}
|
|
}
|
|
|
|
func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool {
|
|
print(roundObject.roundTitle(), match.matchTitle())
|
|
|
|
if let roundStartDate = roundObject.startDate, targetedStartDate < roundStartDate {
|
|
print("can't start \(targetedStartDate) earlier than \(roundStartDate)")
|
|
if targetedStartDate == minimumTargetedEndDate {
|
|
minimumTargetedEndDate = roundStartDate
|
|
} else {
|
|
minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate)
|
|
}
|
|
return false
|
|
}
|
|
|
|
let previousMatches = roundObject.precedentMatches(ofMatch: match)
|
|
if previousMatches.isEmpty { return true }
|
|
|
|
let previousMatchSlots = slots.filter({ slot in
|
|
previousMatches.map { $0.id }.contains(slot.matchID)
|
|
})
|
|
|
|
if previousMatchSlots.isEmpty {
|
|
if previousMatches.filter({ $0.disabled == false }).allSatisfy({ $0.startDate != nil }) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
if previousMatches.filter({ $0.disabled == false }).count > previousMatchSlots.count {
|
|
if previousMatches.filter({ $0.disabled == false }).anySatisfy({ $0.startDate != nil }) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var includeBreakTime = false
|
|
|
|
if accountLoserBracketBreakTime && roundObject.isLoserBracket() {
|
|
includeBreakTime = true
|
|
}
|
|
|
|
if accountUpperBracketBreakTime && roundObject.isLoserBracket() == false {
|
|
includeBreakTime = true
|
|
}
|
|
|
|
let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex + rotationDifference(loserBracket: roundObject.isLoserBracket()) < rotationIndex })
|
|
|
|
guard let minimumPossibleEndDate = previousMatchSlots.map({ $0.estimatedEndDate(includeBreakTime: includeBreakTime) }).max() else {
|
|
return previousMatchIsInPreviousRotation
|
|
}
|
|
|
|
if targetedStartDate >= minimumPossibleEndDate {
|
|
if rotationDifferenceIsImportant {
|
|
return previousMatchIsInPreviousRotation
|
|
} else {
|
|
return true
|
|
}
|
|
} else {
|
|
if targetedStartDate == minimumTargetedEndDate {
|
|
minimumTargetedEndDate = minimumPossibleEndDate
|
|
} else {
|
|
minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate)
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? {
|
|
slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min()
|
|
}
|
|
|
|
func getNextEarliestAvailableDate(from slots: [TimeMatch]) -> [(Int, Date)] {
|
|
let byCourt = Dictionary(grouping: slots, by: { $0.courtIndex })
|
|
return (byCourt.keys.flatMap { courtIndex in
|
|
let matchesByCourt = byCourt[courtIndex]?.sorted(by: \.startDate)
|
|
let lastMatch = matchesByCourt?.last
|
|
var results = [(Int, Date)]()
|
|
if let courtFreeDate = lastMatch?.estimatedEndDate(includeBreakTime: false) {
|
|
results.append((courtIndex, courtFreeDate))
|
|
}
|
|
return results
|
|
}
|
|
)
|
|
}
|
|
|
|
func getAvailableCourts(from matches: [Match]) -> [(Int, Date)] {
|
|
let validMatches = matches.filter({ $0.courtIndex != nil && $0.startDate != nil })
|
|
let byCourt = Dictionary(grouping: validMatches, by: { $0.courtIndex! })
|
|
return (byCourt.keys.flatMap { court in
|
|
let matchesByCourt = byCourt[court]?.sorted(by: \.startDate!)
|
|
let lastMatch = matchesByCourt?.last
|
|
var results = [(Int, Date)]()
|
|
if let courtFreeDate = lastMatch?.estimatedEndDate(additionalEstimationDuration) {
|
|
results.append((court, courtFreeDate))
|
|
}
|
|
return results
|
|
}
|
|
)
|
|
}
|
|
|
|
func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher {
|
|
|
|
var slots = [TimeMatch]()
|
|
var _startDate: Date?
|
|
var rotationIndex = 0
|
|
var availableMatchs = flattenedMatches.filter({ $0.startDate == nil })
|
|
let courtsUnavailability = courtsUnavailability
|
|
|
|
flattenedMatches.filter { $0.startDate != nil }.sorted(by: \.startDate!).forEach { match in
|
|
if _startDate == nil {
|
|
_startDate = match.startDate
|
|
} else if match.startDate! > _startDate! {
|
|
_startDate = match.startDate
|
|
rotationIndex += 1
|
|
}
|
|
|
|
let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.courtIndex ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: match.matchFormat.breakTime.breakTime)
|
|
slots.append(timeMatch)
|
|
}
|
|
|
|
if slots.isEmpty == false {
|
|
rotationIndex += 1
|
|
}
|
|
|
|
var freeCourtPerRotation = [Int: [Int]]()
|
|
|
|
let availableCourt = numberOfCourtsAvailablePerRotation
|
|
|
|
var courts = initialCourts ?? (0..<availableCourt).map { $0 }
|
|
|
|
var shouldStartAtDispatcherDate = rotationIndex > 0
|
|
|
|
while availableMatchs.count > 0 {
|
|
freeCourtPerRotation[rotationIndex] = []
|
|
let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 })
|
|
var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate
|
|
|
|
if shouldStartAtDispatcherDate {
|
|
rotationStartDate = dispatcherStartDate
|
|
shouldStartAtDispatcherDate = false
|
|
} else {
|
|
courts = rotationIndex == 0 ? courts : (0..<availableCourt).map { $0 }
|
|
}
|
|
courts.sort()
|
|
print("courts available at rotation \(rotationIndex)", courts)
|
|
print("rotationStartDate", rotationStartDate)
|
|
|
|
if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], freeCourtPreviousRotation.count > 0 {
|
|
print("scenario where we are waiting for a breaktime to be over without any match to play in between or a free court was available and we need to recheck breaktime left on it")
|
|
let previousPreviousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) })
|
|
let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: true)
|
|
let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false)
|
|
|
|
let noBreakAlreadyTested = previousRotationSlots.anySatisfy({ $0.startDate == previousEndDateNoBreak })
|
|
if let previousEndDate, let previousEndDateNoBreak {
|
|
let differenceWithBreak = rotationStartDate.timeIntervalSince(previousEndDate)
|
|
let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak)
|
|
print("difference w break", differenceWithBreak)
|
|
print("difference w/o break", differenceWithoutBreak)
|
|
let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60)
|
|
var difference = differenceWithBreak
|
|
if differenceWithBreak <= 0 {
|
|
difference = differenceWithoutBreak
|
|
} else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds {
|
|
difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak)
|
|
}
|
|
|
|
if difference > timeDifferenceLimitInSeconds {
|
|
courts.removeAll(where: { index in freeCourtPreviousRotation.contains(index)
|
|
})
|
|
freeCourtPerRotation[rotationIndex] = courts
|
|
courts = freeCourtPreviousRotation
|
|
rotationStartDate = rotationStartDate.addingTimeInterval(-difference)
|
|
}
|
|
}
|
|
}
|
|
|
|
dispatchCourts(availableCourts: numberOfCourtsAvailablePerRotation, courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability)
|
|
rotationIndex += 1
|
|
}
|
|
|
|
var organizedSlots = [TimeMatch]()
|
|
for i in 0..<rotationIndex {
|
|
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted()
|
|
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
|
|
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.courtIndex))
|
|
|
|
for j in 0..<matches.count {
|
|
matches[j].courtIndex = courts[j]
|
|
organizedSlots.append(matches[j])
|
|
}
|
|
}
|
|
|
|
|
|
return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex)
|
|
}
|
|
|
|
func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) {
|
|
var matchPerRound = [Int: Int]()
|
|
var minimumTargetedEndDate: Date = rotationStartDate
|
|
print("dispatchCourts", courts.sorted(), rotationStartDate, rotationIndex)
|
|
courts.sorted().forEach { courtIndex in
|
|
print("trying to find a match for \(courtIndex) in \(rotationIndex)")
|
|
if let first = availableMatchs.first(where: { match in
|
|
let roundObject = match.roundObject!
|
|
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), courtsUnavailability: courtsUnavailability)
|
|
print("courtsUnavailable \(courtsUnavailable)")
|
|
if courtIndex >= availableCourts - courtsUnavailable {
|
|
return false
|
|
}
|
|
|
|
let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate)
|
|
|
|
let currentRotationSameRoundMatches = matchPerRound[roundObject.index] ?? 0
|
|
|
|
if shouldHandleUpperRoundSlice {
|
|
let roundMatchesCount = roundObject.playedMatches().count
|
|
print("shouldHandleUpperRoundSlice \(roundMatchesCount)")
|
|
if roundObject.parent == nil && roundMatchesCount > courts.count {
|
|
print("roundMatchesCount \(roundMatchesCount) > \(courts.count)")
|
|
if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) {
|
|
print("return false, \(currentRotationSameRoundMatches) >= \(min(roundMatchesCount / 2, courts.count))")
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
let indexInRound = match.indexInRound()
|
|
|
|
print("Upper Round, index > 0, first Match of round \(indexInRound) and more than one court available; looking for next match (same round) \(indexInRound + 1)")
|
|
if roundObject.parent == nil && roundObject.index > 0, indexInRound == 0, courts.count > 1, let nextMatch = match.next() {
|
|
if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) {
|
|
|
|
print("next match and this match can be played, returning true")
|
|
|
|
return true
|
|
} else {
|
|
print("next match and this match can not be played at the same time, returning false")
|
|
return false
|
|
}
|
|
}
|
|
|
|
|
|
print("\(currentRotationSameRoundMatches) modulo \(currentRotationSameRoundMatches%2) same round match is even, index of round is not 0 and upper bracket. If it's not the last court available \(courtIndex) == \(courts.count - 1)")
|
|
if currentRotationSameRoundMatches%2 == 0 && roundObject.index != 0 && roundObject.parent == nil && courtIndex == courts.count - 1 {
|
|
print("we return false")
|
|
return false
|
|
}
|
|
|
|
|
|
return canBePlayed
|
|
}) {
|
|
print(first.roundObject!.roundTitle(), first.matchTitle(), courtIndex, rotationStartDate)
|
|
|
|
if first.roundObject!.parent == nil {
|
|
if let roundIndex = matchPerRound[first.roundObject!.index] {
|
|
matchPerRound[first.roundObject!.index] = roundIndex + 1
|
|
} else {
|
|
matchPerRound[first.roundObject!.index] = 1
|
|
}
|
|
}
|
|
let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, startDate: rotationStartDate, durationLeft: first.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: first.matchFormat.breakTime.breakTime)
|
|
slots.append(timeMatch)
|
|
availableMatchs.removeAll(where: { $0.id == first.id })
|
|
} else {
|
|
freeCourtPerRotation[rotationIndex]!.append(courtIndex)
|
|
}
|
|
}
|
|
|
|
if freeCourtPerRotation[rotationIndex]!.count == availableCourts {
|
|
print("no match found to be put in this rotation, check if we can put anything to another date")
|
|
freeCourtPerRotation[rotationIndex] = []
|
|
let courtsUsed = getNextEarliestAvailableDate(from: slots)
|
|
var freeCourts: [Int] = []
|
|
if courtsUsed.isEmpty {
|
|
freeCourts = (0..<availableCourts).map { $0 }
|
|
} else {
|
|
freeCourts = courtsUsed.filter { (courtIndex, availableDate) in
|
|
availableDate <= minimumTargetedEndDate
|
|
}.sorted(by: \.1).map { $0.0 }
|
|
}
|
|
|
|
dispatchCourts(availableCourts: availableCourts, courts: freeCourts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: minimumTargetedEndDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability)
|
|
}
|
|
}
|
|
|
|
func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) {
|
|
|
|
let upperRounds = tournament.rounds()
|
|
let allMatches = tournament.allMatches()
|
|
|
|
var rounds = [Round]()
|
|
|
|
if shouldEndRoundBeforeStartingNext {
|
|
rounds = upperRounds.flatMap {
|
|
[$0] + $0.loserRoundsAndChildren()
|
|
}
|
|
} else {
|
|
rounds = upperRounds.map {
|
|
$0
|
|
} + upperRounds.flatMap {
|
|
$0.loserRoundsAndChildren()
|
|
}
|
|
}
|
|
|
|
let flattenedMatches = rounds.flatMap { round in
|
|
round._matches().filter({ $0.disabled == false }).sorted(by: \.index)
|
|
}
|
|
|
|
flattenedMatches.forEach({
|
|
if (roundId == nil && matchId == nil) || $0.startDate?.isEarlierThan(startDate) == false {
|
|
$0.startDate = nil
|
|
$0.removeCourt()
|
|
}
|
|
})
|
|
|
|
// if let roundId {
|
|
// if let round : Round = Store.main.findById(roundId) {
|
|
// let matches = round._matches().filter({ $0.disabled == false }).sorted(by: \.index)
|
|
// round.resetFromRoundAllMatchesStartDate()
|
|
// flattenedMatches = matches + flattenedMatches
|
|
// }
|
|
//
|
|
// } else if let matchId {
|
|
// if let match : Match = Store.main.findById(matchId) {
|
|
// if let round = match.roundObject {
|
|
// round.resetFromRoundAllMatchesStartDate(from: match)
|
|
// }
|
|
// flattenedMatches = [match] + flattenedMatches
|
|
// }
|
|
// }
|
|
|
|
if let roundId, let matchId {
|
|
//todo
|
|
if let index = flattenedMatches.firstIndex(where: { $0.round == roundId && $0.id == matchId }) {
|
|
flattenedMatches[index...].forEach {
|
|
$0.startDate = nil
|
|
$0.removeCourt()
|
|
}
|
|
}
|
|
} else if let roundId {
|
|
//todo
|
|
if let index = flattenedMatches.firstIndex(where: { $0.round == roundId }) {
|
|
flattenedMatches[index...].forEach {
|
|
$0.startDate = nil
|
|
$0.removeCourt()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
let usedCourts = getAvailableCourts(from: allMatches.filter({ $0.startDate?.isEarlierThan(startDate) == true && $0.startDate?.dayInt == startDate.dayInt }))
|
|
let initialCourts = usedCourts.filter { (court, availableDate) in
|
|
availableDate <= startDate
|
|
}.sorted(by: \.1).compactMap { $0.0 }
|
|
|
|
let courts : [Int]? = initialCourts.isEmpty ? nil : initialCourts
|
|
|
|
print("initial available courts at beginning: \(courts ?? [])")
|
|
|
|
let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts)
|
|
|
|
roundDispatch.timedMatches.forEach { matchSchedule in
|
|
if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) {
|
|
match.startDate = matchSchedule.startDate
|
|
match.setCourt(matchSchedule.courtIndex)
|
|
}
|
|
}
|
|
|
|
try? DataStore.shared.matches.addOrUpdate(contentOfs: allMatches)
|
|
}
|
|
|
|
|
|
func courtsUnavailable(startDate: Date, duration: Int, courtsUnavailability: [DateInterval]?) -> Int {
|
|
let endDate = startDate.addingTimeInterval(Double(duration) * 60)
|
|
guard let courtsUnavailability else { return 0 }
|
|
let groupedBy = Dictionary(grouping: courtsUnavailability, by: { $0.courtIndex })
|
|
let courts = groupedBy.keys
|
|
return courts.filter {
|
|
courtUnavailable(courtIndex: $0, from: startDate, to: endDate, source: courtsUnavailability)
|
|
}.count
|
|
}
|
|
|
|
func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date, source: [DateInterval]) -> Bool {
|
|
let courtLockedSchedule = source.filter({ $0.courtIndex == courtIndex })
|
|
return courtLockedSchedule.anySatisfy({ dateInterval in
|
|
dateInterval.isDateInside(startDate) || dateInterval.isDateInside(endDate)
|
|
})
|
|
}
|
|
|
|
func updateSchedule(tournament: Tournament) {
|
|
let lastDate = updateGroupStageSchedule(tournament: tournament)
|
|
updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate)
|
|
}
|
|
}
|
|
|
|
struct GroupStageTimeMatch {
|
|
let matchID: String
|
|
let rotationIndex: Int
|
|
var courtIndex: Int
|
|
let groupIndex: Int
|
|
}
|
|
|
|
struct TimeMatch {
|
|
let matchID: String
|
|
let rotationIndex: Int
|
|
var courtIndex: Int
|
|
var startDate: Date
|
|
var durationLeft: Int //in minutes
|
|
var minimumBreakTime: Int //in minutes
|
|
|
|
func estimatedEndDate(includeBreakTime: Bool) -> Date {
|
|
let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0))
|
|
return startDate.addingTimeInterval(minutesToAdd * 60.0)
|
|
}
|
|
}
|
|
|
|
struct GroupStageMatchDispatcher {
|
|
let timedMatches: [GroupStageTimeMatch]
|
|
let freeCourtPerRotation: [Int: [Int]]
|
|
let rotationCount: Int
|
|
let groupLastRotation: [Int: Int]
|
|
}
|
|
|
|
struct MatchDispatcher {
|
|
let timedMatches: [TimeMatch]
|
|
let freeCourtPerRotation: [Int: [Int]]
|
|
let rotationCount: Int
|
|
}
|
|
|
|
extension Match {
|
|
func teamIds() -> [String] {
|
|
return teams().map { $0.id }
|
|
}
|
|
|
|
func containsTeamId(_ id: String) -> Bool {
|
|
teamIds().contains(id)
|
|
}
|
|
}
|
|
|