Return array in Spreadsheet Function - arrays

The code below returns an array. I would like to use it in a spread sheet as an excel formula to return the array. However, when I do, it only returns the first value to the cell. Is there anyway to return the array in a range of equal size as the array?
Function LoadNumbers(Low As Long, High As Long) As Long()
'''''''''''''''''''''''''''''''''''''''
' Returns an array of Longs, containing
' the numbers from Low to High. The
' number of elements in the returned
' array will vary depending on the
' values of Low and High.
''''''''''''''''''''''''''''''''''''''''
'''''''''''''''''''''''''''''''''''''''''
' Declare ResultArray as a dynamic array
' to be resized based on the values of
' Low and High.
'''''''''''''''''''''''''''''''''''''''''
Dim ResultArray() As Long
Dim Ndx As Long
Dim Val As Long
'''''''''''''''''''''''''''''''''''''''''
' Ensure Low <= High
'''''''''''''''''''''''''''''''''''''''''
If Low > High Then
Exit Function
End If
'''''''''''''''''''''''''''''''''''''''''
' Resize the array
'''''''''''''''''''''''''''''''''''''''''
ReDim ResultArray(1 To (High - Low + 1))
''''''''''''''''''''''''''''''''''''''''
' Fill the array with values.
''''''''''''''''''''''''''''''''''''''''
Val = Low
For Ndx = LBound(ResultArray) To UBound(ResultArray)
ResultArray(Ndx) = Val
Val = Val + 1
Next Ndx
''''''''''''''''''''''''''''''''''''''''
' Return the array.
''''''''''''''''''''''''''''''''''''''''
LoadNumbers = ResultArray()
End Function

A UDF can certainly return an array, and your function works fine. Just select, e.g., range B2:D2, put =LoadNumbers(1, 3) into the formula bar, and hit Ctrl+Shift+Enter to tell Excel it's an array function.
Now, you can't have the UDF auto-resize the range it was called from according to its inputs (at least not without some ugly Application.OnTime hack), but you don't need to do that anyways. Just put the function in a 1000-cell-wide range to begin with, and have the UDF fill in the unused space with blank cells, like this:
Function LoadNumbers(ByVal Low As Long, ByVal High As Long) As Variant()
Dim ResultArray() As Variant
Dim Ndx As Long
Dim Val As Long
Dim SourceCols As Long
SourceCols = Application.Caller.Columns.Count
If Low > High Then
Exit Function
End If
If High - Low + 1 > SourceCols Then High = Low + SourceCols - 1
ReDim ResultArray(1 To SourceCols)
Val = Low
For Ndx = LBound(ResultArray) To (High - Low + 1)
ResultArray(Ndx) = Val
Val = Val + 1
Next Ndx
For Ndx = (High - Low + 2) To UBound(ResultArray)
ResultArray(Ndx) = vbNullString
Next Ndx
LoadNumbers = ResultArray()
End Function

A worksheet formula can only output a value to the same cell the formula was written in. As it stands, the code already produces an array. If you want the values to be shown as you copy the formula down, use a formula like this (in any cell you want) and then copy down:
=INDEX(LoadNumbers(1,10),ROWS($A$1:$A1))
If you copy down too far, you'll get a #REF! error because the LoadNumbers ran out of numbers.

I was looking for something similar (create a function in a macro, take inputs from a sheet, output an multi-dim array), and I hope my use-case below helps to answer. If not, my apologies:
Use-case:
Create and apply well-known numerical option valuation function, and output the stock price, valuation, and payoff as a 3-D array (3 columns) of #rows as specified in the function (20 in this case, as NAS variable). The code is copied - but the idea is to get the output into the sheet....
a) These inputs are static in the sheet.
b) I called the macro formula 'optval' via the 'fx' function list from an output cell I wanted to start in, and put the starting inputs into the formula.
b) The output will propagate to the cells as per the code using the NAS bound of 20 rows. Trivial, but it works.
c) you can automate the execution of this and output to the sheet - but anyhow, I hope this way helps anyway.
The module function is below (copied!) - but just put the starter inputs in i.e.
Vol=.2, Int rate = 0.05, Strike=120, Exp = 1, P type = C (or P), US?= N, i.e. european, , NAS=20 (or however many rows you want to see, and it affects the granularity of the numerical method)
Function optval(Vol, Intrate, Strike, Expn, Ptype, Etype, NAS)
ReDim S(0 To NAS) As Double
ReDim VOld(0 To NAS) As Double
ReDim VNew(0 To NAS) As Double
ReDim dummy(0 To NAS, 1 To 3)
dS = 2 * Strike / NAS
dt = 0.9 / NAS / NAS / Vol / Vol
NTS = Int(Expn / dt) + 1
dt = Expn / NTS
q = 1
If Ptype = "P" Then q = -1
For i = 0 To NAS
S(i) = i * dS
VOld(i) = Application.Max(q * (S(i) - Strike), 0)
dummy(i, 1) = S(i)
dummy(i, 2) = VOld(i) 'Payoff
Next i
For k = 1 To NTS
For i = 1 To NAS - 1
Delta = (VOld(i + 1) - VOld(i - 1)) / 2 / dS
Gamma = (VOld(i + 1) - 2 * VOld(i) + VOld(i - 1)) / dS / dS
Theta = -0.5 * Vol * Vol * S(i) * S(i) * Gamma - _
Intrate * S(i) * Delta + Intrate * VOld(i)
VNew(i) = VOld(i) - Theta * dt 'BSE
Next i
VNew(0) = VOld(0) * (1 - Intrate * dt) 'S=0
VNew(NAS) = 2 * VNew(NAS - 1) - VNew(NAS - 2) 'Infty
For i = 0 To NAS
VOld(i) = VNew(i)
Next i
If Etype = "Y" Then
For i = 0 To NAS
VOld(i) = Application.Max(VOld(i), dummy(i, 2))
Next i
End If
Next k
For i = 0 To NAS
dummy(i, 3) = VOld(i)
Next i
optval = dummy
End Function

=INDEX(LoadNumbers(1,10),ROWS($A$1:$A1),COLUMNS($B$1,B$1))

Related

VBA Excel: How to assign values to empty slots in an array through a function

Currently I'm working on a little interpolation code (Newton's Divided Difference). Problem is I'm having trouble assigning the table values that I've calculated in a function to my initial array that has the f(x) values. I keep getting the error "Can't assign to array". Any help would be greatly appreciated
Here is a little snippet of the code in question and how I declared the variables
'Dim our variables, arrays
`Dim n, i, order As Integer
Dim x(9), y(9, 9), xi As Double
n = 9
'select our x data
For i = 0 To n
x(i) = Worksheets("Newton").Range("B" & i + 4)
Next
'select our y data
For i = 0 To n
y(i, 0) = Worksheets("Newton").Range("D" & i + 4)
Next
'select the value at which we want to interpolate
xi = Range("E1")
'This is where the problem is
y = divided_Table(x, y, n)
'Message the output
MsgBox "The estimate of f(x) is " & applyNewton(xi, x, y, n)
End Sub
Function divided_Table(x, y, n)
Dim i, j As Integer
For i = 1 To n
For j = 0 To n - i
y(j, i) = ((y(j, i - 1) - y(j + 1, i - 1)) / (x(j) - x(i + j)))
Next j
Next i
End Function
Would be nice to know at which line you get the error. But here are two items you may not know...
When you say:
Dim x(9), y(9, 9), xi As Double
the system will dim x() and y() as variants and NOT as doubles. The xi will be a double.
And if you dim x(9) the array does not have 9 slots - it has 10 (0-9).
Can't say that either of these items is your problem, but it is still good to be in the know.

Excel VBA - Determining Column or Row Target of Array UDF

I have a simple excel UDF for converting an array of mass values to mol fractions. Most times, the output will be a column array (n rows by 1 column).
How, from within the VBA environment, do I determine the dimensions of the target cells on the worksheet to ensure that it should be returned as n rows by 1 column versus n columns by 1 row?
Function molPct(chemsAndMassPctsRng As Range)
Dim chemsRng As Range
Dim massPctsRng As Range
Dim molarMasses()
Dim molPcts()
Set chemsRng = chemsAndMassPctsRng.Columns(1)
Set massPctsRng = chemsAndMassPctsRng.Columns(2)
chems = oneDimArrayZeroBasedFromRange(chemsRng)
massPcts = oneDimArrayZeroBasedFromRange(massPctsRng)
'oneDimArrayZeroBasedFromRange is a UDF to return a zero-based array from a range.
ReDim molarMasses(UBound(chems))
ReDim molPcts(UBound(chems))
totMolarMass = 0
For chemNo = LBound(chems) To UBound(chems)
molarMasses(chemNo) = massPcts(chemNo) / mw(chems(chemNo))
totMolarMass = totMolarMass + molarMasses(chemNo)
Next chemNo
For chemNo = LBound(chems) To UBound(chems)
molPcts(chemNo) = Round(molarMasses(chemNo) / totMolarMass, 2)
Next chemNo
molPct = Application.WorksheetFunction.Transpose(molPcts)
End Function
I understand that, if nothing else, I could have an input parameter to flag if return should be as a row array. I'm hoping to not go that route.
Here is a small example of a UDF() that:
accepts a variable number of input ranges
extracts the unique values in those ranges
creates a suitable output array (column,row, or block)
dumps the unique values to the area
Public Function ExtractUniques(ParamArray Rng()) As Variant
Dim i As Long, r As Range, c As Collection, OutPut
Dim rr As Range, k As Long, j As Long
Set c = New Collection
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
' First grab all the data and make a Collection of uniques
'
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
On Error Resume Next
For i = LBound(Rng) To UBound(Rng)
Set r = Rng(i)
For Each rr In r
c.Add rr.Value, CStr(rr.Value)
Next rr
Next i
On Error GoTo 0
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
' next create an output array the same size and shape
' as the worksheet output area
'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
k = 1
With Application.Caller
ReDim OutPut(1 To .Rows.Count, 1 To .Columns.Count)
End With
For i = LBound(OutPut, 1) To UBound(OutPut, 1)
For j = LBound(OutPut, 2) To UBound(OutPut, 2)
If k < c.Count + 1 Then
OutPut(i, j) = c.Item(k)
k = k + 1
Else
OutPut(i, j) = ""
End If
Next j
Next i
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
' put the data on the sheet
'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
ExtractUniques = OutPut
End Function
You should return two dimensional arrays: n × 1 for row and 1 × n for column vectors.
So you need either
Redim molPcts(1, Ubound(chems) + 1)
or
Redim molPcts(Ubound(chems) + 1, 1)
To refer to them, you need to use both indices:
molPcts(1, chemNo + 1)
or
molPcts(chemNo + 1, 1)
If you prefer 0-based arrays, the redim should be like this:
Redim molPcts(0 To 0, 0 To Ubound(chems))
Redim molPcts(0 To Ubound(chems), 0 To 0)

Sort an array by selecting the occurances vba

I am currently writing a macro for Solidworks using VBA. This macro sets an array with the x, y, and arctan locations of a note. I am then looking to sort array using a bubblesort method. The array is a single dimensional array and it first sets the x values, then the y and then the arctan. If there are 9 notes then 0-8 is the x values, 9-17 are the y values, and 18-26 are the arctan. This is where I am running into an issue. I need to select the range that the sort uses.
arrlen = arrlen - 1
ReDim Preserve vloc(arrlen - 1)
BubbleSort1 vloc
arrlen1 = arrlen1 - 1
ReDim Preserve vloc(arrlen To arrlen1)
BubbleSort1 vloc
Above is my code where I call up the sorts. arrlen and arrlen1 are counters for the total notes for each value.
I am getting a script out of range error on the second sort call out.
I am not against rewriting both the initial setting of the array and the sort to us a multidimensional array, but I just don't know how to properly do that.
Below is the code where I set the array.
Set swFirstNote = swView.GetFirstNote
Set swNote = swView.GetFirstNote
ReDim notes(notesTotalCounter)
ReDim vloc(notesTotalCounter)
i = 0
arrlen = 0
While Not swNote Is Nothing
If swNote.GetText Like "`*" Then
Set swAnno = swNote.GetAnnotation
loc = swAnno.GetPosition
Dim t As Double
Dim x As Double
Dim y As Double
x = loc(0)
y = loc(1)
t = ArcTan2(cpX - loc(0), cpY - loc(1))
vloc(i) = x
i = i + 1
arrlen = arrlen + 1
End If
Set swNote = swNote.GetNext
Wend
Set swFirstNote = swView.GetFirstNote
Set swNote = swView.GetFirstNote
ReDim Preserve vloc(notesTotalCounter)
i = arrlen
arrlen1 = arrlen
While Not swNote Is Nothing
If swNote.GetText Like "`*" Then
Set swAnno = swNote.GetAnnotation
ReDim Preserve vloc(i)
loc = swAnno.GetPosition
x = loc(0)
y = loc(1)
t = ArcTan2(cpX - loc(0), cpY - loc(1))
vloc(i) = y
i = i + 1
arrlen1 = arrlen1 + 1
End If
Set swNote = swNote.GetNext
Wend
Any help would be greatly appreciated.
Here is something that I wrote recently to sort an array of training subjects into alphabetical order. After returning from this subroutine, I load the array into a combo box. The training subjects are listed in a priority order in a row of a worksheet, from cell C2 and to the left. The number of training subjects varies as it can be added to and deleted at various times. Here is the subroutine that gets called to create the global array strSubject() to pass to the Userform Activate event to populate the combo box.
You should be able to take the basic idea behind the sorting routine and adapt it for your code. Because all of my records are text, I use the tilde (~) to ensure the second array gets populated with the next available record alphabetically. As you are using numbers, you may want to replace the tilde in thie code with a number that is less than any possible value in your data, so if your lowest value is 1, make the comparator -1. If your lowest possible value is -i,ooo,ooo,ooo, make your comparator lower than that. If you are unsure, run a third routine to find the lowest value in your data array, subtract 1 and make that value your comparator value.
Sub Create_Alphabetical_Subject_List()
Dim f As Integer
Dim x As Integer: x = 1
Dim y As Integer: y = 1
Dim strTempSubject() As String
Set wss = ThisWorkbook.Sheets("Internal Training Matrix")
varSubjectCount = WorksheetFunction.CountA(Worksheets("Internal Training Matrix").Range("C2", Worksheets("Internal Training Matrix").Range("C2").End(xlEnd)))
ReDim varSortedFlag(varSubjectCount)
ReDim varSorted(varSubjectCount)
ReDim strTempSubject(varSubjectCount)
ReDim strSubject(varSubjectCount)
For x = 1 To varSubjectCount
If wss.Cells.Item(2, x + 2) <> "" And wss.Cells.Item(2, x + 2) <> "Spare" Then
strTempSubject(x) = wss.Cells.Item(2, x + 2) ' From row 2 col 3 initially
varSorted(x) = "~"
varSortedFlag(x) = ""
End If
Next x
For y = 1 To varSubjectCount
For x = 1 To varSubjectCount
If varSortedFlag(x) = "" And strTempSubject(x) < varSorted(y) Then
varSorted(y) = strTempSubject(x) ' Swap out a lower value
f = x ' Track which record was last copied to new array
End If
Next x
strSubject(y) = varSorted(y)
varSortedFlag(f) = "Done" ' Remove the record from future comparrisons
Next y
End Sub

Excel VBA Array For Each Unique Values

i try to create an array and fill it with unique values.
Due to check if the value is unique, i fail to create a basic "for each" procedure.
I just want to create a unique random number, put it into an array, create another unique random number an put it into the same array.
If fail into search about that topic and failed to google that problem, therefore i have to ask the community at stackoverlofw.
I dont have any valuable code because nothing worked.
I only have an google code which worked for me in cells at excel, but not in array.
For Each Cell In Selection.Cells
Do
rndNumber = Int((High - Low + 1) * Rnd() + Low)
Loop Until Selection.Cells.Find(rndNumber, LookIn:=xlValues, LookAt:=xlWhole) Is Nothing
Cell.Value = rndNumber
Next
Hopefully
Option Explicit
Function GetRandomsArray(ByVal n As Long, Low As Long, High As Long)
Dim rndNumber As Long
If High - Low + 1 < n Then n = High - Low + 1
With CreateObject("Scripting.Dictionary")
Do
rndNumber = Int((High - Low + 1) * Rnd() + Low)
.Item(rndNumber) = 1
Loop While .Count < n
GetRandomsArray = .Keys
End With
End Function
To be used in a "Main" sub as follows:
Dim myArray As Variant
myArray = GetRandomsArray(5, 1, 10) '<--| get an array of 5 unique random numbers between 1 and 10

Interpolate between array points

In the following code I built 2 arrays:
One contains a "degree data" (so basically an Integer array increasing one by one) representing a portion of degrees in a circumference.
The second one a "power data" (a Double, whose values keep more or less cubic increasing until a maximum corresponding to a 0 in the first array. Then they will decrease almost as they increased). This array goes for example from values that are more or less -4.0 to more or less -4.0.
I thought I could "simplify" the second one to a parabola (looks very much like one) and use its coefficients (calculated via LinEst) A, B, C and D to interpolate data between points.
What I do need is to find with a 0.1 degrees precision the 2 numbers which are nearest to -3.0 and then find their "distance" in degrees.
Problem is: I can't manage to do it. There is something I'm missing and the coefficients seems not to represent my set of data.
Dim i As Integer, j As Integer
Dim MaxVal As Double, MaxAngle As Integer, CyclicAngle As Double
Dim XValues() As Double, YValues() As Double, Coeff As Variant
Dim LeftAngle As Double, RightAngle As Double, LeftAngleValue As Double, RightAngleValue As Double
' Searches for the maximum value and its angle
MaxVal = -80
ReDim YValues(359 * 3 + 2)
For i = 3 To 362 ' This will fill arrays from a worksheet (defined as Public in
' another subroutine) in which the data starts from row 3. I need the data stored in
' the 2nd column)
For j = 0 To 2 ' since the array represents a circumference, i make it "cyclic"
YValues((i - 3) + (360 * j)) = TargetSheet.Cells(i, 2)
Next j
If TargetSheet.Cells(i, 2) > MaxVal Then
MaxVal = TargetSheet.Cells(i, 2)
MaxAngle = i - 3
End If
Next
' The following searches the "middle" maximum
i = 0
j = 0
Do Until j = 2
If YValues(i) = MaxVal Then
j = j + 1
CyclicAngle = i
End If
i = i + 1
Loop
' Searches in the middle for the <-3 (we name it "-4") values
i = CyclicAngle
Do Until YValues(i) < -3
i = i + 1
Loop
RightAngle = i + 1
i = CyclicAngle
Do Until YValues(i) < -3
i = i - 1
Loop
LeftAngle = i - 1
' Copying only the "-4" to "-4"
ReDim XValues(RightAngle - LeftAngle)
For i = 0 To RightAngle - LeftAngle
XValues(i) = YValues(LeftAngle + i)
Next i
' Now correctly store the data in a new ordered array
ReDim YValues(UBound(XValues))
For i = 0 To UBound(XValues)
YValues(i) = XValues(i)
XValues(i) = LeftAngle - 360 + i
Next i
Here is the critic point:
' Gets the coefficients of a 3rd degree curve representing the Y-Array
Coeff = Application.LinEst(Application.Transpose(YValues), Application.Power(Application.Transpose(XValues), Array(1, 2, 3)), True, False)
' Sets the arrays to have a point every 0.1°
LeftAngle = LeftAngle * 10
RightAngle = RightAngle * 10
MaxAngle = MaxAngle * 10
ReDim XValues(RightAngle - LeftAngle)
ReDim YValues(RightAngle - LeftAngle)
For i = LeftAngle To RightAngle
XValues(i - LeftAngle) = i / 10
YValues(i - LeftAngle) = Coeff(1) * (i / 10) ^ 3 + Coeff(2) * (i / 10) ^ 2 + Coeff(3) * (i / 10) + Coeff(4)
Next
Now if I look at the YValues array, the numbers stored inside don't look absolutely how they should be.
How do I interpolate to find those -3s then?
The error was in this piece of code: I didn't think I could not "rotate" the XValues array
' Now correctly store the data in a new ordered array
ReDim YValues(UBound(XValues))
For i = 0 To UBound(XValues)
YValues(i) = XValues(i)
XValues(i) = LeftAngle + i ' Was LeftAngle - 360 + i
Next i
I should now be able to find my -3.0 with a simple loop iteration.
There are of course a few obstacles but since I have the LeftAngle and RightAngle references I should be able to solve all.
Full Solution
Please see my Question and correct it with the code above. From the "critic point" it goes:
' Gets the coefficients of a 3rd degree curve representing the Y-Array
' and build an interpolation array whose points are every 0.1°
Coeff = Application.LinEst(Application.Transpose(YValues), Application.Power(Application.Transpose(XValues), Array(1, 2, 3)), True, False)
'TargetSheet.Range("E1:H1") = Coeff
' Sets the arrays to be every 0.1°
LeftAngle = LeftAngle * 10
RightAngle = RightAngle * 10
j = CInt(LeftAngle)
k = CInt(RightAngle)
ReDim XValues(k - j)
ReDim YValues(k - j)
For i = j To k
XValues(i - LeftAngle) = i / 10
YValues(i - LeftAngle) = Coeff(1) * (i / 10) ^ 3 + Coeff(2) * (i / 10) ^ 2 + Coeff(3) * (i / 10) + Coeff(4)
Next
' Now searches for the -3dB angles with a better resolution
RightAngleValue = 10
LeftAngleValue = 10
CyclicAngle = CyclicAngle * 10
i = 0
Do Until XValues(i) > CyclicAngle / 10
If Abs(YValues(i) +3) < LeftAngleValue Then
LeftAngleValue = Abs(YValues(i) + 3)
LeftAngle = i * 0.1
End If
i = i + 1
Loop
Do Until i > UBound(XValues)
If Abs(YValues(i) +3) < RightAngleValue Then
RightAngleValue = Abs(YValues(i) + 3)
RightAngle = i * 0.1
End If
i = i + 1
Loop
myFunction = RightAngle - LeftAngle

Resources