Visual Basic for Applications (VBA) Count Unique Cells - vba

I'm attempting to make a Visual Basic Macro to count unique items in a row without doing the copy and pasting and data remove duplicates.
For some reason I'm having issues with my syntax. When I run the script it outputs with the number of rows.
This is my first time programming in Visual Basic for Applications (VBA).
Private Sub FrequencyCount_Click()
Dim rng As Range
Dim outcell As Range
Dim outnum As Integer
Dim MyArray() As Variant
Dim ArrayLength As Integer
Dim unique As Boolean
Dim i
outnum = 0
ArrayLength = 1
unique = False
Set rng = Application.InputBox("Select a Range to Count Uniques", "Obtain Range Object", Type:=8)
Set outcell = Application.InputBox("Select an Output Box for Uniques", "Obtain Range Object", Type:=8)
For Each B In rng.Rows
If outnum = 0 Then
ReDim MyArray(1)
MyArray(ArrayLength) = B.Value
outnum = outnum + 1
ArrayLength = ArrayLength + 1
Else
i = 0
unique = True
Do Until i < ArrayLength
If MyArray(i) = B.Value Then
unique = False
End If
i = i + 1
Loop
MsgBox unique
If unique = True Then
ReDim Preserve MyArray(0 To ArrayLength + 1)
MyArray(ArrayLength) = B.Value
ArrayLength = ArrayLength + 1
outnum = outnum + 1
End If
End If
Next
End
outcell.Value = outnum
End Sub

It is generally considered bad practice to ReDim Arrays in Loop and not recommended. If you search internet then many discussions like this will come up
ReDim in Loop
You can use built-in functionality to get where you want. Example code which should work for you.
Sub FrequencyCount_Click()
Dim rng As Range
Dim outcell As Range
Set rng = Application.InputBox("Select a Range to Count Uniques", "Obtain Range Object", Type:=8)
Set outcell = Application.InputBox("Select an Output Box for Uniques", "Obtain Range Object", Type:=8)
rng.Copy outcell.Cells(1, 1)
outcell.Resize(rng.Cells.Count, 1).RemoveDuplicates 1, xlNo
End Sub

As #RyanWildry suggests, you can use the Dictionary object for this.
The code to call the procedure will also define the range containing the duplicates and the start range to paste the unique values to:
Sub Test()
'This will take values in the first range and paste the uniques starting at the second range cell.
'NB: Search for With...End With.
With ThisWorkbook.Worksheets("Sheet1")
FrequencyCount .Range("B2:B48"), .Range("D2")
End With
End Sub
This code will then place the values into a dictionary, which also removes any duplicates and then uses a couple of techniques to paste back into rows or columns.
I've added lots of comments and this link may help for further reading on the Dictionary object: https://excelmacromastery.com/vba-dictionary/
Public Sub FrequencyCount(SourceRange As Range, TargetRange As Range)
Dim oDict As Object
Dim rCell As Range
Dim vKey As Variant
Dim vArr As Variant
Dim x As Long
Set oDict = CreateObject("Scripting.Dictionary")
oDict.comparemode = vbTextCompare 'Non-case sensitive. Use vbBinaryCompare to make case sensitive.
'Go through each cell in the source range and copy the value to the dictionary.
For Each rCell In SourceRange
'Change the value in the dictionary referenced by key value.
'If key value doesn't exist create it.
oDict(rCell.Value) = rCell.Value
Next rCell
'Paste in rows.
x = 1
ReDim vArr(1 To oDict.Count)
For Each vKey In oDict.Keys
vArr(x) = oDict(vKey)
x = x + 1
Next vKey
TargetRange.Resize(UBound(vArr)) = WorksheetFunction.Transpose(vArr)
'Paste in columns.
TargetRange.Resize(1, UBound(Application.Transpose(oDict.Keys))) = oDict.Keys
End Sub

The problem is you are setting i = 0 then saying
Do until i < arraylength`
Well if i = 0 then it will always be less than arraylength, this probably should be
Do until i > arraylength
Hope this helps :)

This is an more compact solution, I cobbled together from other solutions.
Sub UniqueCountinSelection()
Dim outcell As Range
Dim itms As Object, c As Range, k, tmp As String
Set rng = Application.InputBox("Select a Range to Count Uniques", "Obtain Range Object", Type:=8)
Set outcell = Application.InputBox("Select an Output Box for Uniques", "Obtain Range Object", Type:=8)
Set itms = CreateObject("scripting.dictionary")
For Each c In rng
tmp = Trim(c.Value) 'removes leading and trailing spaces
If Len(tmp) > 0 Then itms(tmp) = itms(tmp) + 1
Next c
outcell.Value = UBound(itms.Keys) + 1
End Sub

Related

How to bypass 255 character limit of VBA mass replace function?

Sub MultiFindNReplace()
'Update 20140722
Dim Rng As Range
Dim InputRng As Range, ReplaceRng As Range
xTitleId = "KutoolsforExcel"
Set InputRng = Application.Selection
Set InputRng = Application.InputBox("Original Range ", xTitleId, InputRng.Address, Type:=8)
Set ReplaceRng = Application.InputBox("Replace Range :", xTitleId, Type:=8)
Application.ScreenUpdating = False
For Each Rng In ReplaceRng.Columns(1).Cells
    InputRng.Replace what:=Rng.Value, replacement:=Rng.Offset(0, 1).Value
Next
Application.ScreenUpdating = True
End Sub
Source:
Extend Office - How To Find And Replace Multiple Values At Once In Excel?
Data type:
Using the Excel Application.InputBox method
I tried to replace Type:=8 with Type:=2 for text instead of range, but it didn't work. Please help me by pass the 255 character limit.
Example Data:
Google Spreadsheet
I'm not 100% clear on what data you have and what you're trying to do, but I think you will have more success if you use the:
MSDN : Replace Function (VBA)
...instead of:
Office Support: Range.Replace Method (Excel)
The second one is basically a worksheet function, therefore subject to various limits that the first one doesn't have.
Your code should require only minor changes to adapt to the Replace function.
Replace Whole Cell Content
So my idea to replace whole cell contents is:
Read replacements into a dictionary
Read data into an array
Replace in array
Write array back to cells
I choose the array because using arrays is much faster than working with cells. So we have only one slow cell reading and one slow cell writing and working with the array is fast.
Option Explicit
Public Sub MultiReplaceWholeCells()
Const xTitleId As String = "KutoolsforExcel"
Dim InputRange As Range
Set InputRange = Range("A2:F10") 'Application.InputBox("Original Range ", xTitleId, InputRng.Address, Type:=8)
Dim ReplaceRange As Range
Set ReplaceRange = Range("A12:B14") 'Application.InputBox("Original Range ", xTitleId, InputRng.Address, Type:=8)
Dim Replacements As Object
Set Replacements = CreateObject("Scripting.Dictionary")
'read replacements into an array
Dim ReplaceValues As Variant
ReplaceValues = ReplaceRange.Value
'read replacements into a dictionary
Dim iRow As Long
For iRow = 1 To ReplaceRange.Rows.Count
Replacements.Add ReplaceValues(iRow, 1), ReplaceValues(iRow, 2)
Next iRow
'read values into an array
Dim Data As Variant
Data = InputRange.Value
'loop through array data and replace whole data
Dim r As Long, c As Long
For r = 1 To InputRange.Rows.Count
For c = 1 To InputRange.Columns.Count
If Replacements.Exists(Data(r, c)) Then
Data(r, c) = Replacements(Data(r, c))
End If
Next c
Next r
'write data from array back to range
InputRange.Value = Data
End Sub
Replace Part Of Cell Content
For replacing a part of a cell this would be slower:
Option Explicit
Public Sub MultiReplaceWholeCells()
Const xTitleId As String = "KutoolsforExcel"
Dim InputRange As Range
Set InputRange = Range("A2:F10") 'Application.InputBox("Original Range ", xTitleId, InputRng.Address, Type:=8)
Dim ReplaceRange As Range
Set ReplaceRange = Range("A12:B14") 'Application.InputBox("Original Range ", xTitleId, InputRng.Address, Type:=8)
'read replacements into an array
Dim ReplaceValues As Variant
ReplaceValues = ReplaceRange.Value
'read values into an array
Dim Data As Variant
Data = InputRange.Value
'loop through array data and replace PARTS of data
Dim r As Long, c As Long
For r = 1 To InputRange.Rows.Count
For c = 1 To InputRange.Columns.Count
Dim iRow As Long
For iRow = 1 To ReplaceRange.Rows.Count
Data(r, c) = Replace(Data(r, c), ReplaceValues(iRow, 1), ReplaceValues(iRow, 2))
Next iRow
Next c
Next r
'write data from array back to range
InputRange.Value = Data
End Sub
If you only need to replace whole cell contents use the first one which should be faster.
Procedure to Do Both Replacement Types
Or if you need both write a procedure so you can chose if you need to replace xlWhole or xlPart. Even a different output range would be possible.
Option Explicit
Public Sub TestReplace()
Const xTitleId As String = "KutoolsforExcel"
Dim InputRange As Range
Set InputRange = Range("A2:F10") 'Application.InputBox("Original Range ", xTitleId, InputRng.Address, Type:=8)
Dim ReplaceRange As Range
Set ReplaceRange = Range("A12:B14") 'Application.InputBox("Original Range ", xTitleId, InputRng.Address, Type:=8)
MultiReplaceInCells InputRange, ReplaceRange, xlWhole, Range("A20") 'replace whole to output range
MultiReplaceInCells InputRange, ReplaceRange, xlPart, Range("A30") 'replace parts to output range
MultiReplaceInCells InputRange, ReplaceRange, xlWhole 'replace whole in place
End Sub
Public Sub MultiReplaceInCells(InputRange As Range, ReplaceRange As Range, Optional LookAt As XlLookAt = xlWhole, Optional OutputRange As Range)
'read replacements into an array
Dim ReplaceValues As Variant
ReplaceValues = ReplaceRange.Value
'read values into an array
Dim Data As Variant
Data = InputRange.Value
Dim r As Long, c As Long, iRow As Long
If LookAt = xlPart Then
'loop through array data and replace PARTS of data
For r = 1 To InputRange.Rows.Count
For c = 1 To InputRange.Columns.Count
For iRow = 1 To ReplaceRange.Rows.Count
Data(r, c) = Replace(Data(r, c), ReplaceValues(iRow, 1), ReplaceValues(iRow, 2))
Next iRow
Next c
Next r
Else
'read replacements into a dictionary
Dim Replacements As Object
Set Replacements = CreateObject("Scripting.Dictionary")
For iRow = 1 To ReplaceRange.Rows.Count
Replacements.Add ReplaceValues(iRow, 1), ReplaceValues(iRow, 2)
Next iRow
'loop through array data and replace WHOLE data
For r = 1 To InputRange.Rows.Count
For c = 1 To InputRange.Columns.Count
If Replacements.Exists(Data(r, c)) Then
Data(r, c) = Replacements(Data(r, c))
End If
Next c
Next r
End If
'write data from array back to range
If OutputRange Is Nothing Then
InputRange.Value = Data
Else
OutputRange.Resize(InputRange.Rows.Count, InputRange.Columns.Count).Value = Data
End If
End Sub

How can I compare and change cells in Excel

I have two sheets. In both sheets I have a column (B) where each row is a different name:
Sheet 'A' has names in the column, these names are 'leading'.
Sheet 'B' also has names in this column, these names CAN BE outdated.
What I want: A macro which can check the names.
delete the names in Sheet B which are not (anymore) in Sheet A (delete entire affecting row)
Add the names from sheet A which are not (yet) in sheet B in new rows
What I have so far:
Sub DeleteRowsThatDoNotMatch()
Dim rng As Range
Dim Rng1 As Range, Rng2 As Range
Dim thisRow As Long
Dim arr1 As Variant
Dim arr2 As Variant
Dim dic2 As Variant
Dim OutArr As Variant
xTitleId = "Test"
Set Rng1 = Application.Selection
Set Rng1 = Application.InputBox("Welke data moet gewijzigd worden? :", xTitleId, Rng1.Address, Type:=8)
Set Rng2 = Application.InputBox("Selecteer de nieuwe waardes:", xTitleId, Type:=8)
Set Rng1 = Rng1.Columns(1)
Set dic2 = CreateObject("Scripting.Dictionary")
arr2 = Rng2.Value
For i = 1 To UBound(arr2, 1)
xKey = arr2(i, 1)
dic2(xKey) = ""
Next
thisRow = Rng1.Rows.Count
Do While thisRow > 0
If Not dic2.Exists(Rng1.Cells(thisRow, 1).Value) Then
Rng1.Cells(thisRow, 1).EntireRow.Delete
End If
thisRow = thisRow - 1
Loop
End Sub
This works as expected; the problem is adding the names which ARE in Sheet A but not (yet) in Sheet B.

VBA - How to loop

I'm pretty new into this and I got stuck.
If I have a text string in column A (A1:A10) let's say. And I have a macro that looks for a keyword in that string, if it's found I want a word to be entered into column B (B1:B10).
For example A1-Pizza Hut - B1 Pizza, A2 Burger King - B2 Burger.
I got to the point where I can find the keyword, but when I try to do anything that would loop through the range, I always end up getting the same result in B.
Thank you for the answers. I thought I posted my code, but I guess it didn't. Anyways I figured out a way after looking online for the whole day.
Sub one()
Dim food As String, type As String
Dim rng As Range
Dim cel As Range
Set rng = Range("A:A")
For Each cel In rng
food = cel.Value
If InStr(UCase(food), UCase("pizza")) <> 0 Then
type = "Fast food"
Elseif InStr(UCase(food), UCase("burger")) <> 0 Then
type = "Fast food"
Else
type = "Not Fast food"
End If
cel.offset (0, 1).Value = type
Next cel
End Sub
Use a For Each Loop & Split:
Option Explicit
Public Sub Example()
Dim Sht As Worksheet
Dim rng As Range
Set Sht = ActiveWorkbook.Sheets("Sheet2")
For Each rng In Sht.Range("A1", Range("A11").End(xlUp))
rng.Offset(0, 1).Value = Split(rng, " ")(0)
Next
Set Sht = Nothing
Set rng = Nothing
End Sub
This should do what you want:
Sub Find_and_Copy():
Dim keywords() As Variant
keywords = Array("Pizza", "Burger", "Chicken")
Dim endRow As Integer
Dim SearchRng As Range
With Sheets("Sheet1")
endRow = .Cells(Rows.Count, "A").End(xlUp).Row
Set SearchRng = .Range("A1:A" & endRow).Cells
End With
Dim r As Range
Dim firstAddress As String
Dim i As Integer
For i = 0 To UBound(keywords):
With SearchRng
Set r = .Find(keywords(i), LookIn:=xlValues)
If Not r Is Nothing Then
firstAddress = r.Address
Do
Cells(r.Row, "B").Value = keywords(i)
Set r = .FindNext(r)
Loop While Not r Is Nothing And r.Address <> firstAddress
End If
End With
Next
End Sub
It will find all occurrences of each entry in the 'keywords' array that matches cells of column "A" - and of course, set column "B" to that keyword.
Note that say you have an entry like "ala Burger Chicken" it'll put 'Chicken' (which I added to 'keywords' just to keep in the spirit of things) in column B for that row because that's the last thing it did a search for - hence overwriting the previous 'Burger' entry that was in that cell.

Find column with name "x" and count word occurrences in that column

I have a worksheet of data with headers. I am trying, in VBA, to find the column with the header "type" and then in that column count the amount of times string "x" appears, i.e count the number of times "add" appears in column with header "type".
I know you can create a scripting dictionary to count the amount of times each word appears, I am having issues with searching through the headers to find the column "type".
My code so far looks at every cell in the sheet however i just want to limit it to the column "type":
Dim shtSheet1 As String
Dim dict As Object
Dim mycell As Range
shtSheet1 = "Test"
Set dict = CreateObject("Scripting.Dictionary")
dict.Add "Add", 0
dict.Add "Delete", 0
dict.Add "Update", 0
For Each mycell In ActiveWorkbook.Worksheets(shtSheet1).UsedRange
If dict.Exists(ActiveWorkbook.Worksheets(shtSheet1).Cells(mycell.Row, mycell.Column).Value) Then
dict(ActiveWorkbook.Worksheets(shtSheet1).Cells(mycell.Row, mycell.Column).Value) = dict(ActiveWorkbook.Worksheets(shtSheet1).Cells(mycell.Row, mycell.Column).Value) + 1
End If
Next
Thanks for any help!
if I understood correctly, then you can use this:
Sub test()
Dim Dict As Object: Set Dict = CreateObject("Scripting.Dictionary")
Dim shtSheet1 As Worksheet: Set shtSheet1 = Sheets("Test")
Dim mycell As Range, n&, z&
Dim Fx As Object, Key As Variant
Set Fx = WorksheetFunction
Dict.CompareMode = vbTextCompare
With shtSheet1
n = .Rows(1).Find("Type").Column
z = .Cells(.Rows.Count, n).End(xlUp).Row
For Each mycell In .Range(.Cells(2, n), Cells(z, n))
If Not Dict.Exists(Fx.Trim(mycell)) Then Dict.Add Fx.Trim(mycell), 0
Next
For Each mycell In .Range(.Cells(2, n), Cells(z, n))
If Dict.Exists(Fx.Trim(mycell)) Then
Dict(Fx.Trim(mycell)) = CLng(Dict(Fx.Trim(mycell))) + 1
End If
Next
End With
For Each Key In Dict
Debug.Print Key, Dict(Key)
Next Key
End Sub
output with data example is below:
update
variant using worksheetfunction.countif with dictionary
Sub test2()
Dim Dict As Object: Set Dict = CreateObject("Scripting.Dictionary")
Dim shtSheet1 As Worksheet: Set shtSheet1 = Sheets("Test")
Dim mycell As Range, n&, Data As Range
Dim Fx As Object, Key As Variant
Set Fx = WorksheetFunction
Dict.CompareMode = vbTextCompare
With shtSheet1
n = .Rows(1).Find("Type").Column
Set Data = .Range(.Cells(2, n), Cells(.Cells(.Rows.Count, n).End(xlUp).Row, n))
For Each mycell In Data
If Not Dict.Exists(Fx.Trim(mycell)) Then Dict.Add Fx.Trim(mycell), Fx.CountIf(Data, "*" & Fx.Trim(mycell) & "*")
Next
End With
For Each Key In Dict
Debug.Print Key, Dict(Key)
Next Key
End Sub
I would use this code block to iterate through your column headers. Further, I would use the Worksheet Function COUNTIF so you only have to iterate through the column headers rather than every cell in your range.
Dim shtSheet1 As String
Dim dict As Object
Dim myCell As Range
Dim firstHeaderCell As Range
shtSheet1 = "Test"
Set dict = CreateObject("Scripting.Dictionary")
Set firstHeaderCell = Range("A1")
'Iterate across column headers only
For Each myCell In Range(firstHeaderCell, _
Cells(firstHeaderCell.Row, _
firstHeaderCell.Column + firstHeaderCell.CurrentRegion.Columns.Count - 1))
'Add it to the dictionary if it isn't there (this future proofs the code)
If Not dict.Exists(myCell.Value) Then
dict.Add myCell.Value, 0
End If
'Use worksheet function COUNTIF to count number of instances of column header value in the column
dict(myCell.Value) = WorksheetFunction.CountIf(Range(Cells(firstHeaderCell.Row + 1, myCell.Column), _
Cells(firstHeaderCell.CurrentRegion.Rows.Count - 1, firstHeaderCell.Column)), _
myCell.Value)
Next

VBA Object Required Error

I've created a form to reformat a report that I receive and I'm having an issue with automating part of it. Everything works until I define and set the last variable codelength which I want to set as the length of a cell (first column, second row) in a defined range. I receive run time error 424, "Object Required". I appreciate any help!!
Here is the code:
Private Sub CommandButton1_Click()
Application.ScreenUpdating = False
Dim rg As Range
Dim rgg As Range
Dim Addr1 As String
Dim Addr2 As String
'Get the address, or reference, from the RefEdit control.
Addr1 = RefEdit1.Value
Addr2 = RefEdit2.Value
'Set the SelRange Range object to the range specified in the
'RefEdit control.
Set rg = Range(Addr1)
Set rgg = Range(Addr2)
ActiveWorkbook.Names.Add Name:="codes", RefersTo:=rgg
'Infill
'Copies the value from the row above into blank cells.
Dim cel As Range, col As Range
Set rg = Range(Addr1).Columns(1).Resize(, 2)
On Error Resume Next
For Each col In rg.Columns
Set rgg = Nothing
Set rgg = col.SpecialCells(xlCellTypeBlanks)
If Not rgg Is Nothing Then
rgg.FormulaR1C1 = "=R[-1]C" 'Blank cells set equal to value from row above
rgg.Formula = rgg.Value 'Optional: Replace the formulas with the values returned by the formulas
End If
Next
Set rgg = rg.Offset(1, 0).Resize(rg.Rows.Count - 1, rg.Columns.Count)
For Each cel In rgg.Cells
If cel = "" Then cel.Value = cel.Offset(-1, 0).Value
Next
On Error GoTo 0
'ColCDeleter
Dim i As Long, n As Long
Set rg = Intersect(ActiveSheet.UsedRange, Range(Addr1).Columns(3))
n = rg.Rows.Count
For i = n To 1 Step -1
If rg.Cells(i, 1) = "" Then rg.Cells(i, 1).EntireRow.Delete
Next
'insert corresponding values
Dim codelength As Integer
codelength = Len(codes.Cells(2, 1).Value)
rg.Columns(2).EntireColumn.Insert
rg.Columns(2).EntireColumn.Insert
rg.Columns(2).EntireColumn.Insert
rg.Columns(2).EntireColumn.Insert
If codelength = 6 Then
rg.Columns(2).FormulaR1C1 = "=VLOOKUP((MID(RC1,9,9)),codes,2,FALSE)"
rg.Columns(3).FormulaR1C1 = "=VLOOKUP((MID(RC1,9,9)),codes,3,FALSE)"
Else
rg.Columns(2).FormulaR1C1 = "=VLOOKUP((MID(RC1,8,9)),codes,2,FALSE)"
rg.Columns(3).FormulaR1C1 = "=VLOOKUP((MID(RC1,8,9)),codes,3,FALSE)"
End If
rg.Cells(1, 2).Value = "Plan"
rg.Cells(1, 3).Value = "Status"
'Unload the userform.
Unload Me
End Sub
When you first name a range using the following syntax
Dim rng as Range
Set rng = Range("A1:A10")
ActiveWorkbook.Names.Add Name:="codes", RefersTo:=rng
Then this becomes just a name - it's not a stand alone object. So the error you are getting tells you exactly what is happening -> Object required.
To refer to the named Range you wrap it in double quotes and stick it as the parameter for the Range object. Therefore, Range("codes") creates a Range object referring to the rng Range.
An alternative, omitting the name would be to use the rng Range object simply replacing the Range("codes"). with rng.