How to read another cell from within a UDF? - excel

I am aware that it's very hard to change the value of another cell using a User Defined Function - forbbiden by MS, even. However, I'd like to just read from another cell and do something with that information, but the function never runs thoroughly. For instance, in
Public Function ADD(arg1 as Double, arg2 as Double) as Double
If Worksheets("sheet1").Cells(1,1).Value = 0 Then
ADD = -1
Exit Function
End If
MsgBox "I got here"
ADD = arg1 + arg2
End Function
the message box does not show up. It can only be the if-statement. When I remove it, it works. Is there any way to read from another fixed cell without crashing the function? The real function I'm working with does all the reading from within if-statements.
Also, this can be found on the MS website, but I don't understand why it prevents me from reading..
For correct calculation, all ranges that are used in the calculation should be passed to the function as arguments. If you do not pass the calculation ranges as arguments, instead of referring to the ranges within the VBA code of the function, Excel cannot account for them within the calculation engine. Therefore, Excel may not adequately calculate the workbook to make sure that all precedents are calculated before calculating the user-defined function.
Thank you in advance!

I changed the UDF name to ADDD() and it works fine:
Public Function ADDD(arg1 As Double, arg2 As Double) As Double
If Worksheets("Sheet1").Cells(1, 1).Value = 0 Then
ADDD = -1
Exit Function
End If
MsgBox "I got here"
ADDD = arg1 + arg2
End Function

You cannot run a message box with a UDF. Can you image how many times you would have to click "OK" if you could and your sheet recalculated frequently?
Instead, you can use a debug.print statement, and you can run your UDF from the VBA IDE to check the debug values. For example:
Private Function TestMyUDF()
Dim examplevalue1 as Double
Dim examplevalue2 as Double
MyUDF examplevalue1, examplevalue2
End Function
Off-question, but if your referenced (read-only) cell is significant, then use a named range (and make it a workbook name, not a sheet one). Let us call it "CheckSumCell" for the sake of an example.
Public Function MyUDF(arg1 as Double, arg2 as Double) as Double
If ThisWorkbook.Names("CheckSumCell").Value = 0 Then '<-- assumes single cell
MyUDF = -1
Exit Function
End If
MyUDF= arg1 + arg2
End Function
This will then better survive cell relocation (insertion of rows or columns)! If you decide to use another cell, then just change the definition of the named range and you will not require to edit the macro/UDF at all.

Related

Why declare function's type?

Sorry if this has been asked, but seriously can't find anything, so would also appreciate on how to search for this stuff.
So my question: what is the point of declaring the function's type in general? E.g. here 'as double'
Function myFunction(ByVal j As Integer) As Double
Return 3.87 * j
End Function
For a normal variable it has tons of benefits, like less memory, easier to see typos, but why here?
Edit: so, it's good because we can avoid errors, like it giving back a different type of values than expected.
Functions RETURN something. That type is the type of the return.
In your function:
Function myFunction(ByVal j As Integer) As Double
Return 3.87 * j
End Function
You are returning a decimal, so type Double make sense.
If you don't return anything, then you can declare it as a Sub.
And, for clarification, your function would throw a compile error. Unlike other languages, in VBA to return, we set the function name's value to the thing we want to return:
Function myFunction(ByVal j As Integer) As Double
myFunction=3.87 * j
End Function
Now we can call this function to get the Double value that it creates:
Sub testSub()
msgbox("This is the result of the function: " & myFunction(10))
End Sub
Which would launch a message box saying "This is the result of the function: 38.7"
Since I can't mark a comment the answer, let me quote:
#John Coleman
My opinion is that it a good thing to declare your return types because it increases the likelihood that the compiler will complain when you are doing something that really doesn't make sense.
Excel VBA is different from other programming languages in that it centers around a particular application: Excel.
Functions are useful in Excel VBA primarily because they can be typed directly into a cell on a sheet by an end user. User defined functions provide near infinite flexibility. The value the user defined function prints to Excel is formatted based on the function's type--and in a program which is about data visualization, formatting is a huge part.
For example, try putting these four functions into a blank worksheet module:
Function myInt(x, y) As Integer
myInt = x / y
End Function
Function myDouble(x, y) As Double
myDouble = x / y
End Function
Function myString(x, y) As String
myString = x / y
End Function
Function myVariant(x, y)
myVariant = x / y
End Function
Next, enter each of these functions into a different cell in the workbook. Use x=1 and y=2.
myInt produces "=0"
myDouble produces "=0.5"
myString produces "'0"
myVariant produces "=0.5"
If you're okay with Excel deciding how to format your result, that's your choice, but specifying the type offers an entire new level of control. For example, by simply declaring a function an integer, you can avoid having to devote a line of code to rounding. By declaring a function to be a string, you can avoid several lines of formatting code trying to get a number to be saved as text instead.

How to implement an ISBETWEEN-like function for arrays of values without having to calculate the array more than once?

An ISBETWEEN function tests whether a value falls between a lower bound and a higher bound. With no native ISBETWEEN function in Excel, the value under test must be compared twice; first with '>' and then with '<' (or '>=' and '<=' for an ISBETWEEN test that is inclusive of the bounds.)
Comparing the value twice means having to calculate it twice, and this can be extremely expensive when that value is an array. With array functions being somewhat cryptic even at the best of times, doubling up on such a calculation also sends the readability of the function plummeting.
My question is whether anyone knows of a technique that delivers ISBETWEEN-like functionality for an array of values without the double calculation of that array? My preference is to do this with native Excel functionality but, if anyone has some great VBA, that would be good too.
Many thanks for your time!
Will
Building from my comment above: This doesn't provide a 100% answer to your question, but since it was pretty generic, I think this is the closest to an answer that I can get.
Imagine a spreadsheet set up like:
We can get a count of all the values that are between 3 and 5 using CTE/Array formula:
={SUM(IF(LOOKUP(A1:A6,{3,"B";6,"C"})="B",1,0))}
Results:
5
That's a pretty round-about way of doing this, but the array of A1:A6 only needs to be referenced once. Which is pretty cool.
Note that the squirrely brackets in the above formula aren't actually entered, but are placed by excel when you enter the array formula to indicate that it's an array formula... you probably already know that though if you've read this far.
So I've been able to develop a piece of VBA, based on the idea here.
Dim vValueArg As Variant, vLowerArg As Variant, vUpperArg As Variant, vTestLower As Variant, vTestUpper As Variant
Function ISBETWEEN(vValue As Variant, vLower As Variant, vUpper As Variant, Optional bInc As Boolean = True) As Variant
vValueArg = vValue
vLowerArg = vLower
vUpperArg = vUpper
If bInc Then
vTestLower = [GetValue() >= GetLower()]
vTestUpper = [GetValue() <= GetUpper()]
Else
vTestLower = [GetValue() > GetLower()]
vTestUpper = [GetValue() < GetUpper()]
End If
ISBETWEEN = [IF((GetTestLower() * GetTestUpper()) = 1, TRUE, FALSE)]
End Function
Function GetValue() As Variant
GetValue = vValueArg
End Function
Function GetLower() As Variant
GetLower = vLowerArg
End Function
Function GetUpper() As Variant
GetUpper = vUpperArg
End Function
Function GetTestLower() As Variant
GetTestLower = vTestLower
End Function
Function GetTestUpper() As Variant
GetTestUpper = vTestUpper
End Function
The first argument can be a single value, range or array. If a single value, then the next two arguments must also be single values (but this kinda defeats the purpose of the code!)
The second and third arguments can also be a single value, range or array. If a range consisting of multiple cells or array of multiple values, then the dimensions of these arguments must match those of the first argument. (NB - I have NOT tested the code with 2 dimensional ranges or arrays!)
The final, optional, argument determines whether the ISBETWEEN test is performed including or excluding the bounds. TRUE = include bounds; i.e. arg2 <= arg1 <= arg3 (the default, and can therefore be omitted). FALSE = exclude bounds; i.e. arg2 < arg1 < arg3.
While this might not be the prettiest code in the world, it is compact, fast (no loops) and copes with ranges and arrays of any size.
Hope some of you find this useful! :)

Excel VBA failure of repeated Evaluate method

I have written a little tool in VBA that charts a function you pass it as a string (e.g. "1/(1+x)" or "exp(-x^2)"). I use the built-in Evaluate method to parse the formula. The nub of it is this function, which evaluates a function of some variable at a given value:
Function eval(func As String, variable As String, value As Double) As Double
eval = Evaluate(Replace(func, variable, value))
End Function
This works fine, e.g. eval("x^2, "x", 2) = 4. I apply it element-wise down an array of x values to generate the graph of the function.
Now I want to enable my tool to chart the definite integral of a function. I have created an integrate function which takes an input formula string and uses Evaluate to evaluate it at various points and approximate the integral. My actual integrate function uses the trapezoidal rule, but for simplicity's sake let's suppose it is this:
Function integrate(func As String, variable As String, value As Double) As Double
integrate = value * (eval(func, variable, 0) + eval(func, variable, value)) / 2
End Function
This also works as expected, e.g. integrate("t", "t", 2) = 2 for the area of the triangle under the identity function.
The problem arises when I try to run integrate through the charting routine. When VBA encounters a line like this
eval("integrate(""t"",""t"",x)", "x", 2)
then it will stop with no error warning when Evaluate is called inside the eval function. (The internal quotes have to be doubled up to read the formula properly.) I expect to get the value 2 since Evaluate appears to try and evaluate integrate("t", "t", 2)
I suspect the problem is with second call on Evaluate inside integrate, but I've been going round in circles trying to figure it out. I know Evaluate is finicky and poorly documented http://fastexcel.wordpress.com/2011/11/02/evaluate-functions-and-formulas-fun-how-to-make-excels-evaluate-method-twice-as-fast but can anyone think of a way round this?
Thanks
George
Excel 2010 V14, VBA 7.0
Thanks Chris, your Debug.Print suggestion got me thinking and I narrowed the problem down a bit more. It does seem like Evaluate gets called twice, as this example shows:
Function g() As Variant
Debug.Print "g"
g = 1
End Function
Run from the Immediate Window:
?Evaluate("g()")
g
g
1
I found this http://www.decisionmodels.com/calcsecretsh.htm which shows a way round this by using Worksheet.Evaluate (Evaluate is actually the default for Application.Evaluate):
?ActiveSheet.Evaluate("g()+0")
g
1
However this still doesn't solve the problem with Evaluate calling itself. Define
Function f() As Variant
Debug.Print "f"
f = ActiveSheet.Evaluate("g()+0")
End Function
Then in the Immediate Window:
?ActiveSheet.Evaluate("f()+0")
f
Error 2015
The solution I found was define a different function for the second formula evaluation:
Function eval2(formula As String) As Variant
[A1] = "=" & formula
eval2 = [A1]
End Function
This still uses Excel's internal evaluation mechanism, but via a worksheet cell calculation. Then I get what I want:
?eval2("f()")
f
g
1
It's slower due to the repeated worksheet hits, but that's the best I can do. So in my original example, I use eval to calculate the integral and eval2 to chart it. Still interested if anyone has any other suggestions.

How to make VBA function "VBA only" and disable it as UDF

I'm writing a VBA function which I want to be publically available in other VBA modules within the same document, however I don't want it to be available as a UDF (User defined function).
If I use the public access modifier however my function is also be available as a UDF-formula that can be called from the cells within the workbook. I don't want this.
Is there an access modifier or other way that can help me obtain this "VBA only" behaviour?
Kind regards
If you use Option Private Module in the module in which the function appears, the function can be declared as Public and used in any of your other modules within your VBA project, but won't be accessible by other applications or projects, including Excel itself.
This will return a #VALUE error if used in Excel.
Function VBAOnly() As Variant
If TypeName(Application.Caller) <> "Range" Then
VBAOnly = 1 'or some other return value
Else
VBAOnly = CVErr(xlErrValue)
End If
End Function
There's a little bit of confusion among answers, so here's a more encompassing explanation:
Technically... all functions in the standard VBA Modules can be called if the exact formula is entered. Even Formulas in Private Functions within Private Modules
Example
Option Private Module
Private Function hiddenEverythingExample() As String
hiddenEverythingExample= "NOPE!!!"
End Function
Will still return a value if a cell has =hiddenEverythingExample()
However, I believe the objective of the OP was to avoid having the itellisense populate these functions in the Excel Formula bar.
The most frequent way I accomplish this is create a specific Module for all VBA-Only functions and put Option Private Module in the module definition (area above the functions).
This ensures all functions in this module will NOT appear in itellisense, but still be accessible to other modules along with itellisense.
Defining asPrivate Function also accomplishes this, but then the function is scoped exclusively to that Module, which may or may not be the requirement.
Note that YowE3K infers that a function must be both Private Function AND Option Private Module, but only one OR the other is necessary to eliminate the itellisense.
Instead of writing a Function, write a Sub, and set the return via a ByRef argument. This way your function will be invisible to Excel (except via Alt F8, or Developer tab > Macros) and won't appear in Excel's intellisense.
Instead of
Function Add(Num1 As Double, Num2 As Double)
Add = Num1 + Num2
End Function
use
Sub AddInvisible(ByRef Result As Double, Num1 As Double, Num2 As Double)
Result = Num1 + Num2
End Sub
Note
That ByRef is not strictly necessary (since arguments are by default by reference in VBA) but it serves as a useful reminder that Result carries the return value.
You will need to make the necessary changes to your code, for example:
z = Add(x,y)
would become
AddInvisible z,x,y
as demonstrated below:
Sub DemoAddInvisible()
Dim Num1 As Double
Dim Num2 As Double
Dim Result As Double 'Result initialises to 0
Num1 = 1
Num2 = 2
AddInvisible Result, Num1, Num2
MsgBox Result ' See that Result has become 3
End Sub
A downside of all this is that the new code is somewhat harder to understand.
pass a parameter that only allows the function to run if a "magic* value is given to it.
Example - This will give the error #NAME! unless you know what the key is:
Function VBAOnly(key As Long)
If key <> 12345 Then
VBAOnly = CVErr(xlErrName)
Exit Function
End If
VBAOnly = True
End Function
Using the Private modifier should only allow the execution in the module the function exists in.

Pass multidimensional array into Excel UDF in VBA

How do I pass multidimensional arrays or nested arrays into an Excel UDF which allow me to reference other ranges?
I have a UDF defined as "ARY" which does what Array() does in VBA but in a worksheet function.
This allows me to have a worksheet formula like
=TEST1(ARY(ARY("A", "B"), "C"))
or
=TEST1(ARY(ARY(A1, B1), C1)
However, I get Error 2015 when executing TEST1 as a worksheet function. If I execute TEST1 from VBA, it works fine and returns "A".
Public Function TEST1(Params As Variant) As Variant
TEST1 = Params(0)(0)
End Function
'Returns 1D ARRAY
Public Function ARY(ParamArray Params() As Variant)
ReDim result(0 To UBound(Params)) As Variant
Dim nextIndex As Integer
Dim p As Variant
nextIndex = 0
For Each p In Params
result(nextIndex) = p
nextIndex = nextIndex + 1
Next
ARY = result
End Function
In general, you can't do this. VBA is fine with nested arrays as values, but Excel just isn't. This makes sense, because Excel ultimately has to treat any values it gets from your UDFs as things that can go into the worksheet grid.
It would be nice if intermediate results didn't have to obey this restriction. In your example above, you're not trying to put a jagged array into any cells; you just want to create it and pass it to 'TEST1'. Unfortunately, Excel just doesn't work that way. It evaluates each expression in a formula to a legal Excel value, and #VALUE! is the catch-all for "not a legal value". (There are other limitations to returned arrays besides jaggedness. For example, arrays over a certain size also result in a #VALUE! error.)
Note that nested arrays that are all the same length can be passed back from UDFs:
Public Function thisKindOfNestingWorks()
thisKindOfNestingWorks = Array(Array(1, 2), Array(3, 4))
End Function
This can be useful when you're building up some list-of-lists that you actually want coerced to a 2-D array.
So, calling your ARY function above like this should work just fine:
=ARY(ARY(A1, B1), ARY(C1, D1))
However, your TEST1 function would fail, since calling
=TEST1(ARY(ARY(A1, B1), ARY(C1, D1)))
would result in a 2-D array being passed to TEST1, not a 1-D array of 1-D arrays.
You might also find this question and my answer to it to be helpful:
Return a User Defined Data Type in an Excel Cell
MUCH LATER EDIT:
Incidentally, your 'ARY' function could be a lot shorter. This has nothing to do with your original question, but it's worth mentioning:
Public Function arr(ParamArray args())
arr = args
End Function
There is an example of using it in this answer:
Excel Select Case?

Resources