UDF: Handling ranges and variable numbers of arguments - excel

I'm trying to write a UDF (user-defined function) to create an average for non-numeric data (I'm converting it into numeric form then back again at the end). I can get the UDF to work if I list individual cells; I get a #VALUE! error if I try to refer to a range of cells. There may be a mix of both ranges and individual cells to process.
Any ideas?
The code so far is below.
Function avlvl(ParamArray av() As Variant)
Dim a As Integer
'creates an average ks3 level from data in format "5a"
a = 0
n = 0
total = 0
Do While a < UBound(av()) + 1
'ignore blank or zero cells
If av(a) = 0 Or av(a) = "" Then
a = a + 1
Else
'convert data into numeric value - split into level and sub level
level = Val(Left(av(a), 1))
sl = Right(av(a), 1)
If sl = "c" Then
sublevel = 0
ElseIf sl = "C" Then
sublevel = 0
ElseIf sl = "b" Or sl = "B" Then
sublevel = 1 / 3
ElseIf sl = "a" Or sl = "A" Then
sublevel = 2 / 3
Else
sublevel = 0
End If
'score is numeric value of the data
score = level + sublevel
'total is teh toatl of the cells so far
total = total + score
a = a + 1
n = n + 1
End If
Loop
ave = total / n
'reconvert into format level and sublevel (a,b,c)
averagelevel = Application.WorksheetFunction.RoundDown(ave, 0)
asl = ave - averagelevel
If asl < 0.17 Then
averagesublevel = "c"
ElseIf asl < 0.5 Then
averagesublevel = "b"
ElseIf asl < 0.84 Then
averagesublevel = "a"
ElseIf asl < 1 Then
averagelevel = averagelevel + 1
averagesublevel = "c"
Else
averagesublevel = "c"
End If
avlvl = averagelevel & averagesublevel
End Function

What's going on is that the range is coming in as a single object of type Range, and your code is trying to treat is as though it is coming in as an array.
The best approach would be to create a new array within the body of the function, and then assign the elements in the range to the new array. You need to test for the type of the elements of the ParamArray. If an element is type String, then put it directly in the new array; if an element is type Range, loop through it, assigning its cell values to the new array.
Then you would do your processing on the new array.
The following code provides the machinery to pass in ranges as well as individual cells or values. I've not included your code but have indicated where it would go.
Function avlvl(ParamArray av() As Variant) As Variant
Dim a As Integer
Dim i As Long
Dim avArr()
Dim element As Variant
a = 0
i = 0
Do While a < UBound(av) + 1
If TypeName(av(a)) = "String" Then
avArr(i) = av(a)
i = i + 1
ElseIf TypeName(av(a)) = "Range" Then
For Each element In av(a)
ReDim Preserve avArr(0 To i)
avArr(i) = element
i = i + 1
Next
Else
avlvl = CVErr(xlErrValue)
Exit Function
End If
a = a + 1
Loop
i = 0
Do While i < UBound(avArr) + 1
'...
'now process the elements of avArr()
'...
i = i + 1
Loop
End Function

If you have a disjoint range of cells and you want to pass them to a UDF, one approach is to create a Defined Name and pass it to the UDF as a single argument.

Related

Excel subroutine to create all possible combinations from a data set excluding duplicates

Does anyone know a routine on how to get a data set composed by 7 columns into all possible combinations?
the combination is composed by 7 numbers like this--> 1|3|8|10|35|40|50
The routine needs to look into the first table and make a list of all possible combination excluding the duplicate numbers from the combination in the second table. Please see picture.
The table on the left contains the combination which need to be reshuffled, into the right table which contain all possible combinations.
I would do something like:
The number of options are 6^7 so there will be alot of cases: 279936
To get all of it, you should loop through them.
First we should find all the options.
To generate all the possible combinations including duplicates, the probles is the same as get all the may 7 digit long numbers in base 6 ( as we have 6 number in each column)
in newer excels you can use the BASE funtion, but if you can not access it you can use this:
if you cange a code a bit you can call the value of the original table instead of the 0-5 numbers.
Then just remove duplicates.
Sub generateAllBase6()
Dim i As Double 'number tries
Dim n As String ' the number of item from the column 1-7
For i = 0 To 279936 - 1
n = ConvertBase10(i, "012345")
For k = 1 To 7
If Len(n) < k Then
Cells(i + 2, k) = 0
Else
Cells(i + 2, k) = Right(Left(n, k), 1)
End If
Next k
Next i
End Sub
Public Function ConvertBase10(ByVal d As Double, ByVal sNewBaseDigits As String) As String
Dim S As String, tmp As Double, i As Integer, lastI As Integer
Dim BaseSize As Integer
BaseSize = Len(sNewBaseDigits)
Do While Val(d) <> 0
tmp = d
i = 0
Do While tmp >= BaseSize
i = i + 1
tmp = tmp / BaseSize
Loop
If i <> lastI - 1 And lastI <> 0 Then S = S & String(lastI - i - 1, Left(sNewBaseDigits, 1)) 'get the zero digits inside the number
tmp = Int(tmp) 'truncate decimals
S = S + Mid(sNewBaseDigits, tmp + 1, 1)
d = d - tmp * (BaseSize ^ i)
lastI = i
Loop
S = S & String(i, Left(sNewBaseDigits, 1)) 'get the zero digits at the end of the number
ConvertBase10 = S
End Function
I found the funcion here: http://www.freevbcode.com/ShowCode.asp?ID=6604

How do I modify a sample code for primefactorization in Excel VBA to a specific column of numbers?

I have in Column K:
K8 is 6384 i.e. =SUM(J1:J8)
K9 is 2598 i.e. =SUM(J2:J9)
K10 is 12176 i.e =SUM(J3:J10)
:
:
K5488
up to K5488 (No numbers in sequence, all different numbers)
The largest number appearing in K is 1 400 000.
I need in Column M: The prime factors of each number in K
e.g. K8 is 6384 then M8 should be 2,2,2,2,3,7,19
k9 is 2598 then M9 should be 2,3,433 etc.
I found the following code by John Coleman on your site (Mar 28) which tested well, but seeing I have no programming knowledge, I don't know how to modify it to use in my columns K & M setup.
Here's the sample code:
Function Factor(ByVal n As Long, Optional FirstTrial As Long = 2) As String
Dim i As Long
Dim t As Long
Dim limit As Long
Dim rest As String
Dim s As String
If n = 1 Then
Factor = n
Exit Function
End If
limit = Int(Sqr(n))
t = FirstTrial
Do While t <= limit
If n Mod t = 0 Then
rest = Factor(n / t, t)
If rest <> "1" Then
s = t & "," & rest
End If
Factor = s
Exit Function
Else
If t = 2 Then t = 3 Else t = t + 2
End If
Loop
'if we get here:
Factor = n
End Function
Function PrimeOrFactor(n As Long) As String
Dim s As String
s = Factor(n)
If n = 1 Then
PrimeOrFactor = "Neither"
ElseIf (s) = Trim(n) Then
PrimeOrFactor = "Prime"
Else
PrimeOrFactor = s
End If
End Function
Tested like:
Sub test()
Dim i As Long
For i = 1 To 20
Cells(i, 1) = i
Cells(i, 2) = PrimeOrFactor(i)
Next i
End Sub
The function you provided is a udf (user defined function) to be used in your worksheet.
If you put the functions you provided in a normal code module, you can enter the following in your worksheet in M8:
=Factor(K8)
and copy that function down to your desired range.

Searching a table both horizontally and vertically and printing the values

sorry for the ambiguous title. I'm not quite sure how to name what I'm trying to do.
I have data in a worksheet that looks like this:
I would like to search for the smallest number in the range and write out the name on the y axis and the number. It then ignores this number and searches for the smallest number on the x axis as well. In that same row, it searches for the smallest value horizontally, excludes the number and then looks vertically as well. It continues this way until all possibilities are exhausted. Is this possible with Excel?
A sample output will be:
y5 : 40
x3: 60
y3: 90
x4: 80
y2 : 85
x3: 75
y1 : 70
and so on.
Interesting problem. You should be able to modify the following. To run it you need to include a reference to Microsoft Scripting Runtime (Under Tools/References in the VBA editor) since it uses a dictionary data structure -- the natural choice to keep track of already picked numbers:
'The following code assumes than Nums is a 1-based 2-dimensional array
Function MinPath(Nums As Variant) As Variant
Dim counter As Long
Dim mins As Variant
Dim PickedNums As New Dictionary
Dim i As Long, j As Long, m As Long, n As Long
Dim report As String
Dim direction As String
Dim num As Variant
Dim min As Variant, min_i As Long, min_j As Long
m = UBound(Nums, 1)
n = UBound(Nums, 2)
ReDim mins(1 To m * n)
min = Nums(1, 1)
min_i = 1
min_j = 1
For i = 1 To m
For j = 1 To n
If Nums(i, j) < min Then
min = Nums(i, j)
min_i = i
min_j = j
End If
Next j
Next i
PickedNums.Add min, 0
counter = 1
mins(counter) = Array(min_i, min_j, min)
direction = "vertical"
min = Empty
Do While True
If direction = "vertical" Then
For i = 1 To m
num = Nums(i, min_j)
If Not PickedNums.Exists(num) Then
If IsEmpty(min) Then
min = num
min_i = i
ElseIf num < min Then
min = num
min_i = i
End If
End If
Next i
If IsEmpty(min) Then
ReDim Preserve mins(1 To counter)
MinPath = mins
Exit Function
Else
PickedNums.Add min, 0
counter = counter + 1
mins(counter) = Array(min_i, min_j, min)
direction = "horizontal"
End If
Else
'direction = horizontal case
For j = 1 To n
num = Nums(min_i, j)
If Not PickedNums.Exists(num) Then
If IsEmpty(min) Then
min = num
min_j = j
ElseIf num < min Then
min = num
min_j = j
End If
End If
Next j
If IsEmpty(min) Then
ReDim Preserve mins(1 To counter)
MinPath = mins
Exit Function
Else
PickedNums.Add min, 0
counter = counter + 1
mins(counter) = Array(min_i, min_j, min)
direction = "vertical"
End If
End If
min = Empty
Loop
End Function
The function repeatedly searches a either a row or a column (depending on the search direction) to find the smallest non-picked number. At the beginning of each pass the variable min is set to Empty until a non-picked number is encountered. If after a pass min is still Empty the function returns. This function returns an array of arrays where each array is of the form Array(i,j,min) (e.g. the values (5,3,40) in the first step). VBA's Array function returns a 0-based array so the i (row) coordinate is at index 0 and the j coordinate is at index 1. What you do with this data is up to you. For example:
Sub test()
Dim i As Long, n As Long
Dim mins As Variant
mins = MinPath(Range("B2:F6").Value)
n = UBound(mins)
For i = 1 To n
If i Mod 2 = 1 Then 'odd step
Range("A7").Offset(i).Value = "y" & mins(i)(0) & ":"
Else 'even step
Range("A7").Offset(i).Value = "x" & mins(i)(1) & ":"
End If
Range("B7").Offset(i).Value = mins(i)(2)
Next i
End Sub
Which results in:

Summing items under a merged cell

I would like to ask help to sum all items under a merged cell.
Item looks like this:
June
0 1 2 3 4 5 6 7 8 9
2 3 4 5 6 7 8 9 0 1
June is a merged cell, and I want to sum all items under it.
Is this even possible?
This will work whether the cells are merged or un-merged. Say we have merged A1 through C2 with contents like:
The following UDF() will give the sum of the numeric values:
Public Function InternalSum(rin As Range) As Double
Dim v As String, CH As String, temp As String
Dim dot As String, L As Long, i As Long
Dim capture As Boolean
v = rin(1).Text
InternalSum = 0
dot = "."
temp = ""
capture = False
L = Len(v)
If L = 0 Then Exit Function
For i = 1 To L
CH = Mid(v, i, 1)
If IsNumeric(CH) Or CH = dot Then
capture = True
temp = temp & CH
If i = L Then
InternalSum = InternalSum + CDbl(temp)
End If
Else
If capture Then
capture = False
InternalSum = InternalSum + CDbl(temp)
temp = ""
End If
End If
Next i
End Function
Note that both:
=internalsum(A1)
and
=internalsum(A1:C2)
will work.

Loop Not Working in UDF Function Excel VBA?

I am having a problem running the code below, the code is to calculate the difference between two array of dates, values is separted by line carriage (CHR(10)), for example in cell A1 I have the following dates
A1
12/12/2012
11/12/2021
7/8/2015
9/4/2014
B1
12/12/2012
11/12/2021
7/8/2015
9/4/2014
C1
2D
1D
4D
10D
in D1 I call the function from which is inside module 1 as following
=calcSumDurations(A1,B1,C1)
it will always return 0
and when I try to trace the code, it will enter the for loop only once, even than intmax = 3, or 4 or 40 in some cases, I tried while, for, foreach, none working.
Function calcSumDurations(dateFrom, dateTo, dateDuration As String)
Dim intmax, intSum, i, intError As Integer
Dim varDateFrom, varDateTo, varDateDuration As Variant
intSum = 0
intmax = -1
i = 0
intError = 0
varDateFrom = Split(dateFrom, Chr(10))
varDateTo = Split(dateTo, Chr(10))
varDateDuration = Split(dateDuration, Chr(10))
intmax = UBound(varDateFrom)
If UBound(varDateFrom) = UBound(varDateTo) Then ' both are same lenght
If intmax >= 0 Then ' more than one line
For i = 0 To intmax
'While i < intmax
MsgBox (i)
If CInt(CDate(varDateTo(i))) >= CDate(varDateFrom(i)) Then 'check dates are more
If testDate(CStr(varDateTo(i))) And testDate(CStr(varDateFrom(i))) Then
intDuration = Abs(CInt(CDate(varDateTo(i)) - CDate(varDateFrom(i)))) + 1
intSum = intSum + intDuration
'strRes = strRes & CStr(intDuration) & Chr(10)
Else
intError = 1
'Exit For
End If
Else
intError = 2
End If
Next i
End If
Else
intError = 3
End If
calcSumDurations = intSum
End Function
The problem is in this line of code:
If CInt(CDate(varDateTo(i))) >= CDate(varDateFrom(i)) Then
an integer is too small to hold the date value and is causing an overflow exception. I'm not sure why you're trying to convert it into an integer anyways as the comparison won't work if you do that.
Try this:
If CDate(varDateTo(i)) >= CDate(varDateFrom(i)) Then
It'll at least start getting through the loop.
I'd also define what you want the function to return
Function calcSumDurations(dateFrom As String, dateTo As String, dateDuration As String) As Long

Resources