From a865efc696c51b3fed2826e688975a15b8a8723d Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 13 May 2025 15:05:52 +0200 Subject: [PATCH] add accountGroupStageBreakTime groupStageRotationDifference in matchscheduler and uniqueRandomIndex in team reg --- .../Data/Gen/BaseMatchScheduler.swift | 16 +++- PadelClubData/Data/Gen/BaseRound.swift | 23 +++-- .../Data/Gen/BaseTeamRegistration.swift | 9 +- PadelClubData/Data/Gen/MatchScheduler.json | 10 +++ PadelClubData/Data/Gen/TeamRegistration.json | 6 ++ PadelClubData/Data/MatchScheduler.swift | 72 +++++++++++++-- PadelClubData/Data/Tournament.swift | 89 ++++++++++++++++++- 7 files changed, 200 insertions(+), 25 deletions(-) diff --git a/PadelClubData/Data/Gen/BaseMatchScheduler.swift b/PadelClubData/Data/Gen/BaseMatchScheduler.swift index 8e86c9d..a4ef27a 100644 --- a/PadelClubData/Data/Gen/BaseMatchScheduler.swift +++ b/PadelClubData/Data/Gen/BaseMatchScheduler.swift @@ -28,6 +28,8 @@ public class BaseMatchScheduler: BaseModelObject, Storable { public var shouldTryToFillUpCourtsAvailable: Bool = false public var courtsAvailable: Set = Set() public var simultaneousStart: Bool = true + public var groupStageRotationDifference: Int = 0 + public var accountGroupStageBreakTime: Bool = false public init( id: String = Store.randomId(), @@ -45,7 +47,9 @@ public class BaseMatchScheduler: BaseModelObject, Storable { overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false, courtsAvailable: Set = Set(), - simultaneousStart: Bool = true + simultaneousStart: Bool = true, + groupStageRotationDifference: Int = 0, + accountGroupStageBreakTime: Bool = false ) { super.init() self.id = id @@ -64,6 +68,8 @@ public class BaseMatchScheduler: BaseModelObject, Storable { self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable self.courtsAvailable = courtsAvailable self.simultaneousStart = simultaneousStart + self.groupStageRotationDifference = groupStageRotationDifference + self.accountGroupStageBreakTime = accountGroupStageBreakTime } required public override init() { super.init() @@ -86,6 +92,8 @@ public class BaseMatchScheduler: BaseModelObject, Storable { case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable" case _courtsAvailable = "courtsAvailable" case _simultaneousStart = "simultaneousStart" + case _groupStageRotationDifference = "groupStageRotationDifference" + case _accountGroupStageBreakTime = "accountGroupStageBreakTime" } required init(from decoder: Decoder) throws { @@ -106,6 +114,8 @@ public class BaseMatchScheduler: BaseModelObject, Storable { self.shouldTryToFillUpCourtsAvailable = try container.decodeIfPresent(Bool.self, forKey: ._shouldTryToFillUpCourtsAvailable) ?? false self.courtsAvailable = try container.decodeIfPresent(Set.self, forKey: ._courtsAvailable) ?? Set() self.simultaneousStart = try container.decodeIfPresent(Bool.self, forKey: ._simultaneousStart) ?? true + self.groupStageRotationDifference = try container.decodeIfPresent(Int.self, forKey: ._groupStageRotationDifference) ?? 0 + self.accountGroupStageBreakTime = try container.decodeIfPresent(Bool.self, forKey: ._accountGroupStageBreakTime) ?? false try super.init(from: decoder) } @@ -127,6 +137,8 @@ public class BaseMatchScheduler: BaseModelObject, Storable { try container.encode(self.shouldTryToFillUpCourtsAvailable, forKey: ._shouldTryToFillUpCourtsAvailable) try container.encode(self.courtsAvailable, forKey: ._courtsAvailable) try container.encode(self.simultaneousStart, forKey: ._simultaneousStart) + try container.encode(self.groupStageRotationDifference, forKey: ._groupStageRotationDifference) + try container.encode(self.accountGroupStageBreakTime, forKey: ._accountGroupStageBreakTime) try super.encode(to: encoder) } @@ -152,6 +164,8 @@ public class BaseMatchScheduler: BaseModelObject, Storable { self.shouldTryToFillUpCourtsAvailable = matchscheduler.shouldTryToFillUpCourtsAvailable self.courtsAvailable = matchscheduler.courtsAvailable self.simultaneousStart = matchscheduler.simultaneousStart + self.groupStageRotationDifference = matchscheduler.groupStageRotationDifference + self.accountGroupStageBreakTime = matchscheduler.accountGroupStageBreakTime } public static func relationships() -> [Relationship] { diff --git a/PadelClubData/Data/Gen/BaseRound.swift b/PadelClubData/Data/Gen/BaseRound.swift index 398283e..0c055f2 100644 --- a/PadelClubData/Data/Gen/BaseRound.swift +++ b/PadelClubData/Data/Gen/BaseRound.swift @@ -7,11 +7,11 @@ import SwiftUI @Observable public class BaseRound: SyncedModelObject, SyncedStorable { - + public static func resourceName() -> String { return "rounds" } public static func tokenExemptedMethods() -> [HTTPMethod] { return [] } public static var copyServerResponse: Bool = false - + public var id: String = Store.randomId() public var tournament: String = "" public var index: Int = 0 @@ -19,13 +19,13 @@ public class BaseRound: SyncedModelObject, SyncedStorable { public var format: MatchFormat? = nil public var startDate: Date? = nil { didSet { - self.didSetStartDate() + self.didSetStartDate() } } public var groupStageLoserBracket: Bool = false public var loserBracketMode: LoserBracketMode = .automatic public var plannedStartDate: Date? = nil - + public init( id: String = Store.randomId(), tournament: String = "", @@ -51,9 +51,9 @@ public class BaseRound: SyncedModelObject, SyncedStorable { required public override init() { super.init() } - + public func didSetStartDate() {} - + public enum CodingKeys: String, CodingKey { case _id = "id" case _tournament = "tournament" @@ -65,7 +65,7 @@ public class BaseRound: SyncedModelObject, SyncedStorable { case _loserBracketMode = "loserBracketMode" case _plannedStartDate = "plannedStartDate" } - + required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() @@ -79,7 +79,7 @@ public class BaseRound: SyncedModelObject, SyncedStorable { self.plannedStartDate = try container.decodeIfPresent(Date.self, forKey: ._plannedStartDate) ?? nil try super.init(from: decoder) } - + public override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.id, forKey: ._id) @@ -93,11 +93,11 @@ public class BaseRound: SyncedModelObject, SyncedStorable { try container.encode(self.plannedStartDate, forKey: ._plannedStartDate) try super.encode(to: encoder) } - + func tournamentValue() -> Tournament? { return Store.main.findById(tournament) } - + public func copy(from other: any Storable) { guard let round = other as? BaseRound else { return } self.id = round.id @@ -110,11 +110,10 @@ public class BaseRound: SyncedModelObject, SyncedStorable { self.loserBracketMode = round.loserBracketMode self.plannedStartDate = round.plannedStartDate } - + public static func relationships() -> [Relationship] { return [ Relationship(type: Tournament.self, keyPath: \BaseRound.tournament), ] } - } diff --git a/PadelClubData/Data/Gen/BaseTeamRegistration.swift b/PadelClubData/Data/Gen/BaseTeamRegistration.swift index b98b267..abdd3a7 100644 --- a/PadelClubData/Data/Gen/BaseTeamRegistration.swift +++ b/PadelClubData/Data/Gen/BaseTeamRegistration.swift @@ -33,6 +33,7 @@ public class BaseTeamRegistration: SyncedModelObject, SyncedStorable { public var qualified: Bool = false public var finalRanking: Int? = nil public var pointsEarned: Int? = nil + public var uniqueRandomIndex: Int = 0 public init( id: String = Store.randomId(), @@ -55,7 +56,8 @@ public class BaseTeamRegistration: SyncedModelObject, SyncedStorable { confirmationDate: Date? = nil, qualified: Bool = false, finalRanking: Int? = nil, - pointsEarned: Int? = nil + pointsEarned: Int? = nil, + uniqueRandomIndex: Int = 0 ) { super.init() self.id = id @@ -79,6 +81,7 @@ public class BaseTeamRegistration: SyncedModelObject, SyncedStorable { self.qualified = qualified self.finalRanking = finalRanking self.pointsEarned = pointsEarned + self.uniqueRandomIndex = uniqueRandomIndex } required public override init() { super.init() @@ -106,6 +109,7 @@ public class BaseTeamRegistration: SyncedModelObject, SyncedStorable { case _qualified = "qualified" case _finalRanking = "finalRanking" case _pointsEarned = "pointsEarned" + case _uniqueRandomIndex = "uniqueRandomIndex" } required init(from decoder: Decoder) throws { @@ -131,6 +135,7 @@ public class BaseTeamRegistration: SyncedModelObject, SyncedStorable { self.qualified = try container.decodeIfPresent(Bool.self, forKey: ._qualified) ?? false self.finalRanking = try container.decodeIfPresent(Int.self, forKey: ._finalRanking) ?? nil self.pointsEarned = try container.decodeIfPresent(Int.self, forKey: ._pointsEarned) ?? nil + self.uniqueRandomIndex = try container.decodeIfPresent(Int.self, forKey: ._uniqueRandomIndex) ?? 0 try super.init(from: decoder) } @@ -157,6 +162,7 @@ public class BaseTeamRegistration: SyncedModelObject, SyncedStorable { try container.encode(self.qualified, forKey: ._qualified) try container.encode(self.finalRanking, forKey: ._finalRanking) try container.encode(self.pointsEarned, forKey: ._pointsEarned) + try container.encode(self.uniqueRandomIndex, forKey: ._uniqueRandomIndex) try super.encode(to: encoder) } @@ -188,6 +194,7 @@ public class BaseTeamRegistration: SyncedModelObject, SyncedStorable { self.qualified = teamregistration.qualified self.finalRanking = teamregistration.finalRanking self.pointsEarned = teamregistration.pointsEarned + self.uniqueRandomIndex = teamregistration.uniqueRandomIndex } public static func relationships() -> [Relationship] { diff --git a/PadelClubData/Data/Gen/MatchScheduler.json b/PadelClubData/Data/Gen/MatchScheduler.json index 132bb36..960b39e 100644 --- a/PadelClubData/Data/Gen/MatchScheduler.json +++ b/PadelClubData/Data/Gen/MatchScheduler.json @@ -84,6 +84,16 @@ "name": "simultaneousStart", "type": "Bool", "defaultValue": "true" + }, + { + "name": "groupStageRotationDifference", + "type": "Int", + "defaultValue": "0" + }, + { + "name": "accountGroupStageBreakTime", + "type": "Bool", + "defaultValue": "false" } ] } diff --git a/PadelClubData/Data/Gen/TeamRegistration.json b/PadelClubData/Data/Gen/TeamRegistration.json index 995d08e..bb9cebc 100644 --- a/PadelClubData/Data/Gen/TeamRegistration.json +++ b/PadelClubData/Data/Gen/TeamRegistration.json @@ -111,6 +111,12 @@ "name": "pointsEarned", "type": "Int", "optional": true + }, + { + "name": "uniqueRandomIndex", + "type": "Int", + "defaultValue": "0", + "optional": false } ] } diff --git a/PadelClubData/Data/MatchScheduler.swift b/PadelClubData/Data/MatchScheduler.swift index e2b2466..a29f2ee 100644 --- a/PadelClubData/Data/MatchScheduler.swift +++ b/PadelClubData/Data/MatchScheduler.swift @@ -105,8 +105,15 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable { 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 + var estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) + if accountGroupStageBreakTime { + estimatedDuration += match.matchFormat.breakTime.breakTime * 60 + } + + var timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60 + + timeIntervalToAdd += Double(matchSchedule.rotationIndex) * Double(self.groupStageRotationDifference) * Double(match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) * 60 + if let startDate = match.groupStageObject?.startDate { let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd) match.startDate = matchStartDate @@ -129,8 +136,16 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable { 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 + var estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) + + if accountGroupStageBreakTime { + estimatedDuration += match.matchFormat.breakTime.breakTime * 60 + } + + var timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60 + + timeIntervalToAdd += Double(matchSchedule.rotationIndex) * Double(self.groupStageRotationDifference) * Double(match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) * 60 + if let startDate = match.groupStageObject?.startDate { let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd) match.startDate = matchStartDate @@ -207,8 +222,16 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable { courtsAvailable.forEach { courtIndex in print("Checking availability for court \(courtIndex) in rotation \(rotationIndex)") if let first = rotationMatches.first(where: { match in - let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration) - let timeIntervalToAdd = Double(rotationIndex) * Double(estimatedDuration) * 60 + var estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration) + + if accountGroupStageBreakTime { + estimatedDuration += match.matchFormat.breakTime.breakTime * 60 + } + + var timeIntervalToAdd = (Double(rotationIndex)) * Double(estimatedDuration) * 60 + + timeIntervalToAdd += Double(rotationIndex) * Double(self.groupStageRotationDifference) * Double(match.matchFormat.getEstimatedDuration(additionalEstimationDuration)) * 60 + let rotationStartDate: Date = startingDate.addingTimeInterval(timeIntervalToAdd) let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), courtsUnavailability: courtsUnavailability) @@ -822,7 +845,31 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable { if tournament.groupStages(atStep: 1).isEmpty == false { lastDate = updateGroupStageSchedule(tournament: tournament, atStep: 1, startDate: lastDate) } - return updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate) + let allMatches = tournament.groupStagesMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false }) + + let groupMatchesByDay = tournament.groupMatchesByDay(matches: allMatches) + + let countedSet = tournament.matchCountPerDay(matchesByDay: groupMatchesByDay) + + var bracketStartDate = lastDate + print("lastDate", lastDate) + var errorFormat = false + let dates = countedSet.keys + if let first = dates.first, let matchesInLastDate = countedSet[first] { + let totalMatches = matchesInLastDate.totalCount() + let count = matchesInLastDate.count(for: tournament.groupStageMatchFormat) + let totalForThisFormat = tournament.groupStageMatchFormat.maximumMatchPerDay(for: totalMatches) + print(totalMatches, count, totalForThisFormat) + errorFormat = count >= totalForThisFormat + print("bracketStartDate", bracketStartDate) + } + + + if tournament.dayDuration > 1 && (lastDate.timeOfDay == .evening || errorFormat) { + bracketStartDate = lastDate.tomorrowAtNine + } + + return updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: bracketStartDate) } } @@ -886,3 +933,14 @@ extension Match { return groupStageObject._matchUp(for: index).map { groupStageObject.id + "_\($0)" } } } + +// Extension to compute the total count in an NSCountedSet +extension NSCountedSet { + func totalCount() -> Int { + var total = 0 + for element in self { + total += self.count(for: element) + } + return total + } +} diff --git a/PadelClubData/Data/Tournament.swift b/PadelClubData/Data/Tournament.swift index 967c99b..9bb4fed 100644 --- a/PadelClubData/Data/Tournament.swift +++ b/PadelClubData/Data/Tournament.swift @@ -636,7 +636,7 @@ defer { let defaultSorting : [MySortDescriptor] = _defaultSorting() - let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false }.prefix(teamCount).sorted(using: [.keyPath(\.initialWeight), .keyPath(\.id)], order: .ascending) + let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false }.prefix(teamCount).sorted(using: [.keyPath(\.initialWeight), .keyPath(\.uniqueRandomIndex), .keyPath(\.id)], order: .ascending) let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending) @@ -1777,9 +1777,9 @@ defer { private func _defaultSorting() -> [MySortDescriptor] { switch teamSorting { case .rank: - [.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)] + [.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.uniqueRandomIndex), .keyPath(\TeamRegistration.id)] case .inscriptionDate: - [.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)] + [.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.uniqueRandomIndex), .keyPath(\TeamRegistration.id)] } } @@ -1789,7 +1789,7 @@ defer { && federalTournamentAge == build.age } - private let _currentSelectionSorting : [MySortDescriptor] = [.keyPath(\.weight), .keyPath(\.id)] + private let _currentSelectionSorting : [MySortDescriptor] = [.keyPath(\.weight), .keyPath(\.uniqueRandomIndex), .keyPath(\.id)] private func _matchSchedulers() -> [MatchScheduler] { guard let tournamentStore = self.tournamentStore else { return [] } @@ -2227,6 +2227,87 @@ defer { return Array(leftInterval...maxSize) } + public func groupMatchesByDay(matches: [Match]) -> [Date: [Match]] { + var matchesByDay = [Date: [Match]]() + let calendar = Calendar.current + + for match in matches { + // Extract day/month/year and create a date with only these components + let components = calendar.dateComponents([.year, .month, .day], from: match.computedStartDateForSorting) + let strippedDate = calendar.date(from: components)! + + // Group matches by the strippedDate (only day/month/year) + if matchesByDay[strippedDate] == nil { + matchesByDay[strippedDate] = [] + } + + let shouldIncludeMatch: Bool + switch match.matchType { + case .groupStage: + shouldIncludeMatch = !matchesByDay[strippedDate]!.filter { $0.groupStage != nil }.compactMap { $0.groupStage }.contains(match.groupStage!) + case .bracket: + shouldIncludeMatch = !matchesByDay[strippedDate]!.filter { $0.round != nil }.compactMap { $0.round }.contains(match.round!) + case .loserBracket: + shouldIncludeMatch = true + } + + if shouldIncludeMatch { + matchesByDay[strippedDate]!.append(match) + } + } + + return matchesByDay + } + + public func matchCountPerDay(matchesByDay: [Date: [Match]]) -> [Date: NSCountedSet] { + let days = matchesByDay.keys + var matchCountPerDay = [Date: NSCountedSet]() + + for day in days { + if let matches = matchesByDay[day] { + var groupStageCount = 0 + let countedSet = NSCountedSet() + + for match in matches { + switch match.matchType { + case .groupStage: + if let groupStage = match.groupStageObject { + if groupStageCount < groupStage.size - 1 { + groupStageCount = groupStage.size - 1 + } + } + case .bracket: + countedSet.add(match.matchFormat) + case .loserBracket: + break + } + } + + if groupStageCount > 0 { + for _ in 0..