Find Realm objects where any sub-objects satisfy all criteria - swift

Background / context:
In RealmSwift, if I have an object that has a List of sub-objects, I can find all objects where any sub-objects satisfy some criteria by using ANY in my predicate.
For example, say I have the following schema:
import RealmSwift
class Parent: Object {
#objc dynamic var name = "" // Used to identify query results
var children = List<Child>()
}
class Child: Object {
#objc dynamic var x = 0
#objc dynamic var y = 0
}
And say I create and persist a parent like this:
let parent = Parent()
let child = Child()
child.x = 1
parent.children.append(child)
let realm = try! Realm()
try! realm.write { realm.add(parent) }
If I want to find all parents with children where x is 1, I can simply do a query like this:
let result = realm
.objects(Parent.self)
.filter(NSPredicate(format: "ANY children.x == 1"))
Problem:
But now, if I want to find all parents with children where x is 1 and y is 2, I don't understand how to write the query.
Say I create and persist two parents like this:
// Parent 1 should satisfy the query:
// It has 1 child that meets all criteria.
let parent1 = Parent()
parent1.name = "Parent 1"
let childOfParent1 = Child()
childOfParent1.x = 1
childOfParent1.y = 2
parent1.children.append(childOfParent1)
// Parent 2 should not satisfy the query:
// It has 2 children, each of which only partially meets the criteria.
let parent2 = Parent()
parent2.name = "Parent 2"
let child1OfParent2 = Child()
child1OfParent2.x = 1
child1OfParent2.y = -100 // Does not match criteria
parent2.children.append(child1OfParent2)
let child2OfParent2 = Child()
child2OfParent2.x = -100 // Does not match criteria
child2OfParent2.y = 2
parent2.children.append(child2OfParent2)
let realm = try! Realm()
try! realm.write {
realm.add(parent1)
realm.add(parent2)
}
Now, I can try to find only parents with children where x is 1 and y is 2 like this:
let results = realm
.objects(Parent.self)
.filter(NSPredicate(format: "ANY children.x == 1 && ANY children.y == 2"))
results.forEach { print($0.name) }
// This prints:
// Parent 1
// Parent 2
I want the query to return only Parent 1, but it returns both parents. The query matches any parents with children where x is 1, and also with children where y is 2, even if those are not the same children.
How would I rewrite my query so that it only matches parent 1?

Ah, found it. The key is to use SUBQUERY.
A subquery is formatted as SUBQUERY(collection, itemName, query), and returns all sub-objects matching the query. So to find parents with children matching the query, you need to check the count of the resulting sub-objects.
In my example, the query would be:
let results = realm
.objects(Parent.self)
.filter(NSPredicate(format: "SUBQUERY(children, $child, $child.x == 1 && $child.y == 2).#count > 0"))
I.e fetch all Parent objects with more than 0 children matching the predicate x == 1 && y == 2.

Related

filtering an array and counting the number of elements

I am trying to count the number of items in an array which correspond to a particular attribute.
func subCount() {
let arr = subDS.subFolders // array
var counts: [String: Int] = [:]
arr.forEach { counts[$0.parentFolder!, default: 0] += 1 }
print(Array(counts.values))
}
when the code above is executed, if the count is zero it does not appear in the array. Also the order of the array formed in an incorrect order.
You can use filter method
for example :
var filterArray = subDS.subFolders.filter { $0. parentFolder == 0 }
let count = filterArray.count
The first $0 is from the filter and it represents each subFolders.

Sorting arrays based on number of matches

I am trying to find the number of array item matches between multiple test arrays and one control array. After finding the number of matches, I want to append the test arrays to another array, sorted by number of matches between the control array and test array. For example, a test array with 3 matches would be at index 0, 2 matches at index 1, and so on.
let controlArray = ["milk", "honey"]
let test1 = ["honey", "water"]
let test2 = ["milk", "honey", "eggs"]
var sortedArrayBasedOnMatches = [[String]]()
/*I want to append test1 and test2 to sortedArrayBasedOnMatches based on how many items
test1 and test2 have in common with controlArray*/
/*in my example above, I would want sortedArrayBasedOnMatches to equal
[test2, test1] since test 2 has two matches and test 1 only has one*/
This can be done in a very functional and Swiftish way by writing a pipeline to process the input arrays:
let sortedArrayBasedOnMatches = [test1, test2] // initial unsorted array
.map { arr in (arr, arr.filter { controlArray.contains($0) }.count) } // making pairs of (array, numberOfMatches)
.sorted { $0.1 > $1.1 } // sorting by the number of matches
.map { $0.0 } // getting rid of the match count, if not needed
Update As #Carpsen90 pointed out, Switf 5 comes with support for count(where:) which reduces the amount of code needed in the first map() call. A solution that makes use of this could be written along the lines of
// Swift 5 already has this, let's add it for current versions too
#if !swift(>=5)
extension Sequence {
// taken from the SE proposal
// https://github.com/apple/swift-evolution/blob/master/proposals/0220-count-where.md#detailed-design
func count(where predicate: (Element) throws -> Bool) rethrows -> Int {
var count = 0
for element in self {
if try predicate(element) {
count += 1
}
}
return count
}
}
#endif
let sortedArrayBasedOnMatches = [test1, test2] // initial unsorted array
.map { (arr: $0, matchCount: $0.count(where: controlArray.contains)) } // making pairs of (array, numberOfMatches)
.sorted { $0.matchCount > $1.matchCount } // sorting by the number of matches
.map { $0.arr } // getting rid of the match count, if not needed
Another change in style from the original solution is to use labels for the tuple components, this makes the code a little bit clearer, but also a little bit more verbose.
One option is to convert each array to a Set and find the count of elements in the intersection with controlArray.
let controlArray = ["milk", "honey"]
let test1 = ["honey", "water"]
let test2 = ["milk", "honey", "eggs"]
var sortedArrayBasedOnMatches = [ test1, test2 ].sorted { (arr1, arr2) -> Bool in
return Set(arr1).intersection(controlArray).count > Set(arr2).intersection(controlArray).count
}
print(sortedArrayBasedOnMatches)
This will cover the case where elements are not unique in your control array(such as milk, milk, honey...) and with any number of test arrays.
func sortedArrayBasedOnMatches(testArrays:[[String]], control: [String]) -> [[String]]{
var final = [[String]].init()
var controlDict:[String: Int] = [:]
var orderDict:[Int: [[String]]] = [:] // the value is a array of arrays because there could be arrays with the same amount of matches.
for el in control{
if controlDict[el] == nil{
controlDict[el] = 1
}
else{
controlDict[el] = controlDict[el]! + 1
}
}
for tArr in testArrays{
var totalMatches = 0
var tDict = controlDict
for el in tArr{
if tDict[el] != nil && tDict[el] != 0 {
totalMatches += 1
tDict[el] = tDict[el]! - 1
}
}
if orderDict[totalMatches] == nil{
orderDict[totalMatches] = [[String]].init()
}
orderDict[totalMatches]?.append(tArr)
}
for key in Array(orderDict.keys).sorted(by: >) {
for arr in orderDict[key]! {
final.append(arr)
}
}
return final
}

Find object by comparing two array

I have two array, which has the same model.
I'm trying to find the object where it has the same id. I have tried this method which I can find it but how I can make it without for loop?
for item in userList {
let userSelection = user.list.first(where: {$0.id == item.id})
item.approved = userSelection.approved
print(userSelection)
}
Try something like this
let userSelection = user.list.filter({userList.map({$0.id}).contains({$0.id})})
Explanation:
//get all the ids from one list
let ids = userList.map({$0.id})
//filter the second list by including all the users whose id is in the first list
let userSelection = user.list.filter({ids.contains({$0.id})})
If you don't care about performance, you can use set.intersection:
let set1:Set<UserType> = Set(userList)
let set2:Set<UserType> = Set(user.list)
let commonItems = set1.intersection(set2)// Intersection of two sets
Even if your model is not Hashable, you can use sets to perform the validation:
if Set(userList.map{$0.id}).subtracting(user.list.map{$0.id}).count == 0
{
// all entries in userList exist in user.list
}
else
{
// at least one entry of userList is not in user.list
}

Swift - Custom object found nil even though objectId is accurate

let parent = path[row-1]
let child = path[row]
let indexOfChild = matrix[parent.objectId!]!.index(of: child)
print("indexOfChild = \(indexOfChild)")
print("keyValuePair = \(matrix[parent.objectId!]!)")
print("child = \(child)")
let indexAfter = matrix[parent.objectId!]!.index(after: indexOfChild!)
As a result i'm getting this information printed in console.
As you can see "child" (which is the argument object with argumentText: "example") exists in the keyValuePair (they have the same objectId). Yet, it's index in keyValuePair appears nil in console. Apparently, the mere difference is: "child" has "0x608000333f60" next to it whereas the second argument object in keyValuePair has "0x600000321a40".
I don't know the purpose of these and how they get calculated but it seems to me index is found nil because these codes are different from each other
I have been trying to fix it for too long now. Could someone please help me through it?
indexOfChild = nil
keyValuePair = [<Argument: 0x608000334e60, objectId: IG9ekqMRw9, localId: (null)> {
ACL = "<PFACL: 0x60800042f200>";
argumentText = "the only thing is I can't get over how I feel when it ";
creatorId = hWRXoRvnYd;
level = 4;
parentId = j7GkpwUKsm;
reach = 0;
threadId = Dtq632QYJ2;
}, <Argument: 0x600000321a40, objectId: 56AsB1juNP, localId: (null)> {
ACL = "<PFACL: 0x600000235440>";
argumentText = "example ";
creatorId = hWRXoRvnYd;
level = 4;
parentId = j7GkpwUKsm;
reach = 0;
threadId = Dtq632QYJ2;
}]
child = <Argument: 0x608000333f60, objectId: 56AsB1juNP, localId: (null)> {
ACL = "<PFACL: 0x608000235540>";
argumentText = "example ";
creatorId = hWRXoRvnYd;
level = 4;
parentId = j7GkpwUKsm;
reach = 0;
threadId = Dtq632QYJ2;
}
fatal error: unexpectedly found nil while unwrapping an Optional value
The 0x608000333f60 and 0x600000321a40 values are memory address locations for the relevant variables. Since the address locations are different, I believe the two child instances are treated as two separate values and probably will not match. And that might be the issue you are running into.
It might be better to check the parent for the child instance by a unique ID value so that you can match the child instance in the parent array correctly. Does that make sense?
You can find the index for the child this way:
var index = -1
var found = false
for chld in matrix[parent.objectId!]! {
index = index + 1
if chld.objectId == child.objectId {
found = true
break
}
}
if found {
// The index value at this point is the index for the child
}
You could also simply filter the child array to find the child in the parent array which has the same objectId as your child variable and then find the index from the child array, something like this:
let chld = matrix[parent.objectId!]!.filter{ $0.objectId == child.objectId }.first
let indexOfChild = matrix[parent.objectId!]!.index(of: chld)

Why Realm "to many" relationship having always the same reference?

Why is the realm-list containing the very same elements instead of different ones ?
As you can see in the picture below, there are two relam-objects (UndoMemoryNameEntry and NameEntry). The first one contains a list of 8 elements. The list's element-type is of type NameEntry !
My last NameEntry object is written with currentScorePlayer=1 and currentScoreMe=15 as you can see in the picture below:
The list in UndoMemoryNameEntry is correctly inserted the last NameEntry object. You find the insertion-code further down...
But now the problem: Why are all the existing list-elements as well changed to the newest inserted element ???? As you can see in the picture below, all the elements are unfortunately identical to the last one added - why ??????
If I change the NameEntry to the following :
And inserting at index=0 to the list, then the List changes to :
Why are all the elments changed ? And not just the inserted one ??? Thanks for any help on this !
My two realm-objects are :
class NameEntry: Object {
dynamic var playerName = ""
dynamic var isMyAdversary: Bool = false
dynamic var currentScorePlayer: Int = 0
dynamic var currentScoreMe: Int = 0
}
and the List :
class UndoMemoryNameEntry: Object {
dynamic var undoPlayerName = ""
let NameEntryList = List<NameEntry>()
}
The following code creates the Realm-List :
// query rlm for existing object (with name adversary
let undoPredicate = NSPredicate(format: "undoPlayerName == %#", adversaryName)
let undoPlayerName = rlm.objects(UndoMemoryNameEntry).sorted("undoPlayerName", ascending: true).filter(undoPredicate)
// if undoPlayerName object does not exist - then create it!
if (undoPlayerName.count < 1) {
rlm.beginWrite()
let undoEntry = UndoMemoryNameEntry()
undoEntry.undoPlayerName = adversaryName
rlm.add(undoEntry)
rlm.commitWrite()
}
The following code adds a "NameEntry"-Element in the List :
let undoPredicate = NSPredicate(format: "undoPlayerName == %#", plaNameLab)
let undoPlayerName = rlm.objects(UndoMemoryNameEntry).sorted("undoPlayerName", ascending: true).filter(undoPredicate)
if (undoPlayerName.count == 1) {
rlm.beginWrite()
println(entry)
var undoEntry = undoPlayerName[0] as UndoMemoryNameEntry
undoEntry.NameEntryList.insert(entry, atIndex: 0)
rlm.commitWrite()
}
The above code-excerts work perfectly - except that the realm-List always changes all its elements to the one just inserted.
I finally found a solution:
First of all rearrange the two realm objects as follows:
class NameEntry: Object {
dynamic var playerName = ""
dynamic var currentScorePlayer: Int = 0
dynamic var currentScoreMe: Int = 0
// the undo-list is better placed in the first object...
let undoEntryList = List<UndoMemoryNameEntry>()
override static func primaryKey() -> String? {
return "playerName"
}
}
class UndoMemoryNameEntry: Object {
dynamic var undoPlayerName = ""
dynamic var currentScorePlayer: Int = 0
dynamic var currentScoreMe: Int = 0
// no primary key here since the undoEntry will have several items with the same undoPlayerName
}
Then when adding a "NameEntry"-Element in the List :
let predicate = NSPredicate(format: "playerName == %#", plaNameLab)
let playerName = rlm.objects(NameEntry).sorted("playerName", ascending: true).filter(predicate)
if (playerName.count == 1) {
rlm.beginWrite()
var entry = playerName[0] as NameEntry
// you need to create a new list object first !!!!!!!!!!!!
// ...in my initial example, this creation was missing !!!!!!
var siblingEntry = UndoMemoryNameEntry()
siblingEntry.undoPlayerName = plaNameLab
siblingEntry.currentScorePlayer = entry.currentScorePlayer
siblingEntry.currentScoreMe = entry.currentScoreMe
// insert new list-element
entry.undoEntryList.insert(siblingEntry, atIndex: 0)
// alternatively choose append if you want to add the element at the end of the list
entry.undoEntryList.append(siblingEntry)
// or choose the "ringbuffer-solution" given in the add-on below if you want to restrict the number of list-elements to ringbuffer-size !
// ...
rlm.commitWrite()
}
Add-on: If you want to create a ringbuffer having only a limited number of list-elements:
// create ringbuffer of 20 elements (20th element will be newest)
let ringBufferSize = 20
let undoPredicate = NSPredicate(format: "undoPlayerName == %#", plaNameLab)
if (entry.undoEntryList.filter(undoPredicate).sorted("undoPlayerName").count < ringBufferSize) {
entry.undoEntryList.append(siblingEntry)
}
else {
// entry.undoEntryList.replace(ringBufferSize-1, object: siblingEntry)
entry.undoEntryList.removeAtIndex(ringBufferSize-1)
entry.undoEntryList.append(siblingEntry)
for index in 0..<ringBufferSize-1 {
let tempEntry1 = rlm.objects(UndoMemoryNameEntry).filter(undoPredicate).sorted("undoPlayerName")[index] as UndoMemoryNameEntry
let tempEntry2 = rlm.objects(UndoMemoryNameEntry).filter(undoPredicate).sorted("undoPlayerName")[index+1] as UndoMemoryNameEntry
tempEntry1.currentScorePlayer = tempEntry2.currentScorePlayer
tempEntry1.currentScoreMe = tempEntry2.currentScoreMe
}
let tempEntry = rlm.objects(UndoMemoryNameEntry).filter(undoPredicate).sorted("undoPlayerName")[ringBufferSize-1] as UndoMemoryNameEntry
rlm.delete(tempEntry)
}