fix issue with search

sync_v2
Raz 8 months ago
parent b27cdfa1e6
commit f83cb5f251
  1. 493
      PadelClub/ViewModel/SearchViewModel.swift
  2. 4
      PadelClub/Views/Tournament/Screen/AddTeamView.swift

@ -14,13 +14,13 @@ class DebouncableViewModel: ObservableObject {
class SearchViewModel: ObservableObject, Identifiable { class SearchViewModel: ObservableObject, Identifiable {
let id: UUID = UUID() let id: UUID = UUID()
var allowSelection : Int = 0 var allowSelection: Int = 0
var codeClub: String? = nil var codeClub: String? = nil
var clubName: String? = nil var clubName: String? = nil
var ligueName: String? = nil var ligueName: String? = nil
var showFemaleInMaleAssimilation: Bool = false var showFemaleInMaleAssimilation: Bool = false
var hidePlayers: [String]? var hidePlayers: [String]?
@Published var debouncableText: String = "" @Published var debouncableText: String = ""
@Published var searchText: String = "" @Published var searchText: String = ""
@Published var task: DispatchWorkItem? @Published var task: DispatchWorkItem?
@ -37,91 +37,103 @@ class SearchViewModel: ObservableObject, Identifiable {
@Published var isPresented: Bool = false @Published var isPresented: Bool = false
@Published var selectedAgeCategory: FederalTournamentAge = .unlisted @Published var selectedAgeCategory: FederalTournamentAge = .unlisted
@Published var mostRecentDate: Date? = nil @Published var mostRecentDate: Date? = nil
var selectionIsOver: Bool { var selectionIsOver: Bool {
if allowSingleSelection && selectedPlayers.count == 1 { if allowSingleSelection && selectedPlayers.count == 1 {
return true return true
} else if allowMultipleSelection && selectedPlayers.count == allowSelection { } else if allowMultipleSelection && selectedPlayers.count == allowSelection {
return true return true
} }
return false return false
} }
var allowMultipleSelection: Bool { var allowMultipleSelection: Bool {
allowSelection > 1 || allowSelection == -1 allowSelection > 1 || allowSelection == -1
} }
var allowSingleSelection: Bool { var allowSingleSelection: Bool {
allowSelection == 1 allowSelection == 1
} }
var debounceTrigger: Double { var debounceTrigger: Double {
(dataSet == .national || dataSet == .ligue) ? 0.4 : 0.1 (dataSet == .national || dataSet == .ligue) ? 0.4 : 0.1
} }
var throttleTrigger: Double { var throttleTrigger: Double {
(dataSet == .national || dataSet == .ligue) ? 0.15 : 0.1 (dataSet == .national || dataSet == .ligue) ? 0.15 : 0.1
} }
var contentUnavailableMessage: String { var contentUnavailableMessage: String {
var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."] var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."]
if tokens.isEmpty { if tokens.isEmpty {
message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois, dans ce cas, Padel Club ne pourra pas le trouver.") message.append(
"Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois, dans ce cas, Padel Club ne pourra pas le trouver."
)
} }
return message.joined(separator: "\n") return message.joined(separator: "\n")
} }
func codeClubs() -> [String] { func codeClubs() -> [String] {
let clubs: [Club] = DataStore.shared.user.clubsObjects() let clubs: [Club] = DataStore.shared.user.clubsObjects()
return clubs.compactMap { $0.code } return clubs.compactMap { $0.code }
} }
func getCodeClub() -> String? { func getCodeClub() -> String? {
if let codeClub { return codeClub } if let codeClub { return codeClub }
if let userCodeClub = DataStore.shared.user.currentPlayerData()?.clubCode { return userCodeClub } if let userCodeClub = DataStore.shared.user.currentPlayerData()?.clubCode {
return userCodeClub
}
return nil return nil
} }
func getLigueName() -> String? { func getLigueName() -> String? {
if let ligueName { return ligueName } if let ligueName { return ligueName }
if let userLigueName = DataStore.shared.user.currentPlayerData()?.ligueName { return userLigueName } if let userLigueName = DataStore.shared.user.currentPlayerData()?.ligueName {
return userLigueName
}
return nil return nil
} }
func shouldIncludeSearchTextPredicate() -> Bool { func shouldIncludeSearchTextPredicate() -> Bool {
if allowMultipleSelection { if allowMultipleSelection {
return true return true
} }
if allowSingleSelection { if allowSingleSelection {
return true return true
} }
if tokens.isEmpty == false || hideAssimilation || selectedAgeCategory != .unlisted { if tokens.isEmpty == false || hideAssimilation || selectedAgeCategory != .unlisted {
return true return true
} }
return dataSet == .national && searchText.isEmpty == false && (tokens.isEmpty == true && hideAssimilation == false && selectedAgeCategory == .unlisted) return dataSet == .national && searchText.isEmpty == false
&& (tokens.isEmpty == true && hideAssimilation == false
&& selectedAgeCategory == .unlisted)
} }
func showIndex() -> Bool { func showIndex() -> Bool {
if dataSet == .national { if dataSet == .national {
if searchText.isEmpty == false && (tokens.isEmpty == true && hideAssimilation == false && selectedAgeCategory == .unlisted) { if searchText.isEmpty == false
&& (tokens.isEmpty == true && hideAssimilation == false
&& selectedAgeCategory == .unlisted)
{
return false return false
} else { } else {
return isFiltering() return isFiltering()
} }
} }
if (dataSet == .ligue) { return isFiltering() } if dataSet == .ligue { return isFiltering() }
if filterOption == .all { return isFiltering() } if filterOption == .all { return isFiltering() }
return true return true
} }
func isFiltering() -> Bool { func isFiltering() -> Bool {
searchText.isEmpty == false || tokens.isEmpty == false || hideAssimilation || selectedAgeCategory != .unlisted searchText.isEmpty == false || tokens.isEmpty == false || hideAssimilation
|| selectedAgeCategory != .unlisted
} }
func prompt(forDataSet: DataSet) -> String { func prompt(forDataSet: DataSet) -> String {
switch forDataSet { switch forDataSet {
case .national: case .national:
@ -138,7 +150,7 @@ class SearchViewModel: ObservableObject, Identifiable {
return "dans mes favoris" return "dans mes favoris"
} }
} }
func label(forDataSet: DataSet) -> String { func label(forDataSet: DataSet) -> String {
switch forDataSet { switch forDataSet {
case .national: case .national:
@ -155,102 +167,201 @@ class SearchViewModel: ObservableObject, Identifiable {
} }
func words() -> [String] { func words() -> [String] {
return searchText.canonicalVersionWithPunctuation.trimmed.components(separatedBy: .whitespaces) return searchText.canonicalVersionWithPunctuation.trimmed.components(
separatedBy: .whitespaces)
} }
func wordsPredicates() -> NSPredicate? { func wordsPredicates() -> NSPredicate? {
let words = words().filter({ $0.isEmpty == false }) let words = words().filter({ $0.isEmpty == false })
// Handle special case of hyphenated words
let hyphenatedWords = searchText.components(separatedBy: .whitespaces)
.filter { $0.contains("-") }
var predicates: [NSPredicate] = []
// Add predicates for hyphenated words
for word in hyphenatedWords {
predicates.append(NSPredicate(format: "lastName CONTAINS[cd] %@", word))
let parts = word.components(separatedBy: "-")
for part in parts where part.count > 1 {
predicates.append(NSPredicate(format: "lastName CONTAINS[cd] %@", part))
}
}
// Regular words processing
switch words.count { switch words.count {
case 2: case 2:
let predicates = [ predicates.append(contentsOf: [
NSPredicate(format: "canonicalLastName beginswith[cd] %@ AND canonicalFirstName beginswith[cd] %@", words[0], words[1]), NSPredicate(
NSPredicate(format: "canonicalLastName beginswith[cd] %@ AND canonicalFirstName beginswith[cd] %@", words[1], words[0]), format:
] "canonicalLastName CONTAINS[cd] %@ AND canonicalFirstName CONTAINS[cd] %@",
return NSCompoundPredicate(orPredicateWithSubpredicates: predicates) words[0], words[1]),
NSPredicate(
format:
"canonicalLastName CONTAINS[cd] %@ AND canonicalFirstName CONTAINS[cd] %@",
words[1], words[0]),
// For multi-word first names, try the two words as a first name
NSPredicate(
format: "canonicalFirstName CONTAINS[cd] %@", words.joined(separator: " ")),
])
case 3:
// Handle potential cases like "Jean Christophe CROS"
predicates.append(contentsOf: [
// First two words as first name, last as last name
NSPredicate(
format:
"canonicalFirstName CONTAINS[cd] %@ AND canonicalLastName CONTAINS[cd] %@",
words[0] + " " + words[1], words[2]),
// First as first name, last two as last name
NSPredicate(
format:
"canonicalFirstName CONTAINS[cd] %@ AND canonicalLastName CONTAINS[cd] %@",
words[0], words[1] + " " + words[2]),
// Last as first name, first two as last name
NSPredicate(
format:
"canonicalFirstName CONTAINS[cd] %@ AND canonicalLastName CONTAINS[cd] %@",
words[2], words[0] + " " + words[1]),
])
default: default:
return nil if words.count > 0 {
// For single word or many words, try matching against full name
predicates.append(
NSPredicate(
format: "canonicalFullName CONTAINS[cd] %@",
words.joined(separator: " ")))
}
} }
return predicates.isEmpty
? nil : NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
} }
func searchTextPredicate() -> NSPredicate? { func searchTextPredicate() -> NSPredicate? {
var predicates : [NSPredicate] = [] var predicates: [NSPredicate] = []
let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces).union(
let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed CharacterSet(charactersIn: "-"))
let canonicalVersionWithoutPunctuation = searchText.canonicalVersion
.components(separatedBy: allowedCharacterSet.inverted)
.joined()
.trimmed
if canonicalVersionWithoutPunctuation.isEmpty == false { if canonicalVersionWithoutPunctuation.isEmpty == false {
let wordsPredicates = wordsPredicates() let wordsPredicates = wordsPredicates()
if let wordsPredicates { if let wordsPredicates {
predicates.append(wordsPredicates) predicates.append(wordsPredicates)
} else { } else {
predicates.append(NSPredicate(format: "license contains[cd] %@", canonicalVersionWithoutPunctuation)) predicates.append(
NSPredicate(
format: "license contains[cd] %@", canonicalVersionWithoutPunctuation))
} }
predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation))
// Add match for full name
predicates.append(
NSPredicate(
format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation)
)
// Add pattern match for more flexible matching
let components = canonicalVersionWithoutPunctuation.split(separator: " ") let components = canonicalVersionWithoutPunctuation.split(separator: " ")
let pattern = components.joined(separator: ".*") let pattern = components.joined(separator: ".*")
let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern) predicates.append(NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern))
predicates.append(predicate)
// Look for exact matches on first or last name
let words = canonicalVersionWithoutPunctuation.components(separatedBy: .whitespaces)
for word in words where word.count > 2 {
predicates.append(
NSPredicate(
format: "firstName CONTAINS[cd] %@ OR lastName CONTAINS[cd] %@", word, word)
)
}
} }
if predicates.isEmpty { if predicates.isEmpty {
return nil return nil
} }
return NSCompoundPredicate(orPredicateWithSubpredicates: predicates) return NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
} }
func orPredicate() -> NSPredicate? { func orPredicate() -> NSPredicate? {
var predicates : [NSPredicate] = [] var predicates: [NSPredicate] = []
let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces).union(
let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed CharacterSet(charactersIn: "-"))
let canonicalVersionWithoutPunctuation = searchText.canonicalVersion
.components(separatedBy: allowedCharacterSet.inverted)
.joined()
.trimmed
let canonicalVersionWithPunctuation = searchText.canonicalVersionWithPunctuation.trimmed let canonicalVersionWithPunctuation = searchText.canonicalVersionWithPunctuation.trimmed
if tokens.isEmpty { if tokens.isEmpty {
if shouldIncludeSearchTextPredicate(), canonicalVersionWithoutPunctuation.isEmpty == false { if shouldIncludeSearchTextPredicate(),
canonicalVersionWithoutPunctuation.isEmpty == false
{
if let searchTextPredicate = searchTextPredicate() { if let searchTextPredicate = searchTextPredicate() {
predicates.append(searchTextPredicate) predicates.append(searchTextPredicate)
} }
} }
} }
// Process tokens
for token in tokens { for token in tokens {
switch token { switch token {
case .ligue: case .ligue:
if canonicalVersionWithoutPunctuation.isEmpty { if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "ligueName == nil")) predicates.append(NSPredicate(format: "ligueName == nil"))
} else { } else {
predicates.append(NSPredicate(format: "ligueName contains[cd] %@", canonicalVersionWithoutPunctuation)) predicates.append(
NSPredicate(
format: "ligueName contains[cd] %@", canonicalVersionWithoutPunctuation)
)
} }
case .club: case .club:
if canonicalVersionWithoutPunctuation.isEmpty { if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "clubName == nil")) predicates.append(NSPredicate(format: "clubName == nil"))
} else { } else {
predicates.append(NSPredicate(format: "clubName contains[cd] %@", canonicalVersionWithoutPunctuation)) predicates.append(
NSPredicate(
format: "clubName contains[cd] %@", canonicalVersionWithoutPunctuation))
} }
case .rankMoreThan: case .rankMoreThan:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { if canonicalVersionWithoutPunctuation.isEmpty
|| Int(canonicalVersionWithoutPunctuation) == 0
{
predicates.append(NSPredicate(format: "rank == 0")) predicates.append(NSPredicate(format: "rank == 0"))
} else { } else {
predicates.append(NSPredicate(format: "rank >= %@", canonicalVersionWithoutPunctuation)) predicates.append(
NSPredicate(format: "rank >= %@", canonicalVersionWithoutPunctuation))
} }
case .rankLessThan: case .rankLessThan:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { if canonicalVersionWithoutPunctuation.isEmpty
|| Int(canonicalVersionWithoutPunctuation) == 0
{
predicates.append(NSPredicate(format: "rank == 0")) predicates.append(NSPredicate(format: "rank == 0"))
} else { } else {
predicates.append(NSPredicate(format: "rank <= %@", canonicalVersionWithoutPunctuation)) predicates.append(
NSPredicate(format: "rank <= %@", canonicalVersionWithoutPunctuation))
} }
case .rankBetween: case .rankBetween:
let values = canonicalVersionWithPunctuation.components(separatedBy: ",") let values = canonicalVersionWithPunctuation.components(separatedBy: ",")
if canonicalVersionWithPunctuation.isEmpty || values.count != 2 { if canonicalVersionWithPunctuation.isEmpty || values.count != 2 {
predicates.append(NSPredicate(format: "rank == 0")) predicates.append(NSPredicate(format: "rank == 0"))
} else { } else {
predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!)) predicates.append(
NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!))
} }
case .age: case .age:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 { if canonicalVersionWithoutPunctuation.isEmpty
|| Int(canonicalVersionWithoutPunctuation) == 0
{
predicates.append(NSPredicate(format: "birthYear == 0")) predicates.append(NSPredicate(format: "birthYear == 0"))
} else if let birthYear = Int(canonicalVersionWithoutPunctuation) { } else if let birthYear = Int(canonicalVersionWithoutPunctuation) {
predicates.append(NSPredicate(format: "birthYear == %@", birthYear.formattedAsRawString())) predicates.append(
NSPredicate(format: "birthYear == %@", birthYear.formattedAsRawString()))
} }
} }
} }
if predicates.isEmpty { if predicates.isEmpty {
return nil return nil
} }
@ -258,35 +369,33 @@ class SearchViewModel: ObservableObject, Identifiable {
} }
func predicate() -> NSPredicate? { func predicate() -> NSPredicate? {
var predicates : [NSPredicate?] = [ var predicates: [NSPredicate?] = [
orPredicate(), orPredicate(),
filterOption == .male ? filterOption == .male ? NSPredicate(format: "male == YES") : nil,
NSPredicate(format: "male == YES") : filterOption == .female ? NSPredicate(format: "male == NO") : nil,
nil,
filterOption == .female ?
NSPredicate(format: "male == NO") :
nil,
] ]
if let mostRecentDate { if let mostRecentDate {
predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
} }
if hideAssimilation { if hideAssimilation {
predicates.append(NSPredicate(format: "assimilation == %@", "Non")) predicates.append(NSPredicate(format: "assimilation == %@", "Non"))
} }
if selectedAgeCategory != .unlisted { if selectedAgeCategory != .unlisted {
let computedBirthYear = selectedAgeCategory.computedBirthYear() let computedBirthYear = selectedAgeCategory.computedBirthYear()
if let left = computedBirthYear.0 { if let left = computedBirthYear.0 {
predicates.append(NSPredicate(format: "birthYear >= %@", left.formattedAsRawString())) predicates.append(
NSPredicate(format: "birthYear >= %@", left.formattedAsRawString()))
} }
if let right = computedBirthYear.1 { if let right = computedBirthYear.1 {
predicates.append(NSPredicate(format: "birthYear <= %@", right.formattedAsRawString())) predicates.append(
NSPredicate(format: "birthYear <= %@", right.formattedAsRawString()))
} }
} }
switch dataSet { switch dataSet {
case .national: case .national:
break break
@ -312,23 +421,22 @@ class SearchViewModel: ObservableObject, Identifiable {
if hidePlayers?.isEmpty == false { if hidePlayers?.isEmpty == false {
predicates.append(NSPredicate(format: "NOT (license IN %@)", hidePlayers!)) predicates.append(NSPredicate(format: "NOT (license IN %@)", hidePlayers!))
} }
return NSCompoundPredicate(andPredicateWithSubpredicates: predicates.compactMap({ $0 })) return NSCompoundPredicate(andPredicateWithSubpredicates: predicates.compactMap({ $0 }))
} }
func sortDescriptors() -> [SortDescriptor<ImportedPlayer>] { func sortDescriptors() -> [SortDescriptor<ImportedPlayer>] {
sortOption.sortDescriptors(ascending, dataSet: dataSet) sortOption.sortDescriptors(ascending, dataSet: dataSet)
} }
func nsSortDescriptors() -> [NSSortDescriptor] { func nsSortDescriptors() -> [NSSortDescriptor] {
sortDescriptors().map { NSSortDescriptor($0) } sortDescriptors().map { NSSortDescriptor($0) }
} }
static func getSpecialSlashPredicate(inputString: String) -> NSPredicate? { static func getSpecialSlashPredicate(inputString: String) -> NSPredicate? {
// Define a regular expression to find slashes between alphabetic characters (not digits) // Define a regular expression to find slashes between alphabetic characters (not digits)
print(inputString) print(inputString)
let pattern = /(\b[A-Za-z]+)\s*\/\s*([A-Za-z]+\b)/ let pattern = /(\b[A-Za-z]+)\s*\/\s*([A-Za-z]+\b)/
// Find matches in the input string // Find matches in the input string
@ -336,7 +444,7 @@ class SearchViewModel: ObservableObject, Identifiable {
print("No valid name pairs found") print("No valid name pairs found")
return nil return nil
} }
let lastName1 = match.output.1.trimmingCharacters(in: .whitespacesAndNewlines) let lastName1 = match.output.1.trimmingCharacters(in: .whitespacesAndNewlines)
let lastName2 = match.output.2.trimmingCharacters(in: .whitespacesAndNewlines) let lastName2 = match.output.2.trimmingCharacters(in: .whitespacesAndNewlines)
@ -345,20 +453,23 @@ class SearchViewModel: ObservableObject, Identifiable {
print("One or both names are empty") print("One or both names are empty")
return nil return nil
} }
// Create the NSPredicate for searching in the `lastName` field // Create the NSPredicate for searching in the `lastName` field
let predicate = NSPredicate(format: "lastName CONTAINS[cd] %@ OR lastName CONTAINS[cd] %@", lastName1, lastName2) let predicate = NSPredicate(
format: "lastName CONTAINS[cd] %@ OR lastName CONTAINS[cd] %@", lastName1, lastName2)
// Output the result // Output the result
//print("Generated Predicate: \(predicate)") //print("Generated Predicate: \(predicate)")
return predicate return predicate
} }
static func pastePredicate(
static func pastePredicate(pasteField: String, mostRecentDate: Date?, filterOption: PlayerFilterOption) -> NSPredicate? { pasteField: String, mostRecentDate: Date?, filterOption: PlayerFilterOption
) -> NSPredicate? {
var andPredicates = [NSPredicate]() var andPredicates = [NSPredicate]()
var orPredicates = [NSPredicate]() var orPredicates = [NSPredicate]()
// Check for license numbers
let matches = pasteField.licencesFound() let matches = pasteField.licencesFound()
let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) } let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) }
orPredicates = licensesPredicates orPredicates = licensesPredicates
@ -367,59 +478,110 @@ class SearchViewModel: ObservableObject, Identifiable {
return NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates) return NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates)
} }
let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces) // Add gender filter if specified
// Remove all characters that are not in the allowedCharacterSet
var text = pasteField.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmedMultiline
// Define the regex pattern to match digits
let digitPattern = /\b\w*\d\w*\b/
// Replace all occurrences of the pattern (digits) with an empty string
text = text.replacing(digitPattern, with: "").trimmingCharacters(in: .whitespacesAndNewlines)
let textStrings: [String] = text.components(separatedBy: .whitespacesAndNewlines)
let nonEmptyStrings: [String] = textStrings.compactMap { $0.isEmpty ? nil : $0 }
let nameComponents = nonEmptyStrings.filter({ $0 != "de" && $0 != "la" && $0 != "le" && $0.count > 1 })
//self.wordsCount = nameComponents.count
if filterOption == .female { if filterOption == .female {
andPredicates.append(NSPredicate(format: "male == NO")) andPredicates.append(NSPredicate(format: "male == NO"))
} else if filterOption == .male {
andPredicates.append(NSPredicate(format: "male == YES"))
} }
// Add date filter if specified
if let mostRecentDate { if let mostRecentDate {
andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
} }
// Check for slashes (representing alternatives)
if let slashPredicate = getSpecialSlashPredicate(inputString: pasteField) { if let slashPredicate = getSpecialSlashPredicate(inputString: pasteField) {
orPredicates.append(slashPredicate) orPredicates.append(slashPredicate)
} }
print("nameComponents", nameComponents.count) // Prepare text for processing - preserve hyphens but remove digits
var text =
if nameComponents.count < 50 { pasteField
if nameComponents.count > 1 { .replacingOccurrences(of: "/", with: " ") // Replace slashes with spaces
orPredicates.append(contentsOf: nameComponents.pairs().map { .trimmingCharacters(in: .whitespacesAndNewlines)
return NSPredicate(format: "(firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@) OR (firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@)", $0, $1, $1, $0) })
} else { // Remove digits
orPredicates.append(contentsOf: nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) }) let digitPattern = /\b\w*\d\w*\b/
text = text.replacing(digitPattern, with: "").trimmingCharacters(
in: .whitespacesAndNewlines)
// Split text by whitespace to get potential name components
let textComponents = text.components(separatedBy: .whitespacesAndNewlines)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter {
!$0.isEmpty && $0.count > 1 && !["de", "la", "le", "du"].contains($0.lowercased())
}
if textComponents.count < 50 {
// Handle exact fullname match
let fullName = textComponents.joined(separator: " ")
if !fullName.isEmpty {
orPredicates.append(
NSPredicate(format: "canonicalFullName CONTAINS[cd] %@", fullName))
} }
// Handle hyphenated last names
let hyphenatedComponents = textComponents.filter { $0.contains("-") }
for component in hyphenatedComponents {
orPredicates.append(NSPredicate(format: "lastName CONTAINS[cd] %@", component))
// Also search for each part of the hyphenated name
let parts = component.components(separatedBy: "-")
for part in parts {
if part.count > 1 {
orPredicates.append(NSPredicate(format: "lastName CONTAINS[cd] %@", part))
}
}
}
// Try different combinations for first/last name
if textComponents.count > 1 {
// Try each pair of components as first+last and last+first
for i in 0..<textComponents.count {
for j in 0..<textComponents.count where i != j {
orPredicates.append(
NSPredicate(
format:
"(firstName CONTAINS[cd] %@ AND lastName CONTAINS[cd] %@) OR (firstName CONTAINS[cd] %@ AND lastName CONTAINS[cd] %@)",
textComponents[i], textComponents[j], textComponents[j],
textComponents[i]
))
// Also try beginswith for more precise matches
orPredicates.append(
NSPredicate(
format:
"(firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@) OR (firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@)",
textComponents[i], textComponents[j], textComponents[j],
textComponents[i]
))
}
}
} else if textComponents.count == 1 {
// If only one component, search in both first and last name
orPredicates.append(
NSPredicate(
format: "firstName CONTAINS[cd] %@ OR lastName CONTAINS[cd] %@",
textComponents[0], textComponents[0]))
}
// Add pattern match for canonical full name
let pattern = textComponents.joined(separator: ".*")
orPredicates.append(NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern))
} }
let components = text.split(separator: " ")
let pattern = components.joined(separator: ".*")
print(text, pattern)
let canonicalFullNamePredicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern)
orPredicates.append(canonicalFullNamePredicate)
// Construct final predicate
var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates) var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates)
if orPredicates.isEmpty == false { if !orPredicates.isEmpty {
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates)]) let orCompoundPredicate = NSCompoundPredicate(
orPredicateWithSubpredicates: orPredicates)
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
predicate, orCompoundPredicate,
])
} }
print(predicate)
return predicate return predicate
} }
@ -432,28 +594,33 @@ enum SearchToken: String, CaseIterable, Identifiable {
case rankLessThan = "rang <" case rankLessThan = "rang <"
case rankBetween = "rang <>" case rankBetween = "rang <>"
case age = "âge sportif" case age = "âge sportif"
var id: String { var id: String {
rawValue rawValue
} }
var message: String { var message: String {
switch self { switch self {
case .club: case .club:
return "Taper le nom d'un club pour y voir tous les joueurs ayant déjà joué un tournoi dans les 12 derniers mois." return
"Taper le nom d'un club pour y voir tous les joueurs ayant déjà joué un tournoi dans les 12 derniers mois."
case .ligue: case .ligue:
return "Taper le nom d'une ligue pour y voir tous les joueurs ayant déjà joué un tournoi dans les 12 derniers mois." return
"Taper le nom d'une ligue pour y voir tous les joueurs ayant déjà joué un tournoi dans les 12 derniers mois."
case .rankMoreThan: case .rankMoreThan:
return "Taper un nombre pour chercher les joueurs ayant un classement supérieur ou égale." return
"Taper un nombre pour chercher les joueurs ayant un classement supérieur ou égale."
case .rankLessThan: case .rankLessThan:
return "Taper un nombre pour chercher les joueurs ayant un classement inférieur ou égale." return
"Taper un nombre pour chercher les joueurs ayant un classement inférieur ou égale."
case .rankBetween: case .rankBetween:
return "Taper deux nombres séparés par une virgule pour chercher les joueurs dans cette intervalle de classement" return
"Taper deux nombres séparés par une virgule pour chercher les joueurs dans cette intervalle de classement"
case .age: case .age:
return "Taper une année de naissance" return "Taper une année de naissance"
} }
} }
var titleLabel: String { var titleLabel: String {
switch self { switch self {
case .club: case .club:
@ -468,7 +635,7 @@ enum SearchToken: String, CaseIterable, Identifiable {
return "Chercher une année de naissance" return "Chercher une année de naissance"
} }
} }
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self { switch self {
case .club: case .club:
@ -485,7 +652,7 @@ enum SearchToken: String, CaseIterable, Identifiable {
return "Année de naissance" return "Année de naissance"
} }
} }
var shortLocalizedLabel: String { var shortLocalizedLabel: String {
switch self { switch self {
case .club: case .club:
@ -502,7 +669,7 @@ enum SearchToken: String, CaseIterable, Identifiable {
return "Né(e) en" return "Né(e) en"
} }
} }
func icon() -> String { func icon() -> String {
switch self { switch self {
case .club: case .club:
@ -519,7 +686,7 @@ enum SearchToken: String, CaseIterable, Identifiable {
return "figure.racquetball" return "figure.racquetball"
} }
} }
var systemImage: String { var systemImage: String {
switch self { switch self {
case .club: case .club:
@ -544,9 +711,9 @@ enum DataSet: Int, Identifiable {
case club case club
case favoriteClubs case favoriteClubs
case favoritePlayers case favoritePlayers
static let allCases : [DataSet] = [.national, .ligue, .club, .favoriteClubs] static let allCases: [DataSet] = [.national, .ligue, .club, .favoriteClubs]
var id: Int { rawValue } var id: Int { rawValue }
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self { switch self {
@ -560,9 +727,9 @@ enum DataSet: Int, Identifiable {
return "Favori" return "Favori"
} }
} }
var tokens: [SearchToken] { var tokens: [SearchToken] {
var _tokens : [SearchToken] = [] var _tokens: [SearchToken] = []
switch self { switch self {
case .national: case .national:
_tokens = [.club, .ligue, .rankMoreThan, .rankLessThan, .rankBetween] _tokens = [.club, .ligue, .rankMoreThan, .rankLessThan, .rankBetween]
@ -573,7 +740,7 @@ enum DataSet: Int, Identifiable {
case .favoritePlayers, .favoriteClubs: case .favoritePlayers, .favoriteClubs:
_tokens = [.rankMoreThan, .rankLessThan, .rankBetween] _tokens = [.rankMoreThan, .rankLessThan, .rankBetween]
} }
_tokens.append(.age) _tokens.append(.age)
return _tokens return _tokens
} }
@ -585,7 +752,7 @@ enum SortOption: Int, CaseIterable, Identifiable {
case tournamentCount case tournamentCount
case points case points
case progression case progression
var id: Int { self.rawValue } var id: Int { self.rawValue }
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self { switch self {
@ -601,23 +768,45 @@ enum SortOption: Int, CaseIterable, Identifiable {
return "Progression" return "Progression"
} }
} }
func sortDescriptors(_ ascending: Bool, dataSet: DataSet) -> [SortDescriptor<ImportedPlayer>] { func sortDescriptors(_ ascending: Bool, dataSet: DataSet) -> [SortDescriptor<ImportedPlayer>] {
switch self { switch self {
case .name: case .name:
return [SortDescriptor(\ImportedPlayer.lastName, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation)] return [
SortDescriptor(\ImportedPlayer.lastName, order: ascending ? .forward : .reverse),
SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation),
]
case .rank: case .rank:
if (dataSet == .national || dataSet == .ligue) { if dataSet == .national || dataSet == .ligue {
return [SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse)] return [
SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse)
]
} else { } else {
return [SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)] return [
SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse),
SortDescriptor(\ImportedPlayer.assimilation),
SortDescriptor(\ImportedPlayer.lastName),
]
} }
case .tournamentCount: case .tournamentCount:
return [SortDescriptor(\ImportedPlayer.tournamentCount, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)] return [
SortDescriptor(
\ImportedPlayer.tournamentCount, order: ascending ? .forward : .reverse),
SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation),
SortDescriptor(\ImportedPlayer.lastName),
]
case .points: case .points:
return [SortDescriptor(\ImportedPlayer.points, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)] return [
SortDescriptor(\ImportedPlayer.points, order: ascending ? .forward : .reverse),
SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation),
SortDescriptor(\ImportedPlayer.lastName),
]
case .progression: case .progression:
return [SortDescriptor(\ImportedPlayer.progression, order: ascending ? .forward : .reverse), SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation), SortDescriptor(\ImportedPlayer.lastName)] return [
SortDescriptor(\ImportedPlayer.progression, order: ascending ? .forward : .reverse),
SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation),
SortDescriptor(\ImportedPlayer.lastName),
]
} }
} }
} }
@ -626,20 +815,20 @@ enum PlayerFilterOption: Int, Hashable, CaseIterable, Identifiable {
case all = -1 case all = -1
case male = 1 case male = 1
case female = 0 case female = 0
var id: Int { rawValue } var id: Int { rawValue }
func icon() -> String { func icon() -> String {
switch self { switch self {
case .all: case .all:
return "Tous" return "Tous"
case .male: case .male:
return "Homme" return "Homme"
case .female: case .female:
return "Femme" return "Femme"
} }
} }
var localizedPlayerLabel: String { var localizedPlayerLabel: String {
switch self { switch self {
case .female: case .female:

@ -214,7 +214,9 @@ struct AddTeamView: View {
ToolbarItem(placement: .bottomBar) { ToolbarItem(placement: .bottomBar) {
PasteButton(payloadType: String.self) { strings in PasteButton(payloadType: String.self) { strings in
let first = strings.first ?? "" let first = strings.first ?? ""
handlePasteString(first) DispatchQueue.main.async {
self.handlePasteString(first)
}
} }
.disabled(_limitPlayerCount()) .disabled(_limitPlayerCount())
.foregroundStyle(.master) .foregroundStyle(.master)

Loading…
Cancel
Save