accessing individual array elements in VBA function - arrays

VBA newbie here. I am trying to pass an array (it is static, but please answer for dynamic range as well) to a function. Then assign individual array elements to unique variables and use these variables in a custom formula. I just browsed around and wrote the code but keep getting #VALUE! error. The gist of the code is below:
Public Function mytest(ByRef arr1 As Range)
Dim A As Double
Dim B As Double
A = arr1(0)
B = arr1(1)
mytest = A + B 'The actual formula is a bit more complicated than simple addition
End Function
I am not sure what am i doing wrong at all. If anyone has a solution, can you please explain why it works as well. I appreciate any and all help I can get.
Many thanks !

As Coleman pointed out a range is not an array, consider:
Public Function mytest(ByRef arr1 As Range)
Dim A As Double
Dim B As Double
A = arr1(1, 1)
B = arr1(2, 1)
mytest = A + B 'The actual formula is a bit more complicated than simple addition
End Function
NOTE:
we treat the Range similar to an array
it is two dimensional
it is 1 based
if you are only dealing with the Range's value, you could create an internal array within your function that directly maps to the passed Range.
if the Range is truly dynamic, (like a Spill range) then all you need to pass is the anchor cell.

You seem to be trying to use a worksheet range as a 0-based array. That doesn't really make sense although using the Cells property of a range (which you are actually trying to do implicitly) you can come close:
Public Function mytest(arr1 As Range)
Dim A As Double
Dim B As Double
A = arr1.Cells(1)
B = arr1.Cells(2)
mytest = A + B 'The actual formula is a bit more complicated than simple addition
End Function
In the above code, you can drop Cells() since it will function as the default property here, but most experienced VBA programmers like to make explicit what property they are using.
This will more or less work for 1-dimensional ranges but might not work as expected with 2-dimensional ranges. Cells takes up to 2 indices and in general I think that the code is clearer when you are explicit about the full indices (e.g. A = arr1.Cells(1,1) and B = arr1.Cells(2,1)).

The question isn't in the code you posted but in the procedure that calls it. Here the calling procedure first assigns values to the cells in the worksheet (for testing purposes), then passes the range to the function which extracts the values into an array and then uses that array to calculate a return value.
Private Sub TestmyTest()
Dim Rng1 As Range
Cells(1, "A").Value = 3.14
Cells(2, "A").Value = 3
Set Rng1 = Range("A1:A2")
Debug.Print myTest(Rng1)
End Sub
Function myTest(Rng1 As Range) As Double
' procedures are 'Public' unless declared as 'Private'
' Therefore only declare "Private" or nothing
' Arguments are passed ByRef unless they are declared as 'ByVal'
' Therefore I recommend to omit "ByRef"
Dim Arr As Variant
Dim A As Double
Dim B As Double
' this creates a 1-based 3-D array of 2 row and 1 column
Arr = Rng1.Value
A = Arr(1, 1)
B = Arr(2, 1)
myTest = A + B 'The actual formula is a bit more complicated than simple addition
End Function

Related

How to pass a dynamcally allocated array into a function in Excel VBA

I am very fresh with Excel VBA, so I ask you for understanding. Doing research on the internet has not resolved my problem. So I wish to pass an array of numerical values in VBA into a function, namely enter cash flows into an NPV function all in Excel VBA code.
Dim Postavke() As String, velikost As Integer, i As Integer
velikost = WorksheetFunction.CountA(Worksheets("List1").Columns(11))
ReDim Postavke(velikost)
For i = 1 To velikost
Postavke(i) = Cells(i + 1, 11).Value
Next i
Now I want to pass the Postavke() array to the NPV function:
Dim NSV As Long
NSV = NPV(0.06, ByRef Postavke() As Long))
which always goes an error. Any thoughts to how this might be done?
Thank you in advance. Best regards.
Create the function in this way:
Function NPV(whatever As Double, Postavke() As String) As Long
Dim x As Long
'do whatever you want with your code, work on the 'x' variable and finally ends with:
NPV = x
'or define NPV earier and use Exit Function after..
End Function
You must decide if the Postavke() array must be declared as a string or a long. I kept your initial declaration...
And call the function like this:
NSV = NPV(0.06, Postavke())
In order to properly load velikost, starting with 1, you must add on top of your module code:
Option Base 1
Otherwise, you must take care (like #SJR remarked) that the array is zero-based by default.

Creating an Array from a Range in VBA

I'm having a seemingly basic problem but can't find any resources addressing it.
Simply put, I just want to load the contents of a Range of cells (all one column) into an Array.
I am able to accomplish this by means of
DirArray = Array(Range("A1"), Range("A2"))
But for some reason, I cannot create the array when expressed this way:
DirArray = Array(Range("A1:A2"))
My real Range is much longer (and may vary in length), so I don't want to have to individually enumerate the cells this way. Can anyone tell me how to properly load a whole Range into an Array?
With the latter code:
MsgBox UBound(DirArray, 1)
And
MsgBox UBound(DirArray)
Return 0, whereas with the former they return 1.
Just define the variable as a variant, and make them equal:
Dim DirArray As Variant
DirArray = Range("a1:a5").Value
No need for the Array command.
If we do it just like this:
Dim myArr as Variant
myArr = Range("A1:A10")
the new array will be with two dimensions. Which is not always somehow comfortable to work with:
To get away of the two dimensions, when getting a single column to array, we may use the built-in Excel function “Transpose”. With it, the data becomes in one dimension:
If we have the data in a row, a single transpose will not do the job. We need to use the Transpose function twice:
Note: As you see from the screenshots, when generated this way, arrays start with 1, not with 0. Just be a bit careful.
Edit June.2021:
In newer versions of Excel, the function is: Application.WorksheetFunction.Transpose()
Using Value2 gives a performance benefit. As per Charles Williams blog
Range.Value2 works the same way as Range.Value, except that it does not check the cell format and convert to Date or Currency. And thats probably why its faster than .Value when retrieving numbers.
So
DirArray = [a1:a5].Value2
Bonus Reading
Range.Value: Returns or sets a Variant value that represents the value of the specified range.
Range.Value2: The only difference between this property and the Value property is that the Value2 property doesn't use the Currency and Date data types.
This function returns an array regardless of the size of the range.
Ranges will return an array unless the range is only 1 cell and then it returns a single value instead. This function will turn the single value into an array (1 based, the same as the array's returned by ranges)
This answer improves on previous answers as it will return an array from a range no matter what the size. It is also more efficient that other answers as it will return the array generated by the range if possible. Works with single dimension and multi-dimensional arrays
The function works by trying to find the upper bounds of the array. If that fails then it must be a single value so we'll create an array and assign the value to it.
Public Function RangeToArray(inputRange As Range) As Variant()
Dim size As Integer
Dim inputValue As Variant, outputArray() As Variant
' inputValue will either be an variant array for ranges with more than 1 cell
' or a single variant value for range will only 1 cell
inputValue = inputRange
On Error Resume Next
size = UBound(inputValue)
If Err.Number = 0 Then
RangeToArray = inputValue
Else
On Error GoTo 0
ReDim outputArray(1 To 1, 1 to 1)
outputArray(1,1) = inputValue
RangeToArray = outputArray
End If
On Error GoTo 0
End Function
In addition to solutions proposed, and in case you have a 1D range to 1D array, i prefer to process it through a function like below. The reason is simple: If for any reason your range is reduced to 1 element range, as far as i know the command Range().Value will not return a variant array but just a variant and you will not be able to assign a variant variable to a variant array (previously declared).
I had to convert a variable size range to a double array, and when the range was of 1 cell size, i was not able to use a construct like range().value so i proceed with a function like below.
Public Function Rng2Array(inputRange As Range) As Double()
Dim out() As Double
ReDim out(inputRange.Columns.Count - 1)
Dim cell As Range
Dim i As Long
For i = 0 To inputRange.Columns.Count - 1
out(i) = inputRange(1, i + 1) 'loop over a range "row"
Next
Rng2Array = out
End Function
I'm another vote for iterating through the cells in the range. Unless somebody has found a workaround, my experience trying to assign the range directly to a Variant has been that it works fine (albeit returning a 2-dimensional array when I really only need 1D) except if my range has multiple areas, like for example, when I want just the visible cells in a column of a filtered table, or if I have ctrl-selected different blocks of cells on a sheet.
Iterating through all the cells in the range with a for..each loop always produces the results I expect.
Public Function RangeToArray(ByRef myRange As Range)
Dim i As Long
Dim individualCell As Range
ReDim myArray(myRange.Count - 1)
For Each individualCell In myRange
myArray(i) = individualCell.Text ' or maybe .Value
i = i + 1
Next
RangeToArray = myArray
End Function
I wanted to add this as a comment to Paolo's answer since it's pretty similar but I am a newbie and don't have enough reputation, so here's another slightly different answer.
Adding to #Vityata 's answer, below is the function I use to convert a row / column vector in a 1D array:
Function convertVecToArr(ByVal rng As Range) As Variant
'convert two dimension array into a one dimension array
Dim arr() As Variant, slicedArr() As Variant
arr = rng.value 'arr = rng works too (https://bettersolutions.com/excel/cells-ranges/vba-working-with-arrays.htm)
If UBound(arr, 1) > UBound(arr, 2) Then
slicedArr = Application.WorksheetFunction.Transpose(arr)
Else
slicedArr = Application.WorksheetFunction.index(arr, 1, 0) 'If you set row_num or column_num to 0 (zero), Index returns the array of values for the entire column or row, respectively._
'To use values returned as an array, enter the Index function as an array formula in a horizontal range of cells for a row,_
'and in a vertical range of cells for a column.
'https://usefulgyaan.wordpress.com/2013/06/12/vba-trick-of-the-week-slicing-an-array-without-loop-application-index/
End If
convertVecToArr = slicedArr
End Function
Transpose is a great advice.
I have multiple arrays in my app. Some global, some local, some loaded from ranges and some created programatically.
I had numerous problems with dimensioning. Now, with transpose they are all one dimension.
I did have to modify code slightly, because one version runs on Excel 2003 and another (slower) on 2010.
Caution: You will have to Transpose the array again, when saving it to a range.
Using the shape of the Range
Another approach in creating a function for ArrayFromRange would be using the shape and size of the Range to determine how we should structure the array. This way we don't have to load the data into an intermediate array to determine the dimension.
For instance, if the target range is only one cell, then we know we want to return an array with the single value in it Array(target.value).
Below is the complete function that should deal with all cases. Note, this uses the same technique of using the Application.Transpose method to reshape the array.
' Helper function that returns an array from a range with the
' correct dimensions. This fixes the issue of single values
' not returning as an array, and when a 2 dimension array is returned
' when it only has 1 dimension of data.
'
' #author Robert Todar <robert#roberttodar.com>
Public Function ArrayFromRange(ByVal target As Range) As Variant
Select Case True
' Single cell
Case target.Cells.Count = 1
ArrayFromRange = Array(target.Value)
' Single Row
Case target.Rows.Count = 1
ArrayFromRange = Application.Transpose( _
Application.Transpose(target.Value) _
)
' Single Column
Case target.Columns.Count = 1
ArrayFromRange = Application.Transpose(target.Value)
' Multi dimension array
Case Else
ArrayFromRange = target.Value
End Select
End Function
Testing the ArrayFromRange function
As a bonus, here are the tests that I ran to check that this function works.
' #requires {function} ArrayDimensionLength
' #requires {function} ArrayCount
Private Sub testArrayFromRange()
' Setup a new workbook/worksheet for
' adding testing data
Dim testWorkbook As Workbook
Set testWorkbook = Workbooks.Add
Dim ws As Worksheet
Set ws = testWorkbook.Worksheets(1)
' Add sample data for testing.
ws.Range("A1:A2") = Application.Transpose(Array("A1", "A2"))
ws.Range("B1:B2") = Application.Transpose(Array("B1", "B2"))
' This section will run all the tests.
Dim x As Variant
' Single cell
x = ArrayFromRange(ws.Range("A1"))
Debug.Assert ArrayDimensionLength(x) = 1
Debug.Assert ArrayCount(x) = 1
' Single Row
x = ArrayFromRange(ws.Range("A1:B1"))
Debug.Assert ArrayDimensionLength(x) = 1
Debug.Assert ArrayCount(x) = 2
' Single Column
x = ArrayFromRange(ws.Range("A1:A2"))
Debug.Assert ArrayDimensionLength(x) = 1
Debug.Assert ArrayCount(x) = 2
' Multi Column
x = ArrayFromRange(ws.Range("A1:B2"))
Debug.Assert ArrayDimensionLength(x) = 2
Debug.Assert ArrayCount(x) = 4
' Cleanup testing environment
testWorkbook.Close False
' Print result
Debug.Print "testArrayFromRange: PASS"
End Sub
Helper functions for the tests
In my tests I used two helper functions: ArrayCount, and ArrayDimensionLength. These are listed below for reference.
' Returns the length of the dimension of an array
'
' #author Robert Todar <robert#roberttodar.com>
Public Function ArrayDimensionLength(sourceArray As Variant) As Integer
On Error GoTo catch
Do
Dim currentDimension As Long
currentDimension = currentDimension + 1
' `test` is used to see when the
' Ubound throws an error. It is unused
' on purpose.
Dim test As Long
test = UBound(sourceArray, currentDimension)
Loop
catch:
' Need to subtract one because the last
' one errored out.
ArrayDimensionLength = currentDimension - 1
End Function
' Get count of elements in an array regardless of
' the option base. This Looks purely at the size
' of the array, not the contents within them such as
' empty elements.
'
' #author Robert Todar <robert#roberttodar.com>
' #requires {function} ArrayDimensionLength
Public Function ArrayCount(ByVal sourceArray As Variant) As Long
Dim dimensions As Long
dimensions = ArrayDimensionLength(sourceArray)
Select Case dimensions
Case 0
ArrayCount = 0
Case 1
ArrayCount = (UBound(sourceArray, 1) - LBound(sourceArray, 1)) + 1
Case Else
' Need to set arrayCount to 1 otherwise the
' loop will keep multiplying by zero for each
' iteration
ArrayCount = 1
Dim dimension As Long
For dimension = 1 To dimensions
ArrayCount = ArrayCount * _
((UBound(sourceArray, dimension) - LBound(sourceArray, dimension)) + 1)
Next
End Select
End Function

Reference a range froma different sheet

I'm trying to create a program that optimizes histogram bin width...before that though, I need help with a seemingly simple task - setting a Range mentioned in a cell into a VBA array.
I'd like the data for the histogram to be on any sheet, in this case 'Data'!B4:M12. This is mentioned in D4 of the sheet I want the histogram to appear on. I keep getting an error when I run my code though, even after changing it multiple times. There's clearly a syntax error that I don't know how to handle.
Any assistance would be much appreciated!
Sub Histogram_Shimazaki_Shinomoto()
Dim data_range As String, min_bins As Integer, max_bins As Integer
Dim Data()
Dim x_min As Double, x_max As Double
data_range = Cells(4, 4) ' data range
min_bins = Cells(5, 4) ' min # of bins
max_bins = Cells(6, 4) ' max # of bins
Set Data = Range(data_range)
x_min = WorksheetFunction.Min(Data)
MsgBox x_min
End Sub
You are trying to set an array of variant equal to a Range -- but that isn't possible since you can't assign to an array. You could assign a range to a simple Variant (or to a Range variable). You could change
Dim Data()
to
Dim Data as Variant
Note the absence of parenthesis. Also, as a stylistic point I think that it is good to be explicit about the type, even though Variant is the default.
This might be enough for your code to work, although if all you want is the minimum value in the range, you could change
Set Data = Range(data_range)
to
Data = Range(data_range).Value
If the sheet "Data" isn't the active sheet and data_range contains "B4:M12" then you would need to use
Data = Sheets("Data").Range(data_range).Value
since Range returns a range on the active sheet unless explicitly qualified by a reference to the sheet.

Excel UDF that accepts both Range and Array as Parameter like 'SUM'

I an writing a UDF that needs to accept both Arrays and Ranges.
Usually declaring parameter as variant would work but a Range is an object so this no longer applies. That being said bellow I pasted code that only works when passing an array.
Here is theorethical example, based on SUM:
Function TSUM(numbers() As Variant) As Variant
Dim i As Integer
For i = 1 To UBound(numbers, 1)
TSUM = TSUM + numbers(i)
Next i
End Function
=TSUM({1,1}) Returns 2
=TSUM(A1:B1) Returns #VALUE!
So how can I fix above example to accept Ranges as well?
If you are content to sum the array/range item by item, I would just change to using a For Each loop that works well for either Ranges or Arrays.
Here is that version
Public Function TSUM(numbers As Variant) As Variant
Dim i As Variant
For Each i In numbers
TSUM = TSUM + i
Next i
End Function
If you generally want to work a function based on the type of the argument, you can use TypeName() and switching logic. Here is you function with that approach. I called it TSUM2 for uniqueness.
Public Function TSUM2(numbers As Variant) As Variant
Dim i As Integer
If TypeName(numbers) = "Range" Then
TSUM2 = Application.WorksheetFunction.Sum(numbers)
Else
For i = 1 To UBound(numbers, 1)
TSUM2 = TSUM2 + numbers(i)
Next i
End If
End Function
Note in both examples, I removed the parentheses from the numbers parameters (was numbers() as Variant before). This allows it to accept Range inputs.
If you take the second approach, be sure to debug and verify the TypeNames that could come through.

Assigning an array value to a variable in VBA causes my UDF to exit

I can't seem to figure out why this UDF is exiting on the currentInput = inputArray(i). Here is the relevant code:
Function OrderRange(inputRange As Range) As Variant
Dim length As Integer
inputHeight = inputRange.Count
Dim inputArray As Variant
inputArray = inputRange
Dim strippedArray() As Variant
ReDim strippedArray(0 To (inputHeight - 1))
Dim currentInput As String
Dim i As Integer
For i = 0 To (inputHeight - 1)
currentInput = inputArray(i)
'...computations on currentInput...'
strippedArray(i) = currentInput
Next i
OrderRange = strippedArray
End Function
The debugger reaches currentInput = inputArray(i) but once I move to the next line, the function terminates and a #VALUE! error is entered into the cell I call the function from. I know this is a specific question, but I'm sure it's a general issue and I'll edit this original post to reflect what the general problem is.
Edit: This is an issue regarding assignment of a range to a variant array.
Variant arrays created by setting them equal to ranges have two dimensions even if they are only one column, or row, wide. So if you call the function with A1:A10 you'll get a 10 * 1 array. Also the lower bound of the dimensions will be one, not zero. So you'd have to do something like:
For i = 1 To (inputHeight)
currentInput = inputArray(i, 1)
Also you should use Option Explicit so that you're reminded to declare all variables. InputHeight is never declared.

Resources