When I was using an NSArray, it was easy:
NSArray *array = ...
lastIndex = INT_MAX;
...
int randomIndex;
do {
randomIndex = RANDOM_INT(0, [array count] - 1);
} while (randomIndex == lastIndex);
NSLog(#"%#", [array objectAtIndex:randomIndex]);
lastIndex = randomIndex;
I need to keep track of the lastIndex because I want the feeling of randomness. That is, I don't want to get the same element twice in a row. So it shouldn't be "true" randomness.
From what I can tell, NSDictionary doesn't have something like -objectAtIndex:. So how do I accomplish this?
You can get an array of keys with allKeys (undefined order) or keysSortedByValueUsingSelector (if you want sorting by value). One thing to keep in mind (regarding lastIndex) is that even with sorting, the same index may come to refer to a different key-value pair as the dictionary grows.
Either of these (but especially keysSortedByValueUsingSelector) will come with a performance penalty.
EDIT: Since the dictionary isn't mutable, you should just be able to call allKeys once, and then just pick random keys from that.
You could use the code below:
- (YourObjectType *)getRandomObjectFromDictionary:(NSDictionary *)dictionary
{
NSArray *keys = dictionary.allKeys;
return dictionary[keys[arc4random_uniform((int)keys.count)]];
}
To make it more efficient, you can cache keys in an instance variable. Hope this helps.
Related
Ok so I've recently decided to try to teach myself Objective-C (I'm interested in iPhone development), however I've never used C or any of its derivatives before, and as such am running into some problems.
I decided to start out by writing a very basic card application that creates a deck of cards, shuffles the deck, and then displays the cards on the screen using UIButtons, however I'm having a problem with my shuffling algorithm. Every time it gets called I get an EXC_BAD_ACCESS error, which I know means there's something desperately wrong with my code, but I just can't figure out what it is.
- (void) randomize {
NSMutableArray *tmpDeck = [[NSMutableArray alloc] init];
for(Card *tmp in _cards) {
BOOL didInsert = NO;
while(!didInsert) {
NSUInteger random = arc4random_uniform(54);
if([[tmpDeck objectAtIndex:random] isEqual:nil]) {
[tmpDeck insertObject:tmp atIndex:random];
didInsert = YES;
}
}
}
_cards = tmpDeck;
_hasBeenRandomized = YES;
}
_cards is a pointer to an NSMutableArray containing the unshuffled deck of card objects, and _hasBeenRandomized is a boolean (obviously) that keeps track of whether or not the deck has been randomized.
I've tried to use the debugger to work out what exactly is going on here, but I can't even step into the method without my program crashing. This leads me to believe that the problem has to come from the very first line, but it's just a straightforward creation of an NSMutableArray, so I don't know how it could be that. This method is being called from within viewDidLoad. This is the entirety of the viewDidLoad method currently.
- (void)viewDidLoad
{
[super viewDidLoad];
_deck = [[Deck alloc] init];
[_deck randomize];
}
Any and all help will be appreciated. Sorry if the answer is dead obvious.
This is because you are trying to insert into an index that doesn't exist yet. You need to initialize the array with as many places in the array as you need for your cards. Either that or use a NSMutableDictionary and just insert the object with the index being the key.
To add another note, calling initWithCapacity on the array wouldn't solve this for you either since this just gives a "hint" at the size. You need the count property of the array to actually be at least as large as the index you are trying to insert. If you wanted to do an array, then you would first need to populate something in each index first. You could define this in the new array literal format or use a for loop that loops the number of times you need (your max index) and insert a dummy object in it's place.
for (int i=0; i< _cards.count; ++i)
{
[tmpDeck insertObject:#"dummy" atIndex:i];
}
Then instead of checking for 'nil' before you replace, you check if it is equal to the dummy object you inserted. This would give you an array that you can insert into any of these indexes. I personally would still probably store them in an NSMutableDictionary. But if you need it in an array for some other purpose then this is a way to do it.
You also will need to be sure to replace the object instead of inserting, otherwise you will just keep adding indexes.
[tmpDeck replaceObjectAtIndex:random withObject:tmp];
If you still get the same error, set a breakpoint in your debugger and check what the random number is and what the count of your array is. If your random number is ever greater than your array count, then you will get this error.
I am a newbie so I apologise if I am missing something obvious.
I am trying to write an app in Xcode 4 to produce stats for my local sports team.
This is the relevant detail of the problem area of my programme:
NSError *error;
NSArray *games = [context executeFetchRequest:fetchRequest error:&error];
if (games == nil) {
NSLog(#"There was an error!");
}
int noOfBatOuts = games.count;
int noOfNotOuts=0;
for (NSManagedObject *oneMatch in games) {
if ([oneMatch valueForKey:#"batOut"]==#"NO") {
noOfBatOuts = noOfBatOuts - 1;
noOfNotOuts = noOfNotOuts + 1;
NSLog(#"Not Out!");
}
NSLog(#"How out %#",[oneMatch valueForKey:#"batOut"]);
}
notOuts.text=[NSString stringWithFormat:#"%d",(noOfNotOuts)];
NSLog(#"No of Not Outs is %#",notOuts.text);
When I run with data that has a not out string - NO, - the NSLog(#"Not Out!") is never called and the final NSLog(#"No of Not Outs...) reports zero. However I know that the data is there and it is identified in the middle NSLog(#"How Out....).
I am starting to tear my hair out in frustration and not having the knowledge yet to know the answer. Can anybody help please?
I would assume that batOut is a Boolean rather than string type, in which case you should be checking for truth rather than string equality.
If batOut really is a string, then you still can't compare strings this way. You need to use isEqual: or isEqualToString:. For example:
if ([[oneMatch valueForKey:#"batOut"] isEqual:#"NO"]) {
Normally you'd use isEqualToString: for string comparisons, but -valueForKey: is an id so isEqual: is safer.
The == operator checks that two pointers have the same value. Two strings can have the same contents but not be stored in the same memory.
You can't compare strings with ==. Use NSString's isEqualToString:
Can you fast enumerate a NSIndexSet? if not, what's the best way to enumerate the items in the set?
In OS X 10.6+ and iOS SDK 4.0+, you can use the -enumerateIndexesUsingBlock: message:
NSIndexSet *idxSet = ...
[idxSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
//... do something with idx
// *stop = YES; to stop iteration early
}];
A while loop should do the trick. It increments the index after you use the previous index.
/*int (as commented, unreliable across different platforms)*/
NSUInteger currentIndex = [someIndexSet firstIndex];
while (currentIndex != NSNotFound)
{
//use the currentIndex
//increment
currentIndex = [someIndexSet indexGreaterThanIndex: currentIndex];
}
Fast enumeration must yield objects; since an NSIndexSet contains scalar numbers (NSUIntegers), not objects, no, you cannot fast-enumerate an index set.
Hypothetically, it could box them up into NSNumbers, but then it wouldn't be very fast.
Short answer: no. NSIndexSet does not conform to the <NSFastEnumeration> protocol.
Supposing you have an NSTableView instance (let's call it *tableView), you can delete multiple selected rows from the datasource (uhm.. *myMutableArrayDataSource), using:
[myMutableArrayDataSource removeObjectsAtIndexes:[tableView selectedRowIndexes]];
[tableView selectedRowIndexes] returns an NSIndexSet.
No need to start enumerating over the indexes in the NSIndexSet yourself.
These answers are no longer true for IndexSet in Swift 5. You can perfectly get something like:
let selectedRows:IndexSet = table.selectedRowIndexes
and then enumerate the indices like this:
for index in selectedRows {
// your code here.
}
Is the there a way to multiply each NSNumber contained in the array by 10?
Here is what I have so far:
NSMutableArray *vertex = [NSMutableArray arrayWithCapacity:3];
[vertex addObject:[NSNumber numberWithFloat:1.0]];
[vertex addObject:[NSNumber numberWithFloat:2.0]];
[vertex addObject:[NSNumber numberWithFloat:3.0]];
[vertex makeObjectsPerformSelector:#selector(doSomethingToObject:)];
I am not sure what selector to use to do this, please help!
Not easily, no. You would have to loop through the entire thing and replace all those instances of NSNumber with new instances of NSNumber (NSNumber itself is immutable). So, for example:
for( int i = 0; i < [vertex count]; i++ )
[vertex replaceObjectAtIndex:i withObject:[NSNumber numberWithFloat:[[vertex objectAtIndex:i] floatValue] * 10.0f]];
Obviously this is rather hard to read. You are probably better off just using a regular, primitive array of floats if you are going to be manipulating them often (e.g., applying transformations to them).
Short answer - no.
NSNumber of merely a container for a primitive value. It does not do any mathematical work. The makeObjectsPerformSelector method can be used to tell each object in the array to do something. But the class of each of those objects has to have the method the selector is for. NSNumber also does not provide any method for changing the stored value. So even if you added a category to NSNumber to do the math, you would still have to replace the old value in the array with the newly computed one.
I think a better solution would be to add a category method to NSMutableArray to do the work. It would look through the contents, calculate each new value and then replace each array member with the new one.
NSArray has useful methods to find objects for specified indexes
// To find objects by indexes
- (id)objectAtIndex:(NSUInteger)index
- (NSArray *)objectsAtIndexes:(NSIndexSet *)indexes
// To find index by object
- (NSUInteger)indexOfObject:(id)anObject
However, I want to get NSIndexSet (multiple indexes) for given objects. Something like:
- (NSIndexSet *)indexesOfObjects:(NSArray *)objects
This method does not exist for NSArray. Am I missing something? Does someone know another standard method? Otherwise I have to write this as a category method.
Newer NSArray versions (OSX 10.6 and iOS 4) provides the indexesOfObjectsPassingTest: method.
NSIndexSet *indexesOfObjects = [[array1 indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
return [array2 containsObject:obj];
}];
It might be useful to implement it using a set to specify the objects to find, such as:
- (NSIndexSet *) indicesOfObjectsInSet: (NSSet *) set
{
if ( [set count] == 0 )
return ( [NSIndexSet indexSet] );
NSMutableIndexSet * indices = [NSMutableIndexSet indexSet];
NSUInteger index = 0;
for ( id obj in self )
{
if ( [set containsObject: obj] )
[indices addIndex: index];
index++;
}
return ( [[indices copy] autorelease] );
}
This requires visiting every object in the array, but at least only does so once and makes use of fast enumeration while doing so. Using an NSSet and testing each object in the array against that set is also much faster than testing for inclusion in an array.
There's a potential optimization here, but it would break in the case where a single object is stored in the receiving array multiple times:
if ( [set containsObject: obj] )
{
[indices addIndex: index];
if ( [indices count] == [set count] )
break;
}
That way if you're scanning a 20'000-item array for two objects and they're both inside the first ten, you'll be able to avoid scanning the other 19'990 objects in the array. As I said though, that doesn't help if the array contains duplicates, because it'll stop as soon as it's found 2 indices (even if they both point to the same object).
Having said that, I agree with Mike's comment above. Chances are you're setting yourself up for some pain come optimization-time. It may be worth thinking about different data types; for instance, while NSArray seems the most logical choice for a simple flat container, if you don't actually need the ordering information it's better to use an NSSet instead; this has the added advantage that it won't store the same object (calculated using -isEqual:) twice. If you do want to keep track of duplicates, but don't need ordering, you can use NSCountedSet, which behaves as NSSet except it keeps track of how many times each objects has been added/removed without actually storing duplicates.
You have to implement your own category, as far as I can see.