I saw quite a number of questions/answers along these lines, but after reading a bunch of them, I'm still confused. I'm sorry if this is the nᵗʰ time a variant of this has been asked.
I can't figure out why this code dies on line 5 with a "subscript out of range" error in VBA (Excel for Mac v16.38):
Public Function array_test()
Dim arr As Variant
Dim array_slice As Variant
arr = Range("TestData!B27:F32").Value2
array_slice = arr(2) 'dies here with error 9
array_test = array_slice
End Function
Looking at the values pane, arr is clearly a Variant/Variant(1 to 6, 1 to 5) with all the expected data. There is nothing special about the cells, just non-formula data.
Even if I change the declarations to arr() and array_slice() or remove ".Value2", I get the same results. Even trying Application.WorksheetFunction.Index(arr, 2) rather than arr(2) gets me nowhere.
What am I missing?
P.S. I'm a C/Python programmer normally, so I'm thinking of arrays in those terms.
Usually one would loop an 2D-array for it's elements, however, since you specifically mentioned you would like to slice it through VBA, you could use Application.Index. For example try:
array_slice = Application.Index(arr, 2, 0) 'Slice 2nd row into 1D-array.
The idea here is to feed Application.Index with a static '2' which represents the row of interest. In the same fashion you could slice a specific column of interest, though if you need this to be an 1D-array, you'd need to use Application.Transpose, however there are limitations to this method:
With Application
array_slice = .Transpose(.Index(arr, 0, 2)) 'Slice 2nd column into 1D-array.
End With
When you copy data from a Range and the Range contains more than one cell, you get n 2-dimensional array in any case (even if you have only one row or one column).
To access a single value from that array, you have to provide both indices, like arr(2, 1). However, if you want to get a one-dimensional array, containing all values from a row (first index) or a column (second index), you need to create that array by your own - there is no slice function in VBA. You can dimension an array at runtime using the ReDim-command:
Dim array_slice(), i As Long
ReDim array_slice(LBound(arr, 1) To UBound(arr, 1))
For i = LBound(arr, 1) To UBound(arr, 1)
array_slice(i) = arr(i, 2)
Next i
To get the values of a column, use
Dim array_slice(), i As Long
ReDim array_slice(LBound(arr, 2) To UBound(arr, 2))
For i = LBound(arr, 2) To UBound(arr, 2)
array_slice(i) = arr(2, i)
Next i
Related
I have declared an array as such Dim rArray() As Variantbut when i try and use the values that is stored in it (as shown below) I get a subscript out of range error. The UBound(rArray)and LBound(rArray) both returns values 14 and 1, but the error occurs at the Debug.Print line.
If I use the for statement as below
For Each rArr in rArray
then it works without issues, but for the purposes I am creating this array I need the flexibility to select each item stored in that order- meaning I need to refer to them using subscripts.
I have tried multiple ways to try and solve this with no luck and spend almost half my day on this one issue. Could anyone point out what I need to change to get this to work.
Set rng = Range("D4", Range("D4").End(xlDown))
rng.NumberFormat = "0"
rArray = rng.Value
For x = UBound(rArray) To LBound(rArray) Step -1
Debug.Print rArray(x)
Next x
Edit: another fact worth mentioning is that he array is declared and used within a Function but it is not passed from or to the function. Can't arrays be declared and used in Functions?
When you assign worksheet values to a variant array, you always end up with a 2-D array that is 1 based (e.g. 1 to something, 1 to something; never 0 to something, 0 to something). If you are getting values from a single column the second Rank is merely 1 to 1.
This can be proven with the following.
Dim x As Long, rArray As Variant, rng As Range
Set rng = Range("D4", Range("D4").End(xlDown))
rng.NumberFormat = "0" 'don't really understand why this is here
rArray = rng.Value
Debug.Print LBound(rArray, 1) & ":" & UBound(rArray, 1)
Debug.Print LBound(rArray, 2) & ":" & UBound(rArray, 2)
For x = UBound(rArray, 1) To LBound(rArray, 1) Step -1
Debug.Print rArray(x, 1)
Next x
So you need to ask for the element in the first rank of the array; it is insufficient to just ask for the element.
I found an answer about get the most common value (String) of a column with VBA as below link but I do not understand why we must transpose the .Items.
is it possible to get the most common value (String) of a column that is the result of a filter? VBA
oMax = Application.Max(Application.Transpose(.Items))
Can anyone explain the reason why?
I tried with both Horizontal (7 columns & 2 rows) and vertical (2 columns & 7 rows) for above code without transpose function but it is still working.
Fully code in the answer as below:
Public Function ModeSubTotal(rng As Range) As String
Dim Dn As Range
Dim oMax As Double
Dim K As Variant
Dim val As String
With CreateObject("scripting.dictionary")
.CompareMode = vbTextCompare
For Each Dn In rng
If Dn.Rows.Hidden = False Then
If Not .Exists(Dn.Value) Then
.Add Dn.Value, 1
Else
.Item(Dn.Value) = .Item(Dn.Value) + 1
End If
End If
Next
oMax = Application.Max(Application.Transpose(.Items))
For Each K In .keys
If .Item(K) = oMax Then
val = val & K & ","
End If
Next K
ModeSubTotal = Left(val, Len(val) - 1)
End With
End Function
Application.Max function works on 1-dim and 2-dim arrays
I do not understand why we must transpose the .Items.
Me neither; transposing only transforms the "flat" 1-dimensioned .Items array into a 1-based 2-dim array. Application.Max() works on both dimensions without changing the result regardless of a possible internal search direction :-)
Possible background:
In many cases one executes a double transposing of a data row with the effect to get a transposed 2-dim array first and eventually a "flat" & 1-dimensioned array in the next step as number of functions work only on a 1-dim array. Maybe this got confused in the above code.
I want to rotate values from a separate Excel file like this:
To a vertical list:
I mean, after every ninth value, I want the program to start a new column (as in the picture).
I manage to do this manually (ofc B) ) when the different "sheets" are on the same document (Sheet1, Sheet2 etc).
Is this even possible what I am trying, without too much further programming? Should I be using Excel macros? I appreciate all the help I get..!
Store the data matrix in an array and slice off transposed 'rows' of values.
Dim x As Long, vVALs As Variant
With Worksheets("Sheet1")
vVALs = .Range("A2:I5").Value2
For x = LBound(vVALs, 1) To UBound(vVALs, 1)
.Cells(7, 1).Offset((x - 1) * UBound(vVALs, 2)).Resize(UBound(vVALs, 2), 1) = _
Application.Transpose(Application.Index(vVALs, x, 0))
Next x
End With
I am executing the VBA code below in the following file:
http://www.filedropper.com/error13
I get an Error 13 Type Mismatch.
Here is the macro
Aggregate, Collate and Transpose rows into columns
Works fine when I select some of the rows (for example id 1001 or 1003 and 1004 together but when I try to process more rows I get Error 13.
I'm trying to process each id at a time but I have about 100..
For the sake of curiosity and intellectual exercise, I took a stab at the array processing method originally submitted by ZygD in the original question with a mind to overcome the Runtime error '13': Type mismatch error on larger data sets.
The ReDim Preserve statement can only redimension the last rank while preserving existing values already stored subject to the fact that you are raising the size of the array dimension and not shrinking it. This is what msdn.microsoft.com has to say on the subject:
Resizing with Preserve. If you use Preserve, you can resize only the last dimension of the array. For every other dimension, you must specify the bound of the existing array.
For example, if your array has only one dimension, you can resize that dimension and still preserve all the contents of the array, because you are changing the last and only dimension. However, if your array has two or more dimensions, you can change the size of only the last dimension if you use Preserve.
Due to the orientation of the data peeled out of the worksheet, the first rank was the dimension to grow with ReDim so the orientation was flipped with Application.Transpose, ReDim'ed with Preserve then flipped back. As the array grew with additional records, the Application.Transpose quickly reached its maximum capacity to reorient the array. I found some older documentation on this in XL: Limitations of Passing Arrays to Excel Using Automation but it is horribly out of date.
My solution was to transpose the values from ar1 into ar2 on the fly so that ar2 could be redimensioned without reorientation. Once processing was complete, the results were in the wrong orientation. To get the values back into the worksheet in the correct orientation, I wrote a helper function that transposed ar2 back into a truncated ar1. This pseudo-transpose was only needed once; just before stuffing the new aggregated values back into the reporting area.
Modified sub code:
Sub jpd_Transposing()
Const sDestination As String = "D2"
Dim ar1 As Variant
Dim ar2 As Variant
Dim i As Long 'counter
With ActiveSheet
ar1 = .Range("A2:B" & .Cells(Rows.Count, 1).End(xlUp).Row).Value
ReDim ar2(1 To 2, 1 To 1)
ar2(1, 1) = ar1(1, 1): ar2(2, 1) = ar1(1, 2)
For i = 2 To UBound(ar1, 1)
If ar1(i, 1) = ar2(1, UBound(ar2, 2)) Then
ar2(2, UBound(ar2, 2)) = ar2(2, UBound(ar2, 2)) & ar1(i, 2)
ElseIf ar1(i, 1) = vbNullString Then
ar2(2, UBound(ar2, 2)) = ar2(2, UBound(ar2, 2)) & " "
Else
ReDim Preserve ar2(1 To 2, 1 To UBound(ar2, 2) + 1)
ar2(1, UBound(ar2, 2)) = ar1(i, 1)
ar2(2, UBound(ar2, 2)) = ar1(i, 2)
End If
Next
ar1 = my_2D_Transpose(ar1, ar2)
.Range(sDestination).Resize(UBound(ar1, 1), UBound(ar1, 2)) = ar1
End With
End Sub
Function my_2D_Transpose(a1 As Variant, a2 As Variant)
Dim a As Long, b As Long
ReDim a1(1 To UBound(a2, 2), 1 To UBound(a2, 1))
For a = LBound(a2, 1) To UBound(a2, 1)
For b = LBound(a2, 2) To UBound(a2, 2)
a1(b, a) = Trim(a2(a, b))
Next b
Next a
my_2D_Transpose = a1
End Function
So now you might be wondering just how much improvement over the original worksheet based routine there was with the arrayed memory processing. As that was the logical next step, I ran both with a timer noting start and stop.
Sub timed()
Application.ScreenUpdating = False
Application.EnableEvents = False
Debug.Print Timer
Call concatenate_and_transpose_to_delim_string
Debug.Print Timer
Call jpd_Transposing
Debug.Print Timer
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
Both of the report data results were identical. Note that I turned off screen updating and event handling for the duration of the test. This likely improved the worksheet method more than it improved the array method but I thought it was fair given that these are pretty standard techniques for improving the efficiency of a macro.
Timed Results:
Test environment: 45,894 rows × 2 columns of raw data converted to 123 rows × 2 columns of aggregated report data using a business class i5/8Gb based laptop (Win7, Excel 2010 version 14.0.7145.5000 (32-bit)
concatenate_and_transpose_to_delim_string (worksheet) .... 00:01.01 seconds¹
jpd_Transposing (memory array) ............................................... 00:00.07 seconds¹
¹Test was run several times. Times are typical.
Conclusions:
Okay, so we picked up almost a full second using a variant memory array over the worksheet read/write but that is still a whopping 93% improvement in efficiency of processing identical data to identical results. I've converted other long-running routines from worksheet driven to arrayed memory; the results were at least as appreciable and those were devoted to more repetitious lookup-type operations in large data matrices.
Was it worth it? That's pretty much up to the individual user and situation. Certainly there are benefits to be had but I write worksheet based code a lot faster than array based code so unless this ran several times a day every day, I probably wouldn't bother. The size of the project would also be a factor as benefits would increase with the amount of work to be done. Still, it's good to keep the methods used with memory array methods fresh in the mind and a mashup of methods may produce the best result in some cases.
FWIW, the VBA Trim function used in the transpose helper function produced no measurable detrimental effect (e.g. additional time) whether it was used or not and seemed the best place to ensure that the end result did not have a trailing space character left over from string concatenation.
First off, I apologize for starting a new thread but the original got confusing because I couldn't articulate my ask well (Link to original thread: Dynamic Nested Loops for Autofilter in Excel VBA). But now I have actually written the program to the way I like except using a switch statement instead of the more dynamic use of nested looping.
edit:
RSum is used to store a range and a boolean. The user selects the header cell for a column and chooses whether they want get a summation of that column or a unique count when summarizing. This allows for a collection of these objects to allow summarizing of multiple columns. This input wasn't so bad to make dynamic. The next input which starts as rtemp and ends as array1, is again the user selects the header cell for a column but this it takes the values in that column and saves a unique list to array1. With this list a for loop loops through the array using its value as criteria for an autofilter. For each step in the loop after the autofilter, the summary is calculated using the SumThisA taking the RSum object collection as an input. The data is laid out in columns where each row is a unique record.
So the question is, for the below code, I want the user to be able to select the number of categories to summarize by, have a popup to fill in those ranges (can figure this out), and then run a filter as such:
for i = 0 to UBound(array1)
Autofilter criteria1:=array1(i)
for j = 0 to UBound(array2)
Autofilter criteria1:=array2(j)
......
for x = 0 to UBound(arrayx)
Autofilter criteria1:=arrayx(x)
aSum(i,j,....x) = somefunction
Now I understand I would need to use a recursive function, but having never used one before and the somewhat complexity of this program, it is out of my understanding. Would anyone be able to help explain how to use it in this context? Plus because of the generalization of this program, it could be a useful tool for many people.
'---------Initialize Arrays---------------'
t = sMax - 1
Dim aSum()
ReDim aSum(UBound(arr1), t)
'---------------------Perform Summary----------------'
For i = LBound(arr1) To UBound(arr1)
If i = 0 Then
Data.AutoFilter field:=afield, Criteria1:=arr1, Operator:=xlFilterValues
Else
Data.AutoFilter field:=afield, Criteria1:=arr1(i)
End If
temp = SumThisA(SumValues, sMax)
For j = LBound(temp) To UBound(temp)
aSum(i, j) = temp(j)
Next j
Next i
Sum of Dollars For:
1. arrayA(1)-------100
- arrayB(1)------30
- arrayB(2)------70
2. arrayA(2)-------200
- arrayB(1)-----120
- arrayB(2)------80
3. Total-----------300
Here's a very kludgy example of recursion for what it seems you want to do. I faked up some criteria, so don't get hung up on how I'm testing for that, what's important is how the function Filter functions recursively. If I could pinpoint more exactly what you wanted I could craft it more precisely, and with less hardcoding.
Test Harness:
Public Sub Test()
Dim FilteredArray As Variant, cArray As Variant, working Array As Variant
Dim criteria As Integer
criteria = 1
ReDim criteriaArray(1 To 2)
cArray(1) = Range("C1").Value
cArray(2) = Range("C2").Value
Set workingArray = Range("A1:A7")
FilteredArray = Filter(workingArray, 7, cArray, criteria)
Range("D1") = FilteredArray
End Sub
Recursive Filter Function:
Public Function Filter(workingArray As Variant, index As Integer, _
criteriaArray As Variant, criteria) As Variant
Dim tempArray As Variant, i As Integer
ReDim tempArray(1 To 1)
For i = 1 To index
If Mid(workingArray(i), criteria, 1) = criteriaArray(criteria) Then
ReDim Preserve tempArray(1 To UBound(tempArray) + 1)
tempArray(UBound(tempArray) - 1) = workingArray(i)
End If
Next i
ReDim Preserve tempArray(1 To UBound(tempArray) - 1)
If criteria < 2 Then
Filter = Filter(tempArray, UBound(tempArray), criteriaArray, criteria + 1)
Else
Filter = tempArray
End If
End Function
Have you considered using a pivot table ? Your requirements seem very close to that functionality...