Function will not return array when range contains only one value - vba

I have a function meant to return an array which is created out of a single-column list of data. I have been using this function's return value essentially as a pseudo-global variable (LINENAMES_ARRAY) which I pass to many functions. Those functions than do checks on it such as If Len(Join(LINENAMES_ARRAY)) = 0 Then or go through items with For Each statements. Here is the code:
Function LINENAMES_ARRAY() As Variant
'returns an array of all items in the main sheet linenames column
LINENAMES_ARRAY = Application.Transpose(MAIN.Range( _
MAIN.Cells(MAIN_HEAD_COUNT + 1, MAIN_LINENAMES_COLUMN), _
MAIN.Cells(LINENAMES_COUNT + 1, MAIN_LINENAMES_COLUMN)))
End Function
I recently stumbled on one of those you-don't-see-it-till-you-see-it problems while using this workbook for a new project, where if the array happens to be only 1 element, everything fails. Apparently in that case, this returns a single value so Join() will fail For Each __ in LINENAMES_ARRAY will too. Why won't it treat this as a 1x1 array rather than a free value? I have started to mitigate the problem by rewriting functions where this is called, to check whether it is an array, then do some other procedure. Things like:
For j = 1 To LINENAMES_COUNT
LINES_BOX.AddItem lineNames(j)
Next j
is changed to:
If Not IsArray(LINENAMES_ARRAY) Then
myListBox.AddItem CStr(LINENAMES_ARRAY)
Else
For j = 1 To LINENAMES_COUNT
LINES_BOX.AddItem LINENAMES_ARRAY(j)
Next j
End If
However this becomes messy and is adding many extra checks to my code that I would prefer to handle in the LINENAMES_ARRAY function. Is there a way to return a 1x1 array? Or any other workaround?

An array can have a single element if you create it as a single element array and populate it in an array manner.
Option Explicit
Dim MAIN_HEAD_COUNT As Long
Dim LINENAMES_COUNT As Long
Dim MAIN_LINENAMES_COLUMN As Long
Dim MAIN As Worksheet
Sub stuff()
Dim arr As Variant
Set MAIN = Worksheets("Sheet1")
MAIN_LINENAMES_COLUMN = 2
MAIN_HEAD_COUNT = 2
LINENAMES_COUNT = 2
arr = LINENAMES_ARRAY()
Debug.Print IsArray(arr)
Debug.Print LBound(arr) & ":" & UBound(arr)
End Sub
Function LINENAMES_ARRAY() As Variant
Dim a As Long, tmp() As Variant
ReDim tmp(0 To LINENAMES_COUNT - MAIN_HEAD_COUNT)
For a = 0 To LINENAMES_COUNT - MAIN_HEAD_COUNT
tmp(a) = MAIN.Range(MAIN.Cells(MAIN_HEAD_COUNT + 1, MAIN_LINENAMES_COLUMN), _
MAIN.Cells(LINENAMES_COUNT + 1, MAIN_LINENAMES_COLUMN)).Cells(a).Value2
Next a
'returns an array of all items in the main sheet linenames column
LINENAMES_ARRAY = tmp
End Function
Results from the VBE's Immediate window:
True
0:0

Related

Populate an Excel Table with a One Dimensional array using VBA

I am trying to populate a one column table in excel which I can connect to power query, using vba and a one dimensional array.
So the user puts a list into a textbox where each item is seperated by a semicolon, then it brings that into the array. So far I have :
Dim arrSIOCodes As Variant
arrSIOCodes = Split(tbSIOCodes, ";")
ThisWorkbook.Sheets("CAEATFA_SIO").Activate
Call ChangeTableToArray(tbl:=ThisWorkbook.Sheets("CAEATFA_SIO").ListObjects("tblSIOCodes"), ar:=arrSIOCodes)
and I got this sub from another stack overflow post but I need to customize it to my issue and I am having trouble
Sub ChangeTableToArray(tbl As ListObject, ar)
Dim newRows As Long: newRows = 1 + UBound(ar, 1) - LBound(ar, 1)
If Not tbl.DataBodyRange Is Nothing Then tbl.DataBodyRange.EntireRow.Delete
If newRows > 1 Then tbl.HeaderRowRange.Resize(newRows - 1).Offset(2).EntireRow.Insert
tbl.HeaderRowRange.Resize(newRows, 1 + UBound(ar, 1) - LBound(ar, 1)).Offset (1).value = ar
End Sub
This is what the current code is doing:
however this is what I need it to do:
You are resizing the number of columns in the destination, but you only have one column (if you Transpose the array) to store. So do something like:
tbl.HeaderRowRange.Resize(newRows, 1).Offset (1).value = Application.Transpose(ar)
About to go into a late meeting, so I threw this together quickly and it's untested. But this is just another way to look at it:
Sub ArrayToTable(ByVal ws As Worksheet, ParamArray YourArr() As Variant)
Dim TmpArr As Variant, i As Long, LineNum As Long
TmpArr = YourArr(0)
LineNum = 1 'whatever method you use to find linenumbers
For i = LBound(TmpArr) To UBound(TmpArr)
ws.Cells(LineNum, 1).Value = TmpArr(i)
LineNum = LineNum + 1
Next
End Sub

VBA UBound Function

I'm trying to explore UBound applications for my code in Visual Basic for Applications (VBA). Let's say I have a 4 by 2 array...(A1:B4) and I want to count the number of rows. I would think my code would be...
Function test(list) As Double
test = UBound(list)
End Function
My input is =test(A1:B4)but so far I get "#value!" error. I thought the return would be 4.
What am I doing wrong? I know how to get the number of rows using the row command but I simply want to go through the coding exercise.
List is range object not an array. Range.Value will return an array of values from a range and Range.Formula will return an array of formulas from a range.
Function test(list As Range) As Double
test = UBound(list.Value)
End Function
It seems that you have 2-dimensional array, therefore you have to provide additional parameter for UBound like:
UBound(array, dimension)
Please remember, if you get array from Excel Range than it is 1-based array.
Complete solution can look like this one:
Function testArrray(List, Dimmension)
Dim ListConverted As Variant
ListConverted = List
testArrray = UBound(ListConverted, Dimmension)
End Function
Sample call: =testArrray(G15:H20,1) produces 6 as a result which is correct.
Since you do not Dim list explicitly, it is Variant. So if you call =TEST(A1:B4) it will be a Range and not an Array. A Range does not have a UBound but does have Rows.Count.
So:
Function test(list As Range) As Double
test = list.Rows.Count
End Function
will work.
Or if you really need an Array, you could do:
Function test(list As Variant) As Double
list = list
test = UBound(list)
End Function
The list = list does the following: If list is a Range-Object then it will be implicit converted to Array since Set is not used to set an object. If list is an Array already, then it will be also an Array after that.
This is how you get the number of rows in a function.
Option Explicit
Function l_number_of_rows(rng_range As Range) As Long
l_number_of_rows = rng_range.Rows.Count
End Function
if you want the one from the dimension, this is a possible solution.
Sub test()
Dim MyArray(1 To 2, 0 To 3) As Long
MyArray(1, 0) = 10
MyArray(1, 1) = 11
MyArray(1, 2) = 12
MyArray(1, 3) = 13
MyArray(2, 0) = 20
MyArray(2, 1) = 21
MyArray(2, 2) = 22
MyArray(2, 3) = 23
Debug.Print MyArray(UBound(MyArray), 3)
End Sub
The UBound function is to give you elements in a variant array, you're passing it a range. You could try this:
Sub mains()
Dim myRange As Range, myArray As Variant
Set myRange = Range("A1:B4")
myArray = myRange.Value
Debug.Print test(myArray)
End Sub
Function test(list) As Double
test = UBound(list)
End Function
EDIT With a two dimensional range (as mentioned by KazimierzJawor) the default ubound will be the vertical, as you wanted, if you want to specify, you can add the optional perameter UBound(list, 1), but with the data you specified this defults to 4 as you wished
Ubound is a function that works on arrays you are passing it a range
try this
dim i(3)
i(0) = 1
i(1) =2
i(2) = 3
msgbox ubound(i)

#VALUE ERROR from my VBA function

So I wrote a simple function in VBA and I want to use it in my excel workbook. I wrote the following code:
Option Explicit
Public Function KOLICINA(fiksnacena As Long, ceni() As Long, nedela() As Long) As Long
Dim brojac As Integer
For brojac = 1 To UBound(nedela)
If Not ((IsEmpty(nedela(brojac) Or nedela(brojac) = 0) And ceni(brojac) <> fiksnacena)) Then KOLICINA = nedela(brojac)
Next brojac
End Function
When I try to use it in a worksheet cell (using =KOLICINA(18;G22:G26;H22:H26))
, I get the #VALUE error.
I don't understand why. The function should go through nedela Array and if it finds a Non empty or different value than 0 AND if the matching cell in the ceni Array is different from the number fiksnacena, it should return the value of the cell in nedela.
You cannot simply pass a cell range reference into a UDF and have it interpreted as a single dimensioned array of longs.
Public Function KOLICINA(fiksnacena As Long, ceni As Range, nedela As Range) As Long
Dim brojac As Long, vCeni As Variant, vNedela As Variant
vCeni = ceni.Value2
vNedela = nedela.Value2
For brojac = LBound(vNedela, 1) To UBound(vNedela, 1)
If Not ((IsEmpty(vNedela(brojac, 1) Or vNedela(brojac, 1) = 0) And vCeni(brojac, 1) <> fiksnacena)) Then
KOLICINA = vNedela(brojac, 1)
Exit For
End If
Next brojac
End Function
When you dump values from a range reference into an array, you always end up with a two dimensioned array; in your example it is 1 to 5, 1 to 1.
To further illustrate this point, your original UDF code would work if you pulled the values from the ranges after transposing them and finish off the UDF with CSE so that the values are processed as an array.
=KOLICINA(18, VALUE(TRANSPOSE(G22:G26)), VALUE(TRANSPOSE(H22:H26)))
Finalize with [ctrl]+[shift]+[enter].

VBA Filter Function for dynamic array doesn't seem to be filtering on occasion

I am writing a subroutine in VBA to cycle through all the listed job numbers in a multi-tab time sheet and create a list of all job numbers that have been used (so it takes the original list (with possibly multiple job number occurrences) and creates a list with only one occurrence of each job number. The job numbers on each sheet are found in range("A8:A30"). The code below seems to work for the first several job names on the sample that I'm testing, but then seems to stop filtering. A8:A21 of the first sheet is:
14GCI393
14GCI393
13GCI373
13GCI373
13GCI388
13GCI367:2
14GCI408
14GCI408
13GCI373
13GCI388
14GCI415
14GCI415
00GCI000
And the code is:
Sub listusedjobs()
Dim usedjobs() As String
Dim nextjob As String
Dim i, m, n, lastsheetindexnumber As Integer
Application.ScreenUpdating = False
lastsheetindexnumber = ThisWorkbook.Sheets.Count
m = 0
ReDim usedjobs(m)
usedjobs(m) = "initialize"
For i = 1 To lastsheetindexnumber
Sheets(i).Activate
For n = 8 To 30
nextjob = Range("A" & n).Value
If Not IsInArray(nextjob, usedjobs) Then 'determine if nextjob is already in usedjobs()
ReDim usedjobs(m)
usedjobs(m) = nextjob 'Add each unique job to array "usedjobs"
Sheets(lastsheetindexnumber).Cells(m + 40, 1).Value = nextjob 'Print job name that was just added
m = m + 1
End If
Next n
Next i
Application.ScreenUpdating = True
End Sub
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = (UBound(Filter(arr, stringToBeFound, , vbTextCompare)) > -1)
End Function
Any help figuring out what is going wrong will be much appreciated! The current output I get for this code is below and contains multiple doubles.
14GCI393
13GCI373
13GCI388
13GCI367:2
14GCI408
13GCI373
13GCI388
14GCI415
00GCI000
I think that your problem may be not using ReDim Preserve inside your If Not

Excel VBA - Perform Operations on visible cells only

I have a database that has in excess on 200,000 rows. When I was writing a VBA script I had a database of about 20,000 rows in mind so I didn't care whether the database was filtered or not because the VBA script ran quickly. So given the realization that the database is huge and testing the VBA script I was surprised to notice how slowly it ran. So without further to say this is how my code looks like :
Set wsDB = ThisWorkbook.Sheets("DB")
Dim nameIndex As Long: nameIndex = Application.Match(name, wsDB.Rows(1), 0)
Dim formula As String
formula = "=IFERROR(AVERAGEIFS(" + GRA(nameIndex) + "," + GRA(dateIndex) + ",R2C," + GRA(cellNameIndex) + ",RC1" + "),"""")"
where GRA is a function that returns the address of the range of a column.
Private Function GRA(ByRef rngIndex As Long)
GRA = "DB!" + CStr(Range(Cells(2, rngIndex), Cells(rowNos, rngIndex)).Address(1, 1, xlR1C1, 0, 0))
End Function
So given that I now filter the table beforehand how can I adjust my code so that it ignores all the hidden rows and takes into account only what is visible. Of course I am aware that a simple dirty solution would be to simply copy the filter database and paste it in a new sheet but that will affect the performance which is what I'm trying to improve.
You can use the following function to return a range of only visible cells.
Function VisibleCells(Rng As Range) As Variant
Dim R As Range
Dim Arr() As Integer
Dim RNdx As Long
Dim CNdx As Long
If Rng.Areas.Count > 1 Then
VisibleCells = CVErr(xlErrRef)
Exit Function
End If
ReDim Arr(1 To Rng.Rows.Count, 1 To Rng.Columns.Count)
For RNdx = 1 To Rng.Rows.Count
For CNdx = 1 To Rng.Columns.Count
Set R = Rng(RNdx, CNdx)
If (R.EntireRow.Hidden = True) Or _
(R.EntireColumn.Hidden = True) Then
Arr(RNdx, CNdx) = 0
Else
Arr(RNdx, CNdx) = 1
End If
Next CNdx
Next RNdx
VisibleCells = Arr
End Function
The above code came from http://www.cpearson.com/excel/VisibleCells.aspx.
Normally I would only post code that I write however this does exactly what I was thinking.