Creating a function with unknown number of inputs - excel

I'm trying to create a VBA-function in Excel where the number of inputs are unknown.
The inputs are all going to be numbers.
I've found information about Paramarray, but don't know it thats a good choice, and I am not able to make it work.

You stated that
I am not able to make it work.
However, you:
Have not posted the problematic code
have not posted your inputs
have not posted your expected results
have not posted your actual results
Given this lack of information, it is difficult to pinpoint your specific problem(s). Here is an example of a working function with variable number of inputs. Its purpose is to display for debugging, the values of several variables:
Public Function Displayy(ParamArray s()) As String
Dim i As Long
Displayy = CStr(s(0))
If UBound(s) = 0 Then Exit Function
For i = 1 To UBound(s)
Displayy = Displayy & vbCrLf & CStr(s(i))
Next i
End Function

Since your are building function, I am assuming the numbers are placed in Excel range (Example: A1: A10)
So you can use range as argument
Function YourFunctionName(rng as Range) as integer/Float/....etc

I would use variant. It can be range or array of numbers.
Public Function DisplayZ(Inputs As Variant) As String
Dim i As Long
Dim v As Variant
For Each v In Inputs
DisplayZ = DisplayZ & vbCrLf & v
Next v
End Function

Related

Is there a way to pass a string into the Average function?

I have a worksheet containing a column for the velocity of a vehicle, and I want to take specific ranges from that column (ranges where a turn occurs) and calculate the average speed of all turns combined.
I already have the turn ranges in two arrays, one for the turn starts and one for the turn ends. The code I wrote for this purpose looks like this:
Dim strTurnAvg As String
Dim k As Long
strTurnAvg = ""
For k = LBound(TurnStarts) To UBound(TurnStarts)
If TurnStarts(k) <> TurnEnds(k) Then 'the code for detecting turns rarely messes up and takes the same cell as the start and end of a turn. This is used to ignore such instances
If strTurnAvg = "" Then strTurnAvg = "Range(" & TurnStarts(k) & ":" & TurnEnds(k) & ").Value" Else strTurnAvg = strTurnAvg & ", Range(" & TurnStarts(k) & ":" & TurnEnds(k) & ").Value"
End If
Next
Dim result As Double
result = Application.WorksheetFunction.Average(strTurnAvg) '<- This results in a Run-time error '1004', Unable to get the Average property of the WorksheetFunction class
I tried manually putting the strTurnAvg result into an Average command, and everything worked perfectly fine, but when I try to pass it using the variable, it gives me the runtime error.
Is there a way to achieve this process in another way, or am I doing something wrong?
Try the following:
Dim TurnAvg As Range
Dim k As Long
For k = LBound(TurnStarts) To UBound(TurnStarts)
If TurnStarts(k) <> TurnEnds(k) Then 'the code for detecting turns rarely messes up and takes the same cell as the start and end of a turn. This is used to ignore such instances
If TurnAvg Is Nothing Then
Set TurnAvg = Range(TurnStarts(k), TurnEnds(k))
Else
Set TurnAvg = Union(TurnAvg, Range(TurnStarts(k), TurnEnds(k)))
End If
End If
Next
Dim Result As Double
Result = Application.WorksheetFunction.Average(TurnAvg)
It collects all the ranges using Union into TurnAvg. So you work with the actual ranges and the data in them. You cannot work with strings because you can only calculate with numbers but not with text. The Average functions needs numbers to calculate.
You might want to specify in which workbook/worksheet your ranges are like
ThisWorkbook.Worksheets("Sheet1").Range(TurnStarts(k), TurnEnds(k))
Otherwise VBA might pick the wrong workbook or wrong worksheet.

Function to get end of xlFillSeries vba

I'm trying to build a function to return the end string/cell value of an xlFillSeries. Is there any way to do it without actually writing to the worksheet and then selecting last cell? I want to avoid manipulating the worksheet/workbook
.
Here is the code to generate the series:
Dim SeqStart As String, SeqInt As Integer
SeqStart = "XYZ100"
SeqInt = 42 ' Function should return XYZ141
With Range("A1")
.Value = UCase(SeqStart)
If SeqInt > 1 Then
.AutoFill Destination:=Range("A1").Resize(SeqInt), Type:=xlFillSeries ' Will cause error if only 1 sample sequence
End If
End With
I want to utilize XlFillSeries as it handles odd data well, Eg: If my SeqStart = A1B100 then I can't utilize Regex to strip this down to just numbers, perform math, and then put it back together as there is a B in the middle of the string. I do know the series will/should always end in numbers, but I've struggled a bit to strip just the numeric portion from the right hand of the string without knowing string length and or mix of alphanumeric oddities.
So I guess my question could be answered by figuring out how to strip numbers from right side of string and then doing math and putting string back together. The numbers are what increments.
Or, I would just utilize XlFillSeries but without actually writing to the workbook. Currently I did just set it up so that it writes to a "temp sheet" and then captures the last cell and deletes the temp sheet, but I wondered if there was a better way.
Specifically talking about XlFillSeries there is no way to utilize it without writing to a Range which must be "real cells". Therefor the best method is to utilize a "temp sheet" and return the last cell. It may be possible to utilize functions to strip numbers from right hand side of string and then re-build string, but I trust XlFillSeries more and have resigned to just using a "temp sheet".
Here is my function:
Note: There are some custom functions that I wont include, but you can guess what they do based on the name!
Public Function EndOfXlFillSeries(SeqStart As String, SeqInt As Integer) As String
Dim DestSheet As Worksheet
WorksheetCreateDelIfExists ("XLFillSeriesTmp")
Set DestSheet = Worksheets("XLFillSeriesTmp")
With DestSheet.Range("A1")
.Value = UCase(SeqStart)
If SeqInt > 1 Then
.AutoFill Destination:=DestSheet.Range("A1").Resize(SeqInt), Type:=xlFillSeries ' Will cause error if only 1 SeqInt
Else
EndOfXlFillSeries = SeqStart
End If
End With
Dim lRow As Integer
lRow = lRowOfCol(1)
EndOfXlFillSeries = DestSheet.Range("A" & lRow).Value
Call WorksheetDelete(DestSheet)
End Function

How to implement 'sumifs' in evaluate-function

I have found a macro to evaluate strings. It is working fine with most formulas. But it wont evalute my sumifs-formulas.
The VBA for WM_Eval() goes like this:
Function wm_Eval(myFormula As String, ParamArray variablesAndValues() As
Variant) As Variant
Dim i As Long
'
' replace strings by values
'
For i = LBound(variablesAndValues) To UBound(variablesAndValues) Step 2
myFormula = RegExpReplaceWord(myFormula, variablesAndValues(i),
variablesAndValues(i + 1))
Next
'
' internationalisation
'
myFormula = Replace(myFormula, Application.ThousandsSeparator, "")
myFormula = Replace(myFormula, Application.DecimalSeparator, ".")
myFormula = Replace(myFormula,
Application.International(xlListSeparator), ",")
'
' return value
'
wm_Eval = Application.Evaluate(myFormula)
End Function
If I type wm_Eval("1+1") it works like a charm. But if I do:
="sumifs(b2:b10,a2:a10,"&D2&">=2"&D2&")" where d2=" it returns #Value.
So the formula it should evaluate would be: sumifs(b2:b10,a2:a10,">=2")
I am accustomed to danish excel - so it might just be something very very simple I am missing.
I feel you should give some more information about why you need this function, as I'm sure there'd be a better of way of achieving the task than the code above.
There are a number of reasons the function is returning an error. The main one is that your formula syntax isn't right. The wm_Eval() function requires a formula argument and then pairs of variable and value parameters. You haven't included the code for your RegExpReplaceWord() function, but I'd imagine it runs some kind of replacement of values for pseudo formula variables. Without seeing that part of your code, it's difficult to guess at the syntax, but a mock-up of the principle would be something like:
=wm_Eval("SUMIFS(A1:A4,B1:B4,{a})","{a}",">=1")
where {a} is replaced by ">=1"
Data type is also an issue, as you can see in the following syntax:
=wm_Eval("SUMIFS(A1:A4,B1:B4,{a})","{a}",1)
I don't want to be too brutal, but this code is pretty poorly constructed and there's no way that it'll be able to parse all the different formulae with various passed in parameters. Yes it works for SUM but that's because you don't pass in any variable-value pairs. I would steer away from this solution and come up with something more robust. If for example you want to gather string versions of your formula, why not just pass in the range containing the formula and deal with it from there?
I can tell you that I have written a routine to parse values from formulas. It is long and complex and took many, many hours to develop including some WINAPI calls and hooks, so you have a lot of work ahead of you if you wish to continue with your current approach.
However, I guess I ought to show you how your current function works, so I've mocked-up a RegExpReplaceWord() function and commented out that internationalisation code:
Public Function wm_Eval(myFormula As String, ParamArray variablesAndValues() As Variant) As Variant
Dim i As Long
'
' replace strings by values
'
For i = LBound(variablesAndValues) To UBound(variablesAndValues) Step 2
myFormula = RegExpReplaceWord(myFormula, variablesAndValues(i), variablesAndValues(i + 1))
Next
'
' internationalisation
'
' myFormula = Replace(myFormula, Application.ThousandsSeparator, "")
' myFormula = Replace(myFormula, Application.DecimalSeparator, ".")
' myFormula = Replace(myFormula, Application.International(xlListSeparator), ",")
'
' return value
'
wm_Eval = Application.Evaluate(myFormula)
End Function
Private Function RegExpReplaceWord(myFormula As String, variable As Variant, value As Variant) As String
If VarType(value) = vbString Then
value = """" & value & """"
End If
RegExpReplaceWord = Replace(myFormula, variable, value)
End Function
So in your worksheet cell, you can add the functions as shown in the quote blocks above.
In summary, I wouldn't continue with your current approach or you will be plagued with so many errors that your lost 3 days will seem like nothing.

Index Match in VBA for Ranges (finding a loan balance given a loan number and a date)

I was trying to implement the index match combination in VBA to find a number in a range given 2 conditions. The below seems like a great approach, however, my inputs do not come from excel but from a variable that changes in the code itself. For the life of me I can't figure it out but I am a newbie.
Excel / VBA - Index Match function using Dynamic Ranges
What happens if your name and date instead are a loan number (1,2,3,etc) and date (6/30/2013) and are not in a spreadsheet but are generated in the VBA Code so that then the code can go to a range and look for the balance of that loan in such date and store it to a variable
-----------------RANGE DEFINITIONS-------------------------------------------------------------------------
About the code: Cantidad, ID and Fecha are dymanic ranges defined in the following way:
With Worksheets("CFs")
Set ID = Range("offset($a$3,4,0,counta($A:$A)-4,1)")
Set Fecha = Range("offset($b$3,4,0,counta($B:$B)-4,1)")
Set Cantidad = Range("offset($f$3,4,0,counta($F:$F)-4,1)")
End With
------------------FUNCTION CODE----------------------------------------------------------------------
about the function : dia1 and ID are a date that changes monthly and a loan number that loops one a time until the total number of loans are reached.
Public Function TestIndexMatch1(ByRef Cantidad As Range, _
ByRef Prestamo As Integer, _
ByRef Dia1 As Date, _
ByRef ID As Range, _
ByRef Fecha As Range)
Const Template As String = "=INDEX({0},MATCH(1,({1}={2})*({3}={4},{5}))"
Const MATCH_TYPE = 0
On Error GoTo Err_Handler
Err.Number = 0
Dim originalReferenceStyle
originalReferenceStyle = Application.ReferenceStyle
Application.ReferenceStyle = xlR1C1
Dim myFormula As String
myFormula = Replace(Template, "{0}", Cantidad.Address())
myFormula = Replace(Template, "{1}", Prestamo.Address())
myFormula = Replace(Template, "{2}", Dia1.Address())
myFormula = Replace(Template, "{3}", ID.Address())
myFormula = Replace(Template, "{4}", Fecha.Address())
TestIndexMatch1 = Application.Evaluate(myFormula)
Err_Handler:
If (Err.Number <> 0) Then MsgBox Err.Description
Application.ReferenceStyle = originalReferenceStyle
End Function
First off it looks like you are missing a few things:
a closing bracket in the formula string ...{4}),{5}...
a dot before range in your name definitions (Set ID = .Range)
But i think you might want to go about things a little differently. If you are defining variables in VBA to be used in worksheet functions on the sheet, you need to be careful about defining dependencies. Each range that is used in the VBA function should be an input argument to the function so Excel will recalculate whenever that value changes. Another issue with evaluating formulas in code is there is a lack of intermediate debug information and functions don't always evaluate as they would on the sheet.
For the first part I think you want to setup sheet names, this can be done by making the following adjustment to your code above:
Sub SetUpNames()
With Worksheets("CFs").Names
.Add "ID", "=offset($a$3,4,0,counta($A:$A)-4,1)"
.Add "Fecha", "=offset($b$3,4,0,counta($B:$B)-4,1)"
.Add "Cantidad", "=offset($f$3,4,0,counta($F:$F)-4,1)"
End With
End Sub
This could also be done through the defined name dialog box. These names can then be inserted into the function in the linked post.
For the second section of code, here is an alternative to the function in the linked post which also uses a worksheet function approach:
Public Function TestIndexMatch2(ByRef outputRange As Range, _
ByRef nameCriteria As Range, _
ByRef dateCriteria As Range, _
ByRef nameRange As Range, _
ByRef dateRange As Range)
Dim v as Variant
With Application
v = .CountIfs(nameCriteria, nameRange, dateCriteria, dateRange)
TestIndexMatch2 = .Index(outputRange, .Match(1, v, 0))
End With
End Function
(Now at least the watch window is available to evaluate intermediate results if required).
Note: Using Application without .WorksheetFunction returns a variant which allows for arrays in arguments and results. However VBA doesn't expose all worksheet functions or Excel operators so you need to be more resourceful with this approach, for example the Exact function is missing but there are workarounds like len(substitute(a,b,""))=0.

Excel VBA: Unlimited Range Inputs With Versatility (?)

Could someone help me create a function that will handle an unlimited number of diverse ranges? I have tried "Paramarray variables() as Variant" and "variable as Range" in my arguments list but neither of them provide the versatility that I am looking for.
The key is that I want my function to be able to simultaneously handle things like "MyFunc(A1:A10, B1)" or "MyFunc(A1, B1:10, C11)". The problem I'm finding is that "ParamArray" can only handle comma separated inputs while "variable as Range" can only handle non-comma separated inputs.
Basically, I want to have the same functionality that the SUM() function has. Where SUM can handle an infinite (sort of) number of inputs regardless if they are separated by commas or are in a range.
As requested, here is my code:
Function COMMA_DELIMITER(inputs as Range)
' this function basically concatenates a consecutive set of cells and places commas between values
For Each j in Inputs
stringy = stringy & j.value & chr(44)
Next
stringy = Left(stringy, Len(stringy) - 1)
COMMA_DELIMITER = stringy
End Function
or
Function COMMA_DELIMITER_A(ParamArray others())
'this is the same function, only the cells don't have to be consecutive
For i = 1 to UBound(others) + 1
stringy = stringy & others(i-1) & chr(44)
Next
COMMA_DELIMIERTER_A = Left(stringy, Len(stringy) - 1)
End Function
I pretty much want to create a function that has the flexibility to handle both consecutive cells and/or non-consecutive cells. The inputs would look like this, "=MyFunc(A1, B1:B10, C11, D12:D44)".
Could someone help me create a function that can handle something like this, "MyFunc(A1, B1:B10, C11, D12:D44)"?
Thanks,
Elias
Actually it is possible to do that, and code from chris neilsen is almost there.
Function MyFunc1(ParamArray r()) As Variant
Dim rng As Range
Dim i As Long
Dim j As Variant
Dim s As String
For i = LBound(r) To UBound(r)
For each j in r(i)
s = s & " " & j.Address
Next
Next
MyFunc1 = s
End Function
See? You only have to put one more loop, so if you have a range like [A1:A4] it will loop for into that too. One loop will return another ParamArray, so you have to loop for twice. And if you have just [A1] that's not a problem either, the double looping will cause no problem.
I think there are two issues with your approach
, and   (comma and space) are the Union and Intersect operators for ranges
To pass a multi area range into a single parameter, you need to enclose it in ( )
To demonstrate
ParamArray version
Loop through the array variable to access to individual ranges
Function MyFunc1(ParamArray r()) As Variant
Dim rng As Range
Dim i As Long
Dim s As String
For i = LBound(r) To UBound(r)
s = s & " " & r(i).Address
Next
MyFunc1 = s
End Function
 
Range version
Iterate the range Areas collection to access individual ranges
Function MyFunc2(r As Range) As Variant
Dim rng As Range
Dim i As Long
Dim s As String
For Each rng In r.Areas
s = s & " " & rng.Address
Next
MyFunc2 = s
End Function
Both
=MyFunc1(B5:D5 C4:C6,B10:E10,D13:D16)
and
=MyFunc2((B5:D5 C4:C6,B10:E10,D13:D16))
will return
$C$5 $B$10:$E$10 $D$13:$D$16
(note that the intersection of B5:D5 and C4:C6 is C5)

Resources