Ask Ben: Selecting Random Values Without Repetition In ColdFusion

Posted February 3, 2009 at 4:33 PM

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.

 Launch code in new window » Download code as text file »

  • <!--- 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.

Download Code Snippet ZIP File

Post Comment  |  Ask Ben  |  Permalink  |  Other Searches  |  Print Page




Learning ColdFusion 9 - ColdFusion 9 tutorials, samples, examples, demos

Reader Comments

Feb 3, 2009 at 5:15 PM // reply »
15 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 »
6,516 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 »
177 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 »
39 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 »
39 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 »
4 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);

}


Post Comment  |  Ask Ben

Recent Blog Comments
Nov 22, 2009 at 1:56 AM
Learning ColdFusion 9: Using CFQuery In CFScript Can Enable SQL Injection Attacks
Why adobe would give you script equivalent of cfquery is beyond me. I love cfquery tag because it helps me wriite clean sql, and get away from the horrible jdbc queries If I wanted to write javali ... read »
Nov 22, 2009 at 1:45 AM
Streaming Text Using ColdFusion's CFContent Tag And The Variable Attribute
The reason you would want to do this is to stream. Ack json/xml files to ria clients I used thus technique before because putting json in response stream causes debugging info to come thru As well a ... read »
Nov 21, 2009 at 6:47 PM
Hal Helms - Real World Object Oriented Development, Sarasota - Day Five
@charlie griefer, Thank you.. ... read »
Nov 21, 2009 at 5:15 PM
Using ColdFusion Structures To Remove Duplicate List Values
@Jose Galdamez, Oh heh yeah I didn't paste the whole code. I should have defined the vars -- my bad. It's fixed thou. Thanks. ... read »
Nov 21, 2009 at 4:49 PM
Styling The ColdFusion 8 WriteToBrowser CFImage Output
Great work yet again Ben! Whilst I didn't use this whole code, I copied some of your regex code for a similar problem with the lack of an alt attribute and unescaped ampersands in CFIMAGE for Railo 3 ... read »
Nov 21, 2009 at 1:13 PM
My First ColdFusion Builder Extension - Encrypting And Decrypting CFM / CFC Files
@Ben, Because I am pedantic, I just want to make sure that everyone knows there is absolutely no encryption going on. There is only encoding and obfuscation. The cfencode tool only obfuscates your C ... read »
Nov 21, 2009 at 12:28 PM
Using ColdFusion Structures To Remove Duplicate List Values
@Jody I can't seem to get your code sample to work. If you are still having problems, try this code out and see if it gets you what you wanted. <!--- Comma delimited list with various duplicates ... read »
Nov 21, 2009 at 11:03 AM
Groovy Operator Overloading Does Not Work In The ColdFusion Context
Hi Ben, Thanks for this informative post. Now I am reading ur old posts too ... read »