How to lookup multiple cells based on multiple criteria in VBA - excel

I'm extremely new to VBA and have tried Googling to find what I need, but have fallen short.
I have a sheet (Sheet1) containing a list of companies that currently have, or at some point have had, a subscription. The list contains the City (Col A), the Company (Col B), the Category (Col C) and a Cancellation Date (Col D) (if applicable). What I want to do is fill in the current company for that city/category on a different sheet. I want those headers to be City (Col D), Category 1 (Col E), Category 2 (Col F), and Category 3 (Col G).
Here are images of the two sheets of test data:
Sheet 1
Sheet 2
There can only be one company per category per city. For example: in my test data, company D was under Category 1 in San Antonio, but cancelled on 11/12/2021. Then, company N took that spot in San Antonio. So, in my table on Sheet 2, I want company N to be populated. The data set I'm using this for is very large and constantly changing, so I would like an automated way to do this.
Here is a copy of the code I pieced together:
Sub CompanyLookup()
With Application
.Calculation = xlCalculationManual
.EnableEvents = False
.ScreenUpdating = False
End With
Dim lastRowInCity, lastRowOutCity, i, k, m As Long
Dim lookFor, j, inArray, outArray, findArray As Variant
Dim inWks, outWks As Worksheet
Set inWks = ThisWorkbook.Sheets(1)
Set outWks = ThisWorkbook.Sheets(2)
lastRowInCity = inWks.Cells(Rows.Count, "A").End(xlUp).Row
lastRowOutCity = outWks.Cells(Rows.Count, "D").End(xlUp).Row
lastRowCategory = inWks.Cells(Rows.Count, "C").End(xlUp).Row
lastRowDate = inWks.Cells(Rows.Count, "D").End(xlUp).Row
lastColCategory = outWks.Cells(Columns.Count, "D").End(xlToLeft).Column
inArray = Range(inWks.Cells(1, 1), inWks.Cells(lastRowInCity, 3))
findArray = Range(outWks.Cells(1, 4), outWks.Cells(lastRowOutCity, 4))
outArray = Range(outWks.Cells(1, 5), outWks.Cells(lastRowOutCity, 5))
On Error Resume Next
For i = 2 To lastRowOutCity
For j = 2 To lastRowInCity
For k = 2 To lastRowCategory
For m = 2 To lastRowDate
lookFor = findArray(i, 1)
If inArray(j, 1) = lookFor And inArray(m, 4) < 1 And inArray(k, 3) = outArray(lastColCategory, 1) Then
outArray(i, 1) = inArray(j, 2)
Exit For
End If
Next j
Next m
Next k
Next i
Range(outWks.Cells(1, 5), outWks.Cells(lastRowOutCity, 5)) = outArray
With Application
.Calculation = xlCalculationAutomatic
.EnableEvents = True
.ScreenUpdating = True
End With
End Sub

Assuming your data looks exactly as your screenshots:
Sub CompanyLookup()
Dim sourceData, resultData, rngSource As Range, rngResult As Range
Dim r As Long, c As Long, city As String, cat As String, rSrc As Long
Set rngSource = ThisWorkbook.Sheets(1).Range("A1").CurrentRegion
Set rngResult = ThisWorkbook.Sheets(2).Range("D1").CurrentRegion
sourceData = rngSource.Value
resultData = rngResult.Value
'scan through the results array
For r = 2 To UBound(resultData, 1)
city = resultData(r, 1) 'city
For c = 2 To UBound(resultData, 2)
cat = resultData(1, c) 'category
'Scan the source data for a city+category match,
' ignoring lines with a cancellation date
For rSrc = 2 To UBound(sourceData, 1)
If Len(sourceData(rSrc, 4)) = 0 Then 'no cancellation date
If sourceData(rSrc, 1) = city And sourceData(rSrc, 3) = cat Then
resultData(r, c) = sourceData(rSrc, 2) 'populate the company
Exit For 'done searching
End If
End If
Next rSrc
Next c
Next r
rngResult.Value = resultData 'populate the results
End Sub

I had exact same issue this week, and from what i read online, the fact that you cannot use vlookup or find function for multiple criteria. Mostly people prefer using .find fuction and when you find it, you can use loop to find second criteria. It was what i used.

Related

Scripting.Dictionary to fill a column with a string taking in consideration more then 3 criteria

I am trying to fill a column C with the string consider if the consumer on the row matches one of the criteria:
If the consumer meets one of these rules, the value should be set to Consider:
• Consumer has only 1 Transaction -- (is done)
• Consumer has 2 - 4 Transactions but total volume < 10,000 USD --- (is done)
• Consumer Level (based on rule below) is Level 2 or Level 3 --- ( this information are on column CV and CW)
• If dropdown is 60 Days and max transaction date is older than 30 days
• if dropdown is 1 year and max transaction date is older than 90 days
• If dropdown is 5 years and max transaction date is older than 180 days
'Interdction Review Tab, column C
Sheets("Interdiction Review").Columns(3).Font.Bold = True
Sheets("Interdiction Review").Columns(3).HorizontalAlignment = xlCenter
'Consumer has only 1 Transaction, the value on Interdiction Review Tab on Column C will be Consider
Dim wsStart As Worksheet, lastRow1 As Long, wsFinal As Worksheet
Dim dict As Object, rw As Range, v, v2, k, m, lin
Dim wsSSart As Worksheet
Dim dateDifference As Long
Dim SStartSelection As String
Dim isConsider As Boolean
Dim valid_col(1) As Integer
Dim lvl As Boolean
Set wsSSart = ActiveWorkbook.Sheets("SStart")
Set wsStart = ActiveWorkbook.Sheets("Start")
Set wsFinal = ActiveWorkbook.Sheets("Interdiction Review")
lastRow1 = wsStart.Cells(Cells.Rows.Count, "A").End(xlUp).Row
Set dict = CreateObject("Scripting.Dictionary")
SStartSelection = wsSSart.Cells(7, "A").Value
lvl = False
For Each rw In wsStart.Range("A2:AJ" & lastRow1).Rows
v = rw.Cells(8).Value
v2 = rw.Cells(36).Value
If Len(v) = 0 Or Len(v2) = 0 Then
v = rw.Cells(7).Value
v2 = rw.Cells(35).Value
End If
dict(v) = dict(v) + 1
dict(v2) = dict(v2) + 1
Next rw
For Each k In dict
isConsider = False
m = Application.Match(k, wsFinal.Columns(1), 0)
wsFinal.Cells(m, 7).FormulaArray = wsFinal.Cells(m, 7).Formula
dateDifference = DateDiff("D", wsFinal.Cells(m, 7).Value, Date)
If dict(k) = 1 Then
isConsider = True
ElseIf dict(k) >= 2 And dict(k) <= 4 And wsFinal.Cells(m, 6).Value <= 10000 Then
isConsider = True
End If
If StrComp(SStartSelection, "60 Days") = 0 And dateDifference > 30 Then
isConsider = True
ElseIf StrComp(SStartSelection, "1 Year") = 0 And dateDifference > 90 Then
isConsider = True
ElseIf StrComp(SStartSelection, "5 Years") = 0 And dateDifference > 180 Then
isConsider = True
End If
'Client number
If wsStart.Cells(2, 8) <> "" Then
valid_col(0) = 8
valid_col(1) = 36
Else
valid_col(0) = 7
valid_col(1) = 35
End If
'Level verification
For lin = 2 To lastRow1
If wsStart.Cells(lin, valid_col(0)) = k Then
If wsStart.Cells(lin, 100).Value = "Level 2" Or wsStart.Cells(lin, 100).Value = "Level 3" Then
lvl = True
Exit For
End If
End If
If wsStart.Cells(lin, valid_col(1)) = k Then
If wsStart.Cells(lin, 101).Value = "Level 2" Or wsStart.Cells(lin, 101).Value = "Level 3" Then
lvl = True
Exit For
End If
End If
Next lin
If isConsider And lvl Then
If Not IsError(m) Then wsFinal.Cells(m, 3).Value = "Consider"
End If
Next k
End Sub
It seems that my code is looking in the wrong column to check for the clients Level. ex:
Client number 3 is located on column H so the code needs to check column CV to see the level
client number 3 is as well located on column AJ the code needs to check the Column CW to see the level.
if the client is located on both columns and cod need to check both columns for the find the information.
The level for column CV is when the client number is on column H or/and G
The level for column CW is when the client is on Column AJ or/and AI
I asked here as well (and you can download the file)
https://www.ozgrid.com/forum/index.php?thread/1228270-how-to-populate-a-column-with-a-string-taking-in-consideration-5-different-crite/&postID=1239894#post1239941
The only time that lvl is set to False is before the For Each k In dict loop ever happens.
So, once a particular row sets lvl to True within that loop, every subsequent row will also have lvl be True, because there's nothing in the loop to set lvl back to False. Try this instead:
For Each k In dict
isConsider = False
lvl = False
Your code is too large. I don't think you will get the answer you want because of the time it takes to find the problem. Therefore I will teach you how to structure your code in such a way as to be able to discuss any part of it. Please consider the code below.
Sub NewTest()
' 093
Dim WsIR As Worksheet
Set WsIR = CreateWsIR()
Worksheets("Start").Activate ' probably not useful
End Sub
Private Function CreateWsIR() As Worksheet
' 093
Dim Fun As Worksheet ' = Function return object under preparation
Set Fun = Worksheets.Add ' Excel will make this the ActiveSheet
With Fun
.Name = "Interdiction Review"
.Move After:=Worksheets("Start")
' format your sheet here
End With
Set CreateWsIR = Fun
End Function
Look at the advantages of this structure.
The first 30-odd lines of your code are compressed into just one.
This allows you to clearly develop your narrative in the main procedure.
Meanwhile everything related to creating the new worksheet is bundled into one, separate procedure which is easy to test, easy to maintain and easy to ask questions about should the need arise.
As you continue to create your project's narrative you will come to a point where the task is to populate column C. With the above method that filtering and elimination process will take place in a function which is separate just as the the function CreateWsIR is separate above. It will return one value which you will insert into a cell in the main procedure. In your present setup you can't even pinpoint where that action takes place (and neither can we). If you change the structure to make it more transparent you wouldn't have such a problem and we would be happy to assist.

How to add values from different sheets, and retain the formula pointing to the sheets+cells?

I am trying to add values from different sheets (Sheet 2 to 5) into my main sheet (Sheet 1). In Sheet 1 I want the cells to contain the right formula pointing to the different sheets (if possible).
Typically like this:
='Sheet2'!D5+'Sheet3'!D165
All my sheets have different products, but some sheets contain same products. So I want to search through them all and ADD them in my Main Sheet (Sheet 1).
Sub UpdateMainSheet()
' Kode for å Oppdatere Plukkeliste Alle Artikler Summert
Dim AktivtArk As String
Dim AktivtArkNavn As String
Dim K As Integer
Dim Count As Integer
'Line of code to delete old data in Main Sheet:
Worksheets("Sheet1").Range("A2:H10000").Clear
AktivtArkOverskrift = "List of Articles from Sheet 2 to 5"
'Creates Headline in Main Sheet:
eRow = Sheet1.Cells(Rows.Count, 1).End(xlUp).Offset(1, 0).Row
Worksheets("Sheet1").Cells(eRow, 1) = AktivtArkOverskrift
Worksheets("Sheet1").Cells(eRow, 1).Font.Bold = True
'Script to check and gather data from the other sheets (Sheet 2, 3, 4 and 5):
For K = 2 To 5
'For loop to check each line in sheet "K"
For I = 2 To 1000
'If function to check if the cell I in column F is empty, if so it_jumps to next row and do the same check:
If Worksheets(K).Cells(I, 6) > 0 Then
Count = 0
'For loop to check if I already have a row in the Main Sheet with the article I'm checking:
For L = 2 To 1000
'If function to check if the articles have the same article number:
If Worksheets(K).Cells(I, 1) = Worksheets("Sheet1").Cells(L, 1) Then
'Line of code that are supposed to ADD the values that is currently in the Main Sheet, togheter with the value in Sheet K:
Worksheets("Sheet1").Cells(I, 4).Formula = Worksheets("Sheet1").Cells(I, 4) + Worksheets(K).Cells(L, 4)
End If
Next L
End If
Next I
Next K
End Sub
So what I need to fix in my code is this part (located furthest inside the For Loop):
Worksheets("Sheet1").Cells(I, 4).Formula = Worksheets("Sheet1").Cells(I, 4) + Worksheets(K).Cells(L, 4)
And make it create a formula in the wanted cell, that looks something like this:
='Sheet2'!D5+'Sheet3'!D165
It must be able to add another cell as well, since the Loop are running through several Sheets (Sheet 2 to 5) that may contain the same products.
I.e. I only want one line in my Main Sheet for each product.
I managed to find the solution in the end.
It seemed I had switched the L and I in som of the looping, which resulted in the values not to be added togheter.
The following code (I did not translate to English, but can do this if someone wants/need it) solved my issue, and gave me the values from Sheet 2 to 5 sorted by product in Sheet 1:
Sub OppdaterePlukkelisteSummert()
'Kode for å Oppdatere Plukkeliste Alle Artikler Summert
Dim AktivtArk As String
Dim AktivtArkNavn As String
Dim K As Integer
Dim Teller As Integer
Dim value1 As Integer
Dim value2 As Integer
'Sletter Plukklisten for å oppdatere og sortere på nytt:
Worksheets(1).Range("A2:H10000").Clear
'HENTING AV DATA FRA ARKET "K":
AktivtArk = "Artikler Summert fra Alle Ark"
AktivtArkOverskrift = "Artikler Summert fra Alle Ark"
'Setter inn Overskrift som Forteller kva ark utstyret kommer fra:
eRow = Sheets(1).Cells(Rows.Count, 1).End(xlUp).Offset(1, 0).Row
Worksheets(1).Cells(eRow, 1) = AktivtArkOverskrift
Worksheets(1).Cells(eRow, 1).Font.Bold = True
'Sjekker hvilke/hvilket rader/utstyr som skal plukkes, og legger det inn i "Ark1":
For K = 2 To 5
For I = 2 To 1000
If Worksheets(K).Cells(I, 6) > 0 Then
Teller = 0
For L = 2 To 1000
If Worksheets(K).Cells(I, 1) = Worksheets(1).Cells(L, 1) Then
value1 = Worksheets(1).Cells(L, 4)
value2 = Worksheets(K).Cells(I, 4)
Worksheets(1).Cells(L, 4) = value1 + value2
Worksheets(1).Cells(L, 6) = value1 + value2
Else
Teller = Teller + 1
End If
Next L
If Teller > 998 Then
eRow = Sheets(1).Cells(Rows.Count, 1).End(xlUp).Offset(1, 0).Row
For J = 1 To 11
Worksheets(1).Cells(eRow, J) = Worksheets(K).Cells(I, J)
Next J
Worksheets(1).Cells(eRow, 6).Font.Color = RGB(0, 150, 0)
Worksheets(1).Cells(eRow, 7).Font.Color = RGB(0, 150, 0)
End If
End If
Next I
Next K
Worksheets(1).Range("A2").Select
End Sub
I hope this can be useful for someone else :-)
All help and suggestion in the comments are appreciated!
I was going to illustrate with this simple example:
I = 1 'for example
For K = 2 To 5
Worksheets("Sheet1").Cells(I, 4).Value = Worksheets("Sheet1").Cells(I, 4).Value + _
WorksheetFunction.SumIf(Worksheets(K).Range("A:A"), "Bananas", Worksheets(K).Range("D:D"))
Next K

VBA - Find max percent change based on values in two different sheets in Excel?

I've been staring at this for so long and I honestly have no clue how to do it. Assume I have Sheet1 which has Employee ID in column A and Salary in column B for January 2016, and Employee ID and Salary in column A and B for December 2016 on Sheet2. How would I go about writing a for loop that finds the max percent difference in salaries based on employer ID? I would need to use some form of Vlookup since they don't match exactly.
Currently, this is what I have:
Sub Max_Percent_Change()
Dim Salary
For Each Cell In Worksheets("Sheet1").Range("A2:A1000")
Salary = Application.WorksheetFunction.VLookup(Cell, _
Worksheets("Sheet2").Range("A2:B1000"), 2, False)
If data is setup as shown in the images, please give this a try...
Change the sheet names if required in the code.
Sub FindSalaryPercentageChange()
Dim wsJan As Worksheet, wsDec As Worksheet
Dim x, y, z()
Dim i As Long
Dim janSalary As Double, decSalary As Double
Dim pChng As Double
Dim r
Application.ScreenUpdating = False
Set wsJan = Sheets("January16")
Set wsDec = Sheets("December16")
x = wsJan.Range("A1").CurrentRegion.Value
y = wsDec.Range("A1").CurrentRegion.Value
ReDim z(1 To UBound(x, 1) - 1)
wsDec.Columns("C").Clear
For i = 2 To UBound(y, 1)
r = Application.Match(y(i, 1), Application.Index(x, , 1), 0)
If Not IsError(r) Then
janSalary = x(r, 2)
decSalary = y(i, 2)
pChng = (decSalary - janSalary) / janSalary
z(i - 1) = pChng
End If
Next i
wsDec.Range("C1").Value = "%Change"
wsDec.Range("C2").Resize(UBound(z)).Value = Application.Transpose(z)
wsDec.Columns(3).NumberFormat = "0.00%"
Application.ScreenUpdating = True
End Sub
January16 Data:
December16 Data:

Unique Count Formula for large dataset

I am having trouble determining a way to enter a 1 or 0 into an adjacent cell to indicate whether or not a value is unique when working with a large dataset. I have read of multiple methods for accomplishing this, however none of them seem efficient for my purposes: I am using an instance of Excel 2010 (so I do not have the Distinct Count feature in PivotTables, and when I try to use PowerPivot it crashes my file due to processing limitations.
In this StackOverflow question: Simple Pivot Table to Count Unique Values there are suggestions to use SUMPRODUCT or COUNTIF, but when working with 50,000+ rows as I am, this causes terrible performance and a file size of ~35 MB instead of ~3 MB. I wanted to know if there is a better solution for a large, dynamic dataset whether it is a formula or VBA.
An example of what I would like to accomplish is (with the Unique column being the adjacent cell):
Name Week Unique
John 1 1
Sally 1 1
John 1 0
Sally 2 1
I attempted to script the same functionality of COUNTIF but with no success:
For Each Cell In ThisWorkbook.Worksheets("Overtime & Type Data").Range("Z2:Z" & DataLastRow)
If Worksheets("Overtime & Type Data").Cells(Cell.Row, 26) <> Worksheets("Overtime & Type Data").Cells(Cell.Row - 1, 26) Then
FirstCell = Cell.Row
End If
If (Worksheets("Overtime & Type Data").Range(Cells(FirstCell, 26), Cells(Cell.Row, 26)) = Worksheets("Overtime & Type Data").Range(Cells(Cell.Row, 26))) = True Then
Cell.Value = 1
Else
Cell.Value = 0
End If
Next Cell
This code ran on over 130,000 rows successfully in less than 3 seconds. Adjust the column letters to fit your dataset.
Sub tgr()
Const colName As String = "A"
Const colWeek As String = "B"
Const colOutput As String = "C"
Dim ws As Worksheet
Dim rngData As Range
Dim DataCell As Range
Dim rngFound As Range
Dim collUniques As Collection
Dim arrResults() As Long
Dim ResultIndex As Long
Dim UnqCount As Long
Set ws = ThisWorkbook.Sheets("Overtime & Type Data")
Set rngData = ws.Range(colName & 2, ws.Cells(Rows.Count, colName).End(xlUp))
Set collUniques = New Collection
ReDim arrResults(1 To rngData.Cells.Count, 1 To 1)
On Error Resume Next
For Each DataCell In rngData.Cells
ResultIndex = ResultIndex + 1
collUniques.Add ws.Cells(DataCell.Row, colName).Value & ws.Cells(DataCell.Row, colWeek).Value, ws.Cells(DataCell.Row, colName).Value & ws.Cells(DataCell.Row, colWeek).Value
If collUniques.Count > UnqCount Then
UnqCount = collUniques.Count
arrResults(ResultIndex, 1) = 1
Else
arrResults(ResultIndex, 1) = 0
End If
Next DataCell
On Error GoTo 0
ws.Cells(rngData.Row, colOutput).Resize(rngData.Cells.Count).Value = arrResults
End Sub
One approach is to sort by Name and Week. Then you can determine Unique for any row by comparing with the previous row.
If you need to preserve the order, you could first write a column of Index numbers (1, 2, 3, ...) to keep track of order. After calculating Unique, sort by Index to restore the original order.
The whole process could be done manually with relatively few steps, or automated with VBA.
I'm not sure how well this will work with 50000 values, but it goes through ~1500 in about a second.
Sub unique()
Dim myColl As New Collection
Dim isDup As Boolean
Dim myValue As String
Dim r As Long
On Error GoTo DuplicateValue
For r = 1 To Sheet1.UsedRange.Rows.Count
isDup = False
'Combine the value of the 2 cells together
' and add that string to our collection
'If it is already in the collection it errors
myValue = Sheet1.Cells(r, 1).Value & Sheet1.Cells(r, 2).Value
myColl.Add r, myValue
If isDup Then
Sheet1.Cells(r, 3).Value = "0"
Else
Sheet1.Cells(r, 3).Value = "1"
End If
Next
On Error GoTo 0
Exit Sub
DuplicateValue:
'The value is already in the collection so put a 0
isDup = True
Resume Next
End Sub
Just about any bulk operation will beat a loop involving worksheet cells. You might be able to trim the time down a bit by performing all of the calculations in memory and only returning the values back to the worksheet en masse when it is complete.
Sub is_a_dupe()
Dim v As Long, vTMP As Variant, vUNQs As Variant, dUNQs As Object
Debug.Print Timer
On Error GoTo bm_Uh_Oh
Set dUNQs = CreateObject("Scripting.Dictionary")
With Worksheets("Sheet1")
vTMP = .Range(.Cells(2, 1), .Cells(Rows.Count, 2).End(xlUp)).Value2
ReDim vUNQs(1 To UBound(vTMP, 1), 1 To 1)
For v = LBound(vTMP, 1) To UBound(vTMP, 1)
If dUNQs.Exists(Join(Array(vTMP(v, 1), vTMP(v, 2)))) Then
vUNQs(v, 1) = 0
Else
dUNQs.Add Key:=Join(Array(vTMP(v, 1), vTMP(v, 2))), _
Item:=vTMP(v, 2)
vUNQs(v, 1) = 1
End If
Next v
.Cells(2, 3).Resize(UBound(vUNQs, 1), 1) = vUNQs
End With
Debug.Print Timer
bm_Uh_Oh:
dUNQs.RemoveAll
Set dUNQs = Nothing
End Sub
Previous experience tells me that the variety of data (as well as hardware, etc) will impact timing the process but in my random sample data I received these elapsed times.
 50K records ..... 0.53 seconds
130K records .... 1.32 seconds
500K records .... 4.92 seconds

Count if statement excluding strings and not equal to a number in excel

I have an excel set for which I need to count entries based on names. They're all in the same column and there is supposed to be 4 of each entry. I need a formula to count the number of cells with the same entry that do NOT start with either "Retail" or "Commercial" and only return the names in the cells for which there is NOT 4. For example, if my data looks thusly:
NAME
Retail - John
Retail - Sue
Kara
Kara
Joe
Joe
Joe
Joe
Commercial
Sarah
I want a formula that will search this column, and only return "Kara - 2" and "Sarah - 1". The "Retail" and "Commercial" are excluded from the start and since "Joe"=4 I'm not concerned with that. Is there some way I can have this search the column, have it return the first count to meet that criteria to C1, the next one to C2 and so on until I have a column of just the non-compliant entries? I'd love an output like below:
NAME COUNT
Kara 2
Sarah 1
Thanks for looking, I really appreciate any help and advice you can offer!
If your data is in column A the results table will be in columns B & C after running this macro:
Sub MAIN()
Dim A As Range, wf As WorksheetFunction
Dim s1 As String, s2 As String
Dim col As Collection
Set A = Intersect(Range("A:A"), ActiveSheet.UsedRange)
Set wf = Application.WorksheetFunction
Set col = MakeColl(A)
s1 = "Retail"
s2 = "Commercial"
K = 1
For i = 1 To col.Count
v = col.Item(i)
If InStr(v, s1) = 0 And InStr(v, s2) = 0 Then
n = wf.CountIf(A, v)
If n <> 4 Then
Cells(K, "B").Value = v
Cells(K, "C").Value = n
K = K + 1
End If
End If
Next i
End Sub
Public Function MakeColl(rng As Range) As Collection
Set MakeColl = New Collection
Dim r As Range
On Error Resume Next
For Each r In rng
v = r.Value
If v <> "" Then
MakeColl.Add v, CStr(v)
End If
Next r
MsgBox MakeColl.Count
End Function

Resources