├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ └── StringContainsOperators.xcscheme ├── Package.swift ├── README.md ├── Sources └── StringContainsOperators │ ├── Extensions │ └── String+removeDiacriticsAndCase.swift │ ├── SearchStrategies │ ├── AndSearchStrategy.swift │ ├── DiacriticAndCaseInsensitiveSearchStrategy.swift │ ├── NegatableSearchStrategy.swift │ ├── OrSearchStrategy.swift │ └── RegexSearchStrategy.swift │ ├── SearchStrategy.swift │ ├── SearchStrategyMaker.swift │ └── StringContainsOperators.swift ├── StringContainsOperators.podspec └── Tests └── StringContainsOperatorsTests ├── SearchStrategies ├── AndSearchStrategyTests.swift ├── DiacriticAndCaseInsensitiveSearchStrategyTests.swift ├── NegatableSearchStrategyTests.swift ├── OrSearchStrategyTests.swift └── RegexSearchStrategyTests.swift └── StringContainsOperatorsTests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: macos-latest 12 | 13 | steps: 14 | - name: Get Sources 15 | uses: actions/checkout@v2 16 | 17 | - name: Build Package 18 | run: swift build -v 19 | 20 | - name: Test & publish code coverage to Code Climate 21 | uses: paambaati/codeclimate-action@v3.0.0 22 | env: 23 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 24 | with: 25 | coverageCommand: swift test --enable-code-coverage 26 | debug: true 27 | coverageLocations: ${{github.workspace}}/.build/debug/codecov/*.json:lcov-json 28 | 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | .vscode 11 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/StringContainsOperators.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 75 | 76 | 82 | 83 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "StringContainsOperators", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "StringContainsOperators", 12 | targets: ["StringContainsOperators"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "StringContainsOperators", 23 | dependencies: []), 24 | .testTarget( 25 | name: "StringContainsOperatorsTests", 26 | dependencies: ["StringContainsOperators"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Swift](https://github.com/Tavernari/StringContainsOperators/actions/workflows/swift.yml/badge.svg?branch=main) 2 | [![Maintainability](https://api.codeclimate.com/v1/badges/29ffa494572357c62162/maintainability)](https://codeclimate.com/github/Tavernari/StringContainsOperators/maintainability) 3 | [![Test Coverage](https://api.codeclimate.com/v1/badges/29ffa494572357c62162/test_coverage)](https://codeclimate.com/github/Tavernari/StringContainsOperators/test_coverage) 4 | 5 | # 🐞 StringContainsOperators 6 | 7 | StringContainsOperators is a Swift library that simplifies searching for multiple strings within a given text. By using custom infix operators and predicates, you can create complex and flexible search patterns that make it easy to find if strings exist in your text. 8 | 9 | ![Example](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExMTQxZTA5NzMyNGQ0NjQ2YzY2YmI4OGY5ODZjNGJiNWViNmI0OWE3OSZjdD1n/aWYYLfaHwbQAtuuWAM/giphy.gif) 10 | 11 | ## Operators 12 | 13 | StringContainsOperators provides several operators that can be used to create complex search conditions. These operators allow you to search for strings in a more natural and expressive way, and to combine search conditions using logical operators. 14 | 15 | ### `||` Operator 16 | The || operator performs a logical OR operation between two strings or StringPredicates. It returns a StringPredicate that represents the combined search condition. 17 | 18 | ```swift 19 | // Swift native implementation 20 | let result = text.contains("quick") || text.contains("jumps") 21 | 22 | // StringContainsOperators implementation 23 | let result = try text.contains("quick" || "jumps") 24 | ``` 25 | ## `&&` Operator 26 | The && operator performs a logical AND operation between two strings or StringPredicates. It returns a StringPredicate that represents the combined search condition. 27 | 28 | ```swift 29 | // Swift native implementation 30 | let result = text.contains("fox") && text.contains("dog") 31 | 32 | // StringContainsOperators implementation 33 | let result = try text.contains("fox" && "dog") 34 | ``` 35 | 36 | ## `~` Operator 37 | The ~ operator creates a StringPredicate that performs a case-insensitive and diacritic-insensitive search for a given string. 38 | 39 | ```swift 40 | // Swift native implementation 41 | let options: String.CompareOptions = [.caseInsensitive, .diacriticInsensitive] 42 | let result = text.range(of: "Brown", options: options) != nil || text.range(of: "red", options: options) != nil 43 | 44 | 45 | // StringContainsOperators implementation 46 | let result = try text.contains(~"Brown" || ~"red") 47 | ``` 48 | ## `!` Operator 49 | The ! operator negates a StringPredicate or a string. When used before a StringPredicate, it returns a StringPredicate that represents the negation of the original search condition. When used before a string, it returns a StringPredicate that represents the negation of a simple search condition. 50 | 51 | ```swift 52 | // Swift native implementation 53 | let result = !(text.contains("cat") && text.contains("bird")) 54 | 55 | // StringContainsOperators implementation 56 | let result = try text.contains(!("cat" && "bird")) 57 | ``` 58 | 59 | ## `=~` Operator 60 | The =~ operator creates a StringPredicate that performs a regular expression search for a given pattern. 61 | 62 | ```swift 63 | // Swift native implementation 64 | let pattern = "(quick|jumps).*fox" 65 | let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) 66 | let range = NSRange(location: 0, length: text.utf16.count) 67 | let result = regex.firstMatch(in: text, options: [], range: range) != nil 68 | 69 | // StringContainsOperators implementation 70 | let result = try text.contains(=~"(quick|jumps).*fox") 71 | ``` 72 | 73 | **Note that the =~ operator expects a valid regular expression pattern. If the pattern is invalid, an error will be thrown.** 74 | 75 | ## Usage 76 | 77 | Here's a quick example of how you can use StringContainsOperators: 78 | 79 | ```swift 80 | import StringContainsOperators 81 | 82 | let text = "The quick brown fox jumps over the lazy dog." 83 | 84 | // Check if text contains "quick" OR "jumps" 85 | let result1 = try text.contains("quick" || "jumps") 86 | print(result1) // true 87 | 88 | // Check if text contains "fox" AND "dog" 89 | let result2 = try text.contains("fox" && "dog") 90 | print(result2) // true 91 | 92 | // Check if text contains "fox" AND ("jumps" OR "swift") 93 | let result3 = try text.contains("fox" && ("jumps" || "swift")) 94 | print(result3) // true 95 | 96 | // Check if text contains "Brown" OR "red" case insensitively and without diacritics 97 | let result4 = try text.contains(~"Brown" || ~"red") 98 | print(result4) // true 99 | 100 | // Check if text contains "fox" AND ("Jumps" OR "swift") case insensitively and without diacritics 101 | let result5 = try text.contains(~"fox" && (~"Jumps" || ~"swift")) 102 | print(result5) // true 103 | 104 | // Check if text does NOT contain "cat" AND "bird" 105 | let result6 = try text.contains(!("cat" && "bird)") 106 | print(result6) // true 107 | 108 | // Check if text does NOT contain "brown" 109 | let result7 = try text.contains(!"brown") 110 | print(result7) // false 111 | 112 | // Check if text does NOT contain "cat" case insensitively and without diacritics 113 | let result8 = try text.contains(!~"cat") 114 | print(result8) // true 115 | 116 | // Check if text contains "quick" OR "jumps" AND "fox" using a regular expression 117 | let result9 = try text.contains(=~"(quick|jumps).*fox") 118 | print(result9) // true 119 | 120 | // Check if text contains "jumps" OR "swift" AND "fox" using a regular expression 121 | let result10 = try text.contains(=~"(jumps|swift).*fox") 122 | print(result10) // true 123 | 124 | ``` 125 | 126 | ### Complex Usage 127 | 128 | With the StringContainsOperators, you can combine the different operators to create complex conditions to search for strings. 129 | 130 | For example, let's say you have a list of book titles and you want to find all the books that contain the words "fantasy" or "magic" but do not contain the words "horror" or "thriller". You can use the ||, &&, and ! operators to create a complex search condition: 131 | 132 | ```swift 133 | import StringContainsOperators 134 | 135 | struct Book { 136 | let title: String 137 | let genre: String 138 | } 139 | 140 | let books = [ 141 | Book(title: "The Lord of the Rings", genre: "fantasy"), 142 | Book(title: "Harry Potter and the Philosopher's Stone", genre: "fantasy"), 143 | Book(title: "The Hitchhiker's Guide to the Galaxy", genre: "science fiction"), 144 | Book(title: "The Shining", genre: "horror"), 145 | Book(title: "The Silence of the Lambs", genre: "thriller") 146 | ] 147 | 148 | let searchCondition = (~"Fantasy" || ~"Science Fiction") && !(~"Horror" || ~"Thriller") 149 | 150 | let filteredTitles = try books 151 | .filter { book in try book.genre.contains(searchCondition) } 152 | .map { $0.title } 153 | 154 | print(filteredTitles) // ["The Lord of the Rings", "Harry Potter and the Philosopher's Stone", "The Hitchhiker's Guide to the Galaxy"] 155 | 156 | ``` 157 | 158 | In the example above, we created a searchCondition variable that combines the operators ||, &&, and !. We used this searchCondition with the contains method to filter the bookTitles array, resulting in only the books that match the complex search condition. 159 | 160 | You can also use regular expressions to create complex search conditions. For example, let's say you have a list of email addresses and you want to find all the email addresses that start with "johndoe" and end with "gmail.com". You can use the =~ operator to create a regular expression search condition: 161 | 162 | ```swift 163 | import StringContainsOperators 164 | 165 | let emailAddresses = [ 166 | "johndoe@gmail.com", 167 | "jane_doe@hotmail.com", 168 | "johndoe123@yahoo.com", 169 | "johndoe@gmail.com.br", 170 | "johndoe123@gmail.com" 171 | ] 172 | 173 | let searchCondition = =~"^.*gmail\\.com$" 174 | 175 | let filteredEmails = try emailAddresses.filter { email in 176 | return try email.contains(searchCondition) 177 | } 178 | 179 | print(filteredEmails) // ["johndoe@gmail.com"] 180 | ``` 181 | 182 | In this example, we created a searchCondition variable that uses a regular expression to match email addresses that start with "johndoe" and end with "gmail.com". We used the =~ operator to create the search condition and passed it to the contains method to filter the emailAddresses array, resulting in only the email addresses that match the search condition. 183 | 184 | ## How to Install 185 | 186 | ### SPM 187 | 188 | You can install StringContainsOperators using Swift Package Manager (SPM). Simply add the following line to your dependencies in your Package.swift file: 189 | 190 | ```swift 191 | .package(url: "https://github.com/Tavernari/StringContainsOperators.git", from: "1.3.0") 192 | ``` 193 | 194 | ### Cocoapods 195 | 196 | You can also install StringContainsOperators using CocoaPods. Simply add the following line to your Podfile: 197 | 198 | ```ruby 199 | pod 'StringContainsOperators', '~> 1.3' 200 | ``` 201 | 202 | ## Contributions 203 | 204 | Contributions to StringContainsOperators are welcome! Before making a pull request, please open an issue to discuss your proposed changes. We follow the GitHub Flow for our development process. 205 | 206 | Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. 207 | -------------------------------------------------------------------------------- /Sources/StringContainsOperators/Extensions/String+removeDiacriticsAndCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+removeDiacriticsAndCase.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 23/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An extension of String that removes diacritics and lowercase the string. 11 | extension String { 12 | 13 | /// Removes diacritics and lowercase the string. 14 | /// 15 | /// - Returns: The string with removed diacritics and lowercase. 16 | func removeDiacriticsAndCase() -> String { 17 | 18 | return folding(options: .diacriticInsensitive, 19 | locale: Locale.current) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/StringContainsOperators/SearchStrategies/AndSearchStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AndSearchStrategy.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 23/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// `AndSearchStrategy` is a type of `SearchStrategy` that searches for multiple `String`s with an "AND" condition. 11 | final class AndSearchStrategy: SearchStrategy { 12 | 13 | /// An array of StringPredicateInputKind to search. 14 | let input: [StringPredicateInputKind] 15 | 16 | /// Initializes an instance of `AndSearchStrategy`. 17 | /// - Parameter input: An array of StringPredicateInputKind to search. 18 | init(input: [StringPredicateInputKind]) { 19 | 20 | self.input = input 21 | } 22 | 23 | /// Evaluates if a given string contains all of the `String`s in the `strings` array. 24 | /// 25 | /// - Parameter string: The string to be evaluated. 26 | /// - Returns: `true` if the string contains all of the `String`s in the `strings` array, `false` otherwise. 27 | func evaluate(string: String) throws -> Bool { 28 | 29 | try self.input.allSatisfy { inputKind in 30 | 31 | switch inputKind { 32 | 33 | case let .string(value): 34 | return string.contains(value) 35 | 36 | case let .predicate(predicate): 37 | return try string.contains(predicate) 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/StringContainsOperators/SearchStrategies/DiacriticAndCaseInsensitiveSearchStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiacriticAndCaseInsensitiveSearchStrategy.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 23/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// `DiacriticAndCaseInsensitiveSearchStrategy` is a type of `SearchStrategy` that searches for a `String` case-insensitively and without diacritics. 11 | final class DiacriticAndCaseInsensitiveSearchStrategy: SearchStrategy { 12 | 13 | enum InternalError: Error { 14 | 15 | case notAvailableToPredicates 16 | } 17 | 18 | /// An StringPredicateInputKind to search. 19 | let input: StringPredicateInputKind 20 | 21 | /// Initializes an instance of `DiacriticAndCaseInsensitiveSearchStrategy`. 22 | /// - Parameter input: An StringPredicateInputKind to search. 23 | init(input: StringPredicateInputKind) { 24 | 25 | self.input = input 26 | } 27 | 28 | /// Evaluates if a given string contains the `value` string without diacritics and case-insensitively. 29 | /// 30 | /// - Parameter string: The string to be evaluated. 31 | /// - Returns: `true` if the string contains the `value` string without diacritics and case-insensitively, `false` otherwise. 32 | func evaluate(string: String) throws -> Bool { 33 | 34 | switch self.input { 35 | 36 | case let .string(value): 37 | let string = string.removeDiacriticsAndCase().lowercased() 38 | let value = value.removeDiacriticsAndCase().lowercased() 39 | return string.contains(value) 40 | 41 | case .predicate: 42 | throw InternalError.notAvailableToPredicates 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/StringContainsOperators/SearchStrategies/NegatableSearchStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NegatableValueSearchStrategy.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 24/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A search strategy that negates the presence of a given value in a string. 11 | final class NegatableSearchStrategy: SearchStrategy { 12 | 13 | /// An StringPredicateInputKind to search. 14 | let input: StringPredicateInputKind 15 | 16 | /// Initializes an instance of `NegatableSearchStrategy`. 17 | /// - Parameter input: An StringPredicateInputKind to search. 18 | init(input: StringPredicateInputKind) { 19 | 20 | self.input = input 21 | } 22 | 23 | /// Evaluates the given string with the negated value search strategy. 24 | /// 25 | /// - Parameter string: The string to be evaluated. 26 | /// - Returns: `true` if the given string does not contain the value, `false` otherwise. 27 | func evaluate(string: String) throws -> Bool { 28 | 29 | switch self.input { 30 | 31 | case let .string(value): 32 | return !string.contains(value) 33 | 34 | case let .predicate(predicate): 35 | return try !string.contains(predicate) 36 | } 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /Sources/StringContainsOperators/SearchStrategies/OrSearchStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrSearchStrategy.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 23/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// OrSearchStrategy is a concrete class that represents a strategy to search for a string that contains at least one of a given array of strings. 11 | final class OrSearchStrategy: SearchStrategy { 12 | 13 | /// An array of StringPredicateInputKind to search. 14 | let input: [StringPredicateInputKind] 15 | 16 | /// Initializes an instance of `OrSearchStrategy`. 17 | /// - Parameter input: An array of StringPredicateInputKind to search. 18 | init(input: [StringPredicateInputKind]) { 19 | 20 | self.input = input 21 | } 22 | 23 | /// Evaluates if a given string contains at least one of the strings in `strings`. 24 | /// - Parameter string: The string to search. 25 | /// - Returns: `true` if the string contains at least one of the strings in `strings`, `false` otherwise. 26 | func evaluate(string: String) throws -> Bool { 27 | 28 | try self.input.contains { inputKind in 29 | 30 | switch inputKind { 31 | 32 | case let .string(value): 33 | return string.contains(value) 34 | 35 | case let .predicate(predicate): 36 | return try string.contains(predicate) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/StringContainsOperators/SearchStrategies/RegexSearchStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegexSearchStrategy.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 24/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A search strategy that evaluates whether a string matches a given regular expression pattern. 11 | final class RegexSearchStrategy: SearchStrategy { 12 | 13 | enum InternalError: Error { 14 | 15 | case notAvailableToPredicates 16 | } 17 | 18 | /// An StringPredicateInputKind to search. 19 | let input: StringPredicateInputKind 20 | 21 | /// Initializes an instance of `RegexSearchStrategy`. 22 | /// - Parameter input: An StringPredicateInputKind to search. 23 | init(input: StringPredicateInputKind) { 24 | 25 | self.input = input 26 | } 27 | 28 | /// Evaluates whether a string matches the regular expression pattern. 29 | /// - Parameter string: The string to evaluate. 30 | /// - Returns: `true` if the string matches the regular expression pattern, `false` otherwise. 31 | func evaluate(string: String) throws -> Bool { 32 | 33 | switch self.input { 34 | 35 | case let .string(value): 36 | let regex = try NSRegularExpression(pattern: value, 37 | options: []) 38 | let range = NSRange(location: 0, 39 | length: string.utf16.count) 40 | return regex.firstMatch(in: string, 41 | options: [], 42 | range: range) != nil 43 | 44 | case .predicate: 45 | throw InternalError.notAvailableToPredicates 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /Sources/StringContainsOperators/SearchStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchStrategy.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 23/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A protocol that defines the behavior of a search strategy. 11 | protocol SearchStrategy { 12 | 13 | /// Evaluates if a given string matches the search strategy. 14 | /// 15 | /// - Parameter string: The string to evaluate. 16 | /// - Returns: `true` if the string matches the search strategy, otherwise `false`. 17 | func evaluate(string: String) throws -> Bool 18 | } 19 | -------------------------------------------------------------------------------- /Sources/StringContainsOperators/SearchStrategyMaker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 23/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /// `SearchStrategyMaker` is a factory that produces `SearchStrategy` objects based on a given `StringPredicate`. 12 | enum SearchStrategyMaker { 13 | 14 | /// Creates an appropriate `SearchStrategy` based on the given `StringPredicate`. 15 | /// 16 | /// - Parameter predicate: The `StringPredicate` to base the `SearchStrategy` on. 17 | /// - Returns: The appropriate `SearchStrategy`. 18 | static func make(predicate: StringPredicate) -> SearchStrategy { 19 | 20 | switch predicate { 21 | 22 | case let .or(input): 23 | return OrSearchStrategy(input: input) 24 | 25 | case let .and(input): 26 | return AndSearchStrategy(input: input) 27 | 28 | case let .diacriticAndCaseInsensitive(input): 29 | return DiacriticAndCaseInsensitiveSearchStrategy(input: input) 30 | 31 | case let .regexp(input): 32 | return RegexSearchStrategy(input: input) 33 | 34 | case let .negatable(input): 35 | return NegatableSearchStrategy(input: input) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/StringContainsOperators/StringContainsOperators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringContainsOperators.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 23/03/2023. 6 | // 7 | import Foundation 8 | 9 | infix operator || : LogicalDisjunctionPrecedence 10 | infix operator && : LogicalConjunctionPrecedence 11 | prefix operator ~ 12 | prefix operator =~ 13 | prefix operator ! 14 | 15 | 16 | public enum StringPredicateInputKind { 17 | 18 | case string(String) 19 | case predicate(StringPredicate) 20 | } 21 | 22 | /// An enum representing a string search predicate. 23 | public indirect enum StringPredicate { 24 | 25 | /// Represents a logical OR operation between multiple strings. 26 | case or([StringPredicateInputKind]) 27 | 28 | /// Represents a logical AND operation between multiple strings. 29 | case and([StringPredicateInputKind]) 30 | 31 | /// Represents a case-insensitive and diacritic-insensitive search for a given string. 32 | case diacriticAndCaseInsensitive(StringPredicateInputKind) 33 | 34 | /// Represents a regular expression pattern that can be used to match a string using NSRegularExpression. 35 | /// - Note: The String value should be a valid regular expression pattern. 36 | case regexp(StringPredicateInputKind) 37 | 38 | /// Represents a negatable search predicate for a given string. 39 | case negatable(StringPredicateInputKind) 40 | } 41 | 42 | /// Returns a `StringPredicate` that performs a logical OR operation between two strings. 43 | /// - Parameters: 44 | /// - lhs: The first string to be evaluated. 45 | /// - rhs: The second string to be evaluated. 46 | /// - Returns: A `StringPredicate` that performs a logical OR operation between two strings. 47 | public func || (lhs: String, rhs: String) -> StringPredicate { 48 | 49 | return .or([.string(lhs), .string(rhs)]) 50 | } 51 | 52 | /// Returns a `StringPredicate` that performs a logical OR operation between a string and a `StringPredicate`. 53 | /// - Parameters: 54 | /// - lhs: The string to be evaluated. 55 | /// - rhs: The `StringPredicate` to be evaluated. 56 | /// - Returns: A `StringPredicate` that performs a logical OR operation between a string and a `StringPredicate`. 57 | public func || (lhs: String, rhs: StringPredicate) -> StringPredicate { 58 | 59 | return .or([.string(lhs), .predicate(rhs)]) 60 | } 61 | 62 | /// Returns a `StringPredicate` that performs a logical OR operation between a `StringPredicate` and a string. 63 | /// - Parameters: 64 | /// - lhs: The `StringPredicate` to be evaluated. 65 | /// - rhs: The string to be evaluated. 66 | /// - Returns: A `StringPredicate` that performs a logical OR operation between a `StringPredicate` and a string. 67 | public func || (lhs: StringPredicate, rhs: String) -> StringPredicate { 68 | 69 | return .or([.predicate(lhs), .string(rhs)]) 70 | } 71 | 72 | /// Returns a `StringPredicate` that performs a logical OR operation between two `StringPredicate`s. 73 | /// - Parameters: 74 | /// - lhs: The first `StringPredicate` to be evaluated. 75 | /// - rhs: The second `StringPredicate` to be evaluated. 76 | /// - Returns: A `StringPredicate` that performs a logical OR operation between two `StringPredicate`s. 77 | public func || (lhs: StringPredicate, rhs: StringPredicate) -> StringPredicate { 78 | 79 | return .or([.predicate(lhs), .predicate(rhs)]) 80 | } 81 | 82 | /// Returns a `StringPredicate` that performs a logical AND operation between two strings. 83 | /// - Parameters: 84 | /// - lhs: The first string to be evaluated. 85 | /// - rhs: The second string to be evaluated. 86 | /// - Returns: A `StringPredicate` that performs a logical AND operation between two strings. 87 | public func && (lhs: String, rhs: String) -> StringPredicate { 88 | 89 | return .and([.string(lhs), .string(rhs)]) 90 | } 91 | 92 | /// Returns a `StringPredicate` that performs a logical AND operation between a string and a `StringPredicate`. 93 | /// - Parameters: 94 | /// - lhs: The string to be evaluated. 95 | /// - rhs: The `StringPredicate` to be evaluated. 96 | /// - Returns: A `StringPredicate` that performs a logical AND operation between a string and a `StringPredicate`. 97 | public func && (lhs: String, rhs: StringPredicate) -> StringPredicate { 98 | 99 | return .and([.string(lhs), .predicate(rhs)]) 100 | } 101 | 102 | /// Returns a `StringPredicate` that performs a logical AND operation between a `StringPredicate` and a string. 103 | /// - Parameters: 104 | /// - lhs: The `StringPredicate` to be evaluated. 105 | /// - rhs: The string to be evaluated. 106 | /// - Returns: A `StringPredicate` that performs a logical AND operation between a `StringPredicate` and a string. 107 | public func && (lhs: StringPredicate, rhs: String) -> StringPredicate { 108 | 109 | return .and([.predicate(lhs), .string(rhs)]) 110 | } 111 | 112 | /// Returns a `StringPredicate` that performs a logical AND operation between two `StringPredicate`s. 113 | /// - Parameters: 114 | /// - lhs: The first `StringPredicate` to be evaluated. 115 | /// - rhs: The second `StringPredicate` to be evaluated. 116 | /// - Returns: A `StringPredicate 117 | public func && (lhs: StringPredicate, rhs: StringPredicate) -> StringPredicate { 118 | 119 | return .and([.predicate(lhs), .predicate(rhs)]) 120 | } 121 | 122 | /// Returns a `StringPredicate` that performs a case-insensitive and diacritic-insensitive search for a given string. 123 | /// 124 | /// - Parameter value: The value to be evaluated. 125 | /// - Returns: A `StringPredicate` that performs a case-insensitive and diacritic-insensitive search for a given string. 126 | public prefix func ~ (value: String) -> StringPredicate { 127 | 128 | return .diacriticAndCaseInsensitive(.string(value)) 129 | } 130 | 131 | /// Returns a `StringPredicate` that performs a regular expression pattern that can be used to match a string using NSRegularExpression. 132 | /// 133 | /// - Parameter pattern: The regular expression pattern as a string value. 134 | /// - Returns: A `StringPredicate` that can be used to perform regular expression pattern matching. 135 | public prefix func =~ (pattern: String) -> StringPredicate { 136 | 137 | return .regexp(.string(pattern)) 138 | } 139 | 140 | /// Returns a `StringPredicate` that negates another `StringPredicate`. 141 | /// 142 | /// - Parameter predicate: The predicate to be negated. 143 | /// - Returns: A `StringPredicate` that represents the negation of the given predicate. 144 | public prefix func ! (predicate: StringPredicate) -> StringPredicate { 145 | 146 | return .negatable(.predicate(predicate)) 147 | } 148 | 149 | /// Returns a `StringPredicate` that negates a given value. 150 | /// 151 | /// - Parameter value: The value to be negated. 152 | /// - Returns: A `StringPredicate` that represents the negation of the given value. 153 | public prefix func ! (value: String) -> StringPredicate { 154 | 155 | return .negatable(.string(value)) 156 | } 157 | 158 | public extension String { 159 | 160 | /// Returns a Boolean value indicating whether the string contains the given `StringPredicate`. 161 | /// 162 | /// - Parameter predicate: The `StringPredicate` to search for. 163 | /// - Returns: `true` if the string contains the `StringPredicate`, `false` otherwise. 164 | func contains(_ predicate: StringPredicate) throws -> Bool { 165 | 166 | try SearchStrategyMaker 167 | .make(predicate: predicate) 168 | .evaluate(string: self) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /StringContainsOperators.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "StringContainsOperators" 3 | s.version = "1.3.0" 4 | s.summary = "A Swift library for creating and evaluating complex string predicates using custom operators." 5 | s.description = "StringContainsOperators provides custom operators and an enum type to create complex string predicates, which can be evaluated using the contains() function. This library is designed to be easy to use and flexible, allowing developers to create powerful string matching logic with minimal code." 6 | s.homepage = "https://github.com/Tavernari/StringContainsOperators" 7 | s.license = "MIT" 8 | s.author = { "Victor Carvalho Tavernari" => "victortavernari@gmail.com" } 9 | s.source = { :git => "https://github.com/Tavernari/StringContainsOperators.git", :tag => "#{s.version}" } 10 | s.source_files = "Sources/**/*.swift" 11 | s.ios.deployment_target = '11.0' 12 | s.osx.deployment_target = '10.13' 13 | end 14 | -------------------------------------------------------------------------------- /Tests/StringContainsOperatorsTests/SearchStrategies/AndSearchStrategyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AndSearchStrategyTests.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 25/03/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import StringContainsOperators 10 | 11 | final class AndSearchStrategyTests: XCTestCase { 12 | 13 | let validString = "Hello blue planet" 14 | let invalidString = "---" 15 | 16 | func testStringInputs() throws { 17 | 18 | let strategy = AndSearchStrategy(input: [ 19 | .string("blue"), 20 | .string("planet"), 21 | .string("Hello") 22 | ]) 23 | 24 | XCTAssertTrue(try strategy.evaluate(string: self.validString)) 25 | XCTAssertFalse(try strategy.evaluate(string: self.invalidString)) 26 | } 27 | 28 | func testPredicateInputs() throws { 29 | 30 | let strategy = AndSearchStrategy(input: [ 31 | .predicate(.and([.string("blue"), 32 | .string("planet")])), 33 | .predicate(.and([.string("planet"), 34 | .string("Hello")])) 35 | ]) 36 | 37 | XCTAssertTrue(try strategy.evaluate(string: self.validString)) 38 | XCTAssertFalse(try strategy.evaluate(string: self.invalidString)) 39 | } 40 | 41 | func testPredicateAndStringInputs() throws { 42 | 43 | let strategy = AndSearchStrategy(input: [ 44 | .predicate(.and([.string("blue"), 45 | .string("planet")])), 46 | .string("Hello"), 47 | .string("planet") 48 | ]) 49 | 50 | XCTAssertTrue(try strategy.evaluate(string: self.validString)) 51 | XCTAssertFalse(try strategy.evaluate(string: self.invalidString)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/StringContainsOperatorsTests/SearchStrategies/DiacriticAndCaseInsensitiveSearchStrategyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiacriticAndCaseInsensitiveSearchStrategyTests.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 25/03/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import StringContainsOperators 10 | 11 | final class DiacriticAndCaseInsensitiveSearchStrategyTests: XCTestCase { 12 | 13 | let validString = "Hello blue planet" 14 | let invalidString = "---" 15 | 16 | func testStringInput() throws { 17 | 18 | let strategy = DiacriticAndCaseInsensitiveSearchStrategy(input: .string("héllo")) 19 | 20 | XCTAssertTrue(try strategy.evaluate(string: self.validString)) 21 | XCTAssertFalse(try strategy.evaluate(string: self.invalidString)) 22 | } 23 | 24 | func testPredicateInputs() throws { 25 | 26 | let strategy = DiacriticAndCaseInsensitiveSearchStrategy(input: 27 | .predicate(.diacriticAndCaseInsensitive(.string("héllo"))) 28 | ) 29 | 30 | XCTAssertThrowsError(try strategy.evaluate(string: self.validString)) 31 | XCTAssertThrowsError(try strategy.evaluate(string: self.invalidString)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/StringContainsOperatorsTests/SearchStrategies/NegatableSearchStrategyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NegatableSearchStrategy.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 25/03/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import StringContainsOperators 10 | 11 | final class NegatablesSearchStrategyTests: XCTestCase { 12 | 13 | let validString = "Hello blue planet" 14 | let invalidString = "---" 15 | 16 | func testStringInput() throws { 17 | 18 | let strategy = NegatableSearchStrategy(input: .string("blue")) 19 | 20 | XCTAssertFalse(try strategy.evaluate(string: self.validString)) 21 | XCTAssertTrue(try strategy.evaluate(string: self.invalidString)) 22 | } 23 | 24 | func testPredicateInputs() throws { 25 | 26 | let strategy = NegatableSearchStrategy(input: 27 | .predicate(.negatable(.string("blue"))) 28 | ) 29 | 30 | XCTAssertTrue(try strategy.evaluate(string: self.validString)) 31 | XCTAssertFalse(try strategy.evaluate(string: self.invalidString)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/StringContainsOperatorsTests/SearchStrategies/OrSearchStrategyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrSearchStrategyTests.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 25/03/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import StringContainsOperators 10 | 11 | final class OrSearchStrategyTests: XCTestCase { 12 | 13 | let validString = "Hello blue planet" 14 | let invalidString = "---" 15 | 16 | func testStringInputs() throws { 17 | 18 | let strategy = OrSearchStrategy(input: [ 19 | .string("blue"), 20 | .string("planet"), 21 | .string("Hello") 22 | ]) 23 | 24 | XCTAssertTrue(try strategy.evaluate(string: self.validString)) 25 | XCTAssertFalse(try strategy.evaluate(string: self.invalidString)) 26 | } 27 | 28 | func testPredicateInputs() throws { 29 | 30 | let strategy = OrSearchStrategy(input: [ 31 | .predicate(.or([.string("blue"), 32 | .string("red")])), 33 | .predicate(.or([.string("Hello"), 34 | .string("Hi")])), 35 | .predicate(.or([.string("world"), 36 | .string("planet")])) 37 | ]) 38 | 39 | XCTAssertTrue(try strategy.evaluate(string: self.validString)) 40 | XCTAssertFalse(try strategy.evaluate(string: self.invalidString)) 41 | } 42 | 43 | func testPredicateAndStringInputs() throws { 44 | 45 | let strategy = OrSearchStrategy(input: [ 46 | .predicate(.or([.string("blue"), 47 | .string("red")])), 48 | .string("Hello"), 49 | .string("planet") 50 | ]) 51 | 52 | XCTAssertTrue(try strategy.evaluate(string: self.validString)) 53 | XCTAssertFalse(try strategy.evaluate(string: self.invalidString)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/StringContainsOperatorsTests/SearchStrategies/RegexSearchStrategyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegexSearchStrategyTests.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 25/03/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import StringContainsOperators 10 | 11 | final class RegexSearchStrategyTests: XCTestCase { 12 | 13 | let validString = "Hello blue planet" 14 | let invalidString = "---" 15 | 16 | func testStringInput() throws { 17 | 18 | let strategy = RegexSearchStrategy(input: .string("^Hell.*$")) 19 | 20 | XCTAssertTrue(try strategy.evaluate(string: self.validString)) 21 | XCTAssertFalse(try strategy.evaluate(string: self.invalidString)) 22 | } 23 | 24 | func testPredicateInputs() throws { 25 | 26 | let strategy = RegexSearchStrategy(input: 27 | .predicate(.regexp(.string("^Hell.*$"))) 28 | ) 29 | 30 | XCTAssertThrowsError(try strategy.evaluate(string: self.validString)) 31 | XCTAssertThrowsError(try strategy.evaluate(string: self.invalidString)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/StringContainsOperatorsTests/StringContainsOperatorsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringContainsOperatorsTests.swift 3 | // 4 | // 5 | // Created by Victor C Tavernari on 23/03/2023. 6 | // 7 | import XCTest 8 | @testable import StringContainsOperators 9 | 10 | final class StringContainsOperatorsTests: XCTestCase { 11 | 12 | func testBaseStringPredicate() throws { 13 | 14 | let predicate = "Hello" || "World" 15 | XCTAssertTrue(try "Hello".contains(predicate)) 16 | XCTAssertTrue(try "World".contains(predicate)) 17 | XCTAssertFalse(try "Goodbye".contains(predicate)) 18 | } 19 | 20 | func testOrStringPredicate() throws { 21 | 22 | let predicate = "Hello" || "World" || "Goodbye" 23 | XCTAssertTrue(try "Hello".contains(predicate)) 24 | XCTAssertTrue(try "World".contains(predicate)) 25 | XCTAssertTrue(try "Goodbye".contains(predicate)) 26 | XCTAssertFalse(try "Goodnight".contains(predicate)) 27 | } 28 | 29 | func testOrPredicates() throws { 30 | 31 | let predicate = "Hello" || ("W" && "o" && "r" && "l" && "d") 32 | XCTAssertTrue(try "Hello".contains(predicate)) 33 | XCTAssertTrue(try "World".contains(predicate)) 34 | XCTAssertFalse(try "Goodbye".contains(predicate)) 35 | XCTAssertFalse(try "Hey".contains(predicate)) 36 | } 37 | 38 | func testAndStringPredicate() throws { 39 | 40 | let predicate = "Hello" && "World" 41 | XCTAssertTrue(try "HelloWorld".contains(predicate)) 42 | XCTAssertFalse(try "Hello".contains(predicate)) 43 | XCTAssertFalse(try "World".contains(predicate)) 44 | XCTAssertFalse(try "Goodbye".contains(predicate)) 45 | } 46 | 47 | func testAndStringPredicateInsentitive() throws { 48 | 49 | let predicate = ~"Hello" && ~"World" && "Apple" 50 | XCTAssertTrue(try "HeLLoWórld Apple".contains(predicate)) 51 | XCTAssertTrue(try "HelloWORLDApple".contains(predicate)) 52 | XCTAssertTrue(try "HÉLLoWorlD Apple".contains(predicate)) 53 | XCTAssertFalse(try "ApplEGoodbyeWorld".contains(predicate)) 54 | } 55 | 56 | func testAndPredicates() throws { 57 | 58 | let predicate = "H" && ("e" || "i") && "llo" 59 | XCTAssertTrue(try "Hello".contains(predicate)) 60 | XCTAssertTrue(try "Hillo".contains(predicate)) 61 | XCTAssertFalse(try "Hallo".contains(predicate)) 62 | XCTAssertFalse(try "Hiyo".contains(predicate)) 63 | } 64 | 65 | func testIndirectStringPredicate() throws { 66 | 67 | let predicate = ("Hello" || "World") && "!" 68 | XCTAssertTrue(try "Hello!".contains(predicate)) 69 | XCTAssertTrue(try "World!".contains(predicate)) 70 | XCTAssertFalse(try "Hello".contains(predicate)) 71 | } 72 | 73 | func testNestedStringPredicate() throws { 74 | 75 | let predicate = "Hello" || ("W" && ("o" || "i") && "r" && "l" && "d") 76 | XCTAssertTrue(try "Hello".contains(predicate)) 77 | XCTAssertTrue(try "World".contains(predicate)) 78 | XCTAssertTrue(try "Wirld".contains(predicate)) 79 | XCTAssertFalse(try "Goodbye".contains(predicate)) 80 | } 81 | 82 | func testDiacriticInsensitiveLowercase() throws { 83 | 84 | let predicate = ~"héllo" || ~"wórld" 85 | XCTAssertTrue(try "hello".contains(predicate)) 86 | XCTAssertTrue(try "world".contains(predicate)) 87 | XCTAssertFalse(try "goodbye".contains(predicate)) 88 | } 89 | 90 | func testDiacriticInsensitiveUppercase() throws { 91 | 92 | let predicate = ~"héllo" || ~"wórld" 93 | XCTAssertTrue(try "HELLO".contains(predicate)) 94 | XCTAssertTrue(try "WORLD".contains(predicate)) 95 | XCTAssertFalse(try "GOODBYE".contains(predicate)) 96 | } 97 | 98 | func testDiacriticInsensitiveMixedcase() throws { 99 | 100 | let predicate = ~"héllo" || ~"wórld" 101 | XCTAssertTrue(try "Hello".contains(predicate)) 102 | XCTAssertTrue(try "World".contains(predicate)) 103 | XCTAssertTrue(try "HeLLo".contains(predicate)) 104 | XCTAssertTrue(try "wORLD".contains(predicate)) 105 | XCTAssertFalse(try "Goodbye".contains(predicate)) 106 | } 107 | 108 | func testDiacriticInsensitiveMixedcaseWithOtherChars() throws { 109 | 110 | let predicate = ~"héllo" || ~"wórld" 111 | XCTAssertTrue(try "Hello!".contains(predicate)) 112 | XCTAssertTrue(try "World?".contains(predicate)) 113 | XCTAssertTrue(try "HeLLo.".contains(predicate)) 114 | XCTAssertTrue(try "wORLD-".contains(predicate)) 115 | XCTAssertFalse(try "Goodbye".contains(predicate)) 116 | } 117 | 118 | func testContainsWithRegexp() throws { 119 | 120 | let string = "This is a test string" 121 | 122 | let predicate = "test" && "string" && =~"is.a" 123 | 124 | XCTAssertTrue(try string.contains(predicate)) 125 | 126 | let invalidString = "This is not a valid string" 127 | XCTAssertFalse(try invalidString.contains(predicate)) 128 | } 129 | 130 | func testWithInvalidRegexp() throws { 131 | 132 | let string = "This is a test string" 133 | 134 | XCTAssertThrowsError(try string.contains(=~"^*$(dis.a")) 135 | } 136 | 137 | func testNegatablePredicate() throws { 138 | 139 | let text = "Hello my little friend" 140 | 141 | XCTAssertTrue(try text.contains(!"fiance")) 142 | XCTAssertFalse(try text.contains(!"my")) 143 | XCTAssertTrue(try text.contains(!("enemy" && "little"))) 144 | XCTAssertFalse(try text.contains(!("friend" && "little"))) 145 | XCTAssertTrue(try text.contains(!("enemy" || "big"))) 146 | XCTAssertFalse(try text.contains(!("friend" || "big"))) 147 | } 148 | } 149 | --------------------------------------------------------------------------------