I'm looking to use conditional formatting to mark duplicates, but I want to look for duplicates in multiple columns and mark the entry regardless of which column the duplicate is located in. This is not natively supported in any way I've found, so I made my own UDF. Code listed at the bottom of this post.
The problem is that while the UDF works as expected, I cannot get the CF to work.
I have a CF where I'm formatting some cells based on the value in the same row but different column, and I've used the formula =INDIRECT("Z" & ROW()) <> 0 with great success to achieve this. Building on this, I tried the following for my UDF:
=findCandidatesForDuplicate(ADDRESS(ROW(); COLUMN(); 4))
=findCandidatesForDuplicate("B" & ROW())
But this code does not give any formatting - none whatsoever. The applied range is $B$2:$B$4000, since that's the only range I want it to mark. I've tried changing the UDF and the CF input between passing strings to passing ranges, but nothing I do will get it to apply the formatting.
I assume since formatting is not applied, that the UDF is not run on the intended selected cells, which again I assume is caused by not being able to make Excel understand how I want it to parse the CF input.
In the Worksheet, for row 14 which I know is a duplicate, I can input =findCandidatesForDuplicate("B14") and the cell will show TRUE.
Likewise, I can input =findCandidatesForDuplicate(ADDRESS(ROW(B14); COLUMN(B14);4)) and it will also show TRUE.
The question then boils down to this: how do I make the conditional formatting engine understand what I want to do (which is run this UDF with every cell in the range the rule is applied to as an argument and mark the corresponding cell in column B)?
Here's the function code. Basically a row is regarded as a duplicate if it finds duplicate entries in any of the columns listed.
Function findCandidatesForDuplicate(rngStr As String, Optional countOnly As Boolean, Optional dbg As Boolean) As Variant
Dim rng As Range
Dim colA As Range, searchString As String, result As Long
Dim ws As Worksheet, tbl As ListObject
Set ws = Application.ThisWorkbook.Worksheets(1)
Set tbl = ws.ListObjects("Tabell1")
Set rng = ws.Range(rngStr)
Set colA = Range("A" & rng.Row)
For i = 3 To 5
searchString = colA.Offset(0, i - 1).Value
If searchString = "" Then GoTo NextIteration
'Set rng = Range(rng.Address, tbl.ListColumns(i).DataBodyRange.Address) ' Only searches downwards from input range
Set rng = Range(tbl.ListColumns(i).DataBodyRange.Address) ' Searches the entire column
result = Application.WorksheetFunction.CountIf(rng, "=" & searchString)
If result > 1 Then
If dbg = True Then Debug.Print "Found result in loop no. " & i - 2 & ", matching on value " & searchString
Exit For
End If
NextIteration:
Next i
If countOnly = True And result > 1 Then
findCandidatesForDuplicate = result
ElseIf countOnly = True Then
findCandidatesForDuplicate = 0
ElseIf result > 1 Then
findCandidatesForDuplicate = True
Else
findCandidatesForDuplicate = False
End If
End Function
I haven't found why this doesn't work, nor how to make it work, so here's a workaround:
Populate some far-off column with the UDF calling its own row, then use =INDIRECT("AAA" & ROW()) = whateverValueSetByUDF as the formula to make CF show selected formatting on the cells that trigger the UDF.
It's unelegant and requires a column-wide, manual text-to-column update on the column containing the UDF results every time something changes (filtering the table will work fine, but if you change the sorting you have to update as described)... but it works as long as you know how to use the sheet.
Also, I revised the code in order to have more options for how to format the rows. With this style of output, you can - in addition to finding the rows which are possible duplicates - highlight which columns are triggering.
Usage: Use the INDIRECT formula as above, tailor to your specific setup. I recommend setting = or <> to one of the resultWhereString values.
Function findCandidatesForDuplicate(rng As Range, Optional countOnly As Boolean, Optional dbg As Boolean) As Variant
Dim colA As Range, searchString As String, result As Long, resultWhereString As String
Dim ws As Worksheet, tbl As ListObject
Set ws = Application.ThisWorkbook.Worksheets(1)
Set tbl = ws.ListObjects("Table1")
Set colA = Range("A" & rng.Row)
For i = 3 To 5
searchString = colA.Offset(0, i - 1).Value
' --> You can add more criteria here
If searchString = "" Or searchString = "myEmail#domain.com" Then GoTo NextIteration
Set rng = Range(tbl.ListColumns(i).DataBodyRange.Address) ' Searches the entire column
result = Application.WorksheetFunction.CountIf(rng, "=" & searchString)
If result > 1 Then
If dbg = True Then Debug.Print "Found result in loop no. " & i - 2 & ", matching on value " & searchString
Select Case i
' --> Update this loop if the range of i changes
Case 3
resultWhereString = "ResultCol1"
Case 4
resultWhereString = "ResultCol2"
Case 5
resultWhereString = "ResultCol3"
End Select
Exit For
End If
NextIteration:
Next i
If countOnly = True And result > 1 Then
findCandidatesForDuplicate = result
ElseIf countOnly = True Then
findCandidatesForDuplicate = 0
ElseIf result > 1 Then
findCandidatesForDuplicate = resultWhereString
Else
findCandidatesForDuplicate = ""
End If
End Function
Related
I'm trying to check two sets of information in two different tabs, and then get all the records into a third tab, highlighting discrepancies in the information and also records that are present in a set but not the other. As an added difficulty, the information that I need to check is not written exactly in the same way in both tabs. Eg: in one of the tabs products are called "Product 1, Product 2", etc, whereas the other uses just the numbers.
I'm pretty new to VBA, and my best idea so far is selecting a column with IDs in one of the sets and using Find to check the other set for matches. After that I'd like to use Offset on the value Find returns to check the other cells in the row.
However, I'm encountering and 'Object variable or With block variable not set' error and I don't know what I'm doing wrong.
Below is the code, I'd really appreciate any help with using Offset in this scenario (or ideas on a more efficient way to get the results).
Sub Test()
Dim Candi_ID As String
Dim Full_Name As String
Dim i_Row As Object
Dim i_Cell As Range
Dim MD_Range As Integer
Dim i_Cell As Range
Sheets("M Report").Select
MD_Range = Application.WorksheetFunction.CountA(Range("C:C")) 'column with the IDs
For R = 2 To MD_Range
Candi_ID = Sheets("M Report").Cells(R, 3)
Full_Name = Sheets("M Report").Cells(R, 1)
If Candi_ID <> "" Then
With Sheets("i Report").Range("B:B")
Set i_Cell = .Find(What:="*" & Candi_ID, LookIn:=xlValues)
If i_Cell Is Nothing Then
Sheets("Tracker").Range("A" & Last_Row + 1) = Candi_ID
Sheets("Tracker").Range("A" & Last_Row + 1).Interior.Color = RGB(255, 0, 0)
Else
Last_Row = Sheets("Tracker").Cells(.Rows.Count, "A").End(xlUp).Row
Sheets("Tracker").Range("A" & Last_Row + 1) = Candi_ID
End If
If Full_Name <> "" Then
If Full_Name = i_Cell.Offset(0, -1) Then 'full name is one cell to the left of the ID cell
Sheets("Tracker").Range("C" & Last_Row + 1) = Full_Name
Else
Sheets("Tracker").Range("C" & Last_Row + 1) = Full_Name
Sheets("Tracker").Range("C" & Last_Row + 1).Interior.Color = RGB(255, 0, 0)
End If
End If
End With
End If
Last_Row = Last_Row + 1
Next R
End Sub
You need another test in case i_Cell was not set on this line:
Set i_Cell = .Find(What:="*" & Candi_ID, LookIn:=xlValues)
Something like:
If Full_Name <> vbNullString And Not i_Cell Is Nothing Then
If it is Nothing, and you don't test for this further down, you will get the error you mention.
Also, you have a duplicate declaration, some missing declarations, and use Long rather than Integer. Put Option Explicit at the top of all your modules. Avoid .Select, which slows your code, and use With statements where possible.
I replaced the empty literal string "" with vbNullString.
I am new to VBA, facing the following problem:
I need to return certain value, returned by IF formula, based on numeric data, kept in another worksheet.
I have written something like this, however all the time when it comes to the point of running IF part, it gives me the error Type mismatch, and the problem seems to be in the values, found by vlookup. I was trying to declare it as long, variant and so on but that didn't help. However the MsgBox returnes the result from another sheet properly. Another sheet is formatted as numbers. Any ideas how to make it work?
here is the code i have for now:
Option Explicit
Sub find()
Dim lookup As String
Dim pkgWidth, pkgLength, pkgHeight, displaySize, AllHeaders, headerweight, itemweight, classify As Range
Dim lastrow As Variant
Dim cl As Range
Dim i As Integer
Dim widthh, lengthh, Heightt, display, Weight As Variant
'this part dynamically searches for the columns I need
Set AllHeaders = Worksheets("Sheet2").Range("1:1")
Set pkgWidth = AllHeaders.find("package_width")
Set pkgLength = AllHeaders.find("package_length")
Set pkgHeight = AllHeaders.find("package_height")
Set displaySize = AllHeaders.find("display_size")
Set headerweight = Worksheets("Sheet1").Range("1:1")
Set itemweight = headerweight.find("Item Weight")
Set classify = headerweight.find("AT")
lastrow = Cells(Rows.Count, 1).End(xlUp).Row
For i = 2 To lastrow
lookup = Worksheets("Sheet1").Cells(i, 1).Value
Set cl = Worksheets("Sheet1").Cells(i, classify.Column)
'here the values are being looked up from another sheet
widthh = Application.VLookup(lookup, _
Worksheets("Sheet2").Range("A1").CurrentRegion, pkgWidth.Column, False)
lengthh = Application.VLookup(lookup, _
Worksheets("Sheet2").Range("A1").CurrentRegion, pkgLength.Column, False)
Heightt = Application.VLookup(lookup, _
Worksheets("Sheet2").Range("A1").CurrentRegion, pkgHeight.Column, False)
display = Application.VLookup(lookup, _
Worksheets("Sheet2").Range("A1").CurrentRegion, displaySize.Column, False)
Weight = Application.VLookup(lookup, _
Worksheets("Sheet1").Range("A1").CurrentRegion, itemweight.Column, False)
If display > 6 Then
If Weight < 25 Then
cl.Value = 1.01
Else
cl.Value = 1.02
End If
Else
If widthh >= 1970 Or lengthh >= 1970 Or Heightt >= 1970 Then
If Weight <= 8 Then
cl.Value = 3.01
Else
If Weight >= 35 Then
cl.Value = 3.02
Else
cl.Value = 3.03
End If
End If
Else
If Weight <= 3 Then
cl.Value = 5.01
Else
If Weight >= 8 Then
cl.Value = 5.03
Else
cl.Value = 5.02
End If
End If
End If
End If
Next i
End Sub
When using Application.VLookup (or any of its variants) you must take into account that it can return #N/A, as explained in the documentation:
If lookup_value is smaller than the smallest value in the first column of table_array, VLOOKUP returns the #N/A error value.
If for instance display gets that value, then the expression display > 6 will give you the Type mismatch error.
So to prevent that, either change the logic of your code so that VLookup is guaranteed to not return #N/A (if this is possible in your case, I cannot say), or test for this error value, like this:
If IsError(display) Then
' Treat the error condition...
ElseIf display > Then
' ...etc.
The same precaution may be needed for other variables that get the result of a VLookup call.
I'm trying to use a Do Until to run down a column of data (could be any column across the worksheet) and perform certain tasks based on the value in column A.
Column A results are either 1, 2 or END!? I want to run down any column and perform a routine based on the value in A until the value in A = "END!?"
I can't use the offset function as the Do Until will move across a column each month.
Basically I need the code to always check the value in column A and do one of three functions depending on the value and my mind has gone a complete blank!
I already have the full Do Until code written, and the sub routines to run depending on what is in there.
I just cant get the looking at the value in column A part right!
Use Select Case :
Dim LastRow As Double
Dim wS As Worksheet
Dim i As Double
Set wS = thisworkbook.Worksheets("Sheet1") ' Sheet Name
With wS
LastRow = .Range("A" & .rows.Count).End(xlup).Row
For i = 2 To LastRow
Select Case .Cells(i,1).Value
Case Is = 1
Call SubProc1
Case Is = 2
Call SubProc2
Case Is = "END!?"
Call SubProcEND
Case Else
MsgBox "Value : " & Criteria1 & " not handled!", _
vbCritical + vbOKOnly, "Case not handled"
End Select
Next i
End With 'wS
I have a workbook with three sheets (Dash, HT, RV.)
I am trying to write a macro/function that counts how many times a value from 'Dash' exists in a specific column within sheet 'RV' then output that value in a specific cell within 'Dash'
I could go so far as to say that the value within 'Dash' is static and repeat it (The variable from 'Dash' won't ever change as it's a list of Usernames)
In my head it's something like: Count whatever.variable.Dash in column J of sheet.RV print in Dash.B2...
I was able to find a MsgBox option that works, but I have to manually type in each Username (which is a 16character name (string)) then a MsgBox tells me the occurrences. I'm looking to just automate this option with a fixed/static username in the macro/function because the amount of rows in 'RV' can vary between 700 entries to 23k entries
The MsgBox option is:
Dim Count as Integer
Dim Target As String
Dim Cell as Object
Dim N As Integer
Sub Target_Count()
Count = 0
Target = InputBox("character(s) to find?")
If Target = "" Then GoTo Done
For Each Cell in Selection
N = InStr(1, cell.Value, target)
While N <> 0
Count = count + 1
N = InStr(n + 1, cell.Value, target)
Wend
Next Cell
MsgBox count & " Occurrences of " & target
Done:
End Sub
I want the input box target to be 'Dash.A1:8' and the occurrences to be printed in 'Dash.B1:8'
Can you just use a countif() formula rather than programming a macro? say the column you were counting the "dash"'s in was column B in sheet RV, Then in the cell in sheet Dash, the formula would be:
=COUNTIF(RV!B:B,"dash")
Or, if you wanted to vary what you were counting, you simply replace the hardcoded "dash" in the formula with the input cell address.
If you want VBA you can use this. Adjust it however you want.
Sub Target_Count_2()
Dim wb As Workbook
Set wb = ThisWorkbook
Dim Cell As Range
Dim Count As Integer
Dim LastRow As Long
LastRow = wb.Worksheets("RV").Range("A1").SpecialCells(xlCellTypeLastCell).Row
Dim strArr() As Variant
strArr() = wb.Worksheets("RV").Range("J1:J" & LastRow).Value
Dim i As Long
Dim str As String
For Each Cell In wb.Worksheets("Dash").Range("B1:B8")
Count = 0
str = Cell.Offset(, -1).Value2
For i = LBound(strArr) To UBound(strArr)
If str = strArr(i, 1) Then Count = Count + 1
'If InStr(strArr(i, 1), str) > 0 Then Count = Count + 1
Next
Cell.Value2 = Count
Next
Set Cell = Nothing
Set wb = Nothing
End Sub
Note that str = strArr(i, 1) will match only full value in cells, while InStr(strArr(i, 1), str) > 0 will also match parts in cells. Let's say you are looking for "AAA" in cell with the value "AAAB". The first method will not add additional 1 to the Count, while the second method will.
I'm working on a macro that is supposed to count the number of times the term "GM" appears in a column. I decided to use a countif statement, as I have before and it worked well. However, for some reason when I run my code it outputs 0 every time, which definitely is not correct. I've run this same code with other columns and strings and it has worked fine, but for some reason if I search this certain column for the term "GM" it fails. The only thing I can think of is maybe countif only works if the string you're searching for is the only string in a cell, because in all cases where this is true the code works fine. In this particular case the string I'm looking for is not the only string in the cell and the code is failing. I've tried to find more info on whether or not this is true but I can't find anything online. Here's the code if anyone would like to take a look:
Function OemRequest() As Long
Sheets("CS-CRM Raw Data").Select
Sheets("CS-CRM Raw Data").Unprotect
Dim oem As Long
Dim LastRow As Long
Dim LastColumn As Long
'Determines size of table in document
LastRow = Range("A" & Rows.Count).End(xlUp).row
LastColumn = Cells(1, Columns.Count).End(xlToLeft).Column
oem = Application.WorksheetFunction.CountIf(Range(2 & "2:" & 2 & LastRow), "gm")
OemRequest = oem
End Function
You are correct that the COUNTIF as written will only match cells where the whole content is "gm". The criteria in the COUNTIF function will also accept wildcards, so to match on cells that contain "gm" do:
.CountIf(Range(2 & "2:" & 2 & LastRow), "*gm*")
Update
As you noted there is also an issue with your Range call. As it is, the expression inside the parens will evaluate to "22:2<LastRow>" (where <LastRow> is the value of the LastRow variable).
The 2's in there should be a variable containing the column name you're interested in. Something like:
Dim col as String
col = "B"
... Range(col & "2:" & col & LastRow) ...
This will evaluate to "B2:B<LastRow>", which is what you want.
Another possibility:
oem = WorksheetFunction.CountIf(Columns(LastColumn).Cells(2).Resize(rowsize:=LastRow - 1), "gm")
This will count cells containing "gm" (use wilcards if needed) in the LAST column of the table, except the one in the first row. (It assumes the table upper left corner is in cell "A1")
Of course you can create a variable if you would like to count any other column:
Dim lngCol as Long
lngCol = ...
oem = WorksheetFunction.CountIf(Columns(lngCol).Cells(2).Resize(rowsize:=LastRow - 1), "gm")
I think in this way
Sub Main()
Application.ScreenUpdating = 0
Dim Count As Double
Range("C1").Activate 'Firs row in the column
Do While ActiveCell.Value <> ""
If InStr(ActiveCell.Value, "MyText") Then
Count = Count + 1
End If
ActiveCell.Offset(1, 0).Activate
Loop
Application.ScreenUpdating = 1
End Sub
This will work, only if the data cell is not empty, if there is an empty space in middle of the worksheet, do this:
Sub Main()
Application.ScreenUpdating = 0
Dim Count As Double
Range("C1").Activate
Do While ActiveCell.Row <> Rows.Count ' This wil evaluate all the rows in the 'C' Column
If InStr(ActiveCell.Value, "MyText") Then
Count = Count + 1
End If
ActiveCell.Offset(1, 0).Activate
Loop
Application.ScreenUpdating = 1
End Sub
Hope it's work for you.