Ask Ben: Selecting Random Values Without Repetition In ColdFusion

Posted February 3, 2009 at 4:33 PM by Ben Nadel

Tags: ColdFusion, Ask Ben

Hi, I have string defined like this: myStr = "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20"

I need a function to randomly to pick out any number of the above string, no duplication, mean if it pick 4, then it never pick 4 again. I tried CF functions likerand, randrange, Randomize , but nothing works. This is a survey project, when user hit a button, system suppose randomly to pick 6 out of 20 questions out of database. I wonder if you have any advise please? Thanks.

If we are dealing with randomly selected database rows, then all you would need to do is ORDER BY a random value (such as NEWID()) and select the top N records. But, since you showed me a list, I'm gonna work with a list. In the past, I have come up with ways to randomly select elements from an array, which would work nicely here if you simply converted the list to an array before you made your selection (assuming the list itself doesn't have repeat values). But, some of those examples are complicated. For this, I'm gonna keep it super simple.

For this version of random selection without repetition, the trick is to make good use of the fact that ColdFusion structs won't store the same key twice. If we go with that (and don't worry about case sensitivity, which it looks like you won't have to), then all we need to do is keep adding random values to a struct until its size (number of keys) is equal to the desired set. Then, we just use the key collection as our list of random values.

  • <!--- Define the list of numbers. --->
  • <cfset strList = "1,2,3,4,5,6,7,8,9,10" />
  •  
  • <!---
  • Create a struct to hold the list of selected numbers. Because
  • structs are indexed by key, it will allow us to not select
  • duplicate values.
  • --->
  • <cfset objSelections = {} />
  •  
  •  
  • <!---
  • Now, all we have to do is pick random numbers until our
  • struct count is the desired size (4 in this demo).
  • --->
  • <cfloop condition="(StructCount( objSelections ) LT 4)">
  •  
  • <!--- Select a random list index. --->
  • <cfset intIndex = RandRange( 1, ListLen( strList ) ) />
  •  
  • <!---
  • Add the random item to our collection. If we have
  • already picked this number, then it will simply
  • overwrite the previous and the StructCount() will
  • not be changed.
  • --->
  • <cfset objSelections[ ListGetAt( strList, intIndex ) ] = true />
  •  
  • </cfloop>
  •  
  •  
  • <!--- Output the list collection. --->
  • #StructKeyList( objSelections )#

When I run this a few times, I get:

10,3,4,5
8,9,10,4
7,8,9,3
8,9,10,2
7,8,10,2
9,3,5,6

Works quite nicely and is straightforward. There are things you could do to optimize this; but, I don't think you could do anything to beat this simplicity. I hope that helps.




Reader Comments

Feb 3, 2009 at 5:15 PM // reply »
21 Comments

Ben,

This code has the potential to run, especially if the length of the list and the number of items to pull are fairly close to each other.

Imaging 1000 numbers and pulling 990 out. The random number generator may pick many times in a row already picked items. Of course for relatively small sets of numbers, the extra processing may not be a big deal, but in theory, it's possible (not likely though) for the random number generator to select the same number a billion+ times in a row.

If you were to pluck out the list item when you access it (using ListDeleteAt) then at least you have a guarantee that the current list of items will never have a match. You can copy the list if you want to have an untouched list to use later.

Now granted in my example, you could simply remove the number of unnecessary items (10) and then perform the shuffle on the remaining items.


Feb 3, 2009 at 5:21 PM // reply »
11,246 Comments

@Danilo,

You raise a good point. The close the size of the list and the number of items you need to randomly select, the more the potential processing. I think this kind of solution is only usable in certain situations. I tried to keep is simple only because I have other examples on the site that are more effective but much more complicated.

Probably, the happy medium between ease and speed is to convert the list to an array, shuffle it (as a collection), and then just select the top N indexes. Relatively painless and very fast.


Feb 3, 2009 at 5:33 PM // reply »
211 Comments

Seems to me, for speed reasons, we should be shoving all this into an array via ListToArray() and using array functions.


Feb 3, 2009 at 5:49 PM // reply »
2 Comments

For this guy, it should be done on the database side.

In SQL Server, you add ORDER BY NewID()

This blog has a run down of a host of random ordering options in different databases: http://www.carlj.ca/2007/12/16/selecting-random-records-with-sql/

If you have an Array, MyArray, an efficient process is this:

<cfset ToValue = 4>
<cfset ArrayLength = ArrayLen(MyArray)>
<cfif ArrayLength LTE ToValue>
<cfset ToValue = ArrayLength-1>
</cfif>
<cfloop from="1" to="#ToValue#" index="X">
<cfset Y = RandRange(X, ArrayLength)>
<cfset Element = MyArray[X]>
<cfset MyArray[X] = MyArray[Y]>
<cfset MyArray[Y] = Element>
</cfloop>

This reorders the array using swaps, with only one swap required per element.

Of course, I just wrote the above on the fly - but I remember that being the most efficient way for random ordering without replacement (O(n)) from my undergraduate days.


Feb 3, 2009 at 8:21 PM // reply »
42 Comments

I thought that this might work quite well-

<cffunction name="randomNums" access="public" returntype="array">
<cfargument name="numOutput" type="numeric" required="yes">
<cfargument name="listValues" type="string" required="yes">

<cfset var local = {}>

<!---Convert the list to an array--->
<cfset LOCAL.completeList = listToArray(arguments.listValues)>

<!---Get the length of the array, don't want to calculate this each loop--->
<cfset LOCAL.listLength = arrayLen(LOCAL.completeList)>

<!---We are going to return an array with the values--->
<cfset LOCAL.returnArray = []>

<!---Loop until we have reached the required number--->
<cfloop from="1" to="arguments.numOutput" index="LOCAL.i">
<!---Append the random item to the new array--->
<cfset arrayAppend(LOCAL.returnArray,LOCAL.completeList[randRange(1,LOCAL.listLength)])>
<!---Shorten the length of the list so that we are not out of bounds on the next iteration--->
<cfset LOCAL.listLength -= 1>
</cfloop>

<cfreturn LOCAL.returnArray>
</cffunction>


Feb 4, 2009 at 2:51 AM // reply »
2 Comments

Hi Brandon,

Does that take care of duplicate selections? It seems like you would have a chance of grabbing the first element of the complete list with each loop iteration.

With a List (MyList) and a desire of 4 randomized values (ToValue), you could alter the array code to:

<cfset ListLength = ListLen(MyList)>
<cfif ListLength LTE ToValue>
<cfset ToValue = ListLength-1>
</cfif>
<cfloop from="1" to="#ToValue#" index="X">
<cfset Y = RandRange(X, ListLength)>
<cfset Element = ListGetAt(MyList,X)>
<cfset MyList = ListSetAt(MyList,X,ListGetAt(MyList,Y))>
<cfset MyList = ListSetAt(MyList,Y,Element>
</cfloop>

and have something on the order of O(tovalue) without a chance of duplicated values (assuming no duplicates existed in MyList to begin with).

-Lyle


Feb 4, 2009 at 1:02 PM // reply »
42 Comments

@Lyle

You were right about the dups. I wasn't actually removing anything from the array, which is obviously an issue. Try this one-

<cffunction name="randomNums" access="public" returntype="array">
<cfargument name="numOutput" type="numeric" required="yes">
<cfargument name="listValues" type="string" required="yes">

<cfset var local = {}>

<!---Convert the list to an array--->
<cfset LOCAL.completeList = listToArray(arguments.listValues)>

<!---Get the length of the array, don't want to calculate this each loop--->
<cfset LOCAL.listLength = arrayLen(LOCAL.completeList)>

<!---We are going to return an array with the values--->
<cfset LOCAL.returnArray = []>

<!---Loop until we have reached the required number--->
<cfloop from="1" to="#arguments.numOutput#" index="LOCAL.i">
<cfset LOCAL.randNum = randRange(1,LOCAL.listLength)>
<!---Append the random item to the new array--->
<cfset arrayAppend(LOCAL.returnArray,LOCAL.completeList[LOCAL.randNum])>
<!---Shorten the length of the list so that we are not out of bounds on the next iteration--->
<cfset arrayDeleteAt(LOCAL.completeList,LOCAL.randNum)>
<cfset LOCAL.listLength -= 1>
</cfloop>

<cfreturn LOCAL.returnArray>
</cffunction>

<cfdump var="#randomNums(5,"1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20")#">


Feb 6, 2009 at 3:49 PM // reply »
5 Comments

Are we over thinking this?

list="1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20"
list = customDedupFunctionIfNecessary(list);
randList=""

Loop X times{

Index = randRange(1,listLen(list))
randList = ListAppend(randList,listGetAt(list,Index));
list = listDeleteAt(Index);

}


Sep 29, 2010 at 6:05 PM // reply »
2 Comments

I read thru this post and came up with this code. I used it to pick 2 days of the week to send an email message. I wanted the messages to go out on random days during the week. Days selected by code where stored in DB and used for following week...

<cfset daysOfWkList = "1,2,3,4,5,6,7">
<cfset randList="">

<cfloop from="1" to="2" index="i">

<!--- rand number in betwn 1 & daysOfWkList length --->
<cfset RandPos = randRange(1,listLen(daysOfWkList))>

<!--- number in daysOfWkList that's at random number position --->
<cfset NumAtRandPos = ListGetAt(daysOfWkList,RandPos)>

<!--- add number at random position to randList --->
<cfset randList = ListAppend(randList,NumAtRandPos)>

<!--- now delete number at rand position in daysOfWkList --->
<cfset daysOfWkList = ListDeleteAt(daysOfWkList,RandPos)>

</cfloop>

<!--- sort list --->
<cfset randList = ListSort(randList,"numeric")>


Sep 29, 2010 at 10:54 PM // reply »
11,246 Comments

@Pen,

Looks good my man. I'm glad that I could help point you in the right direction.


Sep 30, 2010 at 1:58 AM // reply »
2 Comments

@Ben,

F.Y.I. Pen is short for Penelope...
;-)


Oct 3, 2010 at 9:52 PM // reply »
11,246 Comments

@Pen,

Nice to meet you Penelope :) My lady....


Jul 28, 2011 at 9:50 AM // reply »
1 Comments

Thanks sir for valuable post



Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 24, 2013 at 5:39 PM
Ask Ben: Manually Enforcing Basic HTTP Authorization In ColdFusion
@Adam Oops! My mistake! I hadn't gotten that far in my testing - I'm still baby stepping my way through the process. ... read »
May 24, 2013 at 5:13 PM
Ask Ben: Manually Enforcing Basic HTTP Authorization In ColdFusion
Hi Jason, Thanks for checking up on that, but I still stand firm on my position. :) There are actually two listLast()'s in use, and you're right that the one using a space as a delimiter is fine. ... read »
May 24, 2013 at 4:45 PM
Ask Ben: Manually Enforcing Basic HTTP Authorization In ColdFusion
@Ben I have been lurking your site for quite some time, and haven't stepped up to comment until today. Thanks for all the great info - keep it up! @Adam I believe you are mistaken... as the commen ... read »
May 24, 2013 at 11:21 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@WebManWalking, Ha ha, let's us never speak of justifying "##" notation again :P ... read »
May 24, 2013 at 11:18 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben, Ah, so it was indeed how I vaguely remembered it to be: A direct assignment value = users.id[ i ] causes value to retain the sticky datatype of the query column. Although unnecessary in ... read »
May 24, 2013 at 9:11 AM
Preventing Links In Standalone iPhone Applications From Opening In Mobile Safari
@Brandon, Hi, No, I haven't been able to do that. I have just kept it as it is. ... read »
May 23, 2013 at 9:52 PM
Preventing Links In Standalone iPhone Applications From Opening In Mobile Safari
@Muhmmadibn Did you figure out a solution to launching PDFs? I am running into the same issues myself. There is no way to close the PDF or go back once you launch it. Thanks in advance! ... read »
May 23, 2013 at 6:06 PM
The Girl Who Broke My Heart, And Made Me A Better Person
Good day,ladies and gentle men, my name is Dr AMADI the great spell caster in Africa, i have help so many people for different kind of problems,who say there is no solution to problems on earth, that ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools