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!
Related
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 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 am looking to count the occurrences of consecutive numbers in a column and cannot seem to find a logical way to calculate this within a loop.
My column of values is simply entries of 0 or 1. What I want to is count each time there is two 0's in a row, three 0's a row, four 0's in a row and so on. The maximum number of times I would expect a consecutive number is 15.
Ideally, I would like the output for each occurrence entered into a table.
I have provided a snapshot below of the column in question.
My attempts so far consist of looping through the column checking for two 0's in a row, starting at row 2 but this causes issues when I have more than two 0's in a row.
'Check for 2
Dim TwoCount, RowNo As Integer, LastRow As Long
LastRow = Sheets("Data").Range("A165536").End(xlUp).Row
TwoCount = 0
RowNo = 2
For i = 2 To LastRow
If Sheets("Data").Range("H" & RowNo).Value = 1 Then
RowNo = RowNo + 1
Else
If Sheets("Data").Range("H" & RowNo).Value = 0 Then
TwoCount = 1
RowNo = RowNo + 1
If Sheets("Data").Range("H" & RowNo).Value = 0 Then
TwoCount = 2
RowNo = RowNo + 1
If Sheets("Data").Range("H" & RowNo).Value = 1 Then
End If
End If
End If
End If
Next i
I welcome any suggestions to how I should approach this? Whether it's easier as a formula or array formula.
Desired output
Count Consecutive Occurrences
Option Explicit
Sub CountConsecutive()
' Source
Const sName As String = "Data"
Const sFirstCellAddress As String = "H1"
Const sCriteria As Variant = 0
' Destination
Const dName As String = "Data"
Const dFirstCellAddress As String = "J1"
Dim dHeaders As Variant
dHeaders = VBA.Array("Occurrences", "Number of Times")
' Workbook
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
' Write the values from the source column to an array.
Dim sws As Worksheet: Set sws = wb.Worksheets(sName)
Dim Data As Variant
Dim rCount As Long
With sws.Range(sFirstCellAddress)
Dim slCell As Range: Set slCell = .Resize(sws.Rows.Count - .Row + 1) _
.Find("*", , xlFormulas, , , xlPrevious)
If slCell Is Nothing Then Exit Sub
rCount = slCell.Row - .Row + 1
If rCount < 2 Then Exit Sub
Data = .Resize(rCount).Value
End With
' Count the occurrences by using a dictionary.
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
Dim Key As Long
Dim r As Long
Dim cCount As Long
Dim MaxCount As Long
For r = 2 To rCount
Key = Data(r, 1)
If IsNumeric(Key) Then
If Key = sCriteria Then
cCount = cCount + 1
Else
If cCount > 0 Then
dict(cCount) = dict(cCount) + 1
If cCount > MaxCount Then MaxCount = cCount
cCount = 0
End If
End If
End If
Next r
If MaxCount = 0 Then Exit Sub
' Write the values from the dictionary to the array.
rCount = MaxCount + 1
ReDim Data(1 To rCount, 1 To 2)
Data(1, 1) = dHeaders(0)
Data(1, 2) = dHeaders(1)
For r = 2 To rCount
Data(r, 1) = r - 1
If dict.Exists(r - 1) Then
Data(r, 2) = dict(r - 1)
Else
Data(r, 2) = 0
End If
Next r
' Write the values from the array to the destination range.
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).Clear
'.Font.Bold = True
'.EntireColumn.AutoFit
End With
'wb.save
MsgBox "Consecutive count created.", vbInformation
End Sub
COUNTING THE FREQUENCY OF CONSECUTIVE OCCURRENCES OF 0 IN A COLUMN
You may try this array formula as well,
• Formula used in cell L2
=SUMPRODUCT(--(FREQUENCY(
IF($H$2:$H$32=0,ROW($H$2:$H$32)),
IF($H$2:$H$32=1,ROW($H$2:$H$32)))=K2))
And Fill Down!
Note: Array formulas need to be entered by pressing CTRL + SHIFT + ENTER (not just ENTER). Hold down both the CTRL key and the SHIFT key then hit ENTER. If you are using Excel 2021 or O365 you can only press ENTER.
Imagine your numbers Win/Lose in column A then add in cell B3 (not B2 this will stay empty) the following formula and copy it down:
=IF(AND(A3=0,A3<>A4),COUNTIF($A$2:A3,A3)-SUM($B$2:B2),"")
Then to count them just use =COUNTIF(B:B,E2) in F2 and copy it down.
You can read this requirements in two ways as I see it:
You can count an occurence of 1,2,3 and 4 in a sequence of 4 zero's;
You can count only the max occurence of the above;
I went with the assumptions of the latter:
Formula in C1:
=LET(X,SEQUENCE(15),Y,LEN(TEXTSPLIT(CONCAT(IF(A2:A32," ",1)),," ",1)),VSTACK({"Occurences of 0","Number of Times"},HSTACK(X,BYROW(X,LAMBDA(a,SUM(--(Y=a)))))))
Important note:
It may not be best to rely on CONCAT() since depending on the amount of rows you want to concatenate, it may strike a character limit. Instead you could try something like:
=LET(X,SEQUENCE(15),Y,LEN(TEXTSPLIT(REDUCE("",A2:A32,LAMBDA(a,b,a&IF(b," ",1))),," ",1)),VSTACK({"Occurences of 0","Number of Times"},HSTACK(X,BYROW(X,LAMBDA(a,SUM(--(Y=a)))))))
Also, please note that ms365 is required for the above functions to run properly (and at time of writing VSTACK(), HSTACK() and TEXTSPLIT() are still in the insider's BETA-channels.
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
I would like to create a macro that will sum the unit column base by Product & DC. It will populate out the same product name, code with dc code and code without.
Sub Button1_Click()
For i = 2 to lstrow
Next I
MsgBox ("Done")
End Sub
Product
DC
Unit
ABC
0
2
ABC
1234
4
ABC
1234
4
DEF
5678
2
DEF
5678
2
GHI
9012
2
I want to the output as below:-
Product
Unit with DC Code
Unit Without DC Code
ABC
6
2
DEF
4
0
GHI
2
0
Sum Unique
Adjust the values in the constants section.
If you want to overwrite ('mimic' RemoveDuplicates), then just replace Sheet2 with Sheet1 and uncomment the Delete below section. Keep in mind that you won't be able to undo.
The Code
Option Explicit
Sub sumUnique()
' Define constants.
Const sName As String = "Sheet1"
Const sFirstCellAddress As String = "A2"
Const dName As String = "Sheet2"
Const dFirstCellAddress As String = "A1"
' Define workbook.
Dim wb As Workbook: Set wb = ThisWorkbook
' Write values from Source Range to Data Array.
Dim LastRow As Long
Dim Data As Variant
With wb.Worksheets(sName).Range(sFirstCellAddress)
LastRow = .Worksheet.Cells(.Worksheet.Rows.Count, .Column).End(xlUp).Row
Data = .Resize(LastRow - .Row + 1, 3)
End With
' Write unique values from Data Array to Unique Dictionary.
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
Dim arr(1 To 2) As Variant
Dim Key As Variant
Dim cArr As Variant
Dim r As Long
For r = 1 To UBound(Data, 1)
Key = Data(r, 1)
If Not IsError(Key) Then
If Not dict.Exists(Key) Then
dict.Add Key, arr
End If
If Data(r, 2) = 0 Then
cArr = dict(Key)
cArr(2) = cArr(2) + Data(r, 3)
dict(Key) = cArr
Else
cArr = dict(Key)
cArr(1) = cArr(1) + Data(r, 3)
dict(Key) = cArr
End If
End If
Next r
' Write values from Unique Dictionary to Result Array.
Dim rCount As Long: rCount = dict.Count + 1
Dim Result As Variant: ReDim Result(1 To rCount, 1 To 3)
Result(1, 1) = "Product"
Result(1, 2) = "Unit With DC Code"
Result(1, 3) = "Unit Without DC Code"
If rCount > 1 Then
r = 1
For Each Key In dict.Keys
r = r + 1
Result(r, 1) = Key
Result(r, 2) = CLng(dict(Key)(1))
Result(r, 3) = CLng(dict(Key)(2))
Next Key
End If
' Write values from Result Array to Destination Range.
With wb.Worksheets(dName).Range(dFirstCellAddress).Resize(, 3)
.Resize(rCount).Value = Result
' Delete below.
'.Resize(.Worksheet.Rows.Count - .Row - rCount + 1) _
.Offset(rCount).ClearContents
End With
End Sub
Here is an example of VBA code that should do the trick:
Sub SubWhyNotSUMIFS()
'Declarations.
Dim RngResult As Range
Dim RngTarget As Range
Dim RngSource As Range
Dim StrPreviousProduct As String
'Settings.
Set RngResult = Range("D1")
Set RngSource = Range("A2:C2")
Set RngSource = Range(RngSource, RngSource.End(xlDown))
'Creating space for the result.
RngResult.EntireColumn.Insert
RngResult.EntireColumn.Insert
RngResult.EntireColumn.Insert
'Reporting the headers of the result list.
Set RngResult = RngResult.Offset(0, -3)
RngResult.Value = "Product"
RngResult.Offset(0, 1) = "Unit with DC Code"
RngResult.Offset(0, 2) = "Unit Without DC Code"
'Covering each cell of the first column of RngSource.
For Each RngTarget In RngSource.Columns(1).Cells
'Checking if it's a different product.
If StrPreviousProduct <> RngTarget.Value Then
'Setting RngResult for a new row.
Set RngResult = RngResult.Offset(1, 0)
'Changing StrPreviousProduct.
StrPreviousProduct = RngTarget.Value
'Reporting the results.
RngResult.Value = RngTarget.Value
RngResult.Offset(0, 1).Value = Excel.WorksheetFunction.SumIfs(RngSource.Columns(3), RngSource.Columns(1), RngTarget.Value, RngSource.Columns(2), "<>0")
RngResult.Offset(0, 2).Value = Excel.WorksheetFunction.SumIfs(RngSource.Columns(3), RngSource.Columns(1), RngTarget.Value, RngSource.Columns(2), 0)
End If
Next
End Sub
You can reverse-engineer it. The lines you will be most interested into are:
RngResult.Offset(0, 1).Value = Excel.WorksheetFunction.SumIfs(RngSource.Columns(3), RngSource.Columns(1), RngTarget.Value, RngSource.Columns(2), "<>0")
RngResult.Offset(0, 2).Value = Excel.WorksheetFunction.SumIfs(RngSource.Columns(3), RngSource.Columns(1), RngTarget.Value, RngSource.Columns(2), 0)
Translated in actual formulas they are basically this:
=SUMIFS(C2:C7,A2:A7,D2,B2:B7,"<>0")
=SUMIFS(C2:C7,A2:A7,D2,B2:B7,"=0")
Assuming that your list spans from cell A1 (headers) to cell C7, that in cell D2 there is the unique product you are looking for. The code itself dynamically covers the list. Since it looks like your list is the only thing in their own columns, formulas like these:
=SUMIFS(C:C,A:A,D2,B:B,"<>0")
=SUMIFS(C:C,A:A,D2,B:B,"=0")
should still be effective.