I'm currently programming an Excel Function which should return the average of the last 5 non-empty positions of an array. To do that I want to go through the array while inside the function as follows:
Function AVERAGE_LAST_5(rng As Range) As Long
Dim x As Integer, i As Integer, j As Integer, sum As Integer
Dim myArr() As Variant
myArr() = Application.Transpose(Application.Transpose(rng))
x = rng.Count
i = 0:: j = 0:: sum = 0
For i = x To 1 Step -1
If myArr(x).Value <> 0 Then
sum = sum + myArr(x)
j = j + 1
Else
End If
If j = 5 Then Stop
x = x - 1
Next
AVERAGE_LAST_5 = sum / 5
End Function
Problem: the for loop doesn't work, when reaching the first if the program aborts.
Does anyone has had the same problem?
Can anyone help me with it?
myarr will be a two-dimensional array, and not a range. You will need to provide both dimensions:
If isarray(myarr) then
for i = ubound(myarr,1) to lbound(myarr,1) step -1
for j = ubound(myarr,2) to lbound (myarr,2) step -1
if myarr(i,j) <> 0 then
K=k+1
Mysum = mysum + myarr(I,j)
Endif
Next j
Next i
Else ‘ single value
mysum =myarr(I,j)
Endif
Arrays Are Faster
Final Version (Hopefully)
This version additionally has the NumberOfLastValues argument (Required) so you can choose how many values will be summed up and it is shortened with the GoSub...Return statement since the If statement is the same for by rows and by columns.
For some other details look in the First Version below.
Usage
In VBA:
Sub LastAverage()
Debug.Print AvgLast(Range("B4:G14"), 5)
End Sub
In Excel:
=AvgLast(B4:G14,5)
Function AvgLast(SearchRange As Range, ByVal NumberOfLastValues As Long, _
Optional ByVal Row_0_Column_1 As Integer = 0) As Double
Dim vntRange As Variant ' Range Array
Dim i As Long ' Range Array Rows Counter
Dim j As Integer ' Range Array Columns Counter
Dim k As Long ' Values Counter
Dim dblSum As Double ' Values Accumulator
If SearchRange Is Nothing Then Exit Function
vntRange = SearchRange.Value
If Row_0_Column_1 = 0 Then
' By Row
For i = UBound(vntRange) To 1 Step -1
For j = UBound(vntRange, 2) To 1 Step -1
GoSub Calc
Next
Next
Else
' By Column
For j = UBound(vntRange, 2) To 1 Step -1
For i = UBound(vntRange) To 1 Step -1
GoSub Calc
Next
Next
End If
TiDa:
If k > 0 Then
AvgLast = dblSum / k
End If
Exit Function
Calc:
If vntRange(i, j) <> "" And IsNumeric(vntRange(i, j)) Then
k = k + 1
dblSum = dblSum + vntRange(i, j)
If k = NumberOfLastValues Then GoTo TiDa
End If
Return
End Function
First Version
It will return the average if there is at least 1 value and at most 5 values, otherwise it will return 0.
The Row_0_Column_1 arguments parameter is by default 0 and means that the search is done by row (first loop). If it is 1, then the search is done by column (second loop).
The basics are that the range is pasted (depsited) into an array and then the array is searched for existing 'numeric' values and not "" values that are summed up and when reaching the fifth value it 'jumps' out of the loop and divides the sum by 5.
Function AvgLast5(SearchRange As Range, Optional Row_0_Column_1 As Integer = 0) _
As Double
Dim vntRange As Variant ' Range Array
Dim i As Long ' Range Array Rows Counter
Dim j As Integer ' Range Array Columns Counter
Dim k As Long ' Values Counter
Dim dblSum As Double ' Values Accumulator
If SearchRange Is Nothing Then Exit Function
vntRange = SearchRange.Value
If Row_0_Column_1 = 0 Then
' By Row
For i = UBound(vntRange) To 1 Step -1
For j = UBound(vntRange, 2) To 1 Step -1
If vntRange(i, j) <> "" And IsNumeric(vntRange(i, j)) Then
k = k + 1
dblSum = dblSum + vntRange(i, j)
If k = 5 Then GoTo TiDa
End If
Next
Next
Else
' By Column
For j = UBound(vntRange, 2) To 1 Step -1
For i = UBound(vntRange) To 1 Step -1
If vntRange(i, j) <> "" And IsNumeric(vntRange(i, j)) Then
k = k + 1
dblSum = dblSum + vntRange(i, j)
If k = 5 Then GoTo TiDa
End If
Next
Next
End If
TiDa:
If k > 0 Then
AvgLast5 = dblSum / k
End If
End Function
after a couple of tough days at work I've finally got some time to improve my function taking your tips on board.
I've made some changes to enable the function to deal either with 1-Row or 1-Column Ranges. Basic Error handling was also added and a function discripton is as well available (under the FX Excel button).
Feel free to comment and/or use the code.
Here the result:
Function AVERAGE_LAST_N(rng As Range, N As Integer)
Dim NrN As Integer, NrR As Integer, NrC As Integer
Dim i As Integer, j As Integer
Dim sum As Double
Dim myArr As Variant
NrN = rng.Count 'Number of array positions
NrR = rng.Rows.Count 'Number of Rows in the array
NrC = rng.Columns.Count 'Number of Rows in the array
i = 0:: j = 0:: sum = 0 'Counters
'####################################################'
'## Transpose Range into array if row or if column ##'
'####################################################'
If rng.Rows.Count > 1 And rng.Columns.Count = 1 Then 'Transpose a Column Range into an Array
myArr = Application.Transpose(rng)
ElseIf rng.Rows.Count = 1 And rng.Columns.Count > 1 Then 'Transpose a Row Range into an Array
myArr = Application.Transpose(Application.Transpose(rng))
ElseIf rng.Rows.Count > 1 And rng.Columns.Count > 1 Then 'Retunrs an Error if Range is a Matrix *ERR_002*
AVERAGE_LAST_N = "ERR_002"
Exit Function
End If
'####################################################'
'## Transpose Range into array if row or if column ##'
'####################################################'
'################'
'## Start Main ##'
'################'
For i = NrN To 1 Step -1
If IsNumeric(myArr(NrN)) Then
sum = sum + myArr(NrN)
j = j + 1
End If
If j = N Then Exit For
NrN = NrN - 1
Next
AVERAGE_LAST_N = sum / N
'##############'
'## End Main ##'
'##############'
'####################'
'## Error Debuging ##'
'####################'
If j < N Then
AVERAGE_LAST_N = "ERR_001"
Exit Function
End If
'####################'
'## Error Debuging ##'
'####################'
End Function
Sub DescribeFunction()
Dim FuncName As String
Dim FuncDesc As String
Dim Category As String
Dim ArgDesc(1) As String
FuncName = "AVERAGE_LAST_N"
FuncDesc = "Returns the average of the last N non-empty values in the selected Range"
Category = 14 'Text category
ArgDesc(0) = "Range that contains the values" & Chr(10) & _
"ERR_001 - There are not enought non-empty or null values in the range" & Chr(10) & _
"ERR_002 - Selected range is a matrix and not a row or column range"
ArgDesc(1) = "Dimention of the sample" & Chr(10) & _
"ERR_001 - There are not enought non-empty or null values in the range" & Chr(10) & _
"ERR_002 - Selected range is a matrix and not a row or column range"
Application.MacroOptions _
Macro:=FuncName, _
Description:=FuncDesc, _
Category:=Category, _
ArgumentDescriptions:=ArgDesc
End Sub
'#######################################################################################
' ###############################################
' ############# Error DB ##############
' ###############################################
'
'
' ERR_001 - There are not enought non-empty values in the range
' ERR_002 - Selected range is a matrix and not a row or column range
'
Rafa
Related
I'm actually facing a problem today. I'm trying to find a row in an Excel Table and return it, based on an entire array of values supposed to match the table
There's a table exemple, they always start with an ID Column that is missing from my Array.
This listbox are the data that I'm getting in my array.
I'm actually using a function that search for a perfect match of my array into a specified table. But I need to make it start on second columns of table.
Here's my function.
Function checkDuplicate(ws As Worksheet, valuesArray As Variant) As Boolean
Dim i As Long, n As Long, j As Long, z As Long
Dim ar
If ws.Name = "Interface" Or ws.Name = "Listes" Then Exit Function
z = LBound(valuesArray)
n = UBound(valuesArray) - z + 1
With ws
ar = .UsedRange.Columns(1).Resize(, n)
For i = 1 To UBound(ar)
j = 1
Do
If ar(i, j) <> valuesArray(j + z - 1) Then
Exit Do
End If
j = j + 1
Loop While j <= n
If j > n Then
checkDuplicate = True
Exit Function
End If
Next
End With
End Function
Any help would be higlhy appreciated, Thank.
This worked for me:
Sub Tester()
Debug.Print checkDuplicate(ActiveSheet, Array("A", "B", "C", "D"))
End Sub
Function checkDuplicate(ws As Worksheet, valuesArray As Variant) As Boolean
Dim i As Long, n As Long, j As Long, z As Long
Dim ar, col As Long, sz As Long
If ws.Name = "Interface" Or ws.Name = "Listes" Then Exit Function
sz = UBound(valuesArray) - LBound(valuesArray) + 1 'size of valuesArray
'pick up data starting with second column
ar = ws.UsedRange.Columns(2).Resize(, sz).Value
For i = 1 To UBound(ar, 1)
checkDuplicate = False
col = 1
For j = LBound(valuesArray) To UBound(valuesArray)
checkDuplicate = ar(i, col) = valuesArray(j) 'match
If Not checkDuplicate Then Exit For 'no match: stop checking
col = col + 1 'next column in sheet array
Next j
If checkDuplicate Then Exit Function 'all columns matched - done searching
Next i
End Function
I'm completely new to VBA and have decided to try recreate excels built in functions. I'm currently trying to create a function that finds the median. for example, it first identifies whether the array is column vector or row vector. i used bubble sort to sort my array in ascending order and then apply a code to find the median value of the sorted array.
However i seem to get a error during the sort, it exists when it tries to swap two values. i get #VALUE error.
Function mymedian(x As Range) As Double
' order array of values asc
' use bubblesort
Dim nr As Integer
Dim nc As Integer
Dim i As Integer
Dim j As Integer
Dim temp As Double
Dim n As Integer
nr = x.Rows.count
nc = x.Columns.count
' col vector
If nc = 1 Then
For i = 2 To nr
For j = 2 To nr
If x.Cells(j - 1, 1).Value > x.Cells(j, 1).Value Then
temp = x.Cells(j, 1)
x.Cells(j, 1).Value = x.Cells(j - 1, 1).Value ' code exists here
x.Cells(j - 1, 1) = temp
n = n + 1
End If
Next j
Next i
Else
' row vector
If nc > 1 Then
For i = 2 To nc
For j = 2 To nc
If x.Cells(1, j - 1).Value > x.Cells(1, j).Value Then
temp = x.Cells(1, j)
x.Cells(1, j) = x.Cells(1, j - 1).Value
x.Cells(1, j - 1) = temp
n = n + 1
End If
Next j
Next i
End If
End If
As a sub this works fine, does this imply bubble sorts only work as sub routines? i also tried to call the sub within a function, however this wasn't working.
Sub bubblesort()
Dim x As Range
Set x = Selection
Dim nr As Integer
Dim temp As Double
Dim i As Integer
Dim j As Integer
nr = x.Rows.count
For i = 2 To nr
For j = 2 To nr
If x.Cells(j - 1, 1).Value > x.Cells(j, 1).Value Then
temp = x.Cells(j, 1)
x.Cells(j, 1) = x.Cells(j - 1, 1)
x.Cells(j - 1, 1) = temp
End If
Next j
Next i
End Sub
Function middle(x As Range)
Dim n As Integer
Dim mid As Double
Call bubblesort(x)
n = x.Rows.count
mid = x.Cells(n / 2, 1).Value
middle = mid
End Function
Reinventing the Wheel: VBA Median UDF
Reinventing the wheel
Median
MEDIAN
VarType
Cell error values
Function MyMedian(ByVal SourceRange As Range) As Variant
Const ProcName As String = "MyMedian"
On Error GoTo ClearError
' Calculate the source range number of cells ('dnCount').
Dim srCount As Long: srCount = SourceRange.Rows.Count
Dim scCount As Long: scCount = SourceRange.Columns.Count
Dim dnCount As Long: dnCount = srCount * scCount
Dim sData() As Variant
' Write the values from the source range to the source array ('sData'),
' a 2D one-based array.
If dnCount = 1 Then ' one cell
ReDim sData(1 To 1, 1 To 1): sData(1, 1) = SourceRange.Value
Else ' multiple cells
sData = SourceRange.Value
End If
' Define the destination array('dArr'), a 1D one-based array.
Dim dArr() As Double: ReDim dArr(1 To dnCount)
Dim sValue As Variant
Dim sr As Long, sc As Long
Dim sNumber As Double
Dim dn As Long, n As Long, cn As Long
Dim dNumber As Double
' Bubble sort the numbers in the destination array
' while reading from the source array.
For sr = 1 To srCount
For sc = 1 To scCount
sValue = sData(sr, sc)
If VarType(sValue) = vbDouble Then ' the source value is a number
sNumber = CDbl(sValue)
dn = dn + 1
' Locate a greater number in the destination array.
For n = 1 To dn - 1
dNumber = dArr(n)
If dNumber > sNumber Then Exit For
Next n
' Shift the greater destination numbers to the right.
If n < dn Then
For cn = dn To n + 1 Step -1
dArr(cn) = dArr(cn - 1)
Next cn
'Else ' the source number is the greatest number; do nothing
End If
' Write the current source number to the destination array.
dArr(n) = sNumber
'Else ' the source value is not a number; do nothing
End If
Next sc
Next sr
' Mimicking the Excel 'MEDIAN' function to return '#NUM!'
' when there is no number in the source range.
If dn = 0 Then MyMedian = CVErr(xlErrNum): Exit Function
' Return the median using the middle destination array value(s).
If dn Mod 2 = 0 Then ' even
MyMedian = (dArr(dn / 2) + dArr(dn / 2 + 1)) / 2
Else ' odd
MyMedian = dArr(Int(dn / 2) + 1)
End If
ProcExit:
Exit Function
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Function
I have been trying to sort the Column values from A to Z which are populated in the List Box.
I have tried with the following but it does not adjust it. Any help will be appreciated.
Dim ws As Worksheet
Dim rng As Range
Dim myArray
Set ws = Sheets("Sheet2")
Set rng = ws.Range("A2:A" & ws.Range("A" & ws.Rows.Count).End(xlUp).Row), Order1:=xlAscending
With Me.ListBox1
.Clear
.ColumnHeads = False
.ColumnCount = rng.Columns.Count
MyArray = rng
.List = SortArray(myArray)
End With
I want to use the Arrays for Sorting Function which will be populated to Listbox.
Sub SortArray(myListBox As MSForms.ListBox, Optional resetMacro As String)
Dim j As Long
Dim i As Long
Dim temp As Variant
If resetMacro <> "" Then
Run resetMacro, myListBox
End If
With myListBox
For j = 0 To .ListCount - 2
For i = 0 To .ListCount - 2
If LCase(.List(i)) > LCase(.List(i + 1)) Then
temp = .List(i)
.List(i) = .List(i + 1)
.List(i + 1) = temp
End If
Next i
Next j
End With
End Sub
Method 1: Sort Data in Cells
You need to sort the range using the Range.Sort method
Set rng = ws.Range("A2:A" & ws.Range("A" & ws.Rows.Count).End(xlUp).Row)
rng.Sort key1:=ws.Range("A2"), order1:=xlAscending, Header:=xlNo
Also see VBA Excel sort range by specific column.
Method 2: Sort Data in Array
Or load the data into an array and sort the array. See VBA array sort function?
Note: The QuickSort algorithm was retrieved from the link above.
Option Explicit
Private Sub LoadButton_Click()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet2")
Dim DataRange As Range
Set DataRange = ws.Range("A2:A" & ws.Range("A" & ws.Rows.Count).End(xlUp).Row)
' 2-dimensional array of the data
Dim DataArray() As Variant
DataArray = DataRange.Value
' Sort data in 2-dimensional array DataArray
QuickSortArray SortArray:=DataArray, SortColumn:=1
' Load sorted data into ListBox
SortedListForm.SortedListBox.List = DataArray
End Sub
' QickSort algorithm that takes a 2-dimensional array
Public Sub QuickSortArray(ByRef SortArray As Variant, Optional ByVal Min As Long = -1, Optional ByVal Max As Long = -1, Optional ByVal SortColumn As Long = 0)
On Error Resume Next
'Sort a 2-Dimensional array
' SampleUsage: sort arrData by the contents of column 3
'
' QuickSortArray arrData, , , 3
'
'Posted by Jim Rech 10/20/98 Excel.Programming
'Modifications, Nigel Heffernan:
' ' Escape failed comparison with empty variant
' ' Defensive coding: check inputs
Dim i As Long
Dim j As Long
Dim RowTemp As Variant
Dim ColTempIdx As Long
If IsEmpty(SortArray) Then
Exit Sub
End If
If InStr(TypeName(SortArray), "()") < 1 Then 'IsArray() is somewhat broken: Look for brackets in the type name
Exit Sub
End If
If Min = -1 Then
Min = LBound(SortArray, 1)
End If
If Max = -1 Then
Max = UBound(SortArray, 1)
End If
If Min >= Max Then ' no sorting required
Exit Sub
End If
i = Min
j = Max
Dim SortItem As Variant
SortItem = Empty
SortItem = SortArray((Min + Max) \ 2, SortColumn)
' We send 'Empty' and invalid data items to the end of the list:
If IsObject(SortItem) Then ' note that we don't check isObject(SortArray(n)) - SortItem *might* pick up a valid default member or property
i = Max
j = Min
ElseIf IsEmpty(SortItem) Then
i = Max
j = Min
ElseIf IsNull(SortItem) Then
i = Max
j = Min
ElseIf SortItem = "" Then
i = Max
j = Min
ElseIf VarType(SortItem) = vbError Then
i = Max
j = Min
ElseIf VarType(SortItem) > 17 Then
i = Max
j = Min
End If
Do While i <= j
Do While SortArray(i, SortColumn) < SortItem And i < Max
i = i + 1
Loop
Do While SortItem < SortArray(j, SortColumn) And j > Min
j = j - 1
Loop
If i <= j Then
' Swap the rows
ReDim RowTemp(LBound(SortArray, 2) To UBound(SortArray, 2))
For ColTempIdx = LBound(SortArray, 2) To UBound(SortArray, 2)
RowTemp(ColTempIdx) = SortArray(i, ColTempIdx)
SortArray(i, ColTempIdx) = SortArray(j, ColTempIdx)
SortArray(j, ColTempIdx) = RowTemp(ColTempIdx)
Next ColTempIdx
Erase RowTemp
i = i + 1
j = j - 1
End If
Loop
If (Min < j) Then
QuickSortArray SortArray, Min, j, SortColumn
End If
If (i < Max) Then
QuickSortArray SortArray, i, Max, SortColumn
End If
End Sub
I am trying to write a simple randomizing program that reads from a column of names and randomly writes them to three columns of four. I have something that kind of works, but it is duplicating my names and I can figure out how to fix it with arrays or collections as those wont let me compare values. Thank you in advance.
Goal: randomization without doubling up two names
Problem: comparing and writing (to worksheet) collection and/or arrays
Option Explicit
Private Sub Randomize_Click()
Dim HowMany As Integer
Dim NoOfNames As Long
Dim RandomNumber As Integer
Dim Names(), dub() As String 'Array to store randomly selected names
Dim i, j, r, a, p As Byte
Dim CellsOut As Long 'Variable to be used when entering names onto worksheet
Dim ArI As Byte 'Variable to increment through array indexes
Application.ScreenUpdating = False
HowMany = 4 ' use with a third loops?
CellsOut = 4
For a = 1 To 6
For r = 1 To 3
For j = 2 To 5
'CellsOut = i 'turn this into loops
ReDim Names(1 To 4) 'Set the array size to how many names required
NoOfNames = Application.CountA(Worksheets("Employees").Range("A:A")) - 1 ' Find how many
names in the list
i = 1
Do While i <= HowMany
RandomNo:
RandomNumber = Application.RandBetween(2, NoOfNames + 1)
dub = RandomNumber
'dub.Add Unit.Value
If Names(i) = Cells(RandomNumber, 1).Value Then
'If Names(i) = dub(Unit) Then
GoTo RandomNo
End If
Names(i) = Worksheets("Employees").Cells(RandomNumber, 1).Value ' Assign random
name to the array
i = i + 1 '
Loop
'Loop through the array and enter names onto the worksheet
For ArI = LBound(Names) To UBound(Names)
Cells(CellsOut, j) = Names(ArI)
CellsOut = CellsOut + 1
Next ArI
Application.ScreenUpdating = True
CellsOut = 4
Next j
Next r
Next a
End Sub
Display
Names
Random Names
Current Setup
This solution uses the dictionary to randomize numbers which I was exploring earlier today.
The complete code goes into a standard module.
Adjust the three constants at the beginning of randomizeNames.
You only run randomizeNames, e.g. via a command button:
Private Sub Randomize_Click()
randomizeNames
End Sub
The Code
Option Explicit
Sub randomizeNames()
' Constants
Const srcFirst As String = "A2"
Const NoC As Long = 3
Const tgtFirst As String = "C2"
' Define Source First Cell Range ('cel').
Dim cel As Range
Set cel = Range(srcFirst)
' Define Source Last Cell Range ('rng').
Dim rng As Range
Set rng = Cells(Rows.Count, cel.Column).End(xlUp)
' Define Source Column Range ('rng').
Set rng = Range(cel, rng)
' Define Number of Elements (names) ('NoE').
Dim NoE As Long
NoE = rng.Rows.Count
' Write values from Source Column Range to Source Array ('Source').
Dim Source As Variant
If NoE > 1 Then
Source = rng.Value
Else
ReDim Source(1 To 1, 1 To 1)
Source(1, 1) = rng.Value
End If
' Define Random Numbers Array ('RNA').
Dim RNA As Variant
' This line uses both functions.
RNA = getDictionary(Dictionary:=getRandomDictionary(1, NoE), _
FirstOnly:=True)
' Instead of numbers, write elements from Source Array
' to Random Number Array (Random Names Array).
Dim i As Long
For i = 1 To NoE
RNA(i, 1) = Source(RNA(i, 1), 1)
Next i
' Define Number of Rows in Target Array ('NoR') and the Remainder
' of elements ('Remainder').
Dim NoR As Long
NoR = Int(NoE / NoC)
Dim Remainder As Long
Remainder = NoE Mod NoC
If Remainder > 0 Then
NoR = NoR + 1
Else
Remainder = NoC
End If
' Define Target Array ('Target').
Dim Target As Variant
ReDim Target(1 To NoR, 1 To NoC)
' Declare additional variables.
Dim j As Long ' Target Array Columns Counter
Dim k As Long ' Random Names Array Rows Counter
' Write values from Random Names Array to Target Array.
For i = 1 To NoR - 1
For j = 1 To NoC
k = k + 1
Target(i, j) = RNA(k, 1)
Next j
Next i
For j = 1 To Remainder
k = k + 1
Target(i, j) = RNA(k, 1)
Next j
' Define Target First Cell Range ('cel').
Set cel = Range(tgtFirst)
' Clear contents from Target First Cell Range to bottom-most cell
' of last column of Target Range.
cel.Resize(Rows.Count - cel.Row + 1, NoC).ClearContents
' Write values from Target Array to Target Range.
Range(tgtFirst).Resize(NoR, NoC).Value = Target
End Sub
Function getRandomDictionary(ByVal LowOrHigh As Long, _
ByVal HighOrLow As Long) _
As Object
' Define Numbers Dictionary ('dict').
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
' Define the lower number ('Minimum') and the Number of Elements ('NoE').
Dim NoE As Long
Dim Minimum As Long
If LowOrHigh < HighOrLow Then
Minimum = LowOrHigh
NoE = HighOrLow - LowOrHigh + 1
Else
Minimum = HighOrLow
NoE = LowOrHigh - HighOrLow + 1
End If
' Write random list of numbers to Numbers Dictionary.
Dim Current As Long
Do
' Randomize ' Takes considerably longer.
Current = Int(Minimum + NoE * Rnd)
dict(Current) = Empty
Loop Until dict.Count = NoE
' Write result.
Set getRandomDictionary = dict
End Function
Function getDictionary(Dictionary As Object, _
Optional ByVal Horizontal As Boolean = False, _
Optional ByVal FirstOnly As Boolean = False, _
Optional ByVal Flip As Boolean = False) _
As Variant
' Validate Dictionary.
If Dictionary Is Nothing Then
GoTo ProcExit
End If
Dim NoE As Long
NoE = Dictionary.Count
If NoE = 0 Then
GoTo ProcExit
End If
' Write values from Dictionary to Data Array ('Data').
Dim Data As Variant
Dim Key As Variant
Dim i As Long
If Not Horizontal Then
If Not FirstOnly Then
ReDim Data(1 To NoE, 1 To 2)
If Not Flip Then
For Each Key In Dictionary.Keys
i = i + 1
Data(i, 1) = Key
Data(i, 2) = Dictionary(Key)
Next Key
Else
For Each Key In Dictionary.Keys
i = i + 1
Data(i, 1) = Dictionary(Key)
Data(i, 2) = Key
Next Key
End If
Else
ReDim Data(1 To NoE, 1 To 1)
If Not Flip Then
For Each Key In Dictionary.Keys
i = i + 1
Data(i, 1) = Key
Next Key
Else
For Each Key In Dictionary.Keys
i = i + 1
Data(i, 1) = Dictionary(Key)
Next Key
End If
End If
Else
If Not FirstOnly Then
ReDim Data(1 To 2, 1 To NoE)
If Not Flip Then
For Each Key In Dictionary.Keys
i = i + 1
Data(1, i) = Key
Data(2, i) = Dictionary(Key)
Next Key
Else
For Each Key In Dictionary.Keys
i = i + 1
Data(1, i) = Dictionary(Key)
Data(2, i) = Key
Next Key
End If
Else
ReDim Data(1 To 1, 1 To NoE)
If Not Flip Then
For Each Key In Dictionary.Keys
i = i + 1
Data(1, i) = Key
Next Key
Else
For Each Key In Dictionary.Keys
i = i + 1
Data(1, i) = Dictionary(Key)
Next Key
End If
End If
End If
' Write result.
getDictionary = Data
ProcExit:
End Function
List of US Top 30 Names
James
John
Robert
Michael
William
Mary
David
Joseph
Richard
Charles
Thomas
Christopher
Daniel
Elizabeth
Matthew
Patricia
George
Jennifer
Linda
Anthony
Barbara
Donald
Paul
Mark
Andrew
Edward
Steven
Kenneth
Margaret
Joshua
I am trying to create a series of unique (non-duplicating) random numbers within a user defined range. I have managed to create the random numbers, but I am getting duplicate values. How can I ensure that the random numbers will never be a duplicate?
Sub GenerateCodesUser()
Application.ScreenUpdating = False
Worksheets("Users").Activate
Dim MINNUMBER As Long
Dim MAXNUMBER As Long
MINNUMBER = 1000
MAXNUMBER = 9999999
Dim Row As Integer
Dim Number As Long
Dim high As Double
Dim Low As Double
Dim i As Integer
If (CustomCodes.CardNumberMin.Value = "") Then
MsgBox ("Fill Card Number Field!")
Exit Sub
ElseIf (CustomCodes.CardNumberMin.Value < MINNUMBER) Then
MsgBox ("Card Number Value must be equal or higher then" & MINNUMBER)
Exit Sub
End If
If (CustomCodes.CardNumberMax.Value = "") Then
MsgBox ("Fill Card Number Field!")
Exit Sub
ElseIf (CustomCodes.CardNumberMax.Value > MAXNUMBER) Then
MsgBox ("Card Number Value must be equal or higher then " & MAXNUMBER)
Exit Sub
End If
Low = CustomCodes.CardNumberMin.Value
high = CustomCodes.CardNumberMax.Value '<<< CHANGE AS DESIRED
If (Low < 1000) Then
'break
End If
For i = 1 To Cells(1, 1).End(xlToRight).Column
If InStr(Cells(1, i), "CardNumber") Then
Row = 2
While Cells(Row, 1) <> 0
Do
Number = ((high - Low + 1) * Rnd() + Low)
Loop Until Number > Low
Cells(Row, i) = Number
Row = Row + 1
Wend
End If
Next
Application.ScreenUpdating = True
End Sub
Here's a method of guaranteeing unique integer random numbers. Inline comments describe the method.
Function UniuqeRandom(Mn As Long, Mx As Long, Sample As Long) As Long()
Dim dat() As Long
Dim i As Long, j As Long
Dim tmp As Long
' Input validation checks here
If Mn > Mx Or Sample > (Mx - Mn + 1) Then
' declare error to suit your needs
Exit Function
End If
' size array to hold all possible values
ReDim dat(0 To Mx - Mn)
' Fill the array
For i = 0 To UBound(dat)
dat(i) = Mn + i
Next
' Shuffle array, unbiased
For i = UBound(dat) To 1 Step -1
tmp = dat(i)
j = Int((i + 1) * Rnd)
dat(i) = dat(j)
dat(j) = tmp
Next
'original biased shuffle
'For i = 0 To UBound(dat)
' tmp = dat(i)
' j = Int((Mx - Mn) * Rnd)
' dat(i) = dat(j)
' dat(j) = tmp
'Next
' Return sample
ReDim Preserve dat(0 To Sample - 1)
UniuqeRandom = dat
End Function
use it like this
Dim low As Long, high As Long
Dim rng As Range
Dim dat() As Long
Set rng = Range(Cells(1, 1), Cells(1, 1).End(xlToRight))
dat = UniuqeRandom(low, high, rng.Columns.Count)
rng.Offset(1, 0) = dat
Note: see this Wikipedia article regarding shuffle bias
The edit fixed one source of bias. The inherent limitations of Rnd (based on a 32 bit seed) and Modulo bias remain.
I see you have an accepted answer, but for whatever it's worth here is my stab at this question. This one uses a boolean function instead of numerical arrays. It's very simple yet fast. The advantage of it, which I'm not saying is perfect, is an effective solution for numbers in a long range because you only ever check the numbers you have already picked and saved and don't need a potentially large array to hold the values you have rejected so it won't cause memory problems because of the size of the array.
Sub UniqueRandomGenerator()
Dim N As Long, MaxNum As Long, MinNum As Long, Rand As Long, i As Long
MinNum = 1 'Put the input of minimum number here
MaxNum = 100 'Put the input of maximum number here
N = MaxNum - MinNum + 1
ReDim Unique(1 To N, 1 To 1)
For i = 1 To N
Randomize 'I put this inside the loop to make sure of generating "good" random numbers
Do
Rand = Int(MinNum + N * Rnd)
If IsUnique(Rand, Unique) Then Unique(i, 1) = Rand: Exit Do
Loop
Next
Sheet1.[A1].Resize(N) = Unique
End Sub
Function IsUnique(Num As Long, Data As Variant) As Boolean
Dim iFind As Long
On Error GoTo Unique
iFind = Application.WorksheetFunction.Match(Num, Data, 0)
If iFind > 0 Then IsUnique = False: Exit Function
Unique:
IsUnique = True
End Function
It Works perfectly:
Option Base 1
Public Function u(a As Variant, b As Variant) As Variant
Application.Volatile
Dim k%, p As Double, flag As Boolean, x() As Variant
k = 1
flag = False
ReDim x(1)
x(1) = Application.RandBetween(a, b)
Do Until k = b - a + 1
Do While flag = False
Randomize
p = Application.RandBetween(a, b)
'Debug.Assert p = 2
resultado = Application.Match(p, x, False)
If IsError(resultado) Then
k = k + 1
ReDim Preserve x(k)
x(k) = p
flag = True
Else
flag = False
End If
Loop
flag = False
Loop
u = x
End Function