I use this to see which clients have been added to our client list and which have left, month to month.
It takes two lists, then outputs the unique and common members of the two lists. There may be better ways of doing this, but the logic is simple and easy to follow and it seems to work. e.g.
A
B
A
B
AB
1
3
1
5
3
2
4
2
6
4
3
5
4
6
Option Base 1
Sub UniqueMembersOfTwoLists()
Dim arrOne() As Variant
Dim arrTwo() As Variant
Dim AB() As Variant
ReDim AB(0 To 0) As Variant
Dim A_Only() As Variant
ReDim A_Only(0 To 0) As Variant
Dim OnlyInListB() As Variant
ReDim OnlyInListB(0 To 0) As Variant
Dim lrOne As Long
Dim lrTwo As Long
Dim r1 As Range
Dim r2 As Range
Dim i As Long
Dim test As Variant
Dim g As Boolean
‘Dim ms As String
‘ if needed
‘ms = "Put list 1 in column A starting in A1, put list 2 in column B staring B1"
‘MsgBox ms
lrOne = Range("A65336").End(xlUp).Row
lrTwo = Range("B65336").End(xlUp).Row
Set r1 = Range((Cells(1, 1)), (Cells(lrOne, 1)))
Set r2 = Range((Cells(1, 2)), (Cells(lrTwo, 2)))
arrOne = r1
arrTwo = r2
‘simple check to see if each member of list B is in List A
For Each Element In arrTwo
test = Element
g = contained(arrOne, test)
If g = True Then
' means is a member of both lists, add to common members list
ReDim Preserve AB(0 To UBound(AB) + 1)
AB(UBound(AB)) = test
Else
‘means only in list A, so add to A only
ReDim Preserve A_Only(0 To UBound(A_Only) + 1)
A_Only(UBound(A_Only)) = test
End If
Next Element
‘ then repeat the other way round to find only in list B
For Each w In arrOne
test = w
g = contained(arrTwo, test)
If g = True Then
' means is a member of both lists, already added so do nothing
Else
ReDim Preserve OnlyInListB(0 To UBound(OnlyInListB) + 1)
OnlyInListB(UBound(OnlyInListB)) = test
End If
Next w
' out put to sheet
For i = 1 To UBound(AB)
Cells(i, 5).Value = AB(i)
Next i
i = 1
For i = 1 To UBound(A_Only)
Cells(i, 4).Value = A_Only(i)
Next i
i = 1
For i = 1 To UBound(OnlyInListB)
Cells(i, 3).Value = OnlyInListB(i)
Next i
i = 1
‘ tidy up
Rows("1:1").Select
Selection.Insert Shift:=xlDown, CopyOrigin:=xlFormatFromLeftOrAbove
Range("A1").Select
ActiveCell.FormulaR1C1 = "List A"
Range("B1").Select
ActiveCell.FormulaR1C1 = "List B"
Range("C1").Select
ActiveCell.FormulaR1C1 = "Only in List A"
Range("D1").Select
ActiveCell.FormulaR1C1 = "Only in List B"
Range("E1").Select
ActiveCell.FormulaR1C1 = "In both A & B"
Rows("1:1").Select
Selection.Font.Bold = True
Cells.Select
Cells.EntireColumn.AutoFit
End Sub
Function contained(arr() As Variant, test As Variant)
Dim i As Long
Dim a As Variant
Dim g As Boolean
g = False
For i = 1 To UBound(arr)
a = arr(i, 1)
If a = test Then
g = True
Exit For
Else
End If
Next i
contained = g
End Function
Is there a more efficient way of achieving the same, possibly using a dictionary?
Related
I have a small VBA Loop but it takes over 2-3 minute to finish, any idea how I can speed up/rewrite it that it will be faster?
The Range "Replace Names" is a List of 500 names of named areas in "Data".
The for loop searches for the one that matches the name in "Data" and replaces the one with the name from "Source". This also works fine, but it takes a while. Is there a faster method?
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
For Each ID_name In wsSupport.Range("ReplaceNames")
wsCheck.Range("Data").Replace ID_name, wsSource.Range(ID_name), xlWhole
Next
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
You might benefit from this valuable piece of text:
https://www.microsoft.com/en-us/microsoft-365/blog/2009/03/12/excel-vba-performance-coding-best-practices/
So in your case the Code could somewhat look like this:
Dim arrData as Variant, arrSource as Variant, k as long
arrData = wsCheck.Range("Data").value2 'this creates a two-dimensional array with rows on the first and columns on the second index
arrSource = wsSource.Range(...).value2
'loop through rows I suppose
for k = LBound(arrData,1) to UBound(arrData,1)
if arrData(k, yourColumn) = ... then
arrData(k, yourColumn) = arrSource(rowhere, columnhere)
endif
next k
wscheck.range("Data") = arrData
Its working now!!!
If you see mistakes let me know!
Dim arrData As Variant, arrSource As Variant, arrNames As Variant, k As Long
arrData = wsChecklist.Range("Checklist").Value2 'this creates a two-dimensional array with rows on the first and columns on the second index
arrSource = wsSupport.Range("ReplaceNames").Value2
arrNames = wsNia.Range("D1:D1000").Value2
'loop through rows I suppose
For k = LBound(arrData, 1) To UBound(arrData, 1)
For j = LBound(arrData, 2) To UBound(arrData, 2)
' If UCase(arrData(k, j)) = UCase(arrSource(x, 1)) Then
If UCase(arrSource(x, 1)) = UCase(arrData(k, j)) Then
For i = 1 To 1000
Name1 = wsNia.Cells(i, 2)
Name2 = wsNia.Cells(i, 3)
Name = Name1 & "_" & Name2
If UCase(arrData(k, j)) = UCase(Name) Then
arrData(k, j) = arrNames(i, 1)
x = x + 1
k = 1
j = 1
i = 1
Exit For
End If
Next i
End If
If k > 2900 And x < 265 Then
x = x + 1
j = 1
k = 1
End If
Next j
Next k
wsChecklist.Range("Checklist").Value2 = arrData
I need to assigned a value on Column B depending on the condition in Column A. I formulate a simple code using IF...ElseIf condition (see code below). I have 1000 conditions and I am thinking if I can use a 2 separate arrays for the value of Column A and get the index of the value in column A to 1st array (Array1 ) and match it to the 2nd array (AssignedArray). Something like, for each value found in Column A check the Array1 if the value is exist and get the index and match the index to AssignedArray. Like for example,
Array1 = Array("DL2005", "EFRUEN", "DESTDIDIER", "EOGRADY3", "EKARLSON1", "EOKUTOMI1")
AssignedArray = Array("Trader", "Trader", "Operations", "Trader", "Analyst", "Operations")
CODE
For x = 1 To Sheets(1).Cells(Rows.Count, 1).End(xlUp).Row
For Each wrd In Sheets(1).Cells(x, 1)
val = wrd
If UCase(val) = "DL2005" Then
Sheets(1).Cells(x, 3).Value = "Trader"
ElseIf UCase(val) = "EFRUEN" Then
Sheets(1).Cells(x, 3).Value = "Trader"
ElseIf UCase(val) = "DESTDIDIER" Then
Sheets(1).Cells(x, 3).Value = "Operations"
ElseIf UCase(val) = "EOGRADY3" Then
Sheets(1).Cells(x, 3).Value = "Trader"
ElseIf UCase(val) = "EKARLSON1" Then
Sheets(1).Cells(x, 3).Value = "Analyst"
ElseIf UCase(val) = "EOKUTOMI1" Then
Sheets(1).Cells(x, 3).Value = "Operations"
End If
Next wrd
Next x
Is it possible to do that?Or is there any way on how to simplify my code instead of using IF ELSEIF condition.
If you have 1k conditions (as you do), then I imagine neither If nor Select statements are appropriate. Furthermore, creating/maintaining expressions (in your code) that evaluate to two 1k-element arrays may be burdensome.
A maintenance friendly approach might be to keep the items in Array1 on some worksheet, and keep the contents of AssignedArray right next to it. Something like the below. Say the yellow values are items that you would have put into Array1 and green values are items you would have put into AssignedArray (I only have 25 as an example).
Then you wouldn't necessarily need any VBA and could purely use Excel functions like VLOOKUP -- or MATCH and INDEX in conjunction. For example, I put this formula in cell E4, which tries to find the value in D4 among the values in column A and returns the corresponding value from column B:
=INDEX($B$1:$B$25,MATCH(D4,$A$1:$A$25,0))
If you still wanted to use VBA, this code should loop over cells D4:D8 (which is the correct range for my spreadsheet, but probably not for yours), make them uppercase (in memory only, not on the sheet), then write the corresponding values in G4:G8:
Option Explicit
Private Sub FillInAssociatedValuesValue()
Dim inputKeys() As Variant ' <-- AKA Array1
inputKeys = ThisWorkbook.Worksheets("Sheet1").Range("A1:A25").Value2 ' Change to wherever items from Array1 are kept
Dim inputValues() As Variant '<-- AKA AssignedArray
inputValues = ThisWorkbook.Worksheets("Sheet1").Range("B1:B25").Value2 ' Change to wherever items from AssignedArray are kept
If (UBound(inputKeys, 1) - LBound(inputKeys, 1)) <> (UBound(inputValues, 1) - LBound(inputValues, 1)) Then
MsgBox ("The number of keys should be the same as the number of associated values. Code will stop running now.")
Exit Sub
End If
Dim dict As Object 'Shouldn't need to add a reference
Set dict = CreateObject("Scripting.Dictionary")
' One pass to fill the dictionary. If there are duplicates, will only add first instance.
Dim rowIndex As Long
For rowIndex = LBound(inputKeys, 1) To UBound(inputKeys, 1)
If Not dict.Exists(inputKeys(rowIndex, 1)) Then
dict.Add UCase$(inputKeys(rowIndex, 1)), inputValues(rowIndex, 1)
End If
Next rowIndex
Dim Key As String
With ThisWorkbook.Worksheets("Sheet1")
For rowIndex = 4 To 8 ' I needed to loop over range D4:D8
Key = UCase$(.Cells(rowIndex, "D").Value2)
If dict.Exists(Key) Then
.Cells(rowIndex, "G").Value2 = dict.Item(Key)
Else
' Some logic in case input is not found, and cannot be mapped to some associated value
.Cells(rowIndex, "G").Value2 = "VALUE NOT FOUND"
End If
Next rowIndex
End With
End Sub
To keep it simple; use For loops to compare Array1 to each cell in column A and if there is a match, use Offset put the corresponding element from AssignedArray into the cell on the right.
Dim Array1 As Variant, AssignedArray As Variant
Dim x As Long, i As Long
Array1 = Array("DL2005", "EFRUEN", "DESTDIDIER", "EOGRADY3", "EKARLSON1", "EOKUTOMI1")
AssignedArray = Array("Trader", "Trader", "Operations", "Trader", "Analyst", "Operations")
For x = 2 To Sheets(1).Cells(Rows.Count, 1).End(xlUp).Row
For i = LBound(Array1) To UBound(Array1)
If Cells(x, 1).Value = Array1(i) Then
Cells(x, 1).Offset(, 1).Value = AssignedArray(i)
End If
Next i
Next x
Try
Sub test()
Dim Ws As Worksheet
Dim Array1, AssignedArray
Dim s As String, i As Integer, r As Long, x As Long
Dim k As Integer
Array1 = Array("DL2005", "EFRUEN", "DESTDIDIER", "EOGRADY3", "EKARLSON1", "EOKUTOMI1")
AssignedArray = Array("Trader", "Trader", "Operations", "Trader", "Analyst", "Operations")
Set Ws = Sheets(1)
r = Ws.Cells(Rows.Count, 1).End(xlUp).Row
With Ws
For x = 1 To r
s = UCase(.Cells(x, 1))
For i = LBound(Array1) To UBound(Array1)
If s = Array1(i) Then
k = i
Exit For
End If
Next i
.Cells(x, 3) = AssignedArray(k)
Next x
End With
End Sub
If you have a lot of data, it is better to speed up the results by arranging the results into a single sheet instead of entering them one by one into the cell.
Sub test2()
Dim Ws As Worksheet
Dim Array1, AssignedArray
Dim s As String, i As Integer, r As Long, x As Long
Dim k As Integer
Dim vDB, vR()
Array1 = Array("DL2005", "EFRUEN", "DESTDIDIER", "EOGRADY3", "EKARLSON1", "EOKUTOMI1")
AssignedArray = Array("Trader", "Trader", "Operations", "Trader", "Analyst", "Operations")
Set Ws = Sheets(1)
With Ws
vDB = .Range("a1", .Range("a" & Rows.Count).End(xlUp))
r = UBound(vDB, 1)
ReDim vR(1 To r, 1 To 1)
For x = 1 To r
s = UCase(vDB(x, 1))
For i = LBound(Array1) To UBound(Array1)
If s = Array1(i) Then
k = i
Exit For
End If
Next i
vR(x, 1) = AssignedArray(k)
Next x
.Range("c1").Resize(r) = vR
End With
End Sub
I have the following situation. In an Excel worksheet, I have a column which contains values that are separated by "|".
e.g.
Option Column
Option 1 | Option 3
Option 4 | Option 7
Option 2 | Option 3 | Option 6
I want to
1. Insert 10 columns to the right, name them "Option 1", "Option 2", "Option 3" ..... "Option 10"
2. In each cell of the first column, if "Option x" exists, split/copy/move to the column named "Option x" (Where x can be 1, 2 .... 10)
This is the code that I use currently to achieve it:
Sub Insert_10_columns()
Columns("B:K").Insert Shift:=xlToRight, _
CopyOrigin:=xlFormatFromLeftOrAbove
For i = 2 To 11
ActiveSheet.Cells(1, i).Value = "Option " & i - 1
Next i
End Sub
Sub Look_For_Text()
Dim LastRow As Long
LastRow = ActiveSheet.Cells(Rows.Count, 1).End(xlUp).Row
For i = 2 To LastRow + 1
For k = 1 To 10
If InStr(1, (Cells(i, 1).Value), "Option " & k) > 0 Then
ActiveSheet.Cells(i, k + 1).Value = "Option " & k
End If
Next k
Next i
End Sub
I was just wondering if loops are the best way to go about it, especially because when I start using it, I would be operating on 20,000+ rows and 15+ columns.
Variant using System.Collections.ArrayList and Scripting.Dictionary, I guess that should be faster than your solution)
Sub test()
Dim data As Range, cl As Range, i&, x As Variant
Dim arrList As Object, Dic As Object
Set arrList = CreateObject("System.Collections.ArrayList")
Set Dic = CreateObject("Scripting.Dictionary")
Set data = Range([A2], Cells(Rows.Count, "A").End(xlUp))
'get unique values from split
For Each cl In data
For Each x In Split(cl, "|"): x = Trim(x)
If Not Dic.exists(x) Then
Dic.Add x, Nothing
arrList.Add x
End If
Next x, cl
Dic.RemoveAll 'clear dictionary
arrList.Sort 'sort values
If sortorder = xlDescending Then
arrList.Reverse
End If
'add headers
i = 2
For Each x In arrList
Cells(1, i).Value2 = x
Dic.Add x, i: i = i + 1
Next x
'split values against headers
For Each cl In data
For Each x In Split(cl, "|"): x = Trim(x)
Cells(cl.Row, Dic(x)).Value2 = x
Next x, cl
End Sub
test here
You will need a loop to walk through while you split the cell contents. Looping through an array is faster than looping through the worksheet. After splitting, populate a target array with matching columns before putting the target array values into the worksheet.
Option Explicit
Sub InsertOptions()
Dim i As Long, j As Long, mx As Long, dlm As String
Dim hdrs As Variant, opts As Variant, vals As Variant, tmp As Variant, m As Variant
dlm = " | " 'column A delimiter; might be " | "
mx = 15 'maximum number of options
With Worksheets("sheet9")
'create an independent array of header labels
ReDim hdrs(1 To 1, 1 To mx)
For i = LBound(hdrs, 2) To UBound(hdrs, 2)
hdrs(1, i) = "Option " & i
Next i
'collect the delimited options from column A
opts = Range(.Cells(2, "A"), .Cells(.Rows.Count, "A").End(xlUp)).Value2
'make room for all options in expanded form
ReDim vals(LBound(opts, 1) To UBound(opts, 1), _
LBound(hdrs, 2) To UBound(hdrs, 2))
'loop through delimited options, split them and look for matches in hdrs
For i = LBound(opts, 1) To UBound(opts, 1)
tmp = Split(opts(i, 1), dlm)
For j = LBound(tmp) To UBound(tmp)
m = Application.Match(tmp(j), hdrs, 0)
If Not IsError(m) Then
vals(i, m) = tmp(j)
End If
Next j
Next i
'insert ten new columns
.Cells(1, "B").Resize(1, UBound(hdrs, 2)).EntireColumn.Insert
'put arrays into new columns
With .Cells(1, "B").Resize(1, UBound(hdrs, 2)).EntireColumn
.ColumnWidth = 9
.Cells(1, 1).Resize(UBound(hdrs, 1), UBound(hdrs, 2)) = hdrs
.Cells(2, 1).Resize(UBound(vals, 1), UBound(vals, 2)) = vals
End With
End With
End Sub
I'm trying to transpose data based on the cell information from another column.
I can fairly quickly with the macro below when I only have two data that are the same. My problem is when I hit more than one data that are the same.
For example:
Clients What they want
20 B
20 C
33 B
33 C
202 A
202 B
202 C
55 A
55 C
The macro I have is this
Sub TransposeDuplciateData()
Sheets("Duplicate").Select
While Range("A2") <> ""
Range("B2").Select
ActiveCell.Resize(2, 1).Select
Selection.Copy
Sheets("Clients").Select
Range("B1").Select
Selection.End(xlDown).Offset(1, 0).Select
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
:=False, Transpose:=True
Sheets("Duplicate").Select
Selection.EntireRow.Delete Shift:=xlUp
Wend
End Sub
The problem is when I hit client number 202, he wants three different things not only two.
I'm therefore looking for a macro that it would first recognize how many times the clients appear and from there copy the relevant information from column B and transpose it into my Clients sheet, then delete the entire rows from my Duplicate sheet (since I dealt with it) and move to the next clients information and do the same thing until there is no more clients information.
Here is the end results I would like it too look like
Clients Option 1 Option 2 Option 3 Option 4
20 B C
33 B C
202 A B C
55 B C
a possible way to achieve your desired outcome is to use a pivot table.
If you set Column A as Row, Column B as Column and values as count of Column B, you get the following output.
A B C
20 1 1
33 1 1
55 1 1
202 1 1 1
Would that help?
For a macro based solution, try the following code. It may need to be adapted to your exact need. Make also sure, column A is sorted in some kind of a way (this can also be done within the macro)
Sub remove_dub()
With Sheets("Dublicate")
Dim row_dubl As Integer
Dim row_clie As Integer
Dim col_clie As Integer
row_dubl = 1
row_clie = 1
col_clie = 2
While .Cells(row_dubl, "A") <> ""
Sheets("Clients").Cells(row_clie, "A") = .Cells(row_dubl, "A")
Sheets("Clients").Cells(row_clie, col_clie) = .Cells(row_dubl, "B")
If .Cells(row_dubl, "A") = .Cells(row_dubl + 1, "A") Then
row_clie = row_clie
col_clie = col_clie + 1
Else
row_clie = row_clie + 1
col_clie = 2
End If
row_dubl = row_dubl + 1
Wend
End With
End Sub
Best regards
A bit "simplified" version:
Dim c As Range
Set c = [a2]
While c > ""
While c = c(2) ' while c equals the cell below it
c.End(xlToRight)(, 2) = c(2, 2) ' get the second value below c
c(2).Resize(, 2).Delete xlShiftUp ' delete the 2 cells below c
Wend
Set c = c(2)
Wend
Here is a macro which creates a user defined object as a class, which has the properties of Client and a dictionary of Opts (for Option). You can easily add other properties, if you want to extend this.
Set reference to Microsoft Scripting Runtime
EDIT: Rename the class module cClient
Class Module
Option Explicit
Private pClient As String
Private pOpt As String
Private pOpts As Dictionary
Public Property Get Client() As String
Client = pClient
End Property
Public Property Let Client(Value As String)
pClient = Value
End Property
Public Property Get Opt() As String
Opt = pOpt
End Property
Public Property Let Opt(Value As String)
pOpt = Value
End Property
Public Property Get Opts() As Dictionary
Set Opts = pOpts
End Property
Public Function ADDOpt(Value As String)
If Not pOpts.Exists(Value) Then
pOpts.Add Key:=Value, Item:=Value
End If
End Function
Private Sub Class_Initialize()
Set pOpts = New Dictionary
pOpts.CompareMode = TextCompare
End Sub
Regular Module
Option Explicit
'Set reference to Microsoft Scripting Runtime
Sub OrganizeClientOptions()
Dim wsSrc As Worksheet, wsRes As Worksheet, rRes As Range
Dim vSrc As Variant, vRes As Variant
Dim cC As cClient, dC As Dictionary
Dim I As Long, J As Long
Dim V As Variant, W As Variant
'Set worksheets
Set wsSrc = Worksheets("sheet1")
On Error Resume Next
Set wsRes = Worksheets("Results")
If Err.Number = 9 Then
Worksheets.Add.Name = "Results"
End If
On Error GoTo 0
Set wsRes = Worksheets("Results")
Set rRes = wsRes.Cells(1, 1)
With wsSrc
vSrc = .Range(.Cells(1, 1), .Cells(.Rows.Count, 1).End(xlUp)).Resize(columnsize:=2)
End With
'collect the data
Set dC = New Dictionary
For I = 2 To UBound(vSrc, 1)
Set cC = New cClient
With cC
.Client = vSrc(I, 1)
.Opt = vSrc(I, 2)
.ADDOpt .Opt
If Not dC.Exists(.Client) Then
dC.Add Key:=.Client, Item:=cC
Else
dC(.Client).ADDOpt .Opt
End If
End With
Next I
'Size vRes
J = 0
For Each V In dC.Keys
I = dC(V).Opts.Count
J = IIf(J > I, J, I)
Next V
ReDim vRes(0 To dC.Count + 1, 1 To J + 1)
'headers
vRes(0, 1) = "Client"
For J = 2 To UBound(vRes, 2)
vRes(0, J) = "Option " & J - 1
Next J
'Data
I = 0
For Each V In dC.Keys
I = I + 1
vRes(I, 1) = V
J = 1
For Each W In dC(V).Opts
J = J + 1
vRes(I, J) = W
Next W
Next V
'Write the results
Set rRes = rRes.Resize(UBound(vRes, 1) + 1, UBound(vRes, 2))
With rRes
.EntireColumn.Clear
.Value = vRes
With .Rows(1)
.Font.Bold = True
.HorizontalAlignment = xlCenter
End With
.EntireColumn.AutoFit
End With
End Sub
Results
i have 2 sheets , i want to find the same rows in 2 sheets , so i put the first row in array , and by a for next i define the first array ...then i define another array from second sheet , then i compare them .... why it doesn't work?
Sub compare()
Dim n(4) As Variant
Dim o(4) As Variant
Dim i As Integer
For i = 3 To 20 'satrha
For j = 2 To 4 'por kardan
n(j) = Sheets("guys").Cells(i, j)
Next 'por kardan
k = 3
Do 'hhhh
For Z = 2 To 4 'por dovomi
o(Z) = Sheets("p").Cells(k, Z)
Next 'por dovomi
If n(j) = o(Z) Then
Sheets("guys").Cells(i, 1) = Sheets("p").Cells(k, 2)
flag = True
Else
flag = False
k = k + 1
End If
Loop Until flag = False 'hhhhh
Next 'satrha
End Sub
Guessing from your existing code, my following code will copy the value from sheet "p" column B into sheet "guys" column A when a match is found.
Sub compare()
Dim i As Integer
Dim j As Integer
Dim l As Integer
l = Sheets("p").Range("B65535").End(xlUp).Row
Debug.Print l
For i = 3 To 20
For j = 3 To l
If Sheets("guys").Cells(i, 2).Value = Sheets("p").Cells(j, 2).Value And _
Sheets("guys").Cells(i, 3).Value = Sheets("p").Cells(j, 3).Value And _
Sheets("guys").Cells(i, 4).Value = Sheets("p").Cells(j, 4).Value Then
Sheets("guys").Cells(i, 1).Value = Sheets("p").Cells(j, 2).Value
Exit For
End If
Next
Next
End Sub
Noted that I explicitly said Value in my code. That will copy the computed value (e.g. result of a formula) instead of the "original" content.