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
I know how to make two functions on each column (in this case TRIM and STRCONV to ProperCase
Dim arrData() As Variant
Dim arrReturnData() As Variant
Dim rng As Excel.Range
Dim lRows As Long
Dim lCols As Long
Dim i As Long, j As Long
Range("H2", Range("H2").End(xlDown)).Select
lRows = Selection.Rows.Count
lCols = Selection.Columns.Count
ReDim arrData(1 To lRows, 1 To lCols)
ReDim arrReturnData(1 To lRows, 1 To lCols)
Set rng = Selection
arrData = rng.Value
For j = 1 To lCols
For i = 1 To lRow
arrReturnData(i, j) = StrConv(Trim(arrData(i, j)), vbProperCase)
Next i
Next j
rng.Value = arrReturnData
Set rng = Nothing
Currently I'm trying to figure out how to add one more FOR which where I could gather more than one selection ranges for example:
Set myAnotherArray(0) = Range("H2", Range("H2").End(xlDown)).Select
Set myAnotherArray(1) = Range("J2", Range("J2").End(xlDown)).Select
For k = 1 To myAnotherArray.lenght
Because I'm copying and pasting whole script to make aciton on three columns. Tried already:
Dim Rng As Range
Dim Area As Range
Set Rng = Range("Range("H2", Range("H2").End(xlDown)).Select,Range("J2", Range("J2").End(xlDown)).Select")
For Each Area In Rng.Areas
Area.Font.Bold = True
Next Area
Even tried to Union range but I failed. Any sugesstions?
And as always... Thank you for your time!
I found a way you could use to perform work on those ranges, refer to the code below:
Sub DoSomethingWithRanges()
Dim m_Worksheet As Excel.Worksheet
Dim m_Columns() As Variant
Set m_Worksheet = ActiveSheet
' fill all your columns in here
m_Columns = Array(2, 3, 4)
For Each m_Column In m_Columns
' the area being used ranges from the second until the last row of your column
With m_Worksheet.Range(m_Worksheet.Cells(2, m_Column), m_Worksheet.Cells(m_Worksheet.UsedRange.Rows.Count, m_Column))
' do things with range
.Font.Bold = True
End With
Next m_Column
End Sub
In the variant array m_Columns you can add all the columns you want. Only downside is that in my example you have to use numbers to specify columns instead of "H". However, you don't have to worry about the row-indexes, since the area automatically ranges from the second to the last used row.
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
I had some great help to get this search tool working in excel but I was wondering if there is room for speed improvement. I did some research and with what little I understand about VB for i = LBOUND(array) To UBOUND(array) seems most optimal. Would 'For Each' be faster? I am wondering if there is a way to isolate the records currently in the worksheet, or if it is already doing this with L/UBOUND? If it is, is there a way to do 'ignore special characters' similar to SQL? After adding screenupdating and calculation, I was able to shave about 10 seconds off of the total run time. And further I was using FormulaR1C1 for my search before this new loop and it would limit the amount of columns to search while being super fast.
Range("W2:W" & LastRow).FormulaR1C1 = _
"=IF(ISERR(SEARCH(R1C23,RC[-22]&RC[-21]&RC[-20]&RC[-19]&RC[-18]&RC[-17]&RC[-16]&RC[-15]&RC[-15]&RC[-14]&RC[-13]&RC[-12]&RC[-11]&RC[-10]&RC[-9]&RC[-8]&RC[-7]&RC[-6]&RC[-5]&RC[-4]&RC[-3]&RC[-2]&RC[-1])),0,1)"
If WorksheetFunction.CountIf(Columns(23), 1) = 0 Then
Columns(23).Delete
Any help or recommendations are greatly appreciated.
Sub FindFeature()
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Dim shResults As Worksheet
Dim vaData As Variant
Dim i As Long, j As Long
Dim sSearchTerm As String
Dim sData As String
Dim rNext As Range
Dim v As Variant
Dim vaDataCopy As Variant
Dim uRange As Range
Dim findRange As Range
Dim nxtRange As Range
Dim ws As Range
'Put all the data into an array
vaData = ActiveSheet.UsedRange.Value
'Get the search term
sSearchTerm = Application.InputBox("What are you looking for?")
'Define and clear the results sheet
Set shResults = ActiveWorkbook.Worksheets("Results")
shResults.Range("A3").Resize(shResults.UsedRange.Rows.Count, 1).EntireRow.Delete
Set uRange = ActiveSheet.UsedRange
vaData = uRange.Value
vaDataCopy = vaData
For Each v In vaDataCopy
v = Anglicize(v)
Next
Application.WorksheetFunction.Transpose (vaDataCopy)
ActiveSheet.UsedRange.Value = vaDataCopy
'Loop through the data
Set ws = Cells.Find(What:=uRange, After:="ActiveCell", LookIn:=xlValues, LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False, SearchFormat:=False)
If Not ws Is Nothing Then
Set findRange = ws
Do
Set nxtRange = Cells.FindNext(After:=ws)
Set findRange = nxtRange
Loop Until ws.Address = findRange.Address
ActiveSheet.UsedRange.Value = vaData
'Write the row to the next available row on Results
Set rNext = shResults.Cells(shResults.Rows.Count, 1).End(xlUp).Offset(1, 0)
rNext.Resize(1, uRange(vaData, 2)).Value = Application.Index(vaData, i, 0)
'Stop looking in that row after one match
End If
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
End Sub
Ultimately, the execution speed here is severely hampered by the apparent requirement to operate on every cell in the range, and because you're asking about performance, I suspect this range may contain many thousands of cells. There are two things I can think of:
1. Save your results in an array and write to the Results worksheet in one statement
Try replacing this:
'Write the row to the next available row on Results
Set rNext = shResults.Cells(shResults.Rows.Count, 1).End(xlUp).Offset(1, 0)
rNext.Resize(1, UBound(vaData, 2)).Value = Application.Index(vaData, i, 0)
'Stop looking in that row after one match
Exit For
With a statement that assigns the value Application.Index(vaData, i, 0) to an array variable, and then when you're completed the For i loop, you can write the results in one pass to the results worksheet.
NOTE This may be noticeably faster if and only if there are many thousands of results. If there are only a few results expected, then exeuction speed is primarily affected by the need to iterate over every cell, not the operation of writing the results to another sheet.
2. Use another method than cell iteration
If you can implement this method, I would use it in conjunction with the above.
Ordinarily I would recommend using the .Find and .FindNext methods as considerably more efficient than using the i,j iteration. But since you need to use the Anglicize UDF on every cell in the range, you would need to make some restructure your code to accommodate. Might require multiple loops, for example, first Anglicize the vaData and preserve a copy of the non-Anglicized data, like:
Dim r as Long, c as Long
Dim vaDataCopy as Variant
Dim uRange as Range
Set uRange = ActiveSheet.UsedRange
vaData = uRange.Value
vaDataCopy = vaData
For r = 1 to Ubound(varDataCopy,1)
For c = 1 to Ubound(varDataCopy,2)
varDataCopy(r,c) = Anglicize(varDataCopy(r,c))
Next
Next
Then, put the Anglicize version on to the worksheet.
ActiveSheet.UsedRange.Value = vaDataCopy
Then, instead of the For i =... For j =... loop, use the .Find and .FindNext method on the uRange object.
Here is an example of how I implement Find/FindNext.
Finally, put the non-Anglicized version back on the worksheet, again with the caveat that it might require use of Transpose function:
ActiveSheet.UsedRange.Value = vaData
Whil this still iterates over every value to perform the Anglicize function, it does not operate on every value a second time (Instr function). So, you're essentially operating on the values only once, rather than twice. I suspect this should be much faster, especially if you combine it with the #1 above.
UPDATE BASED ON OP REVISION EFFORTS
After some comment discussion & emails back and forth, we arrive at this solution:
Option Explicit
Sub FindFeature()
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Dim shSearch As Worksheet:
Dim shResults As Worksheet
Dim vaData As Variant
Dim i As Long, j As Long, r As Long, c As Long
Dim sSearchTerm As String
Dim sData As String
Dim rNext As Range
Dim v As Variant
Dim vaDataCopy As Variant
Dim uRange As Range
Dim findRange As Range
Dim nxtRange As Range
Dim rng As Range
Dim foundRows As Object
Dim k As Variant
Set shSearch = Sheets("City")
shSearch.Activate
'Define and clear the results sheet
Set shResults = ActiveWorkbook.Worksheets("Results")
shResults.Range("A3").Resize(shResults.UsedRange.Rows.Count, 1).EntireRow.Delete
'# Create a dictionary to store our result rows
Set foundRows = CreateObject("Scripting.Dictionary")
'Get the search term
sSearchTerm = Application.InputBox("What are you looking for?")
'# set and fill our range/array variables
Set uRange = shSearch.UsedRange
vaData = uRange.Value
vaDataCopy = Application.Transpose(vaData)
For r = 1 To UBound(vaDataCopy, 1)
For c = 1 To UBound(vaDataCopy, 2)
'MsgBox uRange.Address
vaDataCopy(r, c) = Anglicize(vaDataCopy(r, c))
Next
Next
'# Temporarily put the anglicized text on the worksheet
uRange.Value = Application.Transpose(vaDataCopy)
'# Loop through the data, finding instances of the sSearchTerm
With uRange
.Cells(1, 1).Activate
Set rng = .Cells.Find(What:=sSearchTerm, After:=ActiveCell, _
LookIn:=xlFormulas, LookAt:=xlPart, SearchOrder:=xlByRows, _
SearchDirection:=xlNext, MatchCase:=False, SearchFormat:=False)
If Not rng Is Nothing Then
Set findRange = rng
Do
Set nxtRange = .Cells.FindNext(After:=findRange)
Debug.Print sSearchTerm & " found at " & nxtRange.Address
If Not foundRows.Exists(nxtRange.Row) Then
'# Make sure we're not storing the same row# multiple times.
'# store the row# in a Dictionary
foundRows.Add nxtRange.Row, nxtRange.Column
End If
Set findRange = nxtRange
'# iterate over all matches, but stop when the FindNext brings us back to the first match
Loop Until findRange.Address = rng.Address
'# Iterate over the keys in the Dictionary. This contains the ROW# where a match was found
For Each k In foundRows.Keys
'# Find the next empty row on results page:
With shResults
Set rNext = .Cells(.Rows.Count, 1).End(xlUp).Offset(1, 0). _
Resize(1, UBound(Application.Transpose(vaData), 1))
End With
'# Write the row to the next available row on Results
rNext.Value = Application.Index(vaData, k, 0)
Next
Else:
MsgBox sSearchTerm & " was not found"
End If
End With
'# Put the non-Anglicized values back on the sheet
uRange.Value = vaData
'# Restore application properties
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
'# Display the results
shResults.Activate
End Sub
Public Function Anglicize(ByVal sInput As String) As String
Dim vaGood As Variant
Dim vaBad As Variant
Dim i As Long
Dim sReturn As String
Dim c As Range
'Replace any 'bad' characters with 'good' characters
vaGood = Split("S,Z,s,z,Y,A,A,A,A,A,A,C,E,E,E,E,I,I,I,I,D,N,O,O,O,O,O,U,U,U,U,Y,a,a,a,a,a,a,c,e,e,e,e,i,i,i,i,d,n,o,o,o,o,o,u,u,u,u,y,y", ",")
vaBad = Split("Š,Ž,š,ž,Ÿ,À,Á,Â,Ã,Ä,Å,Ç,È,É,Ê,Ë,Ì,Í,Î,Ï,Ð,Ñ,Ò,Ó,Ô,Õ,Ö,Ù,Ú,Û,Ü,Ý,à,á,â,ã,ä,å,ç,è,é,ê,ë,ì,í,î,ï,ð,ñ,ò,ó,ô,õ,ö,ù,ú,û,ü,ý,ÿ", ",")
sReturn = sInput
Set c = Range("D1:G1")
For i = LBound(vaBad) To UBound(vaBad)
sReturn = Replace$(sReturn, vaBad(i), vaGood(i))
Next i
Anglicize = sReturn
'Sheets("Results").Activate
End Function