I am building an application which must interpret a set of instructions set by the user, instructions which will not be known until runtime.
I am achieving this by using the Evaluate function, as so:
Evaluate("5>4")
Which returns True, as one would expect. My question is how do I evaluate the following statement:
5 is greater than 4, but less than 10
I will be substituting the numbers with variables, of course.
I realise I could split the string into an array of two parts and test individually, but there must be a way of passing a single argument to test.
Application.Evaluate evaluates Formulas so AND(5>A4,5<10) or (5>A4)*(5<10) (results in 0 or 1)
Another alternative could be ScriptControl Eval, but it can't access Excel addresses like Evaluate
With CreateObject("ScriptControl")
.Language = "VBScript" ' either VBScript or JScript
Debug.Print .Eval("5>4 and 5<10")
End With
Related
What's the right way to call method when it comes to using or omitting parentheses? If I understand the results of my google search correctly: you have to use parentheses when assigning the return value of a method (or function) to a variable. Below are some examples:
wbData.Sheets.Add '-> works
Set wsData = wbData.Sheets.Add '-> works
wbData.Sheets.Add(Before:=wbData.Sheets(wbData.Sheets.Count)) '-> syntax error
Set wsData = wbData.Sheets.Add(Before:=wbData.Sheets(wbData.Sheets.Count)) '-> works
wbData.Sheets.Add Before:=wbData.Worksheets(wbData.Worksheets.Count) '-> works
Set wsData = wbData.Sheets.Add Before:=wbData.Worksheets(wbData.Worksheets.Count) '-> syntax error
Just to make sure I get the VBA logic: #3 gives me an error because the parentheses to VBA means the value (= the new worksheet) gets returned, but there's no variable to assign it to? And #6 is the opposite case?
Even if my attempt at an explanation were correct, could someone explain to me why the example on the official help page is not working for me:
ActiveWorkbook.Sheets.Add(Before:=Worksheets(Worksheets.Count))
This gives me a syntax error, same as #3 in my list above. At this I'm just confused.
The "official help page" is on GitHub, it's actively maintained, and multiple changes are being merged every day. If there's an error in an example, open an issue for it, or better, submit a fix yourself!
The example is wrong, the parentheses either shouldn't be there, or the expression should be on the right-hand side of a Set assignment to some object variable.
you have to use parentheses when assigning the return value of a method (or function) to a variable
Correct.
When you don't capture the return value, you don't put the parentheses. If you do, the VBE gives you a hint. If you copied the example from the docs, it would look like this in the editor:
ActiveWorkbook.Sheets.Add (Before:=Worksheets(Worksheets.Count))
Note the space. If you captured the return value:
Set newSheet = ActiveWorkbook.Sheets.Add(Before:=Worksheets(Worksheets.Count))
No space.
Just to make sure I get the VBA logic: #3 gives me an error because the parentheses to VBA means the value (= the new worksheet) gets returned, but there's no variable to assign it to? And #6 is the opposite case?
There's more to it than that. Consider a simpler example:
MsgBox "hi", vbOkCancel
If we wanted to capture or otherwise use the return value, we would need the parentheses:
If MsgBox("hi", vbOkCancel) = vbOk Then
If we added parentheses without capturing/using the return value, we would have this:
MsgBox ("hi", vbOkCancel)
So what does this space mean?
To the VBA compiler, it means "this isn't the argument list, it's the first argument, and this is a value expression: evaluate it, and send the result ByVal to the invoked procedure". The problem, of course, is that ("hi", vbOkCancel) isn't an expression, and can't be evaluated, and we have a compile error.
So back to the docs example: Before:=Worksheets(Worksheets.Count) isn't a legal expression either - it's an argument list consisting of one named argument... but syntactically it's not the argument list: parenthesized, it's an expression that, if it could be evaluated, would be passed to the first argument of the parameter list, ByVal - like so:
ActiveWorkbook.Sheets.Add Argument1:=(the result of the expression)
The ByVal nature of the parenthesized argument is basically an accident: when VBA evaluates the expression, it gets a value... but that value is up in the air, there's no local reference to it - so even though the invoked procedure is accepting a ByRef argument, since the caller isn't holding a reference to that argument, it's discarded - effectively producing the exacty same result as if the function took the parameter ByVal.
Confusing? This should help:
Public Sub Test()
Dim foo As Long
DoSomething (foo) ' evaluates the expression, passes the result of that expression
Debug.Print foo ' prints 0
DoSomething foo ' passes a reference to the local variable
Debug.Print foo ' prints 42
End Sub
Private Sub DoSomething(ByRef value As Long)
value = 42
End Sub
Does the method return a value you need? Use parentheses (but optional if not passing any arguments to the method unless you're using the return value in the same line).
Eg - below RowRange returns a Range object, but you can't then index directly into it using (2,1), since that is interpreted as passing arguments to RowRange (which doesn't take any)
s = myPivotTable.RowRange(2, 1).Value 'fails with "too many parameters"
Adding the parentheses clean this up:
s = myPivotTable.RowRange()(2, 1).Value 'OK
Using Call ? Use parentheses. But Call is typically considered as deprecated.
Anything else? Parentheses not required, and may produce unexpected outcomes by causing arguments to be evaluated before being passed.
One thing to watch out for is when the Vb editor puts a space between the method name and the opening parenthesis - when that happens it's a sign you might not need the parentheses at all.
I am writing the following simple routine:
program scratch
character*4 :: word
word = 'hell'
print *, concat(word)
end program scratch
function concat(x)
character*(*) x
concat = x // 'plus stuff'
end function concat
The program should be taking the string 'hell' and concatenating to it the string 'plus stuff'. I would like the function to be able to take in any length string (I am planning to use the word 'heaven' as well) and concatenate to it the string 'plus stuff'.
Currently, when I run this on Visual Studio 2012 I get the following error:
Error 1 error #6303: The assignment operation or the binary
expression operation is invalid for the data types of the two
operands. D:\aboufira\Desktop\TEMP\Visual
Studio\test\logicalfunction\scratch.f90 9
This error is for the following line:
concat = x // 'plus stuff'
It is not apparent to me why the two operands are not compatible. I have set them both to be strings. Why will they not concatenate?
High Performance Mark's comment tells you about why the compiler complains: implicit typing.
The result of the function concat is implicitly typed because you haven't declared its type otherwise. Although x // 'plus stuff' is the correct way to concatenate character variables, you're attempting to assign that new character object to a (implictly) real function result.
Which leads to the question: "just how do I declare the function result to be a character?". Answer: much as you would any other character variable:
character(len=length) concat
[note that I use character(len=...) rather than character*.... I'll come on to exactly why later, but I'll also point out that the form character*4 is obsolete according to current Fortran, and may eventually be deleted entirely.]
The tricky part is: what is the length it should be declared as?
When declaring the length of a character function result which we don't know ahead of time there are two1 approaches:
an automatic character object;
a deferred length character object.
In the case of this function, we know that the length of the result is 10 longer than the input. We can declare
character(len=LEN(x)+10) concat
To do this we cannot use the form character*(LEN(x)+10).
In a more general case, deferred length:
character(len=:), allocatable :: concat ! Deferred length, will be defined on allocation
where later
concat = x//'plus stuff' ! Using automatic allocation on intrinsic assignment
Using these forms adds the requirement that the function concat has an explicit interface in the main program. You'll find much about that in other questions and resources. Providing an explicit interface will also remove the problem that, in the main program, concat also implicitly has a real result.
To stress:
program
implicit none
character(len=[something]) concat
print *, concat('hell')
end program
will not work for concat having result of the "length unknown at compile time" forms. Ideally the function will be an internal one, or one accessed from a module.
1 There is a third: assumed length function result. Anyone who wants to know about this could read this separate question. Everyone else should pretend this doesn't exist. Just like the writers of the Fortran standard.
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! :)
I'm intending to conduct a formula of the type:
=IF(VOL("Site";"Date")=0;"";VOL("Site";"Date"))
where VOL is a function I'm using through an Add-In. The limitations of this Add-In is, among others, that it is prohibited to call two Add-In function inside a single formula. I.e. the code I've written above is invalid and will result in an error.
Is there a way of achieving the following:
=IF(LHS=RHS;"Value if True";LHS) (2)
where LHS is Left hand side, RHS right hand side and the expression therefore checks if LHS is equal to RHS, and if so prints a corresponding value, else LHS, without having Excel evaluate LHS twice?
I haven't found any solution to this except importing the formula in one cell, and refer to that cell as the value to print if the logical expression in the IF statement is false, but this will become a quite extensive "double work". A solution like (2) would also become more readable, especially when LHS is of the type "'C:\pathtofile[filename]SheetName!'Cell".
Hope anyone has some clever solution to this
Here is one (rather ugly) way, just using formulas:
=IFERROR(1/IFERROR(1/vol("Site";"Date"),0),"")
This makes use of the IFERROR function, which kind of does what you want but only tests for errors. Division by zero results in an error, so the inner IFERROR returns zero if VOL is zero, and 1/VOL otherwise. Now we need to take the reciprocal again to return the original value, so we repeat the trick, this time returning "" if there is an error.
If you want to test for another value (e.g. 3), just use something like:
=IFERROR(3+1/IFERROR(1/(vol("Site";"Date")-3),0),"")
A much neater way would be to create a function in VBA which wraps the VOL function and does what you want:
Public Function MyVol(varSite As Variant, varDate As Variant) As Variant
MyVol = vol(varSite, varDate)
If MyVol = 0 Then MyVol = ""
End Function
Assuming you can call VOL from VBA.
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.