I'm new in VBA and I’m getting wrong results by doing a dictionary in VBA.
Input :
column B : societies's ID
column A : their stores'IDs
column C : amounts
Output expected:
Column E: societies ID
Column F : stores ID (unique values)
Column G : total amount of each stores ID
What I get :
Example: For the store ID FRPAN3 I’m supposed to have 351,48.
Code :
Option Explicit
Dim dico As Object, f As Worksheet, i&
Sub ValeursUniques()
Set dico = CreateObject("Scripting.Dictionary")
Set f = Sheets("Feuil1")
For i = 2 To f.Range("B" & Rows.Count).End(xlUp).Row
dico(f.Range("B" & i).Value) = dico(f.Range("B" & i).Value) + Val(f.Range("C" & i))
Next i
Range("F2").Resize(dico.Count, 1) = Application.Transpose(dico.keys)
Range("G2").Resize(dico.Count, 1) = Application.Transpose(dico.items)
End Sub
Any idea why I get those results ?
The Val function may not return the correct value. If your values in column F Val(f.Range("F" & i)) are actually non-integers their decimals can get cut off!
The documentation says
The Val function stops reading the string at the first character that it can't recognize as part of a number.
The Val function recognizes only the period ( . ) as a valid decimal separator. When different decimal separators are used, as in international applications, use CDbl instead to convert a string to a number.
So if there is any character in your number it will cut off. In your case the , counts as a character and therefore your values are turned into integers as the , is not treated as a decimal seperator.
Make sure to use Type conversion functions instead:
cDbl(f.Range("F" & i))
will convert the value into a floating point with double precision.
Uniquify Data by Using a Dictionary
If a value in the first Unique column (in this case column 2) is an error value or a blank,
the record will not be included.
If a value in the other Unique columns (in this case only column 1) is an error value,
it will be converted to Empty (implicitly).
If a value in the Value column (in this case column 3) is not a number,
0 (zero) will be used instead.
Adjust (play with) the values in the constants section.
Option Explicit
Sub UniquifyData()
' Source
Const sName As String = "Feuil1"
Const sFirstCellAddress As String = "A1"
Dim uCols As Variant: uCols = VBA.Array(2, 1)
Const svCol As Long = 3
' Destination
Const dName As String = "Feuil1"
Const dFirstCellAddress As String = "E1"
' Both
Const Delimiter As String = "#"
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
' Reference the source range and write its values to the source array.
Dim sws As Worksheet: Set sws = wb.Worksheets(sName)
Dim srg As Range: Set srg = sws.Range(sFirstCellAddress).CurrentRegion
Dim Data As Variant: Data = srg.Value
Dim srCount As Long: srCount = UBound(Data, 1)
Dim cCount As Long: cCount = UBound(Data, 2)
' Write the headers from the source array to the headers array.
Dim cUpper As Long: cUpper = UBound(uCols)
Dim Headers As Variant: ReDim Headers(1 To cUpper + 2)
Dim c As Long
For c = 0 To cUpper
Headers(c + 1) = Data(1, uCols(c))
Next c
Headers(cCount) = Data(1, svCol)
' Write the unique values from the source array to a dictionary.
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
Dim Key As Variant
Dim sString As String
Dim r As Long
For r = 2 To srCount
For c = 0 To cUpper
Key = Data(r, uCols(c))
If c = 0 Then
If Not IsError(Key) Then
If Len(Key) > 0 Then
sString = CStr(Key)
End If
End If
If Len(sString) = 0 Then Exit For
Else
If IsError(Key) Then Key = ""
sString = sString & Delimiter & CStr(Key) ' join uniques
End If
Next c
If Len(sString) > 0 Then
If IsNumeric(Data(r, svCol)) Then
dict(sString) = dict(sString) + Data(r, svCol)
Else
If Not dict.Exists(sString) Then dict(sString) = 0
End If
sString = ""
End If
Next r
' Define the destination array.
Dim drCount As Long: drCount = dict.Count + 1
ReDim Data(1 To drCount, 1 To cCount)
' Write the headers from the headers array to the destination array.
For c = 1 To cCount
Data(1, c) = Headers(c)
Next c
' Write the values from the dictionary to the destination array.
r = 1
For Each Key In dict.Keys
r = r + 1
' Write uniques.
uCols = Split(Key, Delimiter) ' split uniques
For c = 0 To cUpper
Data(r, c + 1) = uCols(c)
Next
' Write value.
Data(r, cCount) = dict(Key)
Next Key
' Write the values from the destination array to the destination range.
Dim dws As Worksheet: Set dws = wb.Worksheets(dName)
With dws.Range(dFirstCellAddress).Resize(, cCount) ' reference first row
' Write data.
.Resize(drCount).Value = Data
' Clear below.
.Resize(dws.Rows.Count - .Row - drCount + 1).Offset(drCount).Clear
' Apply some formatting.
'.Font.Bold = True ' headers
'.EntireColumn.AutoFit ' columns
End With
' Inform.
MsgBox "Data uniquified.", vbInformation
End Sub
Related
How could I make my code faster...? I tried two different approaches to achieve what I need to, but they are rather slow when I work with ~140,000 rows of data.
The data is found in one sheet, and four additional sheets exist which have the names
LogName(1) = "Log_1"
LogName(2) = "Log_2"
LogName(3) = "Log_3"
LogName(4) = "Custom_Log"
LogNameSz = UBound(LogName) - LBound(LogName) + 1
Each data row contain one of these labels per row, and all of these labels are in the same column (see example below):
enter image description here
The spirit of the code is the following: for each row, look at the row's label, copy the entire row of data, and paste it into the sheet with the corresponding name.
These are my approaches:
''FIRST METHOD
'For j = 1 To LogNameSz
' cnt = 4
' Set celE = ColLog.Find(LogName(j), LookIn:=xlValues)
' fstadd = celE.Address
' 'Debug.Print fstadd
' Do
' celE.EntireRow.Copy Worksheets(LogName(j)).Rows(cnt)
' Set celE = ColLog.FindNext(celE)
' cnt = cnt + 1
' Loop While celE.Address <> fstadd
'Next j
''SECOND METHOD
' For s = 1 To Lastrworig
' If CkList(ColLog.Rows(s).Value, LogName, LogNameSz) = True Then
' Set ColEValue = Worksheets(ColLog.Rows(s).Value).UsedRange
' Lastrwlog = ColEValue.Row + ColEValue.Rows.count - 1
' ColLog.Rows(s).EntireRow.Copy Worksheets(ColLog.Rows(s).Value).Rows(Lastrwlog + 1)
' End If
' Next s
The reason why cnt = 4 is because the four additional sheets have 3 rows that have been added to the top of the sheets, and the data needs to be pasted starting on the 4th row. This is also the same reason why I look for the last row of each sheet by using UsedRange in my second method.
Does anyone one have suggestions for how to make either of these methods faster?
Copy +100k Criteria Rows
Sub CopyData()
Const SRC_NAME As String = "Master"
Const SRC_CRITERIA_COLUMN As Long = 5
Const SRC_FIRST_CELL As String = "A2"
Const DST_FIRST_CELL As String = "A4"
Dim dNames(): dNames = VBA.Array("Log_1", "Log_2", "Log_3", "Custom_Log")
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
Dim sws As Worksheet: Set sws = wb.Sheets(SRC_NAME)
Dim srg As Range
With sws.UsedRange
Set srg = sws.Range(SRC_FIRST_CELL, .Cells(.Rows.Count, .Columns.Count))
End With
Dim cCount As Long: cCount = srg.Columns.Count
Dim sData(): sData = srg.Value
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
Dim dKey
For Each dKey In dNames
Set dict(dKey) = New Collection
Next dKey
Erase dNames
Dim sr As Long, srString As String
For sr = 1 To UBound(sData, 1)
srString = CStr(sData(sr, SRC_CRITERIA_COLUMN))
If dict.Exists(srString) Then dict(srString).Add sr
Next sr
Dim dws As Worksheet, dData(), srItem, dr As Long, c As Long
For Each dKey In dict.Keys
ReDim dData(1 To dict(dKey).Count, 1 To cCount)
dr = 0
For Each srItem In dict(dKey)
dr = dr + 1
For c = 1 To cCount
dData(dr, c) = sData(srItem, c)
Next c
Next srItem
Set dws = wb.Sheets(dKey)
dws.Range(DST_FIRST_CELL).Resize(dr, cCount).Value = dData
Next dKey
MsgBox "Data copied.", vbInformation
End Sub
I have an array of values result that I got from a REST API call - result = [1,2,3,4,5] and they are parsed in as variant in the AppendUnique function.
What I want to do:
AppendUnique function appends unique values from a growing result array to a range in excel. I want to add a new feature to AppendUnique, where the repeated values in the result array will be highlighted in the excel cell.
Explanation on my current code:
In the beginning
I input each value in the array result to populate cells from A1 to A5 (the range is dynamic, based on the number of values in the array, so might not be A5 all the time).
So, if the range (A1-A100) is empty, we populate the cells normally.
^ this part is completed
As the result array grows
Since the result will increase as we run the Macro again, for example, 15 minutes later the result may become [1,2,3,4,5,6,7,8]
So, if the range (A1-A5) is not empty, we append the array's additional items at the back of the cell range, if they do not appear in the range (meaning they are additional ones)
^ this part is completed
The result may also contain duplicates, for example, 30 minutes later, the result may become [1,2,3,4,5,6,7,8,3], where 3 is the duplicate.
If there is duplicate - 3, the cell A3 (where we populated 3) needs to be highlighted.
^ this question is about this part
My current code:
Sub AppendUnique( _
Arr() As Variant, _
ByVal ws As Worksheet, _
ByVal FirstCellAddress As String, _
Optional ByVal OverWrite As Boolean = False)
' Write the data from the source range to the source array ('sData').
' Reference the first destination cell ('fCell').
If ws.FilterMode Then ws.ShowAllData
Dim fCell As Range: Set fCell = ws.Range(FirstCellAddress)
Dim sData() As Variant, srCount As Long
With fCell
Dim lCell As Range: Set lCell = .Resize(ws.Rows.Count - .Row + 1) _
.Find("*", , xlFormulas, , , xlPrevious)
If Not lCell Is Nothing Then
srCount = lCell.Row - .Row + 1
If srCount = 1 Then
ReDim sData(1 To 1, 1 To 1): sData(1, 1) = .Value
Else
sData = .Resize(srCount).Value
End If
If Not OverWrite Then Set fCell = lCell.Offset(1)
End If
End With
' Write the unique data from the source array to a dictionary.
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
Dim sr As Long
For sr = 1 To srCount: dict(CStr(sData(sr, 1))) = Empty: Next sr
Erase sData
' Define the destination array ('dData').
Dim lb As Long: lb = LBound(Arr)
Dim ub As Long: ub = UBound(Arr)
Dim dData() As Variant: ReDim dData(1 To ub - lb + 1, 1 To 1)
' Check the values from the given array ('Arr') against the values
' in the dictionary and write the non-matches to the destination array.
Dim dr As Long, c As Long, cString As String
For c = lb To ub
cString = CStr(Arr(c))
If Len(cString) > 0 Then ' is not blank
If Not dict.Exists(cString) Then ' is not in the dictionary
dict(cString) = Empty ' prevent dupes from the given array
dr = dr + 1
dData(dr, 1) = cString
End If
End If
Next c
If dr = 0 Then
MsgBox "No new values found.", vbExclamation
Exit Sub
End If
' Write the values from the destination array to the destination range.
fCell.Resize(dr).Value = dData
If OverWrite Then ' clear below
fCell.Resize(ws.Rows.Count - fCell.Row - dr + 1).Offset(dr).Clear
End If
' Inform.
MsgBox "Data appended.", vbInformation
End Sub
I initially thought maybe I can do something under the line - If Len(cString) > 0 Then, to add If dict. Exists(cstring) Then, highlight the cell by doing something like interior.color = vbYellow.
However, I realised that in my current code, the products are appended altogether after checking the repeated items, so I am not exactly sure how to highlight the cell of repeated value, since we are not looping over the appended range.
Any help would be greatly appreciated, thanks in advance.
Use the dictionary value to store a reference to the relevant row. The complication is to differentiate between existing keys from the sheet and those added from the array. For the Overwrite mode the values from the sheet become obsolete. I have used a concatenated string of the row offset and either ";sht" or ";arr". It is easy to separate the 2 values with split().
To identify duplicate in the array I have added another dictionary - dupl.
Sub AppendUnique( _
Arr() As Variant, _
ByVal ws As Worksheet, _
ByVal FirstCellAddress As String, _
Optional ByVal OverWrite As Boolean = False)
If ws.FilterMode Then ws.ShowAllData
Dim fCell As Range, lCell As Range, tcell As Range
Dim sData() As Variant, srCount As Long
' Write the data from the source range to the source array ('sData').
' Reference the first destination cell ('fCell').
Set fCell = ws.Range(FirstCellAddress)
If Len(fCell) = 0 Then
srCount = 0
' target cell for appending new items
Set tcell = fCell
fCell.ClearFormats
Else
Set lCell = ws.Cells(ws.Rows.Count, fCell.Column).End(xlUp)
srCount = lCell.Row - fCell.Row + 1
If srCount > 1 Then
sData = fCell.Resize(srCount).Value2
Else
ReDim sData(1 To 1, 1 To 1):
sData(1, 1) = fCell.Value2
End If
' clear any existing coloring
fCell.Resize(srCount).ClearFormats
' target cell for appending new items
Set tcell = lCell.Offset(1)
End If
' Write the unique data from the source array to a dictionary.
Dim dict As Object, sr As Long, r As Long
Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
r = 0 ' row offset
If srCount > 0 Then
For sr = 1 To UBound(sData)
dict(CStr(sData(sr, 1))) = r & ";sht" ' fcell row offset +1
r = r + 1
Next sr
End If
' reset target cell
If OverWrite Then
Set tcell = fCell
r = 0
End If
' Define the destination array ('dData').
Dim lb As Long, ub As Long
Dim dr As Long, c As Long, cString As String
Dim dData() As Variant:
lb = LBound(Arr)
ub = UBound(Arr)
ReDim dData(1 To ub - lb + 1, 1 To 1)
' Check the values in Arr
' against the values in the dictionary and
' write the non-matches to the destination array.
Dim dupl As Object, k
Set dupl = CreateObject("Scripting.Dictionary")
For c = lb To ub
' dictionary key
k = CStr(Arr(c))
If Len(k) > 0 Then ' is not blank
If Not dict.Exists(k) Then
' is not in the dictionary
' prevent dupes from the given array
dict(k) = r & ";arr ' store fcell offset"
r = r + 1
dr = dr + 1
dData(dr, 1) = k
End If
' check for duplicates in arr
If dupl.Exists(k) Then
dupl(k) = dupl(k) + 1
Else
dupl.Add k, 1
End If
End If
Next c
' clear existing data
If OverWrite And srCount > 0 And dr > 0 Then
fCell.Resize(srCount).Clear
End If
' Write the values from the destination array
' to the destination range.
If dr > 0 Then
tcell.Resize(dr).Value = dData
End If
' highligh if duplicate
Dim ar
For Each k In dupl.keys
If dupl(k) > 1 Then
ar = Split(dict(k), ";")
r = ar(0)
If dr > 0 And OverWrite And ar(1) = "sht" Then
' do nothing as row information is useless
' for existing value with overwrite
Else
fCell.Offset(r).Interior.Color = RGB(255, 255, 0)
End If
End If
Next
If dr = 0 Then
MsgBox "No new values found.", vbExclamation
Else
' Inform.
MsgBox dr & " Data rows appended.", vbInformation
End If
End Sub
I have the following data in Excel (created for the example), where in the rightest column "Key" is my target. I would like to keep only top row for each name and delete older entries with the same name, based on date & time. Thus, I would like to keep only green rows i.e., Apple Big 14:14:50 and delete the one below.
I have problem how the code should distinguish between names and limited idea how to tell my VBA code that all other rows with the same name just delete.
My data:
My idea, it's not a full code, only idea:
Dim LR As Long
LR = Cells(Rows.Count, "A").End(xlUp).Row
With ThisWorkbook.Worksheets("Data").Range("A2:A" & LR)
If A = "Key" Then Delete.Row
Else "Move one cell down" and set cell = "Key"
End Sub
Remove Duplicates Using a Dictionary
If your data is not sorted, you can't use RemoveDuplicates but you could use the following procedure.
The star of the show is a dictionary whose keys will hold the unique values from the 1st column (Keys) and whose items will hold a two-element array, whose 1st element will hold the row index while the 2nd element will hold the value retrieved by adding the values from the 3rd and 4th columns. On each iteration, the new 2nd value (cValue) will be checked against the old value and the old data will be replaced with the new data if the new 2nd value is greater than the old.
Sub RemoveDupes()
Const UNIQUE_COLUMN_INDEX As Long = 1 ' CStr
Const DATE_COLUMN_INDEX As Long = 3 ' IsDate
Const TIME_COLUMN_INDEX As Long = 4 ' IsNumeric
Const COPY_VALUES_ONLY As Boolean = False
Dim rg As Range
Set rg = ThisWorkbook.Worksheets("Data").Range("A1").CurrentRegion
Dim rCount As Long: rCount = rg.Rows.Count - 1
Dim cCount As Long: cCount = rg.Columns.Count
Set rg = rg.Resize(rCount).Offset(1) ' data without headers
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
Dim Data() As Variant: Data = rg.Value
Dim Arr() As Double: ReDim Arr(1 To 2)
Dim r As Long
Dim c As Long
Dim cString As String
Dim cValue As Double
Dim IsValid As Boolean
Dim DontWrite As Boolean
For r = 1 To rCount
cString = CStr(Data(r, UNIQUE_COLUMN_INDEX))
If IsDate(Data(r, DATE_COLUMN_INDEX)) Then
If IsNumeric(Data(r, TIME_COLUMN_INDEX)) Then IsValid = True
End If
If IsValid Then
cValue = Data(r, DATE_COLUMN_INDEX) + Data(r, TIME_COLUMN_INDEX)
IsValid = False
Else
cValue = 0
End If
If dict.Exists(cString) Then
If cValue <= dict(cString)(2) Then DontWrite = True
End If
If DontWrite Then
DontWrite = False
Else
Arr(1) = r
Arr(2) = cValue
dict(cString) = Arr
End If
Next r
Dim Key As Variant
Dim sr As Long
Dim dr As Long
If COPY_VALUES_ONLY Then ' only values (fast)
For Each Key In dict.Keys
sr = dict(Key)(1)
dr = dr + 1
For c = 1 To cCount
Data(dr, c) = Data(sr, c)
Next c
Next Key
For r = dr + 1 To rCount
For c = 1 To cCount
Data(r, c) = Empty
Next c
Next r
rg.Value = Data
Else ' values, formatting, formulas (slow)
For Each Key In dict.Keys
sr = dict(Key)(1)
dr = dr + 1
rg.Rows(sr).Copy rg.Rows(dr)
Next Key
If rCount > dr Then
rg.Resize(rCount - dr).Offset(dr).Clear
End If
End If
End Sub
I use two data dumps which are saved in OPL_Dump and OPL_DUMP_2 sheets.
The code I am trying to improve, finds the data in one of the dumps and copies and pastes as a new parameter as addition to the same corresponding value it sees for the other dump.
The length of both the data dumps varies. I manually amend the length of the range every time.
I am trying to make my code a bit more robust
I tried defining N and L instead of fixed numbers of last rows.
Sub Merging_Both_Dumps_for_Product_Type()
Dim out() As String
'Dim out2() As String
L As Long
L = ThisWorkbook.Sheets("OPL_DUMP_2").Select.Cells(Rows.Count, "B").End(xlUp).Row
ThisWorkbook.Sheets("OPL_DUMP_2").Select
keyarray = Range("F" & 2 & ":F" & L)
valuearray = Range("J" & 2 & ":J" & L)
N As Long
N = ThisWorkbook.Sheets("OPL_DUMP").Select.Cells(Rows.Count, "B").End(xlUp).Row
ReDim out(N, 0)
For j = 2 To N
ind = Index(keyarray, ThisWorkbook.Sheets("OPL_DUMP").Cells(j, 2).Value)
out(j - 2, 0) = valuearray(ind, 1)
Next j
'ReDim out2(1, 0)
'out2(1, 0) = "test"
'ThisWorkbook.Sheets("OPL_DUMP").Range("AD2:AD3") = out2()
ThisWorkbook.Sheets("OPL_DUMP").Range("AC" & 2 & ":AC" & N) = out
End Sub
Try this code, should work fine, fast and always no matter the size of your dumps:
Option Explicit
Sub Merging_Both_Dumps_for_Product_Type()
'You need the reference Microsoft Scripting Runtime
'under tools-references activated for this code to work.
Dim output_values As Dictionary
Set output_values = load_output_values(ThisWorkbook.Sheets("OPL_DUMP_2").UsedRange.Value)
'Store your output worksheet inside an array
Dim arr As Variant: arr = ThisWorkbook.Sheets("OPL_DUMP").UsedRange.Value
'loop through the array
Dim i As Long
For i = 2 To UBound(arr)
'check if the value in col B exists in the dictionary
If output_values.Exists(arr(i, 2)) Then
arr(i, 29) = output_values(arr(i, 2))
End If
Next i
'paste back the array to the worksheet
ThisWorkbook.Sheets("OPL_DUMP").UsedRange.Value = arr
'Note that using worksheet.usedrange.value will store
'everything in the sheet that has been used, even if its blank
'meaning if you do ctrl+end in your keyboard, the array will be
'as big as A1: the cell where ctrl+end sends you.
End Sub
Private Function load_output_values(arr As Variant) As Dictionary
'this function will store in a dictionary each key (col F = index 2)
'with it's item (col J = index 10)
'Since we stored the sheet inside an array we can loop through it
Set load_output_values = New Dictionary ' init the dictionary
Dim i As Long
For i = 2 To UBound(arr)
'first check either column B is empty or already exists
'will take the first ocurrence if col B is duplicated.
If Not arr(i, 2) = vbNullString _
And Not load_output_values.Exists(arr(i, 2)) Then
load_output_values.Add arr(i, 2), arr(i, 10)
End If
Next i
End Function
Lookup Data Using Application.Match
Option Explicit
Sub LookupData()
' 1. Define constants.
' Source
Const sName As String = "OPL_DUMP_2"
Const skCol As String = "F" ' 2. ... lookup the key...
Const svCol As String = "J" ' 3. ... read the associated value...
Const sfRow As Long = 2
' Destination
Const dName As String = "OPL_DUMP"
Const dkCol As String = "B" ' 1. Read the key...
Const dvCol As String = "AC" ' 4. ... write the value.
Const dfRow As Long = 2
' Workbook
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
' 2. Reference the source key (one-column) range ('skrg')
' and write the values from the source value (one-column) range ('svrg')
' to a 2D one-based (one-column) array ('svData').
' We will use 'skrg' because 'Application.Match' is faster on a range.
' We will use 'svData' because reading from an array is faster than
' from a range.
Dim sws As Worksheet: Set sws = wb.Worksheets(sName)
Dim slRow As Long: slRow = sws.Cells(sws.Rows.Count, skCol).End(xlUp).Row
Dim srCount As Long: srCount = slRow - sfRow + 1
If srCount < 1 Then
MsgBox "No data in column range.", vbCritical
Exit Sub
End If
Dim skrg As Range: Set skrg = sws.Cells(sfRow, skCol).Resize(srCount)
' ... which is the same as:
'Set skrg = sws.Range(sws.Cells(sfRow, skCol), sws.Cells(slrow, skCol))
Dim svrg As Range: Set svrg = skrg.EntireRow.Columns(svCol)
Dim svData() As Variant
If srCount = 1 Then ' one cell
ReDim svData(1 To 1, 1 To 1): svData(1, 1) = svrg.Value
Else ' multiple cells
svData = svrg.Value
End If
' 3. Reference the destination key (one-column) range ('skrg')
' and write its values the to a 2D one-based (one-column) array,
' the destination keys array ('dkData').
' We will use 'dkData' because reading from an array is faster than
' from a range.
Dim dws As Worksheet: Set dws = wb.Worksheets(dName)
Dim dlRow As Long: dlRow = dws.Cells(dws.Rows.Count, dkCol).End(xlUp).Row
Dim drCount As Long: drCount = dlRow - dfRow + 1
If drCount < 1 Then
MsgBox "No data in column range.", vbCritical
Exit Sub
End If
Dim dkrg As Range: Set dkrg = dws.Cells(dfRow, dkCol).Resize(drCount)
' ... which is the same as:
'Set dkrg = dws.Range(dws.Cells(dfRow, dkCol), dws.Cells(dlrow, dkCol))
Dim dkData() As Variant
If drCount = 1 Then ' one cell
ReDim dkData(1 To 1, 1 To 1): dkData(1, 1) = dkrg.Value
Else ' multiple cells
dkData = dkrg.Value
End If
' 3. Write the matching values to the destination values array ('dvData'),
' a 2D one-based one-column array, with the same number of rows
' as the number of rows of the destination keys array.
Dim dvData() As Variant: ReDim dvData(1 To drCount, 1 To 1)
Dim sr As Variant
Dim dValue As Variant
Dim dr As Long
For dr = 1 To drCount
dValue = dkData(dr, 1)
sr = Application.Match(dValue, skrg, 0)
If IsNumeric(sr) Then ' is a number (the row index)
dvData(dr, 1) = svData(sr, 1)
'Else ' is an error value (no match); do nothing
End If
Next dr
' 4. Write the values from the destination values array
' to the destination values range ('dvrg').
Dim dvrg As Range: Set dvrg = dkrg.EntireRow.Columns(dvCol)
dvrg.Value = dvData
' Save the workbook.
'wb.Save
' 5. Inform.
MsgBox "Lookup has finished.", vbInformation
End Sub
I have:
Column A: (IDs)
A
A
A
C
C
Z
Column B: (Values)
3
2
-6
-12
6
2
I'm trying to create a macro that fills all unique ID's into column C, and counts whether they pass/fail in column D. A pass would be having an associated value in column B between -5 and 5.
Column C/D would look like:
C
D
A
2
C
0
Z
1
If anyone can start me off or link a similar example id appreciate.
You can do it using formulas. But if you like/want VBA, please try the next piece of code. It uses arrays and a dictionary. Working only in memory, it should be very fast, even for large ranges:
Sub CountPassed()
Dim dict As Object, sh As Worksheet, lastR As Long
Dim arr, arrFin, i As Long
Set sh = ActiveSheet
lastR = sh.Range("A" & sh.rows.count).End(xlUp).row
arr = sh.Range("A2:B" & lastR).value 'place the range in an array for faster iteration
Set dict = CreateObject("Scripting.Dictionary")
For i = 1 To UBound(arr) 'extract unique keys and their item value according to the rule:
dict(arr(i, 1)) = dict(arr(i, 1)) + IIf(arr(i, 2) >= -5 And arr(i, 2) <= 5, 1, 0)
Next i
'create the necessary final array:
ReDim arrFin(1 To dict.count, 1 To 2)
For i = 0 To dict.count - 1
arrFin(i + 1, 1) = dict.Keys()(i)
arrFin(i + 1, 2) = dict.items()(i)
Next i
'drop the final array at once
sh.Range("C2").Resize(UBound(arrFin), 2).value = arrFin
End Sub
Count Unique With Limits
Adjust the values in the constants section.
Option Explicit
Sub CountUniqueWithLimits()
Const sName As String = "Sheet1"
Const sFirstCellAddress As String = "A1"
Const dName As String = "Sheet1"
Const dFirstCellAddress As String = "C1"
Const lLimit As String = ">=-5"
Const uLimit As String = "<=5"
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
Dim sws As Worksheet: Set sws = wb.Worksheets(sName)
Dim srg As Range
Dim rCount As Long
With sws.Range(sFirstCellAddress)
Dim lCell As Range: Set lCell = .Resize(sws.Rows.Count - .Row + 1) _
.Find("*", , xlFormulas, , , xlPrevious)
If lCell Is Nothing Then Exit Sub
rCount = lCell.Row - .Row + 1
Set srg = .Resize(rCount, 2)
End With
Dim Data As Variant: Data = srg.Value
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
Dim Key As Variant
Dim nkey As Variant
Dim r As Long
For r = 1 To rCount
Key = Data(r, 1)
If Not IsError(Key) Then
If Len(Key) > 0 Then
If Not dict.Exists(Key) Then
dict(Key) = 0
End If
nkey = Data(r, 2)
If IsNumeric(nkey) Then
If Len(nkey) > 0 Then
If Evaluate(nkey & lLimit) Then
If Evaluate(nkey & uLimit) Then
dict(Key) = dict(Key) + 1
End If
End If
End If
End If
End If
End If
Next r
rCount = dict.Count
If rCount = 0 Then Exit Sub
ReDim Data(1 To rCount, 1 To 2)
r = 0
For Each Key In dict.Keys
r = r + 1
Data(r, 1) = Key
Data(r, 2) = dict(Key)
Next Key
Dim dws As Worksheet: Set dws = wb.Worksheets(dName)
With dws.Range(dFirstCellAddress).Resize(, 2)
.Resize(rCount).Value = Data
.Resize(dws.Rows.Count - .Row - rCount + 1).Offset(rCount).ClearContents
End With
MsgBox "Unique values with limits counted.", vbInformation
End Sub
Well, it may happen you are not familiar of writing VBA Codes, then you may try any of the options using Excel Formula (Formulas Shown Below Are Exclusively For Excel 2021 & O365 Users)
=CHOOSE({1,2},UNIQUE(ID),COUNTIFS(ID,UNIQUE(ID),Values,">=-5",Values,"<=5"))
In the above formula, we are combining two arrays within a CHOOSE Function.
• The first array contains the unique values in the database
UNIQUE(ID)
Where ID refers to the range =$A$3:$A$8, created using the Define Name Manager.
• The second array is essentially the COUNTIFS Function,
COUNTIFS(ID,UNIQUE(ID),Values,">=-5",Values,"<=5")
Where Values refers to the range =$B$3:$B$8, created using the Define Name Manager.
The CHOOSE function combines both the arrays into a single array, which produces as a two-column table as shown in the image below.
Note that we can also use the LET function to elegantly perform, by defining a variable, U to hold the unique values,
• Formula can also be used in cell C3
=LET(U,UNIQUE(ID),CHOOSE({1,2},U,COUNTIFS(ID,U,Values,">=-5",Values,"<=5")))
You may see that this version of the formula calls the UNIQUE function once only, storing the result in U, which is used twice!