1

I've a problem with the generation of random values with arc4random in Xcode. I want to exclude from the random generation those number that are already picked up. This is the algorithm that I've wroten

int randomValue =  (arc4random() % numberofquest)+ 1;
int kk=1;

if (kk==1) {

//first time add the first random value
[oldquest addObject: randomValue];
    }
else {
        //control if the number is already in the vector
        for (int j=1; j<[oldquest count]; j++)
        {

            if (randomValue==oldquest[j])
            {

                randomValue =  (arc4random() % numerodomande)+ 1;
            }
            else
            {
                [oldquest addObject: randomValue];
            }
        }

}
kk=kk+1

But It doesn't work. I suppose maybe because randomvalue and the j-th object in the array are not comparable (int the first and string the second?). Can anyone help me please?

TheInterestedOne
  • 748
  • 3
  • 12
  • 33
  • 1
    See http://crypto.stackexchange.com/questions/1379/how-to-generate-a-list-of-unique-random-numbers for the general solutions. – rossum Feb 21 '13 at 12:56
  • Note that you should never % the result of `arc4random()`. It injects modulo bias. If you want a random number in a range, use `arc4random_uniform()`. – Rob Napier Feb 22 '13 at 18:56

3 Answers3

2

I'm sorry, Ares answer was almost right. You can compare two different NSNumber objects.

Here is my solution:

- (int) generateRandomNumber {
    int number = arc4random() % 100;

    if ([chosen_numbers indexOfObject:[NSNumber numberWithInt:number]]!=NSNotFound)
        number = [self generateRandomNumber];

    [chosen_numbers addObject:[NSNumber numberWithInt:number]];
    return number;
}

Since Alessandro has some trouble implementing it heres an example inside a UIViewController class. This works for 100 numbers:

#import "ViewController.h"

@implementation ViewController {
    NSMutableArray * chosen_numbers;    
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    chosen_numbers = [[NSMutableArray alloc] init];

    for(int i = 0; i<90; i++) {
        NSLog(@"number: %d",[self generateRandomNumber]);
    }
}

- (int) generateRandomNumber {
    int number = arc4random() % 100;

    if ([chosen_numbers indexOfObject:[NSNumber numberWithInt:number]]!=NSNotFound)
        number = [self generateRandomNumber];

    [chosen_numbers addObject:[NSNumber numberWithInt:number]];
    return number;
}

@end
Odrakir
  • 4,254
  • 1
  • 20
  • 52
  • how can I call this function and pass the value number to randomValue? TY – TheInterestedOne Feb 22 '13 at 19:39
  • randomValue = [self generateRandomNumber]; – Odrakir Feb 22 '13 at 19:47
  • chosen_numbers should be a NSMutableArray previously initialized. – Odrakir Feb 22 '13 at 19:47
  • Thank You for the answer. But if I try this method my app crashes when I touch from the main menu the new game, returning a BAD ACCESS MEMORY. Why? – TheInterestedOne Feb 22 '13 at 21:25
  • This method generates 130913 random numbers before crash. But it never exit from the loop – TheInterestedOne Feb 22 '13 at 21:36
  • The problem seems to be in the if statement and in the if/else cycle – TheInterestedOne Feb 22 '13 at 21:43
  • Did you declare the chosen_numbers ivar outside this method? NSArray* chosen_numbers = [[NSMutableArray alloc] init]; – Odrakir Feb 23 '13 at 07:33
  • sure! but I've declared it in a Mutable Array. Why allocate a huge vector when the player can lose in 3 questions? In this case I could have a big computational cost. – TheInterestedOne Feb 23 '13 at 09:11
  • I've wroten the code like this..but it doesn't work... Someone know why? - (int) generateRandomNumber { int number = (arc4random() % numerodomande)+ 1; if ([oldquest indexOfObject:[NSNumber numberWithInt:number]]!=NSNotFound) { [oldquest addObject:[NSNumber numberWithInt:number]]; return number; } else { [self generateRandomNumber]; } return -1; }' – TheInterestedOne Feb 23 '13 at 09:30
  • your code generate a bad acces memory, with an infinite number of randoms value without exiting from the function – TheInterestedOne Feb 23 '13 at 11:07
  • initializing the vector as a NSmutableArray, the code works for 2 questions and after "Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Value length must be greater than 0'" – TheInterestedOne Feb 23 '13 at 11:13
  • I added some code showing a complete UIViewController class that just generates and prints those numbers. It works for 100 different numbers (between 0 and 100). – Odrakir Feb 23 '13 at 16:09
1

How about this:

    -(void) generateRandomNumber {

        int randomValue =  arc4random_uniform(numberofquest) + 1;

        if([oldQuest indexOfObject:[NSNumber numberWithInt:randomValue] == NSNotFound) {
            //Unique value
            [oldQuest addObject:[NSNumber numberWithInt:randomValue]];
        }
        else {
            //Value already exists. Look for another one
            return [self generateRandomNumber];

        }
    }

Obviously oldQuest is NSMutableArray instance that was previously initialized.

Ares
  • 5,905
  • 3
  • 35
  • 51
1

You are right - it doesn't work because here:

            if (randomValue==oldquest[j])

you are trying to compare an int with an object... NSArrays can only store objects. In fact this line should not work for that reason:

[oldquest addObject: randomValue];

You need to box the int as an NSNumber and store that in the array:

NSNumber* boxedRandomValue = [NSNumber numberWithInt:randomValue];
[oldquest addObject: boxedRandomValue];

Then unbox it using the NSNumber instance method -(int)intValue before comparing values:

 if (randomValue==[oldquest[j] intValue])

update

There are a few other issues you will have to attend to:

  • the value of kk is reset to 1 on each iteration of the test, so kk == 1 is always true, the else clause is never invoked. You need to set it once only outside of this block of code (for example you could make it a property, set it to 1 on initialise, then access and increment it here). Better still, just use [oldquest count] instead: if ([oldquest count]==0) {} else {}. Then you can dispense with your kk counter altogether.

  • your for-loop starts with j=1. This should be j=0 to address the first item in the array (item 0).

update 2

This line: randomValue = (arc4random() % numerodomande)+ 1 is going to cause all sort of other problems due to it's position in the checking loop. Try one of these suggestions:

  • just return when you come across a dupe. No number gets added to the array...

  • set a BOOL test inside the loop, deal with it outside:

    BOOL repeatedValue = NO;
    for (int j=0; j<[self.oldquest count]; j++){
        if (randomValue==[self.oldquest[j] intValue]) {
            repeatedValue = YES;
            break;
        }
    }
    if (repeatedValue){
        NSLog (@"value repeated");
        [self addRandom];  
        //recursive call to this code block, 
        //assuming it is a method called `addRandom`
    }
    
  • Try a compact version of the last suggestion (similar to Odrakir's solution) - I've enclosed it in an addRandom method so you can see how to call it recursively.

      - (void) addRandom {
        int numberofquest = 5;
        int randomValue =  (arc4random() % numberofquest)+ 1;
        NSNumber* boxedValue = [NSNumber numberWithInt:randomValue];
        if ([self.oldquest indexOfObject:boxedValue]==NSNotFound) {
            [self.oldquest addObject: boxedValue];
        } else {
            [self addRandom];
        }
    }
    

    (If you do loop until you find a unique number you will have to watch out, as your total set of numbers is finitely limited to numberofquest, so when you have a full set you may end up with an infinite loop.)

  • Instead of using NSMutableArray, you could use MutableOrderedSet instead - it's an ordered collection of unique objects, so will not add an object twice.

in @interface

@property (nonatomic, strong) NSMutableOrderedSet setOfRandoms;

in @implementation

int randomValue =  (arc4random() % numberofquest)+ 1;
NSNumber randomValueBoxed = [NSNumber numberWithInt:randomValue];
[setOfRandoms addObject:randomValueBoxed];

update 3

The previous hints assumed it was the list of randoms you were interested in. Here is a complete solution to return a new unique random int in a self-contained method.

You need to set up 2 properties in your @interface and initialise them somewhere:

@property (nonatomic, assign) int maxRand;
    //stores the highest allowed random number

@property (nonatomic, strong) NSMutableArray* oldRands;
    //stores the previously generated numbers

uniqueRandom returns a unique random number between 1 and self.maxRand each time. If all allowable numbers have been returned it returns 0.

- (int) uniqueRandom {
    int result = 0;
    if ([self.oldRands count] != self.maxRand) {
        int randomValue =  (arc4random() %  self.maxRand )+ 1;
        NSNumber* boxedValue = [NSNumber numberWithInt:randomValue];
        if ([self.oldRands indexOfObject:boxedValue]==NSNotFound) {
            [self.oldRands addObject: boxedValue];
            result = randomValue;
        } else {
             result = [self uniqueRandom];
        }
    }
    return result;
}

You should consider that it doesn't make sense to change self.maxRand once it is initialised unless you also reset self.oldRands. So you might want to use a const or #define instead, and/or tie it in to your self.oldRands initialiser.

foundry
  • 31,615
  • 9
  • 90
  • 125
  • I've tryed but It doesn't work! The questions are still repeating :( – TheInterestedOne Feb 22 '13 at 19:32
  • @AlessandroBava, you have a couple of other issues, see my updated answer – foundry Feb 22 '13 at 20:45
  • Thank You for your help! You're in right with the kk, and I've made a property. for the j I was confused with other codes where the first index is 1. Thank You to notice that. I've understood what you've said in point 3, but I don't know how to implement a new random value and then how to check it. – TheInterestedOne Feb 22 '13 at 21:19
  • @AlessandroBava, see my next update with a few more suggestions – foundry Feb 22 '13 at 21:41
  • but he compact function is not a void funtion? how about a complete script that's working? sorry but I'm trying to mesh all these hints but nothing is stil working... – TheInterestedOne Feb 23 '13 at 11:31
  • it was a void function because I thought it was the _list_ of unique randoms you were interested in, stored in self.oldquest. I've added a new update method (complete) that returns a unique random int. – foundry Feb 23 '13 at 14:39
  • why my app crashes when I add this code? How can i give to you my project to see it? Thank You – TheInterestedOne Feb 23 '13 at 15:24
  • @AlessandroBava, you could post a link to your project in the question. Better though, accept my answer (as we have strayed quite far from your original post, and in your question there is not enough context to answer everything). Start a new question showing your current code. If it is crashing, show the crash error. Perhaps try some logging first to see where it is unhappy. – foundry Feb 23 '13 at 16:00