wip scheduler re-write

multistore
Razmig Sarkissian 2 years ago
parent 21e1417bf4
commit 58afa51005
  1. 7
      PadelClub/Data/Match.swift
  2. 120
      PadelClub/ViewModel/MatchScheduler.swift
  3. 25
      PadelClub/Views/Planning/PlanningSettingsView.swift
  4. 3
      PadelClub/Views/Planning/PlanningView.swift
  5. 7
      PadelClub/Views/Planning/RoundScheduleEditorView.swift
  6. 20
      PadelClub/Views/Planning/SchedulerView.swift
  7. 9
      PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift

@ -151,6 +151,13 @@ class Match: ModelObject, Storable {
func next() -> Match? {
Store.main.filter(isIncluded: { $0.round == round && $0.index == index + 1 }).first
}
func roundTitle() -> String? {
if groupStage != nil { return "Poule" }
else if let roundObject { return roundObject.roundTitle() }
else { return nil }
}
func topPreviousRoundMatchIndex() -> Int {
index * 2 + 1
}

@ -7,11 +7,34 @@
import Foundation
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
let groupIndex: Int
var startDate: Date
var durationLeft: Int //in minutes
var minimumBreakTime: Int //in minutes
var courtLocked: Bool = false
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 {
@ -34,7 +57,7 @@ extension Match {
class MatchScheduler {
static let shared = MatchScheduler()
func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?, randomizeCourts: Bool) -> MatchDispatcher {
func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?, randomizeCourts: Bool) -> GroupStageMatchDispatcher {
let _groupStages = groupStages.filter { startingDate == nil || $0.startDate == startingDate }
@ -50,7 +73,7 @@ class MatchScheduler {
}
}
var slots = [TimeMatch]()
var slots = [GroupStageTimeMatch]()
var availableMatchs = flattenedMatches
var rotationIndex = 0
var teamsPerRotation = [Int: [String]]()
@ -81,7 +104,8 @@ class MatchScheduler {
if let first = rotationMatches.first(where: { match in
teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true
}) {
slots.append(TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.groupStageObject!.index ))
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 })
@ -96,7 +120,7 @@ class MatchScheduler {
rotationIndex += 1
}
var organizedSlots = [TimeMatch]()
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
@ -109,10 +133,10 @@ class MatchScheduler {
}
return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation)
return GroupStageMatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation)
}
func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int) -> Bool {
func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date) -> Bool {
print(roundObject.roundTitle(), match.matchTitle())
let previousMatches = roundObject.precedentMatches(ofMatch: match)
if previousMatches.isEmpty { return true }
@ -135,34 +159,91 @@ class MatchScheduler {
return false
}
let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex + (roundObject.loser == nil ? 1 : 0) < rotationIndex })
return previousMatchIsInPreviousRotation
// if roundObject.isLoserBracket() {
// let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex < rotationIndex })
// return previousMatchIsInPreviousRotation
// }
let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex < rotationIndex })
guard let minimumPossibleEndDate = previousMatchSlots.map({ $0.estimatedEndDate(includeBreakTime: true) }).max() else {
return previousMatchIsInPreviousRotation
}
return targetedStartDate >= minimumPossibleEndDate
}
func getAvailableCourt(inSlots slots: [TimeMatch], nextStartDate: Date) -> [TimeMatch] {
guard let minimumDuration = slots.compactMap({ $0.durationLeft }).min() else { return [] }
var newSlots = [TimeMatch]()
slots.forEach { timeMatch in
let durationLeft = timeMatch.durationLeft
if durationLeft - minimumDuration > 0 {
let timeMatch = TimeMatch(matchID: timeMatch.matchID, rotationIndex: timeMatch.rotationIndex + 1, courtIndex: timeMatch.courtIndex, groupIndex: timeMatch.groupIndex, startDate: nextStartDate, durationLeft: durationLeft, minimumBreakTime: timeMatch.minimumBreakTime, courtLocked: true)
newSlots.append(timeMatch)
}
}
return newSlots
}
func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? {
slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min()
}
func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, initialOccupiedCourt: Int = 0) -> MatchDispatcher {
func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, initialOccupiedCourt: Int = 0, dispatcherStartDate: Date) -> MatchDispatcher {
var slots = [TimeMatch]()
var availableMatchs = flattenedMatches
var rotationIndex = 0
var freeCourtPerRotation = [Int: [Int]]()
var groupLastRotation = [Int: Int]()
while slots.count < flattenedMatches.count {
var courts = [Int]()
var timeToAdd = 0.0
while availableMatchs.count > 0 {
freeCourtPerRotation[rotationIndex] = []
var matchPerRound = [Int: Int]()
var availableCourt = numberOfCourtsAvailablePerRotation
if rotationIndex == 0 {
availableCourt = availableCourt - initialOccupiedCourt
}
(0..<availableCourt).forEach { courtIndex in
courts = (0..<availableCourt).map { $0 }
let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 })
var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate
let duplicatedSlots = getAvailableCourt(inSlots: previousRotationSlots, nextStartDate: rotationStartDate)
print("duplicatedSlots", duplicatedSlots)
slots.append(contentsOf: duplicatedSlots)
courts.removeAll(where: { index in
duplicatedSlots.anySatisfy { $0.courtIndex == index }
})
courts.sort()
print("courts available at rotation \(rotationIndex)", courts)
print("rotationStartDate", rotationStartDate)
let freeCourtPreviousRotation = rotationIndex > 0 ? freeCourtPerRotation[rotationIndex - 1]!.count : 0
if previousRotationSlots.isEmpty && rotationIndex > 0 {
let previousPreviousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 2 })
rotationStartDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate
} else if freeCourtPreviousRotation > 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 })
if let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: true) {
rotationStartDate = previousEndDate
courts = freeCourtPerRotation[rotationIndex - 1]!
}
}
courts.forEach { courtIndex in
//print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) })
if let first = availableMatchs.first(where: { match in
let roundObject = match.roundObject!
let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex)
let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate)
if roundObject.loser == nil && roundObject.index > 0, match.indexInRound() == 0, numberOfCourtsAvailablePerRotation > 1, let nextMatch = match.next() {
if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex) {
if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate) {
return true
} else {
return false
@ -184,11 +265,13 @@ class MatchScheduler {
matchPerRound[first.roundObject!.index] = 1
}
}
slots.append(TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.roundObject!.index ))
let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.roundObject!.index, startDate: rotationStartDate, durationLeft: first.matchFormat.estimatedDuration, minimumBreakTime: first.matchFormat.breakTime.breakTime)
slots.append(timeMatch)
availableMatchs.removeAll(where: { $0.id == first.id })
if let index = first.roundObject?.index {
groupLastRotation[index] = rotationIndex
}
timeToAdd = 0.0
} else {
freeCourtPerRotation[rotationIndex]!.append(courtIndex)
}
@ -199,7 +282,7 @@ class MatchScheduler {
var organizedSlots = [TimeMatch]()
for i in 0..<rotationIndex {
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted()
let courtsSorted = slots.filter({ $0.rotationIndex == i && $0.courtLocked == false }).map { $0.courtIndex }.sorted()
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex))
@ -235,12 +318,11 @@ class MatchScheduler {
flattenedMatches.forEach({ $0.startDate = nil })
let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, randomizeCourts: randomizeCourts, initialOccupiedCourt: 0)
let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, randomizeCourts: randomizeCourts, initialOccupiedCourt: 0, dispatcherStartDate: startDate)
roundDispatch.timedMatches.forEach { matchSchedule in
if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) {
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60
match.startDate = startDate.addingTimeInterval(timeIntervalToAdd)
match.startDate = matchSchedule.startDate
match.setCourt(matchSchedule.courtIndex + 1)
}
}

@ -87,31 +87,8 @@ struct PlanningSettingsView: View {
}
}
try? dataStore.matches.addOrUpdate(contentOfs: matches)
let upperRounds = tournament.rounds()
let rounds = upperRounds.flatMap {
[$0] + $0.loserRoundsAndChildren()
}
let flattenedMatches = rounds.flatMap { round in
round._matches().filter({ $0.disabled == false }).sorted(by: \.index)
}
flattenedMatches.forEach({ $0.startDate = nil })
let roundDispatch = matchScheduler.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, randomizeCourts: randomCourtDistribution)
roundDispatch.timedMatches.forEach { matchSchedule in
if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) {
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60
if let lastDate {
match.startDate = lastDate.addingTimeInterval(timeIntervalToAdd)
}
match.setCourt(matchSchedule.courtIndex + 1)
}
}
try? dataStore.matches.addOrUpdate(contentOfs: flattenedMatches)
matchScheduler.updateSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, randomizeCourts: randomCourtDistribution, startDate: lastDate ?? tournament.startDate)
scheduleSetup = true
}

@ -74,7 +74,7 @@ struct PlanningView: View {
ForEach(_matches) { match in
LabeledContent {
Text(match.matchFormat.format)
} label: {
if let groupStage = match.groupStageObject {
Text(groupStage.groupStageTitle())
@ -140,6 +140,7 @@ struct PlanningView: View {
Text(matches.count.formatted() + " match" + matches.count.pluralSuffix)
} label: {
Text(key.formatted(date: .omitted, time: .shortened)).font(.largeTitle)
Text(Set(matches.compactMap { $0.roundTitle() }).joined(separator: ", "))
}
}
}

@ -40,7 +40,14 @@ struct RoundScheduleEditorView: View {
}
}
.onChange(of: round.matchFormat) {
let matches = round._matches()
matches.forEach { match in
match.matchFormat = round.matchFormat
}
try? dataStore.matches.addOrUpdate(contentOfs: matches)
_save()
_updateSchedule()
}
}

@ -7,8 +7,16 @@
import SwiftUI
extension GroupStage: Schedulable {}
extension Round: Schedulable {}
extension GroupStage: Schedulable {
func titleLabel() -> String {
self.groupStageTitle()
}
}
extension Round: Schedulable {
func titleLabel() -> String {
self.roundTitle()
}
}
struct SchedulerView: View {
var tournament: Tournament
@ -21,7 +29,7 @@ struct SchedulerView: View {
ForEach(tournament.rounds()) { round in
_schedulerView(round)
ForEach(round.loserRoundsAndChildren()) { loserRound in
if round.isDisabled() == false {
if loserRound.isDisabled() == false {
_schedulerView(loserRound)
}
}
@ -42,12 +50,12 @@ struct SchedulerView: View {
GroupStageScheduleEditorView(groupStage: groupStage)
}
}
.navigationTitle(schedulable.selectionLabel())
.navigationTitle(schedulable.titleLabel())
} label: {
LabeledContent {
Text(schedulable.matchFormat.format).font(.largeTitle)
} label: {
if let startDate = schedulable.startDate {
if let startDate = schedulable.getStartDate() {
Text(startDate.formatted(.dateTime.hour().minute())).font(.largeTitle)
Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year()))
} else {
@ -56,7 +64,7 @@ struct SchedulerView: View {
}
}
} header: {
Text(schedulable.selectionLabel())
Text(schedulable.titleLabel())
}
}
}

@ -7,12 +7,19 @@
import SwiftUI
protocol Schedulable: Selectable, Identifiable {
protocol Schedulable: Identifiable {
var startDate: Date? { get set }
var matchFormat: MatchFormat { get set }
func playedMatches() -> [Match]
func titleLabel() -> String
}
extension Schedulable {
func getStartDate() -> Date? {
startDate ?? playedMatches().first?.startDate
}
}
enum ScheduleDestination: Identifiable, Selectable {
var id: String {
switch self {

Loading…
Cancel
Save