I am trying a few items for a Secret Santa spreadsheet.
A Unique ID generator to print the UID in Column B for a list of names in Column A.
A randomizer to print the ID numbers in a random order in Column C with the restraint that Column B UID cannot equal Column C UID, ensuring no one gets themselves.
List Name for the random UID in Column C in Column D.
The UIDs are to start at 1 and then count until the last name receives an ID. I also want the generator to handle creating an ID for a name that is added anywhere in the list (beginning, middle, end).
I found some answers here and on other websites.
Some use complicated looping others the GUID function that I do not understand.
In general, the answers are for existing lists and not a new list with no UIDs.
I assume I would:
Create the UIDs and print them to Column B.
Save Column A and B into an array.
Randomize and Print the UID's into Column C.
Use the array to get the name for the randomized UID's in Column C and print the corresponding name in Column D.
I am unsure if this methodology is a "good" approach for this type of problem, but I would be interested in hearing other methodologies.
The only code I have so far is the row counter.
Sub secret_santa()
Dim person_count As Integer
Dim uid As Integer
'Count Number of Used Rows
person_count = ActiveSheet.UsedRange.Rows.Count
'Subtract Header from person_count
person_count = person_count - 1
End Sub
If I'm reading correctly, you basically just want to shuffle the array whilst also imposing the constraint that no user ID cannot end up corresponding to itself.
There are a number of ways you can implement this. The approach below is not very generalised, can be refactored (to use more functions/subroutines) and a bit clunky. Nonetheless, it might be okay and may give you some ideas regarding implementation:
Option Explicit
Private Function GetArrayOfNames(ByVal someRange As Range) As Variant
' someRange should be a single-column, vertical range.
Debug.Assert someRange.Columns.Count = 1
Debug.Assert someRange.Areas.Count = 1
Debug.Assert someRange.Rows.Count > 1
GetArrayOfNames = someRange.Value
End Function
Private Sub SecretSantaShuffle()
' This procedure will overwrite the contents of the initial range,
' and the three columns to its right.
Dim rangeContainingNames As Range
Set rangeContainingNames = ThisWorkbook.Worksheets("Sheet1").Range("A2:A7")
Dim inputArray() As Variant
inputArray = GetArrayOfNames(rangeContainingNames)
Dim rowCount As Long
rowCount = UBound(inputArray, 1)
ReDim Preserve inputArray(1 To rowCount, 1 To 4)
Const NAME_COLUMN_INDEX As Long = 1
Const UID_COLUMN_INDEX As Long = 2
Const RANDOM_NAME_COLUMN_INDEX As Long = 3
Const RANDOM_UID_COLUMN_INDEX As Long = 4
Do
Dim userIdPool As Collection
Set userIdPool = New Collection
Dim rowIndex As Long
For rowIndex = LBound(inputArray, 1) To UBound(inputArray, 1)
inputArray(rowIndex, UID_COLUMN_INDEX) = rowIndex
userIdPool.Add rowIndex, Key:=CStr(rowIndex)
Next rowIndex
For rowIndex = LBound(inputArray, 1) To UBound(inputArray, 1)
Dim randomRowIndex As Long
Do While True
randomRowIndex = userIdPool.Item(Application.RandBetween(1, userIdPool.Count))
If randomRowIndex <> rowIndex Then Exit Do
If userIdPool.Count = 1 Then Exit Do
DoEvents
Loop
userIdPool.Remove CStr(randomRowIndex)
inputArray(rowIndex, RANDOM_NAME_COLUMN_INDEX) = inputArray(randomRowIndex, NAME_COLUMN_INDEX)
inputArray(rowIndex, RANDOM_UID_COLUMN_INDEX) = inputArray(randomRowIndex, UID_COLUMN_INDEX)
Next rowIndex
Loop While userIdPool.Count > 0
rangeContainingNames.Resize(UBound(inputArray, 1), UBound(inputArray, 2)).Value = inputArray
End Sub
You will likely need to change what gets assigned to rangeContainingNames. My list of names was in range A2:A7 of worksheet Sheet1 but you should change the sheet name and range address to reflect the location of your names.
Related
I'm new to vba so I need some help making my macro more efficient. It does return the desired outcome however I know there must be a much quicker way to do so I just do not have the vba experience to know how.
I have a column which contains names of people assigned to a project. Some are only one name, and others may be multiple, for example:
At the moment, my code goes through this column, separates the names by comma, and enters them individually into a new range like so:
I then use a collection for the unique names and enter them in the final desired list. The names must show up three times, blank row, next three rows are the next name, so on.It should look like this in the end:
Currently my code is the following
Sub FindUniques()
Dim Ws As Worksheet, Ns As Worksheet
Dim SubString() As String, m As Integer, k As Long, NameCount As Integer
Dim allNames As New Collection, tempRng As Range
Set Ns = Worksheets("Sheet2")
Set Ws = Worksheets("Sheet1")
'Loops through the Assigned To column, separates and finds unique names
On Error Resume Next
For i = 1 To Ws.Range("A:A").End(xlDown).Row - Range("Assigned_to").Row
SubString = Split(Range("Assigned_to").Offset(i), ", ")
For j = 0 To UBound(SubString)
allNames.Add (allNames.count), SubString(j)
Next j
Next i
On Error GoTo 0
NameCount = allNames.count
For k = 1 To NameCount
For m = 1 To 4
Ns.Cells((k - 1) * 4 + m + 7, 2) = allNames.Key(k)
Next
Range("Names").Offset((k - 1) * 4).ClearContents
Next
End Sub
It works, however there must be some way that is more efficient than entering the names into a new range and then deleting the range. How can I use a collection or an array or something of the sort to make it quicker? Any ideas would be really appreciated
edit: I have now updated the code and it is using an collection, taking values from the substring. This enters the item (0, 1, 2, ...) in the cells instead of the keys (keys here are the names). How do I get it to return the key instead of the item number?
The slowest part of VBA are worksheet interactions so we should attempt to minimize that as much as possible.
Sub FindUniques()
Dim ws As Worksheet, ns As Worksheet
Dim splitStr() As String, nameStr As Variant
Dim dict As New Dictionary
Dim lastRow As Long, i As Long
Set ns = Worksheets("Sheet2")
Set ws = Worksheets("Sheet1")
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
'Loops through the Assigned To column, separates and finds unique names
For i = 2 To lastRow
splitStr = Split(CStr(ws.Cells(i, 1).Value), ", ")
For Each nameStr In splitStr
If Not dict.Exists(nameStr) Then dict.Add nameStr , 0
Next
Next i
i = 2
For Each nameStr In dict.Keys
ns.Cells(i, 1).Resize(3).Value = nameStr
i = i + 4
Next
End Sub
Edited With #Toddleson & #BigBen 's suggestions
Good Luck!
I created and defined an array in VBA:
A_Array(2,4) As Variant (Option Base 1)
I want to match numbers in the 2nd column against specific criteria, for example, which row in the second column contains the number "1". Once I find the match then I want to use the corresponding value in the first column to create a variable. How do I specify the range, in this case an entire column of a VBA created array, when i use the index and match functions?
Variable = Worksheet.Function.Index(A_Array, Worksheet.Function.Match(1, **?Second_ Column of A_Array?**,0),**?First Column of A_Array?**)
How do i specify the first column of A_Array inside the Match function above and how do I specify the second column of the A_Array inside the Index function.
Thanks in advance for any help.
Just loop the rows and test, with variant arrays it will be quick.
Dim A_Array(2, 4) As Variant
'fill array
Dim i As Long
For i = LBound(A_Array, 1) To UBound(A_Array, 1)
If A_Array(i, 2) = 1 Then
variable = A_Array(i, 1)
Exit For
End If
Next i
If you are wanting to find an array element equal to 1, this is the slicing approach which doesn't require a loop.
Most of this code is populating an array and showing it on a sheet for the purposes of illustrating the results, so you probably won't need any of that.
Sub x()
Dim A_Array(1 To 4, 1 To 2) As Variant, i As Long, variable As Variant, v As Variant
For i = LBound(A_Array, 1) To UBound(A_Array, 1) 'just populating array with any old stuff so you won't need
A_Array(i, 1) = i * 2
A_Array(i, 2) = i * 3
Next i
A_Array(2, 2) = 1 'make sure something in 2nd column is 1
Range("A1").Resize(4, 2).Value = A_Array
v = Application.Match(1, Application.Index(A_Array, , 2), 0) 'returns position of 1 in second column (or error if no match)
if isnumeric(v) then variable = A_Array(v, 1) 'find corresponding element in 1st column
Range("D1").Value = variable
End Sub
I have Sheet1.ComboBox1 that I would like to fill with an array of values. This array is stored on Sheet2. This array is a list of all customers to be used in the excel file. All customers are listed in one single column.
Some customers appear more than once in the column. It varies by how many part numbers a customer has.
I would like to fill a Sheet1.ComboBox1 with this array, however, I don't want duplicate values.
I read online that I can convert the array into a collection which will automatically weed out duplicates.
I would like to take this collection and input it into the Sheet1.ComboBox1, however, upon some research, I've found that collections are read-only...(am I wrong in this conclusion?)
One strategy I saw was to convert the customer array into a collection and then back into a new simplified array. The hope is to store this new array into Sheet 3, then pull this array into ComboBox1.List. I've posted my code below of this attempt.
'Converts collection to an accessible array
Function collectionToArray(c As Collection) As Variant()
Dim a() As Variant: ReDim a(0 To c.Count - 1)
Dim i As Integer
For i = 1 To c.Count
a(i - 1) = c.item(i)
Next
collectionToArray = a
End Function
Sub PopulateComboBoxes()
Dim ComboBoxArray As New Collection, customer
Dim CustomerArray() As Variant
Dim newarray() As Variant
Dim i As Long
CustomerArray() = Sheet2.Range("A5:A2000")
On Error Resume Next
For Each customer In CustomerArray
ComboBoxArray.Add customer, customer
Next
newarray = collectionToArray(ComboBoxArray)
Sheet3.Range("A1:A2000") = newarray
Sheet1.ComboBox1.List = Sheet3.Range("A1:2000")
I used ' CustomerArray() = Sheet2.Range("A5:2000") ' not because there are that many rows full of values in Sheet 2, rather, that I cover all bases when more customers are eventually added to the list. The total size of my Sheet 2 is currently A1:A110, but I want to future proof it.
When I run the code, the Array is successfully reduced and the new array is placed into Sheet3 with no duplicates. However, the first Customer entry is repeated after the last unique customer value is defined. (A46 is last unique customer, A47:A2000 its the same customer repeated)
Additionally, Sheet1.ComboBox1 remains empty.
Is anyone able to explain how to restrict the number of rows filled by 'collectionToArray' , instead of filling all 2000?
Also, where am I going wrong with filling the ComboBox1? Am I missing a command/function to cause the box to fill?
You don't need that function to make a New Array, seems Excessive to me.
Assigning to CustomerArray will take care of Future Additions in column
You can directly pass on the Collection value to ComboBox
You are missing On Error Goto 0 in your code after addition to Collection. That is making all to errors after that invisible and hard for you to identify which part of code is causing problems.
Here Try this:
Sub PopulateComboBoxes()
Dim ComboBoxArray As New Collection
Dim CustomerArray() As Variant
Dim newarray() As Variant
Dim i As Long
With Worksheets("Sheet2")
CustomerArray = .Range("A5:A" & .Range("A5").End(xlDown).row).Value
End With
On Error Resume Next
For i = LBound(CustomerArray) To UBound(CustomerArray)
ComboBoxArray.Add CustomerArray(i, 1), CustomerArray(i, 1)
Next
On Error GoTo 0
For Each Itm In ComboBoxArray
Worksheets("Sheet1").ComboBox1.AddItem Itm
Next
End Sub
First, you should assign your range dynamically to CustomerArray...
With Sheet2
CustomerArray() = .Range("A5:A" & .Cells(.Rows.Count, "A").End(xlUp).Row).Value
End With
Then, you should disable error handling after you've finished adding the items to your collection. Since you did not do so, it hid the fact that your range reference in assigning the values to your listbox was incorrect, and that you didn't use the Value property to assign them. So you should disable the error handling...
On Error Resume Next
For Each customer In CustomerArray
ComboBoxArray.Add customer, customer
Next
On Error GoTo 0
Then, when transferring newarray to your worksheet, you'll need to transpose the array...
Sheet3.Range("A1").Resize(UBound(newarray) + 1).Value = Application.Transpose(newarray)
Then, as already mentioned, you should assign the items to your listbox with Sheet3.Range("A1:A2000").Value. However, since newarray already contains a list of the items, you can simply assign newarray to your listbox...
Sheet1.ComboBox1.List = newarray
So the complete code would be as follows...
Sub PopulateComboBoxes()
Dim ComboBoxArray As New Collection, customer As Variant
Dim CustomerArray() As Variant
Dim newarray() As Variant
With Sheet2
CustomerArray() = .Range("A5:A" & .Cells(.Rows.Count, "A").End(xlUp).Row).Value
End With
On Error Resume Next
For Each customer In CustomerArray
ComboBoxArray.Add customer, customer
Next
On Error GoTo 0
newarray = collectionToArray(ComboBoxArray)
Sheet3.Range("A1").Resize(UBound(newarray) + 1).Value = Application.Transpose(newarray)
Sheet1.ComboBox1.List = newarray
End Sub
it could be achieved in a number of ways. using collection or dictionary object. i am just presenting simple method without going through collection or dictionary since only 5000 rows is to be processed. it could be further shortened if used directly with combo box without using OutArr. As #Domenic already answered it with explanations, may please go along with that solution.
Option Explicit
Sub test()
Dim InArr As Variant, OutArr() As Variant
Dim i As Long, j As Long, Cnt As Long
Dim have As Boolean
InArr = ThisWorkbook.Sheets("sheet2").Range("A5:A2000")
ReDim OutArr(1 To 1)
Cnt = 0
For i = 1 To UBound(InArr, 1)
If InArr(i, 1) <> "" Then
have = False
For j = 1 To UBound(OutArr, 1)
If OutArr(j) = InArr(i, 1) Then
have = True
Exit For
End If
Next j
If have = False Then
Cnt = Cnt + 1
ReDim Preserve OutArr(1 To Cnt)
OutArr(Cnt) = InArr(i, 1)
End If
End If
Next i
Sheet3.Range("A1").Resize(UBound(OutArr)).Value = Application.Transpose(OutArr)
Sheet1.ComboBox1.Clear
Sheet1.ComboBox1.List = OutArr
Debug.Print Sheet1.ComboBox1.ListCount
End Sub
I am trying to put in place a macro that allows me to match identical entry from one table to another. The tricky part is that if a match is found, it cannot be repeated. The way I theorized it is kind of elementary, however it is the only way I can think of it given my still limited knowledge in VBA.
The structure
Both tables need to be first filtered in order to allow the non-repetition condition.
Store the searching values as arrays in order to speed up the process of the macro
Match the entries to search with the ones from the targeted table in order to find matches. This is done with the in-application function MATCH. The MATCH function returns the cell where the match is situated, this is useful as it constantly shift the range in order to not repeate the same value all the time.
After calculating the shifting range, I use a VLookup function in order to return the second entry.
Unfortunately, the macro is incomplete. I cannot find a way to constantly shift the range without compromising the mechanism. The problem resides in the shifting range that is not created correctly to shift after each match.
Desired result
In the below image the desired result would be to check if all items in the left table are in the right table. Take item A, I need to find two item As. I have in the right column a first item A with value 17 and a second item A with value 81. If I do not find any value I have nothing, as it is the case of Ds and E. If instead I have less entries in the left table (as it is for the case of entry L) then I need to return all values of Entry L: 96; 77; 40.
Sub Matching11()
ThisWorkbook.Activate
Worksheets.add
Worksheets("Test4").Range("A1:T110").copy Destination:=ActiveSheet.Range("A1")
With ActiveSheet
Dim Search_Array As Variant
Search_Array = Range("C2", Range("C1").End(xlDown)) 'use this array to loop through the value to search for
Dim Target_MatchValue As Integer
Dim Target_Range As Range
Dim arr As Variant
Dim counter As Integer
Dim n As Integer
counter = 0
n = 0
Target_MatchValue = 0
For counter = LBound(Search_Array) To UBound(Search_Array)
Target_MatchValue = 0
Target_MatchValue = Application.Match(Search_Array(counter, 1), .Range("H2:H200"), 0) - 1 'change C column with the range where you will have the tyres you need search for
Set Target_Range = .Range(.Cells(2 + n, 8), .Cells(1000, 9)) 'this is supposed to work as a shifting range allowing to match entries without making repetitions. I used the MATCH function in order to set the start of the range. i.e. if there is a match in the target table the range will shift from the location of the match downwards. If the match is at on the same level then it does not shift the range in order to match the same-level entry afterwards it is supposed to shift by one unit in order to prevent repetitions.
'If arr = Application.VLookup(Search_Array(counter, 1), Target_Range, 2, False) Is Nothing Then GoTo NextCounter 'I used Vlookup in order to return the value set in the second column of the targetted table. As alternative, I think I could just use offset since I previously used MQTCH
arr = Application.VLookup(Search_Array(counter, 1), Target_Range, 2, False)
If IsError(arr) Then
GoTo NextCounter
Else
.Range(Cells(1 + counter, 6), Cells(1 + counter, 6)).value = arr 'Return the value of the array in this cell
End If
Target_Range.Select
If Target_MatchValue = 0 Then
n = n + 1
ElseIf Target_MatchValue > 0 Then
n = n + Target_MatchValue
End If
.Range(Cells(1 + counter, 5), Cells(1 + counter, 5)).value = Search_Array(counter, 1) 'Return the value of the array in this cell
Next counter
NextCounter:
Next counter
End With
End Sub
Well, Let's see if this helps you out and probably you can adapt it to your needs.
I replied your data like this:
The macro will create a list in columns H:I like the right table of your image. The macro will always delete any previous result. My macro works on standard ranges, is not designed to work on tables (ListObjects in VBA), but you can easily adapt it to your needs.
Sub CREATE_LIST()
Application.ScreenUpdating = False
Dim LastRow As Long
Dim MyRange As Range
Dim rng As Range
Dim i As Long
'we clear previous list
Columns("H:I").Delete
'we add data
Range("H1").Value = "Target"
Range("I1").Value = "Return"
LastRow = Range("C" & Rows.Count).End(xlUp).Row 'Last row of column C, where data is.
Set MyRange = Range("D2:D" & LastRow).SpecialCells(xlCellTypeConstants, 23) 'we select only NON BLANK cells
i = 2 'initial row
For Each rng In MyRange
Range("H" & i).Value = rng.Offset(0, -1).Value 'value of adjacent cell (Column C)
Range("I" & i).Value = rng.Value 'value of cell in column D
i = i + 1
Next rng
Application.ScreenUpdating = True
End Sub
After executing code I get:
And trying different data works too:
Hope you can adapt this to your needs.
Apologies for the unclear explanation of the problem. I have provided here below a solution I have sorted out. I was looking for a code that could execute vlookup without returning the same values. Below is the solution. I am aware that the code might not be the cleanest and most elegant one but it is effective and run fast enough for large data sample.
Sub Matching()
Dim Search_Array As Variant
Dim Target_MatchValue As Variant
Dim Target_Range As Range
Dim arr As Variant
Dim counter As Integer
Dim n As Integer
'data must be ordered in order to apply the non-repetitive condition
Search_Array = Sheet1.Range("A2", Sheet1.Range("A1").End(xlDown)) 'use this array to loop through the value to search for
n = 0
Sheet1.Activate
With ActiveSheet
For counter = LBound(Search_Array) To UBound(Search_Array)
Target_MatchValue = 0
Target_MatchValue = Application.Match(Search_Array(counter, 1), .Range(Cells(2 + n, 4), Cells(1000, 4)), 0) 'This code will return the value used for the shifting range
Set Target_Range = .Range(Cells(2 + n, 4), Cells(1000, 5)) 'this is supposed to work as a shifting range allowing to match entries without making repetitions. I used the MATCH function in order to set the start of the range. i.e. if there is a match in the target table the range will shift from the location of the match downwards. If the match is at on the same level then it does not shift the range in order to match the same-level entry afterwards it is supposed to shift by one unit in order to prevent repetitions.
'target_range.select Activate this code in order to see the macro in action
arr = Application.VLookup(Search_Array(counter, 1), Target_Range, 2, False) 'store the vlookup value in an array in order to increase the efficiency the code and to speed up the whole proces
If IsError(arr) Then
.Cells(2 + n, 2).value = "" 'if the macro does not find anything, no value will be recorded anywhere
Else
.Cells(1 + n + Target_MatchValue, 2).value = Search_Array(counter, 2) 'Return the value of the search_array in this cell so to match column A values with column D values if they are found
End If
If IsError(arr) Then
n = n
ElseIf Target_MatchValue = 0 Then 'if the macro does not find anything, the shifting range does not shift so that subsequent values can be searched in the same range without missing precious matches
n = n + 1
ElseIf Target_MatchValue > 0 Then 'if there is a matching value between Column A and Column B, the shifting range shifts by the n + the distance between the the current vlookupvalue and the found value. Note that Data must be stored in a filtered order otherwise vlookup will not work correctly
n = n + Target_MatchValue
End If
Next counter
End With
End Sub
By exchanging ideas with some friends, I was told to think about a potential helper column that would be used to store incremental numbers. This helper column would store incremental numbers that would help to meet the non-repetition condition. Please see the below example.
The idea here is that if a value is found in column E, I store n being equal to the value found in the helper column. Then the code needs to verify if the future values' n are bigger than previous n. If this condition is met, then the one-repetition condition is fulfilled. n changes value to the next bigger value.
For example, if I find L in the right table, I report 96 as value and store N being equal to 11. When I search for the next value of L, the new n must be bigger than the current n otherwise I will not store the new found value. The value 77 found has indeed a bigger n than the previous value as 12 is bigger than 11. I hope this helps.
The function below finds the first result.
There could be duplicate rows with matching values that meet my if statements. How would I create an array to store the row numbers the search function found so that I can process the data later.
How would I create the array size based on the number of results found in the for loop?
I am assuming that the for loop counter will have some sort of role to play in this. Let's say the for loop found 2 matches in row numbers 56 and 98 matching my if statements:
array_example(counter, 0) = 56
array_example(counter, 0) = 98
The stored values would be:
array_example(1, 0) = 56
array_example(2, 0) = 98
Private Sub Complex_Search(col1, cval1, col2, cval2)
'MsgBox (col1 & " " & col2)
'MsgBox (cval1 & " " & cval2)
Dim i
Dim lRow
Dim Counter
lRow = Cells(Rows.Count, 1).End(xlUp).row
Counter = 0
With Sheets("Office Spaces")
For i = 2 To lRow
If LCase(.Cells(i, col1).Value) = LCase(cval1) And LCase(.Cells(i, col2).Value) = LCase(cval2) Then
row = i
Counter = Counter + 1
End If
Next i
End With
If row = "" Then
MsgBox ("Search complete. 0 results found")
Else
GetRowData (row)
UserForm1.resmax.Value = 1
End If
End Sub
It's worth noting that you haven't even initialized row, and have just let vba implicitly declare it as a variant type for you. To avoid common problems that arise from typos, include Option Explicit at the top of your code and Dim every variable with the type beside it. For example: Dim i as long. Dim i will work, but it will declare it as a variant type
To initialize an array in VBA you use Dim row() as variant. From there you can re-dimension its size using Redim row(LboundX to UboundX) but this will reset all the stored values to zero. To get around this use Redim Preserve row(LBoundX to UBound X).
If you wish to use a 2D array, add a comma and then put the bounds for the next dimension Redim Preserve row(LBoundX to UBound X, LboundY to UBoundY)
At the top of your code I would include
Dim row() as Variant
Redim Preserve row(1 to 1)
Then within the loop I would change row = i to
row(Ubound(row)) = i
Redim Preserve row(1 to Ubound(row) +1)
Now that you have an array though, the check you do below will no longer work and will likely throw an error because you there's no specified index. Instead I suggest changing it from If row = "" Then to If Counter = 0 Then.
I'm not sure what the intention is with GetRowData(row) but you can just access each row number via row(i). It is worth noting however, that the row array will have Counter +1 number of items, but the last one will be blank. You could get around this by adding in an if statement within the already existing one that would look something like this:
If Counter = 0 Then
row(1) = i
Else
ReDim Preserve row(1 To UBound(row) + 1)
row(UBound(row)) = i
End If
Counter = Counter + 1
By implementing this change, row should have exactly Counter number of items that all have a non-empty value
I don't really suggest making a column array because it becomes more cumbersome to change the array size. Redim preserve or not only allows you to change the last dimension in the array, so to change the number of rows you would have to set the array equal to the transpose of itself, do the redim and then set it to the transpose of itself again. It's just needlessly messy. If you're pasting it to a sheet and need it to be in a column you could just transpose it at the end instead.