Redimension the first value of a multi-dimensional array in VBA Excel - arrays

There has to be a simpler way to do this. I took the advice of one poster on this forum who said that I have to set my multidimensional array to a high number then redimension it to a lower number. But in order to get it to the right number I have to run it through two loops where it seems like there has to be a simpler way to do things. So I have the array ancestors which has several blanks in it which I'm trying to get rid of. The second dimension will always be 2. I first run it through a loop to determine the ubound of it. And I call that ancestors3. Then I run the ancestors3 array through a loop and populate the ancestors2 array.
For s = 1 To UBound(ancestors, 1)
temp_ancest = ancestors(s, 1)
If temp_ancest <> "" Then
uu = uu + 1
ReDim Preserve ancestors3(uu)
ancestors3(uu) = temp_ancest
End If
Next
Dim ancestors2()
ReDim ancestors2(UBound(ancestors3), 2)
For s = 1 To UBound(ancestors3, 1)
temp_ancest = ancestors(s, 1)
temp_ancest2 = ancestors(s, 2)
If temp_ancest <> "" Then
y = y + 1
ancestors2(y, 1) = temp_ancest
ancestors2(y, 2) = temp_ancest2
End If
Next

Reading your question i think you want this:
you have a 2D array ancestors that may have some blank entries in the 1st dimension
you want a copy of ancestors without those blank rows, called ancestors2
Here is one way to do this. See inline comments for explanation
Sub Demo()
Dim ancestors As Variant
Dim ancestors2 As Variant
Dim i As Long, j As Long
Dim LB as long
' Populate ancestors as you see fit
'...
' crate array ancestors2, same size as ancestors, but with dimensions flipped
' so we can redim it later
ReDim ancestors2(LBound(ancestors, 2) To UBound(ancestors, 2), _
LBound(ancestors, 1) To UBound(ancestors, 1))
' Loop ancestors array, copy non-blank items to ancestors2
j = LBound(ancestors, 1)
LB = LBound(ancestors, 1)
For i = LBound(ancestors, 1) To UBound(ancestors, 1)
If ancestors(i, 1) <> vbNullString Then
ancestors2(LB, j) = ancestors(i, LB)
ancestors2(LB + 1, j) = ancestors(i, LB + 1)
j = j + 1
End If
Next
' Redim ancestors2 to match number of copied items
ReDim Preserve ancestors2(LBound(ancestors2, 1) To UBound(ancestors2, 1), _
LBound(ancestors2, 2) To j - 1)
' Transpose ancestors2 to restore flipped dimensions
ancestors2 = Application.Transpose(ancestors2)
End Sub

Related

How to loop through a specific row of a 2-dimensional Array?

I have 2-dimensional array and I would like to inspect each element in a specific row with If-Then statements and assign assign values to the next row depending on the outcome of the If-Then statements? What is the correct syntax for looping through the elements of a row in a 2-d array?
Please, try using the next Sub:
Sub changeRow(arr As Variant, iR As Long, strTxt As String)
Dim i As Long
For i = LBound(arr, 2) To UBound(arr, 2) '(arr, 2) to determine the number of columns
arr(iR, i) = arr(iR, i) & strTxt
Next i
End Sub
Of course, it can be designed to do whatever you need on the respective row. Even extending parameters to be used.
It can easily be tested in the next way:
Sub testIterate2DArrayRow()
Dim sh As Worksheet, arr, arrR, iRow As Long, strAdd As String
Set sh = ActiveSheet
iRow = 2 'the array row to be iterated
strAdd = " - XX" 'string to be added to each row element (instructional example)
arr = sh.Range("A2:D6").value 'the easiest way to create a 2D array
arrR = Application.Index(arr, iRow, 0) 'create a 1D slice of the row to be iterated/modified
'if you need only iterating to extract something, you may stop here
'and iterate between its elements...
Debug.Print Join(arrR, "|") 'just to visually see the row content
changeRow arr, iRow, strAdd 'iterate on the iRow row (and modify something)
Debug.Print Join(Application.Index(arr, iRow, 0), "|") 'visual evidence of the modification...
End Sub
Edited:
I will let the above code for other people liking to learn the general concept.
Please, test the next code, which should process the array as (I understood) you need.
Its first lines only create the opportunity to easily check the concept. So, you should place the necessary bays on an Excel sheet, from "A1" to "J1" and run the above code. It will return the processed array starting from "L1":
Sub analizeBays()
Dim sh As Worksheet, BayRay(), i As Long
Set sh = ActiveSheet
BayRay = sh.Range("A1:J4").value 'only to easily test the concept
For i = LBound(BayRay, 2) To UBound(BayRay, 2)
If BayRay(1, i) <= 10 Then
BayRay(2, i) = 2035
BayRay(3, i) = 2005
BayRay(4, i) = 1005
ElseIf BayRay(1, i) > 10 And BayRay(1, i) <= 12 Then
BayRay(2, i) = 2022
BayRay(3, i) = 1032
BayRay(4, i) = 4344
End If
Next i
'drop the processed array content starting from "L1")
sh.Range("L1").Resize(UBound(BayRay), UBound(BayRay, 2)).value = BayRay
End Sub
Loop Through a Row of a 2D Array
Option Explicit
Sub LoopThroughRow()
Const RowIndex As Long = 2
Const Criteria As Double = 3
Const MinNum As Long = 1
Const MaxNum As Long = 5
' Populate with random integers.
Dim Data As Variant: ReDim Data(1 To 5, 1 To 5)
Dim r As Long, c As Long
For r = LBound(Data, 1) To UBound(Data, 1)
For c = LBound(Data, 2) To UBound(Data, 2)
Data(r, c) = Int((MaxNum - MinNum + 1) * Rnd + MinNum)
Next c
Next r
' Write criteria row.
For c = LBound(Data, 2) To UBound(Data, 2)
If Data(RowIndex, c) > Criteria Then
Data(RowIndex + 1, c) = "Yes"
Else
Data(RowIndex + 1, c) = "No"
End If
Next c
' Print result.
Debug.Print "Column", "Row " & RowIndex, "Row " & RowIndex + 1
For c = LBound(Data, 2) To UBound(Data, 2)
Debug.Print c, Data(RowIndex, c), Data(RowIndex + 1, c)
Next c
End Sub

Excel VBA - Determining Column or Row Target of Array UDF

I have a simple excel UDF for converting an array of mass values to mol fractions. Most times, the output will be a column array (n rows by 1 column).
How, from within the VBA environment, do I determine the dimensions of the target cells on the worksheet to ensure that it should be returned as n rows by 1 column versus n columns by 1 row?
Function molPct(chemsAndMassPctsRng As Range)
Dim chemsRng As Range
Dim massPctsRng As Range
Dim molarMasses()
Dim molPcts()
Set chemsRng = chemsAndMassPctsRng.Columns(1)
Set massPctsRng = chemsAndMassPctsRng.Columns(2)
chems = oneDimArrayZeroBasedFromRange(chemsRng)
massPcts = oneDimArrayZeroBasedFromRange(massPctsRng)
'oneDimArrayZeroBasedFromRange is a UDF to return a zero-based array from a range.
ReDim molarMasses(UBound(chems))
ReDim molPcts(UBound(chems))
totMolarMass = 0
For chemNo = LBound(chems) To UBound(chems)
molarMasses(chemNo) = massPcts(chemNo) / mw(chems(chemNo))
totMolarMass = totMolarMass + molarMasses(chemNo)
Next chemNo
For chemNo = LBound(chems) To UBound(chems)
molPcts(chemNo) = Round(molarMasses(chemNo) / totMolarMass, 2)
Next chemNo
molPct = Application.WorksheetFunction.Transpose(molPcts)
End Function
I understand that, if nothing else, I could have an input parameter to flag if return should be as a row array. I'm hoping to not go that route.
Here is a small example of a UDF() that:
accepts a variable number of input ranges
extracts the unique values in those ranges
creates a suitable output array (column,row, or block)
dumps the unique values to the area
Public Function ExtractUniques(ParamArray Rng()) As Variant
Dim i As Long, r As Range, c As Collection, OutPut
Dim rr As Range, k As Long, j As Long
Set c = New Collection
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
' First grab all the data and make a Collection of uniques
'
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
On Error Resume Next
For i = LBound(Rng) To UBound(Rng)
Set r = Rng(i)
For Each rr In r
c.Add rr.Value, CStr(rr.Value)
Next rr
Next i
On Error GoTo 0
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
' next create an output array the same size and shape
' as the worksheet output area
'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
k = 1
With Application.Caller
ReDim OutPut(1 To .Rows.Count, 1 To .Columns.Count)
End With
For i = LBound(OutPut, 1) To UBound(OutPut, 1)
For j = LBound(OutPut, 2) To UBound(OutPut, 2)
If k < c.Count + 1 Then
OutPut(i, j) = c.Item(k)
k = k + 1
Else
OutPut(i, j) = ""
End If
Next j
Next i
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
' put the data on the sheet
'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
ExtractUniques = OutPut
End Function
You should return two dimensional arrays: n × 1 for row and 1 × n for column vectors.
So you need either
Redim molPcts(1, Ubound(chems) + 1)
or
Redim molPcts(Ubound(chems) + 1, 1)
To refer to them, you need to use both indices:
molPcts(1, chemNo + 1)
or
molPcts(chemNo + 1, 1)
If you prefer 0-based arrays, the redim should be like this:
Redim molPcts(0 To 0, 0 To Ubound(chems))
Redim molPcts(0 To Ubound(chems), 0 To 0)

Cleaning up an array

I have a very large array in VBA which includes a lot of 0 values that I'd like to remove. Something like this:
A B C 12345
D E F 848349
G H I 0
J K L 0
M N O 0
P Q R 4352
S T U 0
V W X 0
I would like to be able to quickly/easily strip out all rows from this array that have a zero in the 4th column, resulting in something like this:
A B C 12345
D E F 848349
P Q R 4352
This array has 100,000 or so rows, that hopefully gets down to a number closer to 20,000 or 30,000 rows instead after processing.
I assume iterating through every entry will prove very time-consuming.
Is there another way that is faster?
I'm not aware of any other way in VBA than to loop through the array and write another array/list.
What makes it trickier is that your array looks to be two-dimensional and VBA will only allow you to redim the last dimension. From the look of your data, you'd want to redim the first dimension as you iterate through your array.
There are several solutions:
Iterate your data twice - once to get the array size (and probably to store the relevant row numbers) and a second time to transfer the raw data into your new data.
Iterate once and just reverse your dimensions (ie row is last).
Use an array of arrays, so that each array only has one dimension).
Use a Collection which doesn't need to be dimensioned - this would be my preferred option.
Option 4 would look like this (I've assumed your array is zero based):
Dim resultList As Collection
Dim r As Long
Set resultList = New Collection
For r = 0 To UBound(raw, 1)
If raw(r, 3) <> 0 Then
resultList.Add Array(raw(r, 0), raw(r, 1), raw(r, 2), raw(r, 3))
End If
Next
If you have to write to a new array, then here's an example of Option 1:
Dim rowList As Collection
Dim result() As Variant
Dim r As Long
Dim c As Long
Dim v As Variant
Set rowList = New Collection
For r = 0 To UBound(raw, 1)
If raw(r, 3) <> 0 Then
rowList.Add r
End If
Next
ReDim result(rowList.Count - 1, 3) As Variant
c = 0
For Each v In rowList
result(c, 0) = raw(v, 0)
result(c, 1) = raw(v, 1)
result(c, 2) = raw(v, 2)
result(c, 3) = raw(v, 3)
c = c + 1
Next
Okay, it's all off-sheet, so all the arrays are zero-based. To test this set-up, I created a worksheet with four columns, as per your data and using random numbers in the fourth column. I saved this to a text file (TestFile.txt), then read it in to be able to get a zero-based array (Excel ranges are 1-based when you take them into an array). I saved 150000 rows to the text file to properly stress the routine. Yes, I have an SSD and that would affect the 2s run time, but I'd still expect it to run in <10s on a spinning HDD, I think.
Anyway, here's the code (requires a VBA reference to Microsoft Scripting Runtime purely to read in the file):
Public Function ReturnFilteredArray(arrSource As Variant, _
strValueToFilterOut As String) As Variant
Dim arrDestination As Variant
Dim lngSrcCounter As Long
Dim lngDestCounter As Long
ReDim arrDestination(UBound(arrSource, 1) + 1, UBound(arrSource, 2) + 1)
lngDestCounter = 1
For lngSrcCounter = LBound(arrSource, 1) To UBound(arrSource, 1)
' Assuming the array dimensions are (100000, 3)
If CStr(arrSource(lngSrcCounter, 3)) <> strValueToFilterOut Then
' Hit an element we want to include
arrDestination(lngDestCounter, 1) = arrSource(lngSrcCounter, 0)
arrDestination(lngDestCounter, 2) = arrSource(lngSrcCounter, 1)
arrDestination(lngDestCounter, 3) = arrSource(lngSrcCounter, 2)
arrDestination(lngDestCounter, 4) = arrSource(lngSrcCounter, 3)
lngDestCounter = lngDestCounter + 1
End If
Next
ReturnFilteredArray = arrDestination
End Function
Sub TestRun()
Dim fso As FileSystemObject
Dim txs As TextStream
Dim arr As Variant
Dim arr2 As Variant
Dim lngCounter As Long
Debug.Print Now()
Set fso = New FileSystemObject
Set txs = fso.OpenTextFile("E:\Users\Thingy\Desktop\TestFile.txt", ForReading)
arr = Split(txs.ReadAll, vbNewLine)
ReDim arr2(UBound(arr), 3)
For lngCounter = 0 To UBound(arr) - 1
arr2(lngCounter, 0) = Split(arr(lngCounter), vbTab)(0)
arr2(lngCounter, 1) = Split(arr(lngCounter), vbTab)(1)
arr2(lngCounter, 2) = Split(arr(lngCounter), vbTab)(2)
arr2(lngCounter, 3) = Split(arr(lngCounter), vbTab)(3)
Next
arr2 = ReturnFilteredArray(arr2, "0")
Range("L2").Resize(UBound(arr2, 1), 5) = arr2
Debug.Print Now()
End Sub
There are a number of assumptions in there, not least the dimensions. Note the difference in the second dimension counter between arrDestination and arrSource. That's to do with Excel being 1-based and normal arrays being 0-based.
Also, when I'm writing out the array, I needed to bump up the second dimension to 5 in order to get all of the array out to the sheet. I wasn't able to trim off the empty elements since ReDim Preserve only works on the uppermost dimension (columns) and it's the first dimension (rows) that's changing here.
Anywho, this should serve as a reminder that despite its faults Excel is pretty amazing.

ReDim Preserve "Subscript Out of Range"

I am trying to move data from 2 Double Arrays to 2 different Double Arrays. I'm not sure what the size is going to be because I am taking a randomized sample out of the first arrays and putting it into the 2nd arrays.
When I add the ReDim Preserve line I get the Subscript Out of Range error.
Function CreateTrainingSet(TrainingPercent As Double, Inputs() As Double, Outputs() As Double)
' Create Randomized Training set data
Dim TrainingInputs() As Double, TrainingOutputs() As Double
Dim i As Integer, j As Integer, count As Integer
'ReDim TrainingInputs(UBound(Inputs, 1), UBound(Inputs, 2))
'ReDim TrainingOutputs(UBound(Outputs, 1), UBound(Outputs, 2))
count = 0
' Move TraningPercent % of data from Inputs and Outputs to TrainingInputs and TrainingOutputs
For i = LBound(Inputs, 1) To UBound(Inputs, 1)
Dim ran As Double
ran = Rnd()
If ran <= TrainingPercent Then
count = count + 1
For j = LBound(Inputs, 2) To UBound(Inputs, 2)
ReDim Preserve TrainingInputs(1 To count, 1 To UBound(Inputs, 2))
TrainingInputs(count, j) = Inputs(i, j)
Next j
For j = LBound(Outputs, 2) To UBound(Outputs, 2)
ReDim Preserve TrainingOutputs(1 To count, 1 To UBound(Outputs, 2))
TrainingOutputs(count, j) = Outputs(i, j)
Next j
End If
Next i
For i = LBound(TrainingInputs, 1) To UBound(TrainingInputs, 1)
For j = LBound(TrainingInputs, 2) To UBound(TrainingInputs, 2)
Cells(i, j + 10).Value = TrainingInputs(i, j)
Next j
Next i
End Function
To summarise the comments above into an answer:
You can only redim the last dimension of a multi dimension array when using Preserve.
Therefore in order to resize a multiple dimension array there are a couple of simple options:
If only one dimension needs to be resized flip the loops and logic around so that the dimension to be resized becomes the last dimension
If both dimensions must be resized use either an array of arrays or a collection of arrays and correct the loops as required

How do I concatenate two multi-dimensional arrays in VB?

I've got this code:
rs1 = getResults(sSQL1)
rs2 = getResults(sSQL2)
rs1 and rs2 and 2D arrays. The first index represents the number of columns (static) and the second index represents the number of rows (dynamic).
I need to join the two arrays and store them in rs3. I don't know what type rs1 and rs2 are though.
Are you sure that the columns will match up? Because if that's not the case I don't know how you'd do it in a generic way in any language. If it is the case, then you could probably do it very simply like this:
rs1 = getResults(sSQL1 & " UNION " sSQL2)
I've figured it out. Turns out I was doing it the right way all along, I was just off by one. You don't need a third array either.
aRS_RU = rowsQuery(sSQL & ", 'RU'")
aRS_KR = rowsQuery(sSQL & ", 'KR'")
uboundRU1 = UBound(aRS_RU, 1)
uboundRU2 = UBound(aRS_RU, 2)
uboundKR2 = Ubound(aRS_KR, 2)
' Redim original array
ReDim Preserve aRS_RU(uboundRU1, uboundRU2 + uboundKR2 + 1 )
uboundRU2 = UBound(aRS_RU, 2)
' Add the values from the second array
For m = LBound(aRS_KR, 1) To UBound(aRS_KR, 1) 'Loop for 1st dimension
For n = LBound(aRS_KR, 2) To UBound(aRS_KR, 2) 'Loop for 2nd dimension
aRS_RU(m, uboundRU2 + n) = aRS_KR(m,n)
Next
Next
I know this post is old, but I adapted the code to fix some errors I had during its execution. The following code sample works for me:
Sub ConcatRecordSets(ByRef avFirstRS As Variant, ByRef avSecondRS As Variant)
Dim lIndex1 As Long, lIndex2 As Long
Dim lFirstRSSize As Long, lSecondRSSize As Long
' Redim original array
lFirstRSSize = UBound(avFirstRS, 2) - LBound(avFirstRS, 2) + 1
lSecondRSSize = UBound(avSecondRS, 2) - LBound(avSecondRS, 2) + 1
ReDim Preserve avFirstRS(LBound(avFirstRS, 1) To UBound(avFirstRS, 1), LBound(avFirstRS, 2) To UBound(avFirstRS, 2) + lSecondRSSize)
' Add the values from the second array
For lIndex1 = LBound(avSecondRS, 1) To UBound(avSecondRS, 1) ' Loop for 1st dimension
For lIndex2 = LBound(avSecondRS, 2) To UBound(avSecondRS, 2) ' Loop for 2nd dimension
avFirstRS(lIndex1, lFirstRSSize + lIndex2) = avSecondRS(lIndex1, lIndex2)
Next lIndex2
Next lIndex1
End Sub

Resources