How do you loop through multi array using VBA? - arrays

I have the below table in Excel (TABLE).
I am trying to cycle through the table and story in an array (CODE).
Then cycle through the array and produce a unique output based on ID (OUTPUT).
I have provided the code I have but am having trouble determine the best way to loop through the array where the ID is the same - ie I want to group array outputs by ID.
TABLE
| ID | Name | Value |
---------------------
| 01 | John | Value |
| 01 | Sam | Value |
| 02 | Luke | Value |
| 03 | Jack | Value |
| 04 | Rob | Value |
| 04 | Bob | Value |
OUTPUT
01 - John, Sam
02 - Luke
03 - Jack
04 - Rob, Bob
CODE
'Store Array
For row = 2 to 6
MyArray(i,0) = Cells(row,1).value
MyArray(i,1) = Cells(row,2).value
MyArray(i,2) = Cells(row,3).calue
next row
'Output Array
For a = Lbound(MyArray) to Ubound(MyArray)
???
Next a
I do not know whether I use if/then/else statements or another loop to achieve this?

Say we begin with:
and we want an output as in your post. Running this:
Sub Macro1()
Range("A2:A22").Copy Range("E1")
ActiveSheet.Range("$E$1:$E$21").RemoveDuplicates Columns:=1, Header:=xlNo
For Each r In Range("E1:E22")
v = r.Value
If v = "" Then Exit Sub
For Each rr In Range("A2:A22")
vv = rr.Value
If v = vv Then
If r.Offset(0, 1).Value = "" Then
r.Offset(0, 1).Value = rr.Offset(0, 1).Value
Else
r.Offset(0, 1).Value = r.Offset(0, 1).Value & "," & rr.Offset(0, 1).Value
End If
End If
Next rr
Next r
End Sub
will produce:

I'll post my version which uses Dictionary.
Sub Test()
Dim sh As Worksheet: Set sh = Sheets("Sheet1") ' I try to always be explicit
With sh
Dim lr As Long, RawArr
lr = .Range("A" & .Rows.Count).End(xlUp).Row
RawArr = .Range("A2:C" & lr) ' pass to array
End With
Dim i As Long, idkey As String, itm As String
' Use Dictionary to handle duplicates and concatenate values
With CreateObject("Scripting.Dictionary")
For i = LBound(RawArr, 1) To UBound(RawArr, 1)
idkey = RawArr(i, 1): itm = RawArr(i, 2)
If Not .Exists(idkey) Then
.Add idkey, idkey & " - " & itm
Else
.Item(idkey) = .Item(idkey) & ", " & itm
End If
Next
' Return values to worksheet
' Use below if you're working on small data set
' If not, replace below with a loop - also posted
sh.Range("E1:E" & .Count) = Application.Transpose(.Items)
End With
End Sub
Above is pretty straight forward with the output exactly as you described.
At the last part, we used Application.Transpose to transfer the values back to the worksheet.
Take note that it has limitations as to how large it can handle like 65k rows.
As long as your data does not go near that value, then you should be ok.
If however you have a lot of data, then you'll have to use another loop to get the values (like manually transposing your data).
Dim key, fArr, n As Long: n = 1
ReDim fArr(1 To .Count, 1 To 2) ' use a 2D array
For Each key In .Keys
fArr(n, 1) = .Item(key)
n = n + 1
Next
sh.Range("E1:E" & .Count) = fArr
Note: I assumed that your ID's are strings (e.g.01) and not numbers formatted as "00". If that is the case, then you'll need to format it first before you use it as idkey like below to get your desired output.
idkey = Format(RawArr(i, 1), "00")

Related

Paste Array to Range in Excel VBA

I am trying to build an array containing ranges using a loop so I can paste it all together to earn some time.
The loop pastes stock name from column L to another sheet. Then, dynamic values generate in range (CA59:CQ59).
I tried transpose function just in case, double loop for manual 2d array,and simpler ranges without luck. My best shot was something like
Sheets("Stocks | Sort").Range("O2:O" & UBound(pool) + 1) = WorksheetFunction.Transpose(pool)
returned one column. Apologize for my lack of knowledge. Any ideas would be highly appreciated. Here is my code :
Sub ForecastAll()
Dim line As Range
Dim pool() As Variant
Dim i As Long
For i = 2 To 133
Sheets("Stocks | Sort").Range("L" & i).Copy (Sheets("Stocks | Synopsis").Range("E3"))
ReDim Preserve pool(i)
pool(i) = Sheets("Stocks | Synopsis").Range("CA59:CQ59").Value
Next i
Sheets("Stocks | Sort").Range("O2:AE133").ClearContents
Sheets("Stocks | Sort").Range("O2:AE133").Value = pool
End Sub
It returns blanks. Thank you for your time in advance.
Made a few changes in your code with explanation. Array populated from Worksheet column or row or table range values is a two dimensional array.
Sub ForecastAll()
Dim pool() As Variant
Dim i As Long
'clearing target range first to fill it with the loop below
Sheets("Stocks | Sort").Range("O2:AE133").ClearContents
For i = 2 To 133
Sheets("Stocks | Sort").Range("L" & i).Copy (Sheets("Stocks | Synopsis").Range("E3"))
ReDim Preserve pool(i)
'pool(i) = Sheets("Stocks | Synopsis").Range("CA59:CQ59").Value
'pool is one dimensional array where in at each iteration you are entering _
'a two dimensional array. Which can be ensured with --
'Debug.Print UBound(pool), Join(Application.Index(pool(i), 1, 0), ",")
'instead of using an array you can directly assign values of one range to another like --
Sheets("Stocks | Sort").Range("O2:AE" & i).Value = Sheets("Stocks | Synopsis").Range("CA59:CQ59").Value
'This method should also work fast as it is not selecting ranges and copying-pasting values _
'If you still want to use array, it should be a 2D array and it can not be populated directly _
'from range and it will need a loop to populate that array
Next i
'Sheets("Stocks | Sort").Range("O2:AE133").ClearContents
'Sheets("Stocks | Sort").Range("O2:AE133").Value = pool
End Sub

Excel VBA Compare Array

I am new to VBA and I tried to put the two tables into arrays. One is master and one is the source. I want to compare the two arrays and bring over the price from the source array to master array. Leave the cells blank if the variables are not the same. Please help, I need some tips/advice.
Sub createarray()
Dim masterarray(11, 3) As Variant
Dim sourcearray(25, 3) As Variant
For i = 1 To 25
sourcearray(i, 1) = Range("H" & i + 2)
sourcearray(i, 2) = Range("I" & i + 2)
sourcearray(i, 3) = Range("J" & i + 2)
Debug.Print sourcearray(i, 1); sourcearray(i, 2); sourcearray(i, 3)
Next
For i = 1 To 11
masterarray(i, 1) = Range("D" & i + 2)
masterarray(i, 2) = Range("E" & i + 2)
masterarray(i, 3) = Range("F" & i + 2)
Debug.Print masterarray(i, 1); masterarray(i, 2); masterarray(i, 3)
Next
End Sub
Why don't you use vlookup and Concat functions instead of VBA? Your code need to improve several lines to work like: define better the arrays and nest the for loops, and create a conditional If to compare both tables
'Note: This is a code example no exactly what you need
Sub createarray()
Dim i, j as integers
Dim wb As Workbook
Dim masterarray As Range: Set wb.worksheet("sheet1").Range("D3:F12")
Dim sourcearray As Range: Set wb..worksheet("sheet1").Range("H3:J26")
'You'll need to concat Cost Centry and Variables
For i = 1 To 11
For j = 1 to 25
If masterarray(i, 2).value = sourcearray(j, 2).value then
masterarray(i, 3).value = sourcearray(j, 3).value
Else
masterarray(i, 3).value = ""
End If
Next j
Next i
End Sub
or option two
Use Concat to merge "Cost Centre" & "Variables" in "C3" cell as: =$D$3&E3 and fill down you will get something like this:
|104Enhacement |
|104IT Operations|
|... |
in "G3" do the same =$H$3&I3 and fill down (be careful when "Call Centre" code change) you will get something like this:
|106Enhacement |
|106IT Operations|
|... |
|104Enhacement |
|104IT Shared Services|
|... |
then in "F3" cell use =iferror(vlookup("C3","$G$3:$J$26",4,0),"")
Because you are using "Format as table" probably the function change a little bit but it is not a big issue.

Filter on one column, return all related results for a different column

I've been trying to think of how to do this and can't quite figure it out. The premise is as follows. I need to filter on COLUMN B, following that I need to use the filtered returned results of COLUMN A to filter all of the results. If that's confusing I tried to 'draw' it out below...
COL A | COL B |
cat | 44 |
cat | 476 |
cat | 19 |
dog | 11 |
dog | 12 |
bird | 44 |
bird | 99 |
bird | 4556 |
So if I filter on Column B for 44 I'll only get the two '44' rows returned (cat | 44 and bird | 44). Instead, I want to somehow get all of the cat rows and all of the bird rows, since 44 was associated with those two 'A' types.
COL A | COL B |
cat | 44 |
cat | 476 |
cat | 19 |
bird | 44 |
bird | 99 |
bird | 4556 |
Have any of you done this before? My idea was initially to copy the unaltered sheet over to a new sheet 2, apply the filter and copy the returned column A results to a new sheet 3, use the returned column A results in sheet 3 to do an autofilter on the sheet2 column A. However, there can be hundreds of filters and this is a pretty manual process in VBA.
I'd be happy to add more detail if needed.
You could avoid the AutoFilter method and Range.SpecialCells method with one or more variant arrays.
Option Explicit
Sub cats_and_birds()
Dim crit As Long
Dim i As Long, j As Long, iCols As Long
Dim arr1 As Variant
Static dict As Object '<~~ faster second time around this way
'create and configure the static dictionary
If dict Is Nothing Then _
Set dict = CreateObject("Scripting.Dictionary")
dict.RemoveAll
dict.CompareMode = vbTextCompare
'number of columns to transfer from column A
iCols = 3
'set filter criteria for column 2 within range
crit = 44
With Worksheets("Sheet1")
'assign raw values
arr1 = .Range(.Cells(2, 1), Cells(.Rows.Count, iCols).End(xlUp)).Value2
'show the data array limits in the Immediate window
'delete this or comment it after the routine works
Debug.Print LBound(arr1, 1) & " to " & UBound(arr1, 1)
Debug.Print LBound(arr1, 2) & " to " & UBound(arr1, 2)
'iterate through the 'rows' of the array and compare column 2
For i = LBound(arr1, 1) To UBound(arr1, 1)
'add/oversrite the pet species as key
If arr1(i, 2) = crit Then _
dict(arr1(i, 1)) = 0
'if pet species in key, transfer information
If dict.exists(arr1(i, 1)) Then
'iterate through the columns backwards to maintain row
For j = UBound(arr1, 2) To LBound(arr1, 2) Step -1
.Cells(.Rows.Count, "Z").End(xlUp).Offset(1, j - 1) = arr1(i, j)
Next j
End If
Next i
End With
End Sub
This would actually be better with two arrays; the second receiving the results and then bulk transferring the information but there is also a (smaller) penalty due to redim'ming the array with preserve (and transposing). For small (<10K) this is probably only marginally longer to process. For <100 rows of filtered information you might not be able to measure the difference without special tools.
Dim'ming the dictionary object as Static shortens the load time the second time around because you do not have to recreate the object. While I prefer to add the Microsoft Scripting Runtime to the Tools ► References and use dim dict as new scripting.dictionary not everyone prefers that so I'll post this with CreateObject and dim dict as static.
You can take this routine as a model: it applies to Sheet1, looks for 44 in column B then shows the rows that match from column A
Sub filterBthenA()
Sheet1.UsedRange.Columns("B").AutoFilter 1, 44 '<-- Filter Sheet1 col B by value 44
Dim cel As Range, dict As Object: Set dict = CreateObject("Scripting.Dictionary")
For Each cel In Sheet1.UsedRange.Columns("A").SpecialCells(xlCellTypeVisible)
dict(cel.Value) = 0
Next
Sheet1.AutoFilterMode = False
Sheet1.UsedRange.Columns("A").AutoFilter 1, dict.Keys, xlFilterValues
End Sub

Turn Excel range into VBA string

I would like to turn values in given range into VBA string where original cell values are separated by any chosen column delimiter and row delimiter. Delimiters could be one character or longer strings. The row delimiter is the string at the end of the line. The string should be done just as we read text from left top corner, from left to right, to bottom right corner.
Here is an example of the VALUES in range A1:C5:
+----+----+----+
| A1 | B1 | C1 |
+----+----+----+
| A2 | B2 | C2 |
+----+----+----+
| A3 | B3 | C3 |
+----+----+----+
| A4 | B4 | C4 |
+----+----+----+
| A5 | B5 | C5 |
+----+----+----+
Desired results is a VBA string:
A1,B1,C1#$A$2,$B$2,$C$2#A3,B3,C3#A4,B4,C4#A5,B5,C5#
For the sake of readability I will show it like this:
A1,B1,C1#
A2,B2,C2#
A3,B3,C3#
A4,B4,C4#
A5,B5,C5#
As a column delimiter I have chosen , (comma), and as a row delimiter # sign. Of course these could be any characters like \r\n.
The reason why I want fast cooking of the string from range is because I want to to send it to SQL Server through ADO connection. As I have tested so far it is the fastest way to transfer lots of data on the fly. The twin question how to split this string on SQL Server is here: Split string into table given row delimiter and column delimiter in SQL server
Solution 1. Loop through all rows and columns. Question is if there be any more elegant way then just looping through all rows and columns? I would prefer VBA solution, not formula one.
Solution 2. Suggested by Mat's Mug in comment. CSV file is desired results. I would like to do it on the fly without saving. But good point - imitate CSV is what I want but I want it without saving.
Edit after bounty
Answer of Thomas Inzina works crazy fast and his solution is portable. Ordinary VBA loop turned out to be way faster then worksheet functions like JOIN on large data sets. I do not recommend using worksheet functions in VBA for that purpose. I have voted up everybody. Thank you all.
To optimize performance my function emulates a String Builder.
Variables
Text: A very large string to hold the data
CELLLENGTH: A contant that determines the size of the BufferSize
BufferSize: The initial size of Text string
Data(): An Array derived from the source range
As the rows and columns of the Data() array are iterated over the current element (Data(x, y)) value replaces a portion of the Text string. The text string is resized as needed. This reduces the number of concatenations immensely. The initial BufferSize is set pretty high. I got my best results, 0.8632813 Second(s), by reducing CELLLENGTH to 25.
Download Sample Data from Sample-Videos.com
Results
Code
Function getRangeText(Source As Range, Optional rowDelimiter As String = "#", Optional ColumnDelimiter As String = ",")
Const CELLLENGTH = 255
Dim Data()
Dim text As String
Dim BufferSize As Double, length As Double, x As Long, y As Long
BufferSize = CELLLENGTH * Source.Cells.Count
text = Space(BufferSize)
Data = Source.Value
For x = 1 To UBound(Data, 1)
If x > 1 Then
Mid(text, length + 1, Len(rowDelimiter)) = rowDelimiter
length = length + Len(rowDelimiter)
End If
For y = 1 To UBound(Data, 2)
If length + Len(Data(x, y)) + 2 > Len(text) Then text = text & Space(CDbl(BufferSize / 4))
If y > 1 Then
Mid(text, length + 1, Len(ColumnDelimiter)) = ColumnDelimiter
length = length + Len(ColumnDelimiter))
End If
Mid(text, length + 1, Len(Data(x, y))) = Data(x, y)
length = length + Len(Data(x, y))
Next
Next
getRangeText = Left(text, length) & rowDelimiter
End Function
Test
Sub TestGetRangeText()
Dim s As String
Dim Start: Start = Timer
s = getRangeText(ActiveSheet.UsedRange)
Debug.Print "Execution Time: "; Timer - Start; "Second(s)"
Debug.Print "Rows: "; ActiveSheet.UsedRange.Rows.Count; "Columns: "; ActiveSheet.UsedRange.Columns.Count
Debug.Print "Result Length: "; Format(Len(s), "#,###")
End Sub
Here's a quick way to test (Note: this will only work with Excel 2016 (or if you have the TextJoin() function).
First, in the empty column D, do =C1&"#", so you get your last column filled with the cell+#
Then, say in cell E1, =TEXTJOIN(",",TRUE,A1:C5)
(Note: TRUE there means to skip blanks. If you have blanks, and want to keep them, change that to FALSE).
THen, on that cell, run
=Substitute(E1,"#,","#")
Or combine the formulas into one: =SUBSTITUTE(TEXTJOIN(",",TRUE,A1:C4),"#,","#").
If you need vba, just throw the formula into a VBA macro and run like that.
Here is a UDF that returns the desired output:
EDIT Changed to add EOL at the end.
Option Explicit
Function MultiJoin(Rng As Range, Delimiter As String, EOL As String) As String
Dim V As Variant, W As Variant
Dim COL As Collection
Dim I As Long, J As Long
V = Rng
Set COL = New Collection
ReDim W(1 To UBound(V, 2))
For I = 1 To UBound(V, 1)
For J = 1 To UBound(V, 2)
W(J) = V(I, J)
Next J
COL.Add W
Next I
ReDim V(1 To COL.Count)
For I = 1 To COL.Count
V(I) = Join(COL(I), Delimiter)
Next I
W = Join(V, EOL)
MultiJoin = W & EOL
End Function
One could shorten the code by using WorksheetFunctions, but I would guess execution time would be slower.
Shortened Code
Option Explicit
Function MultiJoin(Rng As Range, Delimiter As String, EOL As String) As String
Dim V As Variant, W As Variant
Dim I As Long, J As Long
V = Rng
With WorksheetFunction
For I = 1 To UBound(V, 1)
V(I, 1) = Join(.Index(V, I, 0), Delimiter)
Next I
MultiJoin = Join(.Transpose(.Index(V, 0, 1)), EOL) & EOL
End With
End Function
This solution will require either a reference to the Microsoft Forms 2.0 Object Library in your project or some other way of fetching the contents of the clipboard (like through an API call).
Function TurnExcelRangeIntoVBAString(Optional cellDelimiter As String = ",", _
Optional rowDelimiter As String = "#") _
As String
Dim rng As Range
Set rng = ActiveSheet.UsedRange
rng.Copy
Dim clip As New MSForms.DataObject
Dim txt As String
clip.GetFromClipboard
txt = clip.GetText()
txt = Replace(Replace(txt, vbTab, cellDelimiter), vbCrLf, rowDelimiter)
TurnExcelRangeIntoVBAString = txt
End Function
you could try this
Option Explicit
Sub main()
Dim strng As String
Dim cell As Range
With Worksheets("TurnRangeIntoString") '<--| change "TurnRangeIntoString" to your actual worksheet name
For Each cell In Intersect(.UsedRange, .Columns(1)) '<--| loop through its column 1 cells
strng = strng & Join(Application.Transpose(Application.Transpose(.Range(cell, cell.End(xlToRight)).value)), ",") & "#" '<--| build string
Next cell
End With
MsgBox strng
End Sub
Sub aquatique()
dim a(),s$,i&,j&:a=selection.value
for i=1 to ubound(a)
for j=1 to ubound(a,2)
if j=1 then
if i=1 then
s= a(i,j)
else
s=s &"#" & vbnewline & a(i,j)
end if
else
s=s &";" & a(i,j)
end if
next
next
end sub
simple but does the job. Slow on huge ranges, you'd need to use "join"
How about this?:
Sub Concatenate()
Dim Cel As Range, Rng As Range
Dim sString As String, r As Long, c As Long, r2 As Long
Set Rng = Selection
r = Selection.Row
c = Selection.Column
r2 = Selection.Row
For Each Cel In Rng
r = Cel.Row
If sString = "" Then
sString = Cel.Value
Else
If r <> r2 Then sString = sString & "#" & Cel.Value
If r = r2 Then sString = sString & "," & Cel.Value
End If
r2 = Cel.Row
Next
sString = sString & "#"
Debug.Print sString
End Sub

Calculatin within an Array

I'm a little bit familiar with VBA Excel 2013. My problem now is, that I need to sum the values where specific Strings are available.
I need to calculate within a column only the values where a lookup is true.
My table looks like:
9 | AD,DCO,PD
5 | AD
5 | PD
15 | PD
So the sum for AD would be 14 or DCO would be 9 or PD would be 29 an therefore the result table have to look like this:
AD | DCO | PD
—–—–—–—–—–––—–
14 | 9 | 29
Can anybody help me?
Forget VBA. Sumproduct solves this perfectly.
I'm assuming that your commas separate cells, so Column A has the numbers, then columns B-D have the letters. If they do not, you can always use the Text to Columns function in excel (look under the Data tab) to make this so.
Put the following function where you wnat the answer (say, B10):
=SUMPRODUCT(($A$1:$A$4) * ($B$1:$D$4 = $A$10))
You can then type in cell A10 the code letter and it will sum up column A, where columns B-D contain that text.
If you want to display all text options, you can make a list of them (say A10 = AD, A11 = DCO, A12 = PD), then in B10 put the same formula as above but remove the $ from in front of the number (so it reads $A10). You can then drag it down and it will tally up for each of the values in A10-A13 and display the results in B10 - B13.
If I have correctly interpreted your sample data into a pseudo-table then the native SUMIF function will conditionally sum the numbers in column A using a wildcard criteria against column B. Example in D2 (as the image below) would be =SUMIF($B$1:$B$4,"*"&D$1&"*",$A$1:$A$4).
             
Fill or copy right as necessary. The caution is that wildcards may make false positives when the criteria closely resembles other values. This is not the case with your sample data.
Problem Solved, but do you have any suggestion to speed up the code?
Sub Calculate()
Dim Dict As Object
Dim i As Long
Dim e As Long
Set Dict = CreateObject("Scripting.Dictionary")
Dim b, m As Long
Dim a As Long
Dim c As Variant
Dim j As Long
Dim sum_Dict As Variant
Dim column_Dict As Variant
i = 1
e = 16
a = i
a = a - 2
For i = i To e
For Each sum_Dict In Range("A" & i & ":" & "A" & e) 'Spalte V = Price
If sum_Dict = "" Then
i = i + 1
Else
For Each c In Range("B" & i & ":F" & i) 'All Columns from B til F
' iterating over the array
For j = LBound(column_Dict) To UBound(column_Dict)
' assigning the separated values to columns
b = CDbl(sum_Dict) 'convert to double
c = CStr(c) 'convert to string
If Dict.Exists(c) Then 'check if key exists
Dict.Item(c) = Dict.Item(c) + b 'if key exists sum the items
Else 'if the key does not exist create it
Dict.Add Key:=c, Item:=b
End If
Next j
Next c
i = i + 1
'extract keys into variant array
'Debug.Print "Array of Keys and items"
For m = 0 To Dict.Count - 1
'Debug.Print Dict.Keys()(m), Dict.Items()(m)
Next m
'Debug.Print Dict.Count & " Items in Dictionary"
End If
Next sum_Dict
Next
'-- output to sheet using first 1D Array
Range("H" & a + 2).Resize(1, _
UBound(Application.Transpose(Dict.Keys()))) = Dict.Keys()
'-- output to sheet using dictionary
Range("H" & a + 3).Resize(1, _
UBound(Application.Transpose(Dict.Items()))) = Dict.Items()
Dict.RemoveAll ' empty dictionary
End Sub

Resources