Faster NSArray lookup using NSDictionary storing indices as values - objective-c

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

Related

Merging values of an Array

I have an array that contains all the names that I get from JSON
NSArray *name = [newResponseObject valueForKey:#"name"];
I also have another array that contains all the last names that I get from JSON.
NSArray *lastname = [newResponseObject valueForKey:#"lastname"];
What I want to do achieve is creating another array that contains [name, lastname] and I want to display the values for my picker view. for example "John Smith".
I have tried using arrayWithArray but that just added all the values into one array.
What is the correct method to achieve this ?
Depending on what you're planning on doing with them afterwards should dictate how you decide to store them. As #Woodstock mentioned you can store them in an array of concatenated strings "name lastname" similar to option 1 from below.
But from your description it sounds like you may want to store them as an array of arrays similar to option 2 from below.
// option 1 storing them in concatonated strings
NSArray *firstNames = #[#"John", #"Ralph", #"Bob"];
NSArray *lastNames = #[#"Smith", #"Jones", #"Miller"];
NSMutableArray <NSString *> *firstAndLastNames = [NSMutableArray array];
for (NSInteger index = 0; index < firstNames.count; index++) {
[firstAndLastNames addObject:[NSString stringWithFormat:#"%# %#", [firstNames objectAtIndex:index], [lastNames objectAtIndex:index]]];
}
NSLog(#"First and last names = %#", firstAndLastNames);
// option 2 storing them in arrays
NSMutableArray <NSArray *> *firstAndLastNamesInArray = [NSMutableArray array];
for (NSInteger index2 = 0; index2 < firstNames.count; index2++) {
[firstAndLastNamesInArray addObject:#[[firstNames objectAtIndex:index2], [lastNames objectAtIndex:index2]]];
}
NSLog(#"First and last names in array = %#", firstAndLastNamesInArray);
Which would result in outputs of:
Option 1:
First and last names = (
"John Smith",
"Ralph Jones",
"Bob Miller"
)
Option 2:
First and last names in array = (
(
John,
Smith
),
(
Ralph,
Jones
),
(
Bob,
Miller
)
)
The benefit of using the second option is if you want to just get the first name or last name you can just find it by index (rather than having to attempt to separate the first and last names by space -- some first or last names also have spaces in them so this could mess up your logic of trying to fetch just one of the names if you use option 1).
i.e. using option 2 you can find the last name of the 3rd person by doing this:
[[firstAndLastNamesInArray objectAtIndex:2] objectAtIndex:1]
Another option would be to create a Person object and have it have properties firstName and lastName and then store an array of Person objects instead.
One other thing you didn't mention as well (but I'm assuming is true), is are these arrays you're fetching from the server in the correct order -- i.e. does firstName[3] correlate with lastName[3]? Also are you always guaranteed to have a firstName and lastName for each person?
There are lots of ways to achieve this.
Probably the easiest is to concatenate at source.
NSArray *fullName = [NSString stringWithFormat:#"%# %#", [newResponseObject valueForKey:#"name"], [newResponseObject valueForKey:#"lastname"]];

Sort an NSDictionary into an NSMutableArray but keep value and key?

I'm a little confused: trying to take a list of player names and scores that I have in an NSDictionary, and sort them into score order (highest score first). I know I can't sort a Dictionary so I need to move the data into an array first, but in doing so, won't I have to lose one half of each dictionary key/value pair?
For example, let's say I have the following pairs:
Bill / 10000
John / 7500
Stan / 7500
Mark / 5000
If I go and take the scores out and sort them, then retrieve the keys later, won't John and Stan get mixed up since they had identical scores? Might it call one of them twice, etc?
I realise I can't sort the dictionary, but is there a smarter way to do this?
What you could do is to get a sorted array of your players based on their score, and then create a dictionary for each player and append them in another array. Something like this perhaps (I'm using the new literals syntax, use the old one if appropriate):
NSDictionary *scores = #{
#"Stan" : #7500,
#"Mark" : #5000,
#"John" : #7500,
#"Bill" : #10000
};
NSArray *sp = [scores keysSortedByValueUsingComparator:^(id obj1, id obj2){
return [obj2 compare:obj1];
}];
NSMutableArray *players = [NSMutableArray arrayWithCapacity:0];
for (NSString *p in sp) [players addObject:#{ #"name":p, #"score":scores[p] }];
Now your players array is:
(
{
name = Bill;
score = 10000;
},
{
name = John;
score = 7500;
},
{
name = Stan;
score = 7500;
},
{
name = Mark;
score = 5000;
}
)
PS. Is not a good idea to keep a dictionary where the keys are player names, consider that you got 2 different John players... what would happen then? Also a better solution imo would be to create a Player class and keep their data (score, name etc) as properties.
You'll have to create two arrays, one for the score and another for the player. The key point being that the player array is in the same order as the (sorted) score array. The implementation below assumes you keep the score using an NSNumber object, and is not particularly efficient as it sorts the values twice:
(untested)
NSDictionary *dict = ...; // key=player (NSString), value=score (NSNumber).
NSArray *scores = [[dict allValues] sortedArrayUsingComparator:^(id obj1, id obj2) {
return [(NSNumber *)obj2 compare:(NSNumber *)obj1];
}];
NSArray *players = [dict keysSortedByValueUsingComparator:^(id obj1, id obj2) {
return [(NSNumber *)obj2 compare:(NSNumber *)obj1];
}];

How to sort a search result using NSPredicate by relevance?

I have an array of products classes. Each product class has multiple properties such as name, functions, ingredients, etc.
I developed a simple search algorithm using NSPredicate that allows me to search for a specific string in the array of products.
In a nutshell, the predicate looks like: "string in product.name OR string in product.ingredients OR string in product.functions...".
I can get the result array correctly, i.e. list of products that contains the search string in one of its properties.
BUT, my question is, how to sort this result by RELEVANCE? i.e. if a search string hits multiple times in a product class, naturally I want to put this product at the top of the search result array?
Thanks!
I have coded an example that might answer your question. I am not saying this is the best implementation, but it works.
NSArray *strings = #[#"A",#"B",#"C",#"A",#"D",#"E",#"B",#"B",#"B"];
NSMutableDictionary *sortedDictionary = [NSMutableDictionary dictionary];
for (NSString *string in strings) {
NSNumber *count = [sortedDictionary objectForKey:string];
if (count) {
count = #(count.integerValue + 1);
} else {
count = #(1);
}
[sortedDictionary setObject:count forKey:string];
}
NSArray *result = [sortedDictionary keysSortedByValueWithOptions:NSNumericSearch usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj2 compare:obj1];
}];
NSLog(#"%#", result); // (B,A,D,E,C)

iPhone, How can I store a value and a key and lookup by key and index?

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

Getting unique items from NSMutableArray

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.