I'm trying to brush up a little on my VBA skills and I got stuck on arrays. I have the very simple code below, that takes in a few numbers in a vector, multiply with two and the return the numbers. But the cells are all 0? In locals the calculations are right, and the TestVector is populated correctly, so what seems to be the problem?
Function test(Vec)
n = Vec.Rows.Count
Dim TestVector
ReDim TestVector(n, 1)
For i = 1 To n
A = Vec(i) * 2
TestVector(i, 1) = A
Next i
test = TestVector
End Function
VBA arrays are 0-based as a default. It is possible to override this by using Option Base 1 at the top of the module, but that is generally frowned upon among VBA programmers. Instead: just declare the lower bounds:
ReDim TestVector(1 To n, 1 To 1)
Then your code will work as intended.
Even though Option Base 1 is probably not a good idea, using Option Explicit is an extremely good idea. It will save you a great deal of debugging time. You can do this once and for all by enabling Require Variable Declarations in the VBA editor options.
Related
In Excel, changing the value in one cell can change the values in other cells as formulas are propagated. Also, cells can be updated with new formulas at any time. How can I achieve a similar dynamic functionality inside of a 2-dimensional array created in VBA? The only formula that I need to be able to propagate here is summation, so nothing too fancy. For now, we can assume that there will be no circular references. Suppose also that I am already able to figure out a topological ordering of the spots in the array.
Here is an example with a 2X2 array.
Public Sub CanIExcelInVBA()
Dim my_array(1, 1) As Long
my_array(0, 0) = 1
my_array(0, 1) = my_array(0, 0) + 1
my_array(1, 0) = my_array(0, 1) + 1
my_array(1, 1) = my_array(1, 0) + 1
my_array(0, 0) = 2
Debug.Print my_array(1, 1) ' I want this to be a 5, but it's a 4 still.
End Sub
The last position in the array should update because the value at my_array(0, 0) was changed. Also, the summation formulas that are actually inside of the array will need to be able to be changed programmatically at run-time. This would involve conditional logic with variables already in VBA.
I believe that I could achieve what I want with pointers or by writing my array to the worksheet. I am leaning more towards pointers because I want to limit the amount of time spent reading and writing to the worksheet. I am using 64-bit Excel.
In another part of this project, I am already using the VBA stack implementation that uses pointers that I found here:
https://github.com/Evanml2030/Excel-ArrayFunctions/blob/master/ArrayFunctions.xlsm
The problem is that I don't really understand how his code works or how to modify it to fit my current situation.
my_array(i, j) accesses a value in the array, not pointer.
Also there is no mechanism to propagate recalculation in VBA. If you want to do such a thing, you can use a worksheet in Excel, setting formulas with e.g. Range("A2").Formula = "=A1+1" and getting values with Range("A2").Value in VBA. Otherwise, you have to reinvent an entire spreadsheet-like system.
I have a Function that calculates an average payout. The loop calculates down from x days to y days. As part of this function I have to interpolate a number using a specific range. However this is very slow.
I read that a method to speed up the code would be to read the range as values rather than a Range as VBA is slowed down by going to Excel each time the code is run.
Is this true?
My current code.
Function AveragePayout(Time As Double, period)
Dim i As Integer
Dim sum As Double
Dim interpolate_surface As Range
Set interpolate_surface = Range("A1", "D4")
If Time < period Then
AveragePayout = 0
Else
For i = 1 To period
interpolated_val = Interpolation(interpolate_surface, 5, Time)
sum = sum + CustomPricer(interpolated_value)
Time = Time - 1
Next i
AveragePayout = sum / period
End If
End Function
I was thinking to change line 5 to the below to then run the Interpolation on an VBA matrix/array rather than returning to the Excel document each loop (which apparently slows the function tremendously:
Set interpolate_surface = Range("A1", "D4").Value2
Alternatively are there any other methods to speed up the running of this loop?
Many thanks.
While R.Leruth is very close, there are a few things that need to be elaborated on.
First, the reason why a Range object is slower is because you are working on the Object representation of that value, and there are events bound to that Range. As a result, calculations will run, the sheet will need to be evaluated, and accessing the value has to go through the Object, and not through an in-memory representation of that object.
This performance decrease generally stands true for any Range operations, and the performance decrease is directly tied to the size of the range. Thus, operating on 100 cells is much quicker than operating on 1,000,000 cells.
While performance time of an array is also directly linked, accessing each value is much quicker. This is because the the values are in-memory and easy to access. There are no Objects to depend on with an array. This doesn't mean arrays will always be fast. I have encountered instances of array operations taking many minutes or hours because I took their initial speed for granted. You will notice a performance decrease with arrays, but the rate of performance decrease is much much much lower.
To create an array, we use the Variant type. Keep in mind a Variant can be anything, so you have to be somewhat careful. General convention is to use Dim Foo as Variant() but then any argument that accepts or returns a Variant() must be given a Variant() and not a Variant (minor difference, huge impact on code). Because of this, I tend to use Dim Foo as Variant.
We can then assign the Values from a range back to the array. While Foo = Range("A1:B2") is functionally equivalent to Foo = Range("A1:B2").Value, I strongly recommend full qualification. Thus, I don't rely on implicit properties as much as I can (.Value is the implicit property of Range).
So our code should be:
Dim Foo as Variant
Foo = SomeRange.Value
Where Foo is your variable, and SomeRange is replaced with your range.
As long as your Interpolate function accepts an array, this should cause no issues whatsoever. If the Interpolate function doesn't accept an array, you may need to find another workaround (or write your own).
To output the array, we just need to create a range of the same size as our array. There are different ways of doing this. I tend to prefer this method:
SomeRange.Resize(UBound(SomeArray, 1) - LBound(SomeArray, 1) + 1, Ubound(SomeArray, 2) - LBound(SomeArray, 2) + 1)
All this does is takes some range (should be a single cell) and resizes that range by the number of columns in the array, and the number of rows in the array. I use (Ubound - Lbound) + 1 since, for a 0-based array, this will return Ubound + 1 and for a 1-based array it will return Ubound. It makes things much simpler than creating If blocks for the same purpose.
The last thing to make sure in all of this is that your Range variable is fully-qualified. Notice that Range("A1:B2").Value is functionally equivalent to ActiveSheet.Range("A1:B2").Value but again, relying on implicit calls quickly introduces bugs. Squash those out as much as possible. If you need the ActiveSheet then use it. Otherwise, create a Worksheet variable and point that variable to the correct sheet.
And if you must use the ActiveSheet then Dim Foo as Worksheet : Set Foo = ActiveSheet is much better than just using the ActiveSheet (since the ActiveSheet will generally change when you really need it not to, and then everything will break.
Best of luck in using arrays. They are performance changing but they are never an excuse for bad coding practices. Make sure you use them properly, and that you aren't introducing new inefficiencies just because you now can.
What we usually do in VBA to speed up macros is to decrease the amount of interaction between the code and the sheet.
For example :
Get all necessary values in an array
Dim arr() as Variant
arr = Range("A1:D4")
Treat the value
...
Put them back
Range("A1:D4") = arr
In your case just try to change interpolated_surface from Range to an array type.
In Matlab, what is the preferred way to apply operations that make use of the indices of elements they're accessing? Some simple scenarios:
A(i, j) = A(i, j) + 2*i + 3*j
A(i,j) = A(i,j) + A(i+1,j+1)
Besides using loops, is there any straightforward way to make use of the matrix elements' indices when carrying out operations like these? Answers to similar questions, such as "Initialize MATLAB matrix based on indices" make extensive use of repmat(). While solutions involving repmat() work, it is not easy for someone not proficient in Matlab (such as myself) to develop if the problem is somewhat complicated.
There's often nothing wrong with using a for loop, so bear that in mind.
For your first case, I can't think of any solution without using repmat, arrayfun or similar. Something that could work is something like the following:
[m,n]=size(A)
A=A+2*ones(m,1)*(1:n)+3*(1:m).'*ones(1,n)
using matrix multiplication, but I agree that this is not very obvious!
In your second example, Matlab's indexing can help. It's not clear what you want to happen to elements along the last row/column of A, but you can do something like this:
A(1:end-1,1:end-1)=A(1:end-1,1:end-1)+A(2:end,2:end)
although you may want to make a new matrix or save your old one if you need to do more things to it.
Your question is quite broad, and there are many techniques, hopefully these two give you some ideas, and don't automatically reject using a for loop either. There are also lots of handy Matlab functions that can help with this sort of thing that you will see pop up in answers here.
For the first case, you could make use of bsxfun, which is a function which carries out a Binary operation with Singleton Expansion on two arrays. It takes in two arrays and copies along dimensions with size==1 so that the arrays have the same size, then performs a binary operation on them. For your first example, you can do the following:
i = 1:10; % range for the first dimension
j = (1:5)'; % range for the second dimension, note the transpose
A(i, j) = A(i, j) + bsxfun(#plus,2*i,3*j);
for the second case, it's a simple matter of doing exactly what you've got there
% define i and j - make sure that you won't get an out of bounds error
i = 1:10; % range for the first dimension
j = 2:8; % range for the second dimension
A(i,j) = A(i,j) + A(i+1,j+1)
I'm trying to write a simulator in VB (Excel macro) where the input to a simulation is taken from cells in one sheet. The input will be placed in a number of arrays, for example timePerUser(10) and bytesPerUser(10). Then there will be some simple if/for/while stuff to make calculations based on the arrays and finally I will write the results back to Excel. SO, Excel will only be used to provide input data and to display the results, everything else is happening inside of the macro, including changing values in the arrays.
I am used working with Matlab but can't use it for this simulator, so here are my questions:
Are there any existing matrix/array operations I could use within an Excel macro? For example, is there some command to check the smallest or the next smallest value in an array? The Excel function "SMALL" would be perfect, but it doesn't seem to work in macros. Or do I simply need to solve this with for-loops?
Are there any other suggestions on how to create the arrays? Is it better to have one big matrix where each row corresponds to time, data, user etc (an NxM matrix) or is it better to have separate arrays for each parameter?
How to speed up matrix/array operations? Any general suggestions?
Thanks!
Oscar
Excel does have some simple array operations - SUMPRODUCT (Matlab equivalent: A*B'), MIN, MAX, ... Most of these, including SMALL, need to be called using Application.WorksheetFunction.xxx ; for example, the function SMALL can be called in VBA with the following:
nthSmallest = Application.WorksheetFunction.Small(r, n)
where r is an array or range, and n is an integer between 1 and r.Cells.Count
I would strongly urge you to use one variable per conceptual variable, rather than trying to be clever with multiple entities in one 2D array. Speedwise I suspect the difference is small; but the opportunity to write unreadable code is significant.
As for speeding things up: it really helps to define your variables (and especially your arrays) as some fixed type (rather than Variant), e.g.:
Dim a(1 To 10) as Double
Dim ii as Integer
and it may help a little bit to have all of the arrays use the same (default) base. Since I'm a Matlab junkie myself, I often work with array indices starting at 1 - you enforce this in VBA by adding
Option Base 1
at the start of your module. At this point, if you declare
Dim a(10) as Double
it is equivalent to
Dim a(1 To 10) as Double
Is also good practice for someone who is not proficient in VBA, to add
Option Explicit
at the start of the module, as it will ensure that any variables you did not declare (or more often, that you misspelled) will generate an interpreter error.
One other thing about arrays: as a Matlab person you are used to Matlab increasing array sizes as needed. In VBA it is possible to change array size dynamically at runtime, using, for example
ReDim Preserve a(1 To 20)
which will make the array a 20 elements in size. If it was shorter, it will be padded with zeros; if it was longer, it will be trimmed. This CAN be useful, but is QUITE expensive, since often the entire array needs to be copied to a new location. When you don't know in advance how large your array will be, it's better to overestimate (and check bounds) than to ReDim every time it needs to get bigger - and do a final ReDim at the end to get it to the right size.
Hope this is enough to get your started!
Hi everybody: Following liitle issue:
Option Base 1
Sub Test()
Dim aa() As Integer
Dim bb() As Integer
ReDim aa(3)
ReDim bb(3)
For j = 1 To 3
aa(j) = j * 2
bb(j) = j * 3
Next j
End Sub
Now the only little thing that I want to do is to multiply the two one dimensional arrays elementwise without looping, and then to unload this new array (6,24,54) in a range. I'm sure this must be easily possible. A solution that I would see is to create a diagonal matrix (array) and then to use mmult, but I'm sure this is doable in a very simple manner. Thanks for the help.
There is no way to do multiplication on each element in the array without a loop. Some languages have methods that appear to do just that, but under the hood they are looping.
As you've mentioned in your comments, you have 2 choices:
Loop through a range and do the multiplication
Dump the range into an array, do the multiplication, then dump back onto a range
It all depends on your data, but almost always you'll find that dumping a range into a variant array, doing your work, and dumping it back will be much faster than looping through a range of cells. How you dump it back into a range will also affect the speed, mind you.
It is possible to multiply ranges without explicit looping, e.g. try:
sub try()
[c1:c3].value = [a1:a3 * b1:b3]
end sub
Same logic as in:
=sumproduct(a1:A3-b1:b3;a1:A3-b1:b3)
I'm not entirely sure that this is possible in any language (with the possible exception of some functional languages). Perhaps if you told us why you wanted to do this we could help you more?