@ -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 } )
// H a n d l e s p e c i a l c a s e o f h y p h e n a t e d w o r d s
let hyphenatedWords = searchText . components ( separatedBy : . whitespaces )
. filter { $0 . contains ( " - " ) }
var predicates : [ NSPredicate ] = [ ]
// A d d p r e d i c a t e s f o r h y p h e n a t e d w o r d s
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 ) )
}
}
// R e g u l a r w o r d s p r o c e s s i n g
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 ] ) ,
// F o r m u l t i - w o r d f i r s t n a m e s , t r y t h e t w o w o r d s a s a f i r s t n a m e
NSPredicate (
format : " canonicalFirstName CONTAINS[cd] %@ " , words . joined ( separator : " " ) ) ,
] )
case 3 :
// H a n d l e p o t e n t i a l c a s e s l i k e " J e a n C h r i s t o p h e C R O S "
predicates . append ( contentsOf : [
// F i r s t t w o w o r d s a s f i r s t n a m e , l a s t a s l a s t n a m e
NSPredicate (
format :
" canonicalFirstName CONTAINS[cd] %@ AND canonicalLastName CONTAINS[cd] %@ " ,
words [ 0 ] + " " + words [ 1 ] , words [ 2 ] ) ,
// F i r s t a s f i r s t n a m e , l a s t t w o a s l a s t n a m e
NSPredicate (
format :
" canonicalFirstName CONTAINS[cd] %@ AND canonicalLastName CONTAINS[cd] %@ " ,
words [ 0 ] , words [ 1 ] + " " + words [ 2 ] ) ,
// L a s t a s f i r s t n a m e , f i r s t t w o a s l a s t n a m e
NSPredicate (
format :
" canonicalFirstName CONTAINS[cd] %@ AND canonicalLastName CONTAINS[cd] %@ " ,
words [ 2 ] , words [ 0 ] + " " + words [ 1 ] ) ,
] )
default :
default :
return nil
if words . count > 0 {
// F o r s i n g l e w o r d o r m a n y w o r d s , t r y m a t c h i n g a g a i n s t f u l l n a m e
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 ) )
// A d d m a t c h f o r f u l l n a m e
predicates . append (
NSPredicate (
format : " canonicalFullName contains[cd] %@ " , canonicalVersionWithoutPunctuation )
)
// A d d p a t t e r n m a t c h f o r m o r e f l e x i b l e m a t c h i n g
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 )
// L o o k f o r e x a c t m a t c h e s o n f i r s t o r l a s t n a m e
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 )
}
}
}
}
}
}
// P r o c e s s t o k e n s
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 ? {
// D e f i n e a r e g u l a r e x p r e s s i o n t o f i n d s l a s h e s b e t w e e n a l p h a b e t i c c h a r a c t e r s ( n o t d i g i t s )
// D e f i n e a r e g u l a r e x p r e s s i o n t o f i n d s l a s h e s b e t w e e n a l p h a b e t i c c h a r a c t e r s ( n o t d i g i t s )
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 ) /
// F i n d m a t c h e s i n t h e i n p u t s t r i n g
// F i n d m a t c h e s i n t h e i n p u t s t r i n g
@ -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
}
}
// C r e a t e t h e N S P r e d i c a t e f o r s e a r c h i n g i n t h e ` l a s t N a m e ` f i e l d
// C r e a t e t h e N S P r e d i c a t e f o r s e a r c h i n g i n t h e ` l a s t N a m e ` f i e l d
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 )
// O u t p u t t h e r e s u l t
// O u t p u t t h e r e s u l t
// p r i n t ( " G e n e r a t e d P r e d i c a t e : \ ( p r e d i c a t e ) " )
// p r i n t ( " G e n e r a t e d P r e d i c a t e : \ ( p r e d i c a t e ) " )
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 ] ( )
// C h e c k f o r l i c e n s e n u m b e r s
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 )
// A d d g e n d e r f i l t e r i f s p e c i f i e d
// R e m o v e a l l c h a r a c t e r s t h a t a r e n o t i n t h e a l l o w e d C h a r a c t e r S e t
var text = pasteField . canonicalVersion . components ( separatedBy : allowedCharacterSet . inverted ) . joined ( ) . trimmedMultiline
// D e f i n e t h e r e g e x p a t t e r n t o m a t c h d i g i t s
let digitPattern = / \ b \ w * \ d \ w * \ b /
// R e p l a c e a l l o c c u r r e n c e s o f t h e p a t t e r n ( d i g i t s ) w i t h a n e m p t y s t r i n g
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 } )
// s e l f . w o r d s C o u n t = n a m e C o m p o n e n t s . c o u n t
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 " ) )
}
}
// A d d d a t e f i l t e r i f s p e c i f i e d
if let mostRecentDate {
if let mostRecentDate {
andPredicates . append ( NSPredicate ( format : " importDate == %@ " , mostRecentDate as CVarArg ) )
andPredicates . append ( NSPredicate ( format : " importDate == %@ " , mostRecentDate as CVarArg ) )
}
}
// C h e c k f o r s l a s h e s ( r e p r e s e n t i n g a l t e r n a t i v e s )
if let slashPredicate = getSpecialSlashPredicate ( inputString : pasteField ) {
if let slashPredicate = getSpecialSlashPredicate ( inputString : pasteField ) {
orPredicates . append ( slashPredicate )
orPredicates . append ( slashPredicate )
}
}
print ( " nameComponents " , nameComponents . count )
// P r e p a r e t e x t f o r p r o c e s s i n g - p r e s e r v e h y p h e n s b u t r e m o v e d i g i t s
var text =
if nameComponents . count < 50 {
pasteField
if nameComponents . count > 1 {
. replacingOccurrences ( of : " / " , with : " " ) // R e p l a c e s l a s h e s w i t h s p a c e s
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 {
// R e m o v e d i g i t s
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 )
// S p l i t t e x t b y w h i t e s p a c e t o g e t p o t e n t i a l n a m e c o m p o n e n t s
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 {
// H a n d l e e x a c t f u l l n a m e m a t c h
let fullName = textComponents . joined ( separator : " " )
if ! fullName . isEmpty {
orPredicates . append (
NSPredicate ( format : " canonicalFullName CONTAINS[cd] %@ " , fullName ) )
}
}
// H a n d l e h y p h e n a t e d l a s t n a m e s
let hyphenatedComponents = textComponents . filter { $0 . contains ( " - " ) }
for component in hyphenatedComponents {
orPredicates . append ( NSPredicate ( format : " lastName CONTAINS[cd] %@ " , component ) )
// A l s o s e a r c h f o r e a c h p a r t o f t h e h y p h e n a t e d n a m e
let parts = component . components ( separatedBy : " - " )
for part in parts {
if part . count > 1 {
orPredicates . append ( NSPredicate ( format : " lastName CONTAINS[cd] %@ " , part ) )
}
}
}
// T r y d i f f e r e n t c o m b i n a t i o n s f o r f i r s t / l a s t n a m e
if textComponents . count > 1 {
// T r y e a c h p a i r o f c o m p o n e n t s a s f i r s t + l a s t a n d l a s t + f i r s t
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 ]
) )
// A l s o t r y b e g i n s w i t h f o r m o r e p r e c i s e m a t c h e s
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 {
// I f o n l y o n e c o m p o n e n t , s e a r c h i n b o t h f i r s t a n d l a s t n a m e
orPredicates . append (
NSPredicate (
format : " firstName CONTAINS[cd] %@ OR lastName CONTAINS[cd] %@ " ,
textComponents [ 0 ] , textComponents [ 0 ] ) )
}
// A d d p a t t e r n m a t c h f o r c a n o n i c a l f u l l n a m e
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 )
// C o n s t r u c t f i n a l p r e d i c a t e
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 :