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

614 lines
28 KiB

//
// MatchScheduler.swift
// PadelClub
//
// Created by Razmig Sarkissian on 08/04/2024.
//
import Foundation
import LeStorage
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)
}
}
enum MatchSchedulerOption: Hashable {
case accountUpperBracketBreakTime
case accountLoserBracketBreakTime
case randomizeCourts
case rotationDifferenceIsImportant
case shouldHandleUpperRoundSlice
case shouldEndRoundBeforeStartingNext
}
class MatchScheduler {
static let shared = MatchScheduler()
var additionalEstimationDuration : Int = 0
var options: Set<MatchSchedulerOption> = Set(arrayLiteral: .accountUpperBracketBreakTime)
var timeDifferenceLimit: Double = 300.0
var loserBracketRotationDifference: Int = 0
var upperBracketRotationDifference: Int = 1
var courtsUnavailability: [DateInterval]? = nil
func shouldEndRoundBeforeStartingNext() -> Bool {
options.contains(.shouldEndRoundBeforeStartingNext)
}
func shouldHandleUpperRoundSlice() -> Bool {
options.contains(.shouldHandleUpperRoundSlice)
}
func accountLoserBracketBreakTime() -> Bool {
options.contains(.accountLoserBracketBreakTime)
}
func accountUpperBracketBreakTime() -> Bool {
options.contains(.accountUpperBracketBreakTime)
}
func randomizeCourts() -> Bool {
options.contains(.randomizeCourts)
}
func rotationDifferenceIsImportant() -> Bool {
options.contains(.rotationDifferenceIsImportant)
}
@discardableResult
func updateGroupStageSchedule(tournament: Tournament) -> Date {
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1
let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
courtsUnavailability = tournament.eventObject()?.courtsUnavailability
let matches = groupStages.flatMap({ $0._matches() })
matches.forEach({
$0.removeCourt()
$0.startDate = nil
})
var lastDate : Date = tournament.startDate
groupStages.chunked(into: groupStageCourtCount).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]()
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
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 })
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)
var difference = differenceWithBreak
if differenceWithBreak <= 0 {
difference = differenceWithoutBreak
} else if differenceWithBreak > timeDifferenceLimit && differenceWithoutBreak > timeDifferenceLimit {
difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak)
}
if difference > timeDifferenceLimit {
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)
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]]) {
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))
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)
}
}
func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) {
courtsUnavailability = tournament.eventObject()?.courtsUnavailability
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) -> 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)
}.count
}
func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool {
guard let courtLockedSchedule = courtsUnavailability?.filter({ $0.courtIndex == courtIndex }) else { return true }
return courtLockedSchedule.anySatisfy({ dateInterval in
dateInterval.isDateInside(startDate) || dateInterval.isDateInside(endDate)
})
}
func updateSchedule(tournament: Tournament) {
courtsUnavailability = tournament.eventObject()?.courtsUnavailability
let lastDate = updateGroupStageSchedule(tournament: tournament)
updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate)
}
}