I'm attempting to print small arrays to an Excel spreadsheet. The bulk of the code to loops n times based on the user's discretion.
The output Sub functions print correctly on the first iteration, but when the array changes on the next iteration and the sub functions move to the next line to output, they also modify the first array values in the spreadsheet from the first iteration.
Example: If I go through five iterations, and they all produce different values in their respective arrays, by the 5th iteration All five columns that have been printed are modified to be the same as the last iteration.
I'm trying to prevent the code from replacing previous values.
I've attempted the Erase function for the array inside of the big for loop, which broke the code.
For loop for iterations
Dim Iter As Integer
For Iter = 1 To number_of_iterations
Randomize
reward_present = Int((1 - 0 + 1) * Rnd + 0)
reward_string = reward_present
reward_present = 1
'Randomize whether there is a reward present or not
If reward_present = 1 Then
Dim door_probabilities() As Variant
ReDim door_probabilities(1 To number_of_doors)
Dim remainder As Double
Dim reward_door As Integer
Dim reward_door_string As String
remainder = 1
For i = 1 To (number_of_doors - 1)
door_probabilities(i) = RndDbl(0, remainder)
remainder = remainder - door_probabilities(i)
Next i
door_probabilities(number_of_doors) = remainder
'randomizing probabilities of each door
Dim max As Variant
max = door_probabilities(1)
reward_door = 0
For i = 1 To number_of_doors
If max <= door_probabilities(i) Then
max = door_probabilities(i)
reward_door = i
End If
Next i
reward_door_string = reward_door
'choosing the reward door based on probability
If number_of_doors = 3 Then
random_player_choice = Int((number_of_doors - 1 + 1) * Rnd + 1)
game_doors(random_player_choice) = 1
ArrayFillPlayer1 game_doors, Iter
'choose first player door randomly
'output here
For i = LBound(game_doors) To UBound(game_doors)
msg = msg & game_doors(i) & vbNewLine
Next i
MsgBox "Game doors player choice 1: " + msg
random_host_choice = Int((number_of_doors - 1 + 1) * Rnd + 1)
Do While random_host_choice = random_player_choice
random_host_choice = Int((number_of_doors - 1 + 1) * Rnd + 1)
Loop
If random_host_choice = reward_door Then
Do While random_host_choice = reward_door
random_host_choice = Int((number_of_doors - 1 + 1) * Rnd + 1)
Loop
End If
game_doors(random_host_choice) = 1
ArrayFillHost game_doors, Iter
'choose host door randomly
'output here
For i = LBound(game_doors) To UBound(game_doors)
msg = msg & game_doors(i) & vbNewLine
Next i
MsgBox "Game doors host choice: " + msg
random_player_choice2 = Int((number_of_doors - 1 + 1) * Rnd + 1)
Do While random_player_choice2 = random_host_choice
random_player_choice2 = Int((number_of_doors - 1 + 1) * Rnd + 1)
Loop
game_doors(random_player_choice2) = 1
'choose second player door
ArrayFillPlayer2 game_doors, Iter
For i = LBound(game_doors) To UBound(game_doors)
msg = msg & game_doors(i) & vbNewLine
Next i
ReDim game_doors(1 To number_of_doors)
End If
Sub ArrayFillPlayer1(TempArray As Variant, RowToWrite As Integer)
'Fill a range by transferring an array
Dim CellsDown As Long, CellsAcross As Long
Dim TheRange As Range
CellsDown = 1
CellsAcross = 3
'Cells.Clear
'Set worksheet range
Set TheRange = Range(Cells(RowToWrite, 1), Cells(CellsDown, CellsAcross))
Transfer temporary array to worksheet
TheRange.Value = TempArray
End Sub
Sub ArrayFillHost(TempArray As Variant, RowToWrite As Integer)
'Fill a range by transferring an array
Dim CellsDown As Long, CellsAcross As Long
Dim TheRange As Range
CellsDown = 1
CellsAcross = 6
'Cells.Clear
'Set worksheet range
Set TheRange = Range(Cells(RowToWrite, 4), Cells(CellsDown, CellsAcross))
Transfer temporary array to worksheet
TheRange.Value = TempArray
End Sub
Sub ArrayFillPlayer2(TempArray As Variant, RowToWrite As Integer)
'Fill a range by transferring an array
Dim CellsDown As Long, CellsAcross As Long
Dim TheRange As Range
CellsDown = 1
CellsAcross = 9
'Cells.Clear
'Set worksheet range
Set TheRange = Range(Cells(RowToWrite, 7), Cells(CellsDown, CellsAcross))
Transfer temporary array to worksheet
TheRange.Value = TempArray
End Sub
I expect the output of each consecutive row to be different, but they are all modified retroactively.
Kind of looks like you mean to use Resize() in your subs which fill the array to the sheet?
For example this:
Sub ArrayFillPlayer1(TempArray As Variant, RowToWrite As Integer)
'Fill a range by transferring an array
Dim CellsDown As Long, CellsAcross As Long
Dim TheRange As Range
CellsDown = 1
CellsAcross = 3
'Cells.Clear
'Set worksheet range
Set TheRange = Range(Cells(RowToWrite, 1), Cells(CellsDown, CellsAcross))
Transfer temporary array to worksheet
TheRange.Value = TempArray
End Sub
Here the line:
Set TheRange = Range(Cells(RowToWrite, 1), Cells(CellsDown, CellsAcross))
is the same as:
Set TheRange = Range( Cells(RowToWrite, 1), Range("C1") )
and that "C1" remains constant as RowToWrite increases, so each time you're filling a larger range with TempArray.
This is closer to what you want:
Sub ArrayFillPlayer1(TempArray As Variant, RowToWrite As Integer)
Range(Cells(RowToWrite, 1).Resize(1, 3).Value = TempArray
End Sub
Related
I'm working on a VBA word script that reads in some names and relevant info from an excel sheet, performs some computations to organize them correctly, and then pastes them into the word doc. This went well until I decided to make a function that would move a cell with the value "Anonymous" to the top of a range. For some reason, this isn't happening, and it appears to be because the .Cells method isn't always referring to the cell it was called on.
As the script itself is fairly long, I won't post the entire thing here. However, the relevant parts are a For loop in the main sub which deals with cells with the value "Anonymous"
For curCol = 7 To 15
lastRow = appXL.Cells(appXL.Rows.Count, curCol).End(xlUp).Row
For curRow = 1 To lastRow
Dim curCell As excel.Range
Set curCell = appXL.Cells(curRow, curCol)
Dim anonCount As Integer
anonCount = 0
If curCell.Value = "Anonymous" Or curCell.Value = "Anonymous*" Then
If anonCount < 1 Then
anonCount = anonCount + 1
MoveAnon (curRow), (curCol), (lastRow)
Else
anonCount = anonCount + 1
curCell.Value = curCell.Value + " (" + CStr(anonCount) + ")"
MoveAnon (curRow), (curCol), (lastRow)
End If
End If
Next curRow
Next curCol
You'll notice that within this loop is a call to a subroutine "MoveAnon" which is
Sub MoveAnon(currentRow As Integer, currentCol As Integer, thelastRow As Integer)
Dim text As String
Debug.Print ("Using Row: " + CStr(currentRow) + ", Column: " + CStr(currentCol) + ", Last Row: " + CStr(thelastRow))
text = excel.Application.ActiveSheet.Cells(currentRow, currentCol)
Debug.Print ("Hit On: " & excel.Application.ActiveSheet.Cells(currentRow, currentCol))
If currentRow > 1 Then
excel.Application.ActiveSheet.Range(excel.Application.ActiveSheet.Cells(1, currentCol).Address, excel.Application.ActiveSheet.Cells(currentRow - 1, currentCol).Address).Cut excel.Application.ActiveSheet.Range(excel.Application.ActiveSheet.Cells(2, currentCol).Address)
excel.Application.ActiveSheet.Cells(1, currentCol).Value = text
End If
End Sub
Through testing and with Deubg.Print, I've noticed that the line Debug.Print ("Hit On: " & excel.Application.ActiveSheet.Cells(currentRow, currentCol)) refers to all manner of different cells then the one on which it was called. For example, whenever I use Cells(6, 15), I get a value from a cell that is actually on row 42, column 15. The difference between the cell its called on and the cell it returns is not always the same (I've seen -7, +36, and 0), but it is always in the correct column.
Does anyone have any idea as to what my cause this behavior to arise? Thanks for any help.
It's much faster to read the whole range into an array, then populate another array of the same size with the "Anonymous*" at the top, and replace the range values using the second array.
Eg.
Sub Tester()
Dim curCol As Long, ws As Worksheet
Set ws = ActiveSheet
For curCol = 7 To 15
MoveAnon ws.Range(ws.Cells(1, curCol), _
ws.Cells(ws.Rows.Count, curCol).End(xlUp))
Next curCol
End Sub
'Given a (single-column) range, move all values like "Anonymous*"
' to the top of the range
Sub MoveAnon(rng As Range)
Const TXT As String = "Anonymous*"
Dim v, i As Long, num As Long
Dim arrIn, arrOut, nA As Long, nX As Long
num = Application.CountIf(rng, TXT) 'how many to float up
If num = 0 Then Exit Sub 'nothing to do here?
arrIn = rng.Value 'read to array
ReDim arrOut(1 To UBound(arrIn, 1), 1 To UBound(arrIn, 2)) 'size output array
For i = 1 To UBound(arrIn, 1) 'loop the input array
v = arrIn(i, 1)
If v Like TXT Then
nA = nA + 1
arrOut(nA, 1) = v '"Anonymous*" goes at the top
Else
nX = nX + 1
arrOut(num + nX, 1) = v 'everything else goes below
End If
Next i
rng.Value = arrOut 'replace using the shuffled array
End Sub
I need your help to build a macro that can extract the dates (which are in text format) from a string and report them in a different column - let's say to column K, would you be able to assist?
Below the database in text
Contract
OESX BLT 100 Feb22 Mar22 4200 vs S 5 FESX Mar22 #4080
OESX P 100 Mar22 3050 vs 6 FESX Mar22 #4080
OESX CDIA 100 Feb22 4300 Mar22 4400 vs B 3 FESX Mar22 #4090
OESX CNV 100 Dec23 4100 vs 100 FESX Mar22 #4100
OESX PBUT Feb22 3900 - 4000 - 4100
The length of the column of the database is not fixed, it changes every time.
The final goal would be to put the dates at the beginning of the contract and not in the middle.
I thank you in advance :)
CODE:
Sub Macro8()
Dim sht As Worksheet
Dim fnd As Variant
Dim rplc As Variant
Dim row
Dim column
Dim value
fndList = Array("Dec22 ", "Dec23 ")
rplcList = Array("", "")
Set sht = Sheets("Data")
****For Each cell In Range("A2:A40")
If InStr(cell.Text, fndList) > 0 Then
cell.Offset(0, 1).value = fndList
End If
Next cell****
For x = LBound(fndList) To UBound(fndList)
sht.Cells.Replace What:=fndList(x), Replacement:=rplcList(x), _
LookAt:=xlPart, SearchOrder:=xlByRows, MatchCase:=False, _
SearchFormat:=False, ReplaceFormat:=False
Next x
End Sub
Simple original answer:
Function RearrangeContract(ref As String)
Dim I As Integer
Dim N As Integer
Dim Res As String
Dim Con As String
Con = ref
For I = 1 To Len(ref) - 3
For N = 1 To 12
If Mid(ref, I, 3) = Format(DateSerial(2021, N, 15), "mmm") Then
Res = Res & Mid(ref, I, 5) & " "
Con = Replace(Con, Mid(ref, I, 6), "")
End If
Next N
Next I
RearrangeContract = Res & Con
End Function
Should spit out strings exactly as you requested.
[enter image description here][1]
Either use the function in your own code, or import the contract lines into excel and use =RearrangeContract() as a UDF
And here we have an absolute mess of code for such a small task, but I'm roughly 90% sure it will work perfectly.
FYI: I went the lazy route for the sorting, and borrowed a sorting sub from here: https://bettersolutions.com/vba/arrays/sorting-counting-sort.htm
Should rearrange, sort and filter duplicates
in the top function, you can change the date output format here:
"Res(i) = Format(Res(i), "mmmyy")"
Option Explicit
Option Base 0
Function RearrangeContractUnique(ref As String)
Dim i As Integer 'Character counter
Dim N As Integer 'Month counter
Dim Res() 'Result
Dim Con As String 'Contract - dates
Dim CNT As Integer 'Date found counter
Dim Temp
CNT = 0 'Counter to 0
Con = ref 'Store reference separately
For i = 1 To Len(ref) - 3 'Cycle through character in ref
For N = 1 To 12 'Test each month againt section of ref
If Mid(ref, i, 3) = Format(DateSerial(2021, N, 15), "mmm") Then
CNT = CNT + 1 'Increment counter
ReDim Preserve Res(1 To CNT) 'Resize array
'Debug.Print Mid(ref, i + 3, 2)
Res(CNT) = DateValue(DateSerial(20 & Mid(ref, i + 3, 2), N, 1))
Con = Replace(Con, Mid(ref, i, 6), "") 'Remove date found from ref
End If
Next N
Next i
'Debug.Print "PreSort"
'For i = 1 To CNT
'Debug.Print Res(i)
'Next i
Array_CountingSort Res
'Debug.Print "PostSort"
'For i = 1 To CNT
'Debug.Print Res(i)
'Next i
'Reformat for output
For i = 1 To CNT
Res(i) = Format(Res(i), "mmmyy")
Next i
'Yeah, just shovel more worksheetfunctions into it.
RearrangeContractUnique = Join(Application.WorksheetFunction.Transpose _
(WorksheetFunction.Unique(Application.WorksheetFunction. _
Transpose(Res())))) & " " & Con
End Function
Public Sub Array_CountingSort(ByRef vArrayName As Variant)
Dim vCounting() As Long
Dim lLower As Long
Dim lUpper As Long
Dim larraymin As Long
Dim larraymax As Long
Dim i As Long
Dim j As Long
Dim lnextpos As Long
larraymin = Helper_Minimum(vArrayName)
larraymax = Helper_Maximum(vArrayName)
lLower = LBound(vArrayName)
lUpper = UBound(vArrayName)
ReDim vCounting(larraymin To larraymax)
For i = lLower To lUpper
vCounting(vArrayName(i)) = vCounting(vArrayName(i)) + 1
Next i
lnextpos = lLower
For i = larraymin To larraymax
For j = 1 To vCounting(i)
vArrayName(lnextpos) = i
lnextpos = lnextpos + 1
Next j
Next i
End Sub
Public Function Helper_Maximum(ByVal vArrayName As Variant) As Long
Dim lmaxvalue As Long
Dim lrowlower As Long
Dim lrowupper As Long
Dim i As Long
lrowlower = LBound(vArrayName)
lrowupper = UBound(vArrayName)
lmaxvalue = vArrayName(lrowlower)
For i = lrowlower To lrowupper
If (vArrayName(i) > lmaxvalue) Then
lmaxvalue = vArrayName(i)
End If
Next i
Helper_Maximum = lmaxvalue
End Function
Public Function Helper_Minimum(ByVal vArrayName As Variant) As Long
Dim lminvalue As Long
Dim lrowlower As Long
Dim lrowupper As Long
Dim i As Long
lrowlower = LBound(vArrayName)
lrowupper = UBound(vArrayName)
lminvalue = vArrayName(lrowlower)
For i = lrowlower To lrowupper
If (vArrayName(i) < lminvalue) Then
lminvalue = vArrayName(i)
End If
Next i
Helper_Minimum = lminvalue
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 copy data from one workbook to another based on the values contained in cells in the source workbook that matches the same values in the target workbook. For example, I have a table (Table1) that has four columns say, A1:D5. One of these columns (column A) contains account numbers that match similar account numbers located on another workbook (also in column A). I am trying to find a code that looks through the table (Table1) in the source workbook via the account number column, and if the account number matches the account number in the target workbook, copy and paste the cells on that row in specific locations to the target workbook. Is this possible?
I hope that makes sense. I have looked all over on how to structure such a code, and I was not able to find anything to start the process for this logic.
Any help will be very appreciative.
Thank you
Even if your question is about doing this in VBA, I'm just going to mention that what you are trying to do seems like it could also be done with Power Query.
That being said, if you were to use VBA for this, you would have to use the Match function to find where your rows match and then copy the data from the source to the destination table.
I've adapted the code I provided to this question to better serve your specific needs. One of the things I've done is to add an optional argument called DoOverwrite and set it to false. This will make sure that the information from one row won't be overwritten by another row later down the road.
Sub TableJoinTest()
'Those table columns will have to match for the 2 lines to be a match
Dim MandatoryHeaders() As Variant
MandatoryHeaders = Array("Account Number")
Dim SourceTableAnchor As Range
Set SourceTableAnchor = Workbooks("SourceWorkbook.xlsx").Sheets("Sheet1").Range("A1")
Dim TargetTableAnchor As Range
Set TargetTableAnchor = Workbooks("TargetWorkbook.xlsx").Sheets("Sheet1").Range("A1")
TableJoin _
SourceTableAnchor:=SourceTableAnchor, _
TargetTableAnchor:=TargetTableAnchor, _
MandatoryHeaders:=MandatoryHeaders, _
AddIfMissing:=False, _
IsLogging:=False, _
DoOverwrite:=False
End Sub
Sub TableJoin( _
SourceTableAnchor As Range, _
TargetTableAnchor As Range, _
MandatoryHeaders As Variant, _
Optional OtherHeaders As Variant, _
Optional AddIfMissing As Boolean = False, _
Optional IsLogging As Boolean = False, _
Optional DoOverwrite As Boolean = True)
'''''''''''''''''''''''''''''''''''''''
'Definitions
'''''''''''''''''''''''''''''''''''''''
Dim srng As Range, trng As Range
Set srng = SourceTableAnchor.CurrentRegion
Set trng = TargetTableAnchor.CurrentRegion
Dim sHeaders As Range, tHeaders As Range
Set sHeaders = srng.Rows(1)
Set tHeaders = trng.Rows(1)
'Store in Arrays
Dim sArray() As Variant 'prefix s is for Source
sArray = ExcludeRows(srng, 1).Value2
Dim tArray() As Variant 'prefix t is for Target
tArray = ExcludeRows(trng, 1).Value2
Dim sArrayHeader As Variant
sArrayHeader = sHeaders.Value2
Dim tArrayHeader As Variant
tArrayHeader = tHeaders.Value2
'Find Column correspondance
Dim sMandatoryHeadersColumn As Variant
ReDim sMandatoryHeadersColumn(LBound(MandatoryHeaders) To UBound(MandatoryHeaders))
Dim tMandatoryHeadersColumn As Variant
ReDim tMandatoryHeadersColumn(LBound(MandatoryHeaders) To UBound(MandatoryHeaders))
Dim k As Long
For k = LBound(MandatoryHeaders) To UBound(MandatoryHeaders)
sMandatoryHeadersColumn(k) = Application.Match(MandatoryHeaders(k), sArrayHeader, 0)
tMandatoryHeadersColumn(k) = Application.Match(MandatoryHeaders(k), tArrayHeader, 0)
Next k
Dim sOtherHeadersColumn As Variant
ReDim sOtherHeadersColumn(LBound(OtherHeaders) To UBound(OtherHeaders))
Dim tOtherHeadersColumn As Variant
ReDim tOtherHeadersColumn(LBound(OtherHeaders) To UBound(OtherHeaders))
For k = LBound(OtherHeaders) To UBound(OtherHeaders)
sOtherHeadersColumn(k) = Application.Match(OtherHeaders(k), sArrayHeader, 0)
tOtherHeadersColumn(k) = Application.Match(OtherHeaders(k), tArrayHeader, 0)
Next k
'Merge mandatory headers into one column (aka the helper column method)
Dim i As Long, j As Long
Dim sHelperColumn() As Variant
ReDim sHelperColumn(LBound(sArray, 1) To UBound(sArray, 1), 1 To 1)
For i = LBound(sArray, 1) To UBound(sArray, 1)
For j = LBound(MandatoryHeaders) To UBound(MandatoryHeaders)
sHelperColumn(i, 1) = sHelperColumn(i, 1) & sArray(i, sMandatoryHeadersColumn(j))
Next j
Next i
Dim tHelperColumn() As Variant
ReDim tHelperColumn(LBound(tArray, 1) To UBound(tArray, 1), 1 To 1)
For i = LBound(tArray, 1) To UBound(tArray, 1)
For j = LBound(MandatoryHeaders) To UBound(MandatoryHeaders)
tHelperColumn(i, 1) = tHelperColumn(i, 1) & tArray(i, tMandatoryHeadersColumn(j))
Next j
Next i
'Find all matches
Dim MatchList() As Variant
Dim LoggingColumn() As String
ReDim LoggingColumn(LBound(tArray, 1) To UBound(tArray, 1), 1 To 1)
For i = LBound(sArray, 1) To UBound(sArray, 1)
ReDim MatchList(LBound(tArray, 1) To UBound(tArray, 1))
For j = LBound(tArray, 1) To UBound(tArray, 1)
If sHelperColumn(i, 1) = tHelperColumn(j, 1) Then
MatchList(j) = 1
End If
Next j
'Get the row number for the match
Dim MatchRow As Long
Select Case Application.Sum(MatchList)
Case Is > 1
'Need to do more matching
Dim MatchingScoresList() As Long
ReDim MatchingScoresList(1 To UBound(tArray, 1))
Dim m As Long
For k = LBound(OtherHeaders) To UBound(OtherHeaders)
For m = LBound(tArray, 1) To UBound(tArray, 1)
If tArray(m, sOtherHeadersColumn(k)) = sArray(i, sOtherHeadersColumn(k)) Then
MatchingScoresList(m) = MatchingScoresList(m) + 2 ^ (UBound(OtherHeaders) - k)
End If
Next m
Next k
'Get the max score position
Dim MyMax As Long
MyMax = Application.Max(MatchingScoresList)
If Application.Count(Application.Match(MatchingScoresList(), Array(MyMax), 0)) > 1 Then
MsgBox "Error: can't determine how to match row " & i & " in source table"
Exit Sub
Else
MatchRow = Application.Match(MyMax, MatchingScoresList, 0)
End If
Case Is = 1
MatchRow = Application.Match(1, MatchList, 0)
Case Else
Dim nArray() As Variant, Counter As Long
If AddIfMissing Then
MatchRow = 0
Counter = Counter + 1
ReDim nArray(1 To Counter, 1 To UBound(tArray, 2))
For k = LBound(MandatoryHeaders) To UBound(MandatoryHeaders)
nArray(Counter, tMandatoryHeadersColumn(k)) = sArray(i, sMandatoryHeadersColumn(k))
Next k
For k = LBound(OtherHeaders) To UBound(OtherHeaders)
nArray(Counter, tOtherHeadersColumn(k)) = sArray(i, sOtherHeadersColumn(k))
Next k
Else
MsgBox "Error: Couldn't find a match for data row #" & i
Exit Sub
End If
End Select
'Logging and assigning values
If MatchRow > 0 Then
For k = LBound(OtherHeaders) To UBound(OtherHeaders)
If tArray(MatchRow, tOtherHeadersColumn(k)) <> sArray(i, sOtherHeadersColumn(k)) Then
'Logging
If IsLogging And DoOverwrite Then LoggingColumn(MatchRow, 1) = LoggingColumn(MatchRow, 1) & _
IIf(LoggingColumn(MatchRow, 1) <> "", ", ", "") & _
tHeaders.Cells(1, tOtherHeadersColumn(k)) & " : " & _
tArray(MatchRow, tOtherHeadersColumn(k)) & _
" -> " & sArray(i, sOtherHeadersColumn(k))
'Assign new value
If DoOverwrite Or tArray(MatchRow, tOtherHeadersColumn(k)) = VbNullString Then
tArray(MatchRow, tOtherHeadersColumn(k)) = sArray(i, sOtherHeadersColumn(k))
End If
End If
Next k
End If
Next i
'Write arrays to sheet
ExcludeRows(trng, 1).Value2 = tArray
With trng.Parent
If IsArrayInitialised(nArray) And AddIfMissing Then
.Cells(trng.Cells(1, 1).Row + trng.Rows.Count, trng.Cells(1, 1).Column).Resize(UBound(nArray, 1), UBound(nArray, 2)).Value2 = nArray
End If
If IsLogging Then
.Cells(trng.Cells(1, 1).Row, trng.Cells(1, 1).Column + trng.Columns.Count) = "Changes"
.Cells(trng.Cells(2, 1).Row, trng.Cells(1, 1).Column + trng.Columns.Count).Resize(UBound(LoggingColumn, 1), 1).Value2 = LoggingColumn
End If
End With
End Sub
And also add these functions inside your VBA project to as they are used in the procedure above.
Function IsArrayInitialised(ByRef A() As Variant) As Boolean
On Error Resume Next
IsArrayInitialised = IsNumeric(UBound(A))
On Error GoTo 0
End Function
Function ExcludeRows(MyRng As Range, StartRow As Long, Optional EndRow As Long = -1) As Range
'PURPOSE: Exclude one or more consecutives rows from an existing range
Dim Afterpart As Range, BeforePart As Range
If StartRow < 1 Or EndRow > MyRng.Rows.Count Then Set ExcludeRows = Nothing
If StartRow = 1 And EndRow = MyRng.Rows.Count Then Set ExcludeRows = Nothing
If EndRow = -1 Then EndRow = StartRow
If EndRow < MyRng.Rows.Count Then
With MyRng.Parent
Set Afterpart = .Range(MyRng.Cells(EndRow + 1, 1), MyRng.Cells(MyRng.Rows.Count, MyRng.Columns.Count))
End With
End If
If StartRow > 1 Then
With MyRng.Parent
Set BeforePart = .Range(MyRng.Cells(1, MyRng.Cells(1, 1).Column), MyRng.Cells(StartRow - 1, MyRng.Columns.Count))
End With
End If
Set ExcludeRows = Union2(True, BeforePart, Afterpart)
End Function
Public Function Union2(IgnoreEmptyRange As Boolean, ParamArray RangeArray() As Variant) As Range
'PURPOSE: Samae as Application.Union but allows some range object to be Empty
Dim V As Variant
Dim Rng As Range
For Each V In RangeArray
Do
If VarType(V) = vbEmpty Then Exit Do
Set Rng = V
If Not Union2 Is Nothing Then
Set Union2 = Union(Union2, Rng)
ElseIf Not Rng Is Nothing Then
Set Union2 = Rng
End If
Loop While False
Next
End Function
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