How to get index of Element in two dimensional array? - arrays

I have an array with 2 dimensions.
I also have a For Each loops which loops with elements of these arrays.
How can i get a Index of vElement/vElement2 in the moment of my comment here in code?
I would be very, very thankful if You can help me.
For Each vElement In Table1
For Each vElement2 In Table2
If ws_1.Cells(1, c) = vElement Then
For Row = 3 To lastRow
amountValue = amountValue + ws_1.Cells(Row, c).value
ws_2.Cells(row2, colIlosc) = amountValue
'Here i would love to have index of vElement for example. In my head it would be something like... Index(vElement) or Index(Table1(vElement))
ws_2.Cells(row2, columncodeprod) = vElement2
row2 = row2 + 1
amountValue = 0
Next Row
End If
Next vElement2
Next vElement

Show Indices of an element in a 2-dim Array - the complicated way
If I understand correctly, you are looping through a datafield array via a ►For Each construction and want to get the current row/column index pair of that same array.
In order to answer your question
"How to get indices of an element in a two dimensional array",
I leave aside that you would get these automatically in a more evident and usual way if you changed the logic by looping through array rows first and inside this loop eventually through array columns - see Addendum *).
To allow a reconstruction of e.g. the 6th array element in the example call below as referring to the current index pair (element i=6 ~> table1(3,2) ~> row:=3/column:=2) it would be necessary
to add an element counter i by incrementing its value by +1 each time you get the next element and
to pass this counter as argument (additionally to a reference to the datafield) to a help function getIndex()
returning results as another array, i.e. an array consisting only of two values: (1) the current array row, (2) the current array column:
Example call
Note: For better readibility and in order to condense the answer to the mimimum needed (c.f. MCVE) the following example call executes only one For Each loop over the table1 datafield array; you will be in the position to change this to your needs or to ask another question.
Option Explicit ' declaration head of your code module
Sub ShowIndicesOf2DimArray()
Dim table1 ' declare variant 1-based 2-dim datafield
table1 = Sheet1.Range("A2:B4") ' << change to sheets Code(Name)
Dim vElem, i As Long
Dim curRow As Long, curCol As Long ' current row/column number
For Each vElem In table1
i = i + 1 ' increment element counter
curRow = getIndex(table1, i)(1) ' <~ get row index via help function
curCol = getIndex(table1, i)(2) ' <~ get col index via help function
'optional debug info in VB Editors immediate window (here: Direktbereich)
Debug.Print i & ". " & _
" Table1(" & curRow & "," & curCol & ") = " & vElem & vbTab;
Debug.Print ", where curRow|curCol are " & Join(getIndex(table1, i), "|")
Next vElem
End Sub
Help function getIndex() called by above procedure
Function getIndex(table1, ByVal no As Long) As Variant
'Purpose: get 1-based 1-dim array with current row+column indices
ReDim tmp(1 To 2)
tmp(1) = (no - 1) Mod UBound(table1) + 1
tmp(2) = Int((no - 1) / UBound(table1) + 1)
getIndex = tmp
End Function
*) Addendum - "the simple way"
Just the other way round using row and column variables r and c as mentioned above; allows to refer to an item simply via table1(r,c) :
Sub TheSimpleWay()
Dim table1 ' declare variant 1-based 2-dim datafield
table1 = Sheet1.Range("A2:B4") ' << change to sheets Code(Name)
Dim vElem, i As Long
Dim r As Long, c As Long ' row and column counter
For r = 1 To UBound(table1) ' start by row 1 (1-based!) up to upper boundary in 1st dimension
For c = 1 To UBound(table1, 2) ' start by col 1 (1-based!) up to upper boundary in 2nd dimension
i = i + 1
Debug.Print i & ". " & _
" Table1(" & r & "," & c & ") = " & table1(r, c) & vbTab;
Debug.Print ", where row|col are " & r & "|" & c
Next c
Next r
End Sub

There is NO index in the case you put in discussion...
vElement and vElement2 variables are of the Variant type. They are not objects, to have an Index property.
When you use a For Each vElement In Table1 loop, VBA starts from the array first element, goes down up to the last row and then do the same for the next column.
When you need to know what you name arrays 'indexes' you must use For i = 1 To Ubound(Table1, 1) followed by For j = 1 To Ubound(Table1, 2). In such a case you will know the matching array element row and columns. We can consider them your pseudo-indexes...
If you really want/insist to extract such indexes in an iteration of type For Each vElement In Table1, you must build them. I will try en elocvent code example:
Sub testElemIndex()
Dim sh As Worksheet, Table1 As Variant, vElement As Variant
Dim i As Long, indexRow As Long, indexCol
Set sh = ActiveSheet
sh.Range("C6").value = "TestIndex"
Table1 = sh.Range("A1:E10").value
For Each vElement In Table1
i = i + 1:
If vElement = "TestIndex" Then
If i <= UBound(Table1, 1) Then
indexRow = i: indexCol = 1
Else
indexCol = Int(i / UBound(Table1, 1)) + 1
indexRow = i - Int(i / UBound(Table1, 1)) * UBound(Table1, 1)
End If
Debug.Print Table1(indexRow, indexCol), indexRow, indexCol: Stop
End If
Next
End Sub
You can calculate the rows and columns of the array element. And the code proves that using them, the returned array value is exactly the found one...
Is it a little more light on the array 'indexes'...?

Dim Table1() As Variant
Dim Table2() As Variant
Table1 = Range(Cells(2, 3), Cells(lastRow, vMaxCol))
Table2 = Range(Cells(2, 1), Cells(lastRow, 1))
Table1 is Variant(1 to 33, 1 to 9)
Table2 is Variant(1 to 33, 1 to 1)
This 33 and 9 is dynamic.

Related

Excel VBA compare values on multiple rows and execute additional code

I have the following task:
There are fields in my document, the combination of which needs to be compared, and if they are the same, another field on the same rows need to be updated.
So far, I add the values in arrays (skipping the first row as header, thus iNum = 2) with select statements per column and concatenate them per row for the comparison.
Dim conc As Range 'Concatenated fields
Dim iconc() As Variant
ReDim iconc(UBound(iMatn) - 1, 1)
For iNum = 2 To UBound(iMatn)
iconc(iNum - 1, 1) = iMatn(iNum, 1) & iVendr(iNum, 1) & iInd1(iNum, 1) & iInd2(iNum, 1) 'Current concatenation
Select Case iNum - 1
Case 2: 'Compare two records
If iconc(iNum - 2, 1) = iconc(iNum - 1, 1) Then 'Compare first and second records
'Execute code to update the two fields from Extra field column
End If
Case 3: 'Compare three records
If AllSame(iconc(iNum - 3, 1), iconc(iNum - 2, 1), iconc(iNum - 1, 1)) Then
'Execute code to update the three fields from Extra field column
End If
I go through each value of the concatenation and compare if its the same as the previous ones with Case statement (I don't expect more than 4 or 5 to be the same, even though there could be a couple hundred of lines).
Thus I face two issues:
If there are 3 equal values, for example, the code first jumps to the case for 2. How can I make it so that it skips to the maximum value?
It needs to resume checking after the rows that were already checked. E.g. if the first two are the same, the code should start checking from the third one; basically to start at from the line after the last of any duplicate ones that are located.
Example
Image: the code needs to return that there are 3 equal rows (lines 2 to 4), update the respective cells on the "Extra field" column, proceed further (from line 5), return that there are 2 equal rows (lines 6 and 7), update the same as above again, proceed further (from line 8) etc.
Any help will be highly appreciated as I am stuck with this problem.
Thank you all.
To determine how many are in each group, in order to decide how you will update the extra fields column, I would use a Dictionary & Collection object.
eg:
'Set reference to Microsoft Scripting Runtime
' (or use late-binding)
Option Explicit
Sub due()
Dim myDict As Dictionary, col As Collection
Dim i As Long, v As Variant
Dim sKey As String
Dim rTable As Range
Dim vTable As Variant, vResults As Variant
'there are more robust methods of selecting the table range
'depending on your actual layout
'And code will also make a difference if the original range includes
' or does not include the "Extra Field" Column
' Code below assumes it is NOT included in original data
Set rTable = ThisWorkbook.Worksheets("sheet2").Cells(1, 1).CurrentRegion
vTable = rTable
Set myDict = New Dictionary
For i = 2 To UBound(vTable)
sKey = vTable(i, 1) & vTable(i, 2) & vTable(i, 3) & vTable(i, 4)
Set col = New Collection
If Not myDict.Exists(sKey) Then
col.Add Item:=WorksheetFunction.Index(vTable, i, 0)
myDict.Add Key:=sKey, Item:=col
Else
myDict(sKey).Add Item:=WorksheetFunction.Index(vTable, i, 0)
End If
Next i
For Each v In myDict.Keys
Select Case myDict(v).Count
Case 2
Debug.Print v, "Do update for two rows"
Case 3
Debug.Print v, "Do update for three rows"
Case Else
Debug.Print v, "No update needed"
End Select
Next v
End Sub
=>
1234V22341212 Do update for three rows
1234v22351215 No update needed
2234v22361515 Do update for two rows
2234v22361311 No update needed
Although, I would probably use Power Query (available in Windows Excel 2010+ and 365) which can easily group by the four columns and return a count. You can then add a new column depending on that count.
Not knowing the nature of your updating Extra Field and the difference between what happens for 2, 3, 4, ... the same, it is not possible to supply any code for that purpose.
In general, you would
expand the array for each dictionary item
Add the extra column if it's not already there
Do the update
Add that to a pre-dimensioned results array
Write the results array back to the worksheet
Note that this method of working with VBA arrays will execute an order of magnitude faster than doing repeated worksheet accessing, but the code is longer.
If you use a look ahead to the next row (rather than look behind) you can determine the end of the group and process accordingly.
Option Explicit
Sub CompareRows()
Dim ws As Worksheet, ar, arExtra
Dim lastrow As Long, n As Long, i As Long, k As Long
Dim s As String, sNext As String, rng As Range, iColor As Long
Set ws = ThisWorkbook.Sheets("Sheet1")
With ws
lastrow = .Cells(.Rows.Count, "A").End(xlUp).Row
ar = .Range("A1:D1").Resize(lastrow)
n = 1
For i = 1 To UBound(ar)
' look ahead to next line
If i = UBound(ar) Then
sNext = ""
Else
sNext = ar(i + 1, 1) & "_" & ar(i + 1, 2) & _
"_" & ar(i + 1, 3) & "_" & ar(i + 1, 4)
End If
If i > 1 Then ' skip header
' increment count if matched
If sNext = s Then
n = n + 1
' process group using counter n
Else
If n >= 2 Then
' first row of group
Set rng = .Cells(i, "E").Offset(1 - n)
If n >= 3 Then
iColor = rgb(128, 255, 128) ' green
Else
iColor = rgb(255, 255, 128) ' yellow
End If
'code to update n rows in Extra column
rng.Resize(n).Value = n
rng.Offset(, -4).Resize(n, 5).Interior.color = iColor
k = k + 1 ' group count
End If
n = 1
End If
End If
s = sNext
Next
End With
MsgBox k & " groups found", vbInformation
End Sub

How to join returned values from named range separated by comma

I've spent hours trying to find out how to join returned values from a named range, but the result is a
run-time error 32 - Type mismatch.
As a newbie I'm still struggling with arrays, so maybe I've overlooked some detail. Thank you for helping me out.
Example: (B1)Benzine, (B2)Diesel, (B3)Hybride -> (E1)Gasoline, (E2)Diesel, (E3)Hybrid
This is the named range:
Another example (to be more clear):
Example 2: (B1)Benzine, (B3)Hybride -> (E1)Gasoline, (E3)Hybrid
Option Explicit
Sub splitter()
Dim i As Long
Dim w As Long
'Dim oWB As Workbook
Dim oWS As Worksheet
Dim oWS9 As Worksheet
Dim rngMOTOR As Range
Dim rngMOTOR2 As Range
Dim arrMOTOR() As Variant
Dim LastRow As Long
'Set oWB = Workbooks("BRONBESTAND.xlsm")
Set oWS = Sheets("ONDERDELEN")
Set oWS9 = Sheets("MOTOR") '5 columns: 1 Short & LONG + 1 NL + 3 Languages !!!!! WARNING
LastRow = oWS.Range("A" & Rows.Count).End(xlUp).Row
For i = 2 To LastRow 'Starting below headers
Set rngMOTOR = oWS.Cells(i, "M") 'MOTOR ...
Set rngMOTOR2 = oWS9.Range("MOTOR") 'MOTOR2: MOTOR - Bronbestand arrPOS = rngPOS2.Value
arrMOTOR = rngMOTOR2.Value
'*********
Dim txt As String
Dim j As Integer
Dim Splitted As Variant
Dim arrMOTORall As Variant
Dim arrMOTORsplit As Variant
Dim Motor3 As String
txt = oWS.Cells(i, "M") 'MOTOR ...
Debug.Print ("txt : ") & i & ": "; txt
If Not IsEmpty(txt) Then
Splitted = Split(txt, ", ")
For j = 0 To UBound(Splitted)
Cells(1, j + 1).Value = Splitted(j)
Debug.Print (" ---> Splitted: ") & Splitted(j)
'**** INSERT *****
For w = LBound(arrMOTOR) To UBound(arrMOTOR)
If arrMOTOR(w, 1) = Splitted(j) Then 'EX: B - Benzine
arrMOTORsplit = (arrMOTOR(w, 4)) '(arrMOTOR(y, 2)) -> 1=SHORT+LONG , 2=NL, 3=FR, 4=EN
Debug.Print (" ---> arrMOTORsplit: ") & i & ": " & arrMOTORsplit
'**** JOIN ****
arrMOTORall = Join(arrMOTORsplit, ", ")
Debug.Print ("arrMOTORall: ") & arrMOTORall
End If
Next w
Next j
End If
Next i
End Sub
Get comma separated strings for each column in named range
I didn't analyze your code, but this should work to receive the first three values joined
"Benzine, Diesel, Hybride" ' e.g. from first column
or
"Gasoline, Diesel, Hybrid" ' e.g. from the fourth column
from a named range "Motor" via the Application.Index function.
Notes
The parameter 0 in this Index function indicates to not choose a specific row, the Parameter ColNo chooses each of your columns in a Loop. A subsequent transposition allows to change the 2 dimensioned array values to a 1-dim array. The Join function needs a 1-dim array and concatenates the chosen column items therein.
Hint: The following sample code uses a fully qualified range reference assuming that you don't call the TestMe procedure from your Personal Macro Library. In the latter case you'd have to change references and workbook identification (not using ThisWorkbook!).
Example code
Option Explicit ' declaration head of your code module
Sub TestMe()
Dim v As Variant, ColNo As Long
' assign first three rows to variant 1-based 2-dim datafield array
v = ThisWorkbook.Worksheets("Motor").[Motor].Resize(3, 4) ' Named range value
' write comma separated list for each column
For ColNo = 1 To 4
Debug.Print Join(Application.Transpose(Application.Index(v, 0, ColNo)), ", ")
Next ColNo
End Sub
EDIT - Flexible Search in ANY ORDER to translate joined lists
This solution allows to return joined search words in any combination using the Application.Index function in an advanced way using row and column arrays as parameters. The main function getSplitters() creates a variant 2-dim array in only three steps without loops and redims and uses two language constants (Const DUTCH and Const ENGLISH).:
assigns data to variant 1-based 2-dim datafield array
gets only the selected rows based on comma separated string values
reduces the same array to Dutch and English columns
Calling Code
Due to your OP the calling code anylyzes all comma separated strings in Column M in your sheet "ONDERDELEN" as far as there are values in column A. This is made by passing these found string values to the main function getSplitters with an innovative approach to get results in only three steps without Loops (see function code below).
Translation is based on values in the named range Motor "B1:E4" in sheet "Motor" where rows comprise different sort of fuel with neighbouring columns for different languages (starting with Dutch in the first column and English in the fourth col).
Note that using VBA it is faster to loop through an array to get values than through a range.
Option Explicit ' declaration head of your code module
Const DUTCH As Integer = 1
Const ENGLISH As Integer = 4
Sub TranslateAnyFuelCombination()
' Purpose: returns comma separated lists in column "M" and translates from Dutch to English
' Example: "Benzine, Hybride, Diesel" (Dutch) gets to "Gasoline, Hybrid, Diesel" in English
Dim s As String
Dim oWS As Worksheet, i&, LastRow&, vMOTOR As Variant
Set oWS = Thisworkbook.Worksheets("ONDERDELEN") ' fully qualified reference
' Get last row of wanted data
LastRow = oWS.Range("A" & Rows.Count).End(xlUp).Row
vMOTOR = oWS.Range("M1:M" & LastRow)
For i = 2 To LastRow 'Starting below headers
Debug.Print getSplitters(vMOTOR(i, 1))
Next i
End Sub
Main function
Function getSplitters(ByVal sRows As String) As String
Dim i As Long, j As Long
Dim v As Variant, a As Variant
' [0] analyze selected rows string, e.g. "Benzine, Hybride, Diesel"
a = getRowAr(sRows) ' -> assign 1-dim Rows Array(1, 3, 2)
' [1] assign data to variant 1-based 2-dim datafield array
v = Application.Transpose(ThisWorkbook.Worksheets("Motor").[Motor]) ' Named range value
' [2] get only selected rows, e.g. 1st, 3rd and 2nd -> in free order (!) Benzine, Hybride, Diesel
v = Application.Transpose(Application.Index(v, _
Application.Evaluate("row(1:" & UBound(v, 2) & ")"), _
a)) ' transposed columns array = selected rows
' [3] reduce to Dutch and English columns
v = Application.Transpose(Application.Index(v, _
Application.Evaluate("row(1:" & (UBound(a) + 1) & ")"), _
Array(DUTCH, ENGLISH))) ' selected columns array (above array retransposed)
' [4] return concatenated strings
getSplitters = Join(Application.Transpose(Application.Transpose(Application.Index(v, 1, 0))), ", ") & " -> " & _
Join(Application.Transpose(Application.Transpose(Application.Index(v, 2, 0))), ", ")
End Function
Two helper functions
Function getRowAr(ByVal sList As String) As Variant
' Purpose: split comma separated list into 1-dim number array in FREE ORDER
' Example: "Benzine, Hybride, Diesel" -> Array(1, 3, 2)
Dim ar, i&
' change words in comma separated list to numbers
ar = Split(Replace(sList, " ", ""), ",")
For i = LBound(ar) To UBound(ar)
ar(i) = val(getNumber(ar(i))) ' change to numbers
Next i
getRowAr = ar ' return
End Function
Function getNumber(ByVal s As String) As Long
' Purpose: replaces dutch search words with corresponding row number
Dim arFuel
' get search words to 1-dim array
arFuel = Application.Index(ThisWorkbook.Worksheets("Motor").[Motor], 0, DUTCH)
' return corresponding number
getNumber = Application.Match(s, arFuel)
End Function
Addendum (Edit due to comment)
The above code works as intended if you are sure that the concatenated search words (or starting parts) actually match else an Error 13 is raised. You can solve this issue in two steps:
Insert an empty first row into your named range Motor (or fill it e.g. with ?, #N/A etc.)
Change the 2nd helper function as follows:
Edited function getNumber()
Function getNumber(ByVal s As String) As Long
' Purpose: replaces dutch search words with corresponding row number
Dim arFuel
' get search words to 1-dim array
arFuel = Application.Index(ThisWorkbook.Worksheets("Motor").[Motor], 0, DUTCH)
' return corresponding number
On Error Resume Next ' provide for not found case
getNumber = Application.Match(s, arFuel, 0) ' find only exact matches
If Err.Number <> 0 Then getNumber = 0 ' could be omitted in case of a zero return
End Function
With 2 arrays this is a possible solution:
Sub TestMe()
Dim inputString As String
Dim arrString As Variant
Dim arrResult As Variant
inputString = "Benzine, Diesel, Hybride"
arrString = Split(inputString, ",")
Dim total As Long: total = UBound(arrString)
ReDim arrResult(total)
Dim i As Long
For i = LBound(arrString) To UBound(arrString)
arrResult(total - i) = Trim(arrString(i))
Next i
Debug.Print Join(arrResult, " ,")
End Sub
However, there is a classic solution of this problem, reversing everything twice:
Sub TestMe()
Dim inputString As String
inputString = "Benzine, Diesel, Hybride"
inputString = StrReverse(inputString)
Dim arr As Variant: arr = Split(inputString, ",")
Dim i As Long
For i = LBound(arr) To UBound(arr)
arr(i) = Trim(StrReverse(arr(i)))
Next i
Debug.Print Join(arr, ", ")
End Sub

How do I associate a listbox VBA Form array associate with absolute address

I am populating a listbox in a form using a range as so:
Private Sub UserForm_Initialize()
Names = Range("C6:D" & Cells(Rows.Count, 3).End(xlUp).Row)
For i = LBound(Names, 1) To UBound(Names, 1)
ListBox1.AddItem Names(i, 1) & "-" & Names(i, 2)
Next
OptionButton3.Value = True
End Sub
I need to call the address of each of these items later in my code to act upon; in reality each item in the listbox is to select which rows to act upon by the user placing each item in a different listbox as part of the form.
I have tried to redimension the array like so, with no success due to "Constant Expression Required":
Dim Names(6 To Cells(Rows.Count, 3).End(xlUp).Row, Range("C6:D" & Cells(Rows.Count, 3).End(xlUp).Row))
What is either the best way to associate the address with the array, or record the list of rows ?
Think this is what you need:
Code
Private Sub UserForm_Initialize()
' I. Get started
' a) variables
Const iOffset As Long = 5 ' row offset 5, i.e. start at row C6
Dim names, a ' variant datafield arrays
Dim i As Long, n As Long
Dim ws As Worksheet
' b) set worksheet object to memory
Set ws = ThisWorkbook.Worksheets("MySheet") ' << change to your sheet name
' c) listbox layout just for demo
Me.ListBox1.ColumnCount = 2
Me.ListBox1.ColumnWidths = "100;50"
' d) get last row of column C
n = ws.Cells(Rows.Count, 3).End(xlUp).Row
' II. Get values
' a) create 1-based 2-dim variant datafield array
names = ws.Range("C6:D" & n)
' b) concatenate names and define cell address
For i = LBound(names, 1) To UBound(names, 1)
names(i, 1) = names(i, 1) & "-" & names(i, 2)
names(i, 2) = "C" & i + iOffset
Next
' III. Fill Listbox
' get values of names array (1-liner, allows more than 10 columns :-)
ListBox1.List = names
' IV. Test to get 2nd array column into new array a
a = Application.Index(names, 0, 2)
For i = LBound(a) To UBound(a)
Debug.Print a(i, 1)
Next i
' V. clear memory
Set ws = Nothing
End Sub
Note
I added a constant defining your row Offset (Const iOffset As Long = 5) and speeded up code using an array assignment to your ListBox.List in one code statement instead of adding items one by one (BTW this would allow to use more than 10 listbox columns).
As #Rory remarked, just adding your row offset (e.g. +5) to the current active ListBox1.Listindex would be sufficient to get the row number. In this case you should only take care that a list row is marked (i.e. to check If ListBox1.ListIndex > -1 Then ... do something).
I solved the problem by a rather inelegant hack that I don't particularly like using a helper column listing the row number. There are better ways of doing this I'm sure, but here's what I came up with:
Private Sub UserForm_Initialize()
Names = Range("C6:E" & Cells(Rows.Count, 3).End(xlUp).Row)
For i = LBound(Names, 1) To UBound(Names, 1)
ListBox1.AddItem Names(i, 3) & ": " & Names(i, 1) & "-" & Names(i, 2)
Next
OptionButton3.Value = True
End Sub
I then recall the row number in my code as follows once the user has selected which items to act on:
For i = 0 To (ListBox2.ListCount - 1)
Dim itemName() As String
itemName() = Split(ListBox2.list(i), ":")
deviceRow = itemName(0)
Debug.Print "Row number: " + deviceRow
... <SNIP>
It Prints like this:
Row number: 10
Row number: 7
Row number: 14
Row number: 9
There must be better ways of doing this, but that was my solution.

Add range of data/cells in dynamic multidimensional array vba

I would like to be able to add some range of data in a dynamic multidimensional array without using a double loop that screens each element of the array. But I don't know if it is possible. By double loop, I mean such a code (this is only an example):
Dim Films(1 To 5, 1 To 2) As String
Dim i As Integer, j As Integer
For i = 1 To 5
For j = 1 To 2
Films(i, j) = Cells(i, j).Value
Next j
Next i
I am using VBA 2010. I know how many rows my array has, but the number of columns is variable.
Here is my code :
Sub DRS(Item)
'item is a name to search for in a specific range
Dim SrcRange() As Variant
Dim cell3 As Range
Dim n As Integer, m As Integer
SrcRange() = Array()
ReDim SrcRange(45, 0)
m = -1
n = 0
With Sheets("X")
For Each cell3 In .Range("I13:AG" & .Cells(1, Columns.Count).End(xlToRight).Column)
'the range ("I13:AG...") contains names, and some will match with "item"
m = m + 1
If Len(cell3.Value) > 0 And cell3 = Item Then
SrcRange(0, n) = .Range(m + 8 & "30:" & m + 8 & "75")
'the previous line **should** add a whole range of cells (which contain numbers, one by cell) in a colum of the array, but this is the line that doesn't work.
n = n + 1
ReDim Preserve SrcRange(UBound(SrcRange), n)
End If
Next cell3
End With
End Sub
I already tried those::
SrcRange(:, n) = .Range(m + 8 & "30:" & m + 8 & "75")
SrcRange(0:45, n) = .Range(m + 8 & "30:" & m + 8 & "75")
SrcRange(, n) = .Range(m + 8 & "30:" & m + 8 & "75")
but no one worked.
Is there a way or a formula that would allow me to add a full range of cells to each column of the array, or am I obliged to use a double loop to add the elements one by one?
I'm guessing that this Range...
.Range("I13:AG" & .Cells(1, Columns.Count).End(xlToRight).Column)
...should actually be xlToLeft instead of xlToRight (xlToRight will always return I13:AG16384).
I'm also not entirely sure what the m + 8 & "30:" & m + 8 & "75" is supposed to be evaluating to, because you increment the variable m each time through the loop, and it gives you ranges like 930:975. I'll take a stab in the dark and assume that the m + 8 is supposed to be the column that you found the item in.
That said, the .Value property of a Range object will just give you a 2 dimensional array. There isn't really any reason to build an array - just build a range and then worry about getting the array out of it when you're done. To consolidate the range (you only get the first area if you grab its Value), just copy and paste it to a temporary Worksheet, grab the array, then delete the new sheet.
Sub DRS(Item)
'item is a name to search for in a specific range
Dim SrcRange() As Variant
Dim found As Range
Dim cell3 As Range
With Sheets("X")
For Each cell3 In .Range("I13:AG" & .Cells(1, Columns.Count).End(xlToLeft).Column)
'the range ("I13:AG...") contains names, and some will match with "item"
If Len(cell3.Value) > 0 And cell3.Value = Item Then
If Not found Is Nothing Then
Set found = Union(.Range(.Cells(30, cell3.Column), .Cells(75, cell3.Column)), found)
Else
Set found = .Range(.Cells(30, cell3.Column), .Cells(75, cell3.Column))
End If
End If
Next cell3
End With
If Not found Is Nothing Then
Dim temp_sheet As Worksheet
Set temp_sheet = ActiveWorkbook.Sheets.Add
found.Copy
temp_sheet.Paste
SrcRange = temp_sheet.UsedRange.Value
Application.DisplayAlerts = False
temp_sheet.Delete
Application.DisplayAlerts = True
End If
End Sub

VBA array trouble error 9 script out of range

Thanks for reading my question,
I was given a list of about 250k entries along with names and sign in dates to accompany each entry to show when they logged. My task is to find out which users signed in on consecutive days, how often and how many times.
i.e. Bob smith had 3 consecutive days one time, 5 consecutive days 3 times.
joe smith had 8 consecutive days once, 5 consecutive days 8 times
etc
I am brand new to VBA and have been struggling to write a program to do this.
code:
Option Explicit
Option Base 1
Sub CountUUIDLoop()
Dim UUID As String
Dim Day As Date
Dim Instance() As Variant
ReDim Instance(50, 50)
Dim CountUUID As Variant
Dim q As Integer
Dim i As Long
Dim j As Long
Dim f As Integer
Dim g As Integer
Dim LastRow As String
f = 1
q = 1
g = 2
LastRow = Cells.Find("*", [A1], , , xlByRows, xlPrevious).Row
For i = q To LastRow
UUID = Cells(i, "A")
Instance(f, 1) = UUID
g = 2
For j = 1 To LastRow
If UUID = Cells(j, "A") Then
Instance(f, g) = Cells(j, "B")
g = g + 1
End If
Next j
f = f + 1
q = g - 1
Next i
End Sub
The goal of this code is to go through the entries and store them in the array 'Instance' such that the 2D array would look like [UUID1, B1, B2, B3]
[UUID2, B1, B2, B3, B4]
[UUID3, B1, B2]
Where the UUID is the user, the B1 represents the date that user signed in, b2 would be the next date they signed in etc. Some users have more or less dates than others.
My main issue has come with setting up the array as I keep getting different errors around it. I'm not sure how to define this 2D array partly because there will be over 30 000 rows, each with 1->85 columns.
Any help is appreciated, let me know if anything needs further clarification. Once again this is my first time using VBA so im sorry ahead of time if everything i've been doing is wrong.
P.S. I used ReDim Instance (50,50) as a test to see if i could make it work by predefining but same errors occurred. Thanks again!
As far as I understand from your question and code, you have a table with following structure:
..............A.................B
1........LOGIN1.......DATE1
2........LOGIN1.......DATE2
3........LOGIN1.......DATE3
4........LOGIN2.......DATE4
5........LOGIN2.......DATE5
6........LOGIN3.......DATE6
And your task in this code was to fetch data in a 2D structure like this:
RESULT_ARRAY-
............................|-LOGIN1-
............................................|-DATE1
............................................|-DATE2
............................................|-DATE3
............................|-LOGIN2-
............................................|-DATE4
............................................|-DATE5
............................|-LOGIN3-
............................................|-DATE6
First of all, you need to know what goes wrong in your code. Please see comments in code below to find out the reason of error:
Option Explicit
Option Base 1
Sub CountUUIDLoop()
Dim UUID As String
Dim Day As Date
Dim Instance() As Variant ' If you are using variant data type, it is not necesary to point it: default data type in VBA is Variant. Just write like this: "Dim Instance()"
ReDim Instance(50, 50) ' Limitation in 50 may be the reason, why your script is going into "out of range" error.
' Remember, that this operation means, that your array now will have following dimentions: [1..50,1..50]
Dim CountUUID As Variant 'Just write like this: "Dim CountUUID"
Dim q As Integer ' you can describe all your variables in one line, like this: "Dim q as Integer,f as Integer,g as Integer"
Dim i As Long
Dim j As Long
Dim f As Integer
Dim g As Integer
Dim LastRow As String ' first mistake: you are using String data type to perform numeric operations below in your FOR-cycle
f = 1 ' Your Instance array index starts from {0} and you are not using this index by starting from {1}.
q = 1 ' The reason to use this variable is not obvious. You could just use constant in FOR cycle below and avoid unnecessary variables.
g = 2 ' You could remove this line, because this var is set every time in cycle below (before second FOR)
LastRow = Cells.Find("*", [A1], , , xlByRows, xlPrevious).Row ' The alternative here is to use predefined Excel constants, like this:
' "Cells.SpecialCells(xlLastCell).Row".
'If LastRow is bigger, than {50} - this could be a reason of your Error.
For i = q To LastRow ' Here goes comparison between String and Integer data type, not good thing, but type conversion should work fine here.
UUID = Cells(i, "A") ' No need to perform re-set here, just move forward and assign value from this cell to the Instanse directly:
' Like this: Instance(f, 1) = Cells(i, "A")
Instance(f, 1) = UUID
g = 2
For j = 1 To LastRow ' It is another point, why "q" variable is not necessary. :)
If UUID = Cells(j, "A") Then ' You could use your Instansce value instead of UUID there, like this: "Instance(f, 1)"
Instance(f, g) = Cells(j, "B") 'If "g" variable will somehow become bigger, than {49}, this could become a reason of your Error.
g = g + 1
End If
Next j
f = f + 1
q = g - 1 ' "q" variable is not used after this row, so it is a strange unnecessary action
Next i
End Sub
Now, when we have some information about error, let me do some improvements on your code. I am certain, that to make most simply code, you can use your Excel worksheets to store and count data with VBA as background automations. But if you need the code with arrays, let's do this! :)
Option Explicit ' It is an option that turns on check for every used variable to be defined before execution. If this option is not defined, your code below will find undefined variables and define them when they are used. Good practice is to use this option, because it helps you, for example to prevent missprinting errors in variable names.
Option Base 1 ' This option sets the default index value for arrays in your code. If this option is not set, the default index value will be {0}.
Const HEADER_ROW = 1 ' It is a number to identify your header row, next row after this one will be counted as a row with data
Const UUID = 1 ' ID of element in our "Instance" array to store UUID
Const DATES_ID = 2 ' ID of element in our "Instance" array to store dates
Function CountUUIDLoop()
ActiveSheet.Copy After:=ActiveSheet 'Copy your worksheet to new one to ensure that source data will not be affected.
Dim Instance(), dates() ' "Instance" will be used to store all the data, "dates" will be used to store and operate with dates
ReDim Instance(2, 1) ' Set first limitation to the "Instance" array in style [[uuid, dates],id]
ReDim dates(1) ' Set first limitation to the "dates" array
Instance(DATES_ID, 1) = dates
Dim CountUUID
Dim i as Long, j as Long, f as Long, active_element_id As Long 'Integer is quite enough to perform our array manipulations, but Long datatype is recomended (please refer to UPDATE2 below)
i = HEADER_ROW + 1 ' Set first row to fetch data from the table
active_element_id = 1 ' Set first active element number
With ActiveSheet ' Ensure that we are working on active worksheet.
While .Cells(i, 1) <> "" 'If operated cell is not empty - continue search for data
If i > HEADER_ROW + 1 Then
active_element_id = active_element_id + 1 ' increment active element number
ReDim Preserve Instance(2, active_element_id) ' Assign new limitation (+ 1) for our Instances, don't forget to preserve our results.
ReDim dates(1) ' Set first limitation to the "dates" array
Instance(DATES_ID, active_element_id) = dates
End If
Instance(UUID, active_element_id) = .Cells(i, 1) ' save UUID
dates(1) = .Cells(i, 2) ' save first date
j = i + 1 ' Set row to search next date from as next row from current one.
While .Cells(j, 1) <> "" 'If operated cell is not empty - continue search for data
If .Cells(j, 1) = .Cells(i, 1) Then
ReDim Preserve dates(UBound(dates) + 1) ' Expand "dates" array, if new date is found.
dates(UBound(dates)) = .Cells(j, 2) ' Save new date value.
.Cells(j, 1).EntireRow.Delete 'Remove row with found date to exclude double checking in future
Else
j = j + 1 ' If uuid is not found, try next row
End If
Wend
Instance(DATES_ID, active_element_id) = dates
i = i + 1 'After all the dates are found, go to the next uuid
Wend
.Cells(i, 1) = "UUID COUNT" ' This will write you a "UUID COUNT" text in A column below all the rest of UUIDs on active worksheet
.Cells(i, 2) = i - HEADER_ROW - 1 ' This will write you a count of UUIDs in B column below all the rest of UUIDs on active worksheet
End With
CountUUIDLoop = Instance ' This ensures that your function (!) returns an array with all UUIDs and dates inside.
End Function
This function will print you count of your UUIDs at the bottom of active sheet and return you an array like this:
[[LOGIN1][1], [[DATE1][DATE2][DATE3]][1]]
I have used this order of storing data to avoid error with expanding of multidimentional arrays. This error is similar to yours, so you could read more about this there: How can I "ReDim Preserve" a 2D Array in Excel 2007 VBA so that I can add rows, not columns, to the array? Excel VBA - How to Redim a 2D array? ReDim Preserve to a Multi-Dimensional Array in Visual Basic 6
Anyway, you could use my function output ("Instance" array) to perform your further actions to find what you need or even display your uuid-dates values. :)
Good luck in your further VBA actions!
UPDATE
Here is the test procedure showing how to work with the above function's results:
Sub test()
Dim UUIDs ' The result of the "CountUUIDLoop" function will be stored there
Dim i as Long, j As Long ' Simple numeric variables used as indexies to run through our resulting array
UUIDs = CountUUIDLoop ' assign function result to a new variable
Application.DisplayAlerts = False ' Disable alerts from Excel
ActiveSheet.Delete ' Delete TMP worksheet
Application.DisplayAlerts = True ' Enable alerts from Excel
If UUIDs(UUID, 1) <> Empty Then ' This ensures that UUIDs array is not empty
Sheets.Add After:=ActiveSheet ' Add new worksheet after active one to put data into it
With ActiveSheet 'Ensure that we are working with active worksheet
.Cells(HEADER_ROW, 1) = "UUIDs/dates" ' Put the header into the "HEADER_ROW" row
For i = 1 To UBound(UUIDs, 2) ' run through all the UUIDs
.Cells(1 + HEADER_ROW, i) = UUIDs(UUID, i) ' Put UUID under the header
For j = 1 To UBound(UUIDs(DATES_ID, i)) ' run through all the dates per UUID
.Cells(j + 1 + HEADER_ROW, i) = UUIDs(DATES_ID, i)(j) ' put date into column below the UUID
Next j ' Go to next date
Next i ' Go to next UUID
.Cells.EntireColumn.AutoFit ' This will make all columns' width to fit its contents
End With
Else
MsgBox "No UUIDs are found!", vbCritical, "No UUIDs on worksheet" ' Show message box if there are no UUIDs in function result
End If
End Sub
So, if you'll have following data on the active worksheet:
..............A.................B
1........LOGIN1.......DATE1
2........LOGIN1.......DATE2
3........LOGIN1.......DATE3
4........LOGIN2.......DATE4
5........LOGIN2.......DATE5
6........LOGIN3.......DATE6
...this sub will put UUIDs on the new sheet like this:
..............A.................B.................C
1........UUIDs/dates
2........LOGIN1........LOGIN2........LOGIN3
3........DATE1.........DATE4.........DATE6
4........DATE2.........DATE5
5........DATE3
UPDATE2
It is recomended to use Long data type instead of Integer each type when integer (or whole number) variable is needed. Long is slightly faster, it has much wider limitations and costs no additional memory. Here is proof link:
MSDN:The Integer, Long, and Byte Data Types
I would recommend using collections and a dictionary instead of arrays. The below code will structure the data in a way that is very similar to the way you wanted it.
Sub collect_logins_by_user_()
'you need to enable the microsoft scripting runtime
'in tools - references
'assuming unique ids are in col A and there are no gaps
'and assuming dates in col B and there are no gaps
'
'The expected runtime for this is O(n) and I have used similar code on more than 250.000 record.
'It still takes a while obviously, but should run just fine.
'
'The the data will bestructed in the following format:
'{id_1: [d_1, d_2,...], id_2: [d_3, d_4,...], ...}
Dim current_id As Range: Set current_id = ActiveSheet.Range("A2") 'modify range as required
Dim logins_by_users As New Dictionary
While Not IsEmpty(current_id)
If Not logins_by_users.Exists(current_id.Value) Then
Set logins_by_users(current_id.Value) = New Collection
End If
logins_by_users(current_id.Value).Add current_id.Offset(ColumnOffset:=1).Value
Set current_id = current_id.Offset(RowOffset:=1)
Wend
'Once you have the data structured, you can do whatever you want with it.
'like printing it to the immediate window.
Dim id_ As Variant
For Each id_ In logins_by_users
Debug.Print "======================================================="
Debug.Print id_
Dim d As Variant
For Each d In logins_by_users(id_)
Debug.Print d
Next d
Next id_
Debug.Print "======================================================="
End Sub
I have written a bit of code that does something along the lines of what you are trying to do - it prints to the debug window the different numbers of consecutive logs per user, separeted by commas.
This code makes use of the dictionary object - which essentially is an associative array where the indexes are not restrained to numbers like they are in arrays, and offers a couple of convenient features to manipulate data that arrays don't.
I have tested this on a sheet including user ids in colomn A and log dates in column B - including headers - and this looks to work fine. Fell free to give it a try
Sub mysub()
Dim dic As Object
Dim logs As Variant
Dim myval As Long
Dim mykey As Variant
Dim nb As Long
Dim i As Long
Set dic = CreateObject("Scripting.dictionary")
'CHANGE TO YOUR SHEET REFERENCE HERE
For Each cell In Range(Cells(2, 1), Cells(Worksheets("Sheet8").Rows.count, 1).End(xlUp))
mykey = cell.Value
myval = cell.Offset(0, 1)
If myval <> 0 Then
On Error GoTo ERREUR
dic.Add mykey, myval
On Error GoTo 0
End If
Next cell
For Each Key In dic
logs = Split(dic(Key), ",")
logs = sortArray(logs)
i = LBound(logs) + 1
nb = 1
Do While i <= UBound(logs)
Do While CLng(logs(i)) = CLng(logs(i - 1)) + 1
nb = nb + 1
i = i + 1
Loop
If nb > 1 Then
tot = tot & "," & CStr(nb)
nb = 1
End If
i = i + 1
Loop
If tot <> "" Then dic(Key) = Right(tot, Len(tot) - 1)
Debug.Print "User: " & Key & " - Consecutive logs: " & dic(Key)
tot = ""
mys = ""
Next Key
Exit Sub
ERREUR:
If myval <> 0 Then dic(mykey) = dic(mykey) & "," & CStr(myval)
Resume Next
End Sub
Function sortArray(a As Variant) As Variant
For i = LBound(a) + 1 To UBound(a)
j = i
Do While a(j) < a(j - 1)
temp = a(j - 1)
a(j - 1) = a(j)
a(j) = temp
j = j - 1
If j = 0 Then Exit Do
Loop
Next i
sortArray = a
End Function

Resources