Custom Function with cell reference inputs - excel

I'm new to excel custom functions and trying to create a custom Excel function that outputs a "Y" or "N" and takes four arguments. The error I'm getting on the other end is #VALUE! with no further explanation. I tried looking at the Range object in the MSDN docs and can't understand why my function isn't reading the values.
My function takes four arguments and the datatypes in the cells are median: Double, base: Double, hours: Integer, and exemption: String (it takes a letter "E" or "N"). I get those arguments from four separate cells in my worksheet.
Function ToIncrease(median As Range, base As Range, hours As Range, exemption As Range)
ToIncrease = "N"
exempt = exemption.Value
If exempt.Equals("N") Then
baseSal = base.Value * hours.Value
medianSal = median.Value * hours.Value
If medianSal > baseSal Then
ToIncrease = "Y"
End If
End If
If exempt.Equals("E") Then
If median.Value > base.Value Then
ToIncrease = "Y"
End If
End If
End Function
I first tried exemption.Value.Equals("N") and it didn't work, and then I tried to declare the exempt variable as Dim exempt As String = exemption.Value and got an error about expecting the end of a statement.

First replace lines like:
If exempt.Equals("N") Then
with
If exempt = "N" Then
(there may be other problems as well)
It is MUCH easier to debug VBA code as a Sub rather than a Function() because the error messages are much better.

Related

"Compile Error" while Creating a Function with Multiple ParamArray

I want to include a ParamArray in my function, but my efforts have failed so far.
Let's say I want to build a function to calculate mean for discrete distribution.
The function has the following form:
Function Dist_Discrete(X1, P1, X2, P2, X3, P3, etc.)
There are two solutions currently come to my mind:
Using optional arguments multiple of times.
Using ParamArray
The second one is preferred. But here is some problem when I try to define
Function Dist_Discrete(ParamArray XVal() As Variant, ParamArray Prob() As Variant)
And an error message comes up - "Compile error"
I can find a way to go around this by setting even as probability and odd as value. But I think this might be a good temporary solution
Your posted example shows the inputs as cells in a contiguous column like X1, X2. etc. If this is the case, the just use Range objects as the inputs:
Function Dist_Discrete(X as Range, P as Range) as Double
then in a worksheet cell:
=DistDiscrete(X1:X30, P1:P30)
ParamArray specifies that a procedure parameter takes an optional array of elements of the specified type. ParamArray can be used only on the last parameter of a parameter list. Thus 2 param arrays are not possible. (learn.microsoft.com)
However, you may think about a little trick in Excel. E.g. lets consider the following Function:
Function SumAndMultiply(valuesToSum(), valuesToMultiply()) as Double
which has the idea to sum the values of the valuesToSum() array and to multiply the result with each of the valuesToMultiply().
In VBA, if the first one is presented as a range, then it would work quite nicely:
Public Function SumAndMultiply(valuesToSum As Range, _
ParamArray valuesToMultiply() As Variant) As Double
Dim myCell As Range
Dim sum As Double
For Each myCell In valuesToSum
sum = sum + myCell
Next myCell
Dim param As Variant
SumAndMultiply = sum
For Each param In valuesToMultiply
SumAndMultiply = SumAndMultiply * param
Next
End Function

VBA Multiple Optional Function Parameters

I'm trying to write the SWITCH function in VBA for my coworker who has Excel 2013. I feel that my VBA is strong enough to code this function once I set up all my function parameters. However, I'm not sure how to have an unlimited number of optional parameters in a function (similar to *args in Python). How can I set up a function so that it may have an unlimited number of optional arguments?
You need to use ParamArray, e.g.
Public Function TestSum(ParamArray a())
Dim i As Long
For i = LBound(a) To UBound(a)
TestSum = TestSum + a(i)
Next i
End Function
An interesting question, here's my attempt at replicating the Switch functionality.
You will need to use a ParamArray argument:
Optional. Used only as the last argument in arglist to indicate that the final argument is an Optional array of Variant elements. The ParamArray keyword allows you to provide an arbitrary number of arguments. It may not be used with ByVal, ByRef, or Optional. (source)
Revised, thanks to the comments with #TinMan, we no longer use a Dictionary so this will be compatible with Mac OS without further tweaks.
Function FSwitch2(ValueToMatch As Variant, ParamArray ValuesToMatchAndReturn())
' example of replicating the Switch function available in Office 365, etc.
' https://support.office.com/en-us/article/switch-function-47ab33c0-28ce-4530-8a45-d532ec4aa25e
Dim i As Integer
Dim retVal As Variant
Dim default As Variant
If (UBound(ValuesToMatchAndReturn) + 1) Mod 2 <> 0 Then
' if the array is not evenly sized, assume the last argument is the default value.
default = ValuesToMatchAndReturn(UBound(ValuesToMatchAndReturn))
Else
' Otherwise, default to #N/A error if no match.
default = CVErr(2042)
End If
For i = LBound(ValuesToMatchAndReturn) To UBound(ValuesToMatchAndReturn) Step 2
If ValueToMatch = ValuesToMatchAndReturn(i) Then
retVal = ValuesToMatchAndReturn(i + 1)
Exit For
End If
Next
FSwitch2 = IIf(IsEmpty(retVal), default, retVal)
End Function

Excel Evaluate Text as Logical

I am trying to evaluate a logical expression by concatenating the operands and operator. Is there a formula to convert from text to logical in Excel 2016, similar to how VALUE() converts from text to number? I'm looking to a solution for this so I can dynamically change the condition without changing the actual Excel formula. I've searched and read through the Excel function descriptions, but nothing is jumping out as a solution.
'The operands and operator
A1: 1
A2: >
A3: 0
'Concatenation
B4: =CONCAT(A1:A3) 'This evaluates to 1>0
B5: =A1&A2&A3 'This also evaluates to 1>0
'Some checks
C4: =ISTEXT(B4) 'This evaluates to TRUE.
C5: =ISTEXT(B5) 'This also evaluates to TRUE
D4: =ISLOGICAL(B4) 'This evaluates to FALSE
D5: =ISLOGICAL(B5) 'This also evaluates to FALSE
'Vain attempts
E4: =AND(B4,TRUE) 'This ALWAYS is TRUE, even when my desired output is FALSE
E5: =OR(B5) 'This spits out a #VALUE! error
Since I'm looking for something dynamic, I want to avoid a solution such as
=IF(A2=">",A1>A3,FALSE). I also would prefer to avoid a UDF but am willing to go that route if no built in function exists to convert a logical expression in text and evaluate it as logical.
To write a UDF calling Evaluate, that can handle either a Range or Variant/String input, try the following:
Function L(exp As Variant) As Variant
Dim vExp As Variant
vExp = exp
L = Application.Evaluate(vExp)
End Function
Why does it work?
The line vExp = exp does the magic. If exp is a Range then the assigment uses the default .Value property and copies as a Variant/String (because we are not using Set). If it's a Variant/String, then it's a straight copy.
It does have the downside of using Application.Evaluate (downside as explained
here )
A version to also handle the possibility of being called from VBA, try
Function L(exp As Variant) As Variant
If TypeName(exp) = "Range" Then
L = exp.Worksheet.Evaluate(exp.Value)
Else
If IsError(Application.Caller) Then
'Calls from VBA are hanbled here.
' Note: Best to fully qualify any range references in exp
' to avoid unexpected references to the active sheet
L = ActiveSheet.Evaluate(exp)
Else
L = Application.Caller.Worksheet.Evaluate(exp)
End If
End If
End Function
Based on the comments that no built-in function takes care of this, I made a user-defined function ("UDF") called L() for logical, taking after the built-in functions N() and T() which are for numbers and text.
Function L(exp As Variant) As Variant
Dim limit As Integer
Dim counter As Integer
'Set an upper limit to how many cycles the loop may run
limit = 1000
'Assuming the possibility of nested functions, loop until the expression resolves to logical or to an error or until the loop limit has been reached
Do
exp = [exp] 'This avoids Error 2015 if exp is a Range reference. Comment if there's a better way!
exp = Application.Evaluate(exp) 'Evaluate the expression
counter = counter + 1 'Increment the loop counter
Loop Until Application.IsLogical(exp) Or Application.IsError(exp) Or counter >= limit
'Return the evaluated expression
L = exp
End Function
This function works even when I throw some silly things its way like =L(TRUE) or =L(CONCAT(A1:A2)&A3) or even =l(CONCAT(A1:A2)&"""foo"""). But it does not throw errors in cases where it probably should, such as =l(123) or =l("123"). In these cases, thank goodness for the counter limit because 123 and "123" will never evaluate to a logical or an error.

Excel 2007 find the largest number in text string

I have Excel 2007. I am trying to find the largest number in a cell that contains something like the following:
[[ E:\DATA\SQL\SY0\ , 19198 ],[ E:\ , 18872 ],[ E:\DATA\SQL\ST0\ , 26211 ],[ E:\DATA\SQL\ST1\ , 26211 ],[ E:\DATA\SQL\SD0\ , 9861 ],[ E:\DATA\SQL\SD1\ , 11220 ],[ E:\DATA\SQL\SL0\ , 3377 ],[ E:\DATA\SQL\SL1\ , 1707 ],[ E:\DATA\SQL_Support\SS0\ , 14375 ],[ E:\DATA\SQL_Support\SS1\ , 30711 ]]
I am not a coder but I can get by with some basic instructions. If there is a formula that can do this, great! If the best way to do this is some sort of backend code, just let me know. Thank you for your time.
I do have the following formula that almost gets me there:
=SUMPRODUCT(MID(0&A2,LARGE(INDEX(ISNUMBER(--MID(A2,ROW(INDIRECT("1:"&LEN($A$2))),1))*ROW(INDIRECT("1:"&LEN($A$2))),0),ROW(INDIRECT("1:"&LEN($A$2))))+1,1)*10^ROW(INDIRECT("1:"&LEN($A$2)))/10)
With a cell that contains a string like above, it will work. However, with a string that contains something like:
[[ E:\DATA\SQL\SY0\ , 19198.934678 ],[ E:\ , 18872.2567 ]]
I would end up with the value of 19198934678 as the largest value.
You can use this UDF:
Function MaxInString(rng As String) As Double
Dim splt() As String
Dim i&
splt = Split(rng)
For i = LBound(splt) To UBound(splt)
If IsNumeric(splt(i)) Then
If splt(i) > MaxInString Then
MaxInString = splt(i)
End If
End If
Next i
End Function
Put this in a module attached to the workbook. NOT in the worksheet or ThisWorkbook code.
Then you can call it like any other formula:
=MaxInString(A1)
If there is always a space before and after, you can use this formula. The formula is an array formula and must be confirmed by holding down ctrl + shift while hitting enter
With your string in A1:
=MAX(IFERROR(--TRIM(MID(SUBSTITUTE(A1," ",REPT(" ",99)),IF(seq=1,1,(seq-1)*99),99)),0))
seq is a defined name that refers to:
=ROW(INDEX(Sheet1!$1:$65536,1,1):INDEX(Sheet1!$1:$65536,255,1))
If a VBA UDF is preferable, I suggest the following. The Regex will match anything that might be a number. The number is expected to be in the format of iiii.dddd The integer part and the decimal point are both optional.
Option Explicit
Function LargestNumberFromString(S As String) As Double
Dim RE As Object, MC As Object, M As Object
Dim D As Double
Set RE = CreateObject("vbscript.regexp")
With RE
.Global = True
.Pattern = "\b[0-9]*\.?[0-9]+\b"
If RE.test(S) = True Then
For Each M In MC
D = IIf(D > CDbl(M), D, CDbl(M))
Next M
End If
End With
End Function
This is going to be a rather complex answer for someone with no programming background, so be prepared to spend a lot of time covering and researching this topic if you really wish to achieve a function that finds the largest number in a string in excel.
The solution, requires the use of VBA and Regular expressions
VBA is used in excel when there is a need for more complex functionality that just can't be achieved with the use of built in spreadsheet functions.
Regular expressions are a language used to tell programs how to extract useful information from texts, in this case we can extract all the numbers in your text. with the following regular expression.
(\d+.?\d*)/g
Which roughly means: Match one or more digits with an optional period and subsequent optional digits.
The program that will interpret this will do the following: Look for digits, if you see one, then that's a match, grab all contiguous digits and add them to the match. Once you find a character that is not a digit, start looking for new matches. if at any point you find a dot, add it to the match, but just once, and keep on looking for digits. Rinse and repeat until the end of the text.
You can test it here. In this case, the regex matches 19 numbers.
http://www.regextester.com/
Once you have a collection with the 19 matches (See link to regular expressions), all you would need to do is to loop over each of the matches to find out which of the numbers is the highest:
for each number in matches
if number > highestNumber then
highestNumber = number
end if
next
And highestNumber will be the the result! In order to have this code run in a simple custom function, you can follow this microsoft tutorial ( https://support.office.com/en-us/article/Create-Custom-Functions-in-Excel-2007-2f06c10b-3622-40d6-a1b2-b6748ae8231f?ui=en-US&rs=en-US&ad=US&fromAR=1 )
Where c is your string to find max from
Dim qwe() As String
qwe = Split(c, ", ")
maxed = 0
For x = LBound(qwe) To UBound(qwe)
qwe(x) = Left(qwe(x), InStr(1, qwe(x), " ", vbBinaryCompare))
On Error Resume Next
If CLng(qwe(x)) > maxed Then maxed = CLng(qwe(x))
Next x
MsgBox maxed
The error line is there to ignore when qwe(x) cannot be converted to a LONG number.
I must say this is very specific to your string format, for a more comprehensive doodad you'd want to have the split delimiter as a variable and possibly use the "IsNumeric" function to scan the entire string.

Excel VBA constant as argument

Sorry in advance, I'm a self-taught VBA programmer, and I'm not sure how to phrase my question!
I've declared constants which have similar names, i.e.
Public Const BP1Enable = "something1something:someotherthing1someotherthing"
public const BP2Enable = "something2something:someotherthing2someotherthing"
etc.
I have 10 of these constants. I have a sub with these constants as arguments:
Sub FieldEnable (ByVal enableconst)
Now I want to call the FieldEnable sub in a loop using i as counter:
For i = 1 To 10
BPfieldname = "BP" & i & "Enable"
FieldEnable enableconst:=BPfieldname
Next i
This does not work, what is happening is that the "value" assigned to enableconst in the sub FieldEnable, is "BP1Enable" instead of the value of the constant BP1Enable namely "something1something:someotherthing1someotherthing".
How can I use the variable BPfieldname when calling the sub FieldEnable?
I hope this makes sense. Any help appreciated.
Transform your variables into a single Array.
See this http://msdn.microsoft.com/en-us/library/wak0wfyt.aspx
EDIT: as #sina has correctly pointed out, VBA does not allow constant arrays,
so instead of trying this
Dim BPEnable = {
"something1something:someotherthing1someotherthing",
"something2something:someotherthing2someotherthing",
....
}
you should try this
Dim BPEnable
BPEnable = Array( _
"something1something:someotherthing1someotherthing", _
"something2something:someotherthing2someotherthing", _
"..."
)
For i = 0 To UBound(BPEnable)
BPfieldname = BPEnable(i)
Next i
The best guess would be to use a constant array, but constant arrays are not supported by VBA.
Therefore you could go with building an array out of your constants before you start your loop:
Dim BPEnable
BPEnable = Array(BP1Enable, BP2Enable)
For i = 0 To UBound(BPEnable)
FieldEnable enableconst:=BPEnable(i)
Next i
Another option would be to declare all constants as one long string with some defined separator and use a split function on that string to generate the array.
Also, if you are going to be using this "constant" in more than one place, more than one function, you can effectively make an array constant, that can even be called that way.
Public Sub Test()
For i = LBound(BPEnable) To UBound(BPEnable)
Debug.Print BPEnable(i)
Next i
End Sub
Public Function BPEnable() As Variant
BPEnable = Array("One", "Two", "Three", "Pi", "Four")
End Function
Immediate Window:
One
Two
Three
Pi
Four

Resources