From 06ef1b07a1882c3e34c51b9bbe77d33aeaca967d Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Wed, 18 Mar 2026 18:14:11 -0500 Subject: [PATCH 01/19] add some TODOs regarding determinism; support random number generators when calling random-related functions --- .../LeagueGenerationData.swift | 2 +- .../league-scheduling/data/AssignSlots.swift | 12 +- .../data/BalanceHomeAway.swift | 14 +- .../data/DivisionMatchupCombinations.swift | 182 +++++++++--------- .../league-scheduling/data/Generation.swift | 42 ++-- .../data/LeagueScheduleData.swift | 13 +- .../data/LeagueScheduleDataSnapshot.swift | 8 +- .../league-scheduling/data/MatchupBlock.swift | 45 ++--- .../league-scheduling/data/Redistribute.swift | 3 +- .../data/RedistributionData.swift | 11 +- .../data/SelectMatchup.swift | 20 +- Sources/league-scheduling/data/Shuffle.swift | 4 +- .../data/selectSlot/SelectSlotNormal.swift | 2 +- Sources/league-scheduling/globals.swift | 24 +++ Sources/league-scheduling/util/LCG.swift | 23 +++ .../DivisionMatchupCombinationTests.swift | 10 +- .../MatchupBlockTests.swift | 20 +- 17 files changed, 249 insertions(+), 186 deletions(-) create mode 100644 Sources/league-scheduling/globals.swift create mode 100644 Sources/league-scheduling/util/LCG.swift diff --git a/Sources/league-scheduling/LeagueGenerationData.swift b/Sources/league-scheduling/LeagueGenerationData.swift index 3637044..f2ed26c 100644 --- a/Sources/league-scheduling/LeagueGenerationData.swift +++ b/Sources/league-scheduling/LeagueGenerationData.swift @@ -22,7 +22,7 @@ extension LeagueGenerationData: Codable { func scheduleSorted() -> ContiguousArray<[Matchup]> { var array:ContiguousArray<[Matchup]> = .init(repeating: [], count: schedule.count) - for (dayIndex, matchups) in schedule.enumerated() { + for (dayIndex, matchups) in schedule.enumerated() { // TODO: support determinism array[unchecked: dayIndex] = matchups.sorted(by: { guard $0.time == $1.time else { return $0.time < $1.time } return $0.location < $1.location diff --git a/Sources/league-scheduling/data/AssignSlots.swift b/Sources/league-scheduling/data/AssignSlots.swift index fa21b7b..6888b69 100644 --- a/Sources/league-scheduling/data/AssignSlots.swift +++ b/Sources/league-scheduling/data/AssignSlots.swift @@ -78,7 +78,7 @@ extension LeagueScheduleData { }*/ guard let originalPair = selectMatchup(prioritizedMatchups: prioritizedMatchups) else { return false } var matchup = originalPair - matchup.balanceHomeAway(assignmentState: assignmentState) + matchup.balanceHomeAway(rng: &rng, assignmentState: assignmentState) // successfully selected a matchup guard let _ = assignMatchupPair( matchup, @@ -246,6 +246,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, allAvailableMatchups: Set, + rng: inout some RandomNumberGenerator, assignmentState: inout AssignmentState, shouldSkipSelection: (MatchupPair) -> Bool, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, @@ -258,7 +259,7 @@ extension LeagueScheduleData { availableMatchups: assignmentState.availableMatchups ) while pair == nil { - guard let selected = assignmentState.selectMatchup(prioritizedMatchups: prioritizedMatchups) else { return nil } + guard let selected = assignmentState.selectMatchup(prioritizedMatchups: prioritizedMatchups, rng: &rng) else { return nil } if !shouldSkipSelection(selected) { pair = selected prioritizedMatchups.update(prioritizedEntries: assignmentState.prioritizedEntries, availableMatchups: assignmentState.availableMatchups) @@ -268,7 +269,7 @@ extension LeagueScheduleData { } } guard var pair else { return nil } - pair.balanceHomeAway(assignmentState: assignmentState) + pair.balanceHomeAway(rng: &rng, assignmentState: assignmentState) #if LOG print("AssignSlots;selectAndAssignMatchup;pair=\(pair);remainingAllocations[team1]=\(assignmentState.remainingAllocations[unchecked: pair.team1].map({ $0.description }));remainingAllocations[team2]=\(assignmentState.remainingAllocations[unchecked: pair.team2].map({ $0.description }))") @@ -296,6 +297,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, allAvailableMatchups: Set, + rng: inout some RandomNumberGenerator, assignmentState: inout AssignmentState, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable @@ -307,11 +309,11 @@ extension LeagueScheduleData { availableMatchups: assignmentState.availableMatchups ) while pair == nil { - guard let selected = assignmentState.selectMatchup(prioritizedMatchups: prioritizedMatchups) else { return nil } + guard let selected = assignmentState.selectMatchup(prioritizedMatchups: prioritizedMatchups, rng: &rng) else { return nil } pair = selected } guard var pair else { return nil } - pair.balanceHomeAway(assignmentState: assignmentState) + pair.balanceHomeAway(rng: &rng, assignmentState: assignmentState) #if LOG print("AssignSlots;selectAndAssignMatchup;pair=\(pair);remainingAllocations[team1]=\(assignmentState.remainingAllocations[unchecked: pair.team1].map({ $0.description }));remainingAllocations[team2]=\(assignmentState.remainingAllocations[unchecked: pair.team2].map({ $0.description }))") diff --git a/Sources/league-scheduling/data/BalanceHomeAway.swift b/Sources/league-scheduling/data/BalanceHomeAway.swift index c264322..66e9ffc 100644 --- a/Sources/league-scheduling/data/BalanceHomeAway.swift +++ b/Sources/league-scheduling/data/BalanceHomeAway.swift @@ -3,6 +3,7 @@ extension MatchupPair { /// Balances home/away allocations, mutating `team1` (home) and `team2` (away) if necessary. mutating func balanceHomeAway( + rng: inout some RandomNumberGenerator, assignmentState: borrowing AssignmentState ) { let team1GamesPlayedAgainstTeam2 = assignmentState.assignedEntryHomeAways[unchecked: team1][unchecked: team2] @@ -10,7 +11,7 @@ extension MatchupPair { if team1GamesPlayedAgainstTeam2.home < team1GamesPlayedAgainstTeam2.away { // keep `team1` at home and `team2` at away } else if team1GamesPlayedAgainstTeam2.home == team1GamesPlayedAgainstTeam2.away { - if Self.shouldPlayAtHome(team1: team1, team2: team2, homeMatchups: assignmentState.homeMatchups, awayMatchups: assignmentState.awayMatchups) { + if Self.shouldPlayAtHome(team1: team1, team2: team2, homeMatchups: assignmentState.homeMatchups, awayMatchups: assignmentState.awayMatchups, rng: &rng) { // keep `team1` at home and `team2` at away return } @@ -28,7 +29,8 @@ extension MatchupPair { team1: Entry.IDValue, team2: Entry.IDValue, homeMatchups: [UInt8], - awayMatchups: [UInt8] + awayMatchups: [UInt8], + rng: inout some RandomNumberGenerator ) -> Bool { let home1 = homeMatchups[unchecked: team1] let home2 = homeMatchups[unchecked: team2] @@ -37,7 +39,7 @@ extension MatchupPair { let away1 = awayMatchups[unchecked: team1] let away2 = awayMatchups[unchecked: team2] guard away1 == away2 else { return away1 < away2 } - return Bool.random() + return Bool.random(using: &rng) } } @@ -83,20 +85,20 @@ extension LeagueScheduleData { } } } - while let entryID = unbalancedEntryIDs.randomElement() { + while let entryID = unbalancedEntryIDs.randomElement(using: &rng) { var flipped:FlippableMatchup? if neededFlipsToBalance[unchecked: entryID].home > 0 { flipped = flippable.filter({ $0.matchup.home == entryID && neededFlipsToBalance[unchecked: $0.matchup.home].home > 0 && neededFlipsToBalance[unchecked: $0.matchup.away].away > 0 - }).randomElement() + }).randomElement(using: &rng) } else { flipped = flippable.filter({ $0.matchup.away == entryID && neededFlipsToBalance[unchecked: $0.matchup.home].home > 0 && neededFlipsToBalance[unchecked: $0.matchup.away].away > 0 - }).randomElement() + }).randomElement(using: &rng) } if var flipped { flippable.remove(flipped) diff --git a/Sources/league-scheduling/data/DivisionMatchupCombinations.swift b/Sources/league-scheduling/data/DivisionMatchupCombinations.swift index 3d53744..87a9316 100644 --- a/Sources/league-scheduling/data/DivisionMatchupCombinations.swift +++ b/Sources/league-scheduling/data/DivisionMatchupCombinations.swift @@ -1,110 +1,106 @@ // MARK: All combinations -extension LeagueScheduleData { - /// - Returns: All division matchup combinations separated by division. - /// - Usage: [`Division.IDValue`: `division matchup combinations`] - static func allDivisionMatchupCombinations( - entriesPerMatchup: EntriesPerMatchup, - locations: LocationIndex, - entryCountsForDivision: ContiguousArray - ) -> ContiguousArray>> { - var combinations:ContiguousArray>> = .init(repeating: [], count: entryCountsForDivision.count) - for (divisionIndex, entryCount) in entryCountsForDivision.enumerated() { - if entryCount > 0 { - let matchupsCount = entryCount / entriesPerMatchup - let upperLimit:Int - if matchupsCount > locations { // more available matchups than locations - upperLimit = Int(locations) - } else { - upperLimit = matchupsCount - } - for i in 0...upperLimit { - let right = matchupsCount - i - if i != 1 && right != 1 && right <= upperLimit { - var combo = ContiguousArray() - combo.append(i) - combo.append(right) - combinations[divisionIndex].append(combo) - } - } +/// - Returns: All division matchup combinations separated by division. +/// - Usage: [`Division.IDValue`: `division matchup combinations`] +func calculateAllDivisionMatchupCombinations( + entriesPerMatchup: EntriesPerMatchup, + locations: LocationIndex, + entryCountsForDivision: ContiguousArray +) -> ContiguousArray>> { + var combinations:ContiguousArray>> = .init(repeating: [], count: entryCountsForDivision.count) + for (divisionIndex, entryCount) in entryCountsForDivision.enumerated() { + if entryCount > 0 { + let matchupsCount = entryCount / entriesPerMatchup + let upperLimit:Int + if matchupsCount > locations { // more available matchups than locations + upperLimit = Int(locations) } else { - combinations[divisionIndex] = [] + upperLimit = matchupsCount } + for i in 0...upperLimit { + let right = matchupsCount - i + if i != 1 && right != 1 && right <= upperLimit { + var combo = ContiguousArray() + combo.append(i) + combo.append(right) + combinations[divisionIndex].append(combo) + } + } + } else { + combinations[divisionIndex] = [] } - return combinations } + return combinations } // MARK: Allowed combinations -extension LeagueScheduleData { - /// - Returns: Allowed division matchup combinations - /// - Usage: [`allowed matchup combination index`: [`Division.IDValue`: `division matchup combination`]] - static func allowedDivisionMatchupCombinations( - entriesPerMatchup: EntriesPerMatchup, - locations: LocationIndex, - entryCountsForDivision: ContiguousArray - ) -> ContiguousArray>> { - let allCombinations = allDivisionMatchupCombinations( - entriesPerMatchup: entriesPerMatchup, - locations: locations, - entryCountsForDivision: entryCountsForDivision - ) - var combinations = ContiguousArray>>() - guard let initialResultsCount = allCombinations.first?.first?.count else { return combinations } - var combinationBuilder = ContiguousArray>() - combinationBuilder.reserveCapacity(entryCountsForDivision.count) +/// - Returns: Allowed division matchup combinations +/// - Usage: [`allowed matchup combination index`: [`Division.IDValue`: `division matchup combination`]] +func calculateAllowedDivisionMatchupCombinations( + entriesPerMatchup: EntriesPerMatchup, + locations: LocationIndex, + entryCountsForDivision: ContiguousArray +) -> ContiguousArray>> { + let allCombinations = calculateAllDivisionMatchupCombinations( + entriesPerMatchup: entriesPerMatchup, + locations: locations, + entryCountsForDivision: entryCountsForDivision + ) + var combinations = ContiguousArray>>() + guard let initialResultsCount = allCombinations.first?.first?.count else { return combinations } + var combinationBuilder = ContiguousArray>() + combinationBuilder.reserveCapacity(entryCountsForDivision.count) + yieldAllowedCombinations( + allCombinations: allCombinations, + division: 0, + locations: locations, + results: .init(repeating: 0, count: initialResultsCount), + combinationBuilder: combinationBuilder + ) { + combinations.append($0) + } + return combinations +} +private func yieldAllowedCombinations( + allCombinations: ContiguousArray>>, + division: Division.IDValue, + locations: LocationIndex, + results: ContiguousArray, + combinationBuilder: ContiguousArray>, + yield: (_ combination: ContiguousArray>) -> Void +) { + guard let targetCombinations = allCombinations[uncheckedPositive: division] else { + yield(combinationBuilder) + return + } + guard !targetCombinations.isEmpty else { yieldAllowedCombinations( allCombinations: allCombinations, - division: 0, + division: division + 1, locations: locations, - results: .init(repeating: 0, count: initialResultsCount), - combinationBuilder: combinationBuilder - ) { - combinations.append($0) - } - return combinations + results: results, + combinationBuilder: combinationBuilder, + yield: yield + ) + return } - private static func yieldAllowedCombinations( - allCombinations: ContiguousArray>>, - division: Division.IDValue, - locations: LocationIndex, - results: ContiguousArray, - combinationBuilder: ContiguousArray>, - yield: (_ combination: ContiguousArray>) -> Void - ) { - guard let targetCombinations = allCombinations[uncheckedPositive: division] else { - yield(combinationBuilder) - return - } - guard !targetCombinations.isEmpty else { - yieldAllowedCombinations( - allCombinations: allCombinations, - division: division + 1, - locations: locations, - results: results, - combinationBuilder: combinationBuilder, - yield: yield - ) - return - } - combinationLoop: - for combination in targetCombinations { - let combined = zip(results, combination).map { $0 + $1 } - for value in combined { - if value > locations { - continue combinationLoop - } + combinationLoop: + for combination in targetCombinations { + let combined = zip(results, combination).map { $0 + $1 } + for value in combined { + if value > locations { + continue combinationLoop } - var builder = combinationBuilder - builder.append(combination) - yieldAllowedCombinations( - allCombinations: allCombinations, - division: division + 1, - locations: locations, - results: ContiguousArray(combined), - combinationBuilder: builder, - yield: yield - ) } + var builder = combinationBuilder + builder.append(combination) + yieldAllowedCombinations( + allCombinations: allCombinations, + division: division + 1, + locations: locations, + results: ContiguousArray(combined), + combinationBuilder: builder, + yield: yield + ) } } \ No newline at end of file diff --git a/Sources/league-scheduling/data/Generation.swift b/Sources/league-scheduling/data/Generation.swift index 918f9f6..f257228 100644 --- a/Sources/league-scheduling/data/Generation.swift +++ b/Sources/league-scheduling/data/Generation.swift @@ -61,7 +61,10 @@ extension RequestPayload.Runtime { divisionEntries: divisionEntries, divisions: divisions ) + let rng = SystemRandomNumberGenerator() + //let rng = LCG(seed: 69) let dataSnapshot = LeagueScheduleDataSnapshot( + rng: rng, maxStartingTimes: maxStartingTimes, startingTimes: general.startingTimes, maxLocations: maxLocations, @@ -75,12 +78,25 @@ extension RequestPayload.Runtime { locationTravelDurations: general.locationTravelDurations ?? .init(repeating: .init(repeating: 0, count: maxLocations), count: maxLocations), maxSameOpponentMatchups: maxSameOpponentMatchups ) + return try await generateDivisionSchedulesInParallel( + divisionsCount: divisionsCount, + divisionEntries: divisionEntries, + maxStartingTimes: maxStartingTimes, + maxLocations: maxLocations, + dataSnapshot: dataSnapshot + ) + } + private func generateDivisionSchedulesInParallel( + divisionsCount: Int, + divisionEntries: ContiguousArray>, + maxStartingTimes: TimeIndex, + maxLocations: LocationIndex, + dataSnapshot: LeagueScheduleDataSnapshot + ) async throws -> [LeagueGenerationData] { var grouped = [DayOfWeek:Set]() for (divisionID, division) in divisions.enumerated() { grouped[DayOfWeek(division.dayOfWeek), default: []].formUnion(divisionEntries[divisionID]) } - let finalMaxStartingTimes = maxStartingTimes - let finalMaxLocations = maxLocations guard constraints.timeoutDelay > 0 else { return await withTaskGroup { group in for (dow, scheduledEntries) in grouped { @@ -90,8 +106,8 @@ extension RequestPayload.Runtime { settings: self, dataSnapshot: dataSnapshot, divisionsCount: divisionsCount, - maxStartingTimes: finalMaxStartingTimes, - maxLocations: finalMaxLocations, + maxStartingTimes: maxStartingTimes, + maxLocations: maxLocations, scheduledEntries: scheduledEntries ) } @@ -116,8 +132,8 @@ extension RequestPayload.Runtime { settings: self, dataSnapshot: dataSnapshot, divisionsCount: divisionsCount, - maxStartingTimes: finalMaxStartingTimes, - maxLocations: finalMaxLocations, + maxStartingTimes: maxStartingTimes, + maxLocations: maxLocations, scheduledEntries: scheduledEntries ) } @@ -168,10 +184,10 @@ extension RequestPayload.Runtime { // MARK: Generate schedule extension RequestPayload.Runtime { - private static func generateSchedule( + private static func generateSchedule( dayOfWeek: DayOfWeek, settings: RequestPayload.Runtime, - dataSnapshot: LeagueScheduleDataSnapshot, + dataSnapshot: LeagueScheduleDataSnapshot, divisionsCount: Int, maxStartingTimes: TimeIndex, maxLocations: LocationIndex, @@ -194,7 +210,7 @@ extension RequestPayload.Runtime { scheduledEntries: scheduledEntries ) - var snapshots = [LeagueScheduleDataSnapshot]() + var snapshots = [LeagueScheduleDataSnapshot]() snapshots.reserveCapacity(gameDays) var gameDayRegenerationAttempt:UInt32 = 0 var day:DayIndex = 0 @@ -286,9 +302,9 @@ extension RequestPayload.Runtime { finalizeGenerationData(generationData: &generationData, data: data) return generationData } - private static func finalizeGenerationData( + private static func finalizeGenerationData( generationData: inout LeagueGenerationData, - data: borrowing LeagueScheduleData + data: borrowing LeagueScheduleData ) { generationData.executionSteps = data.executionSteps generationData.shuffleHistory = data.shuffleHistory @@ -297,8 +313,8 @@ extension RequestPayload.Runtime { // MARK: Load max allocations extension RequestPayload.Runtime { - static func loadMaxAllocations( - dataSnapshot: inout LeagueScheduleDataSnapshot, + static func loadMaxAllocations( + dataSnapshot: inout LeagueScheduleDataSnapshot, gameDayDivisionEntries: inout ContiguousArray>>, settings: borrowing RequestPayload.Runtime, maxStartingTimes: TimeIndex, diff --git a/Sources/league-scheduling/data/LeagueScheduleData.swift b/Sources/league-scheduling/data/LeagueScheduleData.swift index d6f001f..647a97d 100644 --- a/Sources/league-scheduling/data/LeagueScheduleData.swift +++ b/Sources/league-scheduling/data/LeagueScheduleData.swift @@ -3,8 +3,9 @@ import StaticDateTimes // MARK: Data /// Fundamental building block that keeps track of and enforces assignment rules when building the schedule. -struct LeagueScheduleData: Sendable, ~Copyable { +struct LeagueScheduleData: Sendable, ~Copyable { let clock = ContinuousClock() + var rng:RNG let entriesPerMatchup:EntriesPerMatchup let entriesCount:Int let entryDivisions:ContiguousArray @@ -40,9 +41,10 @@ struct LeagueScheduleData: Sendable, ~Copyable { var redistributedMatchups = false init( - snapshot: LeagueScheduleDataSnapshot + snapshot: LeagueScheduleDataSnapshot ) { //locations = snapshot.locations + rng = snapshot.rng entriesPerMatchup = snapshot.entriesPerMatchup entriesCount = snapshot.entriesCount entryDivisions = snapshot.entryDivisions @@ -62,8 +64,9 @@ struct LeagueScheduleData: Sendable, ~Copyable { // MARK: Snapshot extension LeagueScheduleData { - mutating func loadSnapshot(_ snapshot: LeagueScheduleDataSnapshot) { + mutating func loadSnapshot(_ snapshot: LeagueScheduleDataSnapshot) { //locations = snapshot.locations + rng = snapshot.rng divisionRecurringDayLimitInterval = snapshot.divisionRecurringDayLimitInterval day = snapshot.day defaultMaxEntryMatchupsPerGameDay = snapshot.defaultMaxEntryMatchupsPerGameDay @@ -77,7 +80,7 @@ extension LeagueScheduleData { shuffleHistory = snapshot.shuffleHistory } - func snapshot() -> LeagueScheduleDataSnapshot { + func snapshot() -> LeagueScheduleDataSnapshot { return .init(self) } } @@ -142,7 +145,7 @@ extension LeagueScheduleData { assignmentState.availableSlots = availableSlots switch daySettings.gameGap { case .no: - allowedDivisionCombinations = Self.allowedDivisionMatchupCombinations( + allowedDivisionCombinations = calculateAllowedDivisionMatchupCombinations( entriesPerMatchup: entriesPerMatchup, locations: daySettings.locations, entryCountsForDivision: entryCountsForDivision diff --git a/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift b/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift index 7455158..f006eb4 100644 --- a/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift +++ b/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift @@ -1,7 +1,8 @@ import StaticDateTimes -struct LeagueScheduleDataSnapshot: Sendable { +struct LeagueScheduleDataSnapshot: Sendable { + let rng:RNG let entriesPerMatchup:EntriesPerMatchup let entriesCount:Int let entryDivisions:ContiguousArray @@ -28,6 +29,7 @@ struct LeagueScheduleDataSnapshot: Sendable { var shuffleHistory = [LeagueShuffleAction]() init( + rng: RNG, maxStartingTimes: TimeIndex, startingTimes: [StaticTime], maxLocations: LocationIndex, @@ -41,6 +43,7 @@ struct LeagueScheduleDataSnapshot: Sendable { locationTravelDurations: [[MatchupDuration]], maxSameOpponentMatchups: MaximumSameOpponentMatchups ) { + self.rng = rng self.entriesPerMatchup = entriesPerMatchup self.entriesCount = entries.count self.gameGap = gameGap @@ -87,7 +90,8 @@ struct LeagueScheduleDataSnapshot: Sendable { ) } - init(_ snapshot: borrowing LeagueScheduleData) { + init(_ snapshot: borrowing LeagueScheduleData) { + rng = snapshot.rng entriesPerMatchup = snapshot.entriesPerMatchup entriesCount = snapshot.entriesCount entryDivisions = snapshot.entryDivisions diff --git a/Sources/league-scheduling/data/MatchupBlock.swift b/Sources/league-scheduling/data/MatchupBlock.swift index d29c3fb..c96decb 100644 --- a/Sources/league-scheduling/data/MatchupBlock.swift +++ b/Sources/league-scheduling/data/MatchupBlock.swift @@ -18,6 +18,7 @@ extension LeagueScheduleData { gameGap: gameGap, entryMatchupsPerGameDay: defaultMaxEntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: divisionRecurringDayLimitInterval, + rng: &rng, assignmentState: &assignmentState, selectSlot: SelectSlotB2B(entryMatchupsPerGameDay: defaultMaxEntryMatchupsPerGameDay), canPlayAt: canPlayAt @@ -33,6 +34,7 @@ extension LeagueScheduleData { gameGap: gameGap, entryMatchupsPerGameDay: defaultMaxEntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: divisionRecurringDayLimitInterval, + rng: &rng, assignmentState: &assignmentState, selectSlot: SelectSlotNormal(), canPlayAt: canPlayAt @@ -53,6 +55,7 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, + rng: inout some RandomNumberGenerator, assignmentState: inout AssignmentState, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable @@ -85,6 +88,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: entryMatchupsPerGameDay, divisionRecurringDayLimitInterval: divisionRecurringDayLimitInterval, allAvailableMatchups: localAssignmentState.availableMatchups, + rng: &rng, localAssignmentState: &localAssignmentState, shouldSkipSelection: { _ in false }, remainingPrioritizedEntries: &remainingPrioritizedEntries, @@ -92,7 +96,7 @@ extension LeagueScheduleData { selectSlot: selectSlot, canPlayAt: canPlayAt ) else { return nil } - adjacentTimes = Self.adjacentTimes(for: firstMatchup.time, entryMatchupsPerGameDay: entryMatchupsPerGameDay) + adjacentTimes = calculateAdjacentTimes(for: firstMatchup.time, entryMatchupsPerGameDay: entryMatchupsPerGameDay) localAssignmentState.availableSlots = localAssignmentState.availableSlots.filter { $0.time == firstMatchup.time } localAssignmentState.recalculateAllRemainingAllocations( day: day, @@ -112,6 +116,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: entryMatchupsPerGameDay, divisionRecurringDayLimitInterval: divisionRecurringDayLimitInterval, allAvailableMatchups: localAssignmentState.availableMatchups, + rng: &rng, localAssignmentState: &localAssignmentState, remainingPrioritizedEntries: &remainingPrioritizedEntries, selectedEntries: &selectedEntries, @@ -133,7 +138,7 @@ extension LeagueScheduleData { let availableMatchups = lastLocalAssignmentStateAvailableMatchups.filter { targetEntries.contains($0.team1) && targetEntries.contains($0.team2) } - for entryID in targetEntries { + for entryID in targetEntries { // TODO: support determinism if availableMatchups.first(where: { $0.team1 == entryID || $0.team2 == entryID }) == nil { #if LOG print("assignBlockOfMatchups;i == lastMatchupIndex;$0=\($0);targetEntries (\(targetEntries.count))=\(targetEntries);entryID=\(entryID);availableMatchups.first of entryID == nil;skipping $0") @@ -152,6 +157,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: entryMatchupsPerGameDay, divisionRecurringDayLimitInterval: divisionRecurringDayLimitInterval, allAvailableMatchups: localAssignmentState.availableMatchups, + rng: &rng, localAssignmentState: &localAssignmentState, shouldSkipSelection: shouldSkipSelection, remainingPrioritizedEntries: &remainingPrioritizedEntries, @@ -160,7 +166,7 @@ extension LeagueScheduleData { canPlayAt: canPlayAt ) else { return nil } // last matchup was successfully assigned; continue - if var time = adjacentTimes.randomElement() { // TODO: pick an adjacent time that needs to be prioritized over others + if var time = adjacentTimes.randomElement(using: &rng) { // TODO: pick an adjacent time that needs to be prioritized over others // assign matchups from previously scheduled entries until they have played all their games localAssignmentState.availableMatchups = localAssignmentState.availableMatchups.filter { selectedEntries.contains($0.team1) && selectedEntries.contains($0.team2) @@ -184,6 +190,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: entryMatchupsPerGameDay, divisionRecurringDayLimitInterval: divisionRecurringDayLimitInterval, allAvailableMatchups: localAssignmentState.availableMatchups, + rng: &rng, assignmentState: &localAssignmentState, selectSlot: selectSlot, canPlayAt: canPlayAt @@ -193,7 +200,7 @@ extension LeagueScheduleData { #if LOG print("assignBlockOfMatchups;j=\(j);finished time \(time)") #endif - if let nextTime = adjacentTimes.randomElement() { + if let nextTime = adjacentTimes.randomElement(using: &rng) { time = nextTime } } @@ -222,6 +229,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, allAvailableMatchups: Set, + rng: inout some RandomNumberGenerator, localAssignmentState: inout AssignmentState, remainingPrioritizedEntries: inout Set, selectedEntries: inout Set, @@ -237,6 +245,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: entryMatchupsPerGameDay, divisionRecurringDayLimitInterval: divisionRecurringDayLimitInterval, allAvailableMatchups: allAvailableMatchups, + rng: &rng, assignmentState: &localAssignmentState, selectSlot: selectSlot, canPlayAt: canPlayAt @@ -259,6 +268,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, allAvailableMatchups: Set, + rng: inout some RandomNumberGenerator, localAssignmentState: inout AssignmentState, shouldSkipSelection: (MatchupPair) -> Bool, remainingPrioritizedEntries: inout Set, @@ -275,6 +285,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: entryMatchupsPerGameDay, divisionRecurringDayLimitInterval: divisionRecurringDayLimitInterval, allAvailableMatchups: allAvailableMatchups, + rng: &rng, assignmentState: &localAssignmentState, shouldSkipSelection: shouldSkipSelection, selectSlot: selectSlot, @@ -289,30 +300,4 @@ extension LeagueScheduleData { selectedEntries.insert(leagueMatchup.away) return leagueMatchup } -} - -// MARK: Adjacent times -extension LeagueScheduleData { - static func adjacentTimes( - for time: TimeIndex, - entryMatchupsPerGameDay: EntryMatchupsPerGameDay - ) -> Set { - var adjacentTimes = Set() - let timeIndex = time % entryMatchupsPerGameDay - if timeIndex == 0 { - for i in 1.. MatchupPair? { - return assignmentState.selectMatchup(prioritizedMatchups: prioritizedMatchups) + mutating func selectMatchup(prioritizedMatchups: borrowing PrioritizedMatchups) -> MatchupPair? { + return assignmentState.selectMatchup( + prioritizedMatchups: prioritizedMatchups, + rng: &rng + ) } } extension AssignmentState { /// - Returns: Matchup pair that should be prioritized to be scheduled due to how many allocations it has remaining. func selectMatchup( - prioritizedMatchups: borrowing PrioritizedMatchups + prioritizedMatchups: borrowing PrioritizedMatchups, + rng: inout some RandomNumberGenerator ) -> MatchupPair? { return Self.selectMatchup( prioritizedMatchups: prioritizedMatchups, numberOfAssignedMatchups: numberOfAssignedMatchups, recurringDayLimits: recurringDayLimits, - remainingAllocations: remainingAllocations + remainingAllocations: remainingAllocations, + rng: &rng ) } @@ -25,7 +30,8 @@ extension AssignmentState { prioritizedMatchups: borrowing PrioritizedMatchups, numberOfAssignedMatchups: [Int], recurringDayLimits: RecurringDayLimits, - remainingAllocations: RemainingAllocations + remainingAllocations: RemainingAllocations, + rng: inout some RandomNumberGenerator ) -> MatchupPair? { #if LOG print("SelectMatchup;selectMatchup;prioritizedMatchups.count=\(prioritizedMatchups.matchups.count);availableMatchupCountForEntry=\(prioritizedMatchups.availableMatchupCountForEntry)") @@ -47,7 +53,7 @@ extension AssignmentState { // - regenerating a failed day // - selecting the last matchup pair out of previous pairs of equal priority var pool = Set() - for pair in prioritizedMatchups.matchups[prioritizedMatchups.matchups.index(after: prioritizedMatchups.matchups.startIndex)...] { + for pair in prioritizedMatchups.matchups[prioritizedMatchups.matchups.index(after: prioritizedMatchups.matchups.startIndex)...] { // TODO: support determinism let (pairMinMatchupsPlayedSoFar, pairTotalMatchupsPlayedSoFar) = numberOfMatchupsPlayedSoFar(for: pair, numberOfAssignedMatchups: numberOfAssignedMatchups) guard pairMinMatchupsPlayedSoFar == selected.minMatchupsPlayedSoFar else { if pairMinMatchupsPlayedSoFar < selected.minMatchupsPlayedSoFar { @@ -158,7 +164,7 @@ extension AssignmentState { #if LOG print("SelectMatchup;selectMatchup;selected.pair=\(selected.pair.description);pool=\(pool.map({ $0.description }))") #endif - return pool.isEmpty ? selected.pair : pool.randomElement() + return pool.isEmpty ? selected.pair : pool.randomElement(using: &rng) } } diff --git a/Sources/league-scheduling/data/Shuffle.swift b/Sources/league-scheduling/data/Shuffle.swift index 79d0d72..2f04a26 100644 --- a/Sources/league-scheduling/data/Shuffle.swift +++ b/Sources/league-scheduling/data/Shuffle.swift @@ -32,7 +32,7 @@ extension AssignmentState { let team2LocationNumbers = assignedLocations[unchecked: matchup.team2] let team2MaxTimeNumbers = maxTimeAllocations[unchecked: matchup.team2] let team2MaxLocationNumbers = maxLocationAllocations[unchecked: matchup.team2] - for swapped in matchups { + for swapped in matchups { // TODO: support determinism // make sure the failed assigned matchup is allowed to go where the assigned matchup is guard canPlayAt.test( time: swapped.time, @@ -96,7 +96,7 @@ extension AssignmentState { let maxHomeLocationNumbers = maxLocationAllocations[unchecked: swapped.home] let maxAwayTimeNumbers = maxTimeAllocations[unchecked: swapped.away] let maxAwayLocationNumbers = maxLocationAllocations[unchecked: swapped.away] - guard let slot = availableSlots.first(where: { + guard let slot = availableSlots.first(where: { // TODO: support determinism return canPlayAt.test( time: $0.time, location: $0.location, diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift index e619b05..f39374a 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift @@ -51,7 +51,7 @@ extension SelectSlotNormal { playableSlots: Set ) -> AvailableSlot? { var selected = getSelectedSlot(playableSlots[playableSlots.startIndex], team1Times, team1Locations, team2Times, team2Locations) - for slot in playableSlots[playableSlots.index(after: playableSlots.startIndex)...] { + for slot in playableSlots[playableSlots.index(after: playableSlots.startIndex)...] { // TODO: support determinism let minimum = getMinimumAssigned(slot, team1Times, team1Locations, team2Times, team2Locations) if minimum <= selected.minimumAssigned { selected.slot = slot diff --git a/Sources/league-scheduling/globals.swift b/Sources/league-scheduling/globals.swift new file mode 100644 index 0000000..8a2bf9f --- /dev/null +++ b/Sources/league-scheduling/globals.swift @@ -0,0 +1,24 @@ + +// MARK: adjacent times +func calculateAdjacentTimes( + for time: TimeIndex, + entryMatchupsPerGameDay: EntryMatchupsPerGameDay +) -> Set { + var adjacentTimes = Set() + let timeIndex = time % entryMatchupsPerGameDay + if timeIndex == 0 { + for i in 1.. UInt64 { + // LCG formula: state = (state * multiplier + increment) % modulus + state = state &* multiplier &+ increment + return state + } +} \ No newline at end of file diff --git a/Tests/LeagueSchedulingTests/DivisionMatchupCombinationTests.swift b/Tests/LeagueSchedulingTests/DivisionMatchupCombinationTests.swift index a5eb464..9f6dca5 100644 --- a/Tests/LeagueSchedulingTests/DivisionMatchupCombinationTests.swift +++ b/Tests/LeagueSchedulingTests/DivisionMatchupCombinationTests.swift @@ -14,7 +14,7 @@ struct DivisionMatchupCombinationTests { ] ] expected += expected - var combos = LeagueScheduleData.allDivisionMatchupCombinations( + var combos = calculateAllDivisionMatchupCombinations( entriesPerMatchup: 2, locations: 6, entryCountsForDivision: [12, 12] @@ -29,7 +29,7 @@ struct DivisionMatchupCombinationTests { [0, 5], [2, 3], [3, 2], [5, 0] ] ] - combos = LeagueScheduleData.allDivisionMatchupCombinations( + combos = calculateAllDivisionMatchupCombinations( entriesPerMatchup: 2, locations: 6, entryCountsForDivision: [14, 10] @@ -45,7 +45,7 @@ struct DivisionMatchupCombinationTests { [0, 5], [2, 3], [3, 2], [5, 0] ] ] - combos = LeagueScheduleData.allDivisionMatchupCombinations( + combos = calculateAllDivisionMatchupCombinations( entriesPerMatchup: 2, locations: 6, entryCountsForDivision: [14, 0, 10] @@ -75,7 +75,7 @@ extension DivisionMatchupCombinationTests { [6, 0], [0, 6] ] ] - var combos = LeagueScheduleData.allowedDivisionMatchupCombinations( + var combos = calculateAllowedDivisionMatchupCombinations( entriesPerMatchup: 2, locations: 6, entryCountsForDivision: [12, 12] @@ -90,7 +90,7 @@ extension DivisionMatchupCombinationTests { [4, 3], [2, 3] ] ] - combos = LeagueScheduleData.allowedDivisionMatchupCombinations( + combos = calculateAllowedDivisionMatchupCombinations( entriesPerMatchup: 2, locations: 6, entryCountsForDivision: [14, 10] diff --git a/Tests/LeagueSchedulingTests/MatchupBlockTests.swift b/Tests/LeagueSchedulingTests/MatchupBlockTests.swift index f96bbd6..1121cfd 100644 --- a/Tests/LeagueSchedulingTests/MatchupBlockTests.swift +++ b/Tests/LeagueSchedulingTests/MatchupBlockTests.swift @@ -11,36 +11,36 @@ struct MatchupBlockTests: ScheduleExpectations { extension MatchupBlockTests { @Test(.timeLimit(.minutes(1))) func adjacentTimes() { - var adjacent = LeagueScheduleData.adjacentTimes(for: 0, entryMatchupsPerGameDay: 2) + var adjacent = calculateAdjacentTimes(for: 0, entryMatchupsPerGameDay: 2) #expect(adjacent == [1]) - adjacent = LeagueScheduleData.adjacentTimes(for: 0, entryMatchupsPerGameDay: 3) + adjacent = calculateAdjacentTimes(for: 0, entryMatchupsPerGameDay: 3) #expect(adjacent == [1, 2]) - adjacent = LeagueScheduleData.adjacentTimes(for: 0, entryMatchupsPerGameDay: 4) + adjacent = calculateAdjacentTimes(for: 0, entryMatchupsPerGameDay: 4) #expect(adjacent == [1, 2, 3]) - adjacent = LeagueScheduleData.adjacentTimes(for: 1, entryMatchupsPerGameDay: 2) + adjacent = calculateAdjacentTimes(for: 1, entryMatchupsPerGameDay: 2) #expect(adjacent == [0]) - adjacent = LeagueScheduleData.adjacentTimes(for: 1, entryMatchupsPerGameDay: 3) + adjacent = calculateAdjacentTimes(for: 1, entryMatchupsPerGameDay: 3) #expect(adjacent == [0, 2]) - adjacent = LeagueScheduleData.adjacentTimes(for: 1, entryMatchupsPerGameDay: 4) + adjacent = calculateAdjacentTimes(for: 1, entryMatchupsPerGameDay: 4) #expect(adjacent == [0, 2, 3]) - adjacent = LeagueScheduleData.adjacentTimes(for: 2, entryMatchupsPerGameDay: 2) + adjacent = calculateAdjacentTimes(for: 2, entryMatchupsPerGameDay: 2) #expect(adjacent == [3]) - adjacent = LeagueScheduleData.adjacentTimes(for: 2, entryMatchupsPerGameDay: 3) + adjacent = calculateAdjacentTimes(for: 2, entryMatchupsPerGameDay: 3) #expect(adjacent == [0, 1]) - adjacent = LeagueScheduleData.adjacentTimes(for: 2, entryMatchupsPerGameDay: 4) + adjacent = calculateAdjacentTimes(for: 2, entryMatchupsPerGameDay: 4) #expect(adjacent == [0, 1, 3]) - adjacent = LeagueScheduleData.adjacentTimes(for: 2, entryMatchupsPerGameDay: 5) + adjacent = calculateAdjacentTimes(for: 2, entryMatchupsPerGameDay: 5) #expect(adjacent == [0, 1, 3, 4]) } } \ No newline at end of file From 2a27e39ab8552b1b0326d248da3c1b1d0e124394 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Thu, 19 Mar 2026 09:04:39 -0500 Subject: [PATCH 02/19] adopt `OrderedSet` from `swift-collections` to enable deterministic output --- Package.swift | 4 ++ Sources/ProtocolBuffers/Determinisim.proto | 37 +++++++++++++++++++ .../GenerationConstraints.proto | 5 +++ .../LeagueGenerationData.swift | 6 ++- .../data/AssignMatchup.swift | 10 +++-- .../league-scheduling/data/AssignSlots.swift | 10 +++-- .../data/AssignmentState.swift | 25 +++++++------ .../data/BalanceHomeAway.swift | 12 +++--- .../league-scheduling/data/Generation.swift | 20 +++++----- .../data/LeagueScheduleData.swift | 17 +++++---- .../data/LeagueScheduleDataSnapshot.swift | 9 +++-- .../league-scheduling/data/MatchupBlock.swift | 36 +++++++++--------- .../data/PrioritizedMatchups.swift | 18 +++++---- .../data/RedistributionData.swift | 14 ++++--- .../data/SelectMatchup.swift | 12 +++--- Sources/league-scheduling/data/Shuffle.swift | 8 ++-- .../data/assignment/Assign.swift | 10 ++--- .../data/assignment/Move.swift | 5 ++- .../data/assignment/Unassign.swift | 8 ++-- .../data/selectSlot/SelectSlotB2B.swift | 6 ++- .../selectSlot/SelectSlotEarliestTime.swift | 8 ++-- ...SlotEarliestTimeAndSameLocationIfB2B.swift | 4 +- .../data/selectSlot/SelectSlotNormal.swift | 10 +++-- .../data/selectSlot/SelectSlotProtocol.swift | 4 +- Sources/league-scheduling/globals.swift | 12 +++--- Sources/league-scheduling/typealiases.swift | 8 ++-- .../CanPlayAtTests.swift | 7 ++-- .../expectations/ScheduleExpectations.swift | 3 +- .../util/MatchupsPlayedPerGameDay.swift | 3 +- 29 files changed, 210 insertions(+), 121 deletions(-) create mode 100644 Sources/ProtocolBuffers/Determinisim.proto diff --git a/Package.swift b/Package.swift index 5a12c19..7a35a52 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,9 @@ let package = Package( dependencies: [ .package(url: "https://github.com/RandomHashTags/swift-staticdatetime", from: "0.3.5"), + // Ordered sets + .package(url: "https://github.com/apple/swift-collections", from: "1.4.0"), + // Protocol buffers .package(url: "https://github.com/apple/swift-protobuf", from: "1.31.0"), ], @@ -28,6 +31,7 @@ let package = Package( .target( name: "LeagueScheduling", dependencies: [ + .product(name: "OrderedCollections", package: "swift-collections"), .product(name: "StaticDateTimes", package: "swift-staticdatetime"), .product(name: "SwiftProtobuf", package: "swift-protobuf") ], diff --git a/Sources/ProtocolBuffers/Determinisim.proto b/Sources/ProtocolBuffers/Determinisim.proto new file mode 100644 index 0000000..30dfd90 --- /dev/null +++ b/Sources/ProtocolBuffers/Determinisim.proto @@ -0,0 +1,37 @@ +// Copyright 2026 Evan Anderson. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Evan Anderson nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package lit_leagues.leagues; + +// Constraints that influence how deterministic the schedule generation process is. +message Determinism { + optional uint32 technique = 1; + optional uint64 seed = 2; +} \ No newline at end of file diff --git a/Sources/ProtocolBuffers/GenerationConstraints.proto b/Sources/ProtocolBuffers/GenerationConstraints.proto index 19521b1..3227e36 100644 --- a/Sources/ProtocolBuffers/GenerationConstraints.proto +++ b/Sources/ProtocolBuffers/GenerationConstraints.proto @@ -30,6 +30,8 @@ syntax = "proto3"; package lit_leagues.leagues; +import "Determinism.proto"; + // Constraints that influence the schedule generation process. message GenerationConstraints { // Maximum number of seconds the schedule can take to generate. Default value is 60; 0=infinite (will continue until the regeneration attempt threshold is met). @@ -44,4 +46,7 @@ message GenerationConstraints { // Maximum number of total regeneration attempts before stopping execution and marking the schedule generation as a failure. // Default value is 10,000. optional uint32 regenerationAttemptsThreshold = 4; + + // Deterministic constraints. If not provided, the output is non-deterministic (heavily relies on randomness and probabilities). + optional Determinism determinism = 5; } \ No newline at end of file diff --git a/Sources/league-scheduling/LeagueGenerationData.swift b/Sources/league-scheduling/LeagueGenerationData.swift index f2ed26c..c91cde6 100644 --- a/Sources/league-scheduling/LeagueGenerationData.swift +++ b/Sources/league-scheduling/LeagueGenerationData.swift @@ -1,9 +1,11 @@ +import OrderedCollections + public struct LeagueGenerationData: Sendable { public var error:LeagueError? = nil public var assignLocationTimeRegenerationAttempts:UInt32 = 0 public var negativeDayIndexRegenerationAttempts:UInt32 = 0 - public var schedule:ContiguousArray> = [] + public var schedule:ContiguousArray> = [] public var executionSteps = [ExecutionStep]() public var shuffleHistory = [LeagueShuffleAction]() } @@ -22,7 +24,7 @@ extension LeagueGenerationData: Codable { func scheduleSorted() -> ContiguousArray<[Matchup]> { var array:ContiguousArray<[Matchup]> = .init(repeating: [], count: schedule.count) - for (dayIndex, matchups) in schedule.enumerated() { // TODO: support determinism + for (dayIndex, matchups) in schedule.enumerated() { array[unchecked: dayIndex] = matchups.sorted(by: { guard $0.time == $1.time else { return $0.time < $1.time } return $0.location < $1.location diff --git a/Sources/league-scheduling/data/AssignMatchup.swift b/Sources/league-scheduling/data/AssignMatchup.swift index d819b4e..7579dcd 100644 --- a/Sources/league-scheduling/data/AssignMatchup.swift +++ b/Sources/league-scheduling/data/AssignMatchup.swift @@ -1,11 +1,13 @@ +import OrderedCollections + // MARK: Assign Matchup extension LeagueScheduleData { /// - Returns: The `Matchup` that was successfully assigned. @discardableResult mutating func assignMatchupPair( _ pair: MatchupPair, - allAvailableMatchups: Set, + allAvailableMatchups: OrderedSet, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { @@ -34,7 +36,7 @@ extension AssignmentState { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Set, + allAvailableMatchups: OrderedSet, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { @@ -91,13 +93,13 @@ extension AssignmentState { // MARK: Playable slots extension AssignmentState { - func playableSlots(for pair: MatchupPair) -> Set { + func playableSlots(for pair: MatchupPair) -> OrderedSet { return Self.playableSlots(for: pair, remainingAllocations: remainingAllocations) } static func playableSlots( for pair: MatchupPair, remainingAllocations: RemainingAllocations - ) -> Set { + ) -> OrderedSet { return remainingAllocations[unchecked: pair.team1].intersection(remainingAllocations[unchecked: pair.team2]) } } \ No newline at end of file diff --git a/Sources/league-scheduling/data/AssignSlots.swift b/Sources/league-scheduling/data/AssignSlots.swift index 6888b69..73ebf1f 100644 --- a/Sources/league-scheduling/data/AssignSlots.swift +++ b/Sources/league-scheduling/data/AssignSlots.swift @@ -1,4 +1,6 @@ +import OrderedCollections + // MARK: Assign slots extension LeagueScheduleData { /// Assigns available slots for the day, taking into account all schedule settings, previously assigned matchups and generation data. @@ -147,8 +149,8 @@ extension LeagueScheduleData { assignmentState.availableMatchups = divisionMatchups assignmentState.prioritizedEntries.removeAll(keepingCapacity: true) for matchup in assignmentState.availableMatchups { - assignmentState.prioritizedEntries.insert(matchup.team1) - assignmentState.prioritizedEntries.insert(matchup.team2) + assignmentState.prioritizedEntries.append(matchup.team1) + assignmentState.prioritizedEntries.append(matchup.team2) } assignmentState.recalculateAllRemainingAllocations( day: day, @@ -245,7 +247,7 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Set, + allAvailableMatchups: OrderedSet, rng: inout some RandomNumberGenerator, assignmentState: inout AssignmentState, shouldSkipSelection: (MatchupPair) -> Bool, @@ -296,7 +298,7 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Set, + allAvailableMatchups: OrderedSet, rng: inout some RandomNumberGenerator, assignmentState: inout AssignmentState, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, diff --git a/Sources/league-scheduling/data/AssignmentState.swift b/Sources/league-scheduling/data/AssignmentState.swift index bb044b3..76c3987 100644 --- a/Sources/league-scheduling/data/AssignmentState.swift +++ b/Sources/league-scheduling/data/AssignmentState.swift @@ -1,4 +1,5 @@ +import OrderedCollections import StaticDateTimes // MARK: Noncopyable @@ -45,27 +46,27 @@ struct AssignmentState: Sendable, ~Copyable { let maxSameOpponentMatchups:MaximumSameOpponentMatchups /// All matchup pairs that can be scheduled. - var allMatchups:Set + var allMatchups:OrderedSet /// All matchup pairs that can be scheduled, grouped by division. /// /// - Usage: [`Division.IDValue`: `available matchups`] - var allDivisionMatchups:ContiguousArray> + var allDivisionMatchups:ContiguousArray> /// Remaining available matchup pairs that can be assigned for the `day`. - var availableMatchups:Set + var availableMatchups:OrderedSet - var prioritizedEntries:Set + var prioritizedEntries:OrderedSet /// Remaining available slots that can be filled for the `day`. - var availableSlots:Set + var availableSlots:OrderedSet var playsAt:PlaysAt var playsAtTimes:PlaysAtTimes var playsAtLocations:PlaysAtLocations /// Available matchups that can be scheduled. - var matchups:Set + var matchups:OrderedSet var shuffleHistory = [LeagueShuffleAction]() @@ -164,25 +165,25 @@ struct AssignmentStateCopyable { var maxSameOpponentMatchups:MaximumSameOpponentMatchups /// All matchup pairs that can be scheduled - var allMatchups:Set + var allMatchups:OrderedSet /// All matchup pairs that can be scheduled, grouped by division. /// /// - Usage: [`Division.IDValue`: `available matchups`] - var allDivisionMatchups:ContiguousArray> + var allDivisionMatchups:ContiguousArray> /// Remaining available matchup pairs that can be assigned for the `day`. - var availableMatchups:Set + var availableMatchups:OrderedSet - var prioritizedEntries:Set + var prioritizedEntries:OrderedSet /// Remaining available slots that can be filled for the `day`. - var availableSlots:Set + var availableSlots:OrderedSet var playsAt:PlaysAt var playsAtTimes:PlaysAtTimes var playsAtLocations:PlaysAtLocations - var matchups:Set + var matchups:OrderedSet var shuffleHistory:[LeagueShuffleAction] diff --git a/Sources/league-scheduling/data/BalanceHomeAway.swift b/Sources/league-scheduling/data/BalanceHomeAway.swift index 66e9ffc..11c7486 100644 --- a/Sources/league-scheduling/data/BalanceHomeAway.swift +++ b/Sources/league-scheduling/data/BalanceHomeAway.swift @@ -1,4 +1,6 @@ +import OrderedCollections + // MARK: Matchup pair extension MatchupPair { /// Balances home/away allocations, mutating `team1` (home) and `team2` (away) if necessary. @@ -54,7 +56,7 @@ extension LeagueScheduleData { #endif let now = clock.now - var unbalancedEntryIDs = Set() + var unbalancedEntryIDs = OrderedSet() unbalancedEntryIDs.reserveCapacity(entriesCount) var neededFlipsToBalance = [(home: UInt8, away: UInt8)](repeating: (0, 0), count: entriesCount) for entryID in 0.. balanceNumber { neededFlipsToBalance[unchecked: entryID].home = home - balanceNumber @@ -75,13 +77,13 @@ extension LeagueScheduleData { appendExecutionStep(now: now) return } - var flippable = Set() + var flippable = OrderedSet() for day in 0.. [LeagueGenerationData] { let divisionsCount = divisions.count - var divisionEntries:ContiguousArray> = .init(repeating: Set(), count: divisionsCount) + var divisionEntries:ContiguousArray> = .init(repeating: OrderedSet(), count: divisionsCount) #if LOG print("LeagueSchedule;generateSchedules;divisionsCount=\(divisionsCount);entries.count=\(entries.count)") #endif for entryIndex in 0..( divisionsCount: Int, - divisionEntries: ContiguousArray>, + divisionEntries: ContiguousArray>, maxStartingTimes: TimeIndex, maxLocations: LocationIndex, dataSnapshot: LeagueScheduleDataSnapshot @@ -197,7 +199,7 @@ extension RequestPayload.Runtime { var generationData = LeagueGenerationData() generationData.assignLocationTimeRegenerationAttempts = 0 generationData.negativeDayIndexRegenerationAttempts = 0 - generationData.schedule = .init(repeating: Set(), count: gameDays) + generationData.schedule = .init(repeating: OrderedSet(), count: gameDays) var dataSnapshot = copy dataSnapshot var gameDayDivisionEntries:ContiguousArray>> = .init(repeating: .init(repeating: Set(), count: divisionsCount), count: gameDays) @@ -433,15 +435,15 @@ extension RequestPayload.Runtime { times: TimeIndex, locations: LocationIndex, locationTimeExclusivity: [Set]? - ) -> Set { - var slots = Set(minimumCapacity: times * locations) + ) -> OrderedSet { + var slots = OrderedSet(minimumCapacity: Int(times) * locations) if let exclusivities = locationTimeExclusivity { for location in 0..>, + divisionEntries: ContiguousArray>, divisions: [Division.Runtime] ) -> MaximumSameOpponentMatchups { var maxSameOpponentMatchups:MaximumSameOpponentMatchups = .init(repeating: .init(repeating: .max, count: entriesCount), count: entriesCount) diff --git a/Sources/league-scheduling/data/LeagueScheduleData.swift b/Sources/league-scheduling/data/LeagueScheduleData.swift index 647a97d..0257572 100644 --- a/Sources/league-scheduling/data/LeagueScheduleData.swift +++ b/Sources/league-scheduling/data/LeagueScheduleData.swift @@ -1,4 +1,5 @@ +import OrderedCollections import StaticDateTimes // MARK: Data @@ -97,7 +98,7 @@ extension LeagueScheduleData { day: DayIndex, daySettings: GeneralSettings.Runtime, divisionEntries: ContiguousArray>, - availableSlots: Set, + availableSlots: OrderedSet, settings: RequestPayload.Runtime, generationData: inout LeagueGenerationData ) throws(LeagueError) { @@ -111,8 +112,8 @@ extension LeagueScheduleData { self.prioritizeEarlierTimes = daySettings.prioritizeEarlierTimes self.gameGap = daySettings.gameGap.minMax self.sameLocationIfB2B = daySettings.sameLocationIfB2B - var availableMatchups = Set() - var prioritizedEntries = Set(minimumCapacity: entriesCount) + var availableMatchups = OrderedSet() + var prioritizedEntries = OrderedSet(minimumCapacity: entriesCount) var entryCountsForDivision:ContiguousArray = .init(repeating: 0, count: divisionEntries.count) expectedMatchupsCount = 0 assignmentState.allDivisionMatchups = .init(repeating: [], count: divisionEntries.count) @@ -157,7 +158,7 @@ extension LeagueScheduleData { assignmentState.allMatchups = availableMatchups assignmentState.availableMatchups = availableMatchups assignmentState.prioritizedEntries = prioritizedEntries - assignmentState.matchups = Set(minimumCapacity: availableSlots.count) + assignmentState.matchups = OrderedSet(minimumCapacity: availableSlots.count) for i in 0.. - ) -> Set { + ) -> OrderedSet { return Self.availableMatchupPairs( for: entries, assignedEntryHomeAways: assignmentState.assignedEntryHomeAways, @@ -213,8 +214,8 @@ extension LeagueScheduleData { for entries: Set, assignedEntryHomeAways: AssignedEntryHomeAways, maxSameOpponentMatchups: MaximumSameOpponentMatchups - ) -> Set { - var pairs = Set(minimumCapacity: (entries.count-1) * 2) + ) -> OrderedSet { + var pairs = OrderedSet(minimumCapacity: (entries.count-1) * 2) let sortedEntries = entries.sorted() var index = 0 @@ -225,7 +226,7 @@ extension LeagueScheduleData { let maxSameOpponentMatchups = maxSameOpponentMatchups[unchecked: home] for away in sortedEntries[index...] { if assignedHome[unchecked: away].sum < maxSameOpponentMatchups[unchecked: away] { - pairs.insert(.init(team1: home, team2: away)) + pairs.append(.init(team1: home, team2: away)) } } } diff --git a/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift b/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift index f006eb4..d391082 100644 --- a/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift +++ b/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift @@ -1,4 +1,5 @@ +import OrderedCollections import StaticDateTimes struct LeagueScheduleDataSnapshot: Sendable { @@ -36,7 +37,7 @@ struct LeagueScheduleDataSnapshot: Sendab entriesPerMatchup: EntriesPerMatchup, maximumPlayableMatchups: [UInt32], entries: [Entry.Runtime], - divisionEntries: ContiguousArray>, + divisionEntries: ContiguousArray>, matchupDuration: MatchupDuration, gameGap: (Int, Int), sameLocationIfB2B: Bool, @@ -49,7 +50,7 @@ struct LeagueScheduleDataSnapshot: Sendab self.gameGap = gameGap self.sameLocationIfB2B = sameLocationIfB2B - var prioritizedEntries = Set(minimumCapacity: entriesCount) + var prioritizedEntries = OrderedSet(minimumCapacity: entriesCount) var entryDivisions = ContiguousArray(repeating: 0, count: entriesCount) for (index, entries) in divisionEntries.enumerated() { prioritizedEntries.formUnion(entries) @@ -82,8 +83,8 @@ struct LeagueScheduleDataSnapshot: Sendab availableMatchups: [], prioritizedEntries: prioritizedEntries, availableSlots: [], - playsAt: .init(repeating: Set(minimumCapacity: defaultMaxEntryMatchupsPerGameDay), count: entriesCount), - playsAtTimes: .init(repeating: Set(minimumCapacity: defaultMaxEntryMatchupsPerGameDay), count: entriesCount), + playsAt: .init(repeating: OrderedSet(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)), count: entriesCount), + playsAtTimes: .init(repeating: OrderedSet(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)), count: entriesCount), playsAtLocations: .init(repeating: Set(minimumCapacity: defaultMaxEntryMatchupsPerGameDay), count: entriesCount), matchups: [], shuffleHistory: [] diff --git a/Sources/league-scheduling/data/MatchupBlock.swift b/Sources/league-scheduling/data/MatchupBlock.swift index c96decb..b6193b7 100644 --- a/Sources/league-scheduling/data/MatchupBlock.swift +++ b/Sources/league-scheduling/data/MatchupBlock.swift @@ -1,4 +1,6 @@ +import OrderedCollections + // MARK: Assign block extension LeagueScheduleData { /// - Returns: The assigned block of matchups @@ -6,7 +8,7 @@ extension LeagueScheduleData { amount: Int, division: Division.IDValue, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable - ) -> Set? { + ) -> OrderedSet? { if gameGap.min == 1 && gameGap.max == 1 { return Self.assignBlockOfMatchups( amount: amount, @@ -59,7 +61,7 @@ extension LeagueScheduleData { assignmentState: inout AssignmentState, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable - ) -> Set? { + ) -> OrderedSet? { let limit = amount * entryMatchupsPerGameDay var remainingPrioritizedEntries = assignmentState.prioritizedEntries var remainingAvailableSlots = assignmentState.availableSlots @@ -75,8 +77,8 @@ extension LeagueScheduleData { print("assignedEntryHomeAways=\(localAssignmentState.assignedEntryHomeAways.map { $0.map { $0.sum } })") #endif // assign initial matchups - var adjacentTimes = Set() - var selectedEntries = Set(minimumCapacity: amount * entriesPerMatchup) + var adjacentTimes = OrderedSet() + var selectedEntries = OrderedSet(minimumCapacity: amount * entriesPerMatchup) // assign the first matchup, prioritizing the matchup's time guard let firstMatchup = selectAndAssignMatchup( @@ -133,12 +135,12 @@ extension LeagueScheduleData { let lastSelectedEntries = selectedEntries let shouldSkipSelection:(MatchupPair) -> Bool = entryMatchupsPerGameDay % 2 == 0 ? { var targetEntries = lastSelectedEntries - targetEntries.insert($0.team1) - targetEntries.insert($0.team2) + targetEntries.append($0.team1) + targetEntries.append($0.team2) let availableMatchups = lastLocalAssignmentStateAvailableMatchups.filter { targetEntries.contains($0.team1) && targetEntries.contains($0.team2) } - for entryID in targetEntries { // TODO: support determinism + for entryID in targetEntries { if availableMatchups.first(where: { $0.team1 == entryID || $0.team2 == entryID }) == nil { #if LOG print("assignBlockOfMatchups;i == lastMatchupIndex;$0=\($0);targetEntries (\(targetEntries.count))=\(targetEntries);entryID=\(entryID);availableMatchups.first of entryID == nil;skipping $0") @@ -228,11 +230,11 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Set, + allAvailableMatchups: OrderedSet, rng: inout some RandomNumberGenerator, localAssignmentState: inout AssignmentState, - remainingPrioritizedEntries: inout Set, - selectedEntries: inout Set, + remainingPrioritizedEntries: inout OrderedSet, + selectedEntries: inout OrderedSet, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { @@ -255,8 +257,8 @@ extension LeagueScheduleData { // successfully assigned remainingPrioritizedEntries.remove(leagueMatchup.home) remainingPrioritizedEntries.remove(leagueMatchup.away) - selectedEntries.insert(leagueMatchup.home) - selectedEntries.insert(leagueMatchup.away) + selectedEntries.append(leagueMatchup.home) + selectedEntries.append(leagueMatchup.away) return leagueMatchup } private static func selectAndAssignMatchup( @@ -267,12 +269,12 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Set, + allAvailableMatchups: OrderedSet, rng: inout some RandomNumberGenerator, localAssignmentState: inout AssignmentState, shouldSkipSelection: (MatchupPair) -> Bool, - remainingPrioritizedEntries: inout Set, - selectedEntries: inout Set, + remainingPrioritizedEntries: inout OrderedSet, + selectedEntries: inout OrderedSet, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { @@ -296,8 +298,8 @@ extension LeagueScheduleData { // successfully assigned remainingPrioritizedEntries.remove(leagueMatchup.home) remainingPrioritizedEntries.remove(leagueMatchup.away) - selectedEntries.insert(leagueMatchup.home) - selectedEntries.insert(leagueMatchup.away) + selectedEntries.append(leagueMatchup.home) + selectedEntries.append(leagueMatchup.away) return leagueMatchup } } \ No newline at end of file diff --git a/Sources/league-scheduling/data/PrioritizedMatchups.swift b/Sources/league-scheduling/data/PrioritizedMatchups.swift index 158f182..c3b238c 100644 --- a/Sources/league-scheduling/data/PrioritizedMatchups.swift +++ b/Sources/league-scheduling/data/PrioritizedMatchups.swift @@ -1,12 +1,14 @@ +import OrderedCollections + struct PrioritizedMatchups: Sendable, ~Copyable { - private(set) var matchups:Set + private(set) var matchups:OrderedSet private(set) var availableMatchupCountForEntry:ContiguousArray init( entriesCount: Int, - prioritizedEntries: Set, - availableMatchups: Set + prioritizedEntries: OrderedSet, + availableMatchups: OrderedSet ) { let matchups = Self.filterMatchups(prioritizedEntries: prioritizedEntries, availableMatchups: availableMatchups) var availableMatchupCountForEntry = ContiguousArray(repeating: 0, count: entriesCount) @@ -19,8 +21,8 @@ struct PrioritizedMatchups: Sendable, ~Copyable { } mutating func update( - prioritizedEntries: Set, - availableMatchups: Set + prioritizedEntries: OrderedSet, + availableMatchups: OrderedSet ) { matchups = Self.filterMatchups(prioritizedEntries: prioritizedEntries, availableMatchups: availableMatchups) for i in availableMatchupCountForEntry.indices { @@ -38,9 +40,9 @@ struct PrioritizedMatchups: Sendable, ~Copyable { } private static func filterMatchups( - prioritizedEntries: Set, - availableMatchups: Set - ) -> Set { + prioritizedEntries: OrderedSet, + availableMatchups: OrderedSet + ) -> OrderedSet { if prioritizedEntries.isEmpty { return availableMatchups } diff --git a/Sources/league-scheduling/data/RedistributionData.swift b/Sources/league-scheduling/data/RedistributionData.swift index b3ec80f..2bd1733 100644 --- a/Sources/league-scheduling/data/RedistributionData.swift +++ b/Sources/league-scheduling/data/RedistributionData.swift @@ -1,4 +1,6 @@ +import OrderedCollections + struct RedistributionData: Sendable { /// The latest `DayIndex` that is allowed to redistribute matchups from. let startDayIndex:DayIndex @@ -53,7 +55,7 @@ extension RedistributionData { #endif var assigned = 0 - var redistributables = Set() + var redistributables = OrderedSet() for fromDayIndex in stride(from: startDayIndex, through: 0, by: -1) { for matchup in generationData.schedule[unchecked: fromDayIndex] { guard !redistributed.contains(matchup) else { continue } @@ -97,7 +99,7 @@ extension RedistributionData { maxLocationNumber: UInt8(awayMaxAssignedLocations[unchecked: slot.location]), gameGap: gameGap ) { - redistributables.insert(.init(fromDay: fromDayIndex, matchup: matchup, toSlot: slot)) + redistributables.append(.init(fromDay: fromDayIndex, matchup: matchup, toSlot: slot)) } assignmentState.incrementAssignData(home: matchup.home, away: matchup.away, slot: matchup.slot) } @@ -128,14 +130,14 @@ extension RedistributionData { // MARK: Select redistributable extension RedistributionData { private func selectRedistributable( - from redistributables: Set, + from redistributables: OrderedSet, generationData: LeagueGenerationData ) -> Redistributable? { var redistributable:Redistributable? = nil // prioritize entries that have been redistributed the least var (cMin, cMax):(UInt16, UInt16) = (.max, .max) - for r in redistributables { // TODO: support determinism + for r in redistributables { if generationData.schedule[unchecked: r.fromDay].count <= minMatchupsRequired { // don't take from the day since the matchups for it will render the day incomplete continue @@ -173,7 +175,7 @@ extension RedistributionData { extension RedistributionData { private mutating func redistribute( redistributable: inout Redistributable, - redistributables: inout Set, + redistributables: inout OrderedSet, assignmentState: inout AssignmentState, generationData: inout LeagueGenerationData ) { @@ -193,7 +195,7 @@ extension RedistributionData { redistributable.matchup.time = redistributable.toSlot.time redistributable.matchup.location = redistributable.toSlot.location - assignmentState.matchups.insert(redistributable.matchup) + assignmentState.matchups.append(redistributable.matchup) assignmentState.availableSlots.remove(redistributable.toSlot) assignmentState.incrementAssignData(home: redistributable.matchup.home, away: redistributable.matchup.away, slot: redistributable.toSlot) assignmentState.insertPlaysAt(home: redistributable.matchup.home, away: redistributable.matchup.away, slot: redistributable.toSlot) diff --git a/Sources/league-scheduling/data/SelectMatchup.swift b/Sources/league-scheduling/data/SelectMatchup.swift index 895417c..eaf35a0 100644 --- a/Sources/league-scheduling/data/SelectMatchup.swift +++ b/Sources/league-scheduling/data/SelectMatchup.swift @@ -1,4 +1,6 @@ +import OrderedCollections + // MARK: Select matchup extension LeagueScheduleData { /// - Returns: Matchup pair that should be prioritized to be scheduled due to how many allocations it has remaining. @@ -52,8 +54,8 @@ extension AssignmentState { // introduce a pool of matchup pairs of equal priority, and random selection, so that we don't repeat identical assignments when // - regenerating a failed day // - selecting the last matchup pair out of previous pairs of equal priority - var pool = Set() - for pair in prioritizedMatchups.matchups[prioritizedMatchups.matchups.index(after: prioritizedMatchups.matchups.startIndex)...] { // TODO: support determinism + var pool = OrderedSet() + for pair in prioritizedMatchups.matchups[prioritizedMatchups.matchups.index(after: prioritizedMatchups.matchups.startIndex)...] { let (pairMinMatchupsPlayedSoFar, pairTotalMatchupsPlayedSoFar) = numberOfMatchupsPlayedSoFar(for: pair, numberOfAssignedMatchups: numberOfAssignedMatchups) guard pairMinMatchupsPlayedSoFar == selected.minMatchupsPlayedSoFar else { if pairMinMatchupsPlayedSoFar < selected.minMatchupsPlayedSoFar { @@ -159,7 +161,7 @@ extension AssignmentState { continue } - pool.insert(pair) + pool.append(pair) } #if LOG print("SelectMatchup;selectMatchup;selected.pair=\(selected.pair.description);pool=\(pool.map({ $0.description }))") @@ -210,10 +212,10 @@ extension AssignmentState { recurringDayLimit: RecurringDayLimitInterval, remainingAllocations: (min: Int, max: Int), remainingMatchupCount: (min: Int, max: Int), - pool: inout Set + pool: inout OrderedSet ) -> SelectedMatchup { pool.removeAll(keepingCapacity: true) - pool.insert(pair) + pool.append(pair) return .init( pair: pair, minMatchupsPlayedSoFar: minMatchupsPlayedSoFar, diff --git a/Sources/league-scheduling/data/Shuffle.swift b/Sources/league-scheduling/data/Shuffle.swift index 2f04a26..e5fd846 100644 --- a/Sources/league-scheduling/data/Shuffle.swift +++ b/Sources/league-scheduling/data/Shuffle.swift @@ -1,4 +1,6 @@ +import OrderedCollections + // MARK: Shuffle extension AssignmentState { /// - Returns: The slot a matchup was sucessfully moved from. @@ -10,7 +12,7 @@ extension AssignmentState { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Set, + allAvailableMatchups: OrderedSet, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> AvailableSlot? { // TODO: fix (can get stuck shuffling the same matchup to the same slot) @@ -32,7 +34,7 @@ extension AssignmentState { let team2LocationNumbers = assignedLocations[unchecked: matchup.team2] let team2MaxTimeNumbers = maxTimeAllocations[unchecked: matchup.team2] let team2MaxLocationNumbers = maxLocationAllocations[unchecked: matchup.team2] - for swapped in matchups { // TODO: support determinism + for swapped in matchups { // make sure the failed assigned matchup is allowed to go where the assigned matchup is guard canPlayAt.test( time: swapped.time, @@ -96,7 +98,7 @@ extension AssignmentState { let maxHomeLocationNumbers = maxLocationAllocations[unchecked: swapped.home] let maxAwayTimeNumbers = maxTimeAllocations[unchecked: swapped.away] let maxAwayLocationNumbers = maxLocationAllocations[unchecked: swapped.away] - guard let slot = availableSlots.first(where: { // TODO: support determinism + guard let slot = availableSlots.first(where: { return canPlayAt.test( time: $0.time, location: $0.location, diff --git a/Sources/league-scheduling/data/assignment/Assign.swift b/Sources/league-scheduling/data/assignment/Assign.swift index 9c43d01..6da1dc2 100644 --- a/Sources/league-scheduling/data/assignment/Assign.swift +++ b/Sources/league-scheduling/data/assignment/Assign.swift @@ -56,7 +56,7 @@ extension AssignmentState { home: home, away: away ) - matchups.insert(leagueMatchup) + matchups.append(leagueMatchup) availableMatchups.remove(matchup) // TODO: fix (why is the following line necessary | it fixes an issue that allowed matchups to exceed the maximumSameOpponentsMatchupsCap, but availableMatchups still contains matchups that shouldn't be scheduled when scheduling b2b) @@ -133,10 +133,10 @@ extension AssignmentState { away: Entry.IDValue, slot: AvailableSlot ) { - playsAt[unchecked: home].insert(slot) - playsAt[unchecked: away].insert(slot) - playsAtTimes[unchecked: home].insert(slot.time) - playsAtTimes[unchecked: away].insert(slot.time) + playsAt[unchecked: home].append(slot) + playsAt[unchecked: away].append(slot) + playsAtTimes[unchecked: home].remove(slot.time) + playsAtTimes[unchecked: away].remove(slot.time) playsAtLocations[unchecked: home].insert(slot.location) playsAtLocations[unchecked: away].insert(slot.location) } diff --git a/Sources/league-scheduling/data/assignment/Move.swift b/Sources/league-scheduling/data/assignment/Move.swift index 79dcc7a..3b6dce1 100644 --- a/Sources/league-scheduling/data/assignment/Move.swift +++ b/Sources/league-scheduling/data/assignment/Move.swift @@ -1,4 +1,5 @@ +import OrderedCollections // MARK: LeagueScheduleData extension LeagueScheduleData { @@ -7,7 +8,7 @@ extension LeagueScheduleData { matchup: Matchup, to slot: AvailableSlot, day: DayIndex, - allAvailableMatchups: Set, + allAvailableMatchups: OrderedSet, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) { assignmentState.move( @@ -36,7 +37,7 @@ extension AssignmentState { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Set, + allAvailableMatchups: OrderedSet, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) { #if LOG diff --git a/Sources/league-scheduling/data/assignment/Unassign.swift b/Sources/league-scheduling/data/assignment/Unassign.swift index ddce8e4..7a50ce8 100644 --- a/Sources/league-scheduling/data/assignment/Unassign.swift +++ b/Sources/league-scheduling/data/assignment/Unassign.swift @@ -1,4 +1,6 @@ +import OrderedCollections + // MARK: Unassign extension AssignmentState { mutating func unassign( @@ -9,7 +11,7 @@ extension AssignmentState { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Set, + allAvailableMatchups: OrderedSet, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) { let recurringDayLimitInterval = divisionRecurringDayLimitInterval[unchecked: entryDivisions[unchecked: matchup.home]] @@ -17,7 +19,7 @@ extension AssignmentState { recurringDayLimits[unchecked: matchup.away][unchecked: matchup.home] -= recurringDayLimitInterval decrementAssignData(home: matchup.home, away: matchup.away, slot: matchup.slot) removePlaysAt(home: matchup.home, away: matchup.away, slot: matchup.slot) - availableSlots.insert(matchup.slot) + availableSlots.append(matchup.slot) matchups.remove(matchup) recalculateAvailableMatchups( @@ -84,7 +86,7 @@ extension AssignmentState { mutating func recalculateAvailableMatchups( day: DayIndex, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, - allAvailableMatchups: Set + allAvailableMatchups: OrderedSet ) { availableMatchups = allAvailableMatchups.filter({ guard assignedEntryHomeAways[unchecked: $0.team1][unchecked: $0.team2].sum < maxSameOpponentMatchups[unchecked: $0.team1][unchecked: $0.team2] diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift index f4c1f44..6abdc0d 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift @@ -1,4 +1,6 @@ +import OrderedCollections + struct SelectSlotB2B: SelectSlotProtocol, ~Copyable { let entryMatchupsPerGameDay:EntryMatchupsPerGameDay @@ -9,7 +11,7 @@ struct SelectSlotB2B: SelectSlotProtocol, ~Copyable { assignedLocations: AssignedLocations, playsAtTimes: PlaysAtTimes, playsAtLocations: PlaysAtLocations, - playableSlots: inout Set + playableSlots: inout OrderedSet ) -> AvailableSlot? { filter( team1: team1, @@ -33,7 +35,7 @@ extension SelectSlotB2B { team1: Entry.IDValue, team2: Entry.IDValue, playsAtTimes: PlaysAtTimes, - playableSlots: inout Set + playableSlots: inout OrderedSet ) { //print("filterSlotBack2Back;playsAtTimes[unchecked: team1].isEmpty=\(playsAtTimes[unchecked: team1].isEmpty);playsAtTimes[unchecked: team2].isEmpty=\(playsAtTimes[unchecked: team2].isEmpty)") if playsAtTimes[unchecked: team1].isEmpty && playsAtTimes[unchecked: team2].isEmpty { diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift index 6cbb718..1c90ff9 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift @@ -1,4 +1,6 @@ +import OrderedCollections + struct SelectSlotEarliestTime: SelectSlotProtocol, ~Copyable { func select( team1: Entry.IDValue, @@ -7,7 +9,7 @@ struct SelectSlotEarliestTime: SelectSlotProtocol, ~Copyable { assignedLocations: AssignedLocations, playsAtTimes: PlaysAtTimes, playsAtLocations: PlaysAtLocations, - playableSlots: inout Set + playableSlots: inout OrderedSet ) -> AvailableSlot? { return Self.select( team1: team1, @@ -25,7 +27,7 @@ extension SelectSlotEarliestTime { team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playableSlots: inout Set + playableSlots: inout OrderedSet ) -> AvailableSlot? { filter(playableSlots: &playableSlots) return SelectSlotNormal.select( @@ -38,7 +40,7 @@ extension SelectSlotEarliestTime { } /// Mutates `playableSlots` so it only contains the slots at the earliest available time. - static func filter(playableSlots: inout Set) { + static func filter(playableSlots: inout OrderedSet) { var earliestTime = TimeIndex.max for slot in playableSlots { if slot.time < earliestTime { diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift index 2b033b4..00a424f 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift @@ -1,4 +1,6 @@ +import OrderedCollections + struct SelectSlotEarliestTimeAndSameLocationIfB2B: SelectSlotProtocol, ~Copyable { func select( team1: Entry.IDValue, @@ -7,7 +9,7 @@ struct SelectSlotEarliestTimeAndSameLocationIfB2B: SelectSlotProtocol, ~Copyable assignedLocations: AssignedLocations, playsAtTimes: PlaysAtTimes, playsAtLocations: PlaysAtLocations, - playableSlots: inout Set + playableSlots: inout OrderedSet ) -> AvailableSlot? { guard !playableSlots.isEmpty else { return nil } let homePlaysAtTimes = playsAtTimes[unchecked: team1] diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift index f39374a..d2784ed 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift @@ -1,4 +1,6 @@ +import OrderedCollections + struct SelectSlotNormal: SelectSlotProtocol, ~Copyable { func select( team1: Entry.IDValue, @@ -7,7 +9,7 @@ struct SelectSlotNormal: SelectSlotProtocol, ~Copyable { assignedLocations: AssignedLocations, playsAtTimes: PlaysAtTimes, playsAtLocations: PlaysAtLocations, - playableSlots: inout Set + playableSlots: inout OrderedSet ) -> AvailableSlot? { return Self.select( team1: team1, @@ -26,7 +28,7 @@ extension SelectSlotNormal { team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playableSlots: Set + playableSlots: OrderedSet ) -> AvailableSlot? { guard !playableSlots.isEmpty else { return nil } let team1Times = assignedTimes[unchecked: team1] @@ -48,10 +50,10 @@ extension SelectSlotNormal { team1Locations: AssignedLocations.Element, team2Times: AssignedTimes.Element, team2Locations: AssignedLocations.Element, - playableSlots: Set + playableSlots: OrderedSet ) -> AvailableSlot? { var selected = getSelectedSlot(playableSlots[playableSlots.startIndex], team1Times, team1Locations, team2Times, team2Locations) - for slot in playableSlots[playableSlots.index(after: playableSlots.startIndex)...] { // TODO: support determinism + for slot in playableSlots[playableSlots.index(after: playableSlots.startIndex)...] { let minimum = getMinimumAssigned(slot, team1Times, team1Locations, team2Times, team2Locations) if minimum <= selected.minimumAssigned { selected.slot = slot diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift index cf26dd5..cc16f6e 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift @@ -1,4 +1,6 @@ +import OrderedCollections + protocol SelectSlotProtocol: Sendable, ~Copyable { func select( team1: Entry.IDValue, @@ -7,6 +9,6 @@ protocol SelectSlotProtocol: Sendable, ~Copyable { assignedLocations: AssignedLocations, playsAtTimes: PlaysAtTimes, playsAtLocations: PlaysAtLocations, - playableSlots: inout Set + playableSlots: inout OrderedSet ) -> AvailableSlot? } \ No newline at end of file diff --git a/Sources/league-scheduling/globals.swift b/Sources/league-scheduling/globals.swift index 8a2bf9f..23b3705 100644 --- a/Sources/league-scheduling/globals.swift +++ b/Sources/league-scheduling/globals.swift @@ -1,22 +1,24 @@ +import OrderedCollections + // MARK: adjacent times func calculateAdjacentTimes( for time: TimeIndex, entryMatchupsPerGameDay: EntryMatchupsPerGameDay -) -> Set { - var adjacentTimes = Set() +) -> OrderedSet { + var adjacentTimes = OrderedSet() let timeIndex = time % entryMatchupsPerGameDay if timeIndex == 0 { for i in 1..> +typealias RemainingAllocations = ContiguousArray> /// When entries can play against each other again. /// @@ -73,7 +75,7 @@ typealias MaximumLocationAllocations = ContiguousArray`] -typealias PlaysAtTimes = ContiguousArray> +typealias PlaysAtTimes = ContiguousArray> /// Locations where an entry has already played at for the `day`. /// @@ -83,4 +85,4 @@ typealias PlaysAtLocations = ContiguousArray> /// Slots where an entry has already played at for the `day`. /// /// - Usage: [`Entry.IDValue`: `Set`] -typealias PlaysAt = ContiguousArray> \ No newline at end of file +typealias PlaysAt = ContiguousArray> \ No newline at end of file diff --git a/Tests/LeagueSchedulingTests/CanPlayAtTests.swift b/Tests/LeagueSchedulingTests/CanPlayAtTests.swift index c0abd8c..721f49f 100644 --- a/Tests/LeagueSchedulingTests/CanPlayAtTests.swift +++ b/Tests/LeagueSchedulingTests/CanPlayAtTests.swift @@ -1,5 +1,6 @@ @testable import LeagueScheduling +import OrderedCollections import StaticDateTimes import Testing @@ -46,8 +47,8 @@ struct CanPlayAtTests { )) } - playsAt.insert(AvailableSlot(time: 0, location: location)) - playsAtTimes.insert(0) + playsAt.append(AvailableSlot(time: 0, location: location)) + playsAtTimes.append(0) #expect(!CanPlayAtNormal.test( time: 0, location: location, @@ -98,7 +99,7 @@ extension CanPlayAtTests { ] var time:TimeIndex = 0 var location:LocationIndex = 0 - var playsAt:Set = [] + var playsAt:PlaysAt.Element = [] var gameGap = GameGap.upTo(5).minMax #expect(CanPlayAtWithTravelDurations.test( diff --git a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift index a34f2e7..d755e41 100644 --- a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift +++ b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift @@ -1,5 +1,6 @@ @testable import LeagueScheduling +import OrderedCollections import Testing protocol ScheduleExpectations: Sendable { @@ -238,7 +239,7 @@ extension ScheduleExpectations { extension ScheduleExpectations { func printMatchups( day: Int, - _ matchups: Set + _ matchups: OrderedSet ) { return let results:String = matchups.sorted(by: { diff --git a/Tests/LeagueSchedulingTests/schedules/util/MatchupsPlayedPerGameDay.swift b/Tests/LeagueSchedulingTests/schedules/util/MatchupsPlayedPerGameDay.swift index 5a1ffa0..78df90c 100644 --- a/Tests/LeagueSchedulingTests/schedules/util/MatchupsPlayedPerGameDay.swift +++ b/Tests/LeagueSchedulingTests/schedules/util/MatchupsPlayedPerGameDay.swift @@ -1,11 +1,12 @@ @testable import LeagueScheduling +import OrderedCollections struct MatchupsPlayedPerGameDay { static func get( gameDays: DayIndex, entriesCount: Int, - schedule: ContiguousArray> + schedule: ContiguousArray> ) -> ContiguousArray> { var matchupsPlayedPerDay = ContiguousArray( repeating: ContiguousArray(repeating: 0, count: entriesCount), From cf5d46738f5ac079220180723bed6d74e7131ce3 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Thu, 19 Mar 2026 09:20:14 -0500 Subject: [PATCH 03/19] fully support custom determinism values --- .../{Determinisim.proto => Determinism.proto} | 0 .../league-scheduling/data/Generation.swift | 34 ++++- .../generated/Determinism.pb.swift | 124 ++++++++++++++++++ .../generated/GenerationConstraints.pb.swift | 18 ++- .../codable/Determinism+Codable.swift | 29 ++++ .../GenerationConstraints+Codable.swift | 7 + 6 files changed, 209 insertions(+), 3 deletions(-) rename Sources/ProtocolBuffers/{Determinisim.proto => Determinism.proto} (100%) create mode 100644 Sources/league-scheduling/generated/Determinism.pb.swift create mode 100644 Sources/league-scheduling/generated/codable/Determinism+Codable.swift diff --git a/Sources/ProtocolBuffers/Determinisim.proto b/Sources/ProtocolBuffers/Determinism.proto similarity index 100% rename from Sources/ProtocolBuffers/Determinisim.proto rename to Sources/ProtocolBuffers/Determinism.proto diff --git a/Sources/league-scheduling/data/Generation.swift b/Sources/league-scheduling/data/Generation.swift index 75e5c39..e3e36ae 100644 --- a/Sources/league-scheduling/data/Generation.swift +++ b/Sources/league-scheduling/data/Generation.swift @@ -63,8 +63,38 @@ extension RequestPayload.Runtime { divisionEntries: divisionEntries, divisions: divisions ) - let rng = SystemRandomNumberGenerator() - //let rng = LCG(seed: 69) + + guard constraints.hasDeterminism else { + return try await generateSchedules( + divisionsCount: divisionsCount, + divisionEntries: divisionEntries, + maxStartingTimes: maxStartingTimes, + maxLocations: maxLocations, + maxSameOpponentMatchups: maxSameOpponentMatchups, + rng: SystemRandomNumberGenerator() + ) + } + switch constraints.determinism.technique { + default: + let seed = constraints.determinism.hasSeed ? constraints.determinism.seed : UInt64.random(in: 0..( + divisionsCount: Int, + divisionEntries: ContiguousArray>, + maxStartingTimes: TimeIndex, + maxLocations: LocationIndex, + maxSameOpponentMatchups: MaximumSameOpponentMatchups, + rng: RNG + ) async throws -> [LeagueGenerationData] { let dataSnapshot = LeagueScheduleDataSnapshot( rng: rng, maxStartingTimes: maxStartingTimes, diff --git a/Sources/league-scheduling/generated/Determinism.pb.swift b/Sources/league-scheduling/generated/Determinism.pb.swift new file mode 100644 index 0000000..fc655dd --- /dev/null +++ b/Sources/league-scheduling/generated/Determinism.pb.swift @@ -0,0 +1,124 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Determinism.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2026 Evan Anderson. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Evan Anderson nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// Constraints that influence how deterministic the schedule generation process is. +public struct LitLeagues_Leagues_Determinism: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var technique: UInt32 { + get {return _technique ?? 0} + set {_technique = newValue} + } + /// Returns true if `technique` has been explicitly set. + public var hasTechnique: Bool {return self._technique != nil} + /// Clears the value of `technique`. Subsequent reads from it will return its default value. + public mutating func clearTechnique() {self._technique = nil} + + public var seed: UInt64 { + get {return _seed ?? 0} + set {_seed = newValue} + } + /// Returns true if `seed` has been explicitly set. + public var hasSeed: Bool {return self._seed != nil} + /// Clears the value of `seed`. Subsequent reads from it will return its default value. + public mutating func clearSeed() {self._seed = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _technique: UInt32? = nil + fileprivate var _seed: UInt64? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "lit_leagues.leagues" + +extension LitLeagues_Leagues_Determinism: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".Determinism" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}technique\0\u{1}seed\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self._technique) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self._seed) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._technique { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 1) + } }() + try { if let v = self._seed { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: LitLeagues_Leagues_Determinism, rhs: LitLeagues_Leagues_Determinism) -> Bool { + if lhs._technique != rhs._technique {return false} + if lhs._seed != rhs._seed {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/league-scheduling/generated/GenerationConstraints.pb.swift b/Sources/league-scheduling/generated/GenerationConstraints.pb.swift index 7c097e9..76e59a1 100644 --- a/Sources/league-scheduling/generated/GenerationConstraints.pb.swift +++ b/Sources/league-scheduling/generated/GenerationConstraints.pb.swift @@ -95,6 +95,16 @@ public struct LitLeagues_Leagues_GenerationConstraints: Sendable { /// Clears the value of `regenerationAttemptsThreshold`. Subsequent reads from it will return its default value. public mutating func clearRegenerationAttemptsThreshold() {self._regenerationAttemptsThreshold = nil} + /// Deterministic constraints. If not provided, the output is non-deterministic (heavily relies on randomness and probabilities). + public var determinism: LitLeagues_Leagues_Determinism { + get {return _determinism ?? LitLeagues_Leagues_Determinism()} + set {_determinism = newValue} + } + /// Returns true if `determinism` has been explicitly set. + public var hasDeterminism: Bool {return self._determinism != nil} + /// Clears the value of `determinism`. Subsequent reads from it will return its default value. + public mutating func clearDeterminism() {self._determinism = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -103,6 +113,7 @@ public struct LitLeagues_Leagues_GenerationConstraints: Sendable { fileprivate var _regenerationAttemptsForFirstDay: UInt32? = nil fileprivate var _regenerationAttemptsForConsecutiveDay: UInt32? = nil fileprivate var _regenerationAttemptsThreshold: UInt32? = nil + fileprivate var _determinism: LitLeagues_Leagues_Determinism? = nil } // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -111,7 +122,7 @@ fileprivate let _protobuf_package = "lit_leagues.leagues" extension LitLeagues_Leagues_GenerationConstraints: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".GenerationConstraints" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}timeoutDelay\0\u{1}regenerationAttemptsForFirstDay\0\u{1}regenerationAttemptsForConsecutiveDay\0\u{1}regenerationAttemptsThreshold\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}timeoutDelay\0\u{1}regenerationAttemptsForFirstDay\0\u{1}regenerationAttemptsForConsecutiveDay\0\u{1}regenerationAttemptsThreshold\0\u{1}determinism\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -123,6 +134,7 @@ extension LitLeagues_Leagues_GenerationConstraints: SwiftProtobuf.Message, Swift case 2: try { try decoder.decodeSingularUInt32Field(value: &self._regenerationAttemptsForFirstDay) }() case 3: try { try decoder.decodeSingularUInt32Field(value: &self._regenerationAttemptsForConsecutiveDay) }() case 4: try { try decoder.decodeSingularUInt32Field(value: &self._regenerationAttemptsThreshold) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._determinism) }() default: break } } @@ -145,6 +157,9 @@ extension LitLeagues_Leagues_GenerationConstraints: SwiftProtobuf.Message, Swift try { if let v = self._regenerationAttemptsThreshold { try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4) } }() + try { if let v = self._determinism { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -153,6 +168,7 @@ extension LitLeagues_Leagues_GenerationConstraints: SwiftProtobuf.Message, Swift if lhs._regenerationAttemptsForFirstDay != rhs._regenerationAttemptsForFirstDay {return false} if lhs._regenerationAttemptsForConsecutiveDay != rhs._regenerationAttemptsForConsecutiveDay {return false} if lhs._regenerationAttemptsThreshold != rhs._regenerationAttemptsThreshold {return false} + if lhs._determinism != rhs._determinism {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/league-scheduling/generated/codable/Determinism+Codable.swift b/Sources/league-scheduling/generated/codable/Determinism+Codable.swift new file mode 100644 index 0000000..87d8cd6 --- /dev/null +++ b/Sources/league-scheduling/generated/codable/Determinism+Codable.swift @@ -0,0 +1,29 @@ + +#if ProtobufCodable +extension LitLeagues_Leagues_Determinism: Codable { + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let v = try container.decodeIfPresent(UInt32.self, forKey: .technique) { + technique = v + } + if let v = try container.decodeIfPresent(UInt64.self, forKey: .seed) { + seed = v + } + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + if hasTechnique { + try container.encode(technique, forKey: .technique) + } + if hasSeed { + try container.encode(seed, forKey: .seed) + } + } + + enum CodingKeys: CodingKey { + case technique + case seed + } +} +#endif \ No newline at end of file diff --git a/Sources/league-scheduling/generated/codable/GenerationConstraints+Codable.swift b/Sources/league-scheduling/generated/codable/GenerationConstraints+Codable.swift index 7372fde..4d5852f 100644 --- a/Sources/league-scheduling/generated/codable/GenerationConstraints+Codable.swift +++ b/Sources/league-scheduling/generated/codable/GenerationConstraints+Codable.swift @@ -15,6 +15,9 @@ extension GenerationConstraints: Codable { if let v = try container.decodeIfPresent(UInt32.self, forKey: .regenerationAttemptsThreshold) { regenerationAttemptsThreshold = v } + if let v = try container.decodeIfPresent(LitLeagues_Leagues_Determinism.self, forKey: .determinism) { + determinism = v + } } public func encode(to encoder: any Encoder) throws { @@ -31,6 +34,9 @@ extension GenerationConstraints: Codable { if hasRegenerationAttemptsThreshold { try container.encode(regenerationAttemptsThreshold, forKey: .regenerationAttemptsThreshold) } + if hasDeterminism { + try container.encode(determinism, forKey: .determinism) + } } enum CodingKeys: CodingKey { @@ -38,6 +44,7 @@ extension GenerationConstraints: Codable { case regenerationAttemptsForFirstDay case regenerationAttemptsForConsecutiveDay case regenerationAttemptsThreshold + case determinism } } #endif \ No newline at end of file From a638f90bd80733f6ca929dabbf6dc06970f48d7a Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Thu, 19 Mar 2026 10:33:20 -0500 Subject: [PATCH 04/19] allow `UInt64.max` to be chosen as the seed --- Sources/league-scheduling/data/Generation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/league-scheduling/data/Generation.swift b/Sources/league-scheduling/data/Generation.swift index e3e36ae..1e64a3b 100644 --- a/Sources/league-scheduling/data/Generation.swift +++ b/Sources/league-scheduling/data/Generation.swift @@ -76,7 +76,7 @@ extension RequestPayload.Runtime { } switch constraints.determinism.technique { default: - let seed = constraints.determinism.hasSeed ? constraints.determinism.seed : UInt64.random(in: 0.. Date: Thu, 19 Mar 2026 11:53:13 -0500 Subject: [PATCH 05/19] avoid crash calculating `availableMatchupPairs`; unit test updates --- .../data/LeagueScheduleData.swift | 1 + .../extensions/Determinism+Extensions.swift | 14 ++++++++++++++ .../GenerationConstraints+Extensions.swift | 9 +++++++-- .../LeagueSchedulingTests/MatchupBlockTests.swift | 6 +++--- .../schedules/ScheduleBeanBagToss.swift | 6 +++--- .../expectations/ScheduleExpectations.swift | 13 ++++++++++++- .../schedules/util/ScheduleTestsProtocol.swift | 2 +- .../schedules/util/ValidLeagueMatchup.swift | 6 +++++- 8 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 Sources/league-scheduling/generated/extensions/Determinism+Extensions.swift diff --git a/Sources/league-scheduling/data/LeagueScheduleData.swift b/Sources/league-scheduling/data/LeagueScheduleData.swift index 0257572..31f7e1c 100644 --- a/Sources/league-scheduling/data/LeagueScheduleData.swift +++ b/Sources/league-scheduling/data/LeagueScheduleData.swift @@ -215,6 +215,7 @@ extension LeagueScheduleData { assignedEntryHomeAways: AssignedEntryHomeAways, maxSameOpponentMatchups: MaximumSameOpponentMatchups ) -> OrderedSet { + guard !entries.isEmpty else { return [] } // https://github.com/apple/swift-collections/issues/608 var pairs = OrderedSet(minimumCapacity: (entries.count-1) * 2) let sortedEntries = entries.sorted() diff --git a/Sources/league-scheduling/generated/extensions/Determinism+Extensions.swift b/Sources/league-scheduling/generated/extensions/Determinism+Extensions.swift new file mode 100644 index 0000000..b88595e --- /dev/null +++ b/Sources/league-scheduling/generated/extensions/Determinism+Extensions.swift @@ -0,0 +1,14 @@ + +extension LitLeagues_Leagues_Determinism { + init( + technique: UInt32? = nil, + seed: UInt64? = nil + ) { + if let technique { + self.technique = technique + } + if let seed { + self.seed = seed + } + } +} \ No newline at end of file diff --git a/Sources/league-scheduling/generated/extensions/GenerationConstraints+Extensions.swift b/Sources/league-scheduling/generated/extensions/GenerationConstraints+Extensions.swift index 162bd51..87468e2 100644 --- a/Sources/league-scheduling/generated/extensions/GenerationConstraints+Extensions.swift +++ b/Sources/league-scheduling/generated/extensions/GenerationConstraints+Extensions.swift @@ -5,12 +5,16 @@ extension GenerationConstraints { timeoutDelay: UInt32, regenerationAttemptsForFirstDay: UInt32, regenerationAttemptsForConsecutiveDay: UInt32, - regenerationAttemptsThreshold: UInt32 + regenerationAttemptsThreshold: UInt32, + determinism: LitLeagues_Leagues_Determinism? ) { self.timeoutDelay = timeoutDelay self.regenerationAttemptsForFirstDay = regenerationAttemptsForFirstDay self.regenerationAttemptsForConsecutiveDay = regenerationAttemptsForConsecutiveDay self.regenerationAttemptsThreshold = regenerationAttemptsThreshold + if let determinism { + self.determinism = determinism + } } } @@ -20,6 +24,7 @@ extension GenerationConstraints { timeoutDelay: 60, regenerationAttemptsForFirstDay: 100, regenerationAttemptsForConsecutiveDay: 100, - regenerationAttemptsThreshold: 10_000 + regenerationAttemptsThreshold: 10_000, + determinism: nil ) } \ No newline at end of file diff --git a/Tests/LeagueSchedulingTests/MatchupBlockTests.swift b/Tests/LeagueSchedulingTests/MatchupBlockTests.swift index 1121cfd..3dd7e93 100644 --- a/Tests/LeagueSchedulingTests/MatchupBlockTests.swift +++ b/Tests/LeagueSchedulingTests/MatchupBlockTests.swift @@ -35,12 +35,12 @@ extension MatchupBlockTests { #expect(adjacent == [3]) adjacent = calculateAdjacentTimes(for: 2, entryMatchupsPerGameDay: 3) - #expect(adjacent == [0, 1]) + #expect(adjacent == [1, 0]) adjacent = calculateAdjacentTimes(for: 2, entryMatchupsPerGameDay: 4) - #expect(adjacent == [0, 1, 3]) + #expect(adjacent == [1, 0, 3]) adjacent = calculateAdjacentTimes(for: 2, entryMatchupsPerGameDay: 5) - #expect(adjacent == [0, 1, 3, 4]) + #expect(adjacent == [1, 0, 3, 4]) } } \ No newline at end of file diff --git a/Tests/LeagueSchedulingTests/schedules/ScheduleBeanBagToss.swift b/Tests/LeagueSchedulingTests/schedules/ScheduleBeanBagToss.swift index 97b0ebf..d4d07c2 100644 --- a/Tests/LeagueSchedulingTests/schedules/ScheduleBeanBagToss.swift +++ b/Tests/LeagueSchedulingTests/schedules/ScheduleBeanBagToss.swift @@ -17,7 +17,7 @@ struct ScheduleBeanBagToss: ScheduleTestsProtocol { ) } static func schedule8GameDays3Times3Locations1Division9Teams( - constraints: GenerationConstraints = .default + constraints: GenerationConstraints = .unitTestDefault ) throws -> RequestPayload.Runtime { let maxEntryMatchupsPerGameDay:EntryMatchupsPerGameDay = 2 let (gameDays, times, locations, teams):(DayIndex, TimeIndex, LocationIndex, Int) = (8, 3, 3, 9) @@ -397,7 +397,7 @@ extension ScheduleBeanBagToss { try expectations(settings: schedule, matchupsCount: 210, data: data) } static func scheduleBeanBagToss_10GameDays4Time8Locations1Division21Teams( - constraints: GenerationConstraints = .default + constraints: GenerationConstraints = .unitTestDefault ) throws -> RequestPayload.Runtime { let maxEntryMatchupsPerGameDay:EntryMatchupsPerGameDay = 2 let (gameDays, times, locations, teams):(DayIndex, TimeIndex, LocationIndex, Int) = (10, 4, 8, 21) @@ -443,7 +443,7 @@ extension ScheduleBeanBagToss { try expectations(settings: schedule, matchupsCount: 230, data: data) } static func scheduleBeanBagToss_10GameDays4Times6Locations2Division23Teams( - constraints: GenerationConstraints = .default + constraints: GenerationConstraints = .unitTestDefault ) throws -> RequestPayload.Runtime { let maxEntryMatchupsPerGameDay:EntryMatchupsPerGameDay = 2 let (gameDays, times, locations, teams):(DayIndex, TimeIndex, LocationIndex, Int) = (10, 4, 6, 23) diff --git a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift index d755e41..c29befc 100644 --- a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift +++ b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift @@ -6,6 +6,16 @@ import Testing protocol ScheduleExpectations: Sendable { } +extension GenerationConstraints { + static let unitTestDefault = Self( + timeoutDelay: Self.default.timeoutDelay, + regenerationAttemptsForFirstDay: Self.default.regenerationAttemptsForFirstDay, + regenerationAttemptsForConsecutiveDay: Self.default.regenerationAttemptsForConsecutiveDay, + regenerationAttemptsThreshold: Self.default.regenerationAttemptsThreshold, + determinism: .init(seed: 69_420) + ) +} + // MARK: Expectations extension ScheduleExpectations { func expectations( @@ -14,8 +24,9 @@ extension ScheduleExpectations { data: LeagueGenerationResult ) throws { guard !Task.isCancelled else { return } + let determinism = settings.constraints.determinism let regenerationAttempts:String = data.results.map { - "assignLocationTimeRegenerationAttempts=\($0.assignLocationTimeRegenerationAttempts);negativeDayIndexRegenerationAttempts=\($0.negativeDayIndexRegenerationAttempts)" + "hasDeterminism=\(settings.constraints.hasDeterminism),technique=\(determinism.technique),seed=\(determinism.seed);assignLocationTimeRegenerationAttempts=\($0.assignLocationTimeRegenerationAttempts);negativeDayIndexRegenerationAttempts=\($0.negativeDayIndexRegenerationAttempts)" }.joined(separator: "\n") if false { for result in data.results { diff --git a/Tests/LeagueSchedulingTests/schedules/util/ScheduleTestsProtocol.swift b/Tests/LeagueSchedulingTests/schedules/util/ScheduleTestsProtocol.swift index be33b2a..6e6f132 100644 --- a/Tests/LeagueSchedulingTests/schedules/util/ScheduleTestsProtocol.swift +++ b/Tests/LeagueSchedulingTests/schedules/util/ScheduleTestsProtocol.swift @@ -86,7 +86,7 @@ extension ScheduleTestsProtocol { divisionsCanPlayOnSameDay: Bool = true, divisionsCanPlayAtSameTime: Bool = true, entries: [Entry.Runtime], - constraints: GenerationConstraints = .default + constraints: GenerationConstraints = .unitTestDefault ) -> RequestPayload.Runtime { let correctMaximumPlayableMatchups = RequestPayload.calculateMaximumPlayableMatchups( gameDays: gameDays, diff --git a/Tests/LeagueSchedulingTests/schedules/util/ValidLeagueMatchup.swift b/Tests/LeagueSchedulingTests/schedules/util/ValidLeagueMatchup.swift index a9c921f..59cf459 100644 --- a/Tests/LeagueSchedulingTests/schedules/util/ValidLeagueMatchup.swift +++ b/Tests/LeagueSchedulingTests/schedules/util/ValidLeagueMatchup.swift @@ -1,7 +1,11 @@ @testable import LeagueScheduling -struct ValidLeagueMatchup: Hashable { +struct ValidLeagueMatchup: CustomStringConvertible, Hashable { let day:DayIndex let matchup:Matchup + + var description: String { + "ValidLeagueMatchup(day: \(day), matchup: \(matchup.description))" + } } \ No newline at end of file From 5ec79323ad2a8f2921ed80c6bebf7f34c46bc530 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Thu, 19 Mar 2026 12:40:02 -0500 Subject: [PATCH 06/19] allow custom `multiplier` and `increment` determinism values and... - fallback to `1` for the seed if none is provided --- Sources/ProtocolBuffers/Determinism.proto | 2 ++ .../league-scheduling/data/Generation.swift | 10 ++++-- .../generated/Determinism.pb.swift | 32 ++++++++++++++++++- .../codable/Determinism+Codable.swift | 14 ++++++++ .../extensions/Determinism+Extensions.swift | 10 +++++- Sources/league-scheduling/util/LCG.swift | 4 +-- 6 files changed, 66 insertions(+), 6 deletions(-) diff --git a/Sources/ProtocolBuffers/Determinism.proto b/Sources/ProtocolBuffers/Determinism.proto index 30dfd90..86f919d 100644 --- a/Sources/ProtocolBuffers/Determinism.proto +++ b/Sources/ProtocolBuffers/Determinism.proto @@ -34,4 +34,6 @@ package lit_leagues.leagues; message Determinism { optional uint32 technique = 1; optional uint64 seed = 2; + optional uint64 multiplier = 3; + optional uint64 increment = 4; } \ No newline at end of file diff --git a/Sources/league-scheduling/data/Generation.swift b/Sources/league-scheduling/data/Generation.swift index 1e64a3b..d7bfe6e 100644 --- a/Sources/league-scheduling/data/Generation.swift +++ b/Sources/league-scheduling/data/Generation.swift @@ -76,14 +76,20 @@ extension RequestPayload.Runtime { } switch constraints.determinism.technique { default: - let seed = constraints.determinism.hasSeed ? constraints.determinism.seed : UInt64.random(in: 0...UInt64.max) + let seed = constraints.determinism.hasSeed ? constraints.determinism.seed : 1 + let multiplier = constraints.determinism.hasMultiplier ? constraints.determinism.multiplier : 6364136223846793005 + let increment = constraints.determinism.hasIncrement ? constraints.determinism.increment : 1442695040888963407 return try await generateSchedules( divisionsCount: divisionsCount, divisionEntries: divisionEntries, maxStartingTimes: maxStartingTimes, maxLocations: maxLocations, maxSameOpponentMatchups: maxSameOpponentMatchups, - rng: LCG(seed: seed) + rng: LCG( + seed: seed, + multiplier: multiplier, + increment: increment + ) ) } } diff --git a/Sources/league-scheduling/generated/Determinism.pb.swift b/Sources/league-scheduling/generated/Determinism.pb.swift index fc655dd..f43db74 100644 --- a/Sources/league-scheduling/generated/Determinism.pb.swift +++ b/Sources/league-scheduling/generated/Determinism.pb.swift @@ -72,12 +72,32 @@ public struct LitLeagues_Leagues_Determinism: Sendable { /// Clears the value of `seed`. Subsequent reads from it will return its default value. public mutating func clearSeed() {self._seed = nil} + public var multiplier: UInt64 { + get {return _multiplier ?? 0} + set {_multiplier = newValue} + } + /// Returns true if `multiplier` has been explicitly set. + public var hasMultiplier: Bool {return self._multiplier != nil} + /// Clears the value of `multiplier`. Subsequent reads from it will return its default value. + public mutating func clearMultiplier() {self._multiplier = nil} + + public var increment: UInt64 { + get {return _increment ?? 0} + set {_increment = newValue} + } + /// Returns true if `increment` has been explicitly set. + public var hasIncrement: Bool {return self._increment != nil} + /// Clears the value of `increment`. Subsequent reads from it will return its default value. + public mutating func clearIncrement() {self._increment = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} fileprivate var _technique: UInt32? = nil fileprivate var _seed: UInt64? = nil + fileprivate var _multiplier: UInt64? = nil + fileprivate var _increment: UInt64? = nil } // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -86,7 +106,7 @@ fileprivate let _protobuf_package = "lit_leagues.leagues" extension LitLeagues_Leagues_Determinism: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Determinism" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}technique\0\u{1}seed\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}technique\0\u{1}seed\0\u{1}multiplier\0\u{1}increment\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -96,6 +116,8 @@ extension LitLeagues_Leagues_Determinism: SwiftProtobuf.Message, SwiftProtobuf._ switch fieldNumber { case 1: try { try decoder.decodeSingularUInt32Field(value: &self._technique) }() case 2: try { try decoder.decodeSingularUInt64Field(value: &self._seed) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self._multiplier) }() + case 4: try { try decoder.decodeSingularUInt64Field(value: &self._increment) }() default: break } } @@ -112,12 +134,20 @@ extension LitLeagues_Leagues_Determinism: SwiftProtobuf.Message, SwiftProtobuf._ try { if let v = self._seed { try visitor.visitSingularUInt64Field(value: v, fieldNumber: 2) } }() + try { if let v = self._multiplier { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 3) + } }() + try { if let v = self._increment { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 4) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: LitLeagues_Leagues_Determinism, rhs: LitLeagues_Leagues_Determinism) -> Bool { if lhs._technique != rhs._technique {return false} if lhs._seed != rhs._seed {return false} + if lhs._multiplier != rhs._multiplier {return false} + if lhs._increment != rhs._increment {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/league-scheduling/generated/codable/Determinism+Codable.swift b/Sources/league-scheduling/generated/codable/Determinism+Codable.swift index 87d8cd6..8c9dd07 100644 --- a/Sources/league-scheduling/generated/codable/Determinism+Codable.swift +++ b/Sources/league-scheduling/generated/codable/Determinism+Codable.swift @@ -9,6 +9,12 @@ extension LitLeagues_Leagues_Determinism: Codable { if let v = try container.decodeIfPresent(UInt64.self, forKey: .seed) { seed = v } + if let v = try container.decodeIfPresent(UInt64.self, forKey: .multiplier) { + multiplier = v + } + if let v = try container.decodeIfPresent(UInt64.self, forKey: .increment) { + increment = v + } } public func encode(to encoder: any Encoder) throws { @@ -19,11 +25,19 @@ extension LitLeagues_Leagues_Determinism: Codable { if hasSeed { try container.encode(seed, forKey: .seed) } + if hasMultiplier { + try container.encode(multiplier, forKey: .multiplier) + } + if hasIncrement { + try container.encode(increment, forKey: .increment) + } } enum CodingKeys: CodingKey { case technique case seed + case multiplier + case increment } } #endif \ No newline at end of file diff --git a/Sources/league-scheduling/generated/extensions/Determinism+Extensions.swift b/Sources/league-scheduling/generated/extensions/Determinism+Extensions.swift index b88595e..b723e28 100644 --- a/Sources/league-scheduling/generated/extensions/Determinism+Extensions.swift +++ b/Sources/league-scheduling/generated/extensions/Determinism+Extensions.swift @@ -2,7 +2,9 @@ extension LitLeagues_Leagues_Determinism { init( technique: UInt32? = nil, - seed: UInt64? = nil + seed: UInt64? = nil, + multiplier: UInt64? = nil, + increment: UInt64? = nil ) { if let technique { self.technique = technique @@ -10,5 +12,11 @@ extension LitLeagues_Leagues_Determinism { if let seed { self.seed = seed } + if let multiplier { + self.multiplier = multiplier + } + if let increment { + self.increment = increment + } } } \ No newline at end of file diff --git a/Sources/league-scheduling/util/LCG.swift b/Sources/league-scheduling/util/LCG.swift index 043272d..0938870 100644 --- a/Sources/league-scheduling/util/LCG.swift +++ b/Sources/league-scheduling/util/LCG.swift @@ -7,8 +7,8 @@ struct LCG: RandomNumberGenerator, Sendable { init( seed: UInt64, - multiplier: UInt64 = 6364136223846793005, - increment: UInt64 = 1442695040888963407 + multiplier: UInt64, + increment: UInt64 ) { self.state = seed == 0 ? 1 : seed self.multiplier = multiplier From ebb2f7f2d86aa12ef526b37b5504a2a1bfcf2087 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Thu, 19 Mar 2026 20:25:04 -0500 Subject: [PATCH 07/19] support both determinism pathways (deterministic & non-deterministic) --- .../data/AssignMatchup.swift | 12 +- .../league-scheduling/data/AssignSlots.swift | 28 +-- .../data/AssignmentState.swift | 40 +-- .../data/BalanceHomeAway.swift | 14 +- .../league-scheduling/data/Generation.swift | 117 ++++----- .../data/LeagueScheduleData.swift | 84 ++----- .../data/LeagueScheduleDataSnapshot.swift | 33 ++- .../league-scheduling/data/MatchupBlock.swift | 57 ++--- .../data/PrioritizedMatchups.swift | 24 +- .../data/RedistributionData.swift | 10 +- .../data/RemainingAllocations.swift | 12 +- .../data/SelectMatchup.swift | 237 +++++++++--------- Sources/league-scheduling/data/Shuffle.swift | 10 +- .../data/assignment/Assign.swift | 18 +- .../data/assignment/Move.swift | 4 +- .../data/assignment/Unassign.swift | 14 +- .../ScheduleConfiguration.swift | 24 ++ .../data/canPlayAt/CanPlayAt+GameGap.swift | 4 +- .../data/canPlayAt/CanPlayAtNormal.swift | 8 +- .../data/canPlayAt/CanPlayAtProtocol.swift | 4 +- .../CanPlayAtSameLocationIfB2B.swift | 6 +- .../CanPlayAtWithTravelDurations.swift | 8 +- .../data/selectSlot/SelectSlotB2B.swift | 12 +- .../selectSlot/SelectSlotEarliestTime.swift | 12 +- ...SlotEarliestTimeAndSameLocationIfB2B.swift | 8 +- .../data/selectSlot/SelectSlotNormal.swift | 26 +- .../data/selectSlot/SelectSlotProtocol.swift | 6 +- .../extensions/OrderedSet+AbstractSet.swift | 46 ++++ .../extensions/Set+AbstractSet.swift | 39 +++ Sources/league-scheduling/globals.swift | 12 +- Sources/league-scheduling/typealiases.swift | 17 +- .../util/array/AbstractArray.swift | 12 + .../util/array/PlaysAtTimesArray.swift | 22 ++ .../util/set/AbstractSet.swift | 61 +++++ .../util/set/SetOfAvailableSlots.swift | 8 + .../util/set/SetOfEntryIDs.swift | 60 +++++ .../util/set/SetOfMatchupPair.swift | 8 + .../util/set/SetOfUInt32.swift | 11 + .../CanPlayAtTests.swift | 12 +- .../MatchupBlockTests.swift | 3 +- 40 files changed, 697 insertions(+), 446 deletions(-) create mode 100644 Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift create mode 100644 Sources/league-scheduling/extensions/OrderedSet+AbstractSet.swift create mode 100644 Sources/league-scheduling/extensions/Set+AbstractSet.swift create mode 100644 Sources/league-scheduling/util/array/AbstractArray.swift create mode 100644 Sources/league-scheduling/util/array/PlaysAtTimesArray.swift create mode 100644 Sources/league-scheduling/util/set/AbstractSet.swift create mode 100644 Sources/league-scheduling/util/set/SetOfAvailableSlots.swift create mode 100644 Sources/league-scheduling/util/set/SetOfEntryIDs.swift create mode 100644 Sources/league-scheduling/util/set/SetOfMatchupPair.swift create mode 100644 Sources/league-scheduling/util/set/SetOfUInt32.swift diff --git a/Sources/league-scheduling/data/AssignMatchup.swift b/Sources/league-scheduling/data/AssignMatchup.swift index 7579dcd..b6aae84 100644 --- a/Sources/league-scheduling/data/AssignMatchup.swift +++ b/Sources/league-scheduling/data/AssignMatchup.swift @@ -7,7 +7,7 @@ extension LeagueScheduleData { @discardableResult mutating func assignMatchupPair( _ pair: MatchupPair, - allAvailableMatchups: OrderedSet, + allAvailableMatchups: Config.DeterministicMatchupPairSet, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { @@ -36,7 +36,7 @@ extension AssignmentState { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: OrderedSet, + allAvailableMatchups: Config.DeterministicMatchupPairSet, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { @@ -86,20 +86,20 @@ extension AssignmentState { #if LOG print("assignMatchupPair;pair=\(pair.description);slot==nil, removing pair from availableMatchups") #endif - availableMatchups.remove(pair) + availableMatchups.removeMember(pair) return nil } } // MARK: Playable slots extension AssignmentState { - func playableSlots(for pair: MatchupPair) -> OrderedSet { + func playableSlots(for pair: MatchupPair) -> Config.DeterministicAvailableSlotSet { return Self.playableSlots(for: pair, remainingAllocations: remainingAllocations) } static func playableSlots( for pair: MatchupPair, - remainingAllocations: RemainingAllocations - ) -> OrderedSet { + remainingAllocations: Config.RemainingAllocations + ) -> Config.DeterministicAvailableSlotSet { return remainingAllocations[unchecked: pair.team1].intersection(remainingAllocations[unchecked: pair.team2]) } } \ No newline at end of file diff --git a/Sources/league-scheduling/data/AssignSlots.swift b/Sources/league-scheduling/data/AssignSlots.swift index 73ebf1f..ff95cdf 100644 --- a/Sources/league-scheduling/data/AssignSlots.swift +++ b/Sources/league-scheduling/data/AssignSlots.swift @@ -55,7 +55,7 @@ extension LeagueScheduleData { var assignmentIndex = 0 var fms = failedMatchupSelections[unchecked: assignmentIndex] var optimalAvailableMatchups = assignmentState.availableMatchups.filter { !fms.contains($0) } - var prioritizedMatchups = PrioritizedMatchups( + var prioritizedMatchups = PrioritizedMatchups( entriesCount: entriesCount, prioritizedEntries: assignmentState.prioritizedEntries, availableMatchups: optimalAvailableMatchups @@ -91,7 +91,7 @@ extension LeagueScheduleData { // failed to assign matchup, skip it for now failedMatchupSelections[unchecked: assignmentIndex].insert(originalPair) prioritizedMatchups.remove(originalPair) - assignmentState.availableMatchups.remove(originalPair) + assignmentState.availableMatchups.removeMember(originalPair) continue } // successfully assigned pair @@ -119,7 +119,7 @@ extension LeagueScheduleData { availableMatchups: optimalAvailableMatchups ) } - assignmentState.availableMatchups.remove(originalPair) + assignmentState.availableMatchups.removeMember(originalPair) } return assignmentState.matchups.count == expectedMatchupsCount } @@ -147,10 +147,10 @@ extension LeagueScheduleData { let division = Division.IDValue(divisionIndex) let divisionMatchups = assignmentState.allDivisionMatchups[unchecked: division] assignmentState.availableMatchups = divisionMatchups - assignmentState.prioritizedEntries.removeAll(keepingCapacity: true) - for matchup in assignmentState.availableMatchups { - assignmentState.prioritizedEntries.append(matchup.team1) - assignmentState.prioritizedEntries.append(matchup.team2) + assignmentState.prioritizedEntries.removeAllKeepingCapacity() + assignmentState.availableMatchups.forEach { matchup in + assignmentState.prioritizedEntries.insertMember(matchup.team1) + assignmentState.prioritizedEntries.insertMember(matchup.team2) } assignmentState.recalculateAllRemainingAllocations( day: day, @@ -247,15 +247,15 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: OrderedSet, + allAvailableMatchups: Config.DeterministicMatchupPairSet, rng: inout some RandomNumberGenerator, - assignmentState: inout AssignmentState, + assignmentState: inout AssignmentState, shouldSkipSelection: (MatchupPair) -> Bool, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { var pair:MatchupPair? = nil - var prioritizedMatchups = PrioritizedMatchups( + var prioritizedMatchups = PrioritizedMatchups( entriesCount: entriesCount, prioritizedEntries: assignmentState.prioritizedEntries, availableMatchups: assignmentState.availableMatchups @@ -267,7 +267,7 @@ extension LeagueScheduleData { prioritizedMatchups.update(prioritizedEntries: assignmentState.prioritizedEntries, availableMatchups: assignmentState.availableMatchups) } else { prioritizedMatchups.remove(selected) - assignmentState.availableMatchups.remove(selected) + assignmentState.availableMatchups.removeMember(selected) } } guard var pair else { return nil } @@ -298,14 +298,14 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: OrderedSet, + allAvailableMatchups: Config.DeterministicMatchupPairSet, rng: inout some RandomNumberGenerator, - assignmentState: inout AssignmentState, + assignmentState: inout AssignmentState, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { var pair:MatchupPair? = nil - var prioritizedMatchups = PrioritizedMatchups( + var prioritizedMatchups = PrioritizedMatchups( entriesCount: entriesCount, prioritizedEntries: assignmentState.prioritizedEntries, availableMatchups: assignmentState.availableMatchups diff --git a/Sources/league-scheduling/data/AssignmentState.swift b/Sources/league-scheduling/data/AssignmentState.swift index 76c3987..405b16c 100644 --- a/Sources/league-scheduling/data/AssignmentState.swift +++ b/Sources/league-scheduling/data/AssignmentState.swift @@ -3,7 +3,7 @@ import OrderedCollections import StaticDateTimes // MARK: Noncopyable -struct AssignmentState: Sendable, ~Copyable { +struct AssignmentState: Sendable, ~Copyable { let entries:[Entry.Runtime] var startingTimes:[StaticTime] var matchupDuration:MatchupDuration @@ -15,7 +15,7 @@ struct AssignmentState: Sendable, ~Copyable { /// Remaining allocations allowed for a matchup pair, for a `DayIndex`. /// /// - Usage: [`Entry.IDValue`: `the number of remaining allocations`] - var remainingAllocations:RemainingAllocations + var remainingAllocations:Config.RemainingAllocations /// When entries can play against each other again. /// @@ -46,23 +46,23 @@ struct AssignmentState: Sendable, ~Copyable { let maxSameOpponentMatchups:MaximumSameOpponentMatchups /// All matchup pairs that can be scheduled. - var allMatchups:OrderedSet + var allMatchups:Config.DeterministicMatchupPairSet /// All matchup pairs that can be scheduled, grouped by division. /// /// - Usage: [`Division.IDValue`: `available matchups`] - var allDivisionMatchups:ContiguousArray> + var allDivisionMatchups:ContiguousArray /// Remaining available matchup pairs that can be assigned for the `day`. - var availableMatchups:OrderedSet + var availableMatchups:Config.DeterministicMatchupPairSet - var prioritizedEntries:OrderedSet + var prioritizedEntries:Config.DeterministicEntryIDSet /// Remaining available slots that can be filled for the `day`. - var availableSlots:OrderedSet + var availableSlots:Config.DeterministicAvailableSlotSet - var playsAt:PlaysAt - var playsAtTimes:PlaysAtTimes + var playsAt:Config.PlaysAt + var playsAtTimes:PlaysAtTimesArray var playsAtLocations:PlaysAtLocations /// Available matchups that can be scheduled. @@ -70,7 +70,7 @@ struct AssignmentState: Sendable, ~Copyable { var shuffleHistory = [LeagueShuffleAction]() - func copyable() -> AssignmentStateCopyable { + func copyable() -> AssignmentStateCopyable { return .init( entries: entries, startingTimes: startingTimes, @@ -134,7 +134,7 @@ struct AssignmentState: Sendable, ~Copyable { } // MARK: Copyable -struct AssignmentStateCopyable { +struct AssignmentStateCopyable { let entries:[Entry.Runtime] let startingTimes:[StaticTime] let matchupDuration:MatchupDuration @@ -142,7 +142,7 @@ struct AssignmentStateCopyable { /// - Usage: [`Entry.IDValue`: `total number of matchups played so far in the schedule`] var numberOfAssignedMatchups:[Int] - var remainingAllocations:RemainingAllocations + var remainingAllocations:Config.RemainingAllocations var recurringDayLimits:RecurringDayLimits var assignedTimes:AssignedTimes var assignedLocations:AssignedLocations @@ -165,29 +165,29 @@ struct AssignmentStateCopyable { var maxSameOpponentMatchups:MaximumSameOpponentMatchups /// All matchup pairs that can be scheduled - var allMatchups:OrderedSet + var allMatchups:Config.DeterministicMatchupPairSet /// All matchup pairs that can be scheduled, grouped by division. /// /// - Usage: [`Division.IDValue`: `available matchups`] - var allDivisionMatchups:ContiguousArray> + var allDivisionMatchups:ContiguousArray /// Remaining available matchup pairs that can be assigned for the `day`. - var availableMatchups:OrderedSet + var availableMatchups:Config.DeterministicMatchupPairSet - var prioritizedEntries:OrderedSet + var prioritizedEntries:Config.DeterministicEntryIDSet /// Remaining available slots that can be filled for the `day`. - var availableSlots:OrderedSet + var availableSlots:Config.DeterministicAvailableSlotSet - var playsAt:PlaysAt - var playsAtTimes:PlaysAtTimes + var playsAt:Config.PlaysAt + var playsAtTimes:PlaysAtTimesArray var playsAtLocations:PlaysAtLocations var matchups:OrderedSet var shuffleHistory:[LeagueShuffleAction] - func noncopyable() -> AssignmentState { + func noncopyable() -> AssignmentState { return .init( entries: entries, startingTimes: startingTimes, diff --git a/Sources/league-scheduling/data/BalanceHomeAway.swift b/Sources/league-scheduling/data/BalanceHomeAway.swift index 11c7486..b618d02 100644 --- a/Sources/league-scheduling/data/BalanceHomeAway.swift +++ b/Sources/league-scheduling/data/BalanceHomeAway.swift @@ -4,9 +4,9 @@ import OrderedCollections // MARK: Matchup pair extension MatchupPair { /// Balances home/away allocations, mutating `team1` (home) and `team2` (away) if necessary. - mutating func balanceHomeAway( + mutating func balanceHomeAway( rng: inout some RandomNumberGenerator, - assignmentState: borrowing AssignmentState + assignmentState: borrowing AssignmentState ) { let team1GamesPlayedAgainstTeam2 = assignmentState.assignedEntryHomeAways[unchecked: team1][unchecked: team2] // TODO: fix; more/less opponents than game days can make this unbalanced @@ -56,7 +56,7 @@ extension LeagueScheduleData { #endif let now = clock.now - var unbalancedEntryIDs = OrderedSet() + var unbalancedEntryIDs = Config.DeterministicEntryIDSet() unbalancedEntryIDs.reserveCapacity(entriesCount) var neededFlipsToBalance = [(home: UInt8, away: UInt8)](repeating: (0, 0), count: entriesCount) for entryID in 0.. balanceNumber { neededFlipsToBalance[unchecked: entryID].home = home - balanceNumber @@ -106,14 +106,14 @@ extension LeagueScheduleData { flippable.remove(flipped) flipHomeAway(matchup: &flipped, neededFlipsToBalance: &neededFlipsToBalance, generationData: &generationData) if neededFlipsToBalance[unchecked: flipped.matchup.home] == (0, 0) { - unbalancedEntryIDs.remove(flipped.matchup.home) + unbalancedEntryIDs.removeMember(flipped.matchup.home) } if neededFlipsToBalance[unchecked: flipped.matchup.away] == (0, 0) { - unbalancedEntryIDs.remove(flipped.matchup.away) + unbalancedEntryIDs.removeMember(flipped.matchup.away) } } else { // TODO: improve? for now we can just skip it - unbalancedEntryIDs.remove(entryID) + unbalancedEntryIDs.removeMember(entryID) } } diff --git a/Sources/league-scheduling/data/Generation.swift b/Sources/league-scheduling/data/Generation.swift index d7bfe6e..40a7179 100644 --- a/Sources/league-scheduling/data/Generation.swift +++ b/Sources/league-scheduling/data/Generation.swift @@ -37,14 +37,9 @@ extension RequestPayload.Runtime { // MARK: Generate schedules extension RequestPayload.Runtime { private func generateSchedules() async throws -> [LeagueGenerationData] { - let divisionsCount = divisions.count - var divisionEntries:ContiguousArray> = .init(repeating: OrderedSet(), count: divisionsCount) #if LOG - print("LeagueSchedule;generateSchedules;divisionsCount=\(divisionsCount);entries.count=\(entries.count)") + print("LeagueSchedule;generateSchedules;entries.count=\(entries.count)") #endif - for entryIndex in 0.., + Set, + Set, + Set + >.self ) } switch constraints.determinism.technique { @@ -80,28 +72,40 @@ extension RequestPayload.Runtime { let multiplier = constraints.determinism.hasMultiplier ? constraints.determinism.multiplier : 6364136223846793005 let increment = constraints.determinism.hasIncrement ? constraints.determinism.increment : 1442695040888963407 return try await generateSchedules( - divisionsCount: divisionsCount, - divisionEntries: divisionEntries, maxStartingTimes: maxStartingTimes, maxLocations: maxLocations, - maxSameOpponentMatchups: maxSameOpponentMatchups, rng: LCG( seed: seed, multiplier: multiplier, increment: increment - ) + ), + ScheduleConfig< + LCG, + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet + >.self ) } } - private func generateSchedules( - divisionsCount: Int, - divisionEntries: ContiguousArray>, + private func generateSchedules( maxStartingTimes: TimeIndex, maxLocations: LocationIndex, - maxSameOpponentMatchups: MaximumSameOpponentMatchups, - rng: RNG + rng: Config.RNG, + _ config: Config.Type ) async throws -> [LeagueGenerationData] { - let dataSnapshot = LeagueScheduleDataSnapshot( + var divisionEntries:ContiguousArray = .init(repeating: .init(), count: divisions.count) + for entryIndex in 0.. = .init( rng: rng, maxStartingTimes: maxStartingTimes, startingTimes: general.startingTimes, @@ -117,23 +121,23 @@ extension RequestPayload.Runtime { maxSameOpponentMatchups: maxSameOpponentMatchups ) return try await generateDivisionSchedulesInParallel( - divisionsCount: divisionsCount, + divisionsCount: divisions.count, divisionEntries: divisionEntries, maxStartingTimes: maxStartingTimes, maxLocations: maxLocations, dataSnapshot: dataSnapshot ) } - private func generateDivisionSchedulesInParallel( + private func generateDivisionSchedulesInParallel( divisionsCount: Int, - divisionEntries: ContiguousArray>, + divisionEntries: ContiguousArray, maxStartingTimes: TimeIndex, maxLocations: LocationIndex, - dataSnapshot: LeagueScheduleDataSnapshot + dataSnapshot: LeagueScheduleDataSnapshot ) async throws -> [LeagueGenerationData] { - var grouped = [DayOfWeek:Set]() + var grouped = [DayOfWeek:Config.DeterministicEntryIDSet]() for (divisionID, division) in divisions.enumerated() { - grouped[DayOfWeek(division.dayOfWeek), default: []].formUnion(divisionEntries[divisionID]) + grouped[DayOfWeek(division.dayOfWeek), default: .init()].formUnion(divisionEntries[divisionID]) } guard constraints.timeoutDelay > 0 else { return await withTaskGroup { group in @@ -222,14 +226,14 @@ extension RequestPayload.Runtime { // MARK: Generate schedule extension RequestPayload.Runtime { - private static func generateSchedule( + private static func generateSchedule( dayOfWeek: DayOfWeek, settings: RequestPayload.Runtime, - dataSnapshot: LeagueScheduleDataSnapshot, + dataSnapshot: LeagueScheduleDataSnapshot, divisionsCount: Int, maxStartingTimes: TimeIndex, maxLocations: LocationIndex, - scheduledEntries: Set + scheduledEntries: Config.DeterministicEntryIDSet ) -> LeagueGenerationData { let gameDays = settings.gameDays var generationData = LeagueGenerationData() @@ -238,7 +242,7 @@ extension RequestPayload.Runtime { generationData.schedule = .init(repeating: OrderedSet(), count: gameDays) var dataSnapshot = copy dataSnapshot - var gameDayDivisionEntries:ContiguousArray>> = .init(repeating: .init(repeating: Set(), count: divisionsCount), count: gameDays) + var gameDayDivisionEntries:ContiguousArray> = .init(repeating: .init(repeating: .init(), count: divisionsCount), count: gameDays) loadMaxAllocations( dataSnapshot: &dataSnapshot, gameDayDivisionEntries: &gameDayDivisionEntries, @@ -248,7 +252,7 @@ extension RequestPayload.Runtime { scheduledEntries: scheduledEntries ) - var snapshots = [LeagueScheduleDataSnapshot]() + var snapshots = [LeagueScheduleDataSnapshot]() snapshots.reserveCapacity(gameDays) var gameDayRegenerationAttempt:UInt32 = 0 var day:DayIndex = 0 @@ -258,7 +262,7 @@ extension RequestPayload.Runtime { if gameDaySettingValuesCount <= day { gameDaySettingValuesCount += 1 let daySettings = settings.daySettings[unchecked: day].general - let availableSlots = Self.availableSlots( + let availableSlots:Config.DeterministicAvailableSlotSet = Self.availableSlots( times: daySettings.timeSlots, locations: daySettings.locations, locationTimeExclusivity: daySettings.locationTimeExclusivities @@ -340,9 +344,9 @@ extension RequestPayload.Runtime { finalizeGenerationData(generationData: &generationData, data: data) return generationData } - private static func finalizeGenerationData( + private static func finalizeGenerationData( generationData: inout LeagueGenerationData, - data: borrowing LeagueScheduleData + data: borrowing LeagueScheduleData ) { generationData.executionSteps = data.executionSteps generationData.shuffleHistory = data.shuffleHistory @@ -351,15 +355,15 @@ extension RequestPayload.Runtime { // MARK: Load max allocations extension RequestPayload.Runtime { - static func loadMaxAllocations( - dataSnapshot: inout LeagueScheduleDataSnapshot, - gameDayDivisionEntries: inout ContiguousArray>>, + static func loadMaxAllocations( + dataSnapshot: inout LeagueScheduleDataSnapshot, + gameDayDivisionEntries: inout ContiguousArray>, settings: borrowing RequestPayload.Runtime, maxStartingTimes: TimeIndex, maxLocations: LocationIndex, - scheduledEntries: Set + scheduledEntries: Config.DeterministicEntryIDSet ) { - for entryIndex in scheduledEntries { + scheduledEntries.forEach { entryIndex in let entry = settings.entries[unchecked: entryIndex] var maxPossiblePlayed:EntryMatchupsPerGameDay = 0 var maxStartingTimesPlayedAt = 0 @@ -393,7 +397,7 @@ extension RequestPayload.Runtime { } } maxLocationsPlayedAt = max(maxLocationsPlayedAt, playable) - gameDayDivisionEntries[unchecked: day][unchecked: entry.division].insert(entry.id) + gameDayDivisionEntries[unchecked: day][unchecked: entry.division].insertMember(entry.id) } maxStartingTimesPlayedAt = max(maxStartingTimesPlayedAt, 1) maxLocationsPlayedAt = max(maxLocationsPlayedAt, 1) @@ -467,19 +471,20 @@ extension RequestPayload.Runtime { // MARK: Get available slots extension RequestPayload.Runtime { - static func availableSlots( + static func availableSlots( times: TimeIndex, locations: LocationIndex, locationTimeExclusivity: [Set]? - ) -> OrderedSet { - var slots = OrderedSet(minimumCapacity: Int(times) * locations) + ) -> DeterministicAvailableSlotSet { + var slots = DeterministicAvailableSlotSet() + slots.reserveCapacity(Int(times) * locations) if let exclusivities = locationTimeExclusivity { for location in 0..( gameDays: DayIndex, entriesCount: Int, - divisionEntries: ContiguousArray>, + divisionEntries: ContiguousArray, divisions: [Division.Runtime] ) -> MaximumSameOpponentMatchups { var maxSameOpponentMatchups:MaximumSameOpponentMatchups = .init(repeating: .init(repeating: .max, count: entriesCount), count: entriesCount) for (divisionIndex, division) in divisions.enumerated() { let divisionEntries = divisionEntries[divisionIndex] let cap = division.maxSameOpponentMatchups - for entryID in divisionEntries { - for opponentEntryID in divisionEntries { + divisionEntries.forEach { entryID in + divisionEntries.forEach { opponentEntryID in maxSameOpponentMatchups[unchecked: entryID][unchecked: opponentEntryID] = cap } } diff --git a/Sources/league-scheduling/data/LeagueScheduleData.swift b/Sources/league-scheduling/data/LeagueScheduleData.swift index 31f7e1c..9c04fb6 100644 --- a/Sources/league-scheduling/data/LeagueScheduleData.swift +++ b/Sources/league-scheduling/data/LeagueScheduleData.swift @@ -4,9 +4,9 @@ import StaticDateTimes // MARK: Data /// Fundamental building block that keeps track of and enforces assignment rules when building the schedule. -struct LeagueScheduleData: Sendable, ~Copyable { +struct LeagueScheduleData: Sendable, ~Copyable { let clock = ContinuousClock() - var rng:RNG + var rng:Config.RNG let entriesPerMatchup:EntriesPerMatchup let entriesCount:Int let entryDivisions:ContiguousArray @@ -32,17 +32,17 @@ struct LeagueScheduleData: Sendable, ~Cop /// - Usage: [`selection index` : `Set`] var failedMatchupSelections:ContiguousArray> - var assignmentState:AssignmentState + var assignmentState:AssignmentState var prioritizeEarlierTimes:Bool var executionSteps = [ExecutionStep]() var shuffleHistory = [LeagueShuffleAction]() - var redistributionData:RedistributionData? + var redistributionData:RedistributionData? var redistributedMatchups = false init( - snapshot: LeagueScheduleDataSnapshot + snapshot: LeagueScheduleDataSnapshot ) { //locations = snapshot.locations rng = snapshot.rng @@ -65,7 +65,7 @@ struct LeagueScheduleData: Sendable, ~Cop // MARK: Snapshot extension LeagueScheduleData { - mutating func loadSnapshot(_ snapshot: LeagueScheduleDataSnapshot) { + mutating func loadSnapshot(_ snapshot: LeagueScheduleDataSnapshot) { //locations = snapshot.locations rng = snapshot.rng divisionRecurringDayLimitInterval = snapshot.divisionRecurringDayLimitInterval @@ -81,7 +81,7 @@ extension LeagueScheduleData { shuffleHistory = snapshot.shuffleHistory } - func snapshot() -> LeagueScheduleDataSnapshot { + func snapshot() -> LeagueScheduleDataSnapshot { return .init(self) } } @@ -97,8 +97,8 @@ extension LeagueScheduleData { mutating func newDay( day: DayIndex, daySettings: GeneralSettings.Runtime, - divisionEntries: ContiguousArray>, - availableSlots: OrderedSet, + divisionEntries: ContiguousArray, + availableSlots: Config.DeterministicAvailableSlotSet, settings: RequestPayload.Runtime, generationData: inout LeagueGenerationData ) throws(LeagueError) { @@ -112,11 +112,12 @@ extension LeagueScheduleData { self.prioritizeEarlierTimes = daySettings.prioritizeEarlierTimes self.gameGap = daySettings.gameGap.minMax self.sameLocationIfB2B = daySettings.sameLocationIfB2B - var availableMatchups = OrderedSet() - var prioritizedEntries = OrderedSet(minimumCapacity: entriesCount) + var availableMatchups = Config.DeterministicMatchupPairSet() + var prioritizedEntries = Config.DeterministicEntryIDSet() + prioritizedEntries.reserveCapacity(entriesCount) var entryCountsForDivision:ContiguousArray = .init(repeating: 0, count: divisionEntries.count) expectedMatchupsCount = 0 - assignmentState.allDivisionMatchups = .init(repeating: [], count: divisionEntries.count) + assignmentState.allDivisionMatchups = .init(repeating: .init(), count: divisionEntries.count) for (divisionIndex, var entriesInDivision) in divisionEntries.enumerated() { if !entriesInDivision.isEmpty { divisionRecurringDayLimitInterval[divisionIndex] = Self.recurringDayLimitInterval( @@ -124,10 +125,9 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: defaultMaxEntryMatchupsPerGameDay ) - var iterator = entriesInDivision.makeIterator() - while let entryID = iterator.next() { + entriesInDivision.forEach { entryID in if assignmentState.numberOfAssignedMatchups[unchecked: entryID] >= daySettings.maximumPlayableMatchups[unchecked: entryID] { - entriesInDivision.remove(entryID) + entriesInDivision.removeMember(entryID) } } @@ -137,7 +137,10 @@ extension LeagueScheduleData { #if LOG print("LeagueScheduleData;newDay;day=\(day);expectedMatchupsCount=\(expectedMatchupsCount);divisionIndex=\(divisionIndex);entryCountsForDivision=\(entriesInDivision.count);divisionRecurringDayLimitInterval=\(divisionRecurringDayLimitInterval[divisionIndex])") #endif - let availableDivisionMatchups = availableMatchupPairs(for: entriesInDivision) + let availableDivisionMatchups:Config.DeterministicMatchupPairSet = entriesInDivision.availableMatchupPairs( + assignedEntryHomeAways: assignmentState.assignedEntryHomeAways, + maxSameOpponentMatchups: assignmentState.maxSameOpponentMatchups + ) self.assignmentState.allDivisionMatchups[divisionIndex] = availableDivisionMatchups availableMatchups.formUnion(availableDivisionMatchups) } @@ -160,11 +163,9 @@ extension LeagueScheduleData { assignmentState.prioritizedEntries = prioritizedEntries assignmentState.matchups = OrderedSet(minimumCapacity: availableSlots.count) for i in 0.. - ) -> OrderedSet { - return Self.availableMatchupPairs( - for: entries, - assignedEntryHomeAways: assignmentState.assignedEntryHomeAways, - maxSameOpponentMatchups: assignmentState.maxSameOpponentMatchups - ) - } - - /// - Parameters: - /// - entries: Entries that will participate in matchup scheduling. - /// - Returns: The available matchup pairs that can play for the `day`. - static func availableMatchupPairs( - for entries: Set, - assignedEntryHomeAways: AssignedEntryHomeAways, - maxSameOpponentMatchups: MaximumSameOpponentMatchups - ) -> OrderedSet { - guard !entries.isEmpty else { return [] } // https://github.com/apple/swift-collections/issues/608 - var pairs = OrderedSet(minimumCapacity: (entries.count-1) * 2) - let sortedEntries = entries.sorted() - - var index = 0 - while index < sortedEntries.count - 1 { - let home = sortedEntries[index] - index += 1 - let assignedHome = assignedEntryHomeAways[unchecked: home] - let maxSameOpponentMatchups = maxSameOpponentMatchups[unchecked: home] - for away in sortedEntries[index...] { - if assignedHome[unchecked: away].sum < maxSameOpponentMatchups[unchecked: away] { - pairs.append(.init(team1: home, team2: away)) - } - } - } - return pairs - } -} - // MARK: Get recurring day limit interval extension LeagueScheduleData { static func recurringDayLimitInterval( diff --git a/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift b/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift index d391082..a5ab16d 100644 --- a/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift +++ b/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift @@ -2,8 +2,8 @@ import OrderedCollections import StaticDateTimes -struct LeagueScheduleDataSnapshot: Sendable { - let rng:RNG +struct LeagueScheduleDataSnapshot: Sendable { + let rng:Config.RNG let entriesPerMatchup:EntriesPerMatchup let entriesCount:Int let entryDivisions:ContiguousArray @@ -23,21 +23,21 @@ struct LeagueScheduleDataSnapshot: Sendab /// - Usage: [`selection index` : `Set`] var failedMatchupSelections:ContiguousArray> - var assignmentState:AssignmentStateCopyable + var assignmentState:AssignmentStateCopyable var prioritizeEarlierTimes = false var executionSteps = [ExecutionStep]() var shuffleHistory = [LeagueShuffleAction]() init( - rng: RNG, + rng: Config.RNG, maxStartingTimes: TimeIndex, startingTimes: [StaticTime], maxLocations: LocationIndex, entriesPerMatchup: EntriesPerMatchup, maximumPlayableMatchups: [UInt32], entries: [Entry.Runtime], - divisionEntries: ContiguousArray>, + divisionEntries: ContiguousArray, matchupDuration: MatchupDuration, gameGap: (Int, Int), sameLocationIfB2B: Bool, @@ -50,17 +50,24 @@ struct LeagueScheduleDataSnapshot: Sendab self.gameGap = gameGap self.sameLocationIfB2B = sameLocationIfB2B - var prioritizedEntries = OrderedSet(minimumCapacity: entriesCount) + var prioritizedEntries = Config.DeterministicEntryIDSet() + prioritizedEntries.reserveCapacity(entriesCount) var entryDivisions = ContiguousArray(repeating: 0, count: entriesCount) for (index, entries) in divisionEntries.enumerated() { prioritizedEntries.formUnion(entries) - for entry in entries { + entries.forEach { entry in entryDivisions[unchecked: entry] = Division.IDValue(index) } } self.entryDivisions = entryDivisions failedMatchupSelections = .init(repeating: Set(), count: entriesCount) + let playsAt = ContiguousArray( + repeating: .init(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)), count: entriesCount + ) + let playsAtTimes = PlaysAtTimesArray( + times: .init(repeating: .init(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)), count: entriesCount) + ) assignmentState = .init( entries: entries, startingTimes: startingTimes, @@ -78,20 +85,20 @@ struct LeagueScheduleDataSnapshot: Sendab homeMatchups: .init(repeating: 0, count: entriesCount), awayMatchups: .init(repeating: 0, count: entriesCount), maxSameOpponentMatchups: maxSameOpponentMatchups, - allMatchups: [], + allMatchups: .init(), allDivisionMatchups: [], - availableMatchups: [], + availableMatchups: .init(), prioritizedEntries: prioritizedEntries, - availableSlots: [], - playsAt: .init(repeating: OrderedSet(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)), count: entriesCount), - playsAtTimes: .init(repeating: OrderedSet(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)), count: entriesCount), + availableSlots: .init(), + playsAt: playsAt, + playsAtTimes: playsAtTimes, playsAtLocations: .init(repeating: Set(minimumCapacity: defaultMaxEntryMatchupsPerGameDay), count: entriesCount), matchups: [], shuffleHistory: [] ) } - init(_ snapshot: borrowing LeagueScheduleData) { + init(_ snapshot: borrowing LeagueScheduleData) { rng = snapshot.rng entriesPerMatchup = snapshot.entriesPerMatchup entriesCount = snapshot.entriesCount diff --git a/Sources/league-scheduling/data/MatchupBlock.swift b/Sources/league-scheduling/data/MatchupBlock.swift index b6193b7..9f76877 100644 --- a/Sources/league-scheduling/data/MatchupBlock.swift +++ b/Sources/league-scheduling/data/MatchupBlock.swift @@ -58,7 +58,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, rng: inout some RandomNumberGenerator, - assignmentState: inout AssignmentState, + assignmentState: inout AssignmentState, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> OrderedSet? { @@ -77,8 +77,9 @@ extension LeagueScheduleData { print("assignedEntryHomeAways=\(localAssignmentState.assignedEntryHomeAways.map { $0.map { $0.sum } })") #endif // assign initial matchups - var adjacentTimes = OrderedSet() - var selectedEntries = OrderedSet(minimumCapacity: amount * entriesPerMatchup) + var adjacentTimes = Config.TimeSet() + var selectedEntries = Config.DeterministicEntryIDSet() + selectedEntries.reserveCapacity(amount * entriesPerMatchup) // assign the first matchup, prioritizing the matchup's time guard let firstMatchup = selectAndAssignMatchup( @@ -133,22 +134,22 @@ extension LeagueScheduleData { // assign the last matchup let lastLocalAssignmentStateAvailableMatchups = localAssignmentState.availableMatchups let lastSelectedEntries = selectedEntries - let shouldSkipSelection:(MatchupPair) -> Bool = entryMatchupsPerGameDay % 2 == 0 ? { + let shouldSkipSelection:(MatchupPair) -> Bool = entryMatchupsPerGameDay % 2 == 0 ? { pair in var targetEntries = lastSelectedEntries - targetEntries.append($0.team1) - targetEntries.append($0.team2) + targetEntries.insertMember(pair.team1) + targetEntries.insertMember(pair.team2) let availableMatchups = lastLocalAssignmentStateAvailableMatchups.filter { targetEntries.contains($0.team1) && targetEntries.contains($0.team2) } - for entryID in targetEntries { + return targetEntries.forEachWithReturn { entryID in if availableMatchups.first(where: { $0.team1 == entryID || $0.team2 == entryID }) == nil { #if LOG - print("assignBlockOfMatchups;i == lastMatchupIndex;$0=\($0);targetEntries (\(targetEntries.count))=\(targetEntries);entryID=\(entryID);availableMatchups.first of entryID == nil;skipping $0") + print("assignBlockOfMatchups;i == lastMatchupIndex;pair=\(pair);targetEntries (\(targetEntries.count))=\(targetEntries);entryID=\(entryID);availableMatchups.first of entryID == nil;skipping $0") #endif return true } - } - return false + return nil + } ?? false } : { _ in false } guard let _ = selectAndAssignMatchup( day: day, @@ -198,7 +199,7 @@ extension LeagueScheduleData { canPlayAt: canPlayAt ) else { return nil } } - adjacentTimes.remove(time) + adjacentTimes.removeMember(time) #if LOG print("assignBlockOfMatchups;j=\(j);finished time \(time)") #endif @@ -212,7 +213,7 @@ extension LeagueScheduleData { assignmentState = localAssignmentState.copy() assignmentState.matchups.formUnion(previousMatchups) for matchup in localAssignmentState.matchups { - remainingAvailableSlots.remove(matchup.slot) + remainingAvailableSlots.removeMember(matchup.slot) } assignmentState.availableSlots = remainingAvailableSlots assignmentState.prioritizedEntries = remainingPrioritizedEntries @@ -230,11 +231,11 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: OrderedSet, + allAvailableMatchups: Config.DeterministicMatchupPairSet, rng: inout some RandomNumberGenerator, - localAssignmentState: inout AssignmentState, - remainingPrioritizedEntries: inout OrderedSet, - selectedEntries: inout OrderedSet, + localAssignmentState: inout AssignmentState, + remainingPrioritizedEntries: inout Config.DeterministicEntryIDSet, + selectedEntries: inout Config.DeterministicEntryIDSet, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { @@ -255,10 +256,10 @@ extension LeagueScheduleData { return nil } // successfully assigned - remainingPrioritizedEntries.remove(leagueMatchup.home) - remainingPrioritizedEntries.remove(leagueMatchup.away) - selectedEntries.append(leagueMatchup.home) - selectedEntries.append(leagueMatchup.away) + remainingPrioritizedEntries.removeMember(leagueMatchup.home) + remainingPrioritizedEntries.removeMember(leagueMatchup.away) + selectedEntries.insertMember(leagueMatchup.home) + selectedEntries.insertMember(leagueMatchup.away) return leagueMatchup } private static func selectAndAssignMatchup( @@ -269,12 +270,12 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: OrderedSet, + allAvailableMatchups: Config.DeterministicMatchupPairSet, rng: inout some RandomNumberGenerator, - localAssignmentState: inout AssignmentState, + localAssignmentState: inout AssignmentState, shouldSkipSelection: (MatchupPair) -> Bool, - remainingPrioritizedEntries: inout OrderedSet, - selectedEntries: inout OrderedSet, + remainingPrioritizedEntries: inout Config.DeterministicEntryIDSet, + selectedEntries: inout Config.DeterministicEntryIDSet, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { @@ -296,10 +297,10 @@ extension LeagueScheduleData { return nil } // successfully assigned - remainingPrioritizedEntries.remove(leagueMatchup.home) - remainingPrioritizedEntries.remove(leagueMatchup.away) - selectedEntries.append(leagueMatchup.home) - selectedEntries.append(leagueMatchup.away) + remainingPrioritizedEntries.removeMember(leagueMatchup.home) + remainingPrioritizedEntries.removeMember(leagueMatchup.away) + selectedEntries.insertMember(leagueMatchup.home) + selectedEntries.insertMember(leagueMatchup.away) return leagueMatchup } } \ No newline at end of file diff --git a/Sources/league-scheduling/data/PrioritizedMatchups.swift b/Sources/league-scheduling/data/PrioritizedMatchups.swift index c3b238c..11bbe24 100644 --- a/Sources/league-scheduling/data/PrioritizedMatchups.swift +++ b/Sources/league-scheduling/data/PrioritizedMatchups.swift @@ -1,18 +1,18 @@ import OrderedCollections -struct PrioritizedMatchups: Sendable, ~Copyable { - private(set) var matchups:OrderedSet +struct PrioritizedMatchups: Sendable, ~Copyable { + private(set) var matchups:Config.DeterministicMatchupPairSet private(set) var availableMatchupCountForEntry:ContiguousArray init( entriesCount: Int, - prioritizedEntries: OrderedSet, - availableMatchups: OrderedSet + prioritizedEntries: Config.DeterministicEntryIDSet, + availableMatchups: Config.DeterministicMatchupPairSet ) { let matchups = Self.filterMatchups(prioritizedEntries: prioritizedEntries, availableMatchups: availableMatchups) var availableMatchupCountForEntry = ContiguousArray(repeating: 0, count: entriesCount) - for matchup in matchups { + matchups.forEach { matchup in availableMatchupCountForEntry[unchecked: matchup.team1] += 1 availableMatchupCountForEntry[unchecked: matchup.team2] += 1 } @@ -21,14 +21,14 @@ struct PrioritizedMatchups: Sendable, ~Copyable { } mutating func update( - prioritizedEntries: OrderedSet, - availableMatchups: OrderedSet + prioritizedEntries: Config.DeterministicEntryIDSet, + availableMatchups: Config.DeterministicMatchupPairSet ) { matchups = Self.filterMatchups(prioritizedEntries: prioritizedEntries, availableMatchups: availableMatchups) for i in availableMatchupCountForEntry.indices { availableMatchupCountForEntry[unchecked: i] = 0 } - for matchup in matchups { + matchups.forEach { matchup in availableMatchupCountForEntry[unchecked: matchup.team1] += 1 availableMatchupCountForEntry[unchecked: matchup.team2] += 1 } @@ -36,13 +36,13 @@ struct PrioritizedMatchups: Sendable, ~Copyable { /// Removes the specified matchup pair from `matchups`. mutating func remove(_ matchup: MatchupPair) { - matchups.remove(matchup) + matchups.removeMember(matchup) } private static func filterMatchups( - prioritizedEntries: OrderedSet, - availableMatchups: OrderedSet - ) -> OrderedSet { + prioritizedEntries: Config.DeterministicEntryIDSet, + availableMatchups: Config.DeterministicMatchupPairSet + ) -> Config.DeterministicMatchupPairSet { if prioritizedEntries.isEmpty { return availableMatchups } diff --git a/Sources/league-scheduling/data/RedistributionData.swift b/Sources/league-scheduling/data/RedistributionData.swift index 2bd1733..29ecc6d 100644 --- a/Sources/league-scheduling/data/RedistributionData.swift +++ b/Sources/league-scheduling/data/RedistributionData.swift @@ -1,7 +1,7 @@ import OrderedCollections -struct RedistributionData: Sendable { +struct RedistributionData: Sendable { /// The latest `DayIndex` that is allowed to redistribute matchups from. let startDayIndex:DayIndex let entryMatchupsPerGameDay:EntryMatchupsPerGameDay @@ -45,7 +45,7 @@ extension RedistributionData { canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable, day: DayIndex, gameGap: GameGap.TupleValue, - assignmentState: inout AssignmentState, + assignmentState: inout AssignmentState, executionSteps: inout [ExecutionStep], generationData: inout LeagueGenerationData ) -> Bool { @@ -70,7 +70,7 @@ extension RedistributionData { let homeMaxAssignedLocations = assignmentState.maxLocationAllocations[unchecked: matchup.home] let awayMaxAssignedLocations = assignmentState.maxLocationAllocations[unchecked: matchup.away] - for slot in assignmentState.availableSlots { + assignmentState.availableSlots.forEach { slot in assignmentState.decrementAssignData(home: matchup.home, away: matchup.away, slot: matchup.slot) if canPlayAt.test( time: slot.time, @@ -176,7 +176,7 @@ extension RedistributionData { private mutating func redistribute( redistributable: inout Redistributable, redistributables: inout OrderedSet, - assignmentState: inout AssignmentState, + assignmentState: inout AssignmentState, generationData: inout LeagueGenerationData ) { generationData.schedule[unchecked: redistributable.fromDay].remove(redistributable.matchup) @@ -196,7 +196,7 @@ extension RedistributionData { redistributable.matchup.time = redistributable.toSlot.time redistributable.matchup.location = redistributable.toSlot.location assignmentState.matchups.append(redistributable.matchup) - assignmentState.availableSlots.remove(redistributable.toSlot) + assignmentState.availableSlots.removeMember(redistributable.toSlot) assignmentState.incrementAssignData(home: redistributable.matchup.home, away: redistributable.matchup.away, slot: redistributable.toSlot) assignmentState.insertPlaysAt(home: redistributable.matchup.home, away: redistributable.matchup.away, slot: redistributable.toSlot) } diff --git a/Sources/league-scheduling/data/RemainingAllocations.swift b/Sources/league-scheduling/data/RemainingAllocations.swift index 96cc13d..db6f282 100644 --- a/Sources/league-scheduling/data/RemainingAllocations.swift +++ b/Sources/league-scheduling/data/RemainingAllocations.swift @@ -6,7 +6,7 @@ extension AssignmentState { ) { remainingAllocations = .init(repeating: availableSlots, count: entriesCount) var cached = Set(minimumCapacity: entriesCount) - for matchup in availableMatchups { + availableMatchups.forEach { matchup in recalculateNewDayRemainingAllocations( for: matchup, cached: &cached @@ -41,9 +41,9 @@ extension AssignmentState { let maxTimeNumbers = maxTimeAllocations[unchecked: team] let maxLocationNumbers = maxLocationAllocations[unchecked: team] var available = availableSlots - for slot in availableSlots { + availableSlots.forEach { slot in if timeNumbers[unchecked: slot.time] >= maxTimeNumbers[unchecked: slot.time] || locationNumbers[unchecked: slot.location] >= maxLocationNumbers[unchecked: slot.location] { - available.remove(slot) + available.removeMember(slot) } } remainingAllocations[unchecked: team] = available @@ -59,7 +59,7 @@ extension AssignmentState { canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) { var cached = Set(minimumCapacity: entriesCount) - for matchup in availableMatchups { + availableMatchups.forEach { matchup in recalculateRemainingAllocations( day: day, for: matchup, @@ -115,7 +115,7 @@ extension AssignmentState { let maxTimeNumbers = maxTimeAllocations[unchecked: team] let maxLocationNumbers = maxLocationAllocations[unchecked: team] var available = availableSlots - for slot in availableSlots { + availableSlots.forEach { slot in if !canPlayAt.test( time: slot.time, location: slot.location, @@ -130,7 +130,7 @@ extension AssignmentState { maxLocationNumber: UInt8(maxLocationNumbers[unchecked: slot.location]), gameGap: gameGap ) { - available.remove(slot) + available.removeMember(slot) } } remainingAllocations[unchecked: team] = available diff --git a/Sources/league-scheduling/data/SelectMatchup.swift b/Sources/league-scheduling/data/SelectMatchup.swift index eaf35a0..e2fc99a 100644 --- a/Sources/league-scheduling/data/SelectMatchup.swift +++ b/Sources/league-scheduling/data/SelectMatchup.swift @@ -4,7 +4,7 @@ import OrderedCollections // MARK: Select matchup extension LeagueScheduleData { /// - Returns: Matchup pair that should be prioritized to be scheduled due to how many allocations it has remaining. - mutating func selectMatchup(prioritizedMatchups: borrowing PrioritizedMatchups) -> MatchupPair? { + mutating func selectMatchup(prioritizedMatchups: borrowing PrioritizedMatchups) -> MatchupPair? { return assignmentState.selectMatchup( prioritizedMatchups: prioritizedMatchups, rng: &rng @@ -15,7 +15,7 @@ extension LeagueScheduleData { extension AssignmentState { /// - Returns: Matchup pair that should be prioritized to be scheduled due to how many allocations it has remaining. func selectMatchup( - prioritizedMatchups: borrowing PrioritizedMatchups, + prioritizedMatchups: borrowing PrioritizedMatchups, rng: inout some RandomNumberGenerator ) -> MatchupPair? { return Self.selectMatchup( @@ -29,144 +29,143 @@ extension AssignmentState { /// - Returns: Matchup pair that should be prioritized to be scheduled due to how many allocations it has remaining. static func selectMatchup( - prioritizedMatchups: borrowing PrioritizedMatchups, + prioritizedMatchups: borrowing PrioritizedMatchups, numberOfAssignedMatchups: [Int], recurringDayLimits: RecurringDayLimits, - remainingAllocations: RemainingAllocations, + remainingAllocations: Config.RemainingAllocations, rng: inout some RandomNumberGenerator ) -> MatchupPair? { #if LOG print("SelectMatchup;selectMatchup;prioritizedMatchups.count=\(prioritizedMatchups.matchups.count);availableMatchupCountForEntry=\(prioritizedMatchups.availableMatchupCountForEntry)") #endif - guard let first = prioritizedMatchups.matchups.first else { return nil } - guard prioritizedMatchups.matchups.count > 1 else { - return first//recurringDayLimit(for: first) <= day ? first : nil - } - let firstNumberOfMatchupsPlayedSoFar = numberOfMatchupsPlayedSoFar(for: first, numberOfAssignedMatchups: numberOfAssignedMatchups) - var selected = SelectedMatchup( - pair: first, - minMatchupsPlayedSoFar: firstNumberOfMatchupsPlayedSoFar.minimum, - totalMatchupsPlayedSoFar: firstNumberOfMatchupsPlayedSoFar.total, - remainingAllocations: Self.remainingAllocations(for: first, remainingAllocations: remainingAllocations), - remainingMatchupCount: remainingMatchupCount(for: first, prioritizedMatchups.availableMatchupCountForEntry), - recurringDayLimit: recurringDayLimit(for: first, recurringDayLimits: recurringDayLimits) - ) + var selected:SelectedMatchup! = nil // introduce a pool of matchup pairs of equal priority, and random selection, so that we don't repeat identical assignments when // - regenerating a failed day // - selecting the last matchup pair out of previous pairs of equal priority - var pool = OrderedSet() - for pair in prioritizedMatchups.matchups[prioritizedMatchups.matchups.index(after: prioritizedMatchups.matchups.startIndex)...] { + var pool = Config.DeterministicMatchupPairSet() + prioritizedMatchups.matchups.forEach { pair in let (pairMinMatchupsPlayedSoFar, pairTotalMatchupsPlayedSoFar) = numberOfMatchupsPlayedSoFar(for: pair, numberOfAssignedMatchups: numberOfAssignedMatchups) - guard pairMinMatchupsPlayedSoFar == selected.minMatchupsPlayedSoFar else { - if pairMinMatchupsPlayedSoFar < selected.minMatchupsPlayedSoFar { - selected = select( - pair: pair, - minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, - totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, - recurringDayLimit: recurringDayLimit(for: pair, recurringDayLimits: recurringDayLimits), - remainingAllocations: Self.remainingAllocations(for: pair, remainingAllocations: remainingAllocations), - remainingMatchupCount: remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), - pool: &pool - ) + if selected == nil { + selected = select( + pair: pair, + minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, + totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, + recurringDayLimit: recurringDayLimit(for: pair, recurringDayLimits: recurringDayLimits), + remainingAllocations: Self.remainingAllocations(for: pair, remainingAllocations: remainingAllocations), + remainingMatchupCount: remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), + pool: &pool + ) + } else { + guard pairMinMatchupsPlayedSoFar == selected.minMatchupsPlayedSoFar else { + if pairMinMatchupsPlayedSoFar < selected.minMatchupsPlayedSoFar { + selected = select( + pair: pair, + minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, + totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, + recurringDayLimit: recurringDayLimit(for: pair, recurringDayLimits: recurringDayLimits), + remainingAllocations: Self.remainingAllocations(for: pair, remainingAllocations: remainingAllocations), + remainingMatchupCount: remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), + pool: &pool + ) + } + return } - continue - } - guard pairTotalMatchupsPlayedSoFar == selected.totalMatchupsPlayedSoFar else { - if pairTotalMatchupsPlayedSoFar < selected.totalMatchupsPlayedSoFar { - selected = select( - pair: pair, - minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, - totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, - recurringDayLimit: recurringDayLimit(for: pair, recurringDayLimits: recurringDayLimits), - remainingAllocations: Self.remainingAllocations(for: pair, remainingAllocations: remainingAllocations), - remainingMatchupCount: remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), - pool: &pool - ) + guard pairTotalMatchupsPlayedSoFar == selected.totalMatchupsPlayedSoFar else { + if pairTotalMatchupsPlayedSoFar < selected.totalMatchupsPlayedSoFar { + selected = select( + pair: pair, + minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, + totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, + recurringDayLimit: recurringDayLimit(for: pair, recurringDayLimits: recurringDayLimits), + remainingAllocations: Self.remainingAllocations(for: pair, remainingAllocations: remainingAllocations), + remainingMatchupCount: remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), + pool: &pool + ) + } + return } - continue - } - let pairRecurringDayLimit = recurringDayLimit(for: pair, recurringDayLimits: recurringDayLimits) - guard pairRecurringDayLimit == selected.recurringDayLimit else { - if pairRecurringDayLimit < selected.recurringDayLimit { - selected = select( - pair: pair, - minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, - totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, - recurringDayLimit: pairRecurringDayLimit, - remainingAllocations: Self.remainingAllocations(for: pair, remainingAllocations: remainingAllocations), - remainingMatchupCount: remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), - pool: &pool - ) + let pairRecurringDayLimit = recurringDayLimit(for: pair, recurringDayLimits: recurringDayLimits) + guard pairRecurringDayLimit == selected.recurringDayLimit else { + if pairRecurringDayLimit < selected.recurringDayLimit { + selected = select( + pair: pair, + minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, + totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, + recurringDayLimit: pairRecurringDayLimit, + remainingAllocations: Self.remainingAllocations(for: pair, remainingAllocations: remainingAllocations), + remainingMatchupCount: remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), + pool: &pool + ) + } + return } - continue - } - let pairRemainingAllocations = Self.remainingAllocations(for: pair, remainingAllocations: remainingAllocations) - guard pairRemainingAllocations.min == selected.remainingAllocations.min else { - if pairRemainingAllocations.min < selected.remainingAllocations.min { - selected = select( - pair: pair, - minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, - totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, - recurringDayLimit: pairRecurringDayLimit, - remainingAllocations: pairRemainingAllocations, - remainingMatchupCount: Self.remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), - pool: &pool - ) + let pairRemainingAllocations = Self.remainingAllocations(for: pair, remainingAllocations: remainingAllocations) + guard pairRemainingAllocations.min == selected.remainingAllocations.min else { + if pairRemainingAllocations.min < selected.remainingAllocations.min { + selected = select( + pair: pair, + minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, + totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, + recurringDayLimit: pairRecurringDayLimit, + remainingAllocations: pairRemainingAllocations, + remainingMatchupCount: Self.remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), + pool: &pool + ) + } + return } - continue - } - guard pairRemainingAllocations.max == selected.remainingAllocations.max else { - if pairRemainingAllocations.max < selected.remainingAllocations.max { - selected = select( - pair: pair, - minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, - totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, - recurringDayLimit: pairRecurringDayLimit, - remainingAllocations: pairRemainingAllocations, - remainingMatchupCount: Self.remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), - pool: &pool - ) + guard pairRemainingAllocations.max == selected.remainingAllocations.max else { + if pairRemainingAllocations.max < selected.remainingAllocations.max { + selected = select( + pair: pair, + minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, + totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, + recurringDayLimit: pairRecurringDayLimit, + remainingAllocations: pairRemainingAllocations, + remainingMatchupCount: Self.remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), + pool: &pool + ) + } + return } - continue - } - let pairRemainingMatchupCount = Self.remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry) - guard pairRemainingMatchupCount.min == selected.remainingMatchupCount.min else { - if pairRemainingMatchupCount.min < selected.remainingMatchupCount.min { - selected = select( - pair: pair, - minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, - totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, - recurringDayLimit: pairRecurringDayLimit, - remainingAllocations: pairRemainingAllocations, - remainingMatchupCount: pairRemainingMatchupCount, - pool: &pool - ) + let pairRemainingMatchupCount = Self.remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry) + guard pairRemainingMatchupCount.min == selected.remainingMatchupCount.min else { + if pairRemainingMatchupCount.min < selected.remainingMatchupCount.min { + selected = select( + pair: pair, + minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, + totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, + recurringDayLimit: pairRecurringDayLimit, + remainingAllocations: pairRemainingAllocations, + remainingMatchupCount: pairRemainingMatchupCount, + pool: &pool + ) + } + return } - continue - } - guard pairRemainingMatchupCount.max == selected.remainingMatchupCount.max else { - if pairRemainingMatchupCount.max < selected.remainingMatchupCount.max { - selected = select( - pair: pair, - minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, - totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, - recurringDayLimit: pairRecurringDayLimit, - remainingAllocations: pairRemainingAllocations, - remainingMatchupCount: pairRemainingMatchupCount, - pool: &pool - ) + guard pairRemainingMatchupCount.max == selected.remainingMatchupCount.max else { + if pairRemainingMatchupCount.max < selected.remainingMatchupCount.max { + selected = select( + pair: pair, + minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, + totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, + recurringDayLimit: pairRecurringDayLimit, + remainingAllocations: pairRemainingAllocations, + remainingMatchupCount: pairRemainingMatchupCount, + pool: &pool + ) + } + return } - continue + pool.insertMember(pair) } - - pool.append(pair) } #if LOG print("SelectMatchup;selectMatchup;selected.pair=\(selected.pair.description);pool=\(pool.map({ $0.description }))") #endif - return pool.isEmpty ? selected.pair : pool.randomElement(using: &rng) + return pool.isEmpty ? selected?.pair : pool.randomElement(using: &rng) } } @@ -189,7 +188,7 @@ extension AssignmentState { private static func recurringDayLimit(for pair: MatchupPair, recurringDayLimits: RecurringDayLimits) -> RecurringDayLimitInterval { return recurringDayLimits[unchecked: pair.team1][unchecked: pair.team2] } - private static func remainingAllocations(for pair: MatchupPair, remainingAllocations: RemainingAllocations) -> (min: Int, max: Int) { + private static func remainingAllocations(for pair: MatchupPair, remainingAllocations: Config.RemainingAllocations) -> (min: Int, max: Int) { let team1 = remainingAllocations[unchecked: pair.team1].count let team2 = remainingAllocations[unchecked: pair.team2].count return ( @@ -212,10 +211,10 @@ extension AssignmentState { recurringDayLimit: RecurringDayLimitInterval, remainingAllocations: (min: Int, max: Int), remainingMatchupCount: (min: Int, max: Int), - pool: inout OrderedSet + pool: inout Config.DeterministicMatchupPairSet ) -> SelectedMatchup { - pool.removeAll(keepingCapacity: true) - pool.append(pair) + pool.removeAllKeepingCapacity() + pool.insertMember(pair) return .init( pair: pair, minMatchupsPlayedSoFar: minMatchupsPlayedSoFar, diff --git a/Sources/league-scheduling/data/Shuffle.swift b/Sources/league-scheduling/data/Shuffle.swift index e5fd846..ad4eca6 100644 --- a/Sources/league-scheduling/data/Shuffle.swift +++ b/Sources/league-scheduling/data/Shuffle.swift @@ -12,7 +12,7 @@ extension AssignmentState { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: OrderedSet, + allAvailableMatchups: Config.DeterministicMatchupPairSet, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> AvailableSlot? { // TODO: fix (can get stuck shuffling the same matchup to the same slot) @@ -69,8 +69,8 @@ extension AssignmentState { let swappedSlot = swapped.slot var homePlaysAt = playsAt[unchecked: swapped.home] var awayPlaysAt = playsAt[unchecked: swapped.away] - homePlaysAt.remove(swappedSlot) - awayPlaysAt.remove(swappedSlot) + homePlaysAt.removeMember(swappedSlot) + awayPlaysAt.removeMember(swappedSlot) let homeAllowedTimes = entries[unchecked: swapped.home].gameTimes[unchecked: day] let awayAllowedTimes = entries[unchecked: swapped.away].gameTimes[unchecked: day] @@ -80,8 +80,8 @@ extension AssignmentState { var homePlaysAtTimes = playsAtTimes[unchecked: swapped.home] var awayPlaysAtTimes = playsAtTimes[unchecked: swapped.away] - homePlaysAtTimes.remove(swapped.time) - awayPlaysAtTimes.remove(swapped.time) + homePlaysAtTimes.removeMember(swapped.time) + awayPlaysAtTimes.removeMember(swapped.time) var homePlaysAtLocations = playsAtLocations[unchecked: swapped.home] var awayPlaysAtLocations = playsAtLocations[unchecked: swapped.away] diff --git a/Sources/league-scheduling/data/assignment/Assign.swift b/Sources/league-scheduling/data/assignment/Assign.swift index 6da1dc2..8e38918 100644 --- a/Sources/league-scheduling/data/assignment/Assign.swift +++ b/Sources/league-scheduling/data/assignment/Assign.swift @@ -41,15 +41,15 @@ extension AssignmentState { divisionRecurringDayLimitInterval: ContiguousArray, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup { - prioritizedEntries.remove(matchup.team1) - prioritizedEntries.remove(matchup.team2) + prioritizedEntries.removeMember(matchup.team1) + prioritizedEntries.removeMember(matchup.team2) let home:Entry.IDValue = matchup.team1 let away:Entry.IDValue = matchup.team2 incrementRecurringDayLimits(home: home, away: away, entryDivisions: entryDivisions, divisionRecurringDayLimitInterval: divisionRecurringDayLimitInterval) incrementAssignData(home: home, away: away, slot: slot) insertPlaysAt(home: home, away: away, slot: slot) - availableSlots.remove(slot) + availableSlots.removeMember(slot) let leagueMatchup = Matchup( time: slot.time, location: slot.location, @@ -58,9 +58,9 @@ extension AssignmentState { ) matchups.append(leagueMatchup) - availableMatchups.remove(matchup) + availableMatchups.removeMember(matchup) // TODO: fix (why is the following line necessary | it fixes an issue that allowed matchups to exceed the maximumSameOpponentsMatchupsCap, but availableMatchups still contains matchups that shouldn't be scheduled when scheduling b2b) - availableMatchups.remove(.init(team1: matchup.team2, team2: matchup.team1)) + availableMatchups.removeMember(.init(team1: matchup.team2, team2: matchup.team1)) if playsAtTimes[unchecked: home].count == entryMatchupsPerGameDay { #if LOG remainingAllocations[unchecked: home].removeAll() @@ -133,10 +133,10 @@ extension AssignmentState { away: Entry.IDValue, slot: AvailableSlot ) { - playsAt[unchecked: home].append(slot) - playsAt[unchecked: away].append(slot) - playsAtTimes[unchecked: home].remove(slot.time) - playsAtTimes[unchecked: away].remove(slot.time) + playsAt[unchecked: home].insertMember(slot) + playsAt[unchecked: away].insertMember(slot) + playsAtTimes.insertMember(entryID: home, member: slot.time) + playsAtTimes.insertMember(entryID: away, member: slot.time) playsAtLocations[unchecked: home].insert(slot.location) playsAtLocations[unchecked: away].insert(slot.location) } diff --git a/Sources/league-scheduling/data/assignment/Move.swift b/Sources/league-scheduling/data/assignment/Move.swift index 3b6dce1..6701ce6 100644 --- a/Sources/league-scheduling/data/assignment/Move.swift +++ b/Sources/league-scheduling/data/assignment/Move.swift @@ -8,7 +8,7 @@ extension LeagueScheduleData { matchup: Matchup, to slot: AvailableSlot, day: DayIndex, - allAvailableMatchups: OrderedSet, + allAvailableMatchups: Config.DeterministicMatchupPairSet, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) { assignmentState.move( @@ -37,7 +37,7 @@ extension AssignmentState { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: OrderedSet, + allAvailableMatchups: Config.DeterministicMatchupPairSet, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) { #if LOG diff --git a/Sources/league-scheduling/data/assignment/Unassign.swift b/Sources/league-scheduling/data/assignment/Unassign.swift index 7a50ce8..c86dbbc 100644 --- a/Sources/league-scheduling/data/assignment/Unassign.swift +++ b/Sources/league-scheduling/data/assignment/Unassign.swift @@ -11,7 +11,7 @@ extension AssignmentState { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: OrderedSet, + allAvailableMatchups: Config.DeterministicMatchupPairSet, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) { let recurringDayLimitInterval = divisionRecurringDayLimitInterval[unchecked: entryDivisions[unchecked: matchup.home]] @@ -19,7 +19,7 @@ extension AssignmentState { recurringDayLimits[unchecked: matchup.away][unchecked: matchup.home] -= recurringDayLimitInterval decrementAssignData(home: matchup.home, away: matchup.away, slot: matchup.slot) removePlaysAt(home: matchup.home, away: matchup.away, slot: matchup.slot) - availableSlots.append(matchup.slot) + availableSlots.insertMember(matchup.slot) matchups.remove(matchup) recalculateAvailableMatchups( @@ -72,10 +72,10 @@ extension AssignmentState { away: Entry.IDValue, slot: AvailableSlot ) { - playsAt[unchecked: home].remove(slot) - playsAt[unchecked: away].remove(slot) - playsAtTimes[unchecked: home].remove(slot.time) - playsAtTimes[unchecked: away].remove(slot.time) + playsAt[unchecked: home].removeMember(slot) + playsAt[unchecked: away].removeMember(slot) + playsAtTimes.removeMember(entryID: home, member: slot.time) + playsAtTimes.removeMember(entryID: away, member: slot.time) playsAtLocations[unchecked: home].remove(slot.location) playsAtLocations[unchecked: away].remove(slot.location) } @@ -86,7 +86,7 @@ extension AssignmentState { mutating func recalculateAvailableMatchups( day: DayIndex, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, - allAvailableMatchups: OrderedSet + allAvailableMatchups: Config.DeterministicMatchupPairSet ) { availableMatchups = allAvailableMatchups.filter({ guard assignedEntryHomeAways[unchecked: $0.team1][unchecked: $0.team2].sum < maxSameOpponentMatchups[unchecked: $0.team1][unchecked: $0.team2] diff --git a/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift new file mode 100644 index 0000000..ef790e0 --- /dev/null +++ b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift @@ -0,0 +1,24 @@ + +import OrderedCollections + +protocol ScheduleConfiguration: Sendable, ~Copyable { + associatedtype RNG:RandomNumberGenerator & Sendable + associatedtype TimeSet:SetOfTimeIndexes + + associatedtype DeterministicEntryIDSet:SetOfEntryIDs + associatedtype DeterministicAvailableSlotSet:SetOfAvailableSlots + associatedtype DeterministicMatchupPairSet:SetOfMatchupPair + + typealias RemainingAllocations = ContiguousArray + typealias PlaysAt = ContiguousArray + typealias PlaysAtTimes = ContiguousArray +} + +enum ScheduleConfig< + RNG: RandomNumberGenerator & Sendable, + TimeSet: SetOfTimeIndexes, + DeterministicEntryIDSet: SetOfEntryIDs, + DeterministicAvailableSlotSet: SetOfAvailableSlots, + DeterministicMatchupPairSet: SetOfMatchupPair + >: ScheduleConfiguration { +} \ No newline at end of file diff --git a/Sources/league-scheduling/data/canPlayAt/CanPlayAt+GameGap.swift b/Sources/league-scheduling/data/canPlayAt/CanPlayAt+GameGap.swift index 780db2c..693e6d0 100644 --- a/Sources/league-scheduling/data/canPlayAt/CanPlayAt+GameGap.swift +++ b/Sources/league-scheduling/data/canPlayAt/CanPlayAt+GameGap.swift @@ -5,11 +5,11 @@ struct CanPlayAtGameGap: Sendable, ~Copyable { /// - Returns: If a team with the provided `playsAtTimes` can play at the given `time` taking into account a `gameGap`. static func test( time: TimeIndex, - playsAtTimes: PlaysAtTimes.Element, + playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, gameGap: GameGap.TupleValue ) -> Bool { var closest:TimeIndex? = nil - for playedTime in playsAtTimes { + playsAtTimes.forEach { playedTime in let distance = abs(playedTime.distance(to: time)) if closest == nil || distance < closest! { closest = TimeIndex(distance) diff --git a/Sources/league-scheduling/data/canPlayAt/CanPlayAtNormal.swift b/Sources/league-scheduling/data/canPlayAt/CanPlayAtNormal.swift index dcf1030..1c76aea 100644 --- a/Sources/league-scheduling/data/canPlayAt/CanPlayAtNormal.swift +++ b/Sources/league-scheduling/data/canPlayAt/CanPlayAtNormal.swift @@ -9,8 +9,8 @@ struct CanPlayAtNormal: CanPlayAtProtocol, ~Copyable { location: LocationIndex, allowedTimes: Set, allowedLocations: Set, - playsAt: PlaysAt.Element, - playsAtTimes: PlaysAtTimes.Element, + playsAt: borrowing some SetOfAvailableSlots & ~Copyable, + playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, playsAtLocations: PlaysAtLocations.Element, timeNumber: UInt8, locationNumber: UInt8, @@ -39,7 +39,7 @@ struct CanPlayAtNormal: CanPlayAtProtocol, ~Copyable { location: LocationIndex, allowedTimes: Set, allowedLocations: Set, - playsAtTimes: PlaysAtTimes.Element, + playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, timeNumber: UInt8, locationNumber: UInt8, maxTimeNumber: UInt8, @@ -66,7 +66,7 @@ struct CanPlayAtNormal: CanPlayAtProtocol, ~Copyable { location: LocationIndex, allowedTimes: Set, allowedLocations: Set, - playsAtTimes: PlaysAtTimes.Element, + playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, timeNumber: UInt8, locationNumber: UInt8, maxTimeNumber: UInt8, diff --git a/Sources/league-scheduling/data/canPlayAt/CanPlayAtProtocol.swift b/Sources/league-scheduling/data/canPlayAt/CanPlayAtProtocol.swift index f4e2f91..44fe2cd 100644 --- a/Sources/league-scheduling/data/canPlayAt/CanPlayAtProtocol.swift +++ b/Sources/league-scheduling/data/canPlayAt/CanPlayAtProtocol.swift @@ -8,8 +8,8 @@ protocol CanPlayAtProtocol: Sendable, ~Copyable { location: LocationIndex, allowedTimes: Set, allowedLocations: Set, - playsAt: PlaysAt.Element, - playsAtTimes: PlaysAtTimes.Element, + playsAt: borrowing some SetOfAvailableSlots & ~Copyable, + playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, playsAtLocations: PlaysAtLocations.Element, timeNumber: UInt8, locationNumber: UInt8, diff --git a/Sources/league-scheduling/data/canPlayAt/CanPlayAtSameLocationIfB2B.swift b/Sources/league-scheduling/data/canPlayAt/CanPlayAtSameLocationIfB2B.swift index 333e0ba..8b70c2e 100644 --- a/Sources/league-scheduling/data/canPlayAt/CanPlayAtSameLocationIfB2B.swift +++ b/Sources/league-scheduling/data/canPlayAt/CanPlayAtSameLocationIfB2B.swift @@ -7,8 +7,8 @@ struct CanPlayAtSameLocationIfB2B: CanPlayAtProtocol, ~Copyable { location: LocationIndex, allowedTimes: Set, allowedLocations: Set, - playsAt: PlaysAt.Element, - playsAtTimes: PlaysAtTimes.Element, + playsAt: borrowing some SetOfAvailableSlots & ~Copyable, + playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, playsAtLocations: PlaysAtLocations.Element, timeNumber: UInt8, locationNumber: UInt8, @@ -40,7 +40,7 @@ struct CanPlayAtSameLocationIfB2B: CanPlayAtProtocol, ~Copyable { static func test( time: TimeIndex, location: LocationIndex, - playsAtTimes: PlaysAtTimes.Element, + playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, playsAtLocations: PlaysAtLocations.Element ) -> Bool { if time > 0 && playsAtTimes.contains(time-1) || playsAtTimes.contains(time+1) { diff --git a/Sources/league-scheduling/data/canPlayAt/CanPlayAtWithTravelDurations.swift b/Sources/league-scheduling/data/canPlayAt/CanPlayAtWithTravelDurations.swift index 467d92f..f9af85d 100644 --- a/Sources/league-scheduling/data/canPlayAt/CanPlayAtWithTravelDurations.swift +++ b/Sources/league-scheduling/data/canPlayAt/CanPlayAtWithTravelDurations.swift @@ -11,8 +11,8 @@ struct CanPlayAtWithTravelDurations: CanPlayAtProtocol, ~Copyable { location: LocationIndex, allowedTimes: Set, allowedLocations: Set, - playsAt: PlaysAt.Element, - playsAtTimes: PlaysAtTimes.Element, + playsAt: borrowing some SetOfAvailableSlots & ~Copyable, + playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, playsAtLocations: PlaysAtLocations.Element, timeNumber: UInt8, locationNumber: UInt8, @@ -50,12 +50,12 @@ extension CanPlayAtWithTravelDurations { travelDurations: [[MatchupDuration]], time: TimeIndex, location: LocationIndex, - playsAt: PlaysAt.Element, + playsAt: borrowing some SetOfAvailableSlots & ~Copyable, gameGap: GameGap.TupleValue ) -> Bool { var closestSlot:AvailableSlot? = nil var closestDistance:TimeIndex? = nil - for slot in playsAt { + playsAt.forEach { slot in let distance = abs(slot.time.distance(to: time)) if closestSlot == nil || distance < closestSlot!.time { closestSlot = slot diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift index 6abdc0d..0c68276 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift @@ -4,14 +4,14 @@ import OrderedCollections struct SelectSlotB2B: SelectSlotProtocol, ~Copyable { let entryMatchupsPerGameDay:EntryMatchupsPerGameDay - func select( + func select( team1: Entry.IDValue, team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playsAtTimes: PlaysAtTimes, + playsAtTimes: borrowing PlaysAtTimesArray, playsAtLocations: PlaysAtLocations, - playableSlots: inout OrderedSet + playableSlots: inout some SetOfAvailableSlots ) -> AvailableSlot? { filter( team1: team1, @@ -31,11 +31,11 @@ struct SelectSlotB2B: SelectSlotProtocol, ~Copyable { extension SelectSlotB2B { /// Mutates `playableSlots`, if `team1` AND `team2` haven't played already, so it only contains the first slots applicable for a matchup block. - private func filter( + private func filter( team1: Entry.IDValue, team2: Entry.IDValue, - playsAtTimes: PlaysAtTimes, - playableSlots: inout OrderedSet + playsAtTimes: borrowing PlaysAtTimesArray, + playableSlots: inout some SetOfAvailableSlots ) { //print("filterSlotBack2Back;playsAtTimes[unchecked: team1].isEmpty=\(playsAtTimes[unchecked: team1].isEmpty);playsAtTimes[unchecked: team2].isEmpty=\(playsAtTimes[unchecked: team2].isEmpty)") if playsAtTimes[unchecked: team1].isEmpty && playsAtTimes[unchecked: team2].isEmpty { diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift index 1c90ff9..dbd6fe7 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift @@ -2,14 +2,14 @@ import OrderedCollections struct SelectSlotEarliestTime: SelectSlotProtocol, ~Copyable { - func select( + func select( team1: Entry.IDValue, team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playsAtTimes: PlaysAtTimes, + playsAtTimes: borrowing PlaysAtTimesArray, playsAtLocations: PlaysAtLocations, - playableSlots: inout OrderedSet + playableSlots: inout some SetOfAvailableSlots ) -> AvailableSlot? { return Self.select( team1: team1, @@ -27,7 +27,7 @@ extension SelectSlotEarliestTime { team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playableSlots: inout OrderedSet + playableSlots: inout some SetOfAvailableSlots ) -> AvailableSlot? { filter(playableSlots: &playableSlots) return SelectSlotNormal.select( @@ -40,9 +40,9 @@ extension SelectSlotEarliestTime { } /// Mutates `playableSlots` so it only contains the slots at the earliest available time. - static func filter(playableSlots: inout OrderedSet) { + static func filter(playableSlots: inout some SetOfAvailableSlots) { var earliestTime = TimeIndex.max - for slot in playableSlots { + playableSlots.forEach { slot in if slot.time < earliestTime { earliestTime = slot.time } diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift index 00a424f..43146f5 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift @@ -2,14 +2,14 @@ import OrderedCollections struct SelectSlotEarliestTimeAndSameLocationIfB2B: SelectSlotProtocol, ~Copyable { - func select( + func select( team1: Entry.IDValue, team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playsAtTimes: PlaysAtTimes, + playsAtTimes: borrowing PlaysAtTimesArray, playsAtLocations: PlaysAtLocations, - playableSlots: inout OrderedSet + playableSlots: inout some SetOfAvailableSlots ) -> AvailableSlot? { guard !playableSlots.isEmpty else { return nil } let homePlaysAtTimes = playsAtTimes[unchecked: team1] @@ -53,7 +53,7 @@ struct SelectSlotEarliestTimeAndSameLocationIfB2B: SelectSlotProtocol, ~Copyable } else { nonBackToBackSlots.append(targetSlot) } - playableSlots.remove(targetSlot) + playableSlots.removeMember(targetSlot) } return nonBackToBackSlots.first } diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift index d2784ed..87df952 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift @@ -2,14 +2,14 @@ import OrderedCollections struct SelectSlotNormal: SelectSlotProtocol, ~Copyable { - func select( + func select( team1: Entry.IDValue, team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playsAtTimes: PlaysAtTimes, + playsAtTimes: borrowing PlaysAtTimesArray, playsAtLocations: PlaysAtLocations, - playableSlots: inout OrderedSet + playableSlots: inout some SetOfAvailableSlots ) -> AvailableSlot? { return Self.select( team1: team1, @@ -28,7 +28,7 @@ extension SelectSlotNormal { team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playableSlots: OrderedSet + playableSlots: some SetOfAvailableSlots ) -> AvailableSlot? { guard !playableSlots.isEmpty else { return nil } let team1Times = assignedTimes[unchecked: team1] @@ -50,14 +50,18 @@ extension SelectSlotNormal { team1Locations: AssignedLocations.Element, team2Times: AssignedTimes.Element, team2Locations: AssignedLocations.Element, - playableSlots: OrderedSet + playableSlots: some SetOfAvailableSlots ) -> AvailableSlot? { - var selected = getSelectedSlot(playableSlots[playableSlots.startIndex], team1Times, team1Locations, team2Times, team2Locations) - for slot in playableSlots[playableSlots.index(after: playableSlots.startIndex)...] { - let minimum = getMinimumAssigned(slot, team1Times, team1Locations, team2Times, team2Locations) - if minimum <= selected.minimumAssigned { - selected.slot = slot - selected.minimumAssigned = minimum + var selected:SelectedSlot! = nil + playableSlots.forEach { slot in + if selected == nil { + selected = getSelectedSlot(slot, team1Times, team1Locations, team2Times, team2Locations) + } else { + let minimum = getMinimumAssigned(slot, team1Times, team1Locations, team2Times, team2Locations) + if minimum <= selected.minimumAssigned { + selected.slot = slot + selected.minimumAssigned = minimum + } } } return selected.slot diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift index cc16f6e..4168c97 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift @@ -2,13 +2,13 @@ import OrderedCollections protocol SelectSlotProtocol: Sendable, ~Copyable { - func select( + func select( team1: Entry.IDValue, team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playsAtTimes: PlaysAtTimes, + playsAtTimes: borrowing PlaysAtTimesArray, playsAtLocations: PlaysAtLocations, - playableSlots: inout OrderedSet + playableSlots: inout some SetOfAvailableSlots ) -> AvailableSlot? } \ No newline at end of file diff --git a/Sources/league-scheduling/extensions/OrderedSet+AbstractSet.swift b/Sources/league-scheduling/extensions/OrderedSet+AbstractSet.swift new file mode 100644 index 0000000..932af5b --- /dev/null +++ b/Sources/league-scheduling/extensions/OrderedSet+AbstractSet.swift @@ -0,0 +1,46 @@ + +import OrderedCollections + +extension OrderedSet: AbstractSet { + init(minimumCapacity: Int) { + self.init() + reserveCapacity(minimumCapacity) + } + + @inline(__always) + mutating func removeMember(_ member: Element) { + self.remove(member) + } + + @inline(__always) + mutating func removeAll() { + self.removeAll(keepingCapacity: false) + } + @inline(__always) + mutating func removeAllKeepingCapacity() { + self.removeAll(keepingCapacity: true) + } + + mutating func removeAll(where condition: (Element) throws -> Bool) rethrows { + var iterator = makeIterator() + while let next = iterator.next() { + if try condition(next) { + remove(next) + } + } + } + + func forEachWithReturn(_ body: (Element) throws -> Result?) rethrows -> Result? { + for e in self { + if let r = try body(e) { + return r + } + } + return nil + } + + @inline(__always) + mutating func insertMember(_ member: Element) { + append(member) + } +} \ No newline at end of file diff --git a/Sources/league-scheduling/extensions/Set+AbstractSet.swift b/Sources/league-scheduling/extensions/Set+AbstractSet.swift new file mode 100644 index 0000000..bb44517 --- /dev/null +++ b/Sources/league-scheduling/extensions/Set+AbstractSet.swift @@ -0,0 +1,39 @@ + +extension Set: AbstractSet { + @inline(__always) + mutating func removeMember(_ member: Element) { + self.remove(member) + } + + @inline(__always) + mutating func removeAll() { + self.removeAll(keepingCapacity: false) + } + @inline(__always) + mutating func removeAllKeepingCapacity() { + self.removeAll(keepingCapacity: true) + } + + mutating func removeAll(where condition: (Element) throws -> Bool) rethrows { + var iterator = makeIterator() + while let next = iterator.next() { + if try condition(next) { + remove(next) + } + } + } + + func forEachWithReturn(_ body: (Element) throws -> Result?) rethrows -> Result? { + for e in self { + if let r = try body(e) { + return r + } + } + return nil + } + + @inline(__always) + mutating func insertMember(_ member: Element) { + insert(member) + } +} \ No newline at end of file diff --git a/Sources/league-scheduling/globals.swift b/Sources/league-scheduling/globals.swift index 23b3705..635efb6 100644 --- a/Sources/league-scheduling/globals.swift +++ b/Sources/league-scheduling/globals.swift @@ -2,23 +2,23 @@ import OrderedCollections // MARK: adjacent times -func calculateAdjacentTimes( +func calculateAdjacentTimes( for time: TimeIndex, entryMatchupsPerGameDay: EntryMatchupsPerGameDay -) -> OrderedSet { - var adjacentTimes = OrderedSet() +) -> TimeSet { + var adjacentTimes = TimeSet() let timeIndex = time % entryMatchupsPerGameDay if timeIndex == 0 { for i in 1..> typealias MaximumSameOpponentMatchupsCap = UInt32 -/// Remaining allocations allowed for a matchup pair, for a `DayIndex`. -/// -/// - Usage: [`Entry.IDValue`: `the number of remaining allocations`] -typealias RemainingAllocations = ContiguousArray> - /// When entries can play against each other again. /// /// - Usage: [`Entry.IDValue`: [opponent `Entry.IDValue`: `RecurringDayLimitInterval`]] @@ -72,17 +67,7 @@ typealias MaximumTimeAllocations = ContiguousArray> /// - Usage: [`Entry.IDValue`: [`LocationIndex`: `maximum allowed at LocationIndex`]] typealias MaximumLocationAllocations = ContiguousArray> -/// Times where an entry has already played at for the `day`. -/// -/// - Usage: [`Entry.IDValue`: `Set`] -typealias PlaysAtTimes = ContiguousArray> - /// Locations where an entry has already played at for the `day`. /// /// - Usage: [`Entry.IDValue`: `Set`] -typealias PlaysAtLocations = ContiguousArray> - -/// Slots where an entry has already played at for the `day`. -/// -/// - Usage: [`Entry.IDValue`: `Set`] -typealias PlaysAt = ContiguousArray> \ No newline at end of file +typealias PlaysAtLocations = ContiguousArray> \ No newline at end of file diff --git a/Sources/league-scheduling/util/array/AbstractArray.swift b/Sources/league-scheduling/util/array/AbstractArray.swift new file mode 100644 index 0000000..35ce142 --- /dev/null +++ b/Sources/league-scheduling/util/array/AbstractArray.swift @@ -0,0 +1,12 @@ + +protocol AbstractArray: Sendable, ~Copyable { + associatedtype Index + associatedtype Element:Sendable + + init() + + mutating func reserveCapacity(_ minimumCapacity: Int) +} + +extension ContiguousArray: AbstractArray { +} \ No newline at end of file diff --git a/Sources/league-scheduling/util/array/PlaysAtTimesArray.swift b/Sources/league-scheduling/util/array/PlaysAtTimesArray.swift new file mode 100644 index 0000000..1329b6a --- /dev/null +++ b/Sources/league-scheduling/util/array/PlaysAtTimesArray.swift @@ -0,0 +1,22 @@ + +struct PlaysAtTimesArray { + internal private(set) var times:ContiguousArray + + subscript(unchecked index: some FixedWidthInteger) -> TimeSet { + times[unchecked: index] + } + + mutating func removeAllKeepingCapacity() { + for i in 0..) + init(minimumCapacity: Int) + + var count: Int { get } + var isEmpty: Bool { get } + + /// Returns a Boolean value that indicates whether the given element exists + /// in the set. + func contains(_ member: Element) -> Bool + + mutating func reserveCapacity(_ minimumCapacity: Int) + + /// Inserts the given element in the set if it is not already present. + mutating func insertMember(_ member: Element) + + /// Removes the specified element from the set. + mutating func removeMember(_ member: Element) + + mutating func removeAll() + mutating func removeAllKeepingCapacity() + mutating func removeAll(where condition: (Element) throws -> Bool) rethrows + + mutating func formUnion(_ other: borrowing Self) + + func randomElement() -> Element? + func randomElement(using: inout some RandomNumberGenerator) -> Element? + + func forEach(_ body: (Element) throws -> Void) rethrows + func forEachWithReturn(_ body: (Element) throws -> Result?) rethrows -> Result? + + //subscript(unchecked index: some FixedWidthInteger) -> Element { get set } + + func filter(_ closure: (Element) throws -> Bool) rethrows -> Self + + var first: Element? { get } + func first(where condition: (Element) throws -> Bool) rethrows -> Element? + + /// Returns a new set with the elements that are common to both this set and + /// the given sequence. + /// + /// In the following example, the `bothNeighborsAndEmployees` set is made up + /// of the elements that are in *both* the `employees` and `neighbors` sets. + /// Elements that are in only one or the other are left out of the result of + /// the intersection. + /// + /// let employees: Set = ["Alicia", "Bethany", "Chris", "Diana", "Eric"] + /// let neighbors: Set = ["Bethany", "Eric", "Forlani", "Greta"] + /// let bothNeighborsAndEmployees = employees.intersection(neighbors) + /// print(bothNeighborsAndEmployees) + /// // Prints "["Bethany", "Eric"]" + /// + /// - Parameter other: Another set. + /// - Returns: A new set. + func intersection(_ other: borrowing Self) -> Self +} \ No newline at end of file diff --git a/Sources/league-scheduling/util/set/SetOfAvailableSlots.swift b/Sources/league-scheduling/util/set/SetOfAvailableSlots.swift new file mode 100644 index 0000000..0ad1e9e --- /dev/null +++ b/Sources/league-scheduling/util/set/SetOfAvailableSlots.swift @@ -0,0 +1,8 @@ + +import OrderedCollections + +protocol SetOfAvailableSlots: AbstractSet, ~Copyable where Element == AvailableSlot { +} + +extension Set: SetOfAvailableSlots {} +extension OrderedSet: SetOfAvailableSlots {} \ No newline at end of file diff --git a/Sources/league-scheduling/util/set/SetOfEntryIDs.swift b/Sources/league-scheduling/util/set/SetOfEntryIDs.swift new file mode 100644 index 0000000..68b6348 --- /dev/null +++ b/Sources/league-scheduling/util/set/SetOfEntryIDs.swift @@ -0,0 +1,60 @@ + +import OrderedCollections + +protocol SetOfEntryIDs: AbstractSet, ~Copyable where Element == Entry.IDValue { + /// - Returns: The available matchup pairs that can play for the `day`. + func availableMatchupPairs( + assignedEntryHomeAways: AssignedEntryHomeAways, + maxSameOpponentMatchups: MaximumSameOpponentMatchups + ) -> DeterministicMatchupPairSet +} + +extension Set: SetOfEntryIDs { + func availableMatchupPairs( + assignedEntryHomeAways: AssignedEntryHomeAways, + maxSameOpponentMatchups: MaximumSameOpponentMatchups + ) -> DeterministicMatchupPairSet { + guard !isEmpty else { return .init() } // https://github.com/apple/swift-collections/issues/608 + var pairs = DeterministicMatchupPairSet() + pairs.reserveCapacity((count-1) * 2) + let sortedEntries = sorted() + var index = 0 + while index < sortedEntries.count - 1 { + let home = sortedEntries[index] + index += 1 + let assignedHome = assignedEntryHomeAways[unchecked: home] + let maxSameOpponentMatchups = maxSameOpponentMatchups[unchecked: home] + for away in sortedEntries[index...] { + if assignedHome[unchecked: away].sum < maxSameOpponentMatchups[unchecked: away] { + pairs.insertMember(.init(team1: home, team2: away)) + } + } + } + return pairs + } +} + +extension OrderedSet: SetOfEntryIDs { + func availableMatchupPairs( + assignedEntryHomeAways: AssignedEntryHomeAways, + maxSameOpponentMatchups: MaximumSameOpponentMatchups + ) -> DeterministicMatchupPairSet { + guard !isEmpty else { return .init() } // https://github.com/apple/swift-collections/issues/608 + var pairs = DeterministicMatchupPairSet() + pairs.reserveCapacity((count-1) * 2) + let sortedEntries = sorted() + var index = 0 + while index < sortedEntries.count - 1 { + let home = sortedEntries[index] + index += 1 + let assignedHome = assignedEntryHomeAways[unchecked: home] + let maxSameOpponentMatchups = maxSameOpponentMatchups[unchecked: home] + for away in sortedEntries[index...] { + if assignedHome[unchecked: away].sum < maxSameOpponentMatchups[unchecked: away] { + pairs.insertMember(.init(team1: home, team2: away)) + } + } + } + return pairs + } +} \ No newline at end of file diff --git a/Sources/league-scheduling/util/set/SetOfMatchupPair.swift b/Sources/league-scheduling/util/set/SetOfMatchupPair.swift new file mode 100644 index 0000000..ee8c671 --- /dev/null +++ b/Sources/league-scheduling/util/set/SetOfMatchupPair.swift @@ -0,0 +1,8 @@ + +import OrderedCollections + +protocol SetOfMatchupPair: AbstractSet, ~Copyable where Element == MatchupPair { +} + +extension Set: SetOfMatchupPair {} +extension OrderedSet: SetOfMatchupPair {} \ No newline at end of file diff --git a/Sources/league-scheduling/util/set/SetOfUInt32.swift b/Sources/league-scheduling/util/set/SetOfUInt32.swift new file mode 100644 index 0000000..7b7a8f3 --- /dev/null +++ b/Sources/league-scheduling/util/set/SetOfUInt32.swift @@ -0,0 +1,11 @@ + +import OrderedCollections + +protocol SetOfUInt32: AbstractSet, ~Copyable where Element == UInt32 {} + +typealias SetOfDayIndexes = SetOfUInt32 +typealias SetOfTimeIndexes = SetOfUInt32 +typealias SetOfLocationIndexes = SetOfUInt32 + +extension Set: SetOfUInt32 {} +extension OrderedSet: SetOfUInt32 {} \ No newline at end of file diff --git a/Tests/LeagueSchedulingTests/CanPlayAtTests.swift b/Tests/LeagueSchedulingTests/CanPlayAtTests.swift index 721f49f..dc415d8 100644 --- a/Tests/LeagueSchedulingTests/CanPlayAtTests.swift +++ b/Tests/LeagueSchedulingTests/CanPlayAtTests.swift @@ -12,8 +12,8 @@ struct CanPlayAtTests { let locations = 3 var gameGap = GameGap.upTo(1).minMax - var playsAt:PlaysAt.Element = [] - var playsAtTimes:PlaysAtTimes.Element = [] + var playsAt:some SetOfAvailableSlots = Set() + var playsAtTimes:OrderedSet = [] var timeNumbers:AssignedTimes.Element = .init(repeating: 0, count: times) var locationNumbers:AssignedLocations.Element = .init(repeating: 0, count: locations) let maxTimeNumbers:MaximumTimeAllocations.Element = .init(repeating: 1, count: times) @@ -47,7 +47,7 @@ struct CanPlayAtTests { )) } - playsAt.append(AvailableSlot(time: 0, location: location)) + playsAt.insertMember(AvailableSlot(time: 0, location: location)) playsAtTimes.append(0) #expect(!CanPlayAtNormal.test( time: 0, @@ -62,7 +62,7 @@ struct CanPlayAtTests { gameGap: gameGap )) - playsAt = [] + playsAt.removeAll() playsAtTimes = [] timeNumbers[0] = 1 #expect(!CanPlayAtNormal.test( @@ -99,7 +99,7 @@ extension CanPlayAtTests { ] var time:TimeIndex = 0 var location:LocationIndex = 0 - var playsAt:PlaysAt.Element = [] + var playsAt:some SetOfAvailableSlots = Set() var gameGap = GameGap.upTo(5).minMax #expect(CanPlayAtWithTravelDurations.test( @@ -113,7 +113,7 @@ extension CanPlayAtTests { )) matchupDuration = .minutes(30) - playsAt = [AvailableSlot(time: 1, location: 0)] + playsAt = .init([AvailableSlot(time: 1, location: 0)]) #expect(CanPlayAtWithTravelDurations.test( startingTimes: startingTimes, matchupDuration: matchupDuration, diff --git a/Tests/LeagueSchedulingTests/MatchupBlockTests.swift b/Tests/LeagueSchedulingTests/MatchupBlockTests.swift index 3dd7e93..a4c4571 100644 --- a/Tests/LeagueSchedulingTests/MatchupBlockTests.swift +++ b/Tests/LeagueSchedulingTests/MatchupBlockTests.swift @@ -1,5 +1,6 @@ @testable import LeagueScheduling +import OrderedCollections import StaticDateTimes import Testing @@ -11,7 +12,7 @@ struct MatchupBlockTests: ScheduleExpectations { extension MatchupBlockTests { @Test(.timeLimit(.minutes(1))) func adjacentTimes() { - var adjacent = calculateAdjacentTimes(for: 0, entryMatchupsPerGameDay: 2) + var adjacent:OrderedSet = calculateAdjacentTimes(for: 0, entryMatchupsPerGameDay: 2) #expect(adjacent == [1]) adjacent = calculateAdjacentTimes(for: 0, entryMatchupsPerGameDay: 3) From 1d0c3bb897a442187394f0480898ff25a21df296 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Fri, 20 Mar 2026 08:30:48 -0500 Subject: [PATCH 08/19] minor improvements --- Sources/league-scheduling/data/LeagueScheduleData.swift | 8 +++----- Sources/league-scheduling/data/SelectMatchup.swift | 2 +- Sources/league-scheduling/data/assignment/Assign.swift | 2 +- .../extensions/OrderedSet+AbstractSet.swift | 9 --------- .../league-scheduling/util/array/PlaysAtTimesArray.swift | 2 +- Sources/league-scheduling/util/set/AbstractSet.swift | 2 ++ .../schedules/expectations/ScheduleExpectations.swift | 2 +- 7 files changed, 9 insertions(+), 18 deletions(-) diff --git a/Sources/league-scheduling/data/LeagueScheduleData.swift b/Sources/league-scheduling/data/LeagueScheduleData.swift index 9c04fb6..9f0ff57 100644 --- a/Sources/league-scheduling/data/LeagueScheduleData.swift +++ b/Sources/league-scheduling/data/LeagueScheduleData.swift @@ -125,11 +125,9 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: defaultMaxEntryMatchupsPerGameDay ) - entriesInDivision.forEach { entryID in - if assignmentState.numberOfAssignedMatchups[unchecked: entryID] >= daySettings.maximumPlayableMatchups[unchecked: entryID] { - entriesInDivision.removeMember(entryID) - } - } + entriesInDivision.removeAll(where: { entryID in + assignmentState.numberOfAssignedMatchups[unchecked: entryID] >= daySettings.maximumPlayableMatchups[unchecked: entryID] + }) entryCountsForDivision[divisionIndex] = entriesInDivision.count expectedMatchupsCount += (entriesInDivision.count * defaultMaxEntryMatchupsPerGameDay) / entriesPerMatchup diff --git a/Sources/league-scheduling/data/SelectMatchup.swift b/Sources/league-scheduling/data/SelectMatchup.swift index e2fc99a..0a4f8cd 100644 --- a/Sources/league-scheduling/data/SelectMatchup.swift +++ b/Sources/league-scheduling/data/SelectMatchup.swift @@ -163,7 +163,7 @@ extension AssignmentState { } } #if LOG - print("SelectMatchup;selectMatchup;selected.pair=\(selected.pair.description);pool=\(pool.map({ $0.description }))") + print("SelectMatchup;selectMatchup;selected.pair=\(selected?.pair.description);pool=\(pool.map({ $0.description }))") #endif return pool.isEmpty ? selected?.pair : pool.randomElement(using: &rng) } diff --git a/Sources/league-scheduling/data/assignment/Assign.swift b/Sources/league-scheduling/data/assignment/Assign.swift index 8e38918..a5af35b 100644 --- a/Sources/league-scheduling/data/assignment/Assign.swift +++ b/Sources/league-scheduling/data/assignment/Assign.swift @@ -81,7 +81,7 @@ extension AssignmentState { } #if LOG - for av in availableMatchups { + availableMatchups.forEach { av in if assignedEntryHomeAways[unchecked: av.team1][unchecked: av.team2].sum == maxSameOpponentMatchups[unchecked: av.team1][unchecked: av.team2] { fatalError("assign;day=\(day);gameGap=\(gameGap);matchup=\(matchup);av=\(av);availableSlots.count=\(availableSlots.count);matchups.count=\(matchups.count)") } else if assignedEntryHomeAways[unchecked: av.team2][unchecked: av.team1].sum == maxSameOpponentMatchups[unchecked: av.team2][unchecked: av.team1] { diff --git a/Sources/league-scheduling/extensions/OrderedSet+AbstractSet.swift b/Sources/league-scheduling/extensions/OrderedSet+AbstractSet.swift index 932af5b..4726eee 100644 --- a/Sources/league-scheduling/extensions/OrderedSet+AbstractSet.swift +++ b/Sources/league-scheduling/extensions/OrderedSet+AbstractSet.swift @@ -21,15 +21,6 @@ extension OrderedSet: AbstractSet { self.removeAll(keepingCapacity: true) } - mutating func removeAll(where condition: (Element) throws -> Bool) rethrows { - var iterator = makeIterator() - while let next = iterator.next() { - if try condition(next) { - remove(next) - } - } - } - func forEachWithReturn(_ body: (Element) throws -> Result?) rethrows -> Result? { for e in self { if let r = try body(e) { diff --git a/Sources/league-scheduling/util/array/PlaysAtTimesArray.swift b/Sources/league-scheduling/util/array/PlaysAtTimesArray.swift index 1329b6a..6f221d2 100644 --- a/Sources/league-scheduling/util/array/PlaysAtTimesArray.swift +++ b/Sources/league-scheduling/util/array/PlaysAtTimesArray.swift @@ -1,5 +1,5 @@ -struct PlaysAtTimesArray { +struct PlaysAtTimesArray: Sendable { internal private(set) var times:ContiguousArray subscript(unchecked index: some FixedWidthInteger) -> TimeSet { diff --git a/Sources/league-scheduling/util/set/AbstractSet.swift b/Sources/league-scheduling/util/set/AbstractSet.swift index 98066b3..2572044 100644 --- a/Sources/league-scheduling/util/set/AbstractSet.swift +++ b/Sources/league-scheduling/util/set/AbstractSet.swift @@ -58,4 +58,6 @@ protocol AbstractSet: Sendable, ~Copyable { /// - Parameter other: Another set. /// - Returns: A new set. func intersection(_ other: borrowing Self) -> Self + + func map(_ body: (Element) throws -> Result) rethrows -> [Result] } \ No newline at end of file diff --git a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift index c29befc..b1ac409 100644 --- a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift +++ b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift @@ -12,7 +12,7 @@ extension GenerationConstraints { regenerationAttemptsForFirstDay: Self.default.regenerationAttemptsForFirstDay, regenerationAttemptsForConsecutiveDay: Self.default.regenerationAttemptsForConsecutiveDay, regenerationAttemptsThreshold: Self.default.regenerationAttemptsThreshold, - determinism: .init(seed: 69_420) + determinism: .init(seed: 69) ) } From 59ba9fad7187d010981fa711505967155df4f69d Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Fri, 20 Mar 2026 08:41:55 -0500 Subject: [PATCH 09/19] use non-deterministic output by default for the unit tests - we should add separate unit tests to test deterministic schedule generation --- .../schedules/expectations/ScheduleExpectations.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift index b1ac409..9c0415e 100644 --- a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift +++ b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift @@ -12,7 +12,7 @@ extension GenerationConstraints { regenerationAttemptsForFirstDay: Self.default.regenerationAttemptsForFirstDay, regenerationAttemptsForConsecutiveDay: Self.default.regenerationAttemptsForConsecutiveDay, regenerationAttemptsThreshold: Self.default.regenerationAttemptsThreshold, - determinism: .init(seed: 69) + determinism: nil//.init(seed: 69) ) } From ebaf9b02129256b4be1e39da76f1c7ea8674d23d Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Fri, 20 Mar 2026 09:08:08 -0500 Subject: [PATCH 10/19] drop the `Deterministic` name prefix for the `ScheduleConfiguration` associated types --- .../data/AssignMatchup.swift | 8 +++---- .../league-scheduling/data/AssignSlots.swift | 4 ++-- .../data/AssignmentState.swift | 20 ++++++++--------- .../data/BalanceHomeAway.swift | 2 +- .../league-scheduling/data/Generation.swift | 22 +++++++++---------- .../data/LeagueScheduleData.swift | 10 ++++----- .../data/LeagueScheduleDataSnapshot.swift | 6 ++--- .../league-scheduling/data/MatchupBlock.swift | 14 ++++++------ .../data/PrioritizedMatchups.swift | 16 +++++++------- .../data/SelectMatchup.swift | 4 ++-- Sources/league-scheduling/data/Shuffle.swift | 2 +- .../data/assignment/Move.swift | 4 ++-- .../data/assignment/Unassign.swift | 4 ++-- .../ScheduleConfiguration.swift | 17 +++++++------- .../util/set/SetOfEntryIDs.swift | 16 +++++++------- 15 files changed, 74 insertions(+), 75 deletions(-) diff --git a/Sources/league-scheduling/data/AssignMatchup.swift b/Sources/league-scheduling/data/AssignMatchup.swift index b6aae84..f88a98a 100644 --- a/Sources/league-scheduling/data/AssignMatchup.swift +++ b/Sources/league-scheduling/data/AssignMatchup.swift @@ -7,7 +7,7 @@ extension LeagueScheduleData { @discardableResult mutating func assignMatchupPair( _ pair: MatchupPair, - allAvailableMatchups: Config.DeterministicMatchupPairSet, + allAvailableMatchups: Config.MatchupPairSet, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { @@ -36,7 +36,7 @@ extension AssignmentState { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Config.DeterministicMatchupPairSet, + allAvailableMatchups: Config.MatchupPairSet, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { @@ -93,13 +93,13 @@ extension AssignmentState { // MARK: Playable slots extension AssignmentState { - func playableSlots(for pair: MatchupPair) -> Config.DeterministicAvailableSlotSet { + func playableSlots(for pair: MatchupPair) -> Config.AvailableSlotSet { return Self.playableSlots(for: pair, remainingAllocations: remainingAllocations) } static func playableSlots( for pair: MatchupPair, remainingAllocations: Config.RemainingAllocations - ) -> Config.DeterministicAvailableSlotSet { + ) -> Config.AvailableSlotSet { return remainingAllocations[unchecked: pair.team1].intersection(remainingAllocations[unchecked: pair.team2]) } } \ No newline at end of file diff --git a/Sources/league-scheduling/data/AssignSlots.swift b/Sources/league-scheduling/data/AssignSlots.swift index ff95cdf..537c224 100644 --- a/Sources/league-scheduling/data/AssignSlots.swift +++ b/Sources/league-scheduling/data/AssignSlots.swift @@ -247,7 +247,7 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Config.DeterministicMatchupPairSet, + allAvailableMatchups: Config.MatchupPairSet, rng: inout some RandomNumberGenerator, assignmentState: inout AssignmentState, shouldSkipSelection: (MatchupPair) -> Bool, @@ -298,7 +298,7 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Config.DeterministicMatchupPairSet, + allAvailableMatchups: Config.MatchupPairSet, rng: inout some RandomNumberGenerator, assignmentState: inout AssignmentState, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, diff --git a/Sources/league-scheduling/data/AssignmentState.swift b/Sources/league-scheduling/data/AssignmentState.swift index 405b16c..ba6986b 100644 --- a/Sources/league-scheduling/data/AssignmentState.swift +++ b/Sources/league-scheduling/data/AssignmentState.swift @@ -46,20 +46,20 @@ struct AssignmentState: Sendable, ~Copyable { let maxSameOpponentMatchups:MaximumSameOpponentMatchups /// All matchup pairs that can be scheduled. - var allMatchups:Config.DeterministicMatchupPairSet + var allMatchups:Config.MatchupPairSet /// All matchup pairs that can be scheduled, grouped by division. /// /// - Usage: [`Division.IDValue`: `available matchups`] - var allDivisionMatchups:ContiguousArray + var allDivisionMatchups:ContiguousArray /// Remaining available matchup pairs that can be assigned for the `day`. - var availableMatchups:Config.DeterministicMatchupPairSet + var availableMatchups:Config.MatchupPairSet - var prioritizedEntries:Config.DeterministicEntryIDSet + var prioritizedEntries:Config.EntryIDSet /// Remaining available slots that can be filled for the `day`. - var availableSlots:Config.DeterministicAvailableSlotSet + var availableSlots:Config.AvailableSlotSet var playsAt:Config.PlaysAt var playsAtTimes:PlaysAtTimesArray @@ -165,20 +165,20 @@ struct AssignmentStateCopyable { var maxSameOpponentMatchups:MaximumSameOpponentMatchups /// All matchup pairs that can be scheduled - var allMatchups:Config.DeterministicMatchupPairSet + var allMatchups:Config.MatchupPairSet /// All matchup pairs that can be scheduled, grouped by division. /// /// - Usage: [`Division.IDValue`: `available matchups`] - var allDivisionMatchups:ContiguousArray + var allDivisionMatchups:ContiguousArray /// Remaining available matchup pairs that can be assigned for the `day`. - var availableMatchups:Config.DeterministicMatchupPairSet + var availableMatchups:Config.MatchupPairSet - var prioritizedEntries:Config.DeterministicEntryIDSet + var prioritizedEntries:Config.EntryIDSet /// Remaining available slots that can be filled for the `day`. - var availableSlots:Config.DeterministicAvailableSlotSet + var availableSlots:Config.AvailableSlotSet var playsAt:Config.PlaysAt var playsAtTimes:PlaysAtTimesArray diff --git a/Sources/league-scheduling/data/BalanceHomeAway.swift b/Sources/league-scheduling/data/BalanceHomeAway.swift index b618d02..bbe893a 100644 --- a/Sources/league-scheduling/data/BalanceHomeAway.swift +++ b/Sources/league-scheduling/data/BalanceHomeAway.swift @@ -56,7 +56,7 @@ extension LeagueScheduleData { #endif let now = clock.now - var unbalancedEntryIDs = Config.DeterministicEntryIDSet() + var unbalancedEntryIDs = Config.EntryIDSet() unbalancedEntryIDs.reserveCapacity(entriesCount) var neededFlipsToBalance = [(home: UInt8, away: UInt8)](repeating: (0, 0), count: entriesCount) for entryID in 0.. [LeagueGenerationData] { - var divisionEntries:ContiguousArray = .init(repeating: .init(), count: divisions.count) + var divisionEntries:ContiguousArray = .init(repeating: .init(), count: divisions.count) for entryIndex in 0..( divisionsCount: Int, - divisionEntries: ContiguousArray, + divisionEntries: ContiguousArray, maxStartingTimes: TimeIndex, maxLocations: LocationIndex, dataSnapshot: LeagueScheduleDataSnapshot ) async throws -> [LeagueGenerationData] { - var grouped = [DayOfWeek:Config.DeterministicEntryIDSet]() + var grouped = [DayOfWeek:Config.EntryIDSet]() for (divisionID, division) in divisions.enumerated() { grouped[DayOfWeek(division.dayOfWeek), default: .init()].formUnion(divisionEntries[divisionID]) } @@ -233,7 +233,7 @@ extension RequestPayload.Runtime { divisionsCount: Int, maxStartingTimes: TimeIndex, maxLocations: LocationIndex, - scheduledEntries: Config.DeterministicEntryIDSet + scheduledEntries: Config.EntryIDSet ) -> LeagueGenerationData { let gameDays = settings.gameDays var generationData = LeagueGenerationData() @@ -242,7 +242,7 @@ extension RequestPayload.Runtime { generationData.schedule = .init(repeating: OrderedSet(), count: gameDays) var dataSnapshot = copy dataSnapshot - var gameDayDivisionEntries:ContiguousArray> = .init(repeating: .init(repeating: .init(), count: divisionsCount), count: gameDays) + var gameDayDivisionEntries:ContiguousArray> = .init(repeating: .init(repeating: .init(), count: divisionsCount), count: gameDays) loadMaxAllocations( dataSnapshot: &dataSnapshot, gameDayDivisionEntries: &gameDayDivisionEntries, @@ -262,7 +262,7 @@ extension RequestPayload.Runtime { if gameDaySettingValuesCount <= day { gameDaySettingValuesCount += 1 let daySettings = settings.daySettings[unchecked: day].general - let availableSlots:Config.DeterministicAvailableSlotSet = Self.availableSlots( + let availableSlots:Config.AvailableSlotSet = Self.availableSlots( times: daySettings.timeSlots, locations: daySettings.locations, locationTimeExclusivity: daySettings.locationTimeExclusivities @@ -357,11 +357,11 @@ extension RequestPayload.Runtime { extension RequestPayload.Runtime { static func loadMaxAllocations( dataSnapshot: inout LeagueScheduleDataSnapshot, - gameDayDivisionEntries: inout ContiguousArray>, + gameDayDivisionEntries: inout ContiguousArray>, settings: borrowing RequestPayload.Runtime, maxStartingTimes: TimeIndex, maxLocations: LocationIndex, - scheduledEntries: Config.DeterministicEntryIDSet + scheduledEntries: Config.EntryIDSet ) { scheduledEntries.forEach { entryIndex in let entry = settings.entries[unchecked: entryIndex] @@ -471,12 +471,12 @@ extension RequestPayload.Runtime { // MARK: Get available slots extension RequestPayload.Runtime { - static func availableSlots( + static func availableSlots( times: TimeIndex, locations: LocationIndex, locationTimeExclusivity: [Set]? - ) -> DeterministicAvailableSlotSet { - var slots = DeterministicAvailableSlotSet() + ) -> AvailableSlotSet { + var slots = AvailableSlotSet() slots.reserveCapacity(Int(times) * locations) if let exclusivities = locationTimeExclusivity { for location in 0.., - availableSlots: Config.DeterministicAvailableSlotSet, + divisionEntries: ContiguousArray, + availableSlots: Config.AvailableSlotSet, settings: RequestPayload.Runtime, generationData: inout LeagueGenerationData ) throws(LeagueError) { @@ -112,8 +112,8 @@ extension LeagueScheduleData { self.prioritizeEarlierTimes = daySettings.prioritizeEarlierTimes self.gameGap = daySettings.gameGap.minMax self.sameLocationIfB2B = daySettings.sameLocationIfB2B - var availableMatchups = Config.DeterministicMatchupPairSet() - var prioritizedEntries = Config.DeterministicEntryIDSet() + var availableMatchups = Config.MatchupPairSet() + var prioritizedEntries = Config.EntryIDSet() prioritizedEntries.reserveCapacity(entriesCount) var entryCountsForDivision:ContiguousArray = .init(repeating: 0, count: divisionEntries.count) expectedMatchupsCount = 0 @@ -135,7 +135,7 @@ extension LeagueScheduleData { #if LOG print("LeagueScheduleData;newDay;day=\(day);expectedMatchupsCount=\(expectedMatchupsCount);divisionIndex=\(divisionIndex);entryCountsForDivision=\(entriesInDivision.count);divisionRecurringDayLimitInterval=\(divisionRecurringDayLimitInterval[divisionIndex])") #endif - let availableDivisionMatchups:Config.DeterministicMatchupPairSet = entriesInDivision.availableMatchupPairs( + let availableDivisionMatchups:Config.MatchupPairSet = entriesInDivision.availableMatchupPairs( assignedEntryHomeAways: assignmentState.assignedEntryHomeAways, maxSameOpponentMatchups: assignmentState.maxSameOpponentMatchups ) diff --git a/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift b/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift index a5ab16d..38e78be 100644 --- a/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift +++ b/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift @@ -37,7 +37,7 @@ struct LeagueScheduleDataSnapshot: Sendable { entriesPerMatchup: EntriesPerMatchup, maximumPlayableMatchups: [UInt32], entries: [Entry.Runtime], - divisionEntries: ContiguousArray, + divisionEntries: ContiguousArray, matchupDuration: MatchupDuration, gameGap: (Int, Int), sameLocationIfB2B: Bool, @@ -50,7 +50,7 @@ struct LeagueScheduleDataSnapshot: Sendable { self.gameGap = gameGap self.sameLocationIfB2B = sameLocationIfB2B - var prioritizedEntries = Config.DeterministicEntryIDSet() + var prioritizedEntries = Config.EntryIDSet() prioritizedEntries.reserveCapacity(entriesCount) var entryDivisions = ContiguousArray(repeating: 0, count: entriesCount) for (index, entries) in divisionEntries.enumerated() { @@ -62,7 +62,7 @@ struct LeagueScheduleDataSnapshot: Sendable { self.entryDivisions = entryDivisions failedMatchupSelections = .init(repeating: Set(), count: entriesCount) - let playsAt = ContiguousArray( + let playsAt = ContiguousArray( repeating: .init(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)), count: entriesCount ) let playsAtTimes = PlaysAtTimesArray( diff --git a/Sources/league-scheduling/data/MatchupBlock.swift b/Sources/league-scheduling/data/MatchupBlock.swift index 9f76877..11f549b 100644 --- a/Sources/league-scheduling/data/MatchupBlock.swift +++ b/Sources/league-scheduling/data/MatchupBlock.swift @@ -78,7 +78,7 @@ extension LeagueScheduleData { #endif // assign initial matchups var adjacentTimes = Config.TimeSet() - var selectedEntries = Config.DeterministicEntryIDSet() + var selectedEntries = Config.EntryIDSet() selectedEntries.reserveCapacity(amount * entriesPerMatchup) // assign the first matchup, prioritizing the matchup's time @@ -231,11 +231,11 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Config.DeterministicMatchupPairSet, + allAvailableMatchups: Config.MatchupPairSet, rng: inout some RandomNumberGenerator, localAssignmentState: inout AssignmentState, - remainingPrioritizedEntries: inout Config.DeterministicEntryIDSet, - selectedEntries: inout Config.DeterministicEntryIDSet, + remainingPrioritizedEntries: inout Config.EntryIDSet, + selectedEntries: inout Config.EntryIDSet, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { @@ -270,12 +270,12 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Config.DeterministicMatchupPairSet, + allAvailableMatchups: Config.MatchupPairSet, rng: inout some RandomNumberGenerator, localAssignmentState: inout AssignmentState, shouldSkipSelection: (MatchupPair) -> Bool, - remainingPrioritizedEntries: inout Config.DeterministicEntryIDSet, - selectedEntries: inout Config.DeterministicEntryIDSet, + remainingPrioritizedEntries: inout Config.EntryIDSet, + selectedEntries: inout Config.EntryIDSet, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> Matchup? { diff --git a/Sources/league-scheduling/data/PrioritizedMatchups.swift b/Sources/league-scheduling/data/PrioritizedMatchups.swift index 11bbe24..a6bc081 100644 --- a/Sources/league-scheduling/data/PrioritizedMatchups.swift +++ b/Sources/league-scheduling/data/PrioritizedMatchups.swift @@ -2,13 +2,13 @@ import OrderedCollections struct PrioritizedMatchups: Sendable, ~Copyable { - private(set) var matchups:Config.DeterministicMatchupPairSet + private(set) var matchups:Config.MatchupPairSet private(set) var availableMatchupCountForEntry:ContiguousArray init( entriesCount: Int, - prioritizedEntries: Config.DeterministicEntryIDSet, - availableMatchups: Config.DeterministicMatchupPairSet + prioritizedEntries: Config.EntryIDSet, + availableMatchups: Config.MatchupPairSet ) { let matchups = Self.filterMatchups(prioritizedEntries: prioritizedEntries, availableMatchups: availableMatchups) var availableMatchupCountForEntry = ContiguousArray(repeating: 0, count: entriesCount) @@ -21,8 +21,8 @@ struct PrioritizedMatchups: Sendable, ~Copyable { } mutating func update( - prioritizedEntries: Config.DeterministicEntryIDSet, - availableMatchups: Config.DeterministicMatchupPairSet + prioritizedEntries: Config.EntryIDSet, + availableMatchups: Config.MatchupPairSet ) { matchups = Self.filterMatchups(prioritizedEntries: prioritizedEntries, availableMatchups: availableMatchups) for i in availableMatchupCountForEntry.indices { @@ -40,9 +40,9 @@ struct PrioritizedMatchups: Sendable, ~Copyable { } private static func filterMatchups( - prioritizedEntries: Config.DeterministicEntryIDSet, - availableMatchups: Config.DeterministicMatchupPairSet - ) -> Config.DeterministicMatchupPairSet { + prioritizedEntries: Config.EntryIDSet, + availableMatchups: Config.MatchupPairSet + ) -> Config.MatchupPairSet { if prioritizedEntries.isEmpty { return availableMatchups } diff --git a/Sources/league-scheduling/data/SelectMatchup.swift b/Sources/league-scheduling/data/SelectMatchup.swift index 0a4f8cd..e17685d 100644 --- a/Sources/league-scheduling/data/SelectMatchup.swift +++ b/Sources/league-scheduling/data/SelectMatchup.swift @@ -42,7 +42,7 @@ extension AssignmentState { // introduce a pool of matchup pairs of equal priority, and random selection, so that we don't repeat identical assignments when // - regenerating a failed day // - selecting the last matchup pair out of previous pairs of equal priority - var pool = Config.DeterministicMatchupPairSet() + var pool = Config.MatchupPairSet() prioritizedMatchups.matchups.forEach { pair in let (pairMinMatchupsPlayedSoFar, pairTotalMatchupsPlayedSoFar) = numberOfMatchupsPlayedSoFar(for: pair, numberOfAssignedMatchups: numberOfAssignedMatchups) if selected == nil { @@ -211,7 +211,7 @@ extension AssignmentState { recurringDayLimit: RecurringDayLimitInterval, remainingAllocations: (min: Int, max: Int), remainingMatchupCount: (min: Int, max: Int), - pool: inout Config.DeterministicMatchupPairSet + pool: inout Config.MatchupPairSet ) -> SelectedMatchup { pool.removeAllKeepingCapacity() pool.insertMember(pair) diff --git a/Sources/league-scheduling/data/Shuffle.swift b/Sources/league-scheduling/data/Shuffle.swift index ad4eca6..7e04072 100644 --- a/Sources/league-scheduling/data/Shuffle.swift +++ b/Sources/league-scheduling/data/Shuffle.swift @@ -12,7 +12,7 @@ extension AssignmentState { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Config.DeterministicMatchupPairSet, + allAvailableMatchups: Config.MatchupPairSet, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) -> AvailableSlot? { // TODO: fix (can get stuck shuffling the same matchup to the same slot) diff --git a/Sources/league-scheduling/data/assignment/Move.swift b/Sources/league-scheduling/data/assignment/Move.swift index 6701ce6..123a265 100644 --- a/Sources/league-scheduling/data/assignment/Move.swift +++ b/Sources/league-scheduling/data/assignment/Move.swift @@ -8,7 +8,7 @@ extension LeagueScheduleData { matchup: Matchup, to slot: AvailableSlot, day: DayIndex, - allAvailableMatchups: Config.DeterministicMatchupPairSet, + allAvailableMatchups: Config.MatchupPairSet, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) { assignmentState.move( @@ -37,7 +37,7 @@ extension AssignmentState { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Config.DeterministicMatchupPairSet, + allAvailableMatchups: Config.MatchupPairSet, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) { #if LOG diff --git a/Sources/league-scheduling/data/assignment/Unassign.swift b/Sources/league-scheduling/data/assignment/Unassign.swift index c86dbbc..edc3856 100644 --- a/Sources/league-scheduling/data/assignment/Unassign.swift +++ b/Sources/league-scheduling/data/assignment/Unassign.swift @@ -11,7 +11,7 @@ extension AssignmentState { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - allAvailableMatchups: Config.DeterministicMatchupPairSet, + allAvailableMatchups: Config.MatchupPairSet, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) { let recurringDayLimitInterval = divisionRecurringDayLimitInterval[unchecked: entryDivisions[unchecked: matchup.home]] @@ -86,7 +86,7 @@ extension AssignmentState { mutating func recalculateAvailableMatchups( day: DayIndex, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, - allAvailableMatchups: Config.DeterministicMatchupPairSet + allAvailableMatchups: Config.MatchupPairSet ) { availableMatchups = allAvailableMatchups.filter({ guard assignedEntryHomeAways[unchecked: $0.team1][unchecked: $0.team2].sum < maxSameOpponentMatchups[unchecked: $0.team1][unchecked: $0.team2] diff --git a/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift index ef790e0..d7cc37e 100644 --- a/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift +++ b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift @@ -4,21 +4,20 @@ import OrderedCollections protocol ScheduleConfiguration: Sendable, ~Copyable { associatedtype RNG:RandomNumberGenerator & Sendable associatedtype TimeSet:SetOfTimeIndexes + associatedtype EntryIDSet:SetOfEntryIDs + associatedtype AvailableSlotSet:SetOfAvailableSlots + associatedtype MatchupPairSet:SetOfMatchupPair - associatedtype DeterministicEntryIDSet:SetOfEntryIDs - associatedtype DeterministicAvailableSlotSet:SetOfAvailableSlots - associatedtype DeterministicMatchupPairSet:SetOfMatchupPair - - typealias RemainingAllocations = ContiguousArray - typealias PlaysAt = ContiguousArray + typealias RemainingAllocations = ContiguousArray + typealias PlaysAt = ContiguousArray typealias PlaysAtTimes = ContiguousArray } enum ScheduleConfig< RNG: RandomNumberGenerator & Sendable, TimeSet: SetOfTimeIndexes, - DeterministicEntryIDSet: SetOfEntryIDs, - DeterministicAvailableSlotSet: SetOfAvailableSlots, - DeterministicMatchupPairSet: SetOfMatchupPair + EntryIDSet: SetOfEntryIDs, + AvailableSlotSet: SetOfAvailableSlots, + MatchupPairSet: SetOfMatchupPair >: ScheduleConfiguration { } \ No newline at end of file diff --git a/Sources/league-scheduling/util/set/SetOfEntryIDs.swift b/Sources/league-scheduling/util/set/SetOfEntryIDs.swift index 68b6348..2924659 100644 --- a/Sources/league-scheduling/util/set/SetOfEntryIDs.swift +++ b/Sources/league-scheduling/util/set/SetOfEntryIDs.swift @@ -3,19 +3,19 @@ import OrderedCollections protocol SetOfEntryIDs: AbstractSet, ~Copyable where Element == Entry.IDValue { /// - Returns: The available matchup pairs that can play for the `day`. - func availableMatchupPairs( + func availableMatchupPairs( assignedEntryHomeAways: AssignedEntryHomeAways, maxSameOpponentMatchups: MaximumSameOpponentMatchups - ) -> DeterministicMatchupPairSet + ) -> MatchupPairSet } extension Set: SetOfEntryIDs { - func availableMatchupPairs( + func availableMatchupPairs( assignedEntryHomeAways: AssignedEntryHomeAways, maxSameOpponentMatchups: MaximumSameOpponentMatchups - ) -> DeterministicMatchupPairSet { + ) -> MatchupPairSet { guard !isEmpty else { return .init() } // https://github.com/apple/swift-collections/issues/608 - var pairs = DeterministicMatchupPairSet() + var pairs = MatchupPairSet() pairs.reserveCapacity((count-1) * 2) let sortedEntries = sorted() var index = 0 @@ -35,12 +35,12 @@ extension Set: SetOfEntryIDs { } extension OrderedSet: SetOfEntryIDs { - func availableMatchupPairs( + func availableMatchupPairs( assignedEntryHomeAways: AssignedEntryHomeAways, maxSameOpponentMatchups: MaximumSameOpponentMatchups - ) -> DeterministicMatchupPairSet { + ) -> MatchupPairSet { guard !isEmpty else { return .init() } // https://github.com/apple/swift-collections/issues/608 - var pairs = DeterministicMatchupPairSet() + var pairs = MatchupPairSet() pairs.reserveCapacity((count-1) * 2) let sortedEntries = sorted() var index = 0 From 37653503ace7dfe6f2e2e8a3d1207234274d09d4 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Fri, 20 Mar 2026 09:15:58 -0500 Subject: [PATCH 11/19] remove unused `OrderedCollections` imports --- Sources/league-scheduling/data/AssignMatchup.swift | 2 -- Sources/league-scheduling/data/AssignSlots.swift | 2 -- Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift | 1 - Sources/league-scheduling/data/PrioritizedMatchups.swift | 2 -- Sources/league-scheduling/data/SelectMatchup.swift | 2 -- Sources/league-scheduling/data/Shuffle.swift | 2 -- Sources/league-scheduling/data/assignment/Move.swift | 2 -- Sources/league-scheduling/data/assignment/Unassign.swift | 2 -- .../data/assignmentState/ScheduleConfiguration.swift | 2 -- Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift | 2 -- .../data/selectSlot/SelectSlotEarliestTime.swift | 2 -- .../selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift | 2 -- .../league-scheduling/data/selectSlot/SelectSlotNormal.swift | 2 -- .../league-scheduling/data/selectSlot/SelectSlotProtocol.swift | 2 -- Sources/league-scheduling/globals.swift | 2 -- Sources/league-scheduling/typealiases.swift | 2 -- 16 files changed, 31 deletions(-) diff --git a/Sources/league-scheduling/data/AssignMatchup.swift b/Sources/league-scheduling/data/AssignMatchup.swift index f88a98a..54320c8 100644 --- a/Sources/league-scheduling/data/AssignMatchup.swift +++ b/Sources/league-scheduling/data/AssignMatchup.swift @@ -1,6 +1,4 @@ -import OrderedCollections - // MARK: Assign Matchup extension LeagueScheduleData { /// - Returns: The `Matchup` that was successfully assigned. diff --git a/Sources/league-scheduling/data/AssignSlots.swift b/Sources/league-scheduling/data/AssignSlots.swift index 537c224..0bf965d 100644 --- a/Sources/league-scheduling/data/AssignSlots.swift +++ b/Sources/league-scheduling/data/AssignSlots.swift @@ -1,6 +1,4 @@ -import OrderedCollections - // MARK: Assign slots extension LeagueScheduleData { /// Assigns available slots for the day, taking into account all schedule settings, previously assigned matchups and generation data. diff --git a/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift b/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift index 38e78be..0439ce1 100644 --- a/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift +++ b/Sources/league-scheduling/data/LeagueScheduleDataSnapshot.swift @@ -1,5 +1,4 @@ -import OrderedCollections import StaticDateTimes struct LeagueScheduleDataSnapshot: Sendable { diff --git a/Sources/league-scheduling/data/PrioritizedMatchups.swift b/Sources/league-scheduling/data/PrioritizedMatchups.swift index a6bc081..49a0419 100644 --- a/Sources/league-scheduling/data/PrioritizedMatchups.swift +++ b/Sources/league-scheduling/data/PrioritizedMatchups.swift @@ -1,6 +1,4 @@ -import OrderedCollections - struct PrioritizedMatchups: Sendable, ~Copyable { private(set) var matchups:Config.MatchupPairSet private(set) var availableMatchupCountForEntry:ContiguousArray diff --git a/Sources/league-scheduling/data/SelectMatchup.swift b/Sources/league-scheduling/data/SelectMatchup.swift index e17685d..cc26571 100644 --- a/Sources/league-scheduling/data/SelectMatchup.swift +++ b/Sources/league-scheduling/data/SelectMatchup.swift @@ -1,6 +1,4 @@ -import OrderedCollections - // MARK: Select matchup extension LeagueScheduleData { /// - Returns: Matchup pair that should be prioritized to be scheduled due to how many allocations it has remaining. diff --git a/Sources/league-scheduling/data/Shuffle.swift b/Sources/league-scheduling/data/Shuffle.swift index 7e04072..bfed453 100644 --- a/Sources/league-scheduling/data/Shuffle.swift +++ b/Sources/league-scheduling/data/Shuffle.swift @@ -1,6 +1,4 @@ -import OrderedCollections - // MARK: Shuffle extension AssignmentState { /// - Returns: The slot a matchup was sucessfully moved from. diff --git a/Sources/league-scheduling/data/assignment/Move.swift b/Sources/league-scheduling/data/assignment/Move.swift index 123a265..8f62ebe 100644 --- a/Sources/league-scheduling/data/assignment/Move.swift +++ b/Sources/league-scheduling/data/assignment/Move.swift @@ -1,6 +1,4 @@ -import OrderedCollections - // MARK: LeagueScheduleData extension LeagueScheduleData { /// Moves the specified matchup to the given slot on the same day. diff --git a/Sources/league-scheduling/data/assignment/Unassign.swift b/Sources/league-scheduling/data/assignment/Unassign.swift index edc3856..a7ad95d 100644 --- a/Sources/league-scheduling/data/assignment/Unassign.swift +++ b/Sources/league-scheduling/data/assignment/Unassign.swift @@ -1,6 +1,4 @@ -import OrderedCollections - // MARK: Unassign extension AssignmentState { mutating func unassign( diff --git a/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift index d7cc37e..2df8c6b 100644 --- a/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift +++ b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift @@ -1,6 +1,4 @@ -import OrderedCollections - protocol ScheduleConfiguration: Sendable, ~Copyable { associatedtype RNG:RandomNumberGenerator & Sendable associatedtype TimeSet:SetOfTimeIndexes diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift index 0c68276..fc47dbf 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift @@ -1,6 +1,4 @@ -import OrderedCollections - struct SelectSlotB2B: SelectSlotProtocol, ~Copyable { let entryMatchupsPerGameDay:EntryMatchupsPerGameDay diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift index dbd6fe7..36f48bc 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift @@ -1,6 +1,4 @@ -import OrderedCollections - struct SelectSlotEarliestTime: SelectSlotProtocol, ~Copyable { func select( team1: Entry.IDValue, diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift index 43146f5..b246b6d 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift @@ -1,6 +1,4 @@ -import OrderedCollections - struct SelectSlotEarliestTimeAndSameLocationIfB2B: SelectSlotProtocol, ~Copyable { func select( team1: Entry.IDValue, diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift index 87df952..9e3df77 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift @@ -1,6 +1,4 @@ -import OrderedCollections - struct SelectSlotNormal: SelectSlotProtocol, ~Copyable { func select( team1: Entry.IDValue, diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift index 4168c97..4e127df 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift @@ -1,6 +1,4 @@ -import OrderedCollections - protocol SelectSlotProtocol: Sendable, ~Copyable { func select( team1: Entry.IDValue, diff --git a/Sources/league-scheduling/globals.swift b/Sources/league-scheduling/globals.swift index 635efb6..043cc54 100644 --- a/Sources/league-scheduling/globals.swift +++ b/Sources/league-scheduling/globals.swift @@ -1,6 +1,4 @@ -import OrderedCollections - // MARK: adjacent times func calculateAdjacentTimes( for time: TimeIndex, diff --git a/Sources/league-scheduling/typealiases.swift b/Sources/league-scheduling/typealiases.swift index b39cc2c..91bcf0b 100644 --- a/Sources/league-scheduling/typealiases.swift +++ b/Sources/league-scheduling/typealiases.swift @@ -1,6 +1,4 @@ -import OrderedCollections - typealias DayIndex = UInt32 typealias TimeIndex = UInt32 typealias LocationIndex = UInt32 From 1caa4554e4bb3877bdb9fe4b9c3fc647f9ac91f1 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Fri, 20 Mar 2026 09:51:17 -0500 Subject: [PATCH 12/19] stuff - move `AssignmentState` - add and adopt `SetOfMatchup` protocol - support more determinism; remove unused `OrderedCollections` imports --- .../LeagueGenerationData.swift | 4 +--- .../league-scheduling/data/AssignSlots.swift | 18 +++++++++--------- .../data/BalanceHomeAway.swift | 2 +- .../league-scheduling/data/Generation.swift | 12 ++++++++---- .../data/LeagueScheduleData.swift | 7 +++---- .../data/LeagueScheduleDataSnapshot.swift | 6 +++--- .../league-scheduling/data/MatchupBlock.swift | 10 ++++------ .../data/RedistributionData.swift | 2 +- Sources/league-scheduling/data/Shuffle.swift | 7 +++---- .../data/assignment/Assign.swift | 2 +- .../data/assignment/Unassign.swift | 2 +- .../AssignmentState.swift | 4 ++-- .../ScheduleConfiguration.swift | 4 +++- .../util/set/SetOfMatchup.swift | 8 ++++++++ .../expectations/ScheduleExpectations.swift | 2 +- .../util/MatchupsPlayedPerGameDay.swift | 2 +- 16 files changed, 50 insertions(+), 42 deletions(-) rename Sources/league-scheduling/data/{ => assignmentState}/AssignmentState.swift (99%) create mode 100644 Sources/league-scheduling/util/set/SetOfMatchup.swift diff --git a/Sources/league-scheduling/LeagueGenerationData.swift b/Sources/league-scheduling/LeagueGenerationData.swift index c91cde6..3637044 100644 --- a/Sources/league-scheduling/LeagueGenerationData.swift +++ b/Sources/league-scheduling/LeagueGenerationData.swift @@ -1,11 +1,9 @@ -import OrderedCollections - public struct LeagueGenerationData: Sendable { public var error:LeagueError? = nil public var assignLocationTimeRegenerationAttempts:UInt32 = 0 public var negativeDayIndexRegenerationAttempts:UInt32 = 0 - public var schedule:ContiguousArray> = [] + public var schedule:ContiguousArray> = [] public var executionSteps = [ExecutionStep]() public var shuffleHistory = [LeagueShuffleAction]() } diff --git a/Sources/league-scheduling/data/AssignSlots.swift b/Sources/league-scheduling/data/AssignSlots.swift index 0bf965d..fc18d75 100644 --- a/Sources/league-scheduling/data/AssignSlots.swift +++ b/Sources/league-scheduling/data/AssignSlots.swift @@ -87,7 +87,7 @@ extension LeagueScheduleData { canPlayAt: canPlayAt ) else { // failed to assign matchup, skip it for now - failedMatchupSelections[unchecked: assignmentIndex].insert(originalPair) + failedMatchupSelections[unchecked: assignmentIndex].insertMember(originalPair) prioritizedMatchups.remove(originalPair) assignmentState.availableMatchups.removeMember(originalPair) continue @@ -136,9 +136,9 @@ extension LeagueScheduleData { } // TODO: pick the optimal combination that should be selected? combinationLoop: for combination in allowedDivisionCombinations { - var assignedSlots = Set() - var combinationTimeAllocations:ContiguousArray> = .init( - repeating: Set(minimumCapacity: defaultMaxEntryMatchupsPerGameDay), + var assignedSlots = Config.AvailableSlotSet() + var combinationTimeAllocations:ContiguousArray = .init( + repeating: .init(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)), count: combination.first?.count ?? 10 ) for (divisionIndex, divisionCombination) in combination.enumerated() { @@ -159,7 +159,7 @@ extension LeagueScheduleData { #if LOG print("assignSlots;b2b;division=\(division);divisionCombination=\(divisionCombination);matchups.count=\(assignmentState.matchups.count);availableSlots=\(assignmentState.availableSlots.map({ $0.description }));remainingAllocations=\(assignmentState.remainingAllocations.map { $0.map({ $0.description }) })") #endif - var disallowedTimes = Set(minimumCapacity: defaultMaxEntryMatchupsPerGameDay) + var disallowedTimes = Config.TimeSet(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)) for (divisionCombinationIndex, amount) in divisionCombination.enumerated() { guard amount > 0 else { continue } let combinationTimeAllocation = combinationTimeAllocations[divisionCombinationIndex] @@ -188,10 +188,10 @@ extension LeagueScheduleData { #endif continue combinationLoop } - for matchup in matchups { - disallowedTimes.insert(matchup.time) - combinationTimeAllocations[divisionCombinationIndex].insert(matchup.time) - assignedSlots.insert(matchup.slot) + matchups.forEach { matchup in + disallowedTimes.insertMember(matchup.time) + combinationTimeAllocations[divisionCombinationIndex].insertMember(matchup.time) + assignedSlots.insertMember(matchup.slot) } assignmentState.availableSlots = slots.filter { !disallowedTimes.contains($0.time) } assignmentState.recalculateAvailableMatchups( diff --git a/Sources/league-scheduling/data/BalanceHomeAway.swift b/Sources/league-scheduling/data/BalanceHomeAway.swift index bbe893a..3583588 100644 --- a/Sources/league-scheduling/data/BalanceHomeAway.swift +++ b/Sources/league-scheduling/data/BalanceHomeAway.swift @@ -147,7 +147,7 @@ extension LeagueScheduleData { matchup.matchup.home = away matchup.matchup.away = home - generationData.schedule[unchecked: matchup.day].append(matchup.matchup) + generationData.schedule[unchecked: matchup.day].insertMember(matchup.matchup) } private struct FlippableMatchup: Hashable, Sendable { let day:DayIndex diff --git a/Sources/league-scheduling/data/Generation.swift b/Sources/league-scheduling/data/Generation.swift index 840b958..72866df 100644 --- a/Sources/league-scheduling/data/Generation.swift +++ b/Sources/league-scheduling/data/Generation.swift @@ -62,7 +62,8 @@ extension RequestPayload.Runtime { Set, Set, Set, - Set + Set, + Set >.self ) } @@ -84,7 +85,8 @@ extension RequestPayload.Runtime { OrderedSet, OrderedSet, OrderedSet, - OrderedSet + OrderedSet, + OrderedSet >.self ) } @@ -239,7 +241,7 @@ extension RequestPayload.Runtime { var generationData = LeagueGenerationData() generationData.assignLocationTimeRegenerationAttempts = 0 generationData.negativeDayIndexRegenerationAttempts = 0 - generationData.schedule = .init(repeating: OrderedSet(), count: gameDays) + generationData.schedule = .init(repeating: .init(), count: gameDays) var dataSnapshot = copy dataSnapshot var gameDayDivisionEntries:ContiguousArray> = .init(repeating: .init(repeating: .init(), count: divisionsCount), count: gameDays) @@ -334,7 +336,9 @@ extension RequestPayload.Runtime { data.loadSnapshot(todayData) } } else { - generationData.schedule[unchecked: day] = data.assignmentState.matchups + var set = Set(minimumCapacity: data.assignmentState.matchups.count) + data.assignmentState.matchups.forEach { set.insert($0) } // TODO: optimize + generationData.schedule[unchecked: day] = set snapshots.append(todayData) day += 1 gameDayRegenerationAttempt = 0 diff --git a/Sources/league-scheduling/data/LeagueScheduleData.swift b/Sources/league-scheduling/data/LeagueScheduleData.swift index 627fd89..0bddfe6 100644 --- a/Sources/league-scheduling/data/LeagueScheduleData.swift +++ b/Sources/league-scheduling/data/LeagueScheduleData.swift @@ -1,5 +1,4 @@ -import OrderedCollections import StaticDateTimes // MARK: Data @@ -30,7 +29,7 @@ struct LeagueScheduleData: Sendable, ~Copyable { var allowedDivisionCombinations:ContiguousArray>> = [] /// - Usage: [`selection index` : `Set`] - var failedMatchupSelections:ContiguousArray> + var failedMatchupSelections:ContiguousArray var assignmentState:AssignmentState var prioritizeEarlierTimes:Bool @@ -155,11 +154,11 @@ extension LeagueScheduleData { default: break } - failedMatchupSelections = .init(repeating: Set(), count: expectedMatchupsCount) + failedMatchupSelections = .init(repeating: .init(), count: expectedMatchupsCount) assignmentState.allMatchups = availableMatchups assignmentState.availableMatchups = availableMatchups assignmentState.prioritizedEntries = prioritizedEntries - assignmentState.matchups = OrderedSet(minimumCapacity: availableSlots.count) + assignmentState.matchups = Config.MatchupSet(minimumCapacity: availableSlots.count) for i in 0..: Sendable { var allowedDivisionCombinations:ContiguousArray>> = [] /// - Usage: [`selection index` : `Set`] - var failedMatchupSelections:ContiguousArray> + var failedMatchupSelections:ContiguousArray var assignmentState:AssignmentStateCopyable var prioritizeEarlierTimes = false @@ -60,7 +60,7 @@ struct LeagueScheduleDataSnapshot: Sendable { } self.entryDivisions = entryDivisions - failedMatchupSelections = .init(repeating: Set(), count: entriesCount) + failedMatchupSelections = .init(repeating: .init(), count: entriesCount) let playsAt = ContiguousArray( repeating: .init(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)), count: entriesCount ) @@ -92,7 +92,7 @@ struct LeagueScheduleDataSnapshot: Sendable { playsAt: playsAt, playsAtTimes: playsAtTimes, playsAtLocations: .init(repeating: Set(minimumCapacity: defaultMaxEntryMatchupsPerGameDay), count: entriesCount), - matchups: [], + matchups: .init(), shuffleHistory: [] ) } diff --git a/Sources/league-scheduling/data/MatchupBlock.swift b/Sources/league-scheduling/data/MatchupBlock.swift index 11f549b..986d6e5 100644 --- a/Sources/league-scheduling/data/MatchupBlock.swift +++ b/Sources/league-scheduling/data/MatchupBlock.swift @@ -1,6 +1,4 @@ -import OrderedCollections - // MARK: Assign block extension LeagueScheduleData { /// - Returns: The assigned block of matchups @@ -8,7 +6,7 @@ extension LeagueScheduleData { amount: Int, division: Division.IDValue, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable - ) -> OrderedSet? { + ) -> Config.MatchupSet? { if gameGap.min == 1 && gameGap.max == 1 { return Self.assignBlockOfMatchups( amount: amount, @@ -61,12 +59,12 @@ extension LeagueScheduleData { assignmentState: inout AssignmentState, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable - ) -> OrderedSet? { + ) -> Config.MatchupSet? { let limit = amount * entryMatchupsPerGameDay var remainingPrioritizedEntries = assignmentState.prioritizedEntries var remainingAvailableSlots = assignmentState.availableSlots var localAssignmentState = assignmentState.copy() - localAssignmentState.matchups.removeAll(keepingCapacity: true) + localAssignmentState.matchups.removeAllKeepingCapacity() localAssignmentState.recalculateAvailableMatchups( day: day, entryMatchupsPerGameDay: entryMatchupsPerGameDay, @@ -212,7 +210,7 @@ extension LeagueScheduleData { let previousMatchups = assignmentState.matchups assignmentState = localAssignmentState.copy() assignmentState.matchups.formUnion(previousMatchups) - for matchup in localAssignmentState.matchups { + localAssignmentState.matchups.forEach { matchup in remainingAvailableSlots.removeMember(matchup.slot) } assignmentState.availableSlots = remainingAvailableSlots diff --git a/Sources/league-scheduling/data/RedistributionData.swift b/Sources/league-scheduling/data/RedistributionData.swift index 29ecc6d..dcbc0e7 100644 --- a/Sources/league-scheduling/data/RedistributionData.swift +++ b/Sources/league-scheduling/data/RedistributionData.swift @@ -195,7 +195,7 @@ extension RedistributionData { redistributable.matchup.time = redistributable.toSlot.time redistributable.matchup.location = redistributable.toSlot.location - assignmentState.matchups.append(redistributable.matchup) + assignmentState.matchups.insertMember(redistributable.matchup) assignmentState.availableSlots.removeMember(redistributable.toSlot) assignmentState.incrementAssignData(home: redistributable.matchup.home, away: redistributable.matchup.away, slot: redistributable.toSlot) assignmentState.insertPlaysAt(home: redistributable.matchup.home, away: redistributable.matchup.away, slot: redistributable.toSlot) diff --git a/Sources/league-scheduling/data/Shuffle.swift b/Sources/league-scheduling/data/Shuffle.swift index bfed453..2d63864 100644 --- a/Sources/league-scheduling/data/Shuffle.swift +++ b/Sources/league-scheduling/data/Shuffle.swift @@ -32,7 +32,7 @@ extension AssignmentState { let team2LocationNumbers = assignedLocations[unchecked: matchup.team2] let team2MaxTimeNumbers = maxTimeAllocations[unchecked: matchup.team2] let team2MaxLocationNumbers = maxLocationAllocations[unchecked: matchup.team2] - for swapped in matchups { + return matchups.forEachWithReturn { swapped in // make sure the failed assigned matchup is allowed to go where the assigned matchup is guard canPlayAt.test( time: swapped.time, @@ -61,7 +61,7 @@ extension AssignmentState { maxLocationNumber: UInt8(team2MaxLocationNumbers[unchecked: swapped.location]), gameGap: gameGap ) else { - continue + return nil } let swappedSlot = swapped.slot @@ -124,7 +124,7 @@ extension AssignmentState { maxLocationNumber: UInt8(maxAwayLocationNumbers[unchecked: $0.location]), gameGap: gameGap ) - }) else { continue } + }) else { return nil } #if LOG print("shuffle;day=\(day);moved \(swapped) to \(slot) to make room for \(matchup)") @@ -144,6 +144,5 @@ extension AssignmentState { shuffleHistory.append(.init(day: day, from: swappedSlot, to: slot, pair: swapped.pair)) return swappedSlot } - return nil } } \ No newline at end of file diff --git a/Sources/league-scheduling/data/assignment/Assign.swift b/Sources/league-scheduling/data/assignment/Assign.swift index a5af35b..8c0ef53 100644 --- a/Sources/league-scheduling/data/assignment/Assign.swift +++ b/Sources/league-scheduling/data/assignment/Assign.swift @@ -56,7 +56,7 @@ extension AssignmentState { home: home, away: away ) - matchups.append(leagueMatchup) + matchups.insertMember(leagueMatchup) availableMatchups.removeMember(matchup) // TODO: fix (why is the following line necessary | it fixes an issue that allowed matchups to exceed the maximumSameOpponentsMatchupsCap, but availableMatchups still contains matchups that shouldn't be scheduled when scheduling b2b) diff --git a/Sources/league-scheduling/data/assignment/Unassign.swift b/Sources/league-scheduling/data/assignment/Unassign.swift index a7ad95d..9fb65e7 100644 --- a/Sources/league-scheduling/data/assignment/Unassign.swift +++ b/Sources/league-scheduling/data/assignment/Unassign.swift @@ -18,7 +18,7 @@ extension AssignmentState { decrementAssignData(home: matchup.home, away: matchup.away, slot: matchup.slot) removePlaysAt(home: matchup.home, away: matchup.away, slot: matchup.slot) availableSlots.insertMember(matchup.slot) - matchups.remove(matchup) + matchups.removeMember(matchup) recalculateAvailableMatchups( day: day, diff --git a/Sources/league-scheduling/data/AssignmentState.swift b/Sources/league-scheduling/data/assignmentState/AssignmentState.swift similarity index 99% rename from Sources/league-scheduling/data/AssignmentState.swift rename to Sources/league-scheduling/data/assignmentState/AssignmentState.swift index ba6986b..1e2948d 100644 --- a/Sources/league-scheduling/data/AssignmentState.swift +++ b/Sources/league-scheduling/data/assignmentState/AssignmentState.swift @@ -66,7 +66,7 @@ struct AssignmentState: Sendable, ~Copyable { var playsAtLocations:PlaysAtLocations /// Available matchups that can be scheduled. - var matchups:OrderedSet + var matchups:Config.MatchupSet var shuffleHistory = [LeagueShuffleAction]() @@ -183,7 +183,7 @@ struct AssignmentStateCopyable { var playsAt:Config.PlaysAt var playsAtTimes:PlaysAtTimesArray var playsAtLocations:PlaysAtLocations - var matchups:OrderedSet + var matchups:Config.MatchupSet var shuffleHistory:[LeagueShuffleAction] diff --git a/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift index 2df8c6b..c6b9d6e 100644 --- a/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift +++ b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift @@ -5,6 +5,7 @@ protocol ScheduleConfiguration: Sendable, ~Copyable { associatedtype EntryIDSet:SetOfEntryIDs associatedtype AvailableSlotSet:SetOfAvailableSlots associatedtype MatchupPairSet:SetOfMatchupPair + associatedtype MatchupSet:SetOfMatchup typealias RemainingAllocations = ContiguousArray typealias PlaysAt = ContiguousArray @@ -16,6 +17,7 @@ enum ScheduleConfig< TimeSet: SetOfTimeIndexes, EntryIDSet: SetOfEntryIDs, AvailableSlotSet: SetOfAvailableSlots, - MatchupPairSet: SetOfMatchupPair + MatchupPairSet: SetOfMatchupPair, + MatchupSet: SetOfMatchup >: ScheduleConfiguration { } \ No newline at end of file diff --git a/Sources/league-scheduling/util/set/SetOfMatchup.swift b/Sources/league-scheduling/util/set/SetOfMatchup.swift new file mode 100644 index 0000000..53afc80 --- /dev/null +++ b/Sources/league-scheduling/util/set/SetOfMatchup.swift @@ -0,0 +1,8 @@ + +import OrderedCollections + +protocol SetOfMatchup: AbstractSet, ~Copyable where Element == Matchup { +} + +extension Set: SetOfMatchup {} +extension OrderedSet: SetOfMatchup {} \ No newline at end of file diff --git a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift index 9c0415e..9100ff2 100644 --- a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift +++ b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift @@ -250,7 +250,7 @@ extension ScheduleExpectations { extension ScheduleExpectations { func printMatchups( day: Int, - _ matchups: OrderedSet + _ matchups: Set ) { return let results:String = matchups.sorted(by: { diff --git a/Tests/LeagueSchedulingTests/schedules/util/MatchupsPlayedPerGameDay.swift b/Tests/LeagueSchedulingTests/schedules/util/MatchupsPlayedPerGameDay.swift index 78df90c..b89e7f0 100644 --- a/Tests/LeagueSchedulingTests/schedules/util/MatchupsPlayedPerGameDay.swift +++ b/Tests/LeagueSchedulingTests/schedules/util/MatchupsPlayedPerGameDay.swift @@ -6,7 +6,7 @@ struct MatchupsPlayedPerGameDay { static func get( gameDays: DayIndex, entriesCount: Int, - schedule: ContiguousArray> + schedule: ContiguousArray> ) -> ContiguousArray> { var matchupsPlayedPerDay = ContiguousArray( repeating: ContiguousArray(repeating: 0, count: entriesCount), From 9b60941ee84ee17c65e1eab9eb93afbe70c1195a Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Fri, 20 Mar 2026 10:01:13 -0500 Subject: [PATCH 13/19] minor code cleanups --- .../data/assignmentState/AssignmentState.swift | 1 - .../league-scheduling/data/selectSlot/SelectSlotB2B.swift | 4 ++-- .../data/selectSlot/SelectSlotEarliestTime.swift | 6 +++--- .../SelectSlotEarliestTimeAndSameLocationIfB2B.swift | 2 +- .../data/selectSlot/SelectSlotNormal.swift | 6 +++--- .../data/selectSlot/SelectSlotProtocol.swift | 2 +- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Sources/league-scheduling/data/assignmentState/AssignmentState.swift b/Sources/league-scheduling/data/assignmentState/AssignmentState.swift index 1e2948d..8a0da16 100644 --- a/Sources/league-scheduling/data/assignmentState/AssignmentState.swift +++ b/Sources/league-scheduling/data/assignmentState/AssignmentState.swift @@ -1,5 +1,4 @@ -import OrderedCollections import StaticDateTimes // MARK: Noncopyable diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift index fc47dbf..8e82ceb 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift @@ -9,7 +9,7 @@ struct SelectSlotB2B: SelectSlotProtocol, ~Copyable { assignedLocations: AssignedLocations, playsAtTimes: borrowing PlaysAtTimesArray, playsAtLocations: PlaysAtLocations, - playableSlots: inout some SetOfAvailableSlots + playableSlots: inout some SetOfAvailableSlots & ~Copyable ) -> AvailableSlot? { filter( team1: team1, @@ -33,7 +33,7 @@ extension SelectSlotB2B { team1: Entry.IDValue, team2: Entry.IDValue, playsAtTimes: borrowing PlaysAtTimesArray, - playableSlots: inout some SetOfAvailableSlots + playableSlots: inout some SetOfAvailableSlots & ~Copyable ) { //print("filterSlotBack2Back;playsAtTimes[unchecked: team1].isEmpty=\(playsAtTimes[unchecked: team1].isEmpty);playsAtTimes[unchecked: team2].isEmpty=\(playsAtTimes[unchecked: team2].isEmpty)") if playsAtTimes[unchecked: team1].isEmpty && playsAtTimes[unchecked: team2].isEmpty { diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift index 36f48bc..20dd36d 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift @@ -7,7 +7,7 @@ struct SelectSlotEarliestTime: SelectSlotProtocol, ~Copyable { assignedLocations: AssignedLocations, playsAtTimes: borrowing PlaysAtTimesArray, playsAtLocations: PlaysAtLocations, - playableSlots: inout some SetOfAvailableSlots + playableSlots: inout some SetOfAvailableSlots & ~Copyable ) -> AvailableSlot? { return Self.select( team1: team1, @@ -25,7 +25,7 @@ extension SelectSlotEarliestTime { team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playableSlots: inout some SetOfAvailableSlots + playableSlots: inout some SetOfAvailableSlots & ~Copyable ) -> AvailableSlot? { filter(playableSlots: &playableSlots) return SelectSlotNormal.select( @@ -38,7 +38,7 @@ extension SelectSlotEarliestTime { } /// Mutates `playableSlots` so it only contains the slots at the earliest available time. - static func filter(playableSlots: inout some SetOfAvailableSlots) { + static func filter(playableSlots: inout some SetOfAvailableSlots & ~Copyable) { var earliestTime = TimeIndex.max playableSlots.forEach { slot in if slot.time < earliestTime { diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift index b246b6d..763ec16 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift @@ -7,7 +7,7 @@ struct SelectSlotEarliestTimeAndSameLocationIfB2B: SelectSlotProtocol, ~Copyable assignedLocations: AssignedLocations, playsAtTimes: borrowing PlaysAtTimesArray, playsAtLocations: PlaysAtLocations, - playableSlots: inout some SetOfAvailableSlots + playableSlots: inout some SetOfAvailableSlots & ~Copyable ) -> AvailableSlot? { guard !playableSlots.isEmpty else { return nil } let homePlaysAtTimes = playsAtTimes[unchecked: team1] diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift index 9e3df77..5889166 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift @@ -7,7 +7,7 @@ struct SelectSlotNormal: SelectSlotProtocol, ~Copyable { assignedLocations: AssignedLocations, playsAtTimes: borrowing PlaysAtTimesArray, playsAtLocations: PlaysAtLocations, - playableSlots: inout some SetOfAvailableSlots + playableSlots: inout some SetOfAvailableSlots & ~Copyable ) -> AvailableSlot? { return Self.select( team1: team1, @@ -26,7 +26,7 @@ extension SelectSlotNormal { team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playableSlots: some SetOfAvailableSlots + playableSlots: borrowing some SetOfAvailableSlots & ~Copyable ) -> AvailableSlot? { guard !playableSlots.isEmpty else { return nil } let team1Times = assignedTimes[unchecked: team1] @@ -48,7 +48,7 @@ extension SelectSlotNormal { team1Locations: AssignedLocations.Element, team2Times: AssignedTimes.Element, team2Locations: AssignedLocations.Element, - playableSlots: some SetOfAvailableSlots + playableSlots: borrowing some SetOfAvailableSlots & ~Copyable ) -> AvailableSlot? { var selected:SelectedSlot! = nil playableSlots.forEach { slot in diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift index 4e127df..4c3172c 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift @@ -7,6 +7,6 @@ protocol SelectSlotProtocol: Sendable, ~Copyable { assignedLocations: AssignedLocations, playsAtTimes: borrowing PlaysAtTimesArray, playsAtLocations: PlaysAtLocations, - playableSlots: inout some SetOfAvailableSlots + playableSlots: inout some SetOfAvailableSlots & ~Copyable ) -> AvailableSlot? } \ No newline at end of file From ccaceb0c8bc63323a67c94d153599843a46c5f04 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Fri, 20 Mar 2026 10:29:09 -0500 Subject: [PATCH 14/19] support more determinism; remove some memberless protocols --- .../league-scheduling/data/BalanceHomeAway.swift | 12 +++--------- Sources/league-scheduling/data/Generation.swift | 6 ++++-- .../assignmentState/ScheduleConfiguration.swift | 16 +++++++++++----- .../util/FlippableMatchup.swift | 8 ++++++++ .../util/set/SetOfEntryIDs.swift | 12 ++++++------ .../util/set/SetOfMatchup.swift | 8 -------- .../util/set/SetOfMatchupPair.swift | 8 -------- 7 files changed, 32 insertions(+), 38 deletions(-) create mode 100644 Sources/league-scheduling/util/FlippableMatchup.swift delete mode 100644 Sources/league-scheduling/util/set/SetOfMatchup.swift delete mode 100644 Sources/league-scheduling/util/set/SetOfMatchupPair.swift diff --git a/Sources/league-scheduling/data/BalanceHomeAway.swift b/Sources/league-scheduling/data/BalanceHomeAway.swift index 3583588..7226d6b 100644 --- a/Sources/league-scheduling/data/BalanceHomeAway.swift +++ b/Sources/league-scheduling/data/BalanceHomeAway.swift @@ -1,6 +1,4 @@ -import OrderedCollections - // MARK: Matchup pair extension MatchupPair { /// Balances home/away allocations, mutating `team1` (home) and `team2` (away) if necessary. @@ -77,13 +75,13 @@ extension LeagueScheduleData { appendExecutionStep(now: now) return } - var flippable = OrderedSet() + var flippable = Config.FlippableMatchupSet() for day in 0.., Set, Set, - Set + Set, + Set >.self ) } @@ -86,7 +87,8 @@ extension RequestPayload.Runtime { OrderedSet, OrderedSet, OrderedSet, - OrderedSet + OrderedSet, + OrderedSet >.self ) } diff --git a/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift index c6b9d6e..bd069b2 100644 --- a/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift +++ b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift @@ -4,8 +4,9 @@ protocol ScheduleConfiguration: Sendable, ~Copyable { associatedtype TimeSet:SetOfTimeIndexes associatedtype EntryIDSet:SetOfEntryIDs associatedtype AvailableSlotSet:SetOfAvailableSlots - associatedtype MatchupPairSet:SetOfMatchupPair - associatedtype MatchupSet:SetOfMatchup + associatedtype MatchupPairSet:AbstractSet where MatchupPairSet.Element == MatchupPair + associatedtype MatchupSet:AbstractSet where MatchupSet.Element == Matchup + associatedtype FlippableMatchupSet:AbstractSet where FlippableMatchupSet.Element == FlippableMatchup typealias RemainingAllocations = ContiguousArray typealias PlaysAt = ContiguousArray @@ -17,7 +18,12 @@ enum ScheduleConfig< TimeSet: SetOfTimeIndexes, EntryIDSet: SetOfEntryIDs, AvailableSlotSet: SetOfAvailableSlots, - MatchupPairSet: SetOfMatchupPair, - MatchupSet: SetOfMatchup - >: ScheduleConfiguration { + MatchupPairSet: AbstractSet, + MatchupSet: AbstractSet, + FlippableMatchupSet: AbstractSet + >: ScheduleConfiguration where + MatchupPairSet.Element == MatchupPair, + MatchupSet.Element == Matchup, + FlippableMatchupSet.Element == FlippableMatchup + { } \ No newline at end of file diff --git a/Sources/league-scheduling/util/FlippableMatchup.swift b/Sources/league-scheduling/util/FlippableMatchup.swift new file mode 100644 index 0000000..5f025dc --- /dev/null +++ b/Sources/league-scheduling/util/FlippableMatchup.swift @@ -0,0 +1,8 @@ + +/// A scheduled `Matchup` where the home and away teams can be swapped. +/// +/// Only used when balancing the final scheduled matchup's home/away. +struct FlippableMatchup: Hashable, Sendable { + let day:DayIndex + var matchup:Matchup +} \ No newline at end of file diff --git a/Sources/league-scheduling/util/set/SetOfEntryIDs.swift b/Sources/league-scheduling/util/set/SetOfEntryIDs.swift index 2924659..fa477c3 100644 --- a/Sources/league-scheduling/util/set/SetOfEntryIDs.swift +++ b/Sources/league-scheduling/util/set/SetOfEntryIDs.swift @@ -3,17 +3,17 @@ import OrderedCollections protocol SetOfEntryIDs: AbstractSet, ~Copyable where Element == Entry.IDValue { /// - Returns: The available matchup pairs that can play for the `day`. - func availableMatchupPairs( + func availableMatchupPairs( assignedEntryHomeAways: AssignedEntryHomeAways, maxSameOpponentMatchups: MaximumSameOpponentMatchups - ) -> MatchupPairSet + ) -> MatchupPairSet where MatchupPairSet.Element == MatchupPair } extension Set: SetOfEntryIDs { - func availableMatchupPairs( + func availableMatchupPairs( assignedEntryHomeAways: AssignedEntryHomeAways, maxSameOpponentMatchups: MaximumSameOpponentMatchups - ) -> MatchupPairSet { + ) -> MatchupPairSet where MatchupPairSet.Element == MatchupPair { guard !isEmpty else { return .init() } // https://github.com/apple/swift-collections/issues/608 var pairs = MatchupPairSet() pairs.reserveCapacity((count-1) * 2) @@ -35,10 +35,10 @@ extension Set: SetOfEntryIDs { } extension OrderedSet: SetOfEntryIDs { - func availableMatchupPairs( + func availableMatchupPairs( assignedEntryHomeAways: AssignedEntryHomeAways, maxSameOpponentMatchups: MaximumSameOpponentMatchups - ) -> MatchupPairSet { + ) -> MatchupPairSet where MatchupPairSet.Element == MatchupPair { guard !isEmpty else { return .init() } // https://github.com/apple/swift-collections/issues/608 var pairs = MatchupPairSet() pairs.reserveCapacity((count-1) * 2) diff --git a/Sources/league-scheduling/util/set/SetOfMatchup.swift b/Sources/league-scheduling/util/set/SetOfMatchup.swift deleted file mode 100644 index 53afc80..0000000 --- a/Sources/league-scheduling/util/set/SetOfMatchup.swift +++ /dev/null @@ -1,8 +0,0 @@ - -import OrderedCollections - -protocol SetOfMatchup: AbstractSet, ~Copyable where Element == Matchup { -} - -extension Set: SetOfMatchup {} -extension OrderedSet: SetOfMatchup {} \ No newline at end of file diff --git a/Sources/league-scheduling/util/set/SetOfMatchupPair.swift b/Sources/league-scheduling/util/set/SetOfMatchupPair.swift deleted file mode 100644 index ee8c671..0000000 --- a/Sources/league-scheduling/util/set/SetOfMatchupPair.swift +++ /dev/null @@ -1,8 +0,0 @@ - -import OrderedCollections - -protocol SetOfMatchupPair: AbstractSet, ~Copyable where Element == MatchupPair { -} - -extension Set: SetOfMatchupPair {} -extension OrderedSet: SetOfMatchupPair {} \ No newline at end of file From 2998faccaa0cd1d5ddaf6167323b8c600e702d63 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Fri, 20 Mar 2026 11:29:45 -0500 Subject: [PATCH 15/19] support more determinism for `RedistributionData` --- .../league-scheduling/data/Generation.swift | 2 ++ .../data/RedistributionData.swift | 29 ++++++------------- .../ScheduleConfiguration.swift | 3 ++ .../util/RedistributableMatchup.swift | 9 ++++++ 4 files changed, 23 insertions(+), 20 deletions(-) create mode 100644 Sources/league-scheduling/util/RedistributableMatchup.swift diff --git a/Sources/league-scheduling/data/Generation.swift b/Sources/league-scheduling/data/Generation.swift index e108010..85510db 100644 --- a/Sources/league-scheduling/data/Generation.swift +++ b/Sources/league-scheduling/data/Generation.swift @@ -64,6 +64,7 @@ extension RequestPayload.Runtime { Set, Set, Set, + Set, Set >.self ) @@ -88,6 +89,7 @@ extension RequestPayload.Runtime { OrderedSet, OrderedSet, OrderedSet, + OrderedSet, OrderedSet >.self ) diff --git a/Sources/league-scheduling/data/RedistributionData.swift b/Sources/league-scheduling/data/RedistributionData.swift index dcbc0e7..9bb3e3a 100644 --- a/Sources/league-scheduling/data/RedistributionData.swift +++ b/Sources/league-scheduling/data/RedistributionData.swift @@ -1,6 +1,4 @@ -import OrderedCollections - struct RedistributionData: Sendable { /// The latest `DayIndex` that is allowed to redistribute matchups from. let startDayIndex:DayIndex @@ -55,7 +53,7 @@ extension RedistributionData { #endif var assigned = 0 - var redistributables = OrderedSet() + var redistributables = Config.RedistributableMatchupSet() for fromDayIndex in stride(from: startDayIndex, through: 0, by: -1) { for matchup in generationData.schedule[unchecked: fromDayIndex] { guard !redistributed.contains(matchup) else { continue } @@ -99,7 +97,7 @@ extension RedistributionData { maxLocationNumber: UInt8(awayMaxAssignedLocations[unchecked: slot.location]), gameGap: gameGap ) { - redistributables.append(.init(fromDay: fromDayIndex, matchup: matchup, toSlot: slot)) + redistributables.insertMember(.init(fromDay: fromDayIndex, matchup: matchup, toSlot: slot)) } assignmentState.incrementAssignData(home: matchup.home, away: matchup.away, slot: matchup.slot) } @@ -130,17 +128,17 @@ extension RedistributionData { // MARK: Select redistributable extension RedistributionData { private func selectRedistributable( - from redistributables: OrderedSet, + from redistributables: Config.RedistributableMatchupSet, generationData: LeagueGenerationData - ) -> Redistributable? { - var redistributable:Redistributable? = nil + ) -> RedistributableMatchup? { + var redistributable:RedistributableMatchup? = nil // prioritize entries that have been redistributed the least var (cMin, cMax):(UInt16, UInt16) = (.max, .max) - for r in redistributables { + redistributables.forEach { r in if generationData.schedule[unchecked: r.fromDay].count <= minMatchupsRequired { // don't take from the day since the matchups for it will render the day incomplete - continue + return } let (rMin, rMax) = calculateMinMax(matchup: r.matchup) if rMin < cMin { @@ -174,8 +172,8 @@ extension RedistributionData { // MARK: Redistribute extension RedistributionData { private mutating func redistribute( - redistributable: inout Redistributable, - redistributables: inout OrderedSet, + redistributable: inout RedistributableMatchup, + redistributables: inout Config.RedistributableMatchupSet, assignmentState: inout AssignmentState, generationData: inout LeagueGenerationData ) { @@ -200,13 +198,4 @@ extension RedistributionData { assignmentState.incrementAssignData(home: redistributable.matchup.home, away: redistributable.matchup.away, slot: redistributable.toSlot) assignmentState.insertPlaysAt(home: redistributable.matchup.home, away: redistributable.matchup.away, slot: redistributable.toSlot) } -} - -// MARK: Redistributable -extension RedistributionData { - private struct Redistributable: Hashable, Sendable { - let fromDay:DayIndex - var matchup:Matchup - let toSlot:AvailableSlot - } } \ No newline at end of file diff --git a/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift index bd069b2..a4c0e7b 100644 --- a/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift +++ b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift @@ -6,6 +6,7 @@ protocol ScheduleConfiguration: Sendable, ~Copyable { associatedtype AvailableSlotSet:SetOfAvailableSlots associatedtype MatchupPairSet:AbstractSet where MatchupPairSet.Element == MatchupPair associatedtype MatchupSet:AbstractSet where MatchupSet.Element == Matchup + associatedtype RedistributableMatchupSet:AbstractSet where RedistributableMatchupSet.Element == RedistributableMatchup associatedtype FlippableMatchupSet:AbstractSet where FlippableMatchupSet.Element == FlippableMatchup typealias RemainingAllocations = ContiguousArray @@ -20,10 +21,12 @@ enum ScheduleConfig< AvailableSlotSet: SetOfAvailableSlots, MatchupPairSet: AbstractSet, MatchupSet: AbstractSet, + RedistributableMatchupSet: AbstractSet, FlippableMatchupSet: AbstractSet >: ScheduleConfiguration where MatchupPairSet.Element == MatchupPair, MatchupSet.Element == Matchup, + RedistributableMatchupSet.Element == RedistributableMatchup, FlippableMatchupSet.Element == FlippableMatchup { } \ No newline at end of file diff --git a/Sources/league-scheduling/util/RedistributableMatchup.swift b/Sources/league-scheduling/util/RedistributableMatchup.swift new file mode 100644 index 0000000..f328244 --- /dev/null +++ b/Sources/league-scheduling/util/RedistributableMatchup.swift @@ -0,0 +1,9 @@ + +/// A scheduled `Matchup` that can be moved from its current day and slot to another. +/// +/// Only used when redistributing matchups. +struct RedistributableMatchup: Hashable, Sendable { + let fromDay:DayIndex + var matchup:Matchup + let toSlot:AvailableSlot +} \ No newline at end of file From bd3a106cf251195789a0f636157c71e3e3a9671a Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Fri, 20 Mar 2026 11:40:20 -0500 Subject: [PATCH 16/19] remove unused `AbstractArray` --- .../league-scheduling/util/array/AbstractArray.swift | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 Sources/league-scheduling/util/array/AbstractArray.swift diff --git a/Sources/league-scheduling/util/array/AbstractArray.swift b/Sources/league-scheduling/util/array/AbstractArray.swift deleted file mode 100644 index 35ce142..0000000 --- a/Sources/league-scheduling/util/array/AbstractArray.swift +++ /dev/null @@ -1,12 +0,0 @@ - -protocol AbstractArray: Sendable, ~Copyable { - associatedtype Index - associatedtype Element:Sendable - - init() - - mutating func reserveCapacity(_ minimumCapacity: Int) -} - -extension ContiguousArray: AbstractArray { -} \ No newline at end of file From 98e23e5b32bac70bb45fca87f6042b7526e0d795 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Fri, 20 Mar 2026 11:47:26 -0500 Subject: [PATCH 17/19] use `subscript(unchecked:)` when calling `availableMatchupPairs` --- Sources/league-scheduling/util/set/SetOfEntryIDs.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/league-scheduling/util/set/SetOfEntryIDs.swift b/Sources/league-scheduling/util/set/SetOfEntryIDs.swift index fa477c3..6fc7574 100644 --- a/Sources/league-scheduling/util/set/SetOfEntryIDs.swift +++ b/Sources/league-scheduling/util/set/SetOfEntryIDs.swift @@ -20,7 +20,7 @@ extension Set: SetOfEntryIDs { let sortedEntries = sorted() var index = 0 while index < sortedEntries.count - 1 { - let home = sortedEntries[index] + let home = sortedEntries[unchecked: index] index += 1 let assignedHome = assignedEntryHomeAways[unchecked: home] let maxSameOpponentMatchups = maxSameOpponentMatchups[unchecked: home] @@ -45,7 +45,7 @@ extension OrderedSet: SetOfEntryIDs { let sortedEntries = sorted() var index = 0 while index < sortedEntries.count - 1 { - let home = sortedEntries[index] + let home = sortedEntries[unchecked: index] index += 1 let assignedHome = assignedEntryHomeAways[unchecked: home] let maxSameOpponentMatchups = maxSameOpponentMatchups[unchecked: home] From 777c384f4a26202ca837d096950674f85e0f3be6 Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Fri, 20 Mar 2026 11:49:05 -0500 Subject: [PATCH 18/19] documentation fix --- Sources/league-scheduling/util/FlippableMatchup.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/league-scheduling/util/FlippableMatchup.swift b/Sources/league-scheduling/util/FlippableMatchup.swift index 5f025dc..c57e349 100644 --- a/Sources/league-scheduling/util/FlippableMatchup.swift +++ b/Sources/league-scheduling/util/FlippableMatchup.swift @@ -1,7 +1,7 @@ /// A scheduled `Matchup` where the home and away teams can be swapped. /// -/// Only used when balancing the final scheduled matchup's home/away. +/// Only used when balancing the final scheduled matchups' home/away. struct FlippableMatchup: Hashable, Sendable { let day:DayIndex var matchup:Matchup From 195ef6b32a4c3ccf46be508d256c8db1afa9af8f Mon Sep 17 00:00:00 2001 From: RandomHashTags Date: Fri, 20 Mar 2026 11:53:39 -0500 Subject: [PATCH 19/19] remove two unused imports --- .../schedules/expectations/ScheduleExpectations.swift | 1 - .../schedules/util/MatchupsPlayedPerGameDay.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift index 9100ff2..9f6d946 100644 --- a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift +++ b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift @@ -1,6 +1,5 @@ @testable import LeagueScheduling -import OrderedCollections import Testing protocol ScheduleExpectations: Sendable { diff --git a/Tests/LeagueSchedulingTests/schedules/util/MatchupsPlayedPerGameDay.swift b/Tests/LeagueSchedulingTests/schedules/util/MatchupsPlayedPerGameDay.swift index b89e7f0..5a1ffa0 100644 --- a/Tests/LeagueSchedulingTests/schedules/util/MatchupsPlayedPerGameDay.swift +++ b/Tests/LeagueSchedulingTests/schedules/util/MatchupsPlayedPerGameDay.swift @@ -1,6 +1,5 @@ @testable import LeagueScheduling -import OrderedCollections struct MatchupsPlayedPerGameDay { static func get(