Multi-dimensional array to store and count occurrences of unique IDs - excel

Background:
In trying to better understand dynamic multi-dimensional arrays, I am attempting to build one to capture unique values and count the occurrences of the unique values (something i should be able to verify pretty quickly with a countif).
In reading about trying to redim preserve a multidimensional array, I had read that you can only redim the last parameters, so I was attempting to set-up for 2 parameters, where the first is the unique value and the second is the count: arr(2,k). If my understanding is wrong, then that also is pretty significant.
The final output of the array I am throwing into column 3 (unique ID) and column 4 (# of occurrences).
Issue:
When adding values to the array, I am not able to collect all unique values. I have been able to collect 3 unique values, when there are 6 in the data, and the occurrences of each are staying at 1, e.g., not iterating.
Question:
I apologize that this is essentially 2 questions...
1) is my use of redim preserver arr(2,0 to k) appropriate syntax?
2) is there a glaring issue with my dynamic array generation which would explain why i'm not getting all unique values captured?
I could ask a third about why i can't get the occurrence count to work, but I am hopeful that if I understand the above issue I can hopefully struggle through this part.
What the data looks like:
All data is in Column A
cat
dog
mouse
cat
mouse
bear
frog
cat
moose
cat
dog
Code in Question:
Option Explicit
Private Sub unique_arr()
Dim arr As Variant, i As Long, lr As Long, k As Long
lr = Cells(Rows.Count, 1).End(xlUp).Row
ReDim arr(2, k)
For i = 1 To lr
If Application.IfError(Application.Match(Cells(i, 1).Value, arr, 0), 0) = 0 Then
ReDim Preserve arr(2, 0 To k)
arr(1, k) = Cells(i, 1).Value
arr(2, k) = 1
k = k + 1
Else
arr(2, Application.Match(Cells(i, 1), arr(1), 0)) = arr(2, Application.Match(Cells(i, 1), arr(1), 0)) + 1
End If
Next i
For i = LBound(arr) To UBound(arr)
Cells(i + 1, 3).Value = arr(1, i)
Cells(i + 1, 4).Value = arr(2, i)
Next i
End Sub

While you would be better off overall with a dictionary, there are a few things wrong with the If comparison.
If Application.IfError(Application.Match(Cells(i, 1).Value, arr, 0), 0) = 0 Then
VBA has its own IsError that returns True/False.
If IsError(Application.Match(Cells(i, 1).Value, arr, 0), 0)) Then
Additionally, arr is a 2-D array; in essence it has both rows and columns. The worksheet's Match can only work on a single column or a single row. You need to 'slice' off what you want with Index.
If Not IsError(Application.Match(Cells(i, 1).Value, application.index(arr, 1, 0), 0), 0)) Then
Finally, arr is defined as ReDim arr(2, k). This makes it arr(0 to 2, 0 to k) so there are three elements in the first rank (0, 1, 2), not 2. You never actually use the 0 in the first rank. It should be,
k = 1
ReDim arr(1 to 2, 1 to k)
Wind it all up and you end up with something like this.
Option Explicit
Private Sub unique_arr()
Dim i As Long, lr As Long, k As Long, arr As Variant, m As Variant
'assign values to some vars
lr = Cells(Rows.Count, 1).End(xlUp).Row
k = 1
ReDim arr(1 To 2, 1 To k)
'loop through cells, finding duplicates and counting
For i = 1 To lr
m = Application.Match(Cells(i, 1).Value, Application.Index(arr, 1, 0), 0)
If IsError(m) Then
ReDim Preserve arr(1 To 2, 1 To k)
arr(1, k) = Cells(i, 1).Value
arr(2, k) = 1
k = k + 1
Else
arr(2, m) = arr(2, m) + 1
End If
Next i
'loop through array's second rank
For i = LBound(arr, 2) To UBound(arr, 2)
Cells(i, 3).Value = arr(1, i)
Cells(i, 4).Value = arr(2, i)
Next i
End Sub

For something like this, I'd use a Dictionary, like so:
Sub ExtractUniqueCounts()
Dim ws As Worksheet
Dim rCell As Range
Dim hUnq As Object
Set ws = ActiveWorkbook.ActiveSheet
Set hUnq = CreateObject("Scripting.Dictionary") 'Create Dictionary object
'Loop through populated cells in column A
For Each rCell In ws.Range("A1", ws.Cells(ws.Rows.Count, "A").End(xlUp)).Cells
'Ignore blanks
If Len(rCell.Value) > 0 Then
'Check if this is a new, unique value that hasn't been added yet
If Not hUnq.Exists(rCell.Value) Then
'New unique value found, add to dictionary and set count to 1
hUnq(rCell.Value) = 1
Else
'Not a unique value, increase existing count
hUnq(rCell.Value) = hUnq(rCell.Value) + 1
End If
End If
Next rCell
'Check if there are any results
If hUnq.Count > 0 Then
'Results found
'Output the keys (unique values)
ws.Range("C1").Resize(hUnq.Count).Value = Application.Transpose(hUnq.keys)
'Output the values of the keys (the counts in this case)
ws.Range("D1").Resize(hUnq.Count).Value = Application.Transpose(hUnq.items)
Else
'No results, return error
MsgBox "No data"
End If
End Sub

Related

Segregate data by data types

I have some data (mixed data types in column A). How can I split each data type into another column?
I mean numbers to be in column, string in column, dates in column and so on
This is my try till now but I didn't get all the results as expected
Sub Test()
Dim a, b(), dic As Object, i As Long, k As Long, ii As Long, n As Long
a = Range("A1:A10").Value
Set dic = CreateObject("Scripting.Dictionary")
For i = LBound(a) To UBound(a)
If Not dic.Exists(VarType(a(i, 1))) Then
dic.Item(VarType(a(i, 1))) = Empty
ReDim Preserve b(UBound(a, 1), k)
k = k + 1
End If
n = 0
Do Until b(i - 1, k - 1) <> Empty
b(i - 1, k - 1) = a(i, 1)
Loop
Next i
Range("J1").Resize(UBound(b, 1), UBound(b, 2)).Value = b
End Sub
There are a number of things I would do differently in your code
using meaningful names for the variables
specifying the worksheet instead of depending on the implied ActiveSheet
clearing the results area on the worksheet
early binding of the dictionary object
dynamic determination of the range to be processed
etc
but the below code modifies your original code minimally, to obtain the output I think you want, based on your screenshot
Sub Test()
Dim a, b(), dic As Object, i As Long, k As Long, ii As Long, n As Long, v
Dim dataType As String
a = Range("A1:A10").Value
ReDim b(1 To UBound(a))
'first create the dictionary with the datatypes
'since you are maintaining the entries in the same rows,
' add an empty array as the item
Set dic = CreateObject("Scripting.Dictionary")
dic.Add Key:="number", Item:=b
dic.Add Key:="date", Item:=b
dic.Add Key:="string", Item:=b
dic.Add Key:="logical", Item:=b
'Add the values to the correct dictionary item
' at the correct spot in the array
For i = LBound(a) To UBound(a)
Select Case VarType(a(i, 1))
Case 2 To 6
dataType = "number"
Case 7
dataType = "date"
Case 8
dataType = "string"
Case 11
dataType = "logical"
Case Else
dataType = ""
End Select
If dataType <> "" Then
v = dic(dataType)
v(i) = a(i, 1)
dic(dataType) = v
End If
Next i
'Next create output array
ReDim b(1 To UBound(a), 1 To dic.Count)
k = 0
For Each v In dic.Keys
k = k + 1
For i = 1 To UBound(dic(v))
b(i, k) = dic(v)(i)
Next i
Next v
Range("J1").Resize(UBound(b, 1), UBound(b, 2)).Value = b
End Sub
Edit:
If, as you indicate in your comments, you don't want to set up the data types initially, you can also do that at the time of creation of the dictionary object. Using the same algorithm of storing the item as an array of the same size as the number of rows in the data base:
Sub Test()
Dim a, b(), dic As Object, i As Long, k As Long, ii As Long, n As Long, v
Dim dataType As Long
a = Range("A1:A10").Value
ReDim b(1 To UBound(a))
Set dic = CreateObject("Scripting.Dictionary")
'Add the values to the correct dictionary item
' at the correct spot in the array
For i = LBound(a) To UBound(a)
dataType = VarType(a(i, 1))
If a(i, 1) <> "" Then
If Not dic.Exists(dataType) Then
ReDim b(UBound(a))
b(i) = a(i, 1)
dic.Add Key:=dataType, Item:=b
Else
b = dic(dataType)
b(i) = a(i, 1)
dic(dataType) = b
End If
End If
Next i
'Next create output array
ReDim b(1 To UBound(a), 1 To dic.Count)
k = 0
For Each v In dic.Keys
k = k + 1
For i = 1 To UBound(dic(v))
b(i, k) = dic(v)(i)
Next i
Next v
Range("J1").Resize(UBound(b, 1), UBound(b, 2)).Value = b
End Sub

Can you create an array from an array in VBA?

Edit: By print array I mean put the array onto a range in a sheet :)
I am using the following code on a table in excel with VBA. This combines rows with matching sales rep. Below is the source table. This is loaded into an array.
After running the below code the rows are combined and I null out the rows that were combined. My challenge is to print certain columns and only print the non null rows. To accomplish this I was trying to loop through the array and create another array with just the non null rows.
Sub mergeCategoryValues2()
Dim arr2 As Variant
Dim rowcount As Long
Dim i As Variant
Dim colcount As Long
arr2 = ActiveSheet.ListObjects("APPLE").Range
rowcount = UBound(arr2, 1)
colcount = UBound(arr2, 2)
For i = rowcount To 2 Step -1
If arr2(i, 3) = arr2(i - 1, 3) Then
arr2(i - 1, 6) = arr2(i - 1, 6) + arr2(i, 6)
For k = 1 To colcount
arr2(i, k) = Null 'this loop is probably not required i can probably just use the first column
Next k
End If
Next i
End Sub
Ultimately I wanted to print just the non null rows and just Columns 3,2,and 6. The best way I thought was to create an array with non null rows
Create an array for the results with the same number of rows as the data array. Scan down the data rows and at each change of value in column C increment a row counter for the results array . Dump the used part of the results using resize.
update - include headers
Sub mergeCategoryValues2()
Dim arr2 As Variant, arOut As Variant
Dim rowcount As Long, colcount As Long
Dim i As Long, k As Long, v
arr2 = ActiveSheet.ListObjects("APPLE").Range
rowcount = UBound(arr2, 1)
colcount = UBound(arr2, 2)
ReDim arOut(1 To rowcount, 1 To 3)
For i = 1 To rowcount
If arr2(i, 3) = v Then
arOut(k, 3) = arOut(k, 3) + arr2(i, 6)
Else
k = k + 1
v = arr2(i, 3) ' compare with following rows
arOut(k, 1) = arr2(i, 2)
arOut(k, 2) = arr2(i, 3)
arOut(k, 3) = arr2(i, 6)
End If
Next i
Sheet2.Range("A1").Resize(k, 3).Value2 = arOut
MsgBox "OK"
End Sub

How to get code to correctly count items (a variable) from one spreadsheet and successfully display this information?

I need my VBA code to count all the "x's" on a certain spreadsheet(pc) and then transfer this information to a report (rp) I am creating to display all the individuals choices. At the moment the code identifies all the ID on the sheet but however only acknowledges the first 4 options for each individual, where as some have much more than this. Throughout the course of this code I have made edits to options from Column K to Y and I assume this is the reason why the code is only acknowledging the options that haven't been altered. I have made adaptions to the code but have no idea how to correct this so that all options are successfully displayed.
Any help would be greatly appreciated!
Specific Spreadsheet Code will Read from
Code Report Results
rp.Cells(1, 1) = "Modules"
rp.Cells(1, 2) = "Student Count"
rp.Cells(1, 4) = "Students registered"
rp.Cells(1, 10) = "Students registered2" 'new
nRow = 2
For c = 2 To pc.Cells(1, Columns.Count).End(xlToLeft).Column
rp.Cells(nRow, 1) = pc.Cells(1, c)
rp.Cells(nRow, 2) = WorksheetFunction.CountIf(pc.Columns(c), "x")
nRow = nRow + 1
Next c
rp.Cells(1, 1).CurrentRegion.Borders.LineStyle = xlContinuous
If rp.Cells(2, 4).Text <> "" Then
rp.Cells(1, 4).CurrentRegion.Borders.LineStyle = xlContinuous
End If
rp.Rows(1).Font.Bold = True
rp.UsedRange.Columns.AutoFit
Although your code snippet is not sufficient to determine the cause of your problem you would definitely gain by not interacting with the sheet when manipulating data. consider the example hereunder as an alternative approach:
Option Explicit
Sub consolidate()
Dim arr, arrH
With Sheet1
arr = .Range("A1").CurrentRegion.Offset(1, 0).Value2 'get all data in memory
arrH = .Range(.Cells(1, 1), .Cells(1, UBound(arr, 2))).Value2 'get the header in an array
End With
Dim j As Long, i As Long, ii As Long: ii = 1
Dim arrC: ReDim arrC(1 To 1, 1 To UBound(arrH, 2)) '=> setup counter array
Dim arr2: ReDim arr2(1 To UBound(arr), 1 To UBound(arr, 2)) '=> setup new array to modify source data
For j = 1 To UBound(arr) 'traverse rows
For i = 1 To UBound(arr, 2) 'traverse columns
'here we can access each cell by referencing our array(<rowCounter>, <columnCounter>
'e.g. arr(j,i) => if j = 1 and i = 1 we'll have the values of Cell A1
'we can dump these values anywhere in the activesheet, other sheet, other workbook, ..
'but to limit the number of interactions with our sheet object we can also use an intermediant arrays
If arr(j, i) <> "" Then 'check if x
arr2(j, ii) = arrH(1, i) 'replace x with the value from the header
arr2(j, 1) = arr(j, 1) 'force the value in col1
ii = ii + 1 'increment consolidated counter
arrC(1, i) = arrC(1, i) + 1 'increment sum
End If
Next i
ii = 1 'reset consolidated counter for next line
Next j
'when we are ready with our data we dumb to the sheet
With Sheet2 'the with allows us the re-use the sheet name without typing it again
'the ubound function allows us to size the "range" to the same size as our array, once that's done we can just dumb it to the sheet
.Range(.Cells(1, 1), .Cells(UBound(arrH, 2), 1)).Value2 = Application.WorksheetFunction.Transpose(arrH) 'transpose to get the col's in rows
.Range(.Cells(1, 2), .Cells(UBound(arrC, 2), 2)).Value2 = Application.WorksheetFunction.Transpose(arrC)
.Range(.Cells(1, 4), .Cells(UBound(arr2), UBound(arr2, 2) + 3)).Value2 = arr2
End With
End Sub

How I can make a loop to divide each cell 3 times?

I have downloaded the USA Gross Domestic Product, but this is originally by trimester and I need it my month, thus, I want to divide each cell of GDP / 3 to make my time series longer and be able to plot it by month: I want to create a loop in VBA to divide each value of GDP list 3 times, and then put it below each new value calculated:
Sub PIB()
Set lista = Range("D13:D275")
For Each cell In lista
For i = 1 To 3
Range("E13").Offset(i - 1, 0).Value = cell.Value / 3
Next i
Next cell
End Sub
Nonetheless, when I run it, it divides properly but just over the 3 first cells:
I want to effectively divide each cell 3 times and put it each value below each other, how I can do it?
Is this what you are trying:
Sub PIB()
Dim arr As Variant: arr = Range("D13:D275").Value
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
Dim x As Long
For x = LBound(arr) To UBound(arr)
For i = 1 To 3
dict.Add Join(Array(arr(x, 1), x, i), "|"), arr(x, 1) / 3
Next i
Next
Range("E13").Resize(dict.Count).Value = Application.Transpose(dict.Items)
End Sub
Be aware of the non-explicit Range references.
If you need to split each value on three rows, please use the next code:
Sub SplitOnThreeMonths()
Dim sh As Worksheet, arr As Variant, arrFin As Variant, lastR As Long
Dim i As Long, j As Long, k As Long
Set sh = ActiveSheet
lastR = sh.Range("D" & Rows.count).End(xlUp).row
arr = sh.Range("D13:D" & lastR).Value
ReDim arrFin(1 To UBound(arr) * 3, 1 To 1)
k = 1
For i = 1 To UBound(arr)
For j = 1 To 3
arrFin(k, 1) = arr(i, 1) / 3: k = k + 1
Next j
Next i
sh.Range("E13").Resize(UBound(arrFin, 1), UBound(arrFin, 2)).Value = arrFin
End Sub
It will also allow the same Gross Domestic Product, even if happening that is not very probable. But, who knows?
I'm assuming you need to insert 2 rows to make space for the divided value three times -
however if you don't need to preserve data positions in the adjacent columns, the other answers provided using arrays are better than this one:
Sub PIB()
For i = 275 To 13 Step -1
Rows(i + 1 & ":" & i + 2).Insert Shift:=xlDown, CopyOrigin:=xlFormatFromLeftOrAbove
Range(Cells(i, "E"), Cells(i + 2, "E")).Value2 = Cells(i, "D").Value2
Next i
End Sub
Please note I haven't qualified your ranges because I don't know what you want them to be, guessing it's the current sheet you're on.

Paste a range over existing data without deleting it

Hope I'm in the right place.
I have a spreadsheet which is around 8000 rows long and I need to paste a column of data from J to E. Problem is that E already has some data in it which I want to retain. The data in J is also partial and needs to be pasted into blank cells in E.
The result would be a complete list of data in E which is a combination of E's original data and the pasted data from J.
Thanks
Place the following routine in a standard code module and run it with the worksheet active:
Public Sub excelhero()
Dim e&, j&, i&, v, vE, vJ
With ActiveSheet
e = .Cells(.Rows.Count, "e").End(xlUp).Row
j = .Cells(.Rows.Count, "j").End(xlUp).Row
End With
ReDim vE(1 To e, 1 To 1)
ReDim vJ(1 To j, 1 To 1)
ReDim v(1 To Application.max(e, j), 1 To 1)
vE = [e1].Resize(UBound(vE))
vJ = [j1].Resize(UBound(vJ))
For i = 1 To UBound(vE)
v(i, 1) = vE(i, 1)
Next
For i = 1 To UBound(vJ)
If Len(vJ(i, 1)) Then
v(i, 1) = vJ(i, 1)
End If
Next
[e1].Resize(UBound(v)) = v
End Sub
The above will work for your specific columns. Here is a more generic version that will work for merging any two columns. It will also work for your scenario because columns E and J are set to be the columns it works with at the top. COLA_A values will not be overwritten, only COL_A empty cells will be.
Public Sub MergeColumns()
Const COL_A = "E" '<-- COL_A has priority.
Const COL_B = "J"
Dim cola&, colb&, i&, v, vA, vB
With ActiveSheet
cola = .Cells(.Rows.Count, COL_A).End(xlUp).Row
colb = .Cells(.Rows.Count, COL_B).End(xlUp).Row
End With
ReDim vA(1 To cola, 1 To 1)
ReDim vB(1 To colb, 1 To 1)
ReDim v(1 To Application.max(cola, colb), 1 To 1)
vA = Range(COL_A & 1).Resize(UBound(vA))
vB = Range(COL_B & 1).Resize(UBound(vB))
For i = 1 To UBound(vA)
v(i, 1) = vA(i, 1)
Next
For i = 1 To UBound(vB)
If Len(vB(i, 1)) Then
v(i, 1) = vB(i, 1)
End If
Next
Range(COL_A & 1).Resize(UBound(v)) = v
End Sub

Resources