I have a question about Objective-C today involving NSMutableArray. Coming from a .net/c# background I'm having some trouble working with these things.
Let's say I have an object called "Song"
My song has 3 properties:
Title
Artist
Genre
I have a NSMutableArray or NSArray which holds all my Song objects.
How would I go about trying to 'query' my array to get a new array with only (Unique) Artists or Genre's.
Where as in .net you would write a simple LINQ query with a DISTINCT clause, how would one solve this in Objective-C? I'm guessing with predicates but am struggling to find a solution.
You could also use:
NSArray *uniqueArtists = [songs valueForKeyPath:#"#distinctUnionOfObjects.artist"];
NSArray *uniqueGenres = [songs valueForKeyPath:#"#distinctUnionOfObjects.genre"];
Likewise, if you need to compare the entire object you could create a new readonly property that combines the values you want to match on (via a hash or otherwise) dynamically and compare on that:
NSArray *array = [songs valueForKeyPath:#"#distinctUnionOfObjects.hash"];
NOTE: Keep in mind this returns the uniques values for the specified property, not the objects themselves. So it will be an array of NSString values, not Song values.
Depending on what you mean by "unique artists," there are a couple of different solutions. If you just want to get all the artists and don't want any to appear more than once, just do [NSSet setWithArray:[songArray valueForKey:#"artist"]]. If you mean you want to set a list of all the songs which are the only song by their artist, you need to first find the artists who only do one song and then find the songs by those artists:
NSCountedSet *artistsWithCounts = [NSCountedSet setWithArray:[songArray valueForKey:#"artist"]];
NSMutableSet *uniqueArtists = [NSMutableSet set];
for (id artist in artistsWithCounts)
if ([artistsWithCounts countForObject:artist] == 1])
[uniqueArtists addObject:artist];
NSPredicate *findUniqueArtists = [NSPredicate predicateWithFormat:#"artist IN %#", uniqueArtists];
NSArray *songsWithUniqueArtists = [songArray filteredArrayUsingPredicate:findUniqueArtists];
Or like this:
filterTags = [[NSSet setWithArray:filterTags] allObjects];
filterTags was not unique at first, but becomes unique after the operation.
This may not be directly applicable but it is a solution to creating a unique set of values...
Assume you have a NSMutable array of events that have multiple duplicate entries. This array is named eventArray. The NSSet object can be used to trim that array and then repopulate that array as shown below...
NSSet *uniqueEvents = [NSSet setWithArray:eventArray];
[eventArray removeAllObjects];
[eventArray addObjectsFromArray:[uniqueEvents allObjects]];
Use NSOrderedSet instead of NSSet.
NSOrderedSet *uniqueOrderedSet = [NSOrderedSet setWithArray:itemsArray];
[itemsArray removeAllObjects];
[itemsArray addObjectsFromArray:[uniqueOrderedSet allObjects]];
I've created a simple query API for Objective-C that makes this kind of task a lot easier. Using the Linq-to-ObjectiveC distinct method, retrieving songs with unique artists would involve the following:
NSArray* songsWithUniqueArtistists = [input distinct:^id(id song) {
return [song artist];
}];
This returns a list of song instances, each with a unique artist.
Related
I have a fun challenging problem. So I have a mutable array that contains all of my items. I have a textfield that **might have one or two of these items if the person types them in. **
items= [[NSArray alloc]initWithObjects:#"apple", #"orange", #"pear", nil];
items2= [[NSArray alloc]initWithObjects:#"cheese", #"milk", #"eggs", nil];
Allitems= [NSMutableArray array];
[Allitems addObjectsFromArray:items];
[Allitems addObjectsFromArray:items2];
NSArray*WORDS =[Textfield componentsSeparatedByString:#" "];
I am trying to detect what specific words from **Allitems are in the textfield. (If the textfield contains any string from ALLitems, how can I find what specific string?**
for (int i = 0; i < [Allitems count]; i++)
{
NSString *grabstring;
grabstring=[Allitems objectAtIndex:i];
if (textfield isEqualto:grabstring){
?????
pull that specific string from allitems.
}
}
You want the intersection of two sets:
NSMutableSet* intersectionSet = [NSMutableSet setWithArray:Allitems];
[intersectionSet intersectSet:[NSSet setWithArray:WORDS]];
NSArray* intersectionArray = [intersectionSet allObjects];
After this intersectionArray contains the items that are present in both Allitems and WORDS.
BTW, why do you capitalise variable names in a non-standard and inconsistent manner? Why not just allItems and words?
As #Arkku suggests: It's better to switch the arrays. In your example it does not matter much, but in case Allitems were (very) big, you can save (a lot of) memory and CPU usage:
NSMutableSet* intersectionSet = [NSMutableSet setWithArray:WORDS];
[intersectionSet intersectSet:[NSSet setWithArray:Allitems]];
NSArray* intersectionArray = [intersectionSet allObjects];
There are a various ways of doing it, each with different pros and cons. Let's have the following (consistently capitalized) variables in common for each case:
NSArray *allItems = #[ #"apple", #"orange", #"pear", #"cheese", #"milk", #"egg" ];
NSString *textFieldText = #"CHEESE ham pear";
NSArray *words = [textFieldText.lowercaseString componentsSeparatedByString:#" "];
NSPredicate
NSArray *matchingItems = [allItems filteredArrayUsingPredicate:
[NSPredicate predicateWithFormat:#"SELF IN %#", words]];
This is perhaps the shortest (in lines of code) way, but not the most performant if allItems can be very long as it requires traversing all of it.
Iteration
Of course you could also simply iterate over the collection and do the matching manually:
NSMutableArray *matchingItems = [NSMutableArray array];
for (NSString *item in allItems) {
if ([words containsObject:item]) {
[matchingItems addObject:item];
}
}
Again requires traversing all of allItems (although you could break the iteration if all words are matched).
In addition to the for loop there are of course many other ways for iteration, e.g., enumerateObjectsUsingBlock:, but they are unlikely to have any advantage here.
NSSet
NSSet is often a good option for this kind of matching since testing set membership is faster than with NSArray. However, if using the most straightforward method intersetSet: (in NSMutableSet) care must be taken to not inadvertently create a large mutable set only to discard most of its items.
If the order of allItems does not matter, the best way would be to change it from an array into a set and always keep that set around, i.e., instead of creating the array allItems, you would create an NSSet:
NSSet *setOfAllItems = [NSSet setWithArray:allItems];
Or if it needs to be mutable:
NSMutableSet *setOfAllItems = [NSMutableSet set];
[setOfAllItems addObjectsFromArray:items1];
[setOfAllItems addObjectsFromArray:items2];
Then, when you have that set, you create a temporary mutable set out of words (which is presumably always the smaller set):
NSMutableSet *setOfMatches = [NSMutableSet setWithArray:words];
[setOfMatches intersectSet:setOfAllItems];
NSArray *matchingItems = setOfMatches.allObjects;
This would be likely be the most performant solution if setOfAllItems is large, but note that the matches will then need to be exact. The other methods are more easily adapted to things like matching the strings in words against fields of objects or keys in a dictionary (and returning the matched objects rather than the strings). In such a case one possibility to consider would be an NSDictionary mapping the words to match to the objects to return (also fast to then iterate over words and test for membership in the dictionary).
Conversion to string
And, since the question included conversion of matches to a string:
[matchingItems componentsJoinedByString:#", "]
In the example case this would result in the string "pear, cheese" (or possibly "cheese, pear" if using sets).
Say I have an NSDictionary and its keys are like this:
#"123_000"
#"223_000"
#"123_111"
#"223_111"
and so on. I want to get a NSArray or NSSet of all values whose corresponding keys contain substring #"123".
Of course I can just loop over the NSDictionary, but I suspect that there must be a less code heavy approach, probably involving KVC or NSPredicate, but I'm not really good in either of them.
Yes, use NSPredicate.
First get allKeys from the dictionary. Then use filteredArrayUsingPredicate: to get the list of keys you want. Then use objectsForKeys:notFoundMarker: with your resulting array of keys to get the associated objects (the not found marker isn't an issue as we know all keys exist).
To correct the answer of VaaChar:
In my example, I have a dictionary with keys and values only (vegan : 1, steak : 2, ...)
To check if my string is within these keys, I use:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF CONTAINS %#", keyword];
NSArray *categories = [[self.categoriesAndIDs allKeys] filteredArrayUsingPredicate:predicate];
SELF represents the current object of the given array, in my case [self.categoriesAndIDs allKeys] so every string within the array will be checked for my "keyword", e.g. "steak" or "st" and the result will be an array with a string called "steak".
You could use something like this:
NSArray *resultArray = [[mainDict allValues] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"self.#allKeys CONTAINS[c] %#", #"123"]];
"self.#allKeys" should return all keys of your dictionary and "CONTAINS[c] %#" checks if the key contains "123".
So suppose I have an NSArray populated with hundreds of NSDictionary objects.
All dictionary objects have a value for the key name but these values names may appear more than once in different objects.
I need to be able to filter this NSArray to only return one object per unique name attribute (whichever object, first or last, I don't care).
This is how far I've got but obviously my filtered array contains all objects rather than only unique ones.
I'm thinking there must be a way to tell the predicate to limit its results to only one / first match?
NSArray *allObjects = ... // This is my array of NSDictionaries
NSArray *uniqueNames = [allObjects valueForKeyPath:#"#distinctUnionOfObjects.name"];
NSArray *filtered = [allObjects filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"(self.name IN %#)", uniqueNames]];
Thanks!
NSMutableDictionary *uniqueObjects = [NSMutableDictionary dictionaryWithCapacity:allObjects.count];
for (NSDictionary *object in allObjects) {
[uniqueObjects setObject:object forKey:object[#"name"]];
}
Lets say I have a Student class as below:
class Student {
NSNumber *id;
NSString *lastName;
NSString *firstName;
}
Now when I get the records of all the students from a web service, I have an NSArray that stores records for all the students. At some point of time I need to look up the array to find a particular student's record based upon first name.
Assume I create a dictionary called studentsFirstNameDictionary.
So while adding objects to students array, I can do
Student objStudent = [[Student alloc] init];
objStudent.Id = someId;
objStudent.firstName = someName;
objStudent.lastName = someLastName;
[studentsDictionary setValue:iterationCounter forKey:objStudent.firstName];
[students addObject:objStudent];
I want to know if it is a good idea to create this dictionary to speed up the look up as below. Also please assume that in any case the array is required and for fast lookup I am creating other dictionaries too storing the last name and id as keys and indices as values like above:
-(Student*)getStudentByFirstName:(NSString *)firstName {
int idxOfStudent = [ studentsDictionary valueForKey:firstName];
return [students idxOfStudent];
}
Do you think this approach is performance wise better than having to iterate through the students array and compare the first name and return the matching student record?
I always need the students array because I need to populate a table view with that array. I am wondering if it is wise to create multiple dictionaries while populating the array so that I can look up a student record faster by fist name, last name or Id?
P.S.: For sake of simplicity, consider that all students have unique first name, last name and id so there will not be any issue while creating dictionaries storing first name, last name or ID as a value.
This sounds more complicated than it needs to be. Generally in Cocoa, if you find yourself consulting a data structures textbook for this common a task, either you've missed something in the Foundation docs or you're optimizing prematurely.
Given an array of Student objects, there are at least a couple of quick and easy ways to get the one with a unique attribute:
use a block test:
NSUInteger index = [studentArray indexOfObjectPassingTest:^(id obj, NSUInteger idx, BOOL *stop) {
if ([obj.firstName isEqualToString:desiredFirstName]) {
*stop = YES; // keeps us from returning multiple students with same name
return YES;
} else
return NO;
}];
if (index != NSNotFound)
Student *desiredStudent = [studentArray objectAtIndex:index];
use a predicate:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"firstName LIKE %#", desiredFirstName];
NSArray *filteredArray = [studentArray filteredArrayUsingPredicate:predicate];
Student *desiredStudent = [lastObject]; // only object if we assume firstNames are unique
Both of these assume your Student class has declared properties (or KVC-complient accessors) for those fields (that is, not just instance variables).
If you find yourself frequently accessing students by name, you might want to consider a dictionary mapping names to Student objects:
NSMutableDictionary *studentsByName = [NSMutableDictionary dictionaryWithCapacity:[students count]];
for (Student *student in students)
[studentsByName setObject:student forKey:[student firstName]];
If you have a very large number of students and want to search them by various attributes, you might consider learning about Core Data.
I don't think you need the array at all.
Create your Student objects:
Student objStudent = [[Student alloc] init];
objStudent.Id = someId;
objStudent.firstName = someName;
objStudent.lastName = someLastName;
[studentsDictionary setObject:student forKey:objStudent.firstName];
To look up a student by firstName:
Student * theStudent = [ studentsDictionary objectForKey:firstName ] ;
To get all Student objects from studentsDictionary, use
NSArray * allStudents = [ studentsDictionary allValues ] ;
This assumes you will only be finding students by their firstName attribute however.. #rickster's solution might be better in general
I've tried NSMutableDictionary however I don't seem to be able to get an object by index.
Any ideas?
EDIT:
I've trying to create a uitableview sections object, which will store the header titles and be able to increment a counter for the rows. I need to be able to get the counter by index, counter value by title value.
Simplest way is to use 2 collections: dictionary for section infos (row numbers, countries etc) and array for section titles.
NSMutableDictionary *sectionInfos;
NSMutableArray *sectionTitles;
When you need a section info by sectionTitle:
NSDictionary *info = [sectionInfos objectForKey:sectionTitle];
int rowsCount = ((NSArray *)[info objectForKey:#"Countries"]).count;
When you need a section info by sectionIndex:
NSString *title = [sectionTitles objectAtIndex:sectionIndex];
NSDictionary *info = [sectionInfos objectForKey:title];
int rowsCount = ((NSArray *)[info objectForKey:#"Countries"]).count;
When you add a section, add sections info:
[sectionInfos setObject:info forKey:sectionTitle];
and a title to array, so infos and titles will be in sync.
[sectionTitles addObject:sectionTitle];
UPDATE: if the only info needed for section is number of rows:
UPDATE2: added types.
NSMutableDictionary *sectionRowCounts;
NSMutableArray *sectionTitles;
Rows count by sectionTitle:
int rowCount = [[sectionRowCounts objectForKey:sectionTitle] intValue];
Rows count by sectionIndex:
NSString *title = [sectionTitles objectAtIndex:sectionIndex];
int rowCount = [[sectionRowCounts objectForKey:title] intValue];
Adding a section:
[sectionRowCounts setObject:[NSNumber numberWithInt:rowCount] forKey:sectionTitle];
[sectionTitles addObject:sectionTitle];
Dictionaries are not ordered; therefore the objects in them do not have an index.
http://developer.apple.com/library/ios/#documentation/cocoa/reference/foundation/Classes/NSDictionary_Class/Reference/Reference.html#//apple_ref/occ/cl/NSDictionary
You need to use the key to retrieve a particular object from a dictionary. If you need to have the objects in a specific order, then you would probably use NSArray instead.
UPDATE
In your edit, you don't show what tableSectionArray is, but it looks like it's a dictionary (which makes it poorly named). You should use an NSArray, not an NSDictionary, to store what you want. If you need more than one value to be stored, then store an object that contains the values you need. Create a class that has the required values as properties; or, if appropriate, add NSDictionary objects to your array. (Based on how you are trying to assign an element from tableSectionArray, it looks like you do want it to contain dictionaries.) But you need the tableSectionArray itself to be an NSArray.
Yes, keep trying with NSMutableDictionary. It's the data structure you need for that. Can you post your code to see why it's not returning the value you expect?
Example:
NSString *yourvalue = #"Hello!";
NSMutableDictionary *d;
[d setValue:yourvalue forKey:#"yourkey"];
NSString *retrievedvalue = [d valueForKey:#"yourkey"];
// you should get value here