0

I am making an app which asks the user a series of questions. The question asked depends on the random int produced. When an int is used, I want to add it to an NSMutableArray, and then check if the array contains a number the next time a random number is chosen. I am currently using the following code to do this:

- (void) selectQuestionNumber {
textNum = lowerBounds + arc4random() % (upperBounds - lowerBounds);

if ([previousQuestions containsObject:[NSNumber numberWithInt:textNum]]) {
    [self selectQuestionNumber];
    NSLog(@"The same question number appeared!");
} else {
    questionLabel.text = [self nextQuestion];
    [self questionTitleChange];

    NSLog(@"New question made");
}
[previousQuestions addObject:[NSNumber numberWithInt:textNum]];
}  

However, the code NSLog(@"The same question number appeared!"); is never shown in the console, even when the same question will appear twice.

My code is obviously non-functional, so what code can I use to check if an NSMutable array contains an int?

Isaac A
  • 361
  • 2
  • 24
  • 1
    have you allocated memory location for previousQuestions? – Balu Nov 01 '14 at 13:53
  • No, I simply declared it as a variable in the .m class using 'NSMutableArray *previousQuestions;' – Isaac A Nov 01 '14 at 14:29
  • 2
    You would do better to generate all the indexes first, and then [pull from that array, taking steps to ensure you get no repeats](http://stackoverflow.com/questions/20917177/objective-c-how-to-pull-several-random-items-out-of-nsarray-without-getting-dupl). See also [Non-repeating random numbers](http://stackoverflow.com/q/1617630) – jscs Nov 01 '14 at 15:15

4 Answers4

2

Your problem is likely something other than detecting membership of NSNumbers in an NSArray. It can take a large number of tries for a set of random numbers to repeat. It is theoretically possible for it to not repeat until every possible value has been generated once. For a large range of legal values it can take quite a while.

I suggest logging the values that you add to the array on each pass, and the new value.

Your code above always adds the new value to the array even it if matched, so your array is going to grow with duplicates. You would be better off only adding the new number to the array if it did not match. you would probably also be better off using an NSMutableSet instead of an array. NSSets contain at most one instance of an object, and their containsObject method is faster than that of NSArray.

Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • Thank you for the answer Duncan - Turns out I hadn't properly initialised my NSMutableArray. Nonetheless, I took your advice, and used an NSMutableSet instead. Thanks again! – Isaac A Nov 02 '14 at 16:53
  • 1
    You could use an NSMutableIndexSet as well, so you don't need to create any NSNumber objects. And it's faster again, because it is restricted to sets containing (not extremely large) integer numbers. – gnasher729 Dec 10 '14 at 16:22
  • @gnasher729, good point. For those particular problem that's probably the best solution of all. (assuming that the OP's values are always in the range of an NSUInteger). No need to create and release NSNumber objects, and it is likely VERY fast since internally its probably implemented as a bit field. – Duncan C Dec 10 '14 at 19:02
2

Original solution (works with Array and Set):

-(void)selectQuestionNumber
 {
   textNum = lowerBounds + arc4random() % (upperBounds - lowerBounds);

    NSPredicate *predicate =  [NSPredicate predicateWithFormat:@"intValue=%i",textNum];
    NSArray *filteredArray = [previousQuestions filteredArrayUsingPredicate:predicate];

    if ([filteredArray count]) {
        [self selectQuestionNumber];
        NSLog(@"The same question number appeared!");
    } else {
        questionLabel.text = [self nextQuestion];
        [self questionTitleChange];

        NSLog(@"New question made");
    }
    [previousQuestions addObject:[NSNumber numberWithInt:textNum]];
}

Best solution, and better performance, especialy with mutableSet ( According to Duncan C).

 -(void)selectQuestionNumber
{

textNum = lowerBounds + arc4random() % (upperBounds - lowerBounds);

if ([previousQuestions containsObject:[NSNumber numberWithInteger:textNum]]) {
    [self selectQuestionNumber];
    NSLog(@"The same question number appeared!");
} else {
    questionLabel.text = [self nextQuestion];
    [self questionTitleChange];

    NSLog(@"New question made");
    // And add the new number to mutableSet of mutableArray.
    [previousQuestions addObject:[NSNumber numberWithInteger:textNum]];
}

}
Onik IV
  • 5,007
  • 2
  • 18
  • 23
  • What you say is absolutely 100% wrong. To quote the docs: "This method determines whether anObject is present in the array by sending an isEqual: message to each of the array’s objects (and passing anObject as the parameter to each isEqual: message)." For NSNumbers, isEqual returns YES when the objects have the same value. Thus you CAN use containsObject to tell if an array (or a set) already contains an NSNumber with the same value. – Duncan C Dec 10 '14 at 13:09
  • Your code using predicates will work, but containsObject is both faster and simpler to implement. NSSets are optimized for a fast implementation of containsObject, so that's why I suggested that approach. – Duncan C Dec 10 '14 at 13:11
  • @DuncanC you are wright, I've fixed the answer. Thanks. – Onik IV Dec 10 '14 at 15:00
0

Instead of using NSArray, you can use NSMutableIndexSet. This is the same as NSSet, with just NSUIntegers instead of objects. Very useful.

//during init
NSMutableIndexSet *tSet = [[NSMutableIndexSet alloc] init];
//...
//later in the code, in whatever loop you have on new values
NSUInteger newInt = lowerBounds + arc4random() % (upperBounds - lowerBounds);
if ([tSet containsIndex:newInt]){
    //value already exists in the set
}
else {
    //value does not exist, add it
    [tSet addIndex:newInt];
}
arinmorf
  • 1,374
  • 16
  • 27
-1
NSMutableSet *myNumbers = [NSMutableSet Set]; // or NSMutableArray..


NSNumber *aNumber = [NSNumber numberWithInt:getRandomInt() ]; //let us say it returns 1.

[myNumbers addObject:aNumber];



    -(BOOL)succesfullyAddNewUniqueRandomMember{

        NSInteger randInt = getRandomInt(); //let us say it returns 1 again..

        NSNumber *aSubsequentNumber = [NSNumber numberWithInt:randInt;

 for (NSNumber *previousEntry in myNumbers){


  if ([previousEntry isEqual:aSubsequentNumber]) return NO;

  }
[myNumbers addObject:aSubsequentNumber];
 return YES;

}

^ are these objects equal (aNumber, aSubsequentNumber) ? YES

^ are they the same object ? NO, two different NSNumbers made with equal integer..

NSSet will also happily add both, because they are not the same object.

therefore you need to loop through and compare directly to each previous member, the (already contains object) filter of NSSet will not do the trick.

by wrapping this in a -(BOOL) type method we can repeat it with a

while(![self succesfullyAddNewUniqueRandomMember])

In other words, in your code

if ([previousQuestions containsObject:[NSNumber numberWithInt:textNum]])

always returns NO because it is comparing NSNumber objects, not their integerValue.

Jef
  • 4,728
  • 2
  • 25
  • 33
  • This is flat-out wrong. `containsObject:` uses `isEqual:` -- **not** object identity -- on the tested object and the set's members to determine whether the object is present. An `NSMutableSet` will _not_ add another instance of `NSNumber` that represents the same value -- `addObject:` also uses `isEqual:`. – jscs Nov 02 '14 at 07:43
  • Thanks I'll delete it. – Jef Nov 02 '14 at 07:47
  • If you ever need it: containsObjectIdenticalTo: checks whether an array contains exactly the same object. – gnasher729 Dec 10 '14 at 16:23