#Name and VBA function - excel

Creating a new formula to more easily deal with weighted median. Function works as a sub but not as a Function. So values are parsed in the first range and weights in the second i.e. {10,20,15,30,15}, {1,2,0,3,4}
Not sure were the error is creaping in.
Function weightedMedian(valueRng As Range, weightRng As Range)
Dim weightValueArr(), valueArr() As Double
ReDim weightValueArr(Application.Sum(weightRng) - 1)
ReDim valueArr(valueRng.Rows.Count - 1)
For k = 0 To valueRng.Rows.Count - 1
valueArr(k) = valueRng(k + 1)
Next
Dim n, m As Integer
n = 0
m = 0
For Each j In weightRng
For i = 0 To j - 1
weightValueArr(m) = valueArr(n)
m = m + 1
Next
n = n + 1
Next
weightedMedian = Application.Median(weightValueArr)
End Function

You need to give the type of your function, unlike a sub, right at the start.
Function weightedMedian(valueRng As Range, weightRng As Range) As double
Of course, this is the only way to know what the type of value returned IS, since you never dim it or receive it as an argument.

Related

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:

I am getting the error "Type mismatch: array or user-defined type expected" in this VBA code

I am getting an error at the subroutine called NLRegress. I think the array types are not the same that are being multiplied in the first call in Sub NLRegress. The Z matrix is the following array [1,0.2,0.04: 1,0.5,0.25: 1,0.8,0.64: 1,1.2,1.44: 1,1.7,2.89: 1,2,4]
This is my code :
Option Explicit
Option Base 1
Sub Main()
Dim x() As Double, y() As Double, n As Integer, p As Integer, _
a() As Double, syx As Double, r2 As Double, m As Integer, _
yf() As Double, Z() As Double
Dim i As Integer, k As Integer
For k = 1 To 100
If Worksheets("Sheet1").Range("A2").Cells(k, 1).Value <> "" Then
n = n + 1 'counts the number of data points
Else
Exit For
End If
Next k
For k = 1 To 100
If Worksheets("Sheet1").Range("B2").Cells(k, 1).Value <> "" Then
p = p + 1 'counts the number of data points
Else
Exit For
End If
Next k
If p = n Then
p = n
ReDim yf(n)
Else: MsgBox ("Unequal number of x and y values")
End If
ReDim x(n)
ReDim y(n)
For i = 1 To n 'Read data for matrix x
x(i) = _
Worksheets("Sheet1").Range("A2").Cells(i, 1).Value
Next
For i = 1 To n 'Read data for matrix y
y(i) = _
Worksheets("Sheet1").Range("B2").Cells(i, 1).Value
Next
m = Worksheets("Sheet1").Range("E2").Value
ReDim a(m + 1)
Call BuildZP(x, Z, n, m)
Call NLRegress(Z, y, a, n, m)
Call MultiplyMatrixByVector(Z, a, yf)
End Sub
Sub Fitted_Data(yf, a, x, n)
Dim q As Integer
For q = 1 To n
yf(q) = a(1) + a(2) * x(q) + a(3) * x(q) ^ 2
Worksheets("Sheet1").Range("C2").Cells(q, 1).Value = yf(q)
Next
End Sub
Sub NLRegress(Z, y, a, n, m)
Dim er As Double, tol As Double, ZT() As Double, ZTZ() As Double, ZTZI() As Double, ZTY() As Double
er = 0
tol = 0.0001
ReDim ZT(m + 1, n)
Call TransposeMatrix(Z, ZT)
Call MultiplyMatrices(ZT, Z, ZTZ)
Call MatrixInverse(ZTZ, ZTZI, m + 1, tol, er)
Call MultiplyMatrixByVector(ZT, y, ZTY)
Call MultiplyMatrixByVector(ZTZI, ZTY, a)
End Sub
Sub BuildZP(x, Z, n, m)
Dim i As Integer, j As Integer
ReDim Z(n, m + 1)
For i = 1 To n
For j = 1 To m + 1
Z(i, j) = x(i) ^ (j - 1)
Next j
Next i
End Sub
This answer will probably not solve your issue (see my comment) - but let me nonetheless give you a few best practices that might make programming in VBA easier and maybe prevent such errors in the first place - in your next project.
Try to incorporate the following into your programming
Proper indenting: Every time you use a programming structure the encloses another block of code - such as For, If, While, indent the enclosed code block one level further. E.g. your first few lines of code should look like
For k = 1 To 100
If Worksheets("Sheet1").Range("A2").Cells(k, 1).Value <> "" Then
n = n + 1 'counts the number of data points
Else
Exit For
End If
Next k
You are already using Option Explicit, which is great. However, you should also properly Dim each variable in the procedure/function calls - e.g. Sub Fitted_Data(yf as Double, ...)
You're using a total of 12 variables in your main procedure. This is a very strong indicator, that your routine is doing too much! Better break it up in to small sub routines and maybe use a few module wide variables - see the example below.
The variable names are absolutely meaningless - which makes it hard to debug for you - and almost impossible for outsiders to understand what your code is doing.
AFAIK your first 25 rows "only" assign two ranges to an array and check if these are the same size. Using the syntax x = StartRange.Resize(NumberOfRows).Cells you can achieve this with much less code - and it executes much faster.
Same thing goes finding the first blank row - instead of looping, use StartRange.End(xlDown) - this will return you the last non-blank row!
Also, if you want to assign an array to a range, it works the other way round, too: StartRange.Resize(NumberOfRows) = x.
Hardcoding Worksheets("Sheet1").Range("A2") will lead to problems when the user changes the worksheet structure, e.g. rename the sheet or insert rows/columns. Better assign the cells A2 and B2 names, e.g. StartVector1 and then access them with Range("StartVector1"). Much more robust - and your code is less cluttered
"Don't repeat yourself" (DRY). If you see yourself doing the same code twice, make it a separate procedure - e.g your code to count the number of data points
No need to use Call Sub(x, y) - Sub x, y is equivalent to it in VBA
Excel function can also be used in VBA. This is especially handy for matrix function. E.g. to transpose an array, you could use this code: transposedX = worksheetFunctions.Transpose(x)
Here's the code structure with the first few
Option Explicit
Private mVec1() As Double 'Better give a better name representing the target content of variable
Private mVec2() As Double 'I use m as a prefix to indicate module wide scoped variables
Public Sub SubDoingSomething() 'Use a name that tells the reader what the sub does
LoadVectors
BuildZP Z, n, m 'use proper variable names here
NLRegress Z, y, a, n, m 'and maybe use some more module wide variables that you don't need to pass
MultiplyMatrixByVector Z, a, yf
End Sub
Private Sub LoadVectors()
Dim count1 As Long, count2 As Long
count1 = GetRowLength(Range("StartVector1"))
count2 = GetRowLength(Range("StartVector2"))
If count1 <> count2 Then
MsgBox ("Unequal number of x and y values")
End
End If
mVec1 = Range("StartVector1").Resize(count1).Cells
mVec2 = Range("StartVector2").Resize(count2).Cells
End Sub
Private Function GetRowLenght(rng As Range)
If rng.Offset(1) = "" Then
GetRowLength = 1
Else
GetRowLength = rng.End(xlDown).Row - rng.Row + 1
End If
End Function

VBA: Summing a matrix

Why doesn't this function work?
Type =funtest(2.1) in Excel and it'll give me #VALUE!.
Public Function funtest(a As Double) As Double
Dim z, j, i As Integer
Dim matrix(3, 3, 3) As Double
For z = 0 To 3 Step 1
For j = 0 To 3 Step 1
For i = 0 To 3 Step 1
matrix(z, j, i) = a
Next i, j, z
funtest = Application.WorksheetFunction.Sum(matrix)
End Function
WorksheetFunction.Sum will work with either a range or a 2 dimentional array. It errors because you are passing it a 3 dimensional array.
So, this works
Public Function funtest(a As Double) As Double
Dim z As Long, j As Long, i As Long
Dim matrix() As Double
ReDim matrix(0 To 3, 0 To 4)
For j = LBound(matrix, 1) To UBound(matrix, 1)
For i = LBound(matrix, 2) To UBound(matrix, 2)
matrix(j, i) = a
Next i, j
funtest = Application.WorksheetFunction.Sum(matrix)
End Function
Note I have modified your declarations slighly, see note at end of answer.
To sum higher dimensional arrays you will need to do some looping.
One option (which may or may not suit your overal requirements) is to declare your array slightly differently, as a so called Jagged Array.
Public Function funtest2(a As Double) As Double
Dim z As Long, j As Long, i As Long
Dim matrix() As Variant
Dim InnerMatrix(0 To 4, 0 To 4) As Double
' Dimension Jagged Array
ReDim matrix(0 To 4)
For i = LBound(matrix, 1) To UBound(matrix, 1)
matrix(i) = InnerMatrix
Next
'Load Data into matrix
For z = LBound(matrix) To UBound(matrix)
For j = LBound(matrix(z), 1) To UBound(matrix(z), 1)
For i = LBound(matrix(z), 2) To UBound(matrix(z), 2)
matrix(z)(j, i) = a
Next i, j, z
' Sum matrix
For z = LBound(matrix) To UBound(matrix)
funtest2 = funtest2 + Application.WorksheetFunction.Sum(matrix(z))
Next
End Function
This is an array of 2 dimensional arrays. The Sum is then applied to each of the inner arrays in turn. This way, at least you are only looping one dimension and not all three.
Note on Dim and Integer
You must specify all As Type's, otherwise variables default to Variant
In your code z and j will be Variants
Also, using Integer rather than Long is actually counter productive on a 32 bit OS: Long's will be slightly faster.
I'm going to take you literally when you say "I'm trying to solve the simple case of a (3,3,3) matrix with each element equal to some double, a". This will do that:
Public Function funtest(a As Double) As Double
funtest = 4*4*4*a
End Function
First, when you get #VALUE! this means there is an error, it can mean using a matrix that is not valid.
To answer your question, your code does not work because your syntax is not correct. The following function creates a matrix from values.
Function FQ_matrix_create(StartValue As Double, Interval As Double,
nrow As Long, ncol As Long) As Double()
Dim M() As Double
' Creates matrix with sequential element values with given row and
' column sizes. Fills matrix row-wise with numbers.
' - set Interval = 0 for constant element values
' - error input arguments nrow and ncol are not positive integers
To SUM the Values use:
Function FQ_matrix_element_sum(M() As Double, SumOption As
MatrixDirection) As Double()
' Returns the sum of elements of matrix M, either row or column wise
' - Rowwise sum returns a horizontal 1xNcol matrix
' - Columnwise sum returns a vertical 1 xNrow matrix
' - Element sum (all elements) returns a 1x1 matrix
' - error if M is not a matrix
' - error if SumOption is not 1 (nRowWiseSum) or 2 (nColWiseSum) or
3 (nElementSum)
To help you understand Matrix in Excel VBA, here is a good resource: http://finaquant.com/download/matrixvectorvba
Specifically, take a look at the PDF Download on the site.

Simple Histogram in VBA?

I have data stored in some column (Say, Column A). The length of Column A is not fixed (depends on previous steps in the code).
I need a histogram for the values in Column A, and have it in the same sheet. I need to take the values in column A, and automatically compute M Bins, then give the plot.
I looked online for a "simple" code, but all codes are really fancy, with tons of details that I don't need, to the extent that I am not even able to use it. (I am a VBA beginner.)
I found the following code that seems to do the job, but I am having trouble even calling the function. Besides, it only does computations but does not make the plot.
Sub Hist(M As Long, arr() As Single)
Dim i As Long, j As Long
Dim Length As Single
ReDim breaks(M) As Single
ReDim freq(M) As Single
For i = 1 To M
freq(i) = 0
Next i
Length = (arr(UBound(arr)) - arr(1)) / M
For i = 1 To M
breaks(i) = arr(1) + Length * i
Next i
For i = 1 To UBound(arr)
If (arr(i) <= breaks(1)) Then freq(1) = freq(1) + 1
If (arr(i) >= breaks(M - 1)) Then freq(M) = freq(M) + 1
For j = 2 To M - 1
If (arr(i) > breaks(j - 1) And arr(i) <= breaks(j)) Then freq(j) = freq(j) + 1
Next j
Next i
For i = 1 To M
Cells(i, 1) = breaks(i)
Cells(i, 2) = freq(i)
Next i
End Sub
And then I try to call it simply by:
Sub TestTrial()
Dim arr() As Variant
Dim M As Double
Dim N As Range
arr = Range("A1:A10").Value
M = 10
Hist(M, arr) ' This does not work. Gives me Error (= Expected)
End Sub
A little late but still I want to share my solution. I created a Histogram function which might be used as array formula in the excel spread sheet. Note: you must press
CTRL+SHIFT+ENTER to enter the formula into your workbook. Input is the range of values and the number M of bins for the histogram. The output range must have M rows and two columns. One column for the bin value and one column for the bin frequency.
Option Explicit
Option Base 1
Public Function Histogram(arr As Range, M As Long) As Variant
On Error GoTo ErrHandler
Dim val() As Variant
val = arr.Value
Dim i As Long, j As Integer
Dim Length As Single
ReDim breaks(M) As Single
ReDim freq(M) As Integer
Dim min As Single
min = WorksheetFunction.min(val)
Dim max As Single
max = WorksheetFunction.max(val)
Length = (max - min) / M
For i = 1 To M
breaks(i) = min + Length * i
freq(i) = 0
Next i
For i = 1 To UBound(val)
If IsNumeric(val(i, 1)) And Not IsEmpty(val(i, 1)) Then
If val(i, 1) > breaks(M) Then
freq(M) = freq(M) + 1
Else
j = Int((val(i, 1) - min) / Length) + 1
freq(j) = freq(j) + 1
End If
End If
Next i
Dim res() As Variant
ReDim res(M, 2)
For i = 1 To M
res(i, 1) = breaks(i)
res(i, 2) = freq(i)
Next i
Histogram = res
ErrHandler:
'Debug.Print Err.Description
End Function
Not 100% sure as to the efficacy of that approach but;
Remove the parens as your calling a sub; Hist M, arr
M is declared as double but received by the function as a long; this won't work so declare it in the calling routine as long
You will need to recieve arr() As Variant
Range -> Array produces a 2 dimensional array so the elements are arr(1, 1) .. arr(n, 1)

Resources