diff --git a/Package.swift b/Package.swift index 7a35a52..8bf30f0 100644 --- a/Package.swift +++ b/Package.swift @@ -13,10 +13,12 @@ let package = Package( traits: [ .default(enabledTraits: [ "ProtobufCodable", - "UncheckedArraySubscript" + "UncheckedArraySubscript", + //"SpecializeScheduleConfiguration" ]), .trait(name: "ProtobufCodable"), - .trait(name: "UncheckedArraySubscript") + .trait(name: "UncheckedArraySubscript"), + .trait(name: "SpecializeScheduleConfiguration") ], dependencies: [ .package(url: "https://github.com/RandomHashTags/swift-staticdatetime", from: "0.3.5"), @@ -43,4 +45,4 @@ let package = Package( dependencies: ["LeagueScheduling"] ) ] -) +) \ No newline at end of file diff --git a/Sources/league-scheduling/data/AssignSlots.swift b/Sources/league-scheduling/data/AssignSlots.swift index 7ce3a12..caab9cd 100644 --- a/Sources/league-scheduling/data/AssignSlots.swift +++ b/Sources/league-scheduling/data/AssignSlots.swift @@ -123,115 +123,6 @@ extension LeagueScheduleData { } } -// MARK: Assign slots b2b -extension LeagueScheduleData { - private mutating func assignSlotsB2B( - canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable - ) throws(LeagueError) -> Bool { - let slots = assignmentState.availableSlots - let assignmentStateCopy = assignmentState.copy() - whileLoop: while assignmentState.matchups.count != expectedMatchupsCount { - if Task.isCancelled { - throw LeagueError.timedOut(function: "assignSlotsB2B") - } - // TODO: pick the optimal combination that should be selected? - combinationLoop: for combination in allowedDivisionCombinations { - var assignedSlots = Config.AvailableSlotSet() - var combinationTimeAllocations:ContiguousArray = .init( - repeating: .init(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)), - count: combination.first?.count ?? 10 - ) - for (divisionIndex, divisionCombination) in combination.enumerated() { - let division = Division.IDValue(divisionIndex) - let divisionMatchups = assignmentState.allDivisionMatchups[unchecked: division] - assignmentState.availableMatchups = divisionMatchups - assignmentState.prioritizedEntries.removeAllKeepingCapacity() - assignmentState.availableMatchups.forEach { matchup in - assignmentState.prioritizedEntries.insertMember(matchup.team1) - assignmentState.prioritizedEntries.insertMember(matchup.team2) - } - assignmentState.recalculateAllPossibleAllocations( - day: day, - entriesCount: entriesCount, - gameGap: gameGap, - canPlayAt: canPlayAt - ) - #if LOG - print("assignSlots;b2b;division=\(division);divisionCombination=\(divisionCombination);matchups.count=\(assignmentState.matchups.count);availableSlots=\(assignmentState.availableSlots.map({ $0.description }));possibleAllocations=\(assignmentState.possibleAllocations.map { $0.map({ $0.description }) })") - #endif - var disallowedTimes = Config.TimeSet(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)) - for (divisionCombinationIndex, amount) in divisionCombination.enumerated() { - guard amount > 0 else { continue } - let combinationTimeAllocation = combinationTimeAllocations[divisionCombinationIndex] - if !combinationTimeAllocation.isEmpty { - assignmentState.availableSlots = slots.filter { combinationTimeAllocation.contains($0.time) } - assignmentState.recalculateAvailableMatchups( - day: day, - entryMatchupsPerGameDay: defaultMaxEntryMatchupsPerGameDay, - allAvailableMatchups: divisionMatchups - ) - assignmentState.recalculateAllPossibleAllocations( - day: day, - entriesCount: entriesCount, - gameGap: gameGap, - canPlayAt: canPlayAt - ) - } - guard let matchups = assignBlockOfMatchups( - amount: amount, - division: division, - canPlayAt: canPlayAt - ) else { - assignmentState = assignmentStateCopy.copy() - #if LOG - print("assignSlotsB2B;failed to assign matchups for division \(division) and combination \(divisionCombination);skipping") - #endif - continue combinationLoop - } - 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( - day: day, - entryMatchupsPerGameDay: defaultMaxEntryMatchupsPerGameDay, - allAvailableMatchups: divisionMatchups - ) - assignmentState.recalculateAllPossibleAllocations( - day: day, - entriesCount: entriesCount, - gameGap: gameGap, - canPlayAt: canPlayAt - ) - #if LOG - print("assignSlots;b2b;combination=\(divisionCombination);assigned \(amount) for division \(division);availableSlots=\(assignmentState.availableSlots.map({ "\($0)" }))") - #endif - // successfully assigned matchup block of for - } - assignmentState.availableSlots = slots.filter { !assignedSlots.contains($0) } - assignmentState.recalculateAllPossibleAllocations( - day: day, - entriesCount: entriesCount, - gameGap: gameGap, - canPlayAt: canPlayAt - ) - #if LOG - print("assignSlots;b2b;assigned \(divisionCombination) for division \(division)") - #endif - } - break whileLoop - } - return false - } - #if LOG - print("assignSlotsB2B;assignmentState.matchups.count=\(assignmentState.matchups.count);expectedMatchupsCount=\(expectedMatchupsCount)") - #endif - return assignmentState.matchups.count == expectedMatchupsCount - } -} - // MARK: Select and assign matchup extension LeagueScheduleData { /// Selects and assigns a matchup to an available slot. @@ -246,7 +137,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, allAvailableMatchups: Config.MatchupPairSet, - rng: inout some RandomNumberGenerator, + rng: inout some RandomNumberGenerator & Sendable, assignmentState: inout AssignmentState, shouldSkipSelection: (MatchupPair) -> Bool, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, @@ -297,7 +188,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, allAvailableMatchups: Config.MatchupPairSet, - rng: inout some RandomNumberGenerator, + rng: inout some RandomNumberGenerator & Sendable, assignmentState: inout AssignmentState, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable diff --git a/Sources/league-scheduling/data/AssignSlotsB2B.swift b/Sources/league-scheduling/data/AssignSlotsB2B.swift new file mode 100644 index 0000000..fed5e76 --- /dev/null +++ b/Sources/league-scheduling/data/AssignSlotsB2B.swift @@ -0,0 +1,108 @@ + +extension LeagueScheduleData { + mutating func assignSlotsB2B( + canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable + ) throws(LeagueError) -> Bool { + let slots = assignmentState.availableSlots + let assignmentStateCopy = assignmentState.copy() + whileLoop: while assignmentState.matchups.count != expectedMatchupsCount { + if Task.isCancelled { + throw LeagueError.timedOut(function: "assignSlotsB2B") + } + // TODO: pick the optimal combination that should be selected? + combinationLoop: for combination in allowedDivisionCombinations { + var assignedSlots = Config.AvailableSlotSet() + var combinationTimeAllocations:ContiguousArray = .init( + repeating: .init(), + count: combination.first?.count ?? 10 + ) + for (divisionIndex, divisionCombination) in combination.enumerated() { + let division = Division.IDValue(divisionIndex) + let divisionMatchups = assignmentState.allDivisionMatchups[unchecked: division] + assignmentState.availableMatchups = divisionMatchups + assignmentState.prioritizedEntries.removeAllKeepingCapacity() + assignmentState.availableMatchups.forEach { matchup in + assignmentState.prioritizedEntries.insertMember(matchup.team1) + assignmentState.prioritizedEntries.insertMember(matchup.team2) + } + assignmentState.recalculateAllPossibleAllocations( + day: day, + entriesCount: entriesCount, + gameGap: gameGap, + canPlayAt: canPlayAt + ) + #if LOG + print("assignSlots;b2b;division=\(division);divisionCombination=\(divisionCombination);matchups.count=\(assignmentState.matchups.count);availableSlots=\(assignmentState.availableSlots.map({ $0.description }));possibleAllocations=\(assignmentState.possibleAllocations.map { $0.map({ $0.description }) })") + #endif + var disallowedTimes = Config.TimeSet() + for (divisionCombinationIndex, amount) in divisionCombination.enumerated() { + guard amount > 0 else { continue } + let combinationTimeAllocation = combinationTimeAllocations[divisionCombinationIndex] + if !combinationTimeAllocation.isEmpty { + assignmentState.availableSlots = slots.filter { combinationTimeAllocation.contains($0.time) } + assignmentState.recalculateAvailableMatchups( + day: day, + entryMatchupsPerGameDay: defaultMaxEntryMatchupsPerGameDay, + allAvailableMatchups: divisionMatchups + ) + assignmentState.recalculateAllPossibleAllocations( + day: day, + entriesCount: entriesCount, + gameGap: gameGap, + canPlayAt: canPlayAt + ) + } + guard let matchups = assignBlockOfMatchups( + amount: amount, + division: division, + canPlayAt: canPlayAt + ) else { + assignmentState = assignmentStateCopy.copy() + #if LOG + print("assignSlotsB2B;failed to assign matchups for division \(division) and combination \(divisionCombination);skipping") + #endif + continue combinationLoop + } + 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( + day: day, + entryMatchupsPerGameDay: defaultMaxEntryMatchupsPerGameDay, + allAvailableMatchups: divisionMatchups + ) + assignmentState.recalculateAllPossibleAllocations( + day: day, + entriesCount: entriesCount, + gameGap: gameGap, + canPlayAt: canPlayAt + ) + #if LOG + print("assignSlots;b2b;combination=\(divisionCombination);assigned \(amount) for division \(division);availableSlots=\(assignmentState.availableSlots.map({ "\($0)" }))") + #endif + // successfully assigned matchup block of for + } + assignmentState.availableSlots = slots.filter { !assignedSlots.contains($0) } + assignmentState.recalculateAllPossibleAllocations( + day: day, + entriesCount: entriesCount, + gameGap: gameGap, + canPlayAt: canPlayAt + ) + #if LOG + print("assignSlots;b2b;assigned \(divisionCombination) for division \(division)") + #endif + } + break whileLoop + } + return false + } + #if LOG + print("assignSlotsB2B;assignmentState.matchups.count=\(assignmentState.matchups.count);expectedMatchupsCount=\(expectedMatchupsCount)") + #endif + return assignmentState.matchups.count == expectedMatchupsCount + } +} \ No newline at end of file diff --git a/Sources/league-scheduling/data/BalanceHomeAway.swift b/Sources/league-scheduling/data/BalanceHomeAway.swift index 454d1ba..ae23b7e 100644 --- a/Sources/league-scheduling/data/BalanceHomeAway.swift +++ b/Sources/league-scheduling/data/BalanceHomeAway.swift @@ -1,9 +1,13 @@ +#if SpecializeScheduleConfiguration +import OrderedCollections +#endif + // MARK: Matchup pair extension MatchupPair { /// Balances home/away allocations, mutating `team1` (home) and `team2` (away) if necessary. mutating func balanceHomeAway( - rng: inout some RandomNumberGenerator, + rng: inout some RandomNumberGenerator & Sendable, assignmentState: borrowing AssignmentState ) { let team1GamesPlayedAgainstTeam2 = assignmentState.assignedEntryHomeAways[unchecked: team1][unchecked: team2] @@ -29,7 +33,7 @@ extension MatchupPair { team2: Entry.IDValue, homeMatchups: [UInt8], awayMatchups: [UInt8], - rng: inout some RandomNumberGenerator + rng: inout some RandomNumberGenerator & Sendable ) -> Bool { let home1 = homeMatchups[unchecked: team1] let home2 = homeMatchups[unchecked: team2] @@ -44,6 +48,11 @@ extension MatchupPair { // MARK: LeagueScheduleData extension LeagueScheduleData { + #if SpecializeScheduleConfiguration + @_specialize(where Config == BitSet64ScheduleConfig) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif mutating func balanceHomeAway( generationData: inout LeagueGenerationData ) { @@ -144,7 +153,7 @@ extension LeagueScheduleData { matchup.matchup.home = away matchup.matchup.away = home - generationData.schedule[unchecked: matchup.day].insertMember(matchup.matchup) + generationData.schedule[unchecked: matchup.day].insert(matchup.matchup) } private mutating func appendExecutionStep(now: ContinuousClock.Instant) { diff --git a/Sources/league-scheduling/data/Generation.swift b/Sources/league-scheduling/data/Generation.swift index 8120f4c..07568af 100644 --- a/Sources/league-scheduling/data/Generation.swift +++ b/Sources/league-scheduling/data/Generation.swift @@ -1,12 +1,9 @@ +#if SpecializeScheduleConfiguration import OrderedCollections - -#if canImport(SwiftGlibc) -import SwiftGlibc -#elseif canImport(Foundation) -import Foundation #endif +// TODO: support divisions on the same day with different times extension RequestPayload.Runtime { // MARK: Generate func generate() async -> LeagueGenerationResult { @@ -35,6 +32,11 @@ extension RequestPayload.Runtime { // MARK: Generate schedules extension RequestPayload.Runtime { + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif private func generateSchedules() async throws -> [LeagueGenerationData] { #if LOG print("LeagueSchedule;generateSchedules;entries.count=\(entries.count)") @@ -43,63 +45,13 @@ extension RequestPayload.Runtime { var maxStartingTimes:TimeIndex = 0 var maxLocations:LocationIndex = 0 for setting in daySettings { - if setting.general.timeSlots > maxStartingTimes { - maxStartingTimes = TimeIndex(setting.general.timeSlots) + if setting.timeSlots > maxStartingTimes { + maxStartingTimes = TimeIndex(setting.timeSlots) } - if setting.general.locations > maxLocations { - maxLocations = setting.general.locations + if setting.locations > maxLocations { + maxLocations = setting.locations } } - - guard constraints.hasDeterminism else { - return try await generateSchedules( - maxStartingTimes: maxStartingTimes, - maxLocations: maxLocations, - rng: SystemRandomNumberGenerator(), - ScheduleConfig< - SystemRandomNumberGenerator, - Set, - Set, - Set, - Set, - Set, - Set, - Set - >.self - ) - } - switch constraints.determinism.technique { - default: - 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( - maxStartingTimes: maxStartingTimes, - maxLocations: maxLocations, - rng: LCG( - seed: seed, - multiplier: multiplier, - increment: increment - ), - ScheduleConfig< - LCG, - OrderedSet, - OrderedSet, - OrderedSet, - OrderedSet, - OrderedSet, - OrderedSet, - OrderedSet - >.self - ) - } - } - private func generateSchedules( - maxStartingTimes: TimeIndex, - maxLocations: LocationIndex, - rng: Config.RNG, - _ config: Config.Type - ) async throws -> [LeagueGenerationData] { var divisionEntries:ContiguousArray = .init(repeating: .init(), count: divisions.count) for entryIndex in 0..( + private func generateDivisionSchedulesInParallel( divisionsCount: Int, divisionEntries: ContiguousArray, maxStartingTimes: TimeIndex, @@ -156,9 +108,10 @@ extension RequestPayload.Runtime { attempts -= 1 await withTaskGroup { group in for (dow, scheduledEntries) in grouped { + let s = self.copy() group.addTask { return (dow, Self.generateSchedule( - settings: self, + settings: s, dataSnapshot: dataSnapshot, divisionsCount: divisionsCount, maxStartingTimes: maxStartingTimes, @@ -193,9 +146,10 @@ extension RequestPayload.Runtime { timeout: remainingTimeoutDelay ) { group in for (dow, scheduledEntries) in grouped { + let s = self.copy() group.addTask { return (dow, Self.generateSchedule( - settings: self, + settings: s, dataSnapshot: dataSnapshot, divisionsCount: divisionsCount, maxStartingTimes: maxStartingTimes, @@ -262,8 +216,13 @@ extension RequestPayload.Runtime { // MARK: Generate schedule extension RequestPayload.Runtime { - private static func generateSchedule( - settings: RequestPayload.Runtime, + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif + private static func generateSchedule( + settings: borrowing RequestPayload.Runtime, dataSnapshot: LeagueScheduleDataSnapshot, divisionsCount: Int, maxStartingTimes: TimeIndex, @@ -296,12 +255,8 @@ extension RequestPayload.Runtime { while day < gameDays { if gameDaySettingValuesCount <= day { gameDaySettingValuesCount += 1 - let daySettings = settings.daySettings[unchecked: day].general - let availableSlots:Config.AvailableSlotSet = Self.availableSlots( - times: daySettings.timeSlots, - locations: daySettings.locations, - locationTimeExclusivity: daySettings.locationTimeExclusivities - ) + let daySettings = settings.daySettings[unchecked: day] + let availableSlots = daySettings.availableSlots() do throws(LeagueError) { try data.newDay( day: day, @@ -332,7 +287,7 @@ extension RequestPayload.Runtime { } if !assignedSlots { guard generationData.assignLocationTimeRegenerationAttempts != settings.constraints.regenerationAttemptsThreshold else { - generationData.error = LeagueError.failedAssignment( + generationData.error = .failedAssignment( regenerationAttemptsThreshold: settings.constraints.regenerationAttemptsThreshold, balanceTimeStrictness: settings.general.balanceTimeStrictness ) @@ -345,7 +300,7 @@ extension RequestPayload.Runtime { if gameDayRegenerationAttempt == settings.constraints.regenerationAttemptsForConsecutiveDay { if day == 0 { guard generationData.negativeDayIndexRegenerationAttempts != settings.constraints.regenerationAttemptsForFirstDay else { - generationData.error = LeagueError.failedNegativeDayIndex + generationData.error = .failedNegativeDayIndex finalizeGenerationData(generationData: &generationData, data: data) return generationData } @@ -362,7 +317,9 @@ extension RequestPayload.Runtime { } else { #if LOG print("failed to assign slots for day \(day)") - 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 break; #endif @@ -381,7 +338,13 @@ extension RequestPayload.Runtime { finalizeGenerationData(generationData: &generationData, data: data) return generationData } - private static func finalizeGenerationData( + + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif + private static func finalizeGenerationData( generationData: inout LeagueGenerationData, data: borrowing LeagueScheduleData ) { @@ -392,10 +355,15 @@ extension RequestPayload.Runtime { // MARK: Load max allocations extension RequestPayload.Runtime { - static func loadMaxAllocations( + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif + static func loadMaxAllocations( dataSnapshot: inout LeagueScheduleDataSnapshot, gameDayDivisionEntries: inout ContiguousArray>, - settings: borrowing RequestPayload.Runtime, + settings: borrowing RequestPayload.Runtime, maxStartingTimes: TimeIndex, maxLocations: LocationIndex, scheduledEntries: Config.EntryIDSet @@ -409,7 +377,7 @@ extension RequestPayload.Runtime { //var maxPossiblePlayedForLocations = [LocationIndex](repeating: 0, count: maxLocations) for day in 0.. TimeIndex { - var totalMatchupsPlayed:LocationIndex = 0 - var filledTimes:TimeIndex = 0 - while totalMatchupsPlayed < matchupsCount { - filledTimes += 1 - totalMatchupsPlayed += locations - } - #if LOG - print("LeagueSchedule;optimalTimeSlots;availableTimeSlots=\(availableTimeSlots);locations=\(locations);matchupsCount=\(matchupsCount);totalMatchupsPlayed=\(totalMatchupsPlayed);filledTimes=\(filledTimes)") - #endif - return min(availableTimeSlots, filledTimes) - } -} - -// MARK: Get available slots -extension RequestPayload.Runtime { - static func availableSlots( - times: TimeIndex, - locations: LocationIndex, - locationTimeExclusivity: [Set]? - ) -> AvailableSlotSet { - var slots = AvailableSlotSet() - slots.reserveCapacity(Int(times) * locations) - if let exclusivities = locationTimeExclusivity { - for location in 0..( - totalMatchupsPlayed: some FixedWidthInteger, - value: some FixedWidthInteger, - strictness: BalanceStrictness - ) -> T { - guard strictness != .lenient else { return .max } - var minimumValue = T(ceil(Double(totalMatchupsPlayed) / Double(value))) - switch strictness { - case .lenient: minimumValue = .max - case .normal: minimumValue += 1 - case .relaxed: minimumValue += 2 - case .very: break - case .UNRECOGNIZED: break - } - return minimumValue - } -} - // MARK: Maximum same opponent matchups extension RequestPayload.Runtime { - static func maximumSameOpponentMatchups( + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif + static func maximumSameOpponentMatchups( gameDays: DayIndex, entriesCount: Int, - divisionEntries: ContiguousArray, - divisions: [Division.Runtime] + divisionEntries: ContiguousArray, + divisions: [Config.DivisionRuntime] ) -> MaximumSameOpponentMatchups { var maxSameOpponentMatchups:MaximumSameOpponentMatchups = .init(repeating: .init(repeating: .max, count: entriesCount), count: entriesCount) for (divisionIndex, division) in divisions.enumerated() { diff --git a/Sources/league-scheduling/data/LeagueScheduleData.swift b/Sources/league-scheduling/data/LeagueScheduleData.swift index 09fd962..4fc12ab 100644 --- a/Sources/league-scheduling/data/LeagueScheduleData.swift +++ b/Sources/league-scheduling/data/LeagueScheduleData.swift @@ -1,6 +1,10 @@ import StaticDateTimes +#if SpecializeScheduleConfiguration +import OrderedCollections +#endif + // MARK: Data /// Fundamental building block that keeps track of and enforces assignment rules when building the schedule. struct LeagueScheduleData: Sendable, ~Copyable { @@ -40,6 +44,11 @@ struct LeagueScheduleData: Sendable, ~Copyable { var redistributionData:RedistributionData? var redistributedMatchups = false + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif init( snapshot: LeagueScheduleDataSnapshot ) { @@ -64,6 +73,11 @@ struct LeagueScheduleData: Sendable, ~Copyable { // MARK: Snapshot extension LeagueScheduleData { + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif mutating func loadSnapshot(_ snapshot: LeagueScheduleDataSnapshot) { //locations = snapshot.locations rng = snapshot.rng @@ -80,6 +94,11 @@ extension LeagueScheduleData { shuffleHistory = snapshot.shuffleHistory } + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif func snapshot() -> LeagueScheduleDataSnapshot { return .init(self) } @@ -93,12 +112,17 @@ extension LeagueScheduleData { /// - day: Day index that will be scheduled. /// - divisionEntries: Division entries that play on the `day`. (`Division.IDValue`: `Set`) /// - entryMatchupsPerGameDay: Number of times a single team will play on `day`. + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif mutating func newDay( day: DayIndex, - daySettings: GeneralSettings.Runtime, + daySettings: GeneralSettings.Runtime, divisionEntries: ContiguousArray, availableSlots: Config.AvailableSlotSet, - settings: RequestPayload.Runtime, + settings: borrowing RequestPayload.Runtime, generationData: inout LeagueGenerationData ) throws(LeagueError) { let now = clock.now @@ -124,8 +148,8 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: defaultMaxEntryMatchupsPerGameDay ) - entriesInDivision.removeAll(where: { entryID in - assignmentState.numberOfAssignedMatchups[unchecked: entryID] >= daySettings.maximumPlayableMatchups[unchecked: entryID] + entriesInDivision.removeAll(where: { + assignmentState.numberOfAssignedMatchups[unchecked: $0] >= daySettings.maximumPlayableMatchups[unchecked: $0] }) entryCountsForDivision[divisionIndex] = entriesInDivision.count @@ -158,13 +182,15 @@ extension LeagueScheduleData { assignmentState.allMatchups = availableMatchups assignmentState.availableMatchups = availableMatchups assignmentState.prioritizedEntries = prioritizedEntries - assignmentState.matchups = Config.MatchupSet(minimumCapacity: availableSlots.count) + assignmentState.matchups = .init(minimumCapacity: availableSlots.count) for i in 0..: Sendable { let rng:Config.RNG let entriesPerMatchup:EntriesPerMatchup @@ -28,6 +32,11 @@ struct LeagueScheduleDataSnapshot: Sendable { var executionSteps = [ExecutionStep]() var shuffleHistory = [LeagueShuffleAction]() + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif init( rng: Config.RNG, maxStartingTimes: TimeIndex, @@ -35,7 +44,7 @@ struct LeagueScheduleDataSnapshot: Sendable { maxLocations: LocationIndex, entriesPerMatchup: EntriesPerMatchup, maximumPlayableMatchups: [UInt32], - entries: [Entry.Runtime], + entries: [Config.EntryRuntime], divisionEntries: ContiguousArray, matchupDuration: MatchupDuration, gameGap: (Int, Int), @@ -64,9 +73,10 @@ struct LeagueScheduleDataSnapshot: Sendable { let playsAt = ContiguousArray( repeating: .init(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)), count: entriesCount ) - let playsAtTimes = PlaysAtTimesArray( - times: .init(repeating: .init(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)), count: entriesCount) + let playsAtTimes = ContiguousArray( + repeating: .init(minimumCapacity: Int(defaultMaxEntryMatchupsPerGameDay)), count: entriesCount ) + let playsAtLocations:ContiguousArray = .init(repeating: .init(), count: entriesCount) assignmentState = .init( entries: entries, startingTimes: startingTimes, @@ -91,12 +101,17 @@ struct LeagueScheduleDataSnapshot: Sendable { availableSlots: .init(), playsAt: playsAt, playsAtTimes: playsAtTimes, - playsAtLocations: .init(repeating: Set(minimumCapacity: defaultMaxEntryMatchupsPerGameDay), count: entriesCount), + playsAtLocations: playsAtLocations, matchups: .init(), shuffleHistory: [] ) } - + + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif init(_ snapshot: borrowing LeagueScheduleData) { rng = snapshot.rng entriesPerMatchup = snapshot.entriesPerMatchup diff --git a/Sources/league-scheduling/data/MatchupBlock.swift b/Sources/league-scheduling/data/MatchupBlock.swift index 6d2c261..9c0347b 100644 --- a/Sources/league-scheduling/data/MatchupBlock.swift +++ b/Sources/league-scheduling/data/MatchupBlock.swift @@ -55,7 +55,7 @@ extension LeagueScheduleData { gameGap: GameGap.TupleValue, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, - rng: inout some RandomNumberGenerator, + rng: inout some RandomNumberGenerator & Sendable, assignmentState: inout AssignmentState, selectSlot: borrowing some SelectSlotProtocol & ~Copyable, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable @@ -230,7 +230,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, allAvailableMatchups: Config.MatchupPairSet, - rng: inout some RandomNumberGenerator, + rng: inout some RandomNumberGenerator & Sendable, localAssignmentState: inout AssignmentState, remainingPrioritizedEntries: inout Config.EntryIDSet, selectedEntries: inout Config.EntryIDSet, @@ -269,7 +269,7 @@ extension LeagueScheduleData { entryMatchupsPerGameDay: EntryMatchupsPerGameDay, divisionRecurringDayLimitInterval: ContiguousArray, allAvailableMatchups: Config.MatchupPairSet, - rng: inout some RandomNumberGenerator, + rng: inout some RandomNumberGenerator & Sendable, localAssignmentState: inout AssignmentState, shouldSkipSelection: (MatchupPair) -> Bool, remainingPrioritizedEntries: inout Config.EntryIDSet, diff --git a/Sources/league-scheduling/data/PossibleAllocations.swift b/Sources/league-scheduling/data/PossibleAllocations.swift index d49843e..2c011ed 100644 --- a/Sources/league-scheduling/data/PossibleAllocations.swift +++ b/Sources/league-scheduling/data/PossibleAllocations.swift @@ -5,7 +5,8 @@ extension AssignmentState { entriesCount: Int ) { possibleAllocations = .init(repeating: availableSlots, count: entriesCount) - var cached = Set(minimumCapacity: entriesCount) + var cached = Config.EntryIDSet() + cached.reserveCapacity(entriesCount) availableMatchups.forEach { matchup in recalculateNewDayPossibleAllocations( for: matchup, @@ -19,7 +20,7 @@ extension AssignmentState { private mutating func recalculateNewDayPossibleAllocations( for pair: MatchupPair, - cached: inout Set + cached: inout Config.EntryIDSet ) { recalculateNewDayPossibleAllocations( for: pair.team1, @@ -32,10 +33,10 @@ extension AssignmentState { } private mutating func recalculateNewDayPossibleAllocations( for team: Entry.IDValue, - cached: inout Set + cached: inout Config.EntryIDSet ) { guard !cached.contains(team) else { return } - cached.insert(team) + cached.insertMember(team) let timeNumbers = assignedTimes[unchecked: team] let locationNumbers = assignedLocations[unchecked: team] let maxTimeNumbers = maxTimeAllocations[unchecked: team] @@ -58,7 +59,8 @@ extension AssignmentState { gameGap: GameGap.TupleValue, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) { - var cached = Set(minimumCapacity: entriesCount) + var cached = Config.EntryIDSet() + cached.reserveCapacity(entriesCount) availableMatchups.forEach { matchup in recalculatePossibleAllocations( day: day, @@ -76,7 +78,7 @@ extension AssignmentState { private mutating func recalculatePossibleAllocations( day: DayIndex, for pair: MatchupPair, - cached: inout Set, + cached: inout Config.EntryIDSet, gameGap: GameGap.TupleValue, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) { @@ -99,12 +101,12 @@ extension AssignmentState { private mutating func recalculatePossibleAllocations( day: DayIndex, for team: Entry.IDValue, - cached: inout Set, + cached: inout Config.EntryIDSet, gameGap: GameGap.TupleValue, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable ) { guard !cached.contains(team) else { return } - cached.insert(team) + cached.insertMember(team) let allowedTimes = entries[unchecked: team].gameTimes[unchecked: day] let allowedLocations = entries[unchecked: team].gameLocations[unchecked: day] let playsAt = playsAt[unchecked: team] diff --git a/Sources/league-scheduling/data/Redistribute.swift b/Sources/league-scheduling/data/Redistribute.swift index 073fcf3..f3cb348 100644 --- a/Sources/league-scheduling/data/Redistribute.swift +++ b/Sources/league-scheduling/data/Redistribute.swift @@ -3,7 +3,7 @@ extension LeagueScheduleData { /// Tries to move previously scheduled matchups to later days. mutating func tryRedistributing( - settings: RequestPayload.Runtime, + settings: borrowing RequestPayload.Runtime, generationData: inout LeagueGenerationData ) throws(LeagueError) { guard day > 0 else { @@ -39,7 +39,7 @@ extension LeagueScheduleData { private mutating func tryRedistributing( startDayIndex: DayIndex, - settings: RequestPayload.Runtime, + settings: borrowing RequestPayload.Runtime, canPlayAt: borrowing some CanPlayAtProtocol & ~Copyable, generationData: inout LeagueGenerationData ) throws(LeagueError) { @@ -47,9 +47,8 @@ extension LeagueScheduleData { redistributionData = .init( dayIndex: day, startDayIndex: startDayIndex, - settings: settings, - entryMatchupsPerGameDay: defaultMaxEntryMatchupsPerGameDay, - entriesPerMatchup: entriesPerMatchup + settings: settings.redistributionSettings(for: day), + data: self ) } let previousSchedule = generationData.schedule diff --git a/Sources/league-scheduling/data/RedistributionData.swift b/Sources/league-scheduling/data/RedistributionData.swift index 9bb3e3a..3456b36 100644 --- a/Sources/league-scheduling/data/RedistributionData.swift +++ b/Sources/league-scheduling/data/RedistributionData.swift @@ -1,4 +1,8 @@ +#if SpecializeScheduleConfiguration +import OrderedCollections +#endif + struct RedistributionData: Sendable { /// The latest `DayIndex` that is allowed to redistribute matchups from. let startDayIndex:DayIndex @@ -8,24 +12,28 @@ struct RedistributionData: Sendable { let maxMovableMatchups:Int private var redistributedEntries:[UInt16] - private(set) var redistributed:Set + private(set) var redistributed:Config.MatchupSet + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif init( dayIndex: DayIndex, startDayIndex: DayIndex, - settings: RequestPayload.Runtime, - entryMatchupsPerGameDay: EntryMatchupsPerGameDay, - entriesPerMatchup: EntriesPerMatchup + settings: LitLeagues_Leagues_RedistributionSettings?, + data: borrowing LeagueScheduleData ) { self.startDayIndex = startDayIndex - self.entryMatchupsPerGameDay = entryMatchupsPerGameDay - redistributedEntries = .init(repeating: 0, count: settings.entries.count) - redistributed = [] + self.entryMatchupsPerGameDay = data.defaultMaxEntryMatchupsPerGameDay + redistributedEntries = .init(repeating: 0, count: data.entriesCount) + redistributed = .init() - let threshold = (settings.entries.count / entriesPerMatchup)// * entryMatchupsPerGameDay + let threshold = (data.entriesCount / data.entriesPerMatchup)// * entryMatchupsPerGameDay var minMatchupsRequired = threshold var maxMovableMatchups = threshold - if let r = settings.daySettings[unchecked: dayIndex].general.redistributionSettings ?? settings.general.redistributionSettings { + if let r = settings { minMatchupsRequired = r.hasMinMatchupsRequired ? Int(r.minMatchupsRequired) : threshold maxMovableMatchups = r.hasMaxMovableMatchups ? Int(r.maxMovableMatchups) : threshold } @@ -180,7 +188,7 @@ extension RedistributionData { generationData.schedule[unchecked: redistributable.fromDay].remove(redistributable.matchup) assignmentState.decrementAssignData(home: redistributable.matchup.home, away: redistributable.matchup.away, slot: redistributable.matchup.slot) - redistributed.insert(redistributable.matchup) + redistributed.insertMember(redistributable.matchup) redistributedEntries[unchecked: redistributable.matchup.home] += 1 redistributedEntries[unchecked: redistributable.matchup.away] += 1 // filter redistributables so only the ones that can still play remain diff --git a/Sources/league-scheduling/data/SelectMatchup.swift b/Sources/league-scheduling/data/SelectMatchup.swift index a72a6c7..3089a28 100644 --- a/Sources/league-scheduling/data/SelectMatchup.swift +++ b/Sources/league-scheduling/data/SelectMatchup.swift @@ -14,7 +14,7 @@ extension AssignmentState { /// - Returns: Matchup pair that should be prioritized to be scheduled. func selectMatchup( prioritizedMatchups: borrowing PrioritizedMatchups, - rng: inout some RandomNumberGenerator + rng: inout some RandomNumberGenerator & Sendable ) -> MatchupPair? { return Self.selectMatchup( prioritizedMatchups: prioritizedMatchups, @@ -31,7 +31,7 @@ extension AssignmentState { numberOfAssignedMatchups: [Int], recurringDayLimits: RecurringDayLimits, possibleAllocations: Config.PossibleAllocations, - rng: inout some RandomNumberGenerator + rng: inout some RandomNumberGenerator & Sendable ) -> MatchupPair? { #if LOG print("SelectMatchup;selectMatchup;prioritizedMatchups.count=\(prioritizedMatchups.matchups.count);availableMatchupCountForEntry=\(prioritizedMatchups.availableMatchupCountForEntry)") @@ -43,7 +43,7 @@ extension AssignmentState { var pool = Config.MatchupPairSet() prioritizedMatchups.matchups.forEach { pair in let (pairMinMatchupsPlayedSoFar, pairTotalMatchupsPlayedSoFar) = numberOfMatchupsPlayedSoFar(for: pair, numberOfAssignedMatchups: numberOfAssignedMatchups) - if selected == nil { + guard selected != nil else { selected = select( pair: pair, minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, @@ -53,112 +53,113 @@ extension AssignmentState { 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), - possibleAllocations: Self.possibleAllocations(for: pair, possibleAllocations: possibleAllocations), - remainingMatchupCount: remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), - pool: &pool - ) - } - return + return + } + guard pairMinMatchupsPlayedSoFar == selected.minMatchupsPlayedSoFar else { + if pairMinMatchupsPlayedSoFar < selected.minMatchupsPlayedSoFar { + selected = select( + pair: pair, + minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, + totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, + recurringDayLimit: recurringDayLimit(for: pair, recurringDayLimits: recurringDayLimits), + possibleAllocations: Self.possibleAllocations(for: pair, possibleAllocations: possibleAllocations), + 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), - possibleAllocations: Self.possibleAllocations(for: pair, possibleAllocations: possibleAllocations), - remainingMatchupCount: remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), - pool: &pool - ) - } - return + return + } + guard pairTotalMatchupsPlayedSoFar == selected.totalMatchupsPlayedSoFar else { + if pairTotalMatchupsPlayedSoFar < selected.totalMatchupsPlayedSoFar { + selected = select( + pair: pair, + minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, + totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, + recurringDayLimit: recurringDayLimit(for: pair, recurringDayLimits: recurringDayLimits), + possibleAllocations: Self.possibleAllocations(for: pair, possibleAllocations: possibleAllocations), + 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, - possibleAllocations: Self.possibleAllocations(for: pair, possibleAllocations: possibleAllocations), - remainingMatchupCount: remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), - pool: &pool - ) - } - return + return + } + 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, + possibleAllocations: Self.possibleAllocations(for: pair, possibleAllocations: possibleAllocations), + remainingMatchupCount: remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), + pool: &pool + ) } + return + } - let pairRemainingAllocations = Self.possibleAllocations(for: pair, possibleAllocations: possibleAllocations) - guard pairRemainingAllocations.min == selected.possibleAllocations.min else { - if pairRemainingAllocations.min < selected.possibleAllocations.min { - selected = select( - pair: pair, - minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, - totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, - recurringDayLimit: pairRecurringDayLimit, - possibleAllocations: pairRemainingAllocations, - remainingMatchupCount: Self.remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), - pool: &pool - ) - } - return + let pairRemainingAllocations = Self.possibleAllocations(for: pair, possibleAllocations: possibleAllocations) + guard pairRemainingAllocations.min == selected.possibleAllocations.min else { + if pairRemainingAllocations.min < selected.possibleAllocations.min { + selected = select( + pair: pair, + minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, + totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, + recurringDayLimit: pairRecurringDayLimit, + possibleAllocations: pairRemainingAllocations, + remainingMatchupCount: Self.remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), + pool: &pool + ) } - guard pairRemainingAllocations.max == selected.possibleAllocations.max else { - if pairRemainingAllocations.max < selected.possibleAllocations.max { - selected = select( - pair: pair, - minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, - totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, - recurringDayLimit: pairRecurringDayLimit, - possibleAllocations: pairRemainingAllocations, - remainingMatchupCount: Self.remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), - pool: &pool - ) - } - return + return + } + guard pairRemainingAllocations.max == selected.possibleAllocations.max else { + if pairRemainingAllocations.max < selected.possibleAllocations.max { + selected = select( + pair: pair, + minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, + totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, + recurringDayLimit: pairRecurringDayLimit, + possibleAllocations: pairRemainingAllocations, + remainingMatchupCount: Self.remainingMatchupCount(for: pair, prioritizedMatchups.availableMatchupCountForEntry), + pool: &pool + ) } + return + } - 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, - possibleAllocations: pairRemainingAllocations, - remainingMatchupCount: pairRemainingMatchupCount, - pool: &pool - ) - } - return + 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, + possibleAllocations: 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, - possibleAllocations: pairRemainingAllocations, - remainingMatchupCount: pairRemainingMatchupCount, - pool: &pool - ) - } - return + return + } + guard pairRemainingMatchupCount.max == selected.remainingMatchupCount.max else { + if pairRemainingMatchupCount.max < selected.remainingMatchupCount.max { + selected = select( + pair: pair, + minMatchupsPlayedSoFar: pairMinMatchupsPlayedSoFar, + totalMatchupsPlayedSoFar: pairTotalMatchupsPlayedSoFar, + recurringDayLimit: pairRecurringDayLimit, + possibleAllocations: pairRemainingAllocations, + remainingMatchupCount: pairRemainingMatchupCount, + pool: &pool + ) } - pool.insertMember(pair) + return } + + pool.insertMember(pair) } #if LOG print("SelectMatchup;selectMatchup;selected.pair=\(selected?.pair.description);pool=\(pool.map({ $0.description }))") diff --git a/Sources/league-scheduling/data/Shuffle.swift b/Sources/league-scheduling/data/Shuffle.swift index 1a39c12..9cb7558 100644 --- a/Sources/league-scheduling/data/Shuffle.swift +++ b/Sources/league-scheduling/data/Shuffle.swift @@ -82,8 +82,8 @@ extension AssignmentState { var homePlaysAtLocations = playsAtLocations[unchecked: swapped.home] var awayPlaysAtLocations = playsAtLocations[unchecked: swapped.away] - homePlaysAtLocations.remove(swapped.location) - awayPlaysAtLocations.remove(swapped.location) + homePlaysAtLocations.removeMember(swapped.location) + awayPlaysAtLocations.removeMember(swapped.location) let homeAssignedTimes = assignedTimes[unchecked: swapped.home] let awayAssignedTimes = assignedTimes[unchecked: swapped.away] diff --git a/Sources/league-scheduling/data/assignment/Assign.swift b/Sources/league-scheduling/data/assignment/Assign.swift index ea19c59..9d189b6 100644 --- a/Sources/league-scheduling/data/assignment/Assign.swift +++ b/Sources/league-scheduling/data/assignment/Assign.swift @@ -62,13 +62,13 @@ extension AssignmentState { availableMatchups.removeMember(.init(team1: matchup.team2, team2: matchup.team1)) // necessary because the pair's home/away could've been swapped due to balancing if playsAtTimes[unchecked: home].count == entryMatchupsPerGameDay { #if LOG - possibleAllocations[unchecked: home].removeAll() + possibleAllocations[unchecked: home].removeAllKeepingCapacity() #endif availableMatchups = availableMatchups.filter({ $0.team1 != home && $0.team2 != home }) } if playsAtTimes[unchecked: away].count == entryMatchupsPerGameDay { #if LOG - possibleAllocations[unchecked: away].removeAll() + possibleAllocations[unchecked: away].removeAllKeepingCapacity() #endif availableMatchups = availableMatchups.filter({ $0.team1 != away && $0.team2 != away }) } @@ -134,10 +134,10 @@ extension AssignmentState { ) { 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) + playsAtTimes[unchecked: home].insertMember(slot.time) + playsAtTimes[unchecked: away].insertMember(slot.time) + playsAtLocations[unchecked: home].insertMember(slot.location) + playsAtLocations[unchecked: away].insertMember(slot.location) } } diff --git a/Sources/league-scheduling/data/assignment/Unassign.swift b/Sources/league-scheduling/data/assignment/Unassign.swift index 360d13a..989f734 100644 --- a/Sources/league-scheduling/data/assignment/Unassign.swift +++ b/Sources/league-scheduling/data/assignment/Unassign.swift @@ -72,10 +72,10 @@ extension AssignmentState { ) { 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) + playsAtTimes[unchecked: home].removeMember(slot.time) + playsAtTimes[unchecked: away].removeMember(slot.time) + playsAtLocations[unchecked: home].removeMember(slot.location) + playsAtLocations[unchecked: away].removeMember(slot.location) } } diff --git a/Sources/league-scheduling/data/assignmentState/AssignmentState.swift b/Sources/league-scheduling/data/assignmentState/AssignmentState.swift index ff364b0..928b893 100644 --- a/Sources/league-scheduling/data/assignmentState/AssignmentState.swift +++ b/Sources/league-scheduling/data/assignmentState/AssignmentState.swift @@ -1,9 +1,13 @@ import StaticDateTimes +#if SpecializeScheduleConfiguration +import OrderedCollections +#endif + // MARK: Noncopyable struct AssignmentState: Sendable, ~Copyable { - let entries:[Entry.Runtime] + let entries:[Config.EntryRuntime] var startingTimes:[StaticTime] var matchupDuration:MatchupDuration var locationTravelDurations:[[MatchupDuration]] @@ -61,14 +65,19 @@ struct AssignmentState: Sendable, ~Copyable { var availableSlots:Config.AvailableSlotSet var playsAt:Config.PlaysAt - var playsAtTimes:PlaysAtTimesArray - var playsAtLocations:PlaysAtLocations + var playsAtTimes:ContiguousArray + var playsAtLocations:ContiguousArray /// Available matchups that can be scheduled. var matchups:Config.MatchupSet var shuffleHistory = [LeagueShuffleAction]() + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif func copyable() -> AssignmentStateCopyable { return .init( entries: entries, @@ -134,7 +143,7 @@ struct AssignmentState: Sendable, ~Copyable { // MARK: Copyable struct AssignmentStateCopyable { - let entries:[Entry.Runtime] + let entries:[Config.EntryRuntime] let startingTimes:[StaticTime] let matchupDuration:MatchupDuration let locationTravelDurations:[[MatchupDuration]] @@ -180,12 +189,17 @@ struct AssignmentStateCopyable { var availableSlots:Config.AvailableSlotSet var playsAt:Config.PlaysAt - var playsAtTimes:PlaysAtTimesArray - var playsAtLocations:PlaysAtLocations + var playsAtTimes:ContiguousArray + var playsAtLocations:ContiguousArray var matchups:Config.MatchupSet var shuffleHistory:[LeagueShuffleAction] + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif func noncopyable() -> AssignmentState { return .init( entries: entries, diff --git a/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift index b99a80f..2171f3f 100644 --- a/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift +++ b/Sources/league-scheduling/data/assignmentState/ScheduleConfiguration.swift @@ -1,7 +1,11 @@ +import OrderedCollections + protocol ScheduleConfiguration: Sendable, ~Copyable { associatedtype RNG:RandomNumberGenerator & Sendable + associatedtype DaySet:SetOfDayIndexes associatedtype TimeSet:SetOfTimeIndexes + associatedtype LocationSet:SetOfLocationIndexes associatedtype EntryIDSet:SetOfEntryIDs associatedtype AvailableSlotSet:SetOfAvailableSlots associatedtype MatchupPairSet:AbstractSet where MatchupPairSet.Element == MatchupPair @@ -15,11 +19,16 @@ protocol ScheduleConfiguration: Sendable, ~Copyable { typealias PossibleAllocations = ContiguousArray typealias PlaysAt = ContiguousArray typealias PlaysAtTimes = ContiguousArray + + typealias DivisionRuntime = Division.Runtime + typealias EntryRuntime = Entry.Runtime } enum ScheduleConfig< RNG: RandomNumberGenerator & Sendable, + DaySet: SetOfDayIndexes, TimeSet: SetOfTimeIndexes, + LocationSet: SetOfLocationIndexes, EntryIDSet: SetOfEntryIDs, AvailableSlotSet: SetOfAvailableSlots, MatchupPairSet: AbstractSet, @@ -32,4 +41,89 @@ enum ScheduleConfig< RedistributableMatchupSet.Element == RedistributableMatchup, FlippableMatchupSet.Element == FlippableMatchup { -} \ No newline at end of file +} + +#if SpecializeScheduleConfiguration + +// MARK: BitSet64 + +typealias ScheduleConfigBitSet64 = ScheduleConfig< + SystemRandomNumberGenerator, + BitSet64, + BitSet64, + BitSet64, + BitSet64, + Set, + Set, + Set, + Set, + Set +> +typealias ScheduleConfigDeterministicBitSet64 = ScheduleConfig< + LCG, + BitSet64, + BitSet64, + BitSet64, + BitSet64, + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet +> + +// MARK: BitSet128 + +typealias ScheduleConfigBitSet128 = ScheduleConfig< + SystemRandomNumberGenerator, + BitSet128, + BitSet128, + BitSet128, + BitSet128, + Set, + Set, + Set, + Set, + Set +> +typealias ScheduleConfigDeterministicBitSet128 = ScheduleConfig< + LCG, + BitSet128, + BitSet128, + BitSet128, + BitSet128, + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet +> + +// MARK: Set + +typealias ScheduleConfigSet = ScheduleConfig< + SystemRandomNumberGenerator, + Set, + Set, + Set, + Set, + Set, + Set, + Set, + Set, + Set +> +typealias ScheduleConfigDeterministicSet = ScheduleConfig< + LCG, + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet +> + +#endif \ No newline at end of file diff --git a/Sources/league-scheduling/data/canPlayAt/CanPlayAtNormal.swift b/Sources/league-scheduling/data/canPlayAt/CanPlayAtNormal.swift index 1c76aea..9116a3c 100644 --- a/Sources/league-scheduling/data/canPlayAt/CanPlayAtNormal.swift +++ b/Sources/league-scheduling/data/canPlayAt/CanPlayAtNormal.swift @@ -7,11 +7,11 @@ struct CanPlayAtNormal: CanPlayAtProtocol, ~Copyable { func test( time: TimeIndex, location: LocationIndex, - allowedTimes: Set, - allowedLocations: Set, + allowedTimes: borrowing some SetOfTimeIndexes & ~Copyable, + allowedLocations: borrowing some SetOfLocationIndexes & ~Copyable, playsAt: borrowing some SetOfAvailableSlots & ~Copyable, playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, - playsAtLocations: PlaysAtLocations.Element, + playsAtLocations: borrowing some SetOfLocationIndexes & ~Copyable, timeNumber: UInt8, locationNumber: UInt8, maxTimeNumber: UInt8, @@ -37,8 +37,8 @@ struct CanPlayAtNormal: CanPlayAtProtocol, ~Copyable { static func test( time: TimeIndex, location: LocationIndex, - allowedTimes: Set, - allowedLocations: Set, + allowedTimes: borrowing some SetOfTimeIndexes & ~Copyable, + allowedLocations: borrowing some SetOfLocationIndexes & ~Copyable, playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, timeNumber: UInt8, locationNumber: UInt8, @@ -64,8 +64,8 @@ struct CanPlayAtNormal: CanPlayAtProtocol, ~Copyable { static func isAllowed( time: TimeIndex, location: LocationIndex, - allowedTimes: Set, - allowedLocations: Set, + allowedTimes: borrowing some SetOfTimeIndexes & ~Copyable, + allowedLocations: borrowing some SetOfLocationIndexes & ~Copyable, playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, timeNumber: UInt8, locationNumber: UInt8, diff --git a/Sources/league-scheduling/data/canPlayAt/CanPlayAtProtocol.swift b/Sources/league-scheduling/data/canPlayAt/CanPlayAtProtocol.swift index 44fe2cd..37d397c 100644 --- a/Sources/league-scheduling/data/canPlayAt/CanPlayAtProtocol.swift +++ b/Sources/league-scheduling/data/canPlayAt/CanPlayAtProtocol.swift @@ -6,11 +6,11 @@ protocol CanPlayAtProtocol: Sendable, ~Copyable { func test( time: TimeIndex, location: LocationIndex, - allowedTimes: Set, - allowedLocations: Set, + allowedTimes: borrowing some SetOfTimeIndexes & ~Copyable, + allowedLocations: borrowing some SetOfLocationIndexes & ~Copyable, playsAt: borrowing some SetOfAvailableSlots & ~Copyable, playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, - playsAtLocations: PlaysAtLocations.Element, + playsAtLocations: borrowing some SetOfLocationIndexes & ~Copyable, timeNumber: UInt8, locationNumber: UInt8, maxTimeNumber: UInt8, diff --git a/Sources/league-scheduling/data/canPlayAt/CanPlayAtSameLocationIfB2B.swift b/Sources/league-scheduling/data/canPlayAt/CanPlayAtSameLocationIfB2B.swift index 8b70c2e..ac317f2 100644 --- a/Sources/league-scheduling/data/canPlayAt/CanPlayAtSameLocationIfB2B.swift +++ b/Sources/league-scheduling/data/canPlayAt/CanPlayAtSameLocationIfB2B.swift @@ -5,11 +5,11 @@ struct CanPlayAtSameLocationIfB2B: CanPlayAtProtocol, ~Copyable { func test( time: TimeIndex, location: LocationIndex, - allowedTimes: Set, - allowedLocations: Set, + allowedTimes: borrowing some SetOfTimeIndexes & ~Copyable, + allowedLocations: borrowing some SetOfLocationIndexes & ~Copyable, playsAt: borrowing some SetOfAvailableSlots & ~Copyable, playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, - playsAtLocations: PlaysAtLocations.Element, + playsAtLocations: borrowing some SetOfLocationIndexes & ~Copyable, timeNumber: UInt8, locationNumber: UInt8, maxTimeNumber: UInt8, @@ -41,7 +41,7 @@ struct CanPlayAtSameLocationIfB2B: CanPlayAtProtocol, ~Copyable { time: TimeIndex, location: LocationIndex, playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, - playsAtLocations: PlaysAtLocations.Element + playsAtLocations: borrowing some SetOfLocationIndexes & ~Copyable ) -> Bool { if time > 0 && playsAtTimes.contains(time-1) || playsAtTimes.contains(time+1) { // is back-to-back diff --git a/Sources/league-scheduling/data/canPlayAt/CanPlayAtWithTravelDurations.swift b/Sources/league-scheduling/data/canPlayAt/CanPlayAtWithTravelDurations.swift index f9af85d..dfc485a 100644 --- a/Sources/league-scheduling/data/canPlayAt/CanPlayAtWithTravelDurations.swift +++ b/Sources/league-scheduling/data/canPlayAt/CanPlayAtWithTravelDurations.swift @@ -9,11 +9,11 @@ struct CanPlayAtWithTravelDurations: CanPlayAtProtocol, ~Copyable { func test( time: TimeIndex, location: LocationIndex, - allowedTimes: Set, - allowedLocations: Set, + allowedTimes: borrowing some SetOfTimeIndexes & ~Copyable, + allowedLocations: borrowing some SetOfLocationIndexes & ~Copyable, playsAt: borrowing some SetOfAvailableSlots & ~Copyable, playsAtTimes: borrowing some SetOfTimeIndexes & ~Copyable, - playsAtLocations: PlaysAtLocations.Element, + playsAtLocations: borrowing some SetOfLocationIndexes & ~Copyable, timeNumber: UInt8, locationNumber: UInt8, maxTimeNumber: UInt8, diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift index 8e82ceb..9cd37bb 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotB2B.swift @@ -2,13 +2,13 @@ struct SelectSlotB2B: SelectSlotProtocol, ~Copyable { let entryMatchupsPerGameDay:EntryMatchupsPerGameDay - func select( + func select( team1: Entry.IDValue, team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playsAtTimes: borrowing PlaysAtTimesArray, - playsAtLocations: PlaysAtLocations, + playsAtTimes: ContiguousArray, + playsAtLocations: ContiguousArray, playableSlots: inout some SetOfAvailableSlots & ~Copyable ) -> AvailableSlot? { filter( @@ -32,7 +32,7 @@ extension SelectSlotB2B { private func filter( team1: Entry.IDValue, team2: Entry.IDValue, - playsAtTimes: borrowing PlaysAtTimesArray, + playsAtTimes: ContiguousArray, playableSlots: inout some SetOfAvailableSlots & ~Copyable ) { //print("filterSlotBack2Back;playsAtTimes[unchecked: team1].isEmpty=\(playsAtTimes[unchecked: team1].isEmpty);playsAtTimes[unchecked: team2].isEmpty=\(playsAtTimes[unchecked: team2].isEmpty)") diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift index 20dd36d..c6f5c35 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTime.swift @@ -1,12 +1,12 @@ struct SelectSlotEarliestTime: SelectSlotProtocol, ~Copyable { - func select( + func select( team1: Entry.IDValue, team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playsAtTimes: borrowing PlaysAtTimesArray, - playsAtLocations: PlaysAtLocations, + playsAtTimes: ContiguousArray, + playsAtLocations: ContiguousArray, playableSlots: inout some SetOfAvailableSlots & ~Copyable ) -> AvailableSlot? { return Self.select( diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift index 3fb737d..d44986b 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotEarliestTimeAndSameLocationIfB2B.swift @@ -1,12 +1,12 @@ struct SelectSlotEarliestTimeAndSameLocationIfB2B: SelectSlotProtocol, ~Copyable { - func select( + func select( team1: Entry.IDValue, team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playsAtTimes: borrowing PlaysAtTimesArray, - playsAtLocations: PlaysAtLocations, + playsAtTimes: ContiguousArray, + playsAtLocations: ContiguousArray, playableSlots: inout some SetOfAvailableSlots & ~Copyable ) -> AvailableSlot? { guard !playableSlots.isEmpty else { return nil } @@ -24,9 +24,10 @@ struct SelectSlotEarliestTimeAndSameLocationIfB2B: SelectSlotProtocol, ~Copyable // at least one of the teams already plays let team1Times = assignedTimes[unchecked: team1] let team1Locations = assignedLocations[unchecked: team1] - let team1PlaysAtLocations = playsAtLocations[unchecked: team1] let team2Times = assignedTimes[unchecked: team2] let team2Locations = assignedLocations[unchecked: team2] + + let team1PlaysAtLocations = playsAtLocations[unchecked: team1] let team2PlaysAtLocations = playsAtLocations[unchecked: team2] var nonBackToBackSlots = [AvailableSlot]() diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift index 5889166..de2bbaa 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotNormal.swift @@ -1,12 +1,12 @@ struct SelectSlotNormal: SelectSlotProtocol, ~Copyable { - func select( + func select( team1: Entry.IDValue, team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playsAtTimes: borrowing PlaysAtTimesArray, - playsAtLocations: PlaysAtLocations, + playsAtTimes: ContiguousArray, + playsAtLocations: ContiguousArray, playableSlots: inout some SetOfAvailableSlots & ~Copyable ) -> AvailableSlot? { return Self.select( diff --git a/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift b/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift index 4c3172c..aeb1f47 100644 --- a/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift +++ b/Sources/league-scheduling/data/selectSlot/SelectSlotProtocol.swift @@ -1,12 +1,12 @@ protocol SelectSlotProtocol: Sendable, ~Copyable { - func select( + func select( team1: Entry.IDValue, team2: Entry.IDValue, assignedTimes: AssignedTimes, assignedLocations: AssignedLocations, - playsAtTimes: borrowing PlaysAtTimesArray, - playsAtLocations: PlaysAtLocations, + playsAtTimes: ContiguousArray, + playsAtLocations: ContiguousArray, playableSlots: inout some SetOfAvailableSlots & ~Copyable ) -> AvailableSlot? } \ No newline at end of file diff --git a/Sources/league-scheduling/generated/extensions/RequestPayload+Extensions.swift b/Sources/league-scheduling/generated/extensions/RequestPayload+Extensions.swift index c74cd35..1f3d3ed 100644 --- a/Sources/league-scheduling/generated/extensions/RequestPayload+Extensions.swift +++ b/Sources/league-scheduling/generated/extensions/RequestPayload+Extensions.swift @@ -25,12 +25,4 @@ extension RequestPayload { self.divisions = .init(divisions: divisions) self.entries = teams } -} - -// MARK: Generate -extension RequestPayload { - public func generate() async throws(LeagueError) -> LeagueGenerationResult { - let runtime = try parseSettings() - return await runtime.generate() - } } \ No newline at end of file diff --git a/Sources/league-scheduling/generated/extensions/RequestPayload+ParseSettings.swift b/Sources/league-scheduling/generated/extensions/RequestPayload+Generate.swift similarity index 55% rename from Sources/league-scheduling/generated/extensions/RequestPayload+ParseSettings.swift rename to Sources/league-scheduling/generated/extensions/RequestPayload+Generate.swift index 25a9da8..548322e 100644 --- a/Sources/league-scheduling/generated/extensions/RequestPayload+ParseSettings.swift +++ b/Sources/league-scheduling/generated/extensions/RequestPayload+Generate.swift @@ -1,15 +1,10 @@ +import OrderedCollections import StaticDateTimes -#if canImport(SwiftGlibc) -import SwiftGlibc -#elseif canImport(Foundation) -import Foundation -#endif - -// MARK: Parse +// MARK: Generate extension RequestPayload { - func parseSettings() throws(LeagueError) -> RequestPayload.Runtime { + public func generate() async throws(LeagueError) -> LeagueGenerationResult { guard gameDays > 0 else { throw .malformedInput(msg: "'gameDays' needs to be > 0") } @@ -51,15 +46,99 @@ extension RequestPayload { } else { startingDayOfWeek = 0 } + return try await generate( + defaultGameGap: defaultGameGap, + divisionsCount: divisionsCount, + startingDayOfWeek: startingDayOfWeek + ) + } +} + +extension RequestPayload { + private func generate( + defaultGameGap: GameGap, + divisionsCount: Int, + startingDayOfWeek: DayOfWeek + ) async throws(LeagueError) -> LeagueGenerationResult { + if gameDays <= 64 && settings.timeSlots <= 64 && settings.locations <= 64 && entries.count <= 64 { + return try await generate( + defaultGameGap: defaultGameGap, + divisionsCount: divisionsCount, + startingDayOfWeek: startingDayOfWeek, + ( + BitSet64, + BitSet64, + BitSet64, + BitSet64 + ).self + ) + } else if gameDays <= 128 && settings.timeSlots <= 128 && settings.locations <= 128 && entries.count <= 128 { + return try await generate( + defaultGameGap: defaultGameGap, + divisionsCount: divisionsCount, + startingDayOfWeek: startingDayOfWeek, + ( + BitSet128, + BitSet128, + BitSet128, + BitSet128 + ).self + ) + } else { + guard generationConstraints.hasDeterminism else { + return try await generate( + defaultGameGap: defaultGameGap, + divisionsCount: divisionsCount, + startingDayOfWeek: startingDayOfWeek, + ( + Set, + Set, + Set, + Set + ).self + ) + } + return try await generate( + defaultGameGap: defaultGameGap, + divisionsCount: divisionsCount, + startingDayOfWeek: startingDayOfWeek, + ( + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet + ).self + ) + } + } +} + +extension RequestPayload { + private func generate< + DaySet: SetOfDayIndexes, + TimeSet: SetOfTimeIndexes, + LocationSet: SetOfLocationIndexes, + EntryIDSet: SetOfEntryIDs + >( + defaultGameGap: GameGap, + divisionsCount: Int, + startingDayOfWeek: DayOfWeek, + _ c: ( + DaySet, + TimeSet, + LocationSet, + EntryIDSet + ).Type + ) async throws(LeagueError) -> LeagueGenerationResult { + let divisionDefaults:DivisionDefaults = loadDivisionDefaults(divisionsCount: divisionsCount) var teamsForDivision = [Int](repeating: 0, count: divisionsCount) - let divisionDefaults = loadDivisionDefaults(divisionsCount: divisionsCount) let entries = try parseEntries( divisionsCount: divisionsCount, teams: entries, teamsForDivision: &teamsForDivision, divisionDefaults: divisionDefaults ) - let runtimeDivisions = try parseDivisions( + let divisions = try parseDivisions( divisionsCount: divisionsCount, locations: settings.locations, divisionGameDays: divisionDefaults.gameDays, @@ -67,13 +146,74 @@ extension RequestPayload { fallbackDayOfWeek: startingDayOfWeek, teamsForDivision: teamsForDivision ) - let timesSet = Set(0..(0.., + Set, + Set, + Set, + Set + >.self + ) + } + switch generationConstraints.determinism.technique { + default: + let seed = generationConstraints.determinism.hasSeed ? generationConstraints.determinism.seed : 1 + let multiplier = generationConstraints.determinism.hasMultiplier ? generationConstraints.determinism.multiplier : 6364136223846793005 + let increment = generationConstraints.determinism.hasIncrement ? generationConstraints.determinism.increment : 1442695040888963407 + return try await generate( + defaultGameGap: defaultGameGap, + entries: entries, + divisions: divisions, + rng: LCG( + seed: seed, + multiplier: multiplier, + increment: increment + ), + ScheduleConfig< + LCG, + DaySet, + TimeSet, + LocationSet, + EntryIDSet, + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet, + OrderedSet + >.self + ) + } + } + private func generate( + defaultGameGap: GameGap, + entries: [Entry.Runtime], + divisions: [Division.Runtime], + rng: Config.RNG, + _ c: Config.Type + ) async throws(LeagueError) -> LeagueGenerationResult { + let correctMaximumPlayableMatchups = Self.calculateMaximumPlayableMatchups( + gameDays: gameDays, + entryMatchupsPerGameDay: settings.entryMatchupsPerGameDay, + teamsCount: entries.count, + maximumPlayableMatchups: settings.maximumPlayableMatchups.array + ) + let timesSet = Config.TimeSet(0.. - var balancedLocations:Set + var balancedTimes:Config.TimeSet + var balancedLocations:Config.LocationSet if settings.balanceTimeStrictness != .lenient { balancedTimes = timesSet } else { - balancedTimes = [] + balancedTimes = .init() } if settings.balanceLocationStrictness != .lenient { - balancedLocations = locationsSet + balancedLocations = Config.LocationSet(0..( + gameGap: defaultGameGap, + timeSlots: settings.timeSlots, + startingTimes: settings.startingTimes.times, + entriesPerLocation: settings.entriesPerLocation, + locations: settings.locations, + entryMatchupsPerGameDay: settings.entryMatchupsPerGameDay, + maximumPlayableMatchups: correctMaximumPlayableMatchups, + matchupDuration: settings.matchupDuration, + locationTimeExclusivities: defaultTimeExclusivities, + locationTravelDurations: defaultTravelDurations, + balanceTimeStrictness: settings.balanceTimeStrictness, + balancedTimes: balancedTimes, + balanceLocationStrictness: settings.balanceLocationStrictness, + balancedLocations: balancedLocations, + redistributionSettings: settings.hasRedistributionSettings ? settings.redistributionSettings : nil, + flags: settings.flags + ) ) + } +} + +extension RequestPayload { + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif + private func generate( + rng: Config.RNG, + divisions: [Config.DivisionRuntime], + entries: [Config.EntryRuntime], + correctMaximumPlayableMatchups: [UInt32], + general: GeneralSettings.Runtime + ) async throws(LeagueError) -> LeagueGenerationResult { let daySettings = try parseDaySettings( general: general, correctMaximumPlayableMatchups: correctMaximumPlayableMatchups, @@ -143,36 +298,42 @@ extension RequestPayload { constraints.regenerationAttemptsThreshold = generationConstraints.regenerationAttemptsThreshold } } - return .init( + let runtime = RequestPayload.Runtime( + rng: rng, constraints: constraints, gameDays: gameDays, - divisions: runtimeDivisions, + divisions: divisions, entries: entries, general: general, daySettings: daySettings ) + return await runtime.generate() } } // MARK: Division defaults extension RequestPayload { - struct DivisionDefaults: Sendable, ~Copyable { - let gameDays:[Set] - let byes:[Set] - let gameTimes:[[Set]] - let gameLocations:[[Set]] + struct DivisionDefaults: Sendable, ~Copyable { + let gameDays:[DaySet] + let byes:[DaySet] + let gameTimes:[[Times]] + let gameLocations:[[Locations]] } } // MARK: Load division defaults extension RequestPayload { - private func loadDivisionDefaults( + private func loadDivisionDefaults< + DaySet: SetOfDayIndexes, + Times: SetOfTimeIndexes, + Locations: SetOfLocationIndexes + >( divisionsCount: Int - ) -> DivisionDefaults { - var gameDays = [Set]() - var byes = [Set]() - var gameTimes = [[Set]]() - var gameLocations = [[Set]]() + ) -> DivisionDefaults { + var gameDays = [DaySet]() + var byes = [DaySet]() + var gameTimes = [[Times]]() + var gameLocations = [[Locations]]() gameDays.reserveCapacity(divisionsCount) byes.reserveCapacity(divisionsCount) gameTimes.reserveCapacity(divisionsCount) @@ -189,22 +350,22 @@ extension RequestPayload { } if hasDivisions { for division in divisions.divisions { - let targetGameDays:Set + let targetGameDays:DaySet if division.hasGameDays { - targetGameDays = Set(division.gameDays.gameDayIndexes) + targetGameDays = .init(division.gameDays.gameDayIndexes) } else { - targetGameDays = Set(0..]() + var dgdt = [Times]() for gameDay in 0..]() + var dgdl = [Locations]() for gameDay in 0..]() + var dgdt = [Times]() for gameDay in 0..]() + var dgdl = [Locations]() for gameDay in 0..( divisionsCount: Int, teams: [Entry], teamsForDivision: inout [Int], - divisionDefaults: borrowing DivisionDefaults - ) throws(LeagueError) -> [Entry.Runtime] { - var entries = [Entry.Runtime]() + divisionDefaults: borrowing DivisionDefaults + ) throws(LeagueError) -> [Entry.Runtime] { + var entries = [Entry.Runtime]() entries.reserveCapacity(teams.count) for (i, team) in teams.enumerated() { if team.hasGameDayTimes { @@ -310,15 +471,15 @@ extension RequestPayload { // MARK: Parse divisions extension RequestPayload { - private func parseDivisions( + private func parseDivisions( divisionsCount: Int, locations: LocationIndex, - divisionGameDays: [Set], + divisionGameDays: [DaySet], defaultGameGap: GameGap, fallbackDayOfWeek: DayOfWeek, teamsForDivision: [Int] - ) throws(LeagueError) -> [Division.Runtime] { - var runtimeDivisions = [Division.Runtime]() + ) throws(LeagueError) -> [Division.Runtime] { + var runtimeDivisions = [Division.Runtime]() runtimeDivisions.reserveCapacity(divisionsCount) if hasDivisions { for (i, division) in divisions.divisions.enumerated() { @@ -347,7 +508,11 @@ extension RequestPayload { throw .malformedInput(msg: "'locationTimeExclusivities' size != \(locations) for division at index \(i)") } } - let maxSameOpponentMatchups = try calculateMaximumSameOpponentMatchupsCap(entriesCount: teamsForDivision[Int(i)]) + let maxSameOpponentMatchups = try calculateMaximumSameOpponentMatchupsCap( + gameDays: gameDays, + entryMatchupsPerGameDay: settings.entryMatchupsPerGameDay, + entriesCount: teamsForDivision[Int(i)] + ) try runtimeDivisions.append(division.runtime( defaultGameDays: divisionGameDays[unchecked: i], defaultGameGap: defaultGameGap, @@ -356,7 +521,11 @@ extension RequestPayload { )) } } else { - let maxSameOpponentMatchups = try calculateMaximumSameOpponentMatchupsCap(entriesCount: teamsForDivision[0]) + let maxSameOpponentMatchups = try calculateMaximumSameOpponentMatchupsCap( + gameDays: gameDays, + entryMatchupsPerGameDay: settings.entryMatchupsPerGameDay, + entriesCount: teamsForDivision[Int(0)] + ) runtimeDivisions.append(.init( dayOfWeek: fallbackDayOfWeek, gameDays: divisionGameDays[unchecked: 0], @@ -370,196 +539,44 @@ extension RequestPayload { // MARK: Parse day settings extension RequestPayload { - private func parseDaySettings( - general: GeneralSettings.Runtime, + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif + private func parseDaySettings( + general: GeneralSettings.Runtime, correctMaximumPlayableMatchups: [UInt32], - entries: [Entry.Runtime] - ) throws(LeagueError) -> [DaySettings.Runtime] { - var daySettings = [DaySettings.Runtime]() + entries: [Config.EntryRuntime] + ) throws(LeagueError) -> [GeneralSettings.Runtime] { + var daySettings = [GeneralSettings.Runtime]() daySettings.reserveCapacity(gameDays) if hasIndividualDaySettings { for dayIndex in 0.. GameGap? { - let isDefault = kind == "default" - if isDefault || settings.hasTimeSlots { - guard settings.timeSlots > 0 else { - throw .malformedInput(msg: "\(kind) 'timeSlots' size needs to be > 0") - } - } - if settings.hasStartingTimes { - guard settings.startingTimes.times.count > 0 else { - throw .malformedInput(msg: "\(kind) 'startingTimes' size needs to be > 0") - } - } - if settings.hasTimeSlots && settings.hasStartingTimes { - guard settings.timeSlots == settings.startingTimes.times.count else { - throw .malformedInput(msg: "\(kind) 'timeSlots' and 'startingTimes' size need to be equal") - } - } - if isDefault || settings.hasLocations { - guard settings.locations > 0 else { - throw .malformedInput(msg: "\(kind) 'locations' needs to be > 0") - } - } - if isDefault || settings.hasEntryMatchupsPerGameDay { - guard settings.entryMatchupsPerGameDay > 0 else { - throw .malformedInput(msg: "\(kind) 'entryMatchupsPerGameDay' needs to be > 0") - } - } - if settings.hasMaximumPlayableMatchups { - guard settings.maximumPlayableMatchups.array.count == entries.count else { - throw .malformedInput(msg: "\(kind) 'maximumPlayableMatchups' size != \(entries.count)") - } - } - if isDefault || settings.hasEntriesPerLocation { - guard settings.entriesPerLocation > 0 else { - throw .malformedInput(msg: "\(kind) 'entriesPerLocation' needs to be > 0") - } - } - let locations = settings.hasLocations ? settings.locations : fallbackSettings.locations - if settings.hasLocationTravelDurations { - guard settings.locationTravelDurations.locations.count == locations else { - throw .malformedInput(msg: "\(kind) 'locationTravelDurations.locations' size != \(locations)") - } - } - if settings.hasLocationTimeExclusivities { - guard settings.locationTimeExclusivities.locations.count == locations else { - throw .malformedInput(msg: "\(kind) 'locationTimeExclusivities.locations' size != \(locations)") - } - } - if settings.hasRedistributionSettings { - if settings.redistributionSettings.hasMinMatchupsRequired { - guard settings.redistributionSettings.minMatchupsRequired > 0 else { - throw .malformedInput(msg: "\(kind) redistribution setting 'minMatchupsRequired' needs to be > 0") - } - } - if settings.redistributionSettings.hasMaxMovableMatchups { - guard settings.redistributionSettings.maxMovableMatchups > 0 else { - throw .malformedInput(msg: "\(kind) redistribution setting 'maxMovableMatchups' needs to be > 0") - } - } - } - if isDefault || settings.hasGameGap { - guard let gameGap = GameGap.init(htmlInputValue: settings.gameGap) else { - throw .malformedInput(msg: "\(kind) invalid 'gameGap' value: \(settings.gameGap)") - } - return gameGap - } - return nil - } -} - -// MARK: Calculate maximum same opponent matchups cap -extension RequestPayload { - func calculateMaximumSameOpponentMatchupsCap( - entriesCount: Int - ) throws(LeagueError) -> MaximumSameOpponentMatchupsCap { - return try Self.calculateMaximumSameOpponentMatchupsCap( - gameDays: gameDays, - entryMatchupsPerGameDay: settings.entryMatchupsPerGameDay, - entriesCount: entriesCount - ) - } - - static func calculateMaximumSameOpponentMatchupsCap( - gameDays: DayIndex, - entryMatchupsPerGameDay: EntryMatchupsPerGameDay, - entriesCount: Int - ) throws(LeagueError) -> MaximumSameOpponentMatchupsCap { - guard entriesCount > 1 else { - throw .malformedInput(msg: "Number of teams need to be > 1 when calculating maximum same opponent matchups cap; got \(entriesCount)") - } - return MaximumSameOpponentMatchupsCap( - ceil( - Double(gameDays) / (Double(entriesCount-1) / Double(entryMatchupsPerGameDay)) - ) - ) - } -} - // MARK: Calculate max playable matchups extension RequestPayload { static func calculateMaximumPlayableMatchups( diff --git a/Sources/league-scheduling/generated/extensions/RequestPayload+Validate.swift b/Sources/league-scheduling/generated/extensions/RequestPayload+Validate.swift new file mode 100644 index 0000000..6bfb596 --- /dev/null +++ b/Sources/league-scheduling/generated/extensions/RequestPayload+Validate.swift @@ -0,0 +1,76 @@ + +extension RequestPayload { + @discardableResult + func validateSettings( + kind: String, + settings: GeneralSettings, + fallbackSettings: GeneralSettings + ) throws(LeagueError) -> GameGap? { + let isDefault = kind == "default" + if isDefault || settings.hasTimeSlots { + guard settings.timeSlots > 0 else { + throw .malformedInput(msg: "\(kind) 'timeSlots' size needs to be > 0") + } + } + if settings.hasStartingTimes { + guard settings.startingTimes.times.count > 0 else { + throw .malformedInput(msg: "\(kind) 'startingTimes' size needs to be > 0") + } + } + if settings.hasTimeSlots && settings.hasStartingTimes { + guard settings.timeSlots == settings.startingTimes.times.count else { + throw .malformedInput(msg: "\(kind) 'timeSlots' and 'startingTimes' size need to be equal") + } + } + if isDefault || settings.hasLocations { + guard settings.locations > 0 else { + throw .malformedInput(msg: "\(kind) 'locations' needs to be > 0") + } + } + if isDefault || settings.hasEntryMatchupsPerGameDay { + guard settings.entryMatchupsPerGameDay > 0 else { + throw .malformedInput(msg: "\(kind) 'entryMatchupsPerGameDay' needs to be > 0") + } + } + if settings.hasMaximumPlayableMatchups { + guard settings.maximumPlayableMatchups.array.count == entries.count else { + throw .malformedInput(msg: "\(kind) 'maximumPlayableMatchups' size != \(entries.count)") + } + } + if isDefault || settings.hasEntriesPerLocation { + guard settings.entriesPerLocation > 0 else { + throw .malformedInput(msg: "\(kind) 'entriesPerLocation' needs to be > 0") + } + } + let locations = settings.hasLocations ? settings.locations : fallbackSettings.locations + if settings.hasLocationTravelDurations { + guard settings.locationTravelDurations.locations.count == locations else { + throw .malformedInput(msg: "\(kind) 'locationTravelDurations.locations' size != \(locations)") + } + } + if settings.hasLocationTimeExclusivities { + guard settings.locationTimeExclusivities.locations.count == locations else { + throw .malformedInput(msg: "\(kind) 'locationTimeExclusivities.locations' size != \(locations)") + } + } + if settings.hasRedistributionSettings { + if settings.redistributionSettings.hasMinMatchupsRequired { + guard settings.redistributionSettings.minMatchupsRequired > 0 else { + throw .malformedInput(msg: "\(kind) redistribution setting 'minMatchupsRequired' needs to be > 0") + } + } + if settings.redistributionSettings.hasMaxMovableMatchups { + guard settings.redistributionSettings.maxMovableMatchups > 0 else { + throw .malformedInput(msg: "\(kind) redistribution setting 'maxMovableMatchups' needs to be > 0") + } + } + } + if isDefault || settings.hasGameGap { + guard let gameGap = GameGap.init(htmlInputValue: settings.gameGap) else { + throw .malformedInput(msg: "\(kind) invalid 'gameGap' value: \(settings.gameGap)") + } + return gameGap + } + return nil + } +} \ No newline at end of file diff --git a/Sources/league-scheduling/generated/runtime/DaySettings+Runtime.swift b/Sources/league-scheduling/generated/runtime/DaySettings+Runtime.swift deleted file mode 100644 index 50bbc01..0000000 --- a/Sources/league-scheduling/generated/runtime/DaySettings+Runtime.swift +++ /dev/null @@ -1,21 +0,0 @@ - -extension DaySettings { - func runtime() throws(LeagueError) -> Runtime { - try .init(protobuf: self) - } - - /// For optimal runtime performance. - struct Runtime: Sendable { - let general:GeneralSettings.Runtime - - init(protobuf: DaySettings) throws(LeagueError) { - general = try protobuf.settings.runtime() - } - - init( - general: GeneralSettings.Runtime - ) { - self.general = general - } - } -} \ No newline at end of file diff --git a/Sources/league-scheduling/generated/runtime/Division+Runtime.swift b/Sources/league-scheduling/generated/runtime/Division+Runtime.swift index 587e932..49d2ee3 100644 --- a/Sources/league-scheduling/generated/runtime/Division+Runtime.swift +++ b/Sources/league-scheduling/generated/runtime/Division+Runtime.swift @@ -1,11 +1,11 @@ extension Division { - func runtime( - defaultGameDays: Set, + func runtime( + defaultGameDays: DaySet, defaultGameGap: GameGap, fallbackDayOfWeek: DayOfWeek, fallbackMaxSameOpponentMatchups: MaximumSameOpponentMatchupsCap - ) throws(LeagueError) -> Runtime { + ) throws(LeagueError) -> Runtime { try .init( protobuf: self, defaultGameDays: defaultGameDays, @@ -16,28 +16,28 @@ extension Division { } /// For optimal runtime performance. - struct Runtime: Sendable { + struct Runtime: Sendable { let dayOfWeek:DayOfWeek - let gameDays:Set + let gameDays:DaySet let gameGaps:[GameGap] let maxSameOpponentMatchups:MaximumSameOpponentMatchupsCap init( protobuf: Division, - defaultGameDays: Set, + defaultGameDays: DaySet, defaultGameGap: GameGap, fallbackDayOfWeek: DayOfWeek, fallbackMaxSameOpponentMatchups: MaximumSameOpponentMatchupsCap ) throws(LeagueError) { dayOfWeek = protobuf.hasDayOfWeek ? DayOfWeek(protobuf.dayOfWeek) : fallbackDayOfWeek - self.gameDays = protobuf.hasGameDays ? Set(protobuf.gameDays.gameDayIndexes) : defaultGameDays + self.gameDays = protobuf.hasGameDays ? .init(protobuf.gameDays.gameDayIndexes) : defaultGameDays gameGaps = protobuf.hasGameGaps ? try Self.parseGameGaps(protobuf.gameGaps.gameGaps) : .init(repeating: defaultGameGap, count: defaultGameDays.count) maxSameOpponentMatchups = protobuf.hasMaxSameOpponentMatchups ? protobuf.maxSameOpponentMatchups : fallbackMaxSameOpponentMatchups } init( dayOfWeek: DayOfWeek, - gameDays: Set, + gameDays: DaySet, gameGaps: [GameGap], maxSameOpponentMatchups: MaximumSameOpponentMatchupsCap ) { diff --git a/Sources/league-scheduling/generated/runtime/Entry+Runtime.swift b/Sources/league-scheduling/generated/runtime/Entry+Runtime.swift index eef021a..3e80de8 100644 --- a/Sources/league-scheduling/generated/runtime/Entry+Runtime.swift +++ b/Sources/league-scheduling/generated/runtime/Entry+Runtime.swift @@ -1,13 +1,13 @@ extension Entry { - func runtime( + func runtime( id: IDValue, division: Division.IDValue, - defaultGameDays: Set, - defaultByes: Set, - defaultGameTimes: [Set], - defaultGameLocations: [Set] - ) -> Runtime { + defaultGameDays: DaySet, + defaultByes: DaySet, + defaultGameTimes: [Times], + defaultGameLocations: [Locations] + ) -> Runtime { return .init( id: id, division: division, @@ -19,8 +19,7 @@ extension Entry { ) } - /// For optimal runtime performance. - struct Runtime: Sendable { + struct Runtime: Sendable { /// ID associated with this entry. let id:Entry.IDValue @@ -28,23 +27,23 @@ extension Entry { let division:Division.IDValue /// Game days this entry can play on. - let gameDays:Set + let gameDays:DaySet /// Times this entry can play at for a specific day index. /// /// - Usage: [`DayIndex`: `Set`] - let gameTimes:[Set] + let gameTimes:[TimeSet] /// Locations this entry can play at for a specific day index. /// /// - Usage: [`DayIndex`: `Set`] - let gameLocations:[Set] + let gameLocations:[LocationSet] /// Home locations for this entry. - let homeLocations:Set + let homeLocations:LocationSet /// Day indexes where this entry doesn't play due to being on a bye week. - let byes:Set + let byes:DaySet let matchupsPerGameDay:LitLeagues_Leagues_EntryMatchupsPerGameDay? @@ -52,29 +51,29 @@ extension Entry { id: Entry.IDValue, division: Division.IDValue, protobuf: Entry, - defaultGameDays: Set, - defaultByes: Set, - defaultGameTimes: [Set], - defaultGameLocations: [Set] + defaultGameDays: DaySet, + defaultByes: DaySet, + defaultGameTimes: [TimeSet], + defaultGameLocations: [LocationSet] ) { self.id = id self.division = division - gameDays = protobuf.hasGameDays ? Set(protobuf.gameDays.gameDayIndexes) : defaultGameDays - gameTimes = protobuf.hasGameDayTimes ? protobuf.gameDayTimes.times.map({ Set($0.times) }) : defaultGameTimes - gameLocations = protobuf.hasGameDayLocations ? protobuf.gameDayLocations.locations.map({ Set($0.locations) }) : defaultGameLocations - homeLocations = protobuf.hasHomeLocations ? Set(protobuf.homeLocations.homeLocations) : [] - byes = protobuf.hasByes ? Set(protobuf.byes.byes) : defaultByes + gameDays = protobuf.hasGameDays ? .init(protobuf.gameDays.gameDayIndexes) : defaultGameDays + gameTimes = protobuf.hasGameDayTimes ? protobuf.gameDayTimes.times.map({ .init($0.times) }) : defaultGameTimes + gameLocations = protobuf.hasGameDayLocations ? protobuf.gameDayLocations.locations.map({ .init($0.locations) }) : defaultGameLocations + homeLocations = protobuf.hasHomeLocations ? .init(protobuf.homeLocations.homeLocations) : .init() + byes = protobuf.hasByes ? .init(protobuf.byes.byes) : defaultByes matchupsPerGameDay = protobuf.hasMatchupsPerGameDay ? protobuf.matchupsPerGameDay : nil } init( id: Entry.IDValue, division: Division.IDValue, - gameDays: Set, - gameTimes: [Set], - gameLocations: [Set], - homeLocations: Set, - byes: Set, + gameDays: DaySet, + gameTimes: [TimeSet], + gameLocations: [LocationSet], + homeLocations: LocationSet, + byes: DaySet, matchupsPerGameDay: LitLeagues_Leagues_EntryMatchupsPerGameDay? ) { self.id = id diff --git a/Sources/league-scheduling/generated/runtime/GeneralSettings+Runtime.swift b/Sources/league-scheduling/generated/runtime/GeneralSettings+Runtime.swift index 59d574b..ec6616c 100644 --- a/Sources/league-scheduling/generated/runtime/GeneralSettings+Runtime.swift +++ b/Sources/league-scheduling/generated/runtime/GeneralSettings+Runtime.swift @@ -1,13 +1,12 @@ import StaticDateTimes -extension GeneralSettings { - func runtime() throws(LeagueError) -> Runtime { - try .init(protobuf: self) - } +#if SpecializeScheduleConfiguration +import OrderedCollections +#endif - /// For optimal runtime performance - struct Runtime: Sendable { +extension GeneralSettings { + struct Runtime: Sendable { var gameGap:GameGap var timeSlots:TimeIndex var startingTimes:[StaticTime] @@ -16,81 +15,58 @@ extension GeneralSettings { var defaultMaxEntryMatchupsPerGameDay:EntryMatchupsPerGameDay var maximumPlayableMatchups:[UInt32] var matchupDuration:MatchupDuration - var locationTimeExclusivities:[Set]? + var locationTimeExclusivities:[Config.TimeSet]? var locationTravelDurations:[[MatchupDuration]]? var balanceTimeStrictness:BalanceStrictness - var balancedTimes:Set + var balancedTimes:Config.TimeSet var balanceLocationStrictness:BalanceStrictness - var balancedLocations:Set + var balancedLocations:Config.LocationSet var redistributionSettings:LitLeagues_Leagues_RedistributionSettings? var flags:UInt32 - - init( - protobuf: GeneralSettings - ) throws(LeagueError) { - guard let gameGap = GameGap(htmlInputValue: protobuf.gameGap) else { - throw .malformedInput(msg: "invalid GameGap htmlInputValue: \(protobuf.gameGap)") - } - self.gameGap = gameGap - timeSlots = protobuf.timeSlots - startingTimes = protobuf.startingTimes.times - entriesPerLocation = protobuf.entriesPerLocation - locations = protobuf.locations - defaultMaxEntryMatchupsPerGameDay = protobuf.entryMatchupsPerGameDay - maximumPlayableMatchups = protobuf.maximumPlayableMatchups.array - matchupDuration = protobuf.matchupDuration - if protobuf.hasLocationTimeExclusivities { - locationTimeExclusivities = protobuf.locationTimeExclusivities.locations.map({ Set($0.times) }) - } else { - locationTimeExclusivities = nil - } - if protobuf.hasLocationTravelDurations { - locationTravelDurations = protobuf.locationTravelDurations.locations.map({ $0.travelDurationTo }) - } else { - locationTravelDurations = nil - } - balanceTimeStrictness = protobuf.balanceTimeStrictness - balancedTimes = Set(protobuf.balancedTimes.array) - balanceLocationStrictness = protobuf.balanceLocationStrictness - balancedLocations = Set(protobuf.balancedLocations.array) - if protobuf.hasRedistributionSettings { - redistributionSettings = protobuf.redistributionSettings - } else { - redistributionSettings = nil - } - flags = protobuf.flags - } } } -// MARK: Flags +// MARK: Init extension GeneralSettings.Runtime { - func isFlag(_ flag: SettingFlags) -> Bool { - flags & UInt32(1 << flag.rawValue) != 0 - } - - var optimizeTimes: Bool { - isFlag(.optimizeTimes) - } - - var prioritizeEarlierTimes: Bool { - isFlag(.prioritizeEarlierTimes) - } - - var prioritizeHomeAway: Bool { - isFlag(.prioritizeHomeAway) - } - - var balanceHomeAway: Bool { - isFlag(.balanceHomeAway) - } - - var sameLocationIfB2B: Bool { - isFlag(.sameLocationIfBackToBack) + init( + gameGap: GameGap, + protobuf: GeneralSettings + ) { + self.gameGap = gameGap + timeSlots = protobuf.timeSlots + startingTimes = protobuf.startingTimes.times + entriesPerLocation = protobuf.entriesPerLocation + locations = protobuf.locations + defaultMaxEntryMatchupsPerGameDay = protobuf.entryMatchupsPerGameDay + maximumPlayableMatchups = protobuf.maximumPlayableMatchups.array + matchupDuration = protobuf.matchupDuration + if protobuf.hasLocationTimeExclusivities { + locationTimeExclusivities = protobuf.locationTimeExclusivities.locations.map({ .init($0.times) }) + } else { + locationTimeExclusivities = nil + } + if protobuf.hasLocationTravelDurations { + locationTravelDurations = protobuf.locationTravelDurations.locations.map({ $0.travelDurationTo }) + } else { + locationTravelDurations = nil + } + balanceTimeStrictness = protobuf.balanceTimeStrictness + balancedTimes = .init(protobuf.balancedTimes.array) + balanceLocationStrictness = protobuf.balanceLocationStrictness + balancedLocations = .init(protobuf.balancedLocations.array) + if protobuf.hasRedistributionSettings { + redistributionSettings = protobuf.redistributionSettings + } else { + redistributionSettings = nil + } + flags = protobuf.flags } -} -extension GeneralSettings.Runtime { + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfigBitSet64) + @_specialize(where Config == ScheduleConfigBitSet128) + @_specialize(where Config == ScheduleConfigSet) + #endif init( gameGap: GameGap, timeSlots: TimeIndex, @@ -100,12 +76,12 @@ extension GeneralSettings.Runtime { entryMatchupsPerGameDay: EntryMatchupsPerGameDay, maximumPlayableMatchups: [UInt32], matchupDuration: MatchupDuration, - locationTimeExclusivities: [Set]?, + locationTimeExclusivities: [Config.TimeSet]?, locationTravelDurations: [[MatchupDuration]]?, balanceTimeStrictness: BalanceStrictness, - balancedTimes: Set, + balancedTimes: Config.TimeSet, balanceLocationStrictness: BalanceStrictness, - balancedLocations: Set, + balancedLocations: Config.LocationSet, redistributionSettings: LitLeagues_Leagues_RedistributionSettings?, flags: UInt32 ) { @@ -128,12 +104,144 @@ extension GeneralSettings.Runtime { } } +// MARK: Available slots +extension GeneralSettings.Runtime { + func availableSlots() -> Config.AvailableSlotSet { + var slots = Config.AvailableSlotSet(minimumCapacity: Int(timeSlots) * locations) + if let exclusivities = locationTimeExclusivities { + for location in 0.. Bool { + flags & UInt32(1 << flag.rawValue) != 0 + } + + var optimizeTimes: Bool { + isFlag(.optimizeTimes) + } + + var prioritizeEarlierTimes: Bool { + isFlag(.prioritizeEarlierTimes) + } + + var prioritizeHomeAway: Bool { + isFlag(.prioritizeHomeAway) + } + + var balanceHomeAway: Bool { + isFlag(.balanceHomeAway) + } + + var sameLocationIfB2B: Bool { + isFlag(.sameLocationIfBackToBack) + } +} + // MARK: Compute settings extension GeneralSettings.Runtime { + init( + protobuf: GeneralSettings + ) throws(LeagueError) { + guard let gameGap = GameGap(htmlInputValue: protobuf.gameGap) else { + throw .malformedInput(msg: "invalid GameGap htmlInputValue: \(protobuf.gameGap)") + } + self.init(gameGap: gameGap, protobuf: protobuf) + } + /// Modifies `timeSlots` and `startingTimes` taking into account current settings. mutating func computeSettings( day: DayIndex, - entries: [Entry.Runtime] + entries: [Config.EntryRuntime] ) { if optimizeTimes { var maxMatchupsPlayedToday:LocationIndex = 0 @@ -143,7 +251,7 @@ extension GeneralSettings.Runtime { } } maxMatchupsPlayedToday /= entriesPerLocation - let filledTimeSlots = RequestPayload.Runtime.optimalTimeSlots( + let filledTimeSlots = optimalTimeSlots( availableTimeSlots: timeSlots, locations: locations, matchupsCount: maxMatchupsPlayedToday diff --git a/Sources/league-scheduling/generated/runtime/RequestPayload+Runtime.swift b/Sources/league-scheduling/generated/runtime/RequestPayload+Runtime.swift index bbbaa35..f1fe940 100644 --- a/Sources/league-scheduling/generated/runtime/RequestPayload+Runtime.swift +++ b/Sources/league-scheduling/generated/runtime/RequestPayload+Runtime.swift @@ -1,24 +1,94 @@ +#if SpecializeScheduleConfiguration +import OrderedCollections +#endif + extension RequestPayload { /// For optimal runtime performance. - struct Runtime: Sendable { + struct Runtime: Sendable, ~Copyable { + var rng:Config.RNG + let constraints:GenerationConstraints /// Number of days where games are played. let gameDays:DayIndex /// Divisions associated with this schedule. - let divisions:[Division.Runtime] + let divisions:[Config.DivisionRuntime] /// Entries that participate in this schedule. - let entries:[Entry.Runtime] + let entries:[Config.EntryRuntime] /// General settings for this schedule. - let general:GeneralSettings.Runtime + let general:GeneralSettings.Runtime /// Individual settings for the given day index. /// /// - Usage: [`DayIndex`: `DaySettings`] - let daySettings:[DaySettings.Runtime] + let daySettings:[GeneralSettings.Runtime] + + #if SpecializeScheduleConfiguration + @_specialize(where Config == ScheduleConfig< + SystemRandomNumberGenerator, + BitSet64, + BitSet64, + BitSet64, + BitSet64, + Set, + Set, + Set, + Set, + Set + >) + @_specialize(where Config == ScheduleConfig< + SystemRandomNumberGenerator, + BitSet128, + BitSet128, + BitSet128, + BitSet128, + Set, + Set, + Set, + Set, + Set + >) + @_specialize(where Config == ScheduleConfig< + SystemRandomNumberGenerator, + Set, + Set, + Set, + Set, + Set, + Set, + Set, + Set, + Set + >) + #endif + init( + rng: Config.RNG, + constraints: GenerationConstraints, + gameDays: DayIndex, + divisions: [Config.DivisionRuntime], + entries: [Config.EntryRuntime], + general: GeneralSettings.Runtime, + daySettings: [GeneralSettings.Runtime] + ) { + self.rng = rng + self.constraints = constraints + self.gameDays = gameDays + self.divisions = divisions + self.entries = entries + self.general = general + self.daySettings = daySettings + } + + func copy() -> Self { + .init(rng: rng, constraints: constraints, gameDays: gameDays, divisions: divisions, entries: entries, general: general, daySettings: daySettings) + } + + func redistributionSettings(for day: DayIndex) -> LitLeagues_Leagues_RedistributionSettings? { + daySettings[unchecked: day].redistributionSettings ?? general.redistributionSettings + } } } \ No newline at end of file diff --git a/Sources/league-scheduling/globals.swift b/Sources/league-scheduling/globals.swift index 043cc54..0758525 100644 --- a/Sources/league-scheduling/globals.swift +++ b/Sources/league-scheduling/globals.swift @@ -1,4 +1,28 @@ +#if canImport(SwiftGlibc) +import SwiftGlibc +#elseif canImport(Foundation) +import Foundation +#endif + +// MARK: optimal time slots +func optimalTimeSlots( + availableTimeSlots: TimeIndex, + locations: LocationIndex, + matchupsCount: LocationIndex +) -> TimeIndex { + var totalMatchupsPlayed:LocationIndex = 0 + var filledTimes:TimeIndex = 0 + while totalMatchupsPlayed < matchupsCount { + filledTimes += 1 + totalMatchupsPlayed += locations + } + #if LOG + print("LeagueSchedule;optimalTimeSlots;availableTimeSlots=\(availableTimeSlots);locations=\(locations);matchupsCount=\(matchupsCount);totalMatchupsPlayed=\(totalMatchupsPlayed);filledTimes=\(filledTimes)") + #endif + return min(availableTimeSlots, filledTimes) +} + // MARK: adjacent times func calculateAdjacentTimes( for time: TimeIndex, @@ -21,4 +45,38 @@ func calculateAdjacentTimes( } } return adjacentTimes +} + +// MARK: balance numbers +func calculateBalanceNumber( + totalMatchupsPlayed: some FixedWidthInteger, + value: some FixedWidthInteger, + strictness: BalanceStrictness +) -> T { + guard strictness != .lenient else { return .max } + var minimumValue = T(ceil(Double(totalMatchupsPlayed) / Double(value))) + switch strictness { + case .lenient: minimumValue = .max + case .normal: minimumValue += 1 + case .relaxed: minimumValue += 2 + case .very: break + case .UNRECOGNIZED: break + } + return minimumValue +} + +// MARK: maximum same opponent matchups cap +func calculateMaximumSameOpponentMatchupsCap( + gameDays: DayIndex, + entryMatchupsPerGameDay: EntryMatchupsPerGameDay, + entriesCount: Int +) throws(LeagueError) -> MaximumSameOpponentMatchupsCap { + guard entriesCount > 1 else { + throw .malformedInput(msg: "Number of teams need to be > 1 when calculating maximum same opponent matchups cap; got \(entriesCount)") + } + return MaximumSameOpponentMatchupsCap( + ceil( + Double(gameDays) / (Double(entriesCount-1) / Double(entryMatchupsPerGameDay)) + ) + ) } \ No newline at end of file diff --git a/Sources/league-scheduling/typealiases.swift b/Sources/league-scheduling/typealiases.swift index 91bcf0b..97e674f 100644 --- a/Sources/league-scheduling/typealiases.swift +++ b/Sources/league-scheduling/typealiases.swift @@ -63,9 +63,4 @@ typealias MaximumTimeAllocations = ContiguousArray> /// Maximum number of allocations allowed for a given entry for a given location. /// /// - Usage: [`Entry.IDValue`: [`LocationIndex`: `maximum allowed at LocationIndex`]] -typealias MaximumLocationAllocations = ContiguousArray> - -/// Locations where an entry has already played at for the `day`. -/// -/// - Usage: [`Entry.IDValue`: `Set`] -typealias PlaysAtLocations = ContiguousArray> \ No newline at end of file +typealias MaximumLocationAllocations = ContiguousArray> \ No newline at end of file diff --git a/Sources/league-scheduling/util/EntryAssignmentData.swift b/Sources/league-scheduling/util/EntryAssignmentData.swift index ac26be9..4eb6926 100644 --- a/Sources/league-scheduling/util/EntryAssignmentData.swift +++ b/Sources/league-scheduling/util/EntryAssignmentData.swift @@ -33,8 +33,8 @@ struct EntryAssignmentData: Sendable { var maxSameOpponentMatchups:ContiguousArray var playsAt:Set - var playsAtTimes:Set - var playsAtLocations:Set + var playsAtTimes:BitSet64 + var playsAtLocations:BitSet64 var maxTimeAllocations:[TimeIndex] var maxLocationAllocations:[LocationIndex] @@ -75,8 +75,8 @@ extension EntryAssignmentData { assignedTimes[unchecked: slot.time] += 1 assignedLocations[unchecked: slot.location] += 1 playsAt.insert(slot) - playsAtTimes.insert(slot.time) - playsAtLocations.insert(slot.location) + playsAtTimes.insertMember(slot.time) + playsAtLocations.insertMember(slot.location) } } @@ -84,7 +84,7 @@ extension EntryAssignmentData { extension EntryAssignmentData { mutating func resetPlaysAt() { playsAt.removeAll(keepingCapacity: true) - playsAtTimes.removeAll(keepingCapacity: true) - playsAtLocations.removeAll(keepingCapacity: true) + playsAtTimes.removeAll() + playsAtLocations.removeAll() } } \ No newline at end of file diff --git a/Sources/league-scheduling/util/LeagueError.swift b/Sources/league-scheduling/util/LeagueError.swift index 0251d86..5cd29eb 100644 --- a/Sources/league-scheduling/util/LeagueError.swift +++ b/Sources/league-scheduling/util/LeagueError.swift @@ -42,4 +42,4 @@ public enum LeagueError: CustomStringConvertible, Error, Sendable { return "Failed to build schedule within provided time limit; try regenerating (timed out; function=" + function + ")" } } -} +} \ No newline at end of file diff --git a/Sources/league-scheduling/util/array/PlaysAtTimesArray.swift b/Sources/league-scheduling/util/array/PlaysAtTimesArray.swift deleted file mode 100644 index 6f221d2..0000000 --- a/Sources/league-scheduling/util/array/PlaysAtTimesArray.swift +++ /dev/null @@ -1,22 +0,0 @@ - -struct PlaysAtTimesArray: Sendable { - internal private(set) var times:ContiguousArray - - subscript(unchecked index: some FixedWidthInteger) -> TimeSet { - times[unchecked: index] - } - - mutating func removeAllKeepingCapacity() { - for i in 0..: Sendable { + private(set) var storage:UInt128 + + init() { + storage = 0 + } + + init(minimumCapacity: Int) { + storage = 0 + } + + init(storage: UInt128) { + self.storage = storage + } + + init(_ collection: some Collection) { + storage = 0 + for e in collection { + insertMember(e) + } + } + + var count: Int { + storage.nonzeroBitCount + } + + var isEmpty: Bool { + storage == 0 + } + + func reserveCapacity(_ minimumCapacity: Int) { + } +} + +// MARK: contains +extension BitSet128 { + func contains(_ member: Element) -> Bool { + (storage & (1 << member)) != 0 + } +} + +// MARK: insert +extension BitSet128 { + mutating func insertMember(_ member: Element) { + storage |= (1 << member) + } +} + +// MARK: remove +extension BitSet128 { + mutating func removeMember(_ member: Element) { + storage &= ~(1 << member) + } +} + +// MARK: remove all +extension BitSet128 { + mutating func removeAll() { + storage = 0 + } + mutating func removeAllKeepingCapacity() { + storage = 0 + } + mutating func removeAll(where condition: (Element) throws -> Bool) rethrows { + try forEach { + if try condition($0) { + removeMember($0) + } + } + } +} + +// MARK: iterator +extension BitSet128 { + func forEach(_ body: (Element) throws -> Void) rethrows { + var temp = storage + while temp != 0 { + let index = temp.trailingZeroBitCount + try body(Element(index)) + temp &= (temp - 1) + } + } + func forEachWithReturn(_ body: (Element) throws -> Result?) rethrows -> Result? { + var temp = storage + while temp != 0 { + let index = temp.trailingZeroBitCount + if let r = try body(Element(index)) { + return r + } + temp &= (temp - 1) + } + return nil + } +} + +// MARK: form union +extension BitSet128 { + mutating func formUnion(_ bitSet: Self) { + storage |= bitSet.storage + } +} + +// MARK: Random +extension BitSet128 { + func randomElement() -> Element? { + guard storage != 0 else { return nil } + let skip = Int.random(in: 0.. Element? { + guard storage != 0 else { return nil } + let skip = Int.random(in: 0.. Bool) rethrows -> Self { + var temp = UInt128(0) + try forEach { i in + if try closure(i) { + temp |= 1 << i + } + } + return .init(storage: temp) + } + + var first: Element? { + let e = storage.trailingZeroBitCount + guard e > 0 else { return nil } + return 1 << e + } + + func first(where condition: (Element) throws -> Bool) rethrows -> Element? { + return try forEachWithReturn { i in + if try condition(i) { + return 1 << i + } + return nil + } + } + + func map(_ body: (Element) throws -> Result) rethrows -> [Result] { + var array = [Result]() + array.reserveCapacity(count) + try forEach { + try array.append(body($0)) + } + return array + } + + func intersection(_ other: borrowing Self) -> Self { + return .init(storage: storage & other.storage) + } +} +extension BitSet128: SetOfUInt32 where Element == UInt32 {} + +extension BitSet128: SetOfEntryIDs where Element == Entry.IDValue { + func availableMatchupPairs( + assignedEntryHomeAways: AssignedEntryHomeAways, + maxSameOpponentMatchups: MaximumSameOpponentMatchups + ) -> MatchupPairSet where MatchupPairSet.Element == MatchupPair { + var pairs = MatchupPairSet(minimumCapacity: (count-1) * 2) + forEach { home in + let assignedHome = assignedEntryHomeAways[unchecked: home] + let maxSameOpponentMatchups = maxSameOpponentMatchups[unchecked: home] + forEach { away in + if away > home, 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/BitSet64.swift b/Sources/league-scheduling/util/set/BitSet64.swift new file mode 100644 index 0000000..307d492 --- /dev/null +++ b/Sources/league-scheduling/util/set/BitSet64.swift @@ -0,0 +1,192 @@ + +/// - Warning: Only supports a maximum of 64 entries! +/// - Warning: Only supports integers < `64`! +struct BitSet64: Sendable { + private(set) var storage:UInt64 + + init() { + storage = 0 + } + init(minimumCapacity: Int) { + storage = 0 + } + + init(storage: UInt64) { + self.storage = storage + } + + init(_ collection: some Collection) { + storage = 0 + for e in collection { + insertMember(e) + } + } + init(_ set: T) where T.Element == Element { + storage = 0 + set.forEach { + insertMember($0) + } + } + + var count: Int { + storage.nonzeroBitCount + } + + var isEmpty: Bool { + storage == 0 + } + + func reserveCapacity(_ minimumCapacity: Int) { + } +} + +// MARK: contains +extension BitSet64 { + func contains(_ member: Element) -> Bool { + (storage & (1 << member)) != 0 + } +} + +// MARK: insert +extension BitSet64 { + mutating func insertMember(_ member: Element) { + storage |= (1 << member) + } +} + +// MARK: remove +extension BitSet64 { + mutating func removeMember(_ member: Element) { + storage &= ~(1 << member) + } +} + +// MARK: remove all +extension BitSet64 { + mutating func removeAll() { + storage = 0 + } + mutating func removeAllKeepingCapacity() { + storage = 0 + } + mutating func removeAll(where condition: (Element) throws -> Bool) rethrows { + try forEach { + if try condition($0) { + removeMember($0) + } + } + } +} + +// MARK: iterator +extension BitSet64 { + func forEach(_ body: (Element) throws -> Void) rethrows { + var temp = storage + while temp != 0 { + let index = temp.trailingZeroBitCount + try body(Element(index)) + temp &= (temp - 1) + } + } + func forEachWithReturn(_ body: (Element) throws -> Result?) rethrows -> Result? { + var temp = storage + while temp != 0 { + let index = temp.trailingZeroBitCount + if let r = try body(Element(index)) { + return r + } + temp &= (temp - 1) + } + return nil + } +} + +// MARK: form union +extension BitSet64 { + mutating func formUnion(_ bitSet: Self) { + storage |= bitSet.storage + } +} + +// MARK: Random +extension BitSet64 { + func randomElement() -> Element? { + guard storage != 0 else { return nil } + let skip = Int.random(in: 0.. Element? { + guard storage != 0 else { return nil } + let skip = Int.random(in: 0.. Bool) rethrows -> Self { + var temp = UInt64(0) + try forEach { i in + if try closure(i) { + temp |= 1 << i + } + } + return .init(storage: temp) + } + + var first: Element? { + let e = storage.trailingZeroBitCount + guard e > 0 else { return nil } + return 1 << e + } + + func first(where condition: (Element) throws -> Bool) rethrows -> Element? { + return try forEachWithReturn { i in + if try condition(i) { + return 1 << i + } + return nil + } + } + + func map(_ body: (Element) throws -> Result) rethrows -> [Result] { + var array = [Result]() + array.reserveCapacity(count) + try forEach { + try array.append(body($0)) + } + return array + } + + func intersection(_ other: borrowing Self) -> Self { + return .init(storage: storage & other.storage) + } +} +extension BitSet64: SetOfUInt32 where Element == UInt32 {} + +extension BitSet64: SetOfEntryIDs where Element == Entry.IDValue { + func availableMatchupPairs( + assignedEntryHomeAways: AssignedEntryHomeAways, + maxSameOpponentMatchups: MaximumSameOpponentMatchups + ) -> MatchupPairSet where MatchupPairSet.Element == MatchupPair { + var pairs = MatchupPairSet(minimumCapacity: (count-1) * 2) + forEach { home in + let assignedHome = assignedEntryHomeAways[unchecked: home] + let maxSameOpponentMatchups = maxSameOpponentMatchups[unchecked: home] + forEach { away in + if away > home, 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/Tests/LeagueSchedulingTests/BalanceHomeAwayThroughput.swift b/Tests/LeagueSchedulingTests/BalanceHomeAwayThroughput.swift index 09363b9..98d5aba 100644 --- a/Tests/LeagueSchedulingTests/BalanceHomeAwayThroughput.swift +++ b/Tests/LeagueSchedulingTests/BalanceHomeAwayThroughput.swift @@ -2,7 +2,7 @@ @testable import LeagueScheduling import Testing -struct BalancedHomeAwayThroughput { +struct BalancedHomeAwayThroughput: ScheduleTestsProtocol { //@Test(.timeLimit(.minutes(1))) func balanceHomeAwayThroughput() async throws { try await withThrowingTaskGroup(of: Output.self) { group in @@ -26,7 +26,7 @@ struct BalancedHomeAwayThroughput { let schedule = try ScheduleBack2Back.scheduleB2B_11GameDays4Times6Locations2Divisions24Teams14_10() let entries = schedule.entries let entriesCount = entries.count - let expectation = BalanceHomeAwayExpectations() + let expectation = BalanceHomeAwayExpectations() while !Task.isCancelled { let result = await schedule.generate() output.throughput += 1 diff --git a/Tests/LeagueSchedulingTests/BalanceNumberCalculation.swift b/Tests/LeagueSchedulingTests/BalanceNumberCalculation.swift index 5a695f7..98dd8e5 100644 --- a/Tests/LeagueSchedulingTests/BalanceNumberCalculation.swift +++ b/Tests/LeagueSchedulingTests/BalanceNumberCalculation.swift @@ -17,7 +17,7 @@ struct BalanceNumberCalculation { } var minimumRequired = 4 mutateMinimum(&minimumRequired) - var balanceNumber:Int = RequestPayload.Runtime.balanceNumber( + var balanceNumber:Int = calculateBalanceNumber( totalMatchupsPlayed: 16, value: 4, strictness: strictness @@ -26,7 +26,7 @@ struct BalanceNumberCalculation { minimumRequired = 6 mutateMinimum(&minimumRequired) - balanceNumber = RequestPayload.Runtime.balanceNumber( + balanceNumber = calculateBalanceNumber( totalMatchupsPlayed: 16, value: 3, strictness: strictness @@ -35,7 +35,7 @@ struct BalanceNumberCalculation { minimumRequired = 8 mutateMinimum(&minimumRequired) - balanceNumber = RequestPayload.Runtime.balanceNumber( + balanceNumber = calculateBalanceNumber( totalMatchupsPlayed: 16, value: 2, strictness: strictness @@ -44,7 +44,7 @@ struct BalanceNumberCalculation { minimumRequired = 3 mutateMinimum(&minimumRequired) - balanceNumber = RequestPayload.Runtime.balanceNumber( + balanceNumber = calculateBalanceNumber( totalMatchupsPlayed: 5, value: 2, strictness: strictness @@ -53,7 +53,7 @@ struct BalanceNumberCalculation { minimumRequired = 5 mutateMinimum(&minimumRequired) - balanceNumber = RequestPayload.Runtime.balanceNumber( + balanceNumber = calculateBalanceNumber( totalMatchupsPlayed: 5, value: 1, strictness: strictness @@ -62,7 +62,7 @@ struct BalanceNumberCalculation { minimumRequired = 3 mutateMinimum(&minimumRequired) - balanceNumber = RequestPayload.Runtime.balanceNumber( + balanceNumber = calculateBalanceNumber( totalMatchupsPlayed: 9, value: 3, strictness: strictness @@ -70,7 +70,7 @@ struct BalanceNumberCalculation { minimumRequired = 2 mutateMinimum(&minimumRequired) - balanceNumber = RequestPayload.Runtime.balanceNumber( + balanceNumber = calculateBalanceNumber( totalMatchupsPlayed: 7, value: 4, strictness: strictness @@ -78,7 +78,7 @@ struct BalanceNumberCalculation { minimumRequired = 3 mutateMinimum(&minimumRequired) - balanceNumber = RequestPayload.Runtime.balanceNumber( + balanceNumber = calculateBalanceNumber( totalMatchupsPlayed: 7, value: 3, strictness: strictness diff --git a/Tests/LeagueSchedulingTests/BitSet128Tests.swift b/Tests/LeagueSchedulingTests/BitSet128Tests.swift new file mode 100644 index 0000000..f61e917 --- /dev/null +++ b/Tests/LeagueSchedulingTests/BitSet128Tests.swift @@ -0,0 +1,127 @@ + +@testable import LeagueScheduling +import Testing + +struct BitSet128Tests { + @Test + func bitSet128InsertMember() { + var s = BitSet128() + for i in 0...init(storage: .max) + for i in 0...init(storage: 0x1010101010101010_1010101010101010) + #expect(!s.contains(0)) + #expect(!s.contains(1)) + #expect(!s.contains(2)) + #expect(!s.contains(3)) + #expect(s.contains(4)) + #expect(!s.contains(5)) + #expect(!s.contains(6)) + #expect(!s.contains(7)) + #expect(!s.contains(8)) + + #expect(s.contains(12)) + #expect(s.contains(20)) + #expect(s.contains(28)) + #expect(s.contains(36)) + #expect(s.contains(44)) + #expect(s.contains(52)) + #expect(s.contains(60)) + #expect(s.contains(68)) + #expect(s.contains(76)) + #expect(s.contains(84)) + #expect(s.contains(92)) + #expect(s.contains(100)) + #expect(s.contains(108)) + #expect(s.contains(116)) + #expect(s.contains(124)) + } + + @Test + func bitSet128RandomElement() { + var s = BitSet128() + #expect(s.randomElement() == nil) + + s.insertMember(8) + #expect(s.randomElement() == 8) + + s.removeMember(8) + #expect(s.randomElement() == nil) + + s.insertMember(128) + #expect(s.randomElement() == nil) + } + + @Test + func bitSet128ForEach() { + var s = BitSet128() + s.forEach { _ in + #expect(Bool(false)) + } + + s.insertMember(0) + s.insertMember(32) + s.insertMember(127) + s.forEach { i in + #expect(i == 0 || i == 32 || i == 127) + } + #expect(s.contains(0)) + #expect(s.contains(32)) + #expect(s.contains(127)) + } + + @Test + func bitSet128Count() { + var s = BitSet128() + #expect(s.count == 0) + #expect(s.isEmpty) + + s.insertMember(0) + #expect(s.count == 1) + #expect(!s.isEmpty) + + s.insertMember(128) + #expect(s.count == 1) + #expect(!s.isEmpty) + + s.insertMember(127) + #expect(s.count == 2) + #expect(!s.isEmpty) + + s.removeMember(0) + #expect(s.count == 1) + #expect(!s.isEmpty) + } + + @Test + func bitSet128FormUnion() { + var s = BitSet128(storage: 0x1010101010101010) + s.formUnion(.init(storage: 0x0101010101010101)) + #expect(s.storage == 0x1111111111111111) + } + + @Test + func bitSet128RemoveAllWhere() { + var s = BitSet128(storage: .max) + s.removeAll(where: { $0 % 2 == 0 }) + #expect(s.storage == 0xAAAAAAAAAAAAAAAA_AAAAAAAAAAAAAAAA) + + s.removeAll(where: { $0 % 1 == 0 }) + #expect(s.storage == 0) + } +} \ No newline at end of file diff --git a/Tests/LeagueSchedulingTests/BitSet64Tests.swift b/Tests/LeagueSchedulingTests/BitSet64Tests.swift new file mode 100644 index 0000000..adcf231 --- /dev/null +++ b/Tests/LeagueSchedulingTests/BitSet64Tests.swift @@ -0,0 +1,119 @@ + +@testable import LeagueScheduling +import Testing + +struct BitSet64Tests { + @Test + func bitSet64InsertMember() { + var s = BitSet64() + for i in 0...init(storage: .max) + for i in 0...init(storage: 0x1010101010101010) + #expect(!s.contains(0)) + #expect(!s.contains(1)) + #expect(!s.contains(2)) + #expect(!s.contains(3)) + #expect(s.contains(4)) + #expect(!s.contains(5)) + #expect(!s.contains(6)) + #expect(!s.contains(7)) + #expect(!s.contains(8)) + + #expect(s.contains(12)) + #expect(s.contains(20)) + #expect(s.contains(28)) + #expect(s.contains(36)) + #expect(s.contains(44)) + #expect(s.contains(52)) + #expect(s.contains(60)) + } + + @Test + func bitSet64RandomElement() { + var s = BitSet64() + #expect(s.randomElement() == nil) + + s.insertMember(8) + #expect(s.randomElement() == 8) + + s.removeMember(8) + #expect(s.randomElement() == nil) + + s.insertMember(65) + #expect(s.randomElement() == nil) + } + + @Test + func bitSet64ForEach() { + var s = BitSet64() + s.forEach { _ in + #expect(Bool(false)) + } + + s.insertMember(0) + s.insertMember(32) + s.insertMember(63) + s.forEach { i in + #expect(i == 0 || i == 32 || i == 63) + } + #expect(s.contains(0)) + #expect(s.contains(32)) + #expect(s.contains(63)) + } + + @Test + func bitSet64Count() { + var s = BitSet64() + #expect(s.count == 0) + #expect(s.isEmpty) + + s.insertMember(0) + #expect(s.count == 1) + #expect(!s.isEmpty) + + s.insertMember(64) + #expect(s.count == 1) + #expect(!s.isEmpty) + + s.insertMember(63) + #expect(s.count == 2) + #expect(!s.isEmpty) + + s.removeMember(0) + #expect(s.count == 1) + #expect(!s.isEmpty) + } + + @Test + func bitSet64FormUnion() { + var s = BitSet64(storage: 0x1010101010101010) + s.formUnion(.init(storage: 0x0101010101010101)) + #expect(s.storage == 0x1111111111111111) + } + + @Test + func bitSet64RemoveAllWhere() { + var s = BitSet64(storage: .max) + s.removeAll(where: { $0 % 2 == 0 }) + #expect(s.storage == 0xAAAAAAAAAAAAAAAA) + + s.removeAll(where: { $0 % 1 == 0 }) + #expect(s.storage == 0) + } +} \ No newline at end of file diff --git a/Tests/LeagueSchedulingTests/CanPlayAtTests.swift b/Tests/LeagueSchedulingTests/CanPlayAtTests.swift index dc415d8..0715745 100644 --- a/Tests/LeagueSchedulingTests/CanPlayAtTests.swift +++ b/Tests/LeagueSchedulingTests/CanPlayAtTests.swift @@ -13,7 +13,7 @@ struct CanPlayAtTests { var gameGap = GameGap.upTo(1).minMax var playsAt:some SetOfAvailableSlots = Set() - var playsAtTimes:OrderedSet = [] + var playsAtTimes:some SetOfTimeIndexes = 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) @@ -24,8 +24,8 @@ struct CanPlayAtTests { #expect(CanPlayAtNormal.test( time: time, location: location, - allowedTimes: [0, 1, 2], - allowedLocations: [0, 1, 2], + allowedTimes: BitSet64([0, 1, 2]), + allowedLocations: BitSet64([0, 1, 2]), playsAtTimes: playsAtTimes, timeNumber: timeNumbers[unchecked: time], locationNumber: locationNumbers[unchecked: location], @@ -36,8 +36,8 @@ struct CanPlayAtTests { #expect(!CanPlayAtNormal.test( time: time, location: location, - allowedTimes: [], - allowedLocations: [], + allowedTimes: BitSet64(), + allowedLocations: BitSet64(), playsAtTimes: playsAtTimes, timeNumber: timeNumbers[unchecked: time], locationNumber: locationNumbers[unchecked: location], @@ -48,12 +48,12 @@ struct CanPlayAtTests { } playsAt.insertMember(AvailableSlot(time: 0, location: location)) - playsAtTimes.append(0) + playsAtTimes.insertMember(0) #expect(!CanPlayAtNormal.test( time: 0, location: location, - allowedTimes: [0, 1, 2], - allowedLocations: [0, 1, 2], + allowedTimes: BitSet64([0, 1, 2]), + allowedLocations: BitSet64([0, 1, 2]), playsAtTimes: playsAtTimes, timeNumber: timeNumbers[unchecked: 0], locationNumber: locationNumbers[unchecked: location], @@ -63,13 +63,13 @@ struct CanPlayAtTests { )) playsAt.removeAll() - playsAtTimes = [] + playsAtTimes.removeAll() timeNumbers[0] = 1 #expect(!CanPlayAtNormal.test( time: 0, location: location, - allowedTimes: [0, 1, 2], - allowedLocations: [0, 1, 2], + allowedTimes: BitSet64([0, 1, 2]), + allowedLocations: BitSet64([0, 1, 2]), playsAtTimes: playsAtTimes, timeNumber: timeNumbers[0], locationNumber: locationNumbers[unchecked: location], diff --git a/Tests/LeagueSchedulingTests/DivisionMatchupCombinationTests.swift b/Tests/LeagueSchedulingTests/DivisionMatchupCombinationTests.swift index 9f6dca5..3378341 100644 --- a/Tests/LeagueSchedulingTests/DivisionMatchupCombinationTests.swift +++ b/Tests/LeagueSchedulingTests/DivisionMatchupCombinationTests.swift @@ -7,7 +7,7 @@ import Testing @Suite struct DivisionMatchupCombinationTests { @Test(.timeLimit(.minutes(1))) - func allDivisionMatchupCombinations() { + func testAllDivisionMatchupCombinations() { var expected:ContiguousArray>> = [ [ [0, 6], [2, 4], [3, 3], [4, 2], [6, 0] @@ -57,7 +57,7 @@ struct DivisionMatchupCombinationTests { // MARK: Allowed extension DivisionMatchupCombinationTests { @Test(.timeLimit(.minutes(1))) - func allowedDivisionMatchupCombinations() { + func testAllowedDivisionMatchupCombinations() { var expected:ContiguousArray>> = [ [ [0, 6], [6, 0] diff --git a/Tests/LeagueSchedulingTests/LeagueHTMLFormTests.swift b/Tests/LeagueSchedulingTests/LeagueHTMLFormTests.swift index 912043c..cbd71c9 100644 --- a/Tests/LeagueSchedulingTests/LeagueHTMLFormTests.swift +++ b/Tests/LeagueSchedulingTests/LeagueHTMLFormTests.swift @@ -1,4 +1,5 @@ +/* // TODO: fix @testable import LeagueScheduling import Testing @@ -46,19 +47,19 @@ extension LeagueHTMLFormTests { var settings = try payload.parseSettings() for team in 0.. RequestPayload.Runtime { + static func scheduleB2B_11GameDays4Times6Locations2Divisions24Teams14_10() throws -> UnitTestRuntimeSchedule { let maxEntryMatchupsPerGameDay:EntryMatchupsPerGameDay = 2 let (gameDays, times, locations, teams):(DayIndex, TimeIndex, LocationIndex, Int) = (11, 4, 6, 24) var entryDivisions = [Division.IDValue]() diff --git a/Tests/LeagueSchedulingTests/schedules/ScheduleBeanBagToss.swift b/Tests/LeagueSchedulingTests/schedules/ScheduleBeanBagToss.swift index 38d17f6..c2c140e 100644 --- a/Tests/LeagueSchedulingTests/schedules/ScheduleBeanBagToss.swift +++ b/Tests/LeagueSchedulingTests/schedules/ScheduleBeanBagToss.swift @@ -18,7 +18,7 @@ struct ScheduleBeanBagToss: ScheduleTestsProtocol { } static func schedule8GameDays3Times3Locations1Division9Teams( constraints: GenerationConstraints = .unitTestDefault - ) throws -> RequestPayload.Runtime { + ) throws -> UnitTestRuntimeSchedule { let maxEntryMatchupsPerGameDay:EntryMatchupsPerGameDay = 2 let (gameDays, times, locations, teams):(DayIndex, TimeIndex, LocationIndex, Int) = (8, 3, 3, 9) let entries = getEntries( @@ -364,8 +364,8 @@ extension ScheduleBeanBagToss { let entry = entries[i] var gameTimes = entry.gameTimes for j in 0.. RequestPayload.Runtime { + ) throws -> UnitTestRuntimeSchedule { let maxEntryMatchupsPerGameDay:EntryMatchupsPerGameDay = 2 let (gameDays, times, locations, teams):(DayIndex, TimeIndex, LocationIndex, Int) = (10, 4, 8, 21) return Self.getSchedule( @@ -462,7 +462,7 @@ extension ScheduleBeanBagToss { } static func scheduleBeanBagToss_10GameDays4Times6Locations2Division23Teams( constraints: GenerationConstraints = .unitTestDefault - ) throws -> RequestPayload.Runtime { + ) throws -> UnitTestRuntimeSchedule { let maxEntryMatchupsPerGameDay:EntryMatchupsPerGameDay = 2 let (gameDays, times, locations, teams):(DayIndex, TimeIndex, LocationIndex, Int) = (10, 4, 6, 23) return Self.getSchedule( diff --git a/Tests/LeagueSchedulingTests/schedules/ScheduleMisc.swift b/Tests/LeagueSchedulingTests/schedules/ScheduleMisc.swift index ba6d79c..ed05cd4 100644 --- a/Tests/LeagueSchedulingTests/schedules/ScheduleMisc.swift +++ b/Tests/LeagueSchedulingTests/schedules/ScheduleMisc.swift @@ -198,7 +198,7 @@ extension ScheduleMisc { ) } - static func schedule10GameDays4Times5Locations2Divisions20Teams2Matchups() throws -> RequestPayload.Runtime { + static func schedule10GameDays4Times5Locations2Divisions20Teams2Matchups() throws -> UnitTestRuntimeSchedule { let maxEntryMatchupsPerGameDay:EntryMatchupsPerGameDay = 2 let (gameDays, times, locations, teams):(DayIndex, TimeIndex, LocationIndex, Int) = (10, 4, 5, 20) var entryDivisions = [Division.IDValue]() diff --git a/Tests/LeagueSchedulingTests/schedules/ScheduleSameLocationIfB2B.swift b/Tests/LeagueSchedulingTests/schedules/ScheduleSameLocationIfB2B.swift index 02d351b..4bf9673 100644 --- a/Tests/LeagueSchedulingTests/schedules/ScheduleSameLocationIfB2B.swift +++ b/Tests/LeagueSchedulingTests/schedules/ScheduleSameLocationIfB2B.swift @@ -16,7 +16,7 @@ struct ScheduleSameLocationIfB2B: ScheduleTestsProtocol { data: data ) } - static func scheduleSameLocationIfB2B_8GameDays3Times3Locations1Division9Teams() throws -> RequestPayload.Runtime { + static func scheduleSameLocationIfB2B_8GameDays3Times3Locations1Division9Teams() throws -> UnitTestRuntimeSchedule { let maxEntryMatchupsPerGameDay:EntryMatchupsPerGameDay = 2 let (gameDays, times, locations, teams):(DayIndex, TimeIndex, LocationIndex, Int) = (8, 3, 3, 9) let entries = getEntries( @@ -61,7 +61,7 @@ extension ScheduleSameLocationIfB2B { data: data ) } - static func scheduleSameLocationIfB2B_12GameDays3Times1Locations1Division5Teams() throws -> RequestPayload.Runtime { + static func scheduleSameLocationIfB2B_12GameDays3Times1Locations1Division5Teams() throws -> UnitTestRuntimeSchedule { let maxEntryMatchupsPerGameDay:EntryMatchupsPerGameDay = 2 let (gameDays, times, locations, teams):(DayIndex, TimeIndex, LocationIndex, Int) = (12, 3, 1, 5) let entries = getEntries( @@ -107,7 +107,7 @@ extension ScheduleSameLocationIfB2B { data: data ) } - static func scheduleSameLocationIfB2B_10GameDays4Times4Locations1Division16Teams() throws -> RequestPayload.Runtime { + static func scheduleSameLocationIfB2B_10GameDays4Times4Locations1Division16Teams() throws -> UnitTestRuntimeSchedule { let maxEntryMatchupsPerGameDay:EntryMatchupsPerGameDay = 2 let (gameDays, times, locations, teams):(DayIndex, TimeIndex, LocationIndex, Int) = (10, 4, 4, 16) let entries = getEntries( diff --git a/Tests/LeagueSchedulingTests/schedules/expectations/BalanceHomeAwayExpectations.swift b/Tests/LeagueSchedulingTests/schedules/expectations/BalanceHomeAwayExpectations.swift index cd0ff7d..776b233 100644 --- a/Tests/LeagueSchedulingTests/schedules/expectations/BalanceHomeAwayExpectations.swift +++ b/Tests/LeagueSchedulingTests/schedules/expectations/BalanceHomeAwayExpectations.swift @@ -2,13 +2,13 @@ @testable import LeagueScheduling import Testing -struct BalanceHomeAwayExpectations: ScheduleTestsProtocol { +struct BalanceHomeAwayExpectations: ScheduleTestsProtocol { func expectations( cap: MaximumSameOpponentMatchupsCap, matchupsPlayedPerDay: ContiguousArray>, assignedEntryHomeAways: AssignedEntryHomeAways, entryMatchupsPerGameDay: EntryMatchupsPerGameDay, - divisionEntries: [Entry.Runtime] + divisionEntries: [Config.EntryRuntime] ) { for entry in divisionEntries { let entryMatchupsPlayed = matchupsPlayedPerDay.reduce(0, { $0 + $1[unchecked: entry.id] }) @@ -26,7 +26,7 @@ struct BalanceHomeAwayExpectations: ScheduleTestsProtocol { } } for opponentEntry in divisionEntries { - if entry != opponentEntry { + if !entry.isEqual(to: opponentEntry) { let value = assignedEntryHomeAways[unchecked: entry.id][unchecked: opponentEntry.id] let sum = value.sum #expect(sum <= cap) @@ -40,7 +40,7 @@ struct BalanceHomeAwayExpectations: ScheduleTestsProtocol { } } func isBalanced( - entry: Entry.Runtime, + entry: Config.EntryRuntime, matchupsPlayedPerDay: ContiguousArray>, assignedEntryHomeAways: AssignedEntryHomeAways, entryMatchupsPerGameDay: EntryMatchupsPerGameDay @@ -74,8 +74,15 @@ extension BalanceHomeAwayExpectations { } } -extension Entry.Runtime: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.id == rhs.id +extension Entry.Runtime { + func isEqual(to rhs: Self) -> Bool { + id == rhs.id + && division == rhs.division + //&& gameDays == rhs.gameDays + //&& lhs.gameTimes == rhs.gameTimes + //&& lhs.gameLocations == rhs.gameLocations + //&& lhs.homeLocations == rhs.homeLocations + //&& byes == rhs.byes + //&& lhs.matchupsPerGameDay == rhs.matchupsPerGameDay } } \ No newline at end of file diff --git a/Tests/LeagueSchedulingTests/schedules/expectations/DayExpectations.swift b/Tests/LeagueSchedulingTests/schedules/expectations/DayExpectations.swift index 32cfd4b..85cecf5 100644 --- a/Tests/LeagueSchedulingTests/schedules/expectations/DayExpectations.swift +++ b/Tests/LeagueSchedulingTests/schedules/expectations/DayExpectations.swift @@ -2,11 +2,10 @@ @testable import LeagueScheduling import Testing -struct DayExpectations: ScheduleTestsProtocol { - let settings:GeneralSettings.Runtime +struct DayExpectations: ScheduleTestsProtocol { let b2bMatchupsAtDifferentLocations:Set - func expectations() { + func expectations(_ settings: GeneralSettings.Runtime) { if settings.sameLocationIfB2B { sameLocationIfB2B() } diff --git a/Tests/LeagueSchedulingTests/schedules/expectations/DivisionEntryExpectations.swift b/Tests/LeagueSchedulingTests/schedules/expectations/DivisionEntryExpectations.swift index 66a807f..03b3267 100644 --- a/Tests/LeagueSchedulingTests/schedules/expectations/DivisionEntryExpectations.swift +++ b/Tests/LeagueSchedulingTests/schedules/expectations/DivisionEntryExpectations.swift @@ -2,18 +2,18 @@ @testable import LeagueScheduling import Testing -struct DivisionEntryExpectations: ScheduleTestsProtocol { +struct DivisionEntryExpectations: ScheduleTestsProtocol { let cap:MaximumSameOpponentMatchupsCap let matchupsPlayedPerDay:ContiguousArray> let assignedEntryHomeAways:AssignedEntryHomeAways let entryMatchupsPerGameDay:EntryMatchupsPerGameDay - let divisionEntries:[Entry.Runtime] func expectations( - balanceHomeAway: Bool + balanceHomeAway: Bool, + divisionEntries: [Config.EntryRuntime] ) { if balanceHomeAway { - BalanceHomeAwayExpectations().expectations( + BalanceHomeAwayExpectations().expectations( cap: cap, matchupsPlayedPerDay: matchupsPlayedPerDay, assignedEntryHomeAways: assignedEntryHomeAways, diff --git a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift index 099710a..e9eac74 100644 --- a/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift +++ b/Tests/LeagueSchedulingTests/schedules/expectations/ScheduleExpectations.swift @@ -12,14 +12,14 @@ extension GenerationConstraints { regenerationAttemptsForConsecutiveDay: Self.default.regenerationAttemptsForConsecutiveDay, regenerationAttemptsThreshold: Self.default.regenerationAttemptsThreshold, determinism: nil,//.init(seed: 69), - attempts: 3 + attempts: 1 ) } // MARK: Expectations extension ScheduleExpectations { - func expectations( - settings: RequestPayload.Runtime, + func expectations( + settings: borrowing RequestPayload.Runtime, matchupsCount: Int, data: LeagueGenerationResult ) throws { @@ -44,11 +44,11 @@ extension ScheduleExpectations { var maxStartingTimes:TimeIndex = 0 var maxLocations:LocationIndex = 0 for setting in settings.daySettings { - if setting.general.startingTimes.count > maxStartingTimes { - maxStartingTimes = TimeIndex(setting.general.startingTimes.count) + if setting.startingTimes.count > maxStartingTimes { + maxStartingTimes = TimeIndex(setting.startingTimes.count) } - if setting.general.locations > maxLocations { - maxLocations = setting.general.locations + if setting.locations > maxLocations { + maxLocations = setting.locations } } @@ -67,7 +67,7 @@ extension ScheduleExpectations { matchupsPerDay[unchecked: dayIndex] = UInt32(matchups.count) var b2bMatchupsAtDifferentLocations = Set() - var assignedSlots = [Set](repeating: [], count: entriesCount) + var assignedSlots = [Config.AvailableSlotSet](repeating: .init(), count: entriesCount) for matchup in matchups { let home = matchup.home let away = matchup.away @@ -82,8 +82,8 @@ extension ScheduleExpectations { assignedEntryHomeAways[unchecked: home][unchecked: away].home += 1 assignedEntryHomeAways[unchecked: away][unchecked: home].away += 1 - assignedSlots[unchecked: home].insert(matchup.slot) - assignedSlots[unchecked: away].insert(matchup.slot) + assignedSlots[unchecked: home].insertMember(matchup.slot) + assignedSlots[unchecked: away].insertMember(matchup.slot) let homeSlots = assignedSlots[unchecked: home] if homeSlots.count > 1 { insertB2BSlotsAtDifferentLocations( @@ -103,12 +103,11 @@ extension ScheduleExpectations { ) } } - let settings = settings.daySettings[dayIndex].general - let dayExpectations = DayExpectations( - settings: settings, + let settings = settings.daySettings[dayIndex] + let dayExpectations = DayExpectations( b2bMatchupsAtDifferentLocations: b2bMatchupsAtDifferentLocations ) - dayExpectations.expectations() + dayExpectations.expectations(settings) if true { printMatchups(day: dayIndex, matchups) @@ -117,21 +116,21 @@ extension ScheduleExpectations { for (divisionIndex, division) in settings.divisions.enumerated() { let cap = division.maxSameOpponentMatchups let divisionEntries = settings.entries.filter { $0.division == divisionIndex } - let divisionEntryExpectations = DivisionEntryExpectations( + let divisionEntryExpectations = DivisionEntryExpectations( cap: cap, matchupsPlayedPerDay: matchupsPlayedPerDay, assignedEntryHomeAways: assignedEntryHomeAways, - entryMatchupsPerGameDay: entryMatchupsPerGameDay, - divisionEntries: divisionEntries + entryMatchupsPerGameDay: entryMatchupsPerGameDay ) divisionEntryExpectations.expectations( - balanceHomeAway: settings.general.balanceHomeAway + balanceHomeAway: settings.general.balanceHomeAway, + divisionEntries: divisionEntries ) } let balanceTimeNumber:TimeIndex if !settings.general.balancedTimes.isEmpty { - balanceTimeNumber = RequestPayload.Runtime.balanceNumber( + balanceTimeNumber = calculateBalanceNumber( totalMatchupsPlayed: totalMatchupsPlayed, value: settings.general.balancedTimes.count, strictness: settings.general.balanceTimeStrictness @@ -147,7 +146,7 @@ extension ScheduleExpectations { let balanceLocationNumber:LocationIndex if !settings.general.balancedLocations.isEmpty { - balanceLocationNumber = RequestPayload.Runtime.balanceNumber( + balanceLocationNumber = calculateBalanceNumber( totalMatchupsPlayed: totalMatchupsPlayed, value: settings.general.balancedLocations.count, strictness: settings.general.balanceLocationStrictness @@ -197,10 +196,10 @@ extension ScheduleExpectations { func insertB2BSlotsAtDifferentLocations( dayIndex: DayIndex, matchup: Matchup, - slots: Set, + slots: some SetOfAvailableSlots, b2bMatchupsAtDifferentLocations: inout Set ) { - for slot in slots { + slots.forEach { slot in var b2bTimes = Set() if slot.time > 0 { b2bTimes.insert(slot.time-1) @@ -218,7 +217,7 @@ extension ScheduleExpectations { extension ScheduleExpectations { private func allocatedLessThanOrEqualToBalanceTimeNumber( assignedTimes: AssignedTimes, - balancedTimes: Set, + balancedTimes: borrowing some SetOfTimeIndexes & ~Copyable, balanceTimeNumber: TimeIndex ) { for (entryID, assignedTimes) in assignedTimes.enumerated() { @@ -234,7 +233,7 @@ extension ScheduleExpectations { extension ScheduleExpectations { private func allocatedLessThanOrEqualToBalanceLocationNumber( assignedLocations: AssignedLocations, - balancedLocations: Set, + balancedLocations: borrowing some SetOfLocationIndexes & ~Copyable, balanceLocationNumber: LocationIndex ) { for (entryID, assignedLocations) in assignedLocations.enumerated() { diff --git a/Tests/LeagueSchedulingTests/schedules/util/ScheduleTestsProtocol.swift b/Tests/LeagueSchedulingTests/schedules/util/ScheduleTestsProtocol.swift index 1939c43..290f38f 100644 --- a/Tests/LeagueSchedulingTests/schedules/util/ScheduleTestsProtocol.swift +++ b/Tests/LeagueSchedulingTests/schedules/util/ScheduleTestsProtocol.swift @@ -4,6 +4,19 @@ import struct FoundationEssentials.Date import StaticDateTimes protocol ScheduleTestsProtocol: ScheduleExpectations { + typealias UnitTestScheduleConfig = ScheduleConfig< + SystemRandomNumberGenerator, + BitSet64, + BitSet64, + BitSet64, + BitSet64, + Set, + Set, + Set, + Set, + Set + > + typealias UnitTestRuntimeSchedule = RequestPayload.Runtime } // MARK: Get entries @@ -14,13 +27,13 @@ extension ScheduleTestsProtocol { times: TimeIndex, locations: LocationIndex, teams: Int, - homeLocations: ContiguousArray> = [], - byes: ContiguousArray> = [] - ) -> [Entry.Runtime] { - let playsOn = Array(repeating: Set(0.. = [], + byes: ContiguousArray = [] + ) -> [UnitTestScheduleConfig.EntryRuntime] { + let playsOn = Array(repeating: UnitTestScheduleConfig.DaySet(0.. Division.Runtime { - let maxSameOpponentMatchups = try RequestPayload.calculateMaximumSameOpponentMatchupsCap( + ) throws(LeagueError) -> UnitTestScheduleConfig.DivisionRuntime { + let maxSameOpponentMatchups = try calculateMaximumSameOpponentMatchupsCap( gameDays: values.gameDays, entryMatchupsPerGameDay: values.entryMatchupsPerGameDay, entriesCount: values.entriesCount ) return .init( dayOfWeek: dayOfWeek, - gameDays: Set(0.. RequestPayload.Runtime { + ) -> UnitTestRuntimeSchedule { let correctMaximumPlayableMatchups = RequestPayload.calculateMaximumPlayableMatchups( gameDays: gameDays, entryMatchupsPerGameDay: entryMatchupsPerGameDay, @@ -93,9 +106,9 @@ extension ScheduleTestsProtocol { maximumPlayableMatchups: maximumPlayableMatchups ) let times:TimeIndex = TimeIndex(startingTimes.count) - let timeSlots:Set = Set(0.. = Set(0...init( gameGap: gameGaps, timeSlots: TimeIndex(startingTimes.count), startingTimes: startingTimes, @@ -120,14 +133,15 @@ extension ScheduleTestsProtocol { ) ) - var daySettings = [DaySettings.Runtime]() + var daySettings = [GeneralSettings.Runtime]() daySettings.reserveCapacity(gameDays) for day in 0..