Excel VBA calling Sub from Function, recursive Function, Array Function recalculation - arrays

I’m stuck, please help. I have a small dynamic length array (6:20 x 2 text/numeric) and I want to automatically create a sorted copy so I can plot a monatonic scatter graph. To recalculate automatically, I was thinking Function. I plagiarised QuickSortArray, which works for the sorting as a Sub.
Q1: I can’t manage to call QuickSortArray from a Function even though I can successfully call it from another Sub. The working Sub call is below. I changed Sub QuickSortArray to Function QuickSortArray invoked from the spreadsheet, but could not get it to work. As it does work from a Sub, can I trick CallSub into running when the data changes? That makes the next two questions moot.
Sub CallSub()
temparray = Range("weekdays")
Call QuickSortArray(temparray, , , 2)
Dim MyRange As String
MyRange = "daysout"
Range(MyRange) = temparray
End Sub
Q2: I notice that QuickSortArray is recursive (calls itself): can this work within a function? The VBA I used is the top code here:
Sorting a multidimensionnal array in VBA . At the bottom it has these recursive calls
If (lngMin < j) Then Call QuickSortArray(SortArray, lngMin, j, lngColumn)
If (i < lngMax) Then Call QuickSortArray(SortArray, i, lngMax, lngColumn)
Q3: The code below is the only working way I have found to pass an array and retrieve it. Weekdays is a named range on the spreadsheet. However, Fn SortDays does not automatically recalculate, either as a single cell function, or (as I want to) when using Array arithmetic to retrieve the whole array? The function Test below it recalculates automatically and returns the results into an array of {=test($L$20)} in the spreadsheet ($L$20 = x). So it is not recalculation settings.
Function SortDays() As Variant
Dim InputArray As Variant
InputArray = Range("weekdays")
SortDays = InputArray
End Function
Function Test(x As Integer) As Variant
Dim V() As Variant
Dim N As Long
Dim R As Long
Dim C As Long
ReDim V(1 To 3, 1 To 4)
For R = 1 To 3
For C = 1 To 4
N = N * x + 1
V(R, C) = N
Next C
Next R
Test = V
End Function

Related

Prevent recalculations of User Defined Functions used in named ranges

I have three UDF's:
Private Function IsInArray(stringToBeFound As Variant, arr As Variant) As Boolean
IsInArray = Not IsError(Application.Match(stringToBeFound, arr, 0))
End Function
This function checks if something is in the array.
Private Function data_to_array(data As Range)
Dim arrArray As Variant
Dim cell As Range
Dim z As Integer
z = 0
ReDim arrArray(1 To data.Cells.Count)
For Each cell In data
z = z + 1
arrArray(z) = cell.Value
Next cell
data_to_array = arrArray
End Function
This function extracts selected range values and puts them into an array.
Private Function plot_vals(data As Variant, custom_arr As Variant)
Dim arrPlot As Variant
ReDim arrPlot(1 To UBound(data)) As Variant
Dim c As Integer
Dim cl As Integer
cl = 0
For c = 1 To UBound(data)
cl = cl + 1
If IsInArray(cl, custom_arr) Then
arrPlot(cl) = data(cl)
Else
arrPlot(cl) = CVErr(xlErrNA)
End If
Next c
plot_vals = arrPlot
End Function
The last UDF loops through the data array from second UDF and if index/position of value in data_array is in custom_array, then it returns its value. Otherwise it puts an error into an array.
Data looks like this:
These functions are used like this in Excel:
data_to_array(A1:A5) - this UDF creates an array(1 to 5) with values from cells A1:A5.
plot_vals(data_to_array(A1:A5), {1,5}) - this UDF creates an array(1 to 5), and uses second argument to retrieve first and fifth values while putting errors in the other indexes. The result is array of for example: {5,error,error,error,1}
If I used the function on above data like this: plot_vals(data_to_array(A1:A5), {1,2}) then the result would be an array {5,4,error,error,error}
That plot_vals UDF is used in named range and that named range is used to plot values on chart.
Data is stored in named range myData and the function in second named range is used like this:
plot_vals(myData,{1,5}).
Everything works, I can plot it on chart, all is good but when the named ranges are used on charts, every time I change something in my workbook all functions are recalculated like... 10 times each one, instead of once. It causes Excel to slow down/freeze if those functions are used many times. I have tried to search about function volatility and how to turn it off (it should be turned off by default?), but nothing seems to be working and I do not know how to stop that from happening. I have tried to recreate this in Excel using standard Excel functions in named ranges, but I cannot find a correct function to do what I want. UDF is exactly what I need.
When these named ranges are NOT used in charts then nothing happens, but as soon as I use the named range on chart then it recalculates everything on even a minor change of a workbook. Minor change meaning - copying / pasting / adding rows etc.
How can I stop that from happening? How to recalculate UDF's only once?
EDIT on further research:
I have tried the potential solutions provided by Charles Williams:
https://fastexcel.wordpress.com/2011/11/25/writing-efficient-vba-udfs-part-7-udfs-calculated-multiple-times/
His potential solutions do not change anything.
I have also tried using Sheet_Change event, changing calculations to manual and then back to automatic. It helps but it clears the clipboard (unacceptable) and it causes issues with my other macros, so it is a "no-go" solution.
It is also worth noting, that as soon as the chart is deleted and the UDF's remain in named ranges, everything is working nice and smooth. But when those named ranges are in chart series formulas, everything is recalculating 100's of times.
Aside from trying to fix the basic problem of how many times your functions get called, you can partially address the slow-down by optimizing the basic performance:
Application.Match is relatively slow unless the data being searched in on a worksheet
Reading a range into an array is slower than reading the whole range at once using its .Value (assuming range is a single area)
So:
Sub PerfTester()
Const ARR_SZ As Long = 10
Dim arr(1 To ARR_SZ), i, n, t, v, m
'populate a test array
For i = 1 To ARR_SZ
arr(i) = i
Next i
t = Timer
For n = 1 To 100000
v = Round(Rnd * ARR_SZ, 0)
m = IsInArray(v, arr) 'using match
Next n
Debug.Print Timer - t '~ 1.7 sec
t = Timer
For n = 1 To 100000
v = Round(Rnd * ARR_SZ, 0)
m = IsInArray2(v, arr) 'using a loop
Next n
Debug.Print Timer - t '~0.11 sec
t = Timer
For n = 1 To 100000
v = data_to_array(Range("A1:A50")) 'using cell-by-cell
Next n
Debug.Print Timer - t '~ 11.5 sec
t = Timer
For n = 1 To 100000
v = data_to_array2(Range("A1:A50")) 'using single read from range
Next n
Debug.Print Timer - t '~ 2.8 sec
End Sub
Private Function IsInArray(stringToBeFound As Variant, arr As Variant) As Boolean
IsInArray = Not IsError(Application.Match(stringToBeFound, arr, 0))
End Function
Private Function IsInArray2(stringToBeFound As Variant, arr As Variant) As Boolean
Dim i
For i = LBound(arr) To UBound(arr)
If arr(i) = stringToBeFound Then
IsInArray2 = True
Exit For
End If
Next i
End Function
Private Function data_to_array(data As Range)
Dim arrArray As Variant, cell As Range, z As Integer
z = 0
ReDim arrArray(1 To data.Cells.Count)
For Each cell In data
z = z + 1
arrArray(z) = cell.Value
Next cell
data_to_array = arrArray
End Function
Private Function data_to_array2(data As Range)
Dim arrArray As Variant, cell As Range, z As Long, v
v = data.Value
ReDim arrArray(1 To UBound(v, 1))
For z = 1 To UBound(v, 1)
arrArray = v(z, 1)
Next z
data_to_array2 = arrArray
End Function
You should be able to prevent unnecessary additional calculations by including
Application.EnableEvent = False
Application.Calculation = xlManual
at the start of your functions and
Application.EnableEvents = True
Application.Calculation = xlAutomatic
at the end of your functions. This prevents your spreadsheet from attempting to update and recalculate whenever you make a minor change. If you feel it necessary, you can add
Worksheet.Calculate
at some point in your code to force a recalculation of the current sheet.

accessing individual array elements in VBA function

VBA newbie here. I am trying to pass an array (it is static, but please answer for dynamic range as well) to a function. Then assign individual array elements to unique variables and use these variables in a custom formula. I just browsed around and wrote the code but keep getting #VALUE! error. The gist of the code is below:
Public Function mytest(ByRef arr1 As Range)
Dim A As Double
Dim B As Double
A = arr1(0)
B = arr1(1)
mytest = A + B 'The actual formula is a bit more complicated than simple addition
End Function
I am not sure what am i doing wrong at all. If anyone has a solution, can you please explain why it works as well. I appreciate any and all help I can get.
Many thanks !
As Coleman pointed out a range is not an array, consider:
Public Function mytest(ByRef arr1 As Range)
Dim A As Double
Dim B As Double
A = arr1(1, 1)
B = arr1(2, 1)
mytest = A + B 'The actual formula is a bit more complicated than simple addition
End Function
NOTE:
we treat the Range similar to an array
it is two dimensional
it is 1 based
if you are only dealing with the Range's value, you could create an internal array within your function that directly maps to the passed Range.
if the Range is truly dynamic, (like a Spill range) then all you need to pass is the anchor cell.
You seem to be trying to use a worksheet range as a 0-based array. That doesn't really make sense although using the Cells property of a range (which you are actually trying to do implicitly) you can come close:
Public Function mytest(arr1 As Range)
Dim A As Double
Dim B As Double
A = arr1.Cells(1)
B = arr1.Cells(2)
mytest = A + B 'The actual formula is a bit more complicated than simple addition
End Function
In the above code, you can drop Cells() since it will function as the default property here, but most experienced VBA programmers like to make explicit what property they are using.
This will more or less work for 1-dimensional ranges but might not work as expected with 2-dimensional ranges. Cells takes up to 2 indices and in general I think that the code is clearer when you are explicit about the full indices (e.g. A = arr1.Cells(1,1) and B = arr1.Cells(2,1)).
The question isn't in the code you posted but in the procedure that calls it. Here the calling procedure first assigns values to the cells in the worksheet (for testing purposes), then passes the range to the function which extracts the values into an array and then uses that array to calculate a return value.
Private Sub TestmyTest()
Dim Rng1 As Range
Cells(1, "A").Value = 3.14
Cells(2, "A").Value = 3
Set Rng1 = Range("A1:A2")
Debug.Print myTest(Rng1)
End Sub
Function myTest(Rng1 As Range) As Double
' procedures are 'Public' unless declared as 'Private'
' Therefore only declare "Private" or nothing
' Arguments are passed ByRef unless they are declared as 'ByVal'
' Therefore I recommend to omit "ByRef"
Dim Arr As Variant
Dim A As Double
Dim B As Double
' this creates a 1-based 3-D array of 2 row and 1 column
Arr = Rng1.Value
A = Arr(1, 1)
B = Arr(2, 1)
myTest = A + B 'The actual formula is a bit more complicated than simple addition
End Function

How to display a 1 or 2 dim varraint array() return from an excel UDF function in an excel range without Ctrl+Shift+Enter array forumla method

I've searched for solutions to this question for 3 days & have only found answers that either use Ctrl+Shift+Enter (array formula method) or functions which either return an empty range or which raise err 1004.
I'm using 64-bit Windows 8.1 and Excel-2013 write UDFs that return variant arrays from time to time with unknown array size returns... for example 'MyFunction(args...) as Variant()'. I can see the result in immediate window or write it to file or display it with Ctrl+Shift+Enter as an array formula.
What I want to do is use MyFunction() as an argument to a sub arr2rng(Myfunction) such that arr2rng() fills a range on the activesheet starting at ActiveCell inorder to avoid the whole manual routine of using array formula method (e.g. highlight a range of some size larger, by guessing, than the returned array, then the combination Ctrl+Shift+Enter to display the array).
I've even tried the long subroutine by Nile -- see his A generic VBA Array To Range function at VBA Excel 2-Dimensional Arrays,
Public Sub ArrayToRange(rngTarget As Excel.Range, InputArray As Variant)
but at every statement in 'ArrayToRange()' where 'rngOutput.Value2 = InputArray' occurs function bombs with err.Number 1004. Just before that statement is executed both 'InputArray' and 'rngOutput.VaAlue2' elements are correctly dimensioned and filled (from Immediate or Local Window observations)... though 'rngOutput.Value2'elments are still empty as they should be. After the statement executes 'rgnOutput.value2' elements are still empty though, and err = 1004 has been raised. This occurs no matter which one of his tests in the code are executed. I've even gone so far as to invoke his sub by my function at the end of my own VBA's as:
myFunction(args....) as Variant()
[do stuff...]
ConArr = vbaTransposeVar(ConArr)
Set DestCell = ACTIVE_CELL_DESTINATION
ArrayToRange DestCell, ConArr
myFunction = ConArr
End Function
where 'ConArr' is the name of the resultant variant() array and also the return from myFunction(), where 'ACTTIVE_CELL_DESTINATION' is declared Public as Range in the Module.
What I prefer to do however is just invoke the 'sub ArrayToRange myDest, myArray' or any other sub such as a generic 'sub arr2rng(myDest as range, myArray() as Variant)' either from within some other function that invokes the sub or do it manually from the Macro window.
Can anybody help or tell me why all I get are either an empty range of cells or the 1004 error? I guess what I'm really asking is how to get around using the array formula method Ctrl+Shift+Enter. There must be a way! u
This example worked for me in testing, BUT there is no code to account for clearing any previous values which resulted from any earlier calculations. If the current run results in a smaller array then previous values will not be cleared.
You could account for this maybe by tracking the last range filled and making sure you clear it first, but depending on your exact use case that might not be viable.
Function ChangeIt(func As Range, c1 As Range, c2 As Range)
Dim arr(), r, c, rv, v
For r = 1 To c1
For c = 1 To c2
v = "Row" & r & ":Col" & c
If r = 1 And c = 1 Then
rv = v 'return this to the calling cell via myArray
Else
'all other values are written directly
func.Offset(r - 1, c - 1).Value = v
End If
Next c
Next r
ChangeIt = rv
End Function
'this is called from the worksheet
Function MyArray(arg1 As Range, arg2 As Range)
Dim v
v = arg1.Parent.Evaluate("Changeit(" & Application.Caller.Address(False, False) & "," & _
arg1.Address(False, False) & "," & _
arg2.Address(False, False) & ")")
MyArray = v 'set the top-left array value
End Function

Dynamic Array of dynamic arrays

I can't seem to find this problem addressed anywhere.
I need to declare a bunch of dynamic arrays as follow:
Dim list1 () as variant
Dim list2() as variant
Dim list3() as variant
...
Dim listN() as Variant
Each list is a one-dimensional dynamic array. However, I wouldn't know what "N" will be during the program. I want to make these "N" lists dynamic as well. I have tried two-dimensional dynamic arrays. But the "redim" statement requires both dimensions to be declared at the same time. In particular, I do this:
Dim BigList() as variant
...
Redim BigList(listNum, listLength)
To access/pass into a sub "list1", "list2" , "list3"..., calling "BigList(1)", "BigList(2)" gives me error. In particular, somewhere in my code, there is this portion:
sub ProcessList(byref listToProcess() as variant)
...
end sub
sub main()
...
call ProcessList(list1)
call ProcessList(list2)
...
call ProcessList(listN)
end sub
Now I can do a loop:
for i = 1 to N
Call ProcessList(list"i")
next i
This requires list"i" to be a one-dimensional dynamic array. So, after a redim BigList(listNum,listLength) and I do this:
for i = 1 to N
Call ProcessList(BigList(i)) 'i refers to listNum
next i
This gives me error "Incompatible type".
Here is one example of creating a Dictionary which is keyed to integer values (i.e., the N) and each value is initially an empty array.
You can then use something like the ExtendList function to resize those empty arrays as needed.
Sub foo()
Dim BigList As Object
Dim N As Long
Dim v as Variant
'Create an empty dictionary object
Set BigList = CreateObject("Scripting.Dictionary")
'Add N empty array to the dictionary:
N = 3
For i = 1 To N
BigList(i) = Array()
Next
'Resize one of the items in your BigList
BigList(2) = ExtendList(BigList(2), 1, 10)
v = BigList(2) 'Here you can examine v in the Locals window and see it is an array, of dimensions 1 x 10
End Sub
Function ExtendList(lst, a As Long, b As Long)
ReDim lst(a, b)
ExtendList = lst
End Function
On review of your edited question, I think you merely misunderstood how the ReDim statement works:
Redim BigList(listNum, listLength)
This re-dimensions the BigList based on the parameters listNum and listLength. It does not (as it seems you may have expected) create a list of arrays within BigList.
I think this might also work (untested, and remember arrays are zero-index):
ReDim Preserve BigList(listNum)
BigList(listNum) = Array()
ReDim BigList(listNum)(listSize)

Assigning an array value to a variable in VBA causes my UDF to exit

I can't seem to figure out why this UDF is exiting on the currentInput = inputArray(i). Here is the relevant code:
Function OrderRange(inputRange As Range) As Variant
Dim length As Integer
inputHeight = inputRange.Count
Dim inputArray As Variant
inputArray = inputRange
Dim strippedArray() As Variant
ReDim strippedArray(0 To (inputHeight - 1))
Dim currentInput As String
Dim i As Integer
For i = 0 To (inputHeight - 1)
currentInput = inputArray(i)
'...computations on currentInput...'
strippedArray(i) = currentInput
Next i
OrderRange = strippedArray
End Function
The debugger reaches currentInput = inputArray(i) but once I move to the next line, the function terminates and a #VALUE! error is entered into the cell I call the function from. I know this is a specific question, but I'm sure it's a general issue and I'll edit this original post to reflect what the general problem is.
Edit: This is an issue regarding assignment of a range to a variant array.
Variant arrays created by setting them equal to ranges have two dimensions even if they are only one column, or row, wide. So if you call the function with A1:A10 you'll get a 10 * 1 array. Also the lower bound of the dimensions will be one, not zero. So you'd have to do something like:
For i = 1 To (inputHeight)
currentInput = inputArray(i, 1)
Also you should use Option Explicit so that you're reminded to declare all variables. InputHeight is never declared.

Resources