Identifying Duplicates in Excel - excel

I'm trying to identify duplicate cells in a macro. I'm trying to use macros so I can extract the entire row once the duplicate is identified.
I used this code:
Sub MarkDuplicates()
Dim iWarnColor As Integer
Dim rng As Range
Dim rngCell As Variant
Set rng = Sheets("AllAccounts (12-05-2017)").Range("D1:D1613")
iWarnColor = xlThemeColorAccentz
For Each rngCell In rng.Cells
vVal = rngCell.Text
If (WorksheetFunction.CountIf(rng, vVal) = 1) Then
rngCell.Interior.Pattern = xlNone
Else
rngCell.Interior.ColorIndex = iWarnColor
End If
Next rngCell
End Sub
but it only identified empty cells. At the moment I'm trying to only identify duplicate text and I'll extract them later.
Can you please help me do that?

You don't need to put rng.Cells - the .Cells is implied - just use rng
(^ This is semantics - do whatever you want)
Instead of checking rngCell.Text - try rngCell.Value.
.Text is incredibly slow.
^ Really, based on this, should probably use .Value2 instead of .Value for maximum speeeeeeed!
Of course, if we are that concerned, we would use a variant array, but let's keep it simple.
Also, idk why you use xlThemeColorAccentz and ColorIndex
This may work, but it doesn't work for me - I would just use RGB
You're doing a CountIf on the range which is sort of meh.
As for checking duplicates,
I would recommend using a dictionary for this purpose.
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
Your code becomes:
Sub MarkDuplicates()
Dim iWarnColor As Long
Dim rng As Range
Dim rngCell As Variant
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
Set rng = Sheets("AllAccounts (12-05-2017)").Range("D1:D1613")
rng.Interior.ColorIndex = xlNone 'Optionally clear all coloring
iWarnColor = RGB(230, 180, 180) 'Red
For Each rngCell In rng
If rngCell.Value <> "" Then 'Ignore blank cells
If Not dict.Exists(rngCell.Value) Then
dict.Add rngCell.Value, rngCell.Row 'Store the row if we want
Else
rngCell.Interior.Color = iWarnColor
'Optionally color the original cell:
'Sheets("AllAccounts (12-05-2017)").Cells(dict(rngCell.Value), "D").Interior.Color = RGB(180, 230, 180)
End If
End If
Next rngCell
End Sub
Results with optional coloring:
Edit (Not Using Dictionary):
So, you're using a mac - oh wellz.
I didn't mention it before, but you can use conditional formatting to solve this.
Anyway, let's just use a collection.
A collection works a lot like a dictionary, but we typically have to loop through it to determine if a particular Key/Value pair exists.
We can cheat this by trying to get a value for a key that doesn't exist and catch the error - I added a function to simplify this process.
Sub MarkDuplicates()
Dim iWarnColor As Long
Dim rng As Range
Dim rngCell As Variant
Dim Col As New Collection
Set rng = Sheets("AllAccounts (12-05-2017)").Range("D1:D1613")
rng.Interior.ColorIndex = xlNone
iWarnColor = RGB(230, 180, 180)
For Each rngCell In rng
If rngCell.Value <> "" Then 'Ignore blank cells
If Not IsInCollection(Col, rngCell.Value2) Then
Col.Add rngCell.Row, Key:=rngCell.Value2
Else
rngCell.Interior.Color = iWarnColor
'Optionally color the original cell
Sheets("AllAccounts (12-05-2017)").Cells(Col(rngCell.Value2), "D").Interior.Color = RGB(180, 230, 180)
End If
End If
Next rngCell
End Sub
Function IsInCollection(Col As Collection, Val As Variant) As Boolean
On Error Resume Next
Debug.Print (Col(Val))
IsInCollection = (Err.Number = 0)
On Error GoTo 0
End Function
New Results (The Same):

I suppose there are several ways to do this. Here is one.
Option Explicit
Sub FilterAndCopy()
Dim wstSource As Worksheet, _
wstOutput As Worksheet
Dim rngMyData As Range, _
helperRng As Range
Set wstSource = Worksheets("Sheet1")
Set wstOutput = Worksheets("Sheet2")
Application.ScreenUpdating = False
With wstSource
Set rngMyData = .Range("A1:XF" & .Range("A" & .Rows.Count).End(xlUp).Row)
End With
Set helperRng = rngMyData.Offset(, rngMyData.Columns.Count + 1).Resize(, 1)
With helperRng
.FormulaR1C1 = "=if(countif(C1,RC1)>1,"""",1)"
.Value = .Value
.SpecialCells(xlCellTypeBlanks).EntireRow.Copy Destination:=wstOutput.Cells(1, 1)
.ClearContents
End With
Application.ScreenUpdating = True
End Sub

Related

Select random cell in range

I'm trying to perform an action in VBA on a range of cells. I would like the selection of the cells to be random not in the order of how the range is setup.
Sub Solver_Step_Evo()
Set Rng = GetRange(ThisWorkbook.ActiveSheet.Range("Variable_Range"))
For Each i In Rng
'perform an action on I where I is randomly selected.
Next i
End Sub
My preference is it randomizes the order not just randomly select a cell where a cell can be picked more than once.
Thanks in advance.
Here's a possible solution. I add all of the cells in the relevant range to a collection. Then, I navigate the collection using random indexes. Once an index has been visited, I remove it from the collection and repeat the process.
Does this work for you?
Edit: No need to call the c.Count method for each iteration. We can manage this ourselves ourselves. It would likely be a bit more efficient than calling the object's method.
Sub SuperTester()
Dim c As Collection
Dim rng As Range
Dim cel As Range
Dim idx As Long
Dim remainingCount As Long
Set rng = Range("A2:A17")
Set c = New Collection
For Each cel In rng
c.Add cel
Next cel
remainingCount = c.Count
While remainingCount > 0
idx = WorksheetFunction.RandBetween(1, c.Count)
Debug.Print c.Item(idx).Address
c.Remove idx
remainingCount = remainingCount - 1
Wend
End Sub
You can use WorksheetFunction.RandBetween to get random number between 2 numbers. The numbers will not be unique though. If you want unique then you will have to use a slightly different approach.
Option Explicit
Sub Solver_Step_Evo()
Dim Rng As Range
Set Rng = GetRange(ThisWorkbook.ActiveSheet.Range("Variable_Range"))
Dim lowerBound As Long: lowerBound = 1
Dim UpperBound As Long: UpperBound = Rng.Cells.Count
Dim randomI As Long
Dim i As Long
For i = lowerBound To UpperBound
randomI = Application.WorksheetFunction.RandBetween(lowerBound, UpperBound)
Debug.Print randomI
Next i
End Sub
Try the next function, please:
Function RndCell(rng As Range) As Range
Dim rndRow As Long, rndCol As Long
rndRow = WorksheetFunction.RandBetween(1, rng.rows.count)
rndCol = WorksheetFunction.RandBetween(1, rng.Columns.count)
Set RndCell = rng.cells(rndRow, rndCol)
End Function
It can be tested using the next simple sub:
Sub testSelectRandomCell()
Dim rng As Range
Set rng = Range("A2:D10")
RndCell(rng).Select
End Sub
Edited:
If the random selected cells should not repeat, the function can be adapted in the next way (using a Static array to keep the already selected cells):
Function RndCellOnce(rng As Range, Optional boolClear As Boolean = False) As Range
Dim rndRow As Long, rndCol As Long, k As Long, El, arr1
Static arr
If boolClear And IsArray(arr) Then Erase arr
DoItAgain:
rndRow = WorksheetFunction.RandBetween(1, rng.rows.count)
rndCol = WorksheetFunction.RandBetween(1, rng.Columns.count)
If IsArray(arr) Then
If UBound(arr) = rng.cells.count - 1 Then
rng.Interior.Color = xlNone
ReDim arr(0): GoTo Over
End If
For Each El In arr
If El <> "" Then
arr1 = Split(El, "|")
If CLng(arr1(0)) = rndRow And CLng(arr1(1)) = rndCol Then GoTo DoItAgain
End If
Next El
ReDim Preserve arr(UBound(arr) + 1)
Else
ReDim arr(0)
End If
Over:
arr(UBound(arr)) = rndRow & "|" & rndCol
Set RndCellOnce = rng.cells(rndRow, rndCol)
End Function
It can be tested with the next Sub. In order to visually check it, each selected cell will get a yellow interior color. When all the range cells will be selected (one by one), the static array will be erased and the interior color will be cleaned:
Sub testSelectRandomCell()
Dim rng As Range
Set rng = Range("A2:D10")
With RndCellOnce(rng)
.Interior.Color = vbYellow
.Select
End With
End Sub

Sum / subtotal visible cells based on cell or font color

I have a table that has a number of slicers attached. The table has conditional formatting that will change the font of a column on numbers to red or green depending on certain conditions.
I have seen a number of functions that count cells based on back ground color, but what I want to do is;
subtotal only the visible cells in the range that are the same font colour as cell "R2"
The sum is to be returned in cell "R2"
Range is "M9:M200"
I then need to have this run by slicer change as updating slicer will change all the figures.
I have found some code on here which I have tried to adapt and failed. Any help appreciated
I have been trying to convert the conditional formatting (red cells) to normal red with the below code, but thats not working either.
Sub GetColorSum2()
Dim FCELL As Range
For Each rCell In Range("Y1:Y7").Cells
If rCell.DisplayFormat.Interior.ColorIndex = 24 Then
rCell.Font.ColorIndex = 3
End If
Next
End Sub
MAIN CODE
Function GetColorsum(sumRange As Range, SumColor As Range, Optional VolatileParameter As Variant)
Dim ColVal As Long, rCell As Range
Dim Totalsum As Long
ColVal = SumColor.Font.ColorIndex
For Each rCell In sumRange.Cells
If rCell.Font.ColorIndex = ColVal Then
If rCell.EntireRow.Hidden = False Then
If rCell.EntireColumn.Hidden = False Then
Totalsum = Totalsum + 1
End If
End If
End If
Next rCell
GetColorsum = Totalsum
End Function
I have also tried
Function Sumclr(rColor As Range, rRange As Range)
Dim rCell As Range
Dim lCol As Long
Dim vResult
lCol = rColor.Interior.ColorIndex
For Each rCell In rRange
If rCell.Interior.ColorIndex = lCol Then
vResult = WorksheetFunction.Sum(rCell, vResult)
End If
Next rCell
Sumclr = vResult
End Function
OK, so I managed to get a solution. Basically I did three things,
Replaced the function with a button.
Replaced the conditional formatting with VBA formatting.
summed the numbers using an IF hidden = false code
BUTTON
Sub CreateButton0()
Dim btn As Button
Application.ScreenUpdating = False
Dim t As Range
Set t = ActiveSheet.Range(Cells(1, 18), Cells(1, 18))
Set btn = ActiveSheet.Buttons.Add(t.Left, t.Top, t.Width, t.Height)
With btn
.OnAction = "SUMCOLOUR0"
.Caption = "POPULATE"
.Name = "Btn"
.Font.Size = 8
End With
Application.ScreenUpdating = True
End Sub
FORMATTING
Sub HIGHLIGHT()
Dim FCELL As Range
For Each FCELL In Range("M9:M1000").Cells
If FCELL.Offset(0, 6) = "YES" Then
FCELL.Font.ColorIndex = 3
FCELL.Font.Bold = True
End If
If FCELL.Offset(0, -4) = "SR" Then
FCELL.Font.ColorIndex = 10
FCELL.Font.Bold = True
End If
Next FCELL
End Sub
SUMMING
Sub SUMCOLOUR0()
Dim RCELL As Range
Dim SCELL As Range
Dim RRESULT As Double
Dim SRESULT As Double
''''SUM OTE (RED)''''
For Each RCELL In Range("M9:M1000").Cells
If RCELL.Font.ColorIndex = 3 Then
If RCELL.EntireRow.Hidden = False And RCELL.EntireColumn.Hidden = False Then
RRESULT = WorksheetFunction.SUM(RCELL, RRESULT)
End If
End If
Next RCELL
Cells(2, ActiveSheet.Shapes("BTN").TopLeftCell.Column).Value = RRESULT
''''SUM SW (GREEN)''''
For Each SCELL In Range("M9:M1000").Cells
If SCELL.Font.ColorIndex = 10 Then
If SCELL.EntireRow.Hidden = False And SCELL.EntireColumn.Hidden = False Then
SRESULT = WorksheetFunction.SUM(SCELL, SRESULT)
End If
End If
Next SCELL
Cells(3, ActiveSheet.Shapes("BTN").TopLeftCell.Column).Value = SRESULT
End Sub

Code seems to run forever and Error: block variable not set (VBA)

I am completely new to VBA so please bear with me.
I am trying to write a sub-procedure that will loop through each row in a certain column and compare to another sheet's criteria. if it contains "x", for example, then the value will be returned. However, when I try running the code, the codes run forever and causes the computer to hang.
Here's the code that I have written so far. It keeps prompting an error: Object variable and with block variable not set. PS: I have obtained errors when using 'Application.WorksheetFunction.Index' and when reading other threads, it was suggested to delete 'WorksheetFunction'. I'm not sure if that causes the problem and I would also like to clarify the rationale behind removing the words 'WorksheetFunction'
Thank you so much in advance!
Sub sub_inputData()
Dim ws As Worksheet: Set ws = ActiveSheet
Dim lastrow as range
lastrow = ws.Cells (ws.Rows.Count, 17).End (xlUp).row
Dim rng As Range
Set rng = ws.Range("Q4:Q" & lastrow)
Dim rngCell As Range
On Error Resume Next
For Each rngCell In rng
If rngCell.Offset(0, -13) = "x" Then
rngCell = Application.Index(Sheets("Data").Range _
("D805:D813"), Application.Match(rngCell.Offset(0, -15), Sheets("Data").Range _
("D805:D813"), 1))
ElseIf rngCell.Offset(0, -13) = "y" Then
rngCell = Application.Index(Sheets("Data").Range _
("D27:D34"), Application.Match(rngCell.Offset(0, -15), Sheets("Data").Range _
("D27:D34"), 1))
ElseIf rngCell.Offset(0, -13) = "z" Then
rngCell = Application.Index(Sheets("Data").Range _
("D718:D726"), Application.Match(rngCell.Offset(0, -15), Sheets("Data").Range _
("D718:D726"), 1))
Else: rngCell = vbNullString
End If
Next rngCell
Call sub_code2
Call sub_code3
Set rngCell = Nothing
Set rng = Nothing
End Sub
Couple issue with your code that has been modified here.
1) Dim lastrow As Long, not Range
2) Else: is not necessary, just use Else
3) Set rngCell = Nothing & Set rng = Nothing is not necessary. See this link for explanation
4) Since you are only checking the value of 1 cell, you can use Select Case for a moderately cleaner code.
5) On Error Resume Next is no good for de-bugging code. You want to see the errors so you can handle them. I recommend looking up the do's and dont's of that bit of code.
Sub sub_inputData()
Dim ws As Worksheet: Set ws = ActiveSheet
Dim lastrow As Long: lastrow = ws.Range("Q" & ws.Rows.Count).End(xlUp).Row
Dim rng As Range: Set rng = ws.Range("Q4:Q" & lastrow)
Dim rngCell As Range
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
For Each rngCell In rng
Select Case rngCell.Offset(0, -13)
Case "x"
rngCell = Application.Index(Sheets("Data").Range _
("D805:D813"), Application.Match(rngCell.Offset(0, -15), Sheets("Data").Range _
("D805:D813"), 1))
Case "y"
rngCell = Application.Index(Sheets("Data").Range _
("D27:D34"), Application.Match(rngCell.Offset(0, -15), Sheets("Data").Range _
("D27:D34"), 1))
Case "z"
rngCell = Application.Index(Sheets("Data").Range _
("D718:D726"), Application.Match(rngCell.Offset(0, -15), Sheets("Data").Range _
("D718:D726"), 1))
Case Else
rngCell = ""
End Select
Next rngCell
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
Call sub_code2
Call sub_code3
End Sub
another possibility is using Switch() function:
Sub sub_inputData()
Dim rngCell As Range, rangeToSearch As Range
Dim val As Variant
With ActiveSheet ' reference data sheet (better: With Worksheets("MyDataSheetName"))
For Each rngCell In .Range("Q4", .Cells(.Rows.Count, "Q").End(xlUp)) ' loop throughreferenced sheet column Q cells from row 4 down to last not empty one
val = rngCell.Offset(, -13).Value2 ' store column D current cell row value
Set rangeToSearch = Sheets("Data").Range(Switch(val = "x", "D805:D813", val = "y", "D27:D34", val = "z", "D718:D726", True, "A1")) ' set range to search into with respect to stored value. set it to "A1" to signal no search is needed
If rangeToSearch.Address <> "$A$1" Then ' if search is needed
rngCell.Value = Application.Index(rangeToSearch, Application.Match(rngCell.Offset(, -15).Value2, rangeToSearch, 1)) 'do the lookup
Else
rngCell.ClearContents ' clear current cell
End If
Next
End With
sub_code2 ' no need for 'Call' keyword
sub_code3 ' no need for 'Call' keyword
End Sub
It looks like you are effectively picking a lookup range based on the value in column D, and then doing a lookup against that range based on the value in column B.
If so, you can do this entirely with formulas, which will be more efficient because it will only run on particular cells when needed (i.e. only when their inputs change).
Here's an example, using Tables and Table Notation. Tables are perfect for this, as you never have to amend your formulas to handle new data.
The formula in C2 is =VLOOKUP([#ID],CHOOSE(VLOOKUP([#Condition],Conditions,2,FALSE),X,Y,Z),2,FALSE)
That formula uses the 'Conditions' Table in E1:F3 to work out which of the other tables to do the lookup on. I've named those other tables X, Y, and Z.

Deleting rows in a foreach

I'm trying to update a sheet with an import of a .CSV file..
I can read and update all the information. After the update I want to remove some data. All the rows with D empty must be deleted (whole row).
For that, I have a foreach that checks D3:D486 (is last row).
After running the macro, there are some rows deleted, but not all the rows.
Dim rCell As Range
Dim rRng As Range
Debug.Print CStr(LastRow)
Set rRng = Worksheets("sheet1").Range("D3:D" + CStr(LastRow))
For Each rCell In rRng.Cells
If Not IsEmpty(rCell) Then
Debug.Print rCell.Row
Else
Debug.Print "Empty"
Worksheets("sheet1").Rows(rCell.Row).Delete
End If
Next rCell
I guess there is a problem with the for-each.. By example, If he delete row 100, the next time he goes to row 101.. But thats previous row 102..
I can save the cells maybe in an array, but then it would be the same.
Except if I go the other way (from bottom to top). How can I solve this?
i think you've answered your own question: from bottom to top...
and you can try range.EntireRow.Delete method too, something like below
Dim rCell As Range
Dim lastRow, i
lastRow = 1000
For i = lastRow To 1 Step -1
' if condition met
Worksheets("Sheet1").Range("D:" + i).EntireRow.Delete
Next
I would do it like this:
Dim i As Integer
Dim rRng As Range
Debug.Print CStr(LastRow)
Set rRng = Worksheets("sheet1").Range("D3:D" + CStr(LastRow))
For i = 1 To rRng.Cells.Count
If Not IsEmpty(Worksheets("Sheet1").Range("D:" + i).Value) Then
Debug.Print rCell.Row
Else
Debug.Print "Empty"
Worksheets("Sheet1").Range("D:" + i).EntireRow.Delete
i = i - 1
End If
Next
Rex answer is correct, if you want to get cute you can also do it this way:
Sub DeleteRowsWithCriteria()
Dim rng As Range, rngCell As Range, rngDelete As Range
Set rng = Worksheets("sheet1").UsedRange.Columns("D:D").Cells 'Watch out here, if columns A-C are not all used, this doesn't work
For Each rngCell In rng
If rngCell.Value = "" Then
If rngDelete Is Nothing Then
Set rngDelete = rngCell
Else
Set rngDelete = Union(rngDelete, rngCell)
End If
End If
Next rngCell
rngDelete.EntireRow.Delete xlShiftUp
End Sub

Select Random Cell In A Range Only If It Has A Value - Excel

So here is the following VBA code I'm currently using. It works perfectly but I need to expand the range to check additional cells but some of those cells could contain empty cells and I don't want to select those.
Is there a way to bypass those empty cells?
Dim RNG1 As Range
Set RNG1 = Range("H1:H30")
Dim randomCell1 As Long
randomCell1 = Int(Rnd * RNG1.Cells.Count) + 1
With RNG1.Cells(randomCell1)
.Select
'will do something else here, like copy the cell, fill the cell with a color, etc
End With
This should pick only non-empty cells:
Sub marine()
Dim RNG1 As Range, r As Range, c As Collection
Set c = New Collection
Set RNG1 = Range("H1:H30")
For Each r In RNG1
If r.Value <> "" Then
c.Add r
End If
Next r
Dim N As Long
N = Application.WorksheetFunction.RandBetween(1, c.Count)
Set rselect = c.Item(N)
rselect.Select
End Sub
NOTE:
This is an example of a general technique. To make a random pick from a subset of a range, collect the subset and pick from the Collection.
If the values in column H were XlConstants then something like this using SpecialCells
Sub Option_B()
Dim rng1 As Range
Dim rng2 As Range
Dim lngCel As Long
On Error Resume Next
Set rng1 = Range("H1:H30").SpecialCells(xlCellTypeConstants)
On Error GoTo 0
If rng1 Is Nothing Then Exit Sub
Dim randomCell1 As Long
randomCell1 = Int(Rnd * rng1.Cells.Count) + 1
For Each rng2 In rng1.Cells
'kludgy as there will be multiple areas in a SpecialFCells range with blank cells
lngCel = lngCel + 1
If lngCel = randomCell1 Then
Application.Goto rng2
Exit For
End If
Next
End Sub
A bit too late but no harm in posting :)
Sub test()
Dim rng As Range, cel As Range
Dim NErng
Dim i As Integer
Set rng = Range("A1:A15")
For Each cel In rng
If Len(cel) <> 0 Then
If IsArray(NErng) Then
ReDim Preserve NErng(UBound(NErng) + 1)
NErng(UBound(NErng)) = cel.Address
ElseIf IsEmpty(NErng) Then
NErng = cel.Address
Else
NErng = Array(NErng, cel.Address)
End If
End If
Next
i = Int((UBound(NErng) - LBound(NErng) + 1) * Rnd + LBound(NErng))
Debug.Print Range(NErng(i)).Address
End Sub
EDIT -- #brettdj is right. This is adjusted to better answer the "skip these cells" question.
Try this out:
DangThisCellIsBlank:
RandomCell = Int(Rnd * RNG1.Cells.Count) + 1
With RNG1.Cells(RandomCell)
If .Value <> "" Then
'do stuff
Else
'go back and pick another cell
GoTo DangThisCellIsBlank
End If
End With
Try with IsEmpty(RNG1.Cells(randomCell1))
Dim RNG1 As Range
Set RNG1 = Range("H1:H30")
Dim randomCell1 As Long
randomCell1 = Int(Rnd * RNG1.Cells.Count) + 1
'Keep Looping until you find a non empty cell
Do While IsEmpty(RNG1.Cells(randomCell1))
randomCell1 = Int(Rnd * RNG1.Cells.Count) + 1
Loop
'================================================
With RNG1.Cells(randomCell1)
.Select
'will do something else here, like copy the cell, fill the cell with a color, etc
End With

Resources