I'm having a tough time thinking through how I can make my search function more optimal. So I have a situation where I load an array of objects from a SQLITE database. Essentially one element in the array has multiple properties that can be accessed. Inside one element of the array, you have properties like time and rooms. I want to make the search flexible without having the user to filter what they are searching for. What I don't like about my implementation is that I filter through both time and room, then I for loop the filtered arrays and add them to a general "searchArrayToDisplay." I think the for loop is wasteful. I was wondering if there is a way I can append the filtered elements to the general "searchArrayToDisplay" without having to for loop? The method below is generally fast, but I think there's a better way that I'm just not seeing. Hopefully I am clear. Thanks for any help!
let filteredTime = searchArray.filter { (time: ProductModel) -> Bool in
if time.customText1 == nil {
return false
} else {
return time.customText1.rangeOfString(searchString, options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil, locale: nil) != nil
}
}
var m : ProductModel
for m in filteredTime {
searchArrayToDisplay.append(m)
}
let filteredRooms = searchArray.filter { (session: ProductModel) -> Bool in
if session.roomName == nil {
return false
} else {
return session.roomName.rangeOfString(searchString, options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil, locale: nil) != nil
}
}
for pm in filteredRooms {
searchArrayToDisplay.append(pm)
}
use Logical OR Operator. like this
let filteredTimeAndRooms = searchArray.filter { model in
let foundTime = model.time?.rangeOfString(searchString) != nil
let foundRoom = model.roomName?.rangeOfString(searchString) != nil
return foundTime || foundRoom
}
searchArrayToDisplay = filteredTimeAndRooms
Just merge both comparisons into one and be careful when to return early:
let searched = searchArray.filter { (prod : ProductModel) -> Bool in
if let room = prod.roomName {
if room.rangeOfString(searchString, options: .CaseInsensitiveSearch, range: nil, locale: nil) != nil {
return true
}
}
if let text = prod.customText1 {
return text.rangeOfString(searchString, options: .CaseInsensitiveSearch, range: nil, locale: nil) != nil
}
return false
}
Either you assign that directly via searchArrayToDisplay = searchArray.filter {...} or you append the array via searchArrayToDisplay += searched.
Related
Declaration:
let listArray = ["kashif"]
let word = "kashif"
then this
contains(listArray, word)
Returns true but if declaration is:
let word = "Kashif"
then it returns false because comparison is case sensitive.
How to make this comparison case insensitive?
Xcode 8 • Swift 3 or later
let list = ["kashif"]
let word = "Kashif"
if list.contains(where: {$0.caseInsensitiveCompare(word) == .orderedSame}) {
print(true) // true
}
alternatively:
if list.contains(where: {$0.compare(word, options: .caseInsensitive) == .orderedSame}) {
print(true) // true
}
if you would like to know the position(s) of the element in the array (it might find more than one element that matches the predicate):
let indices = list.indices.filter { list[$0].caseInsensitiveCompare(word) == .orderedSame }
print(indices) // [0]
You can also use localizedStandardContains method which is case and diacritic insensitive and would match a substring as well:
func localizedStandardContains<T>(_ string: T) -> Bool where T : StringProtocol
Discussion This is the most appropriate method for doing user-level string searches, similar to how searches are done generally in the system. The search is locale-aware, case and diacritic insensitive. The exact list of search options applied may change over time.
let list = ["kashif"]
let word = "Káshif"
if list.contains(where: {$0.localizedStandardContains(word) }) {
print(true) // true
}
you can use
word.lowercaseString
to convert the string to all lowercase characters
For checking if a string exists in a array (case insensitively), please use
listArray.localizedCaseInsensitiveContainsString(word)
where listArray is the name of array
and word is your searched text
This code works in Swift 2.2
Swift 4
Just make everything (queries and results) case insensitive.
for item in listArray {
if item.lowercased().contains(word.lowercased()) {
searchResults.append(item)
}
}
You can add an extension:
Swift 5
extension Array where Element == String {
func containsIgnoringCase(_ element: Element) -> Bool {
contains { $0.caseInsensitiveCompare(element) == .orderedSame }
}
}
and use it like this:
["tEst"].containsIgnoringCase("TeSt") // true
Try this:
let loword = word.lowercaseString
let found = contains(listArray) { $0.lowercaseString == loword }
For checking if a string exists in a array with more Options(caseInsensitive, anchored/search is limited to start)
using Foundation range(of:options:)
let list = ["kashif"]
let word = "Kashif"
if list.contains(where: {$0.range(of: word, options: [.caseInsensitive, .anchored]) != nil}) {
print(true) // true
}
if let index = list.index(where: {$0.range(of: word, options: [.caseInsensitive, .anchored]) != nil}) {
print("Found at index \(index)") // true
}
swift 5, swift 4.2 , use the code in the below.
let list = ["kAshif"]
let word = "Kashif"
if list.contains(where: { $0.caseInsensitiveCompare(word) == .orderedSame }) {
print("contains is true")
}
SWIFT 3.0:
Finding a case insensitive string in a string array is cool and all, but if you don't have an index it can not be cool for certain situations.
Here is my solution:
let stringArray = ["FOO", "bar"]()
if let index = stringArray.index(where: {$0.caseInsensitiveCompare("foo") == .orderedSame}) {
print("STRING \(stringArray[index]) FOUND AT INDEX \(index)")
//prints "STRING FOO FOUND AT INDEX 0"
}
This is better than the other answers b/c you have index of the object in the array, so you can grab the object and do whatever you please :)
Expanding on #Govind Kumawat's answer
The simple comparison for a searchString in a word is:
word.range(of: searchString, options: .caseInsensitive) != nil
As functions:
func containsCaseInsensitive(searchString: String, in string: String) -> Bool {
return string.range(of: searchString, options: .caseInsensitive) != nil
}
func containsCaseInsensitive(searchString: String, in array: [String]) -> Bool {
return array.contains {$0.range(of: searchString, options: .caseInsensitive) != nil}
}
func caseInsensitiveMatches(searchString: String, in array: [String]) -> [String] {
return array.compactMap { string in
return string.range(of: searchString, options: .caseInsensitive) != nil
? string
: nil
}
}
My example
func updateSearchResultsForSearchController(searchController: UISearchController) {
guard let searchText = searchController.searchBar.text else { return }
let countries = Countries.getAllCountries()
filteredCountries = countries.filter() {
return $0.name.containsString(searchText) || $0.name.lowercaseString.containsString(searchText)
}
self.tableView.reloadData()
}
If anyone is looking to search values from within model class, say
struct Country {
var name: String
}
One case do case insensitive checks like below -
let filteredList = countries.filter({ $0.name.range(of: "searchText", options: .caseInsensitive) != nil })
i am currently working on an App that needs to compare three Variables with each other.
Rules for Comparison: The result should only be true if:
All three variables are equal OR All three variables are different
My first idea was something like this, but I hope there is a more elegant solution for this:
if (value1 == value2 && value2 == value3) || (value1 != value2 && value2 != value3 && value3 != value1) {
// True
} else {
// False
}
I would be really happy if someone of you can think of a more elegant solution and share it with me.
Thanks for your help in advance!
If your values are also Hashable you can use a Set. Given the fact that a Set discards duplicate values, you can simplify your check to something like this:
let valuesArray = [value1, value2, value3]
let valuesSet = Set(valuesArray)
if valuesSet.count == 1 || valuesSet.count == valuesArray.count {
// True
} else {
// False
}
For a one-off, that's not too bad.
The more "general" solution is kind of messy, because it needs to track 2 different boolean variables, and handle empty collections correctly.
extension Sequence where Element: Equatable {
func classifyElementEquality() -> (allEqual: Bool, allUnequal: Bool) {
var iterator = self.makeIterator()
guard let first = iterator.next() else {
return (true, true) // all empty
}
return AnyIterator(iterator)
.reduce(into: (allEqual: true, allUnequal: true)) { acc, element in
if first == element {
acc.allUnequal = false
} else {
acc.allEqual = false
}
}
}
}
let (value1, value2, value3) = (1, 2, 3)
let result = [value1, value2, value3].classifyElementEquality()
if result.allEqual || result.allUnequal {
print("They're either all equal, or all unequal")
} else {
print("Some of them are different")
}
It can get a bit simpler if this algorithm targets Collection insteaad of Sequence, because accessing the first element is easier without needing to manually manage an iterator.
extension Collection where Element: Equatable {
func classifyElementEquality() -> (allEqual: Bool, allUnequal: Bool) {
guard let first = self.first else {
return (true, true) // all empty
}
return self
.dropFirst()
.reduce(into: (allEqual: true, allUnequal: true)) { acc, element in
if first == element {
acc.allUnequal = false
} else {
acc.allEqual = false
}
}
}
}
i have a Realm results list:
subsList = RealmDB.objects(Downloads).filter("isActive = true").sorted("name", ascending: true)
And it display the results ordered, but the first results are "(item 1)" or 1-item, then comes A-item, B-item etc...
How can i sort it in a ways that A-item, B-item etc.. come first and (item 1), 1-item display at the end?
note: for those who don't know realm, it can take a NSPredicate for sorting
Thanks guys
EDIT:
Following the comments, I'm getting
-chloro
(+-)-1,2,3-octa
(amino)
1-propanol
acetone
benzin
dinoterb
TNT
And i need
acetone
benzin
dinoterb
TNT
-chloro
(+-)-1,2,3-octa
(amino)
1-propanol
This is just an example, fix the code to make it look better.
var arr = ["-chloro", "(+-)-1,2,3-octa", "(amino)", "1-propanol", "acetone", "benzin","dinoterb","TNT"]
func matches(for regex: String, in text: String) -> [String] {
do {
let regex = try NSRegularExpression(pattern: regex)
let nsString = text as NSString
let results = regex.matches(in: text, range: NSRange(location: 0, length: nsString.length))
return results.map { nsString.substring(with: $0.range)}
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
arr.sort { (s1, s2) -> Bool in
let matched = matches(for: "[0-9,\\(,\\-]", in: s1)
let matched2 = matches(for: "[0-9\\(,\\-]", in: s2)
if matched.isEmpty && matched2.isEmpty {
return s1.lowercased() < s2.lowercased()
} else if matched.isEmpty && !matched2.isEmpty {
return true
} else if !matched.isEmpty && matched2.isEmpty {
return false
} else {
return matched.first! < matched2.first!
}
}
result -> ["acetone", "benzin", "dinoterb", "TNT", "(+-)-1,2,3-octa", "(amino)", "-chloro", "1-propanol"]
I have a table that stores information about groups (GroupID, Members, Creator, LastAccessed, GroupName, etc..) as separate rows. Each group has a unique identifier (GroupID) as their hash (primary key). They also have an attribute called GroupName. I have a search box where the user inputs a partial group name. I want to perform a scan on the table and return all of the groups that begin with the users input. Here is what I have so far..
func searchForGroupsWithName(groupName: String) {
self.queryInProgress = true
let cond = AWSDynamoDBCondition()
let v1 = AWSDynamoDBAttributeValue();
v1.S = groupName
cond.comparisonOperator = AWSDynamoDBComparisonOperator.BeginsWith
cond.attributeValueList = [ v1 ]
let exp = AWSDynamoDBScanExpression()
//I only want to return the GroupName and GroupID.
//I think this should be ["GroupID", "GroupName"], but it requires a string
exp.projectionExpression = ??????????
//I am not sure how to incorporate cond with this.
exp.filterExpression = ??????????
dynamoDBObjectMapper.scan(GroupTableRow.self, expression: exp).continueWithBlock({ (task:AWSTask!) -> AnyObject! in
if task.result != nil {
let paginatedOutput = task.result as! AWSDynamoDBPaginatedOutput
for item in paginatedOutput.items as! [GroupTableRow] {
self.searchBarResults.append(item)
}
if ((task.error) != nil) {
print("Error: \(task.error)")
}
self.queryInProgress = false
return nil
}
self.queryInProgress = false
return nil
})
}
After getting help from WestonE, I fixed up my code and have a working example. I have pasted it below for anyone else who needs something similar
func searchForGroupsWithName(groupName: String) {
self.queryInProgress = true
let lowercaseGroupName = groupName.lowercaseString
let scanExpression = AWSDynamoDBScanExpression()
//Specify we want to only return groups whose name contains our search
scanExpression.filterExpression = "contains(LowercaseName, :LowercaseGroupName)"
//Create a scan expression to state what attributes we want to return
scanExpression.projectionExpression = "GroupID, GroupName, LowercaseName"
//Define our variable in the filter expression as our lowercased user input
scanExpression.expressionAttributeValues = [":LowercaseGroupName" : lowercaseGroupName]
dynamoDBObjectMapper.scan(GroupTableRow.self, expression: scanExpression).continueWithBlock({ (task:AWSTask!) -> AnyObject! in
if task.result != nil {
let paginatedOutput = task.result as! AWSDynamoDBPaginatedOutput
//use the results
for item in paginatedOutput.items as! [GroupTableRow] {
}
if ((task.error) != nil) {
print("Error: \(task.error)")
}
self.queryInProgress = false
return nil
}
self.queryInProgress = false
return nil
})
}
The projectionExpression should be a single comma delimited string ["GroupID", "GroupName"] => "GroupID, GroupName"
The filterExpression is also a string, and it's documentation is http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/QueryAndScan.html#FilteringResults . In your case I think the expression would be "begins_with(groupName, BeginningCharacters)" But you might need to experiment with this.
Is there any pretty way to test the below? I have multiple parameters which I need to know if any one of them is nil
This is what I am using now, I am sure there is an efficient way to test all and type nil once but not sure how:
if title == nil || name == nil || height == nil || productName == nil {
//Do something
}
I am using ObjectMapper and at they moment, they don't support error handling, hence, my init() throws errors and I need to check if the values from Map are nil or not and through if they are.
I have created a simple extension on CollectionType to check for a collection of Optional value, if at least one element is not nil, if all elements have value or if none have value.
extension CollectionType where Generator.Element == Optional<AnyObject>, Index.Distance == Int {
func allNotNil() -> Bool {
return !allNil()
}
func atleastOneNotNil() -> Bool {
return self.flatMap { $0 }.count > 0
}
func allNil() -> Bool {
return self.flatMap { $0 }.count == 0
}
}
var title: String? = ""
var name: String? = ""
var height: Float? = 1
var productName: String? = ""
[title, name, height, productName].allNotNil()
[title, name, height, productName].atleastOneNotNil()
[title, name, height, productName].allNil()
In your case, you could use it like this,
if [title, name, height, productName].atLeastOneNotNil() {
}
Or, you could discard the extension above and simply use it like this,
if [title, name, height, productName].flatMap { $0 }.count > 0 {
}
For Swift 4,
extension Collection where Element == Optional<Any> {
func allNotNil() -> Bool {
return !allNil()
}
func atleastOneNotNil() -> Bool {
return self.flatMap { $0 }.count > 0
}
func allNil() -> Bool {
return self.flatMap { $0 }.count == 0
}
}
Updates for Swift 5,
Few new functions have been added to CollectionType such as first(where:) and allSatisfy(where:) and it is used here.
extension Collection where Element == Optional {
func allNil() -> Bool {
return allSatisfy { $0 == nil }
}
func anyNil() -> Bool {
return first { $0 == nil } != nil
}
func allNotNil() -> Bool {
return !allNil()
}
}
Here's a short version using a collection literal:
let isAnyNil = ([title, name, height, productName, nil] as [Optional<Any>]).contains { $0 == nil }
It's similar to #GeneratorOfOne's flatMap and count variant. I prefer the simplicity of contains.
If you do this often, I'd go with a free function to avoid the need to specify the type:
func isAnyNil(optionals: Optional<Any> ...) -> Bool {
return optionals.contains { $0 == nil }
}
isAnyNil(title, name, height, productName)
I'm not sure why you need to know, but if it is kind of unwrapping than it better to do so in Swift 2.0
if let email = emailField?.text, password = passwordField?.text {
//here you have both email & password
}
if you enter a method and need to do something in case any of them is nil, I would recommend using a guard:
guard let email = emailField?.text else {
// It is nil, do something
return
}
// if we got here, we have 'email' and it is not nil.
Side Note:
I'm guessing when you mean efficient you really talk about pretty or easy and not really efficient, because in either cases you would have to evaluate all arguments to see if they are nil.
If indeed you just want it to be pretty, you could use .filter to check
var nilElements = [email,password].filter{0 == nil}
you will get back only the elements which are nil