Although you guys posted really great solutions, I don't have the heart to just give up and so I had to try it on my own to find a better code I posted within the question; here we go...
The basic idea is to generate a table consisting of two columns where the first one contains random numbers with duplicates and the second one contains indexes that constitute all the possible random numbers. The next step is to sort this table by the first column. Finally you need to pick values from the second column from as many rows as you need random numbers (in my approach I use the last rows). All the retrieved numbers are unique as the second column does not contain any duplicates. Here is an example, provided that we need 8
random numbers out of 1,2,3,4,5,6,7,8,9,10
:
generated sorted returned
table table numbers
--------------------------------
1,1 1,1 -
9,2 1,6 -
8,3 10,8 8
3,4 3,4 4
9,5 4,7 7
1,6 5,9 9
4,7 8,10 10
10,8 8,3 3
5,9 9,2 2
8,10 9,5 5
As you can see there are no duplicate numbers returned. The great advantage of this method is that the computation of random numbers does not have to be repeated in case the same one has already been used. The disadvantage is that the full table needs to be created even in case only a few unique random numbers are needed out of a huge pool of possible numbers.
I have to admit that this technique is not my idea, but unfortunately I don't know to whom it is credited...
Here is my code, containing some explanatory remarks:
@echo off
setlocal EnableExtensions EnableDelayedExpansion
rem total number of random numbers, duplicate flag (`0` means no duplicates):
set /A "RND_TOTAL=8, FLAG_DUP=0"
rem range for random numbers (minimum, maximum, interval):
set /A "RND_MIN=1, RND_MAX=10, RND_INTER=1"
rem write table-like data to temporary file:
> "%~n0.tmp" (
rem loop through all possible random numbers:
for /L %%I in (%RND_MIN%,%RND_INTER%,%RND_MAX%) do (
rem compute a random number:
set /A "RND_NUM=!RANDOM!%%((RND_MAX-RND_MIN)/RND_INTER+1)*RND_INTER+RND_MIN"
if %FLAG_DUP% EQU 0 (
rem duplicates denied, so build row with random number as first column:
echo !RND_NUM!,%%I
) else (
rem duplicates allowed, so build row with loop counter as first column:
echo %%I,!RND_NUM!
)
)
)
rem determine how many items of the table need to be skipped to get total number:
set /A "SKIP=(RND_MAX-RND_MIN)/RND_INTER+1, SKIP-=RND_TOTAL"
if %SKIP% GTR 0 (
set "SKIP=skip=%SKIP% "
) else (
set "SKIP="
)
rem read table-like data from temporary file:
set /A "RND_COUNT=0"
< "%~n0.tmp" (
rem sort rows (lines) of table-like data alphabetically, enumerate them:
for /F "%SKIP%tokens=2 delims=," %%N in ('sort') do (
set /A "RND_COUNT+=1"
rem store and return random number:
set /A "RND_NUM[!RND_COUNT!]=%%N"
echo %%N
)
)
rem clean up temporary file:
del /Q "%~n0.tmp"
endlocal
exit /B
In this script, the above described table-like data is stored into a temporary file, which is then passed over to the sort
command which does the alphabetic sorting.
The resulting random sequence is output to the console and stored into the array-like variables RND_NUM[1:8]
(which is only available until the endlocal
command is executed, of course).
Update:
Based on the fact that set
is capable of sorting as well, and that the above described table can also be reflected in names of environment variables in a 2D-array-like style (I used RND_NUM[_,_]
here), the following script does no longer use a temporary file. the random sequence is again output to the console and also stored into the 1D-array RND_NUM[1:8]
).
But before we come to the script, I want to show you the 2D-array that represents the said table, with the same sample data as above (note that the values of the variables are totally irrelevant):
generated sorted returned
2D-array 2D-array numbers
------------------------------------------------
RND_NUM[1,1] RND_NUM[1,1] -
RND_NUM[9,2] RND_NUM[1,6] -
RND_NUM[8,3] RND_NUM[10,8] 8
RND_NUM[3,4] RND_NUM[3,4] 4
RND_NUM[9,5] RND_NUM[4,7] 7
RND_NUM[1,6] RND_NUM[5,9] 9
RND_NUM[4,7] RND_NUM[8,10] 10
RND_NUM[10,8] RND_NUM[8,3] 3
RND_NUM[5,9] RND_NUM[9,2] 2
RND_NUM[8,10] RND_NUM[9,5] 5
Here is the code (including some explanatory remarks):
@echo off
setlocal EnableExtensions EnableDelayedExpansion
rem total number of random numbers, duplicate flag (`0` means no duplicates):
set /A "RND_TOTAL=8, FLAG_DUP=0"
rem range for random numbers (minimum, maximum, interval):
set /A "RND_MIN=1, RND_MAX=10, RND_INTER=1"
rem clean up all variables named `RND_NUM*` (optionally):
call :CLEANUP1
rem loop through all possible random numbers:
for /L %%I in (%RND_MIN%,%RND_INTER%,%RND_MAX%) do (
rem compute a random number:
set /A "RND_INDEX=!RANDOM!%%((RND_MAX-RND_MIN)/RND_INTER+1)*RND_INTER+RND_MIN"
if %FLAG_DUP% EQU 0 (
rem duplicates denied, so build 2D-array with random number as first index:
set "RND_NUM[!RND_INDEX!,%%I]=#"
) else (
rem duplicates allowed, so build 2D-array with loop counter as first index:
set "RND_NUM[%%I,!RND_INDEX!]=#"
)
)
rem determine how many items of the 2D-array need to be skipped to get total number:
set /A "SKIP=(RND_MAX-RND_MIN)/RND_INTER+1-RND_TOTAL"
if %SKIP% GTR 0 (
set "SKIP=skip=%SKIP% "
) else (
set "SKIP="
)
rem let `set` output all `RND_NUM*` variables sorted alphabetically, enumerate them:
set /A "RND_COUNT=0"
for /F "%SKIP%tokens=3 delims=[,]" %%I in ('set "RND_NUM"') do (
set /A "RND_COUNT+=1"
rem store and return random number:
set "RND_NUM[!RND_COUNT!]=%%I"
echo %%I
)
rem clean up all 2D-array variables named `RND_NUM*,*` (optionally):
call :CLEANUP2
endlocal
exit /B
:CLEANUP1
for /F "tokens=1 delims==" %%I in ('2^> nul set "RND_NUM"') do (
set "%%I="
)
exit /B
:CLEANUP2
for /F "tokens=1,2,3 delims=,=" %%I in ('set "RND_NUM"') do (
if not "%%K"=="" (
set "%%I,%%J="
)
)
exit /B
The clean-up routines :CLEANUP1
and :CLEANUP2
are considered as optional and just delete any variables whose names start with RND_NUM
(see :CLEANUP1
) at the beginning and whose names contain a ,
(which is true for the 2D-array but not for the 1D-array; see :CLEANUP2
) at the end.