Compare commits

..

24 Commits
sync3 ... main

Author SHA1 Message Date
Laurent 3df4b37e52 add cache example 4 days ago
Razmig Sarkissian 21359c2f56 fix ongoing 2 weeks ago
Razmig Sarkissian f0f04e9f64 add a way to filter out tournament in ongoing view 2 weeks ago
Razmig Sarkissian 4d3679f95c build 4 2 weeks ago
Razmig Sarkissian 7608122bac fix groupstage 6 size ordering 2 weeks ago
Razmig Sarkissian c2f58e93da fix F format 2 weeks ago
Razmig Sarkissian e994d17946 Merge remote-tracking branch 'refs/remotes/origin/main' 2 weeks ago
Razmig Sarkissian 6152c92d9a fix issue with p25 format 2 weeks ago
Razmig Sarkissian 25563982bb fix issue with p25 format 3 weeks ago
Razmig Sarkissian dd7207ae33 small improvements 3 weeks ago
Razmig Sarkissian 617bb91d6a fix ranking warning 3 weeks ago
Razmig Sarkissian 66abaeeb58 fix send all by event 3 weeks ago
Razmig Sarkissian 9f66c3a769 add a call all event method 3 weeks ago
Razmig Sarkissian a17f3dee1c improve call team view 3 weeks ago
Razmig Sarkissian d64ee6a7e0 add global search 3 weeks ago
Razmig Sarkissian 4842ad4572 improve export data capability for teams / players 4 weeks ago
Razmig Sarkissian 4fc4c2b41c add custom club name option in tournament for calling teams 4 weeks ago
Razmig Sarkissian d321634069 fix head manager match count 4 weeks ago
Razmig Sarkissian 01e455d8b5 fix points won issue 4 weeks ago
Razmig Sarkissian df010931bc fix stuff 4 weeks ago
Razmig Sarkissian 4454a2c2d6 fix stuff headmanager 4 weeks ago
Razmig Sarkissian 54a35571b1 add heads config system 4 weeks ago
Razmig Sarkissian ecf14eaa32 couple of fixes 1 month ago
Razmig Sarkissian 1ef3faf5a2 add format selection to horaire and format view 1 month ago
  1. 4
      PadelClubData/Data/DataStore.swift
  2. 28
      PadelClubData/Data/Event.swift
  3. 9
      PadelClubData/Data/Gen/BaseTournament.swift
  4. 5
      PadelClubData/Data/Gen/Tournament.json
  5. 18
      PadelClubData/Data/GroupStage.swift
  6. 12
      PadelClubData/Data/Match.swift
  7. 42
      PadelClubData/Data/PlayerRegistration.swift
  8. 24
      PadelClubData/Data/Round.swift
  9. 62
      PadelClubData/Data/TeamRegistration.swift
  10. 73
      PadelClubData/Data/Tournament.swift
  11. 12
      PadelClubData/Extensions/Date+Extensions.swift
  12. 93
      PadelClubData/Extensions/String+Extensions.swift
  13. 95
      PadelClubData/Utils/ContactManager.swift
  14. 20
      PadelClubData/Utils/ExportFormat.swift
  15. 67
      PadelClubData/ViewModel/PadelRule.swift

@ -332,7 +332,7 @@ public class DataStore: ObservableObject {
_lastRunningAndNextCheckDate = nil _lastRunningAndNextCheckDate = nil
} }
public func runningAndNextMatches() -> [Match] { public func runningAndNextMatches(_ selectedTournaments: Set<String> = Set()) -> [Match] {
let dateNow : Date = Date() let dateNow : Date = Date()
if let lastCheck = _lastRunningAndNextCheckDate, if let lastCheck = _lastRunningAndNextCheckDate,
let cachedMatches = _cachedRunningAndNextMatches, let cachedMatches = _cachedRunningAndNextMatches,
@ -340,7 +340,7 @@ public class DataStore: ObservableObject {
return cachedMatches return cachedMatches
} }
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) let lastTournaments = self.tournaments.filter { (selectedTournaments.isEmpty || selectedTournaments.contains($0.id) == false) && $0.isDeleted == false && $0.startDate <= dateNow.addingTimeInterval(86_400) && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
var runningMatches: [Match] = [] var runningMatches: [Match] = []
for tournament in lastTournaments { for tournament in lastTournaments {

@ -183,7 +183,35 @@ final public class Event: BaseEvent {
return link.compactMap({ $0 }).joined(separator: "\n\n") return link.compactMap({ $0 }).joined(separator: "\n\n")
} }
public func selectedTeams() -> [TeamRegistration] {
confirmedTournaments().flatMap({ $0.selectedSortedTeams() })
}
public func umpireMail() -> [String]? {
confirmedTournaments().first?.umpireMail()
}
public func mailSubject() -> String {
let tournaments = confirmedTournaments()
guard !tournaments.isEmpty else {
return eventTitle()
}
// Get the date range from all confirmed tournaments
let dates = tournaments.compactMap { $0.startDate }
guard let firstDate = dates.min(), let lastDate = dates.max() else {
return eventTitle()
}
let dateRange = firstDate == lastDate
? firstDate.formattedDate(.short)
: "\(firstDate.formatted(.dateTime.day())) au \(lastDate.formatted(.dateTime.day())) \(lastDate.formatted(.dateTime.month(.wide))) \(lastDate.formatted(.dateTime.year()))"
let subject = [eventTitle(), dateRange, clubObject()?.name].compactMap({ $0 }).joined(separator: " | ")
return subject
}
func insertOnServer() throws { func insertOnServer() throws {
DataStore.shared.events.writeChangeAndInsertOnServer(instance: self) DataStore.shared.events.writeChangeAndInsertOnServer(instance: self)

@ -85,6 +85,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
public var clubMemberFeeDeduction: Double? = nil public var clubMemberFeeDeduction: Double? = nil
public var unregisterDeltaInHours: Int = 24 public var unregisterDeltaInHours: Int = 24
public var currencyCode: String? = nil public var currencyCode: String? = nil
public var customClubName: String? = nil
public init( public init(
id: String = Store.randomId(), id: String = Store.randomId(),
@ -158,7 +159,8 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
showTeamsInProg: Bool = false, showTeamsInProg: Bool = false,
clubMemberFeeDeduction: Double? = nil, clubMemberFeeDeduction: Double? = nil,
unregisterDeltaInHours: Int = 24, unregisterDeltaInHours: Int = 24,
currencyCode: String? = nil currencyCode: String? = nil,
customClubName: String? = nil
) { ) {
super.init() super.init()
self.id = id self.id = id
@ -233,6 +235,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.clubMemberFeeDeduction = clubMemberFeeDeduction self.clubMemberFeeDeduction = clubMemberFeeDeduction
self.unregisterDeltaInHours = unregisterDeltaInHours self.unregisterDeltaInHours = unregisterDeltaInHours
self.currencyCode = currencyCode self.currencyCode = currencyCode
self.customClubName = customClubName
} }
required public override init() { required public override init() {
super.init() super.init()
@ -313,6 +316,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
case _clubMemberFeeDeduction = "clubMemberFeeDeduction" case _clubMemberFeeDeduction = "clubMemberFeeDeduction"
case _unregisterDeltaInHours = "unregisterDeltaInHours" case _unregisterDeltaInHours = "unregisterDeltaInHours"
case _currencyCode = "currencyCode" case _currencyCode = "currencyCode"
case _customClubName = "customClubName"
} }
private static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? { private static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? {
@ -456,6 +460,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.clubMemberFeeDeduction = try container.decodeIfPresent(Double.self, forKey: ._clubMemberFeeDeduction) ?? nil self.clubMemberFeeDeduction = try container.decodeIfPresent(Double.self, forKey: ._clubMemberFeeDeduction) ?? nil
self.unregisterDeltaInHours = try container.decodeIfPresent(Int.self, forKey: ._unregisterDeltaInHours) ?? 24 self.unregisterDeltaInHours = try container.decodeIfPresent(Int.self, forKey: ._unregisterDeltaInHours) ?? 24
self.currencyCode = try container.decodeIfPresent(String.self, forKey: ._currencyCode) ?? nil self.currencyCode = try container.decodeIfPresent(String.self, forKey: ._currencyCode) ?? nil
self.customClubName = try container.decodeIfPresent(String.self, forKey: ._customClubName) ?? nil
try super.init(from: decoder) try super.init(from: decoder)
} }
@ -533,6 +538,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
try container.encode(self.clubMemberFeeDeduction, forKey: ._clubMemberFeeDeduction) try container.encode(self.clubMemberFeeDeduction, forKey: ._clubMemberFeeDeduction)
try container.encode(self.unregisterDeltaInHours, forKey: ._unregisterDeltaInHours) try container.encode(self.unregisterDeltaInHours, forKey: ._unregisterDeltaInHours)
try container.encode(self.currencyCode, forKey: ._currencyCode) try container.encode(self.currencyCode, forKey: ._currencyCode)
try container.encode(self.customClubName, forKey: ._customClubName)
try super.encode(to: encoder) try super.encode(to: encoder)
} }
@ -615,6 +621,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.clubMemberFeeDeduction = tournament.clubMemberFeeDeduction self.clubMemberFeeDeduction = tournament.clubMemberFeeDeduction
self.unregisterDeltaInHours = tournament.unregisterDeltaInHours self.unregisterDeltaInHours = tournament.unregisterDeltaInHours
self.currencyCode = tournament.currencyCode self.currencyCode = tournament.currencyCode
self.customClubName = tournament.customClubName
} }
public static func parentRelationships() -> [Relationship] { public static func parentRelationships() -> [Relationship] {

@ -372,6 +372,11 @@
"name": "currencyCode", "name": "currencyCode",
"type": "String", "type": "String",
"optional": true "optional": true
},
{
"name": "customClubName",
"type": "String",
"optional": true
} }
] ]
} }

@ -337,7 +337,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
case 5: case 5:
order = [3, 5, 8, 2, 6, 1, 9, 4, 7, 0] order = [3, 5, 8, 2, 6, 1, 9, 4, 7, 0]
case 6: case 6:
order = [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] order = [4, 6, 10, 1, 8, 12, 2, 7, 11, 3, 5, 13, 14, 9, 0]
case 7: case 7:
order = [6, 15, 20, 1, 16, 19, 2, 10, 18, 3, 9, 14, 4, 7, 12, 5, 8, 11, 0, 13, 17] order = [6, 15, 20, 1, 16, 19, 2, 10, 18, 3, 9, 14, 4, 7, 12, 5, 8, 11, 0, 13, 17]
case 8: case 8:
@ -628,6 +628,22 @@ final public class GroupStage: BaseGroupStage, SideStorable {
} }
tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams) tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
} }
public func setData(from correspondingGroupStage: GroupStage, tournamentStartDate: Date, previousTournamentStartDate: Date) {
self.matchFormat = correspondingGroupStage.matchFormat
if let correspondingPlannedStartDate = correspondingGroupStage.plannedStartDate {
let offset = correspondingPlannedStartDate.timeIntervalSince(previousTournamentStartDate)
self.startDate = tournamentStartDate.addingTimeInterval(offset)
}
self.size = correspondingGroupStage.size
self.name = correspondingGroupStage.name
let matches = correspondingGroupStage._matches()
for (index, match) in self._matches().enumerated() {
match.setData(from: matches[index], tournamentStartDate: tournamentStartDate, previousTournamentStartDate: previousTournamentStartDate)
}
}
public override func deleteDependencies(store: Store, actionOption: ActionOption) { public override func deleteDependencies(store: Store, actionOption: ActionOption) {
store.deleteDependencies(type: Match.self, actionOption: actionOption) { $0.groupStage == self.id } store.deleteDependencies(type: Match.self, actionOption: actionOption) { $0.groupStage == self.id }

@ -1139,6 +1139,18 @@ defer {
public func initialStartDate() -> Date? { public func initialStartDate() -> Date? {
plannedStartDate ?? startDate plannedStartDate ?? startDate
} }
public func setData(from correspondingMatch: Match, tournamentStartDate: Date, previousTournamentStartDate: Date) {
if let correspondingMatchPlannedStartDate = correspondingMatch.plannedStartDate {
let offset = correspondingMatchPlannedStartDate.timeIntervalSince(previousTournamentStartDate)
self.startDate = tournamentStartDate.addingTimeInterval(offset)
}
self.disabled = correspondingMatch.disabled
self.matchFormat = correspondingMatch.matchFormat
self.courtIndex = correspondingMatch.courtIndex
self.name = correspondingMatch.name
}
func insertOnServer() { func insertOnServer() {
self.tournamentStore?.matches.writeChangeAndInsertOnServer(instance: self) self.tournamentStore?.matches.writeChangeAndInsertOnServer(instance: self)

@ -56,12 +56,23 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
return nil return nil
} }
public func pasteData(_ exportFormat: ExportFormat = .rawText) -> String { public func pasteData(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> String {
switch exportFormat { switch type {
case .rawText: case .payment:
return [firstName.capitalized, lastName.capitalized, licenceId?.computedLicense].compactMap({ $0 }).joined(separator: exportFormat.separator()) switch exportFormat {
case .csv: case .rawText:
return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator()) return [firstName.capitalized, lastName.capitalized, hasPaidOnline() ? "Payé [X]" : "Payé  [ ]"].compactMap({ $0 }).joined(separator: exportFormat.separator())
case .csv:
return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator())
}
case .sharing:
switch exportFormat {
case .rawText:
return [firstName.capitalized, lastName.capitalized, licenceId?.computedLicense].compactMap({ $0 }).joined(separator: exportFormat.separator())
case .csv:
return [lastName.uppercased() + " " + firstName.capitalized, hasPaid() ? "Payé" : "", hasPaidOnline() ? "En ligne" : ""]
.joined(separator: exportFormat.separator())
}
} }
} }
@ -70,6 +81,13 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
} }
public func contains(_ searchField: String) -> Bool { public func contains(_ searchField: String) -> Bool {
if let paymentId, paymentId.localizedCaseInsensitiveContains(searchField) { return true }
if let email, email.lowercased().localizedCaseInsensitiveContains(searchField) { return true }
if let contactEmail, contactEmail.localizedCaseInsensitiveContains(searchField) { return true }
if let licenceId, licenceId.localizedCaseInsensitiveContains(searchField) { return true }
if searchField.isPhoneNumber(), let phoneNumber, phoneNumber.isSamePhoneNumber(as: searchField) { return true }
if searchField.isPhoneNumber(), let contactPhoneNumber, contactPhoneNumber.isSamePhoneNumber(as: searchField) { return true }
let nameComponents = searchField.canonicalVersion.split(separator: " ") let nameComponents = searchField.canonicalVersion.split(separator: " ")
if nameComponents.count > 1 { if nameComponents.count > 1 {
@ -167,7 +185,7 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
} }
public func setComputedRank(in tournament: Tournament) { public func setComputedRank(in tournament: Tournament) {
let maleUnranked = tournament.unrankValue(for: isMalePlayer()) ?? 90_415 let maleUnranked = tournament.unrankValue(for: isMalePlayer()) ?? 92_327
let femaleUnranked = tournament.unrankValue(for: false) ?? 0 let femaleUnranked = tournament.unrankValue(for: false) ?? 0
let currentRank = rank ?? maleUnranked let currentRank = rank ?? maleUnranked
switch tournament.tournamentCategory { switch tournament.tournamentCategory {
@ -212,6 +230,16 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
registrationStatus = .confirmed registrationStatus = .confirmed
} }
public func hasMail() -> Bool {
let mails = [email, contactEmail].compactMap({ $0 })
return mails.isEmpty == false && mails.anySatisfy({ $0.isValidEmail() })
}
public func hasMobilePhone() -> Bool {
let phones = [phoneNumber, contactPhoneNumber].compactMap({ $0 })
return phones.isEmpty == false && phones.anySatisfy({ $0.isPhoneNumber() })
}
public func paidAmount(_ tournament: Tournament, accountForGiftOrForfeit: Bool = false) -> Double { public func paidAmount(_ tournament: Tournament, accountForGiftOrForfeit: Bool = false) -> Double {
if accountForGiftOrForfeit == false, paymentType == .gift { if accountForGiftOrForfeit == false, paymentType == .gift {
return 0.0 return 0.0

@ -56,6 +56,30 @@ final public class Round: BaseRound, SideStorable {
return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == true } return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == true }
} }
public func setData(from correspondingRound: Round, tournamentStartDate: Date, previousTournamentStartDate: Date) {
let matches = correspondingRound._matches()
for (index, match) in self._matches().enumerated() {
match.setData(from: matches[index], tournamentStartDate: tournamentStartDate, previousTournamentStartDate: previousTournamentStartDate)
}
self.matchFormat = correspondingRound.matchFormat
if let correspondingPlannedStartDate = correspondingRound.plannedStartDate {
let offset = correspondingPlannedStartDate.timeIntervalSince(previousTournamentStartDate)
self.startDate = tournamentStartDate.addingTimeInterval(offset)
}
self.loserBracketMode = correspondingRound.loserBracketMode
self.groupStageLoserBracket = correspondingRound.groupStageLoserBracket
loserRounds().forEach { round in
if let pRound = correspondingRound.loserRounds().first(where: { r in
r.index == round.index
}) {
round.setData(from: pRound, tournamentStartDate: tournamentStartDate, previousTournamentStartDate: previousTournamentStartDate)
}
}
}
// MARK: - // MARK: -
public var matchFormat: MatchFormat { public var matchFormat: MatchFormat {

@ -171,7 +171,7 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
} else if let roundMatchStartDate = initialMatch()?.startDate { } else if let roundMatchStartDate = initialMatch()?.startDate {
return roundMatchStartDate return roundMatchStartDate
} }
return nil return callDate
} }
public var initialWeight: Int { public var initialWeight: Int {
@ -378,14 +378,20 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
resetBracketPosition() resetBracketPosition()
} }
public func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0) -> String { public func pasteData(_ exportFormat: ExportFormat = .rawText, type: ExportType, _ index: Int = 0) -> String {
switch exportFormat { switch exportFormat {
case .rawText: case .rawText:
return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name] switch type {
.compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator()) case .sharing:
return [playersPasteData(exportFormat, type: type), formattedInscriptionDate(exportFormat), name]
.compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator())
case .payment:
return [playersPasteData(exportFormat, type: type), name]
.compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator())
}
case .csv: case .csv:
return [ return [
index.formatted(), playersPasteData(exportFormat), index.formatted(), playersPasteData(exportFormat, type: type),
isWildCard() ? "WC" : weight.formatted(), isWildCard() ? "WC" : weight.formatted(),
].joined(separator: exportFormat.separator()) ].joined(separator: exportFormat.separator())
} }
@ -435,15 +441,20 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
} }
public func playersPasteData(_ exportFormat: ExportFormat = .rawText) -> String { public func playersPasteData(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> String {
switch exportFormat { switch exportFormat {
case .rawText: case .rawText:
return players().map { $0.pasteData(exportFormat) }.joined( return players().map { $0.pasteData(exportFormat, type: type) }.joined(
separator: exportFormat.newLineSeparator()) separator: exportFormat.newLineSeparator())
case .csv: case .csv:
return players().map { return players().map {
[$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted()] switch type {
.joined(separator: exportFormat.separator()) case .sharing:
[$0.pasteData(exportFormat, type: type), isWildCard() ? "WC" : $0.computedRank.formatted()]
.joined(separator: exportFormat.separator())
case .payment:
$0.pasteData(exportFormat, type: type)
}
}.joined(separator: exportFormat.separator()) }.joined(separator: exportFormat.separator())
} }
} }
@ -540,7 +551,7 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
public func unrankValue(for malePlayer: Bool) -> Int { public func unrankValue(for malePlayer: Bool) -> Int {
return tournamentObject()?.unrankValue(for: malePlayer) ?? 90_415 return tournamentObject()?.unrankValue(for: malePlayer) ?? 92_327
} }
public func groupStageObject() -> GroupStage? { public func groupStageObject() -> GroupStage? {
@ -676,6 +687,37 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
return nil return nil
} }
public func followingMatches() -> [Match] {
guard let tournamentStore else { return [] }
let allTeamScores = tournamentStore.teamScores.filter({ $0.teamRegistration == self.id })
let ids = allTeamScores.compactMap({ $0.match })
let matches = tournamentStore.matches.filter({ match in
ids.contains(match.id)
})
return matches.sorted(by: \.computedStartDateForSorting)
}
public func nextMatch(in followingMatches: [Match]) -> Match? {
return followingMatches.filter({ $0.hasEnded() == false }).first
}
public func lastMatchPlayed(in followingMatches: [Match]) -> Match? {
return followingMatches.first(where: { $0.hasEnded() })
}
public func numberOfRotation(in followingMatches: [Match]) -> (Int, Int)? {
if let nextMatch = nextMatch(in: followingMatches), let nextMatchPlannedStartDate = nextMatch.plannedStartDate, let lastMatchPlayed = lastMatchPlayed(in: followingMatches), let lastMatchPlayedPlannedStartDate = lastMatchPlayed.plannedStartDate {
let courtCount = self.tournamentStore?.matches.filter({ $0.plannedStartDate == nextMatchPlannedStartDate && $0.disabled == false && $0.hasEnded() == false && $0.confirmed == true && $0.id != nextMatch.id && $0.hasStarted() == false }).count ?? 0
let interval = nextMatchPlannedStartDate.timeIntervalSince(lastMatchPlayedPlannedStartDate)
let matchDuration = lastMatchPlayed.matchFormat.defaultEstimatedDuration * 60
let rotation = Int(interval) / matchDuration
print("numberOfRotation", interval, matchDuration, courtCount, rotation)
return (rotation, (rotation - 1) * lastMatchPlayed.courtCount() + courtCount + 1)
}
return nil
}
func insertOnServer() { func insertOnServer() {
self.tournamentStore?.teamRegistrations.writeChangeAndInsertOnServer(instance: self) self.tournamentStore?.teamRegistrations.writeChangeAndInsertOnServer(instance: self)

@ -125,9 +125,15 @@ final public class Tournament: BaseTournament {
public func groupStages(atStep step: Int = 0) -> [GroupStage] { public func groupStages(atStep step: Int = 0) -> [GroupStage] {
guard let tournamentStore = self.tournamentStore else { return [] } guard let tournamentStore = self.tournamentStore else { return [] }
let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step } let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.step == step }
return groupStages.sorted(by: \.index) return groupStages.sorted(by: \.index)
} }
public func hasGroupeStages() -> Bool {
if groupStageCount > 0 { return true }
guard let tournamentStore = self.tournamentStore else { return false }
return tournamentStore.groupStages.isEmpty == false
}
public func allGroupStages() -> [GroupStage] { public func allGroupStages() -> [GroupStage] {
guard let tournamentStore = self.tournamentStore else { return [] } guard let tournamentStore = self.tournamentStore else { return [] }
@ -271,17 +277,24 @@ defer {
return Store.main.findById(event) return Store.main.findById(event)
} }
public func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String { public func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> String {
let _selectedSortedTeams = selectedSortedTeams() let _selectedSortedTeams = selectedSortedTeams()
let selectedSortedTeams = _selectedSortedTeams + waitingListSortedTeams(selectedSortedTeams: _selectedSortedTeams) let selectedSortedTeams = _selectedSortedTeams + waitingListSortedTeams(selectedSortedTeams: _selectedSortedTeams)
switch exportFormat { switch exportFormat {
case .rawText: case .rawText:
return (selectedSortedTeams.compactMap { $0.pasteData(exportFormat) } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData(exportFormat) }).joined(separator: exportFormat.newLineSeparator(2)) let waitingList = waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true)
var stats = [String]()
if type == .payment, isAnimation(), minimumPlayerPerTeam == 1 {
stats += ["\(self.selectedPlayers().count.formatted()) personnes"]
} else {
stats += [selectedSortedTeams.count.formatted() + " équipes"]
}
return (stats + selectedSortedTeams.compactMap { $0.pasteData(exportFormat, type: type) } + (waitingList.isEmpty == false ? ["Liste d'attente"] : []) + waitingList.compactMap { $0.pasteData(exportFormat, type: type) }).joined(separator: exportFormat.newLineSeparator(1))
case .csv: case .csv:
let headers = ["", "Nom Prénom", "rang", "Nom Prénom", "rang", "poids", "Paire"].joined(separator: exportFormat.separator()) let headers = ["", "Nom Prénom", "rang", "Nom Prénom", "rang", "poids", "Paire"].joined(separator: exportFormat.separator())
var teamPaste = [headers] var teamPaste = [headers]
for (index, team) in selectedSortedTeams.enumerated() { for (index, team) in selectedSortedTeams.enumerated() {
var teamData = team.pasteData(exportFormat, index + 1) var teamData = team.pasteData(exportFormat, type: type, index + 1)
teamData.append(exportFormat.separator()) teamData.append(exportFormat.separator())
teamData.append(team.teamLastNames().joined(separator: " / ")) teamData.append(team.teamLastNames().joined(separator: " / "))
teamPaste.append(teamData) teamPaste.append(teamData)
@ -708,7 +721,7 @@ defer {
var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count
if groupStageTeamCount < 0 { groupStageTeamCount = 0 } if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
if bracketSeeds < 0 { bracketSeeds = 0 } if bracketSeeds < 0 { bracketSeeds = 0 }
let clubName = self.clubName
if prioritizeClubMembers { if prioritizeClubMembers {
var bracketTeams: [TeamRegistration] = [] var bracketTeams: [TeamRegistration] = []
@ -1209,14 +1222,7 @@ defer {
} }
public func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String { public func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle { startDate.formattedDate(displayStyle)
case .title:
startDate.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).year())
case .wide:
startDate.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted)
case .short:
startDate.formatted(date: .numeric, time: .omitted)
}
} }
public func qualifiedFromGroupStage() -> Int { public func qualifiedFromGroupStage() -> Int {
@ -1466,7 +1472,7 @@ defer {
var _groupStages = [GroupStage]() var _groupStages = [GroupStage]()
for index in 0..<groupStageCount { for index in 0..<groupStageCount {
let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, format: groupStageFormat) let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, format: groupStageSmartMatchFormat())
_groupStages.append(groupStage) _groupStages.append(groupStage)
} }
@ -1547,6 +1553,9 @@ defer {
public func deleteGroupStages() { public func deleteGroupStages() {
self.tournamentStore?.groupStages.delete(contentOfs: allGroupStages()) self.tournamentStore?.groupStages.delete(contentOfs: allGroupStages())
if let gs = self.groupStageLoserBracket() {
self.tournamentStore?.rounds.delete(instance: gs)
}
} }
public func refreshGroupStages(keepExistingMatches: Bool = false) { public func refreshGroupStages(keepExistingMatches: Bool = false) {
@ -1711,7 +1720,6 @@ defer {
public func groupStageSmartMatchFormat() -> MatchFormat { public func groupStageSmartMatchFormat() -> MatchFormat {
let format = tournamentLevel.federalFormatForGroupStage() let format = tournamentLevel.federalFormatForGroupStage()
if tournamentLevel == .p25 { return .superTie }
if format.rank < groupStageMatchFormat.rank { if format.rank < groupStageMatchFormat.rank {
return format return format
} else { } else {
@ -1849,7 +1857,6 @@ defer {
public func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { public func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
let format = tournamentLevel.federalFormatForBracketRound(roundIndex) let format = tournamentLevel.federalFormatForBracketRound(roundIndex)
if tournamentLevel == .p25 { return .superTie }
if format.rank < matchFormat.rank { if format.rank < matchFormat.rank {
return format return format
} else { } else {
@ -2116,7 +2123,18 @@ defer {
} }
} }
} }
public func removeRound(_ round: Round) async {
await MainActor.run {
let teams = round.seeds()
teams.forEach { team in
team.resetBracketPosition()
}
tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
tournamentStore?.rounds.delete(instance: round)
}
}
public func addNewRound(_ roundIndex: Int) async { public func addNewRound(_ roundIndex: Int) async {
await MainActor.run { await MainActor.run {
let round = Round(tournament: id, index: roundIndex, format: matchFormat) let round = Round(tournament: id, index: roundIndex, format: matchFormat)
@ -2302,7 +2320,11 @@ defer {
} }
public func onlineTeams() -> [TeamRegistration] { public func onlineTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.hasRegisteredOnline() }) // guard let teamRegistrations = tournamentStore?.teamRegistrations else { return [] }
// return teamRegistrations.cached(key: "online") { collection in
// collection.filter { $0.hasRegisteredOnline() }
// }
return unsortedTeams().filter({ $0.hasRegisteredOnline() })
} }
public func paidOnlineTeams() -> [TeamRegistration] { public func paidOnlineTeams() -> [TeamRegistration] {
@ -2337,7 +2359,7 @@ defer {
} }
public func mailSubject() -> String { public func mailSubject() -> String {
let subject = [tournamentTitle(hideSenior: true), formattedDate(.short), clubName].compactMap({ $0 }).joined(separator: " | ") let subject = [tournamentTitle(hideSenior: true), formattedDate(.short), customClubName ?? clubName].compactMap({ $0 }).joined(separator: " | ")
return subject return subject
} }
@ -2442,6 +2464,9 @@ defer {
} }
public func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int { public func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int {
if tournamentCategory != .men {
return 0
}
switch playerRank { switch playerRank {
case 0: return 0 case 0: return 0
case womanMax: return manMax - womanMax case womanMax: return manMax - womanMax
@ -2481,6 +2506,16 @@ defer {
self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds) self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds)
} }
public func formatSummary() -> String {
var label = [String]()
if groupStageCount > 0 {
label.append("Poules " + groupStageMatchFormat.format)
}
label.append("Tableau " + matchFormat.format)
label.append("Classement " + loserBracketMatchFormat.format)
return label.joined(separator: ", ")
}
// MARK: - // MARK: -
func insertOnServer() throws { func insertOnServer() throws {

@ -311,6 +311,18 @@ public extension Date {
let calendar = Calendar.current let calendar = Calendar.current
return calendar.date(bySetting: .minute, value: 0, of: self)!.withoutSeconds() return calendar.date(bySetting: .minute, value: 0, of: self)!.withoutSeconds()
} }
func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .title:
self.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).year())
case .wide:
self.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted)
case .short:
self.formatted(date: .numeric, time: .omitted)
}
}
} }
public extension Date { public extension Date {

@ -213,16 +213,96 @@ public extension String {
// MARK: - FFT Source Importing // MARK: - FFT Source Importing
public extension String { public extension String {
enum RegexStatic { enum RegexStatic {
static let mobileNumber = /^(?:\+33|0033|0)[6-7](?:[ .-]?[0-9]{2}){4}$/ // Patterns for France only
static let phoneNumber = /^(?:\+33|0033|0)[1-9](?:[ .-]?[0-9]{2}){4}$/ static let phoneNumber = /^(\+33|0033|33|0)[1-9][0-9]{8}$/
static let phoneNumberWithExtra0 = /^33[0][1-9][0-9]{8}$/
static let mobileNumber = /^(\+33|0033|33|0)[6-7][0-9]{8}$/
static let mobileNumberWithExtra0 = /^33[0][6-7][0-9]{8}$/
}
private func cleanedNumberForValidation() -> String {
// Keep leading '+' if present, remove all other non-digit characters
var cleaned = self.trimmingCharacters(in: .whitespacesAndNewlines)
if cleaned.hasPrefix("+") {
// Preserve '+' at start, remove all other non-digit characters
let digitsOnly = cleaned.dropFirst().components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
cleaned = "+" + digitsOnly
} else {
// Remove all non-digit characters
cleaned = cleaned.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
}
return cleaned
}
// MARK: - Phone Number Validation
/// Validate if the string is a mobile number for the specified locale.
/// - Parameter locale: The locale to validate against. Defaults to `.current`.
/// - Returns: True if the string matches the mobile number pattern for the locale.
func isMobileNumber(locale: Locale = .current) -> Bool {
// TODO: Support additional regions/locales in the future.
switch locale.region?.identifier {
case "FR", "fr", nil:
// French logic for now
let cleaned = cleanedNumberForValidation()
if cleaned.firstMatch(of: RegexStatic.mobileNumber) != nil {
return true
}
if cleaned.firstMatch(of: RegexStatic.mobileNumberWithExtra0) != nil {
return true
}
return false
default:
// For unsupported locales, fallback to checking if the string contains at least 8 digits
// This is a generic minimum length for most countries' phone numbers
let digitsOnly = self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
return digitsOnly.count >= 8
}
} }
func isMobileNumber() -> Bool { /// Validate if the string is a phone number for the specified locale.
firstMatch(of: RegexStatic.mobileNumber) != nil /// - Parameter locale: The locale to validate against. Defaults to `.current`.
/// - Returns: True if the string matches the phone number pattern for the locale.
func isPhoneNumber(locale: Locale = .current) -> Bool {
// TODO: Support additional regions/locales in the future.
switch locale.region?.identifier {
case "FR", "fr", nil:
// French logic for now
let cleaned = cleanedNumberForValidation()
if cleaned.firstMatch(of: RegexStatic.phoneNumber) != nil {
return true
}
if cleaned.firstMatch(of: RegexStatic.phoneNumberWithExtra0) != nil {
return true
}
return false
default:
// For unsupported locales, fallback to checking if the string contains at least 8 digits
// This is a generic minimum length for most countries' phone numbers
let digitsOnly = self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
return digitsOnly.count >= 8
}
}
func normalize(_ phone: String) -> String {
var normalized = phone.trimmingCharacters(in: .whitespacesAndNewlines)
// Remove all non-digit characters
normalized = normalized.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
// Remove leading country code for France (33) if present
if normalized.hasPrefix("33") {
if normalized.dropFirst(2).hasPrefix("0") {
// Keep as is, don't strip the zero after 33
} else {
normalized = "0" + normalized.dropFirst(2)
}
} else if normalized.hasPrefix("0033") {
normalized = "0" + normalized.dropFirst(4)
}
return normalized
} }
func isPhoneNumber() -> Bool { func isSamePhoneNumber(as other: String) -> Bool {
firstMatch(of: RegexStatic.phoneNumber) != nil return normalize(self) == normalize(other)
} }
func cleanSearchText() -> String { func cleanSearchText() -> String {
@ -312,3 +392,4 @@ public extension String {
return self // Return the original string if parsing fails return self // Return the original string if parsing fails
} }
} }

@ -55,6 +55,89 @@ public enum ContactManagerError: LocalizedError {
} }
} }
public enum SummonType: Int, Identifiable {
case contact
case contactWithoutSignature
case summon
case summonWalkoutFollowUp
case summonErrorFollowUp
public func isRecall() -> Bool {
switch self {
case .contact, .contactWithoutSignature:
return false
case .summon:
return false
case .summonWalkoutFollowUp:
return true
case .summonErrorFollowUp:
return true
}
}
public func mainWord() -> String {
switch self {
case .contact:
return "Contacter"
case .contactWithoutSignature:
return "Contacter"
case .summon:
return "Convoquer"
case .summonWalkoutFollowUp:
return "Reconvoquer"
case .summonErrorFollowUp:
return "Reconvoquer"
}
}
public func caption() -> String? {
switch self {
case .contact:
return nil
case .contactWithoutSignature:
return "Sans texte par défaut"
case .summon:
return nil
case .summonWalkoutFollowUp:
return "Suite à un forfait"
case .summonErrorFollowUp:
return "Suite à une erreur"
}
}
public func shouldConfirm() -> Bool {
switch self {
case .contact, .contactWithoutSignature:
return false
case .summon:
return true
case .summonWalkoutFollowUp:
return true
case .summonErrorFollowUp:
return true
}
}
public func intro() -> String {
switch self {
case .contactWithoutSignature:
return ""
case .contact:
return "Vous êtes"
case .summon:
return "Vous êtes"
case .summonWalkoutFollowUp:
return "Suite à des forfaits, vous êtes finalement"
case .summonErrorFollowUp:
return "Suite à une erreur, vous êtes finalement"
}
}
public var id: Int {
self.rawValue
}
}
public enum ContactType: Identifiable { public enum ContactType: Identifiable {
case mail(date: Date?, recipients: [String]?, bccRecipients: [String]?, body: String?, subject: String?, tournamentBuild: TournamentBuild?) case mail(date: Date?, recipients: [String]?, bccRecipients: [String]?, body: String?, subject: String?, tournamentBuild: TournamentBuild?)
case message(date: Date?, recipients: [String]?, body: String?, tournamentBuild: TournamentBuild?) case message(date: Date?, recipients: [String]?, body: String?, tournamentBuild: TournamentBuild?)
@ -76,7 +159,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
static func callingCustomMessage(source: String? = nil, tournament: Tournament?, startDate: Date?, roundLabel: String) -> String { static func callingCustomMessage(source: String? = nil, tournament: Tournament?, startDate: Date?, roundLabel: String) -> String {
let tournamentCustomMessage = source ?? DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage let tournamentCustomMessage = source ?? DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage
let clubName = tournament?.clubName ?? "" let clubName = tournament?.customClubName ?? tournament?.clubName ?? ""
var text = tournamentCustomMessage var text = tournamentCustomMessage
let date = startDate ?? tournament?.startDate ?? Date() let date = startDate ?? tournament?.startDate ?? Date()
@ -97,8 +180,10 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
return text return text
} }
static func callingMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?, reSummon: Bool = false) -> String { static func callingMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?, summonType: SummonType = .summon) -> String {
if summonType == .contactWithoutSignature {
return ""
}
let useFullCustomMessage = DataStore.shared.user.summonsUseFullCustomMessage let useFullCustomMessage = DataStore.shared.user.summonsUseFullCustomMessage
if useFullCustomMessage { if useFullCustomMessage {
@ -107,7 +192,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
let date = startDate ?? tournament?.startDate ?? Date() let date = startDate ?? tournament?.startDate ?? Date()
let clubName = tournament?.clubName ?? "" let clubName = tournament?.customClubName ?? tournament?.clubName ?? ""
let message = DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage let message = DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage
let signature = DataStore.shared.user.getSummonsMessageSignature() ?? DataStore.shared.user.defaultSignature(tournament) let signature = DataStore.shared.user.getSummonsMessageSignature() ?? DataStore.shared.user.defaultSignature(tournament)
@ -129,7 +214,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
[entryFeeMessage, message, linkMessage].compacted().map { $0.trimmedMultiline }.joined(separator: "\n\n") [entryFeeMessage, message, linkMessage].compacted().map { $0.trimmedMultiline }.joined(separator: "\n\n")
} }
let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes" let intro = summonType.intro()
if let tournament { if let tournament {
return "Bonjour,\n\n\(intro) \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle(.title, hideSenior: true)) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)" return "Bonjour,\n\n\(intro) \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle(.title, hideSenior: true)) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)"

@ -35,3 +35,23 @@ public enum ExportFormat: Int, Identifiable, CaseIterable {
return Array(repeating: "\n", count: count).joined() return Array(repeating: "\n", count: count).joined()
} }
} }
public enum ExportType: Int, Identifiable, CaseIterable {
public var id: Int { self.rawValue }
case sharing
case payment
public func localizedString() -> String {
switch self {
case .sharing:
return "inscriptions"
case .payment:
return "pointage"
}
}
public func newLineSeparator(_ count: Int = 1) -> String {
return Array(repeating: "\n", count: count).joined()
}
}

@ -349,7 +349,9 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
return 4 return 4
} else { } else {
switch level { switch level {
case .p25, .p100, .p250: case .p25:
return 4
case .p100, .p250:
if category == .women { if category == .women {
return 4 return 4
} }
@ -710,7 +712,7 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable
case 13...16: case 13...16:
return [175,150,138,113,100,88,75,63,53,45,38,25,13,8,3] return [175,150,138,113,100,88,75,63,53,45,38,25,13,8,3]
case 17...20: case 17...20:
return [188,163,150,138,123,113,100,88,75,63,58,50,45,38,30,25,13,8,3] return [188,163,150,138,125,113,100,88,75,63,58,50,45,38,30,25,13,8,3]
case 21...24: case 21...24:
return [188,175,163,150,138,125,118,108,100,93,83,75,70,63,58,50,45,38,30,25,13,8,3] return [188,175,163,150,138,125,118,108,100,93,83,75,70,63,58,50,45,38,30,25,13,8,3]
case 25...28: case 25...28:
@ -1199,6 +1201,7 @@ public enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable {
public enum SetFormat: Int, Hashable, Codable { public enum SetFormat: Int, Hashable, Codable {
case nine case nine
case four case four
case three
case six case six
case superTieBreak case superTieBreak
case megaTieBreak case megaTieBreak
@ -1225,6 +1228,10 @@ public enum SetFormat: Int, Hashable, Codable {
if teamOne == 5 || teamTwo == 5 { if teamOne == 5 || teamTwo == 5 {
return true return true
} }
case .three:
if teamOne == 4 || teamTwo == 4 {
return true
}
case .six: case .six:
if teamOne == 7 || teamTwo == 7 { if teamOne == 7 || teamTwo == 7 {
return true return true
@ -1243,6 +1250,8 @@ public enum SetFormat: Int, Hashable, Codable {
return 8 return 8
case .four: case .four:
return 4 return 4
case .three:
return 3
case .six: case .six:
return 6 return 6
case .superTieBreak, .megaTieBreak: case .superTieBreak, .megaTieBreak:
@ -1260,6 +1269,10 @@ public enum SetFormat: Int, Hashable, Codable {
if teamOneScore == 4 { if teamOneScore == 4 {
return [] return []
} }
case .three:
if teamOneScore == 3 {
return []
}
case .six: case .six:
if teamOneScore == 6 { if teamOneScore == 6 {
return [] return []
@ -1282,6 +1295,8 @@ public enum SetFormat: Int, Hashable, Codable {
return [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] return [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
case .four: case .four:
return [5, 4, 3, 2, 1, 0] return [5, 4, 3, 2, 1, 0]
case .three:
return [4, 3, 2, 1, 0]
case .six: case .six:
return [7, 6, 5, 4, 3, 2, 1, 0] return [7, 6, 5, 4, 3, 2, 1, 0]
case .superTieBreak: case .superTieBreak:
@ -1297,6 +1312,8 @@ public enum SetFormat: Int, Hashable, Codable {
return 9 return 9
case .four: case .four:
return 4 return 4
case .three:
return 4
case .six: case .six:
return 6 return 6
case .superTieBreak: case .superTieBreak:
@ -1364,15 +1381,15 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
return 2 return 2
case .nineGames, .nineGamesDecisivePoint: case .nineGames, .nineGamesDecisivePoint:
return 3 return 3
case .superTie: case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 4 return 4
case .megaTie: case .superTie:
return 5 return 5
case .twoSetsOfSuperTie: case .megaTie:
return 6 return 6
case .singleSet, .singleSetDecisivePoint: case .twoSetsOfSuperTie:
return 7 return 7
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: case .singleSet, .singleSetDecisivePoint:
return 8 return 8
} }
} }
@ -1395,15 +1412,15 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
return 3 return 3
case .nineGamesDecisivePoint: case .nineGamesDecisivePoint:
return 3 return 3
case .superTie: case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 4 return 4
case .megaTie: case .superTie:
return 5 return 5
case .twoSetsOfSuperTie: case .megaTie:
return 6 return 6
case .singleSet, .singleSetDecisivePoint: case .twoSetsOfSuperTie:
return 7 return 7
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: case .singleSet, .singleSetDecisivePoint:
return 8 return 8
} }
} }
@ -1641,7 +1658,7 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
case .megaTie: case .megaTie:
return "supertie de 15 points" return "supertie de 15 points"
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return "1 set de 4 jeux, tiebreak à 4/4" return "1 set de 4 jeux, tiebreak à 3/3"
} }
} }
@ -1670,8 +1687,10 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
switch self { switch self {
case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSet, .singleSetDecisivePoint: case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSet, .singleSetDecisivePoint:
return .six return .six
case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint:
return .four return .four
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return .three
case .nineGames, .nineGamesDecisivePoint: case .nineGames, .nineGamesDecisivePoint:
return .nine return .nine
case .superTie, .twoSetsOfSuperTie: case .superTie, .twoSetsOfSuperTie:
@ -1850,6 +1869,16 @@ public enum RoundRule {
} }
} }
public static func cumulatedNumberOfMatches(forTeams teams: Int) -> Int {
var i = teams / 2
var loserTeams = teams / 2
while loserTeams > 1 {
i += Self.cumulatedNumberOfMatches(forTeams: loserTeams)
loserTeams = loserTeams / 2
}
return i
}
public static func teamsInFirstRound(forTeams teams: Int) -> Int { public static func teamsInFirstRound(forTeams teams: Int) -> Int {
Int(pow(2.0, ceil(log2(Double(teams))))) Int(pow(2.0, ceil(log2(Double(teams)))))
} }
@ -1880,12 +1909,16 @@ public enum RoundRule {
} }
public static func numberOfMatches(forRoundIndex roundIndex: Int) -> Int { public static func numberOfMatches(forRoundIndex roundIndex: Int) -> Int {
Int(pow(2.0, Double(roundIndex))) (1 << roundIndex)
}
public static func baseIndex(forRoundIndex roundIndex: Int) -> Int {
numberOfMatches(forRoundIndex: roundIndex) - 1
} }
static func matchIndexWithinRound(fromMatchIndex matchIndex: Int) -> Int { static func matchIndexWithinRound(fromMatchIndex matchIndex: Int) -> Int {
let roundIndex = roundIndex(fromMatchIndex: matchIndex) let roundIndex = roundIndex(fromMatchIndex: matchIndex)
let matchIndexWithinRound = matchIndex - (Int(pow(2.0, Double(roundIndex))) - 1) let matchIndexWithinRound = matchIndex - baseIndex(forRoundIndex: roundIndex)
return matchIndexWithinRound return matchIndexWithinRound
} }
@ -1909,7 +1942,7 @@ public enum RoundRule {
} }
return "Quart de finale" return "Quart de finale"
default: default:
return "\(Int(pow(2.0, Double(roundIndex))))ème" return "\((1 << roundIndex))ème"
} }
} }
} }

Loading…
Cancel
Save