Finding if a string is in a 2 dimensional VBA Excel array - arrays

I have a great function that I use all of the time for a 1 dimensional Excel VBA array that checks if a string is in an array:
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = (UBound(Filter(arr(), stringToBeFound)) > -1)
End Function
Unfortunately it does not work when using it to check for a 2 dimensional array, like I have here:
Sub new_idea_filter()
home_sheet = ActiveSheet.Name
c = 1
Dim myfilters(1 To 4, 1 To 5000)
myfilters(1, 4) = "Test"
If IsInArray("Test", myfilters()) = True Then
killer = True
End If
End Sub
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = (UBound(Filter(arr(), stringToBeFound)) > -1)
End Function
It keeps erroring out in the function saying subscript out of range, anyone have a thought how I can check if a string is in the 2 dimensional array?

Something from my code collection
You can use Application.Match. This will work for both 1D and 2D array :)
See this
Sub Sample()
Dim myfilters(1 To 4, 1 To 5000)
myfilters(1, 4) = "Test"
If IsInArray("Test", myfilters()) = True Then MsgBox "Found"
End Sub
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
Dim bDimen As Byte, i As Long
On Error Resume Next
If IsError(UBound(arr, 2)) Then bDimen = 1 Else bDimen = 2
On Error GoTo 0
Select Case bDimen
Case 1
On Error Resume Next
IsInArray = Application.Match(stringToBeFound, arr, 0)
On Error GoTo 0
Case 2
For i = 1 To UBound(arr, 2)
On Error Resume Next
IsInArray = Application.Match(stringToBeFound, Application.Index(arr, , i), 0)
On Error GoTo 0
If IsInArray = True Then Exit For
Next
End Select
End Function

As long as you're in Excel (or have a reference to it), you can use the Index function to slice your array into rows or columns.
Public Function IsInArray(ByVal vToFind As Variant, vArr As Variant) As Boolean
Dim i As Long
Dim bReturn As Boolean
Dim vLine As Variant
For i = LBound(vArr, 1) To UBound(vArr, 1)
vLine = Application.WorksheetFunction.Index(vArr, i) 'slice off one line
If IsArray(vLine) Then 'if it's an array, use the filter
bReturn = UBound(Filter(vLine, vToFind)) > -1
Else 'if it's not an array, it was 1d so check the value
bReturn = vLine = vToFind
End If
If bReturn Then Exit For 'stop looking if one found
Next i
IsInArray = bReturn
End Function
Public Sub test()
Dim arr() As Variant
ReDim arr(1 To 2, 1 To 2)
arr(1, 2) = "Test"
Debug.Assert IsInArray("Test", arr)
arr(1, 2) = "Wrong"
Debug.Assert Not IsInArray("Test", arr)
ReDim arr(1 To 3)
arr(2) = "Test"
Debug.Assert IsInArray("Test", arr)
arr(2) = "Wrong"
Debug.Assert Not IsInArray("Test", arr)
Debug.Print "Passed"
End Sub

If you get the data from a recordset i use this method; first i use GetString for the recordset, second use Split to convert the string in a array unidimensional where each item is a string with all the information. After that you con use the function IsInArray.
The code is:
RecSet.Open strSQL, Cn
RecSet.MoveFirst
RecString = RecSet.GetString(, , ";", vbCr) 'genera una cadena con los datos. Campos separados por ; y registros por vbCR
RecSplit = Split(RecString, vbCr) 'Genera un array unidimensional con la cadena
you can test the code, but remember only works if you get the data from a recordset

You can try converting your original Function to be able to work with arrays. Please try the following, though note that I have not tested if it works.
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
Dim cell As Variant
For Each cell In arr
IsInArray = IsInArray Or (UBound(Filter(cell(), stringToBeFound)) > -1)
Next
End Function
Regards

#Siddharth-Rout answer above is working perfectly with Application.Match in addition to the Filter function :-). - My solution tries to use the OP Filter function only: As the filter function needs a 1dim array, the array is splitted into portions.
A) Alternative solution using the original FILTER function instead of Match plus error handling
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
Dim i As Long
If nDim(arr) = 1 Then
IsInArray = (UBound(Filter(arr(), stringToBeFound)) > -1)
Else ' allows using filter function in portions
For i = 1 To UBound(arr, 2)
If (UBound(Filter(Application.Transpose(Application.Index(arr, 0, i)), stringToBeFound)) > -1) Then IsInArray = True: Exit For
Next i
End If
End Function
Helper function to get array Dimension
Function nDim(ByVal vArray As Variant) As Long
' Purp: get number of array dimensions
' Site: http://support.microsoft.com/kb/152288
Dim dimnum As Long
Dim ErrorCheck As Variant
On Error GoTo FinalDimension
For dimnum = 1 To 60000
ErrorCheck = LBound(vArray, dimnum)
Next
FinalDimension:
nDim = dimnum - 1
End Function
B) Recursive solution using the original FILTER function instead of Match plus error handling
Function IsInArray(stringToBeFound As String, arr As Variant, Optional i As Long = 0) As Boolean
Select Case i
Case -1: ' stop 2dim calls
Case 0: IsInArray = IsInArray(stringToBeFound, arr, nDim(arr)) ' start recursive call
Case 1: IsInArray = (UBound(Filter(arr(), stringToBeFound)) > -1) ' 1dim array
Case Else ' allows using filter function in portions
If (UBound(Filter(Application.Transpose(Application.Index(arr, 0, i)), stringToBeFound)) > -1) Then
IsInArray = True
Else ' recursive calls (2dim array)
IsInArray = IsInArray(stringToBeFound, arr, IIf(i + 1 > UBound(arr), -1, i + 1))
End If
End Select
End Function

I have an Excel users version solution for this as well.
Cant you just split concatenate the array into a single column (1-d array)? you got x columns. who cares about the # of rows for now.
I would do : col 1 & "/// unique character delimiter"& col#1 & col 2 & "/// unique character delimiter"& col#2 & col 3 & "/// unique character delimiter"& col#2 & ... & & col (n-1) & "/// unique character delimiter"& col#(n-1) & & "/// unique character delimiter"& col#n
turning the 2-d array into a 1-d array.
and index match this joined-up array/column, to find the multiple occurances of the string located in the original array.
And whats good about this, because of the unique way you joined it (any unique delimator charavter + col# ) it can and will also tell you the original column each found return value of the string your looking for resided in. SO you dont loose any information.
(you can do that implementing =match ("/"&string&"/")) the position of the looked-for text in the lookup output and the next occurrence of the special unique delimiter & the next (subsequent) col # that's to the right of it.
Doesn't this do the same thing , as the macros above or the question asks for ?
and in an (almost) non-macro*/non-vba way?
*see below for why it can be done with out without macros.
So in the end, you can just turn any 2-d N.M array into an 1-d X array, while keeping all the information (of which column the text was originally belonging to) and still do a simple lookup, index-match or a LoopALL function (which is great) :
Lookupall macro to use to find and return multiple found occurrences of string:
Function LookupAll(vVal, rTable As Range, ColumnI As Long) As Variant
Dim rFound As Range, lLoop As Long
Dim strResults As String
With rTable.Columns(1)
Set rFound = .Cells(1, 1)
For lLoop = 1 To WorksheetFunction.CountIf(.Cells, vVal)
Set rFound = .Find(what:=vVal, After:=rFound, LookIn:=xlFormulas, lookAt _
:=xlWhole, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:= _
False, SearchFormat:=False)
strResults = strResults & "," & rFound(1, ColumnI)
Next lLoop
End With
LookupAll = Trim(Right(strResults, Len(strResults) - 1))
End Function
Up to you whether you use VBA lookup all function above or an index-match formula in excel which can find and return multiple occurrences of a search find.
Delimation and join of separate columns of an array strips a need for an array search (which I've never been able to do as I wanted - ie. get the results all into 1 cell), and turns it into a single and simpler 1-d array without any information loss.
I believe the speed would be as fast (and accurate) as anything else. Particularly as you've reduced/condensed the array into a single array - 1 column.
Any thoughts?

Related

Function gives Value error when returning array of arrays

I am trying to create a TextSplit function in Excel that can accept either a single reference or a range.
If it is a single string it returns an array of sub strings.
If it is a range it should return an array of sub string arrays.
A single string works but when I pass it a single column range it give me a #VALUE! error.
The commented lines work.
If I store the result of Array to arr Excel displays a grid of "test" strings.
If instead I set TextSplit to just arr(1) I get a single array of substrings similar to the single string version.
Function TextSplit(text, delimiter)
If IsArray(text) Then
Dim arr() As Variant: ReDim arr(0 To text.Count - 1)
For i = 1 To text.Count
arr(i-1) = Split(text(i), delimiter)
'arr(i-1) = Array("test", "test")
Next
TextSplit = arr
'TextSplit = arr(1)
Else
TextSplit = Split(text, delimiter)
End If
With the help of a different question Array and Split commands to create a 2 dimensional array
I was able to work your question out a bit, however I'm still unable to fill out the array from the cell where you'd call the function like with your single string which fills out in the columns next to it.
If it's for a column, you could just autofill text.split(cell,delimiter) if you're working from Excel.
If you're working from out vba and want to return the split array (2D like #Tim said) back to a sub:
Sub testingTextSplitter()
Dim arr As Variant, tArr As Variant
Dim testStr As String
testStr = Range("A1").Value 'Testing single cell
Range("G2").Value = TextSplit(testStr, "-")
arr = Range("A1:A8").Value
tArr = TextSplit(arr, "-")
For i = 0 To UBound(tArr, 1)
For j = 0 To UBound(tArr, 2)
Cells(i + 3, j + 3).Value = "'" & tArr(i, j) 'fills out from Range("C3"), adjust as needed
' This writing out is basically the same as fillingdown the formule of text.split() btw
Next j
Next i
End Sub
With the Function
Function TextSplit(tArray As Variant, delimiter As String) As String()
If IsArray(tArray) Then
Dim uBoundInput As Long, uBoundCells As Long 'I couldn't get your arr.Count to work on my end so gotta use the UBound
Dim arr() As String, testArr() As String
Dim i As Long, j As Long, maxColumns As Long
uBoundInput = UBound(tArray)
maxColumns = 0
For i = 0 To uBoundInput - 1
Debug.Print (tArray(i + 1, 1))
testArr = Split(tArray(i + 1, 1), "-")
uBoundCells = UBound(testArr)
If maxColumns < uBoundCells Then
maxColumns = uBoundCells
End If
Next i
ReDim arr(0 To uBoundInput - 1, 0 To maxColumns)
For i = 0 To uBoundInput - 1
testArr = Split(tArray(i + 1, 1), "-")
For j = 0 To UBound(testArr)
arr(i, j) = testArr(j)
Next j
Next i
TextSplit = arr()
Else
TextSplit = Split(tArray, delimiter)
End If
End Function
I'm quite new to VBA as well so apologies in advance for redundancies like not filling testArray when figuring out the maxColumns, I couldn't figure that one out. First time working with 2D arrays.
Other question that might help:
VBA UDF Return Array
(I tried using the array formulay with {} but got same Value error as before)
Hope this helps.
I don't know what happened, but the array branch of my code is now working. I have been messing with a few things, but I am not sure why it is working. The "As Variant()" declaration is new from the above code, but that may have been omitted before. (This code is on my work machine but I wrote the original post from my personal computer so I couldn't copy and paste. I am on my work computer now.)
The only other change that I made was to the index values of the arr array.
Thanks for your help, not sure what was wrong or how it got fixed though.
Function TextSplit(text, delimiter) As Variant()
If IsArray(text) Then
Dim arr() As Variant: ReDim arr(1 To text.Count)
For i = 1 To text.Count
arr(i) = Split(text(i), delimiter, -1, 1)
Next
TextSplit = arr
Else
TextSplit = Split(text, delimiter, -1, 1)
End If
End Function

Store data by using FILTER Function within VBA [duplicate]

I'm trying to make a function MonstersInLevel() that filters the second column of my "LevelMonsters" named range based on the value of the first column. The range's first column represents a game level ID and the second column represents a monster ID that appears in that level. Here's what my range looks like.
If I call MonstersInLevel(2) I expect the function to return a range consisting of "2", "3" and "4".
Function MonstersInLevel(level As Integer) As Range
MonstersInLevel = Application.WorksheetFunction.Filter(Range("LevelMonsters").Columns(2), Range("LevelMonsters").Columns(1) = level)
End Function
I get:
A value used in the formula is of the wrong data type
I'm using the FILTER function as I would as an Excel formula. I assume there's some difference in the Excel and VBA syntax for FILTER's criteria.
Just encountered this problem myself and wanted to post my workaround.
We need to return an array of True/False to the worksheet function. To do this I created a Function that takes a 2D array, the column wanted and the value to compare. It then returns a 2d single column array of the necessary True/False.
Function myeval(arr() As Variant, clm As Long, vl As Variant) As Variant()
Dim temp() As Variant
ReDim temp(1 To UBound(arr, 1), 1 To 1)
Dim i As Long
For i = 1 To UBound(arr, 1)
temp(i, 1) = arr(i, clm) = vl
Next i
myeval = temp
End Function
So in this particular case it would be called:
Function MonstersInLevel(level As Integer) As Variant
MonstersInLevel = Application.WorksheetFunction.Filter(Range("LevelMonsters").Columns(2), myeval(Range("LevelMonsters").Value, 1, level),"""")
End Function
Avoid type mismatch in Worksheetfunction via VBA
Keeping in mind that the 2nd argument reflects a dynamic matrix condition
based entirely on â–ºworksheet logic (returning an array of 0 or 1 cell values /False or True])
it seems that you have
to execute an evaluation at least within this argument and
declare the function type (explicitly or implicitly) as Variant
Function MonstersInLevel(level As Integer) As Variant
'' Failing assignment:
' MonstersInLevel = Application.WorksheetFunction.Filter(Range("LevelMonsters").Columns(2), _
' Range("LevelMonsters").Columns(1) = level _
' )
MonstersInLevel = Application.WorksheetFunction.Filter( _
Range("LevelMonsters").Columns(2), _
Evaluate(Range("LevelMonsters").Columns(1).Address & "=" & level) _
)
End Function
...or to evaluate the complete function
Function MonstersInLevel(level As Integer) As Variant
Dim expr As String
expr = "=Filter(" & _
Range("LevelMonsters").Columns(2).Address & "," & _
Range("LevelMonsters").Columns(1).Address & "=" & level & _
")"
'Debug.Print expr
MonstersInLevel = Evaluate(expr)
End Function
Example call writing results to any target
Dim v
v = MonstersInLevel(2)
Sheet1.Range("D2").Resize(UBound(v), UBound(v, 2)) = v
Of course it would be possible as well to write .Formula2 expressions programmatically, even splitting into spill range references.
Addendum ........... //as of Jan 10th
Backwards compatible workaround via VBA.Filter()
"If you guys know any other VBA function that would be more appropriate
than Application.WorksheetFunction.Filter I'd be ok."
In order to provide also a backwards compatible alternative,
I demonstrate the following approach using the classic (VBA.)Filter() function (see section [3]) based upon
prior matching results (see [1]).
Note that Application.Match() comparing two (!) array inputs
delivers a whole array of possible findings (instead of a single result as most frequently executed).
Non findings are identified by IsError() values of -1; adding +1 results in a set
of zeros and ones. Section [2] enters corresponding data for positive findings.
Eventually non-findings (i.e. 0or zero) are removed by a tricky negative filtering.
Function getLevels()
Function getLevels(rng As Range, ByVal level As Long)
'Site: https://stackoverflow.com/questions/65630126/how-to-remove-only-the-duplicate-row-instead-of-removing-all-the-rows-that-follo
'[0] get datafield array
Dim v, v2
v = Application.Index(rng.Value2, 0, 1) ' 1st column
v2 = Application.Index(rng.Value2, 0, 2) ' 2nd column
'[1] check data (with Match comparing 2 arrays :-)
Dim results
results = Application.Transpose(Application.Match(v, Array(level), 0))
'[2] rebuild with False/True entries
Dim i As Long
For i = 1 To UBound(results)
results(i) = IsError(results(i)) + 1 ' 0 or 1-values
If results(i) Then results(i) = v2(i, 1) ' get current value if true
Next i
'[3] remove zeros (negative filtering)
results = Filter(results, "0", False)
'[4] return results as vertical 1-based array
getLevels = Application.Transpose(results)
End Function
Example call
Const LVL = 2 ' define level
With Sheet1 ' change to project's sheet Code(Name)
'define data range (assuming columns A:B)
Dim rng As Range
Set rng = .UsedRange.Resize(, 2)
'function call getLevels()
Dim levels
levels = getLevels(rng, level:=LVL)
'write to target
.Columns("I:I").Clear
.Range("I2").Resize(UBound(levels), 1) = levels
End With
Solution without any supporting VBA function:
Function MonstersInLevel(level As Integer) As Variant
With Application.WorksheetFunction
MonstersInLevel = .Filter(Range("LevelMonsters").Columns(2), _
.IfError(.XLookup(Range("LevelMonsters").Columns(1), level, True), False))
End With
End Function
XLookup returns an array of #N/A or True. IfError replaces errors with False. Finally, the Filter function receives an array of booleans as the second parameter.
EDIT
Removed the IfError function thanks to #ScottCraner:
Function MonstersInLevel(level As Integer) As Variant
With Application.WorksheetFunction
MonstersInLevel = .Filter(Range("LevelMonsters").Columns(2), _
.XLookup(Range("LevelMonsters").Columns(1), level, True, False))
End With
End Function
I couldn't resolve your question but as I did some testing on the subject trying to do so, I thought I'd share my findings:
Based on this Microsoft community post, or at least the answers there, it seems you will need to loop through the output in one way or another...
That question seems to want to achieve the same as what you are wanting to do (I think?).
On the other hand, I have never used the WorksheetFunction.Filter method, and the closest I could get it to working was like so:
Here is my sample data - RangeOne is Column A and RangeTwo is Column B. I have used the =FILTER() function in cell C1 evaluating the input in D1 for reference of expected results. Naturally this function is working as expected! The VBA routine is outputting to Columns E, F and G.
Sub TestFilterFunction()
Dim TestArray As Variant
Range("E1:E3") = Application.Filter(Range("RangeTwo"), Range("RangeOne"), Range("D1"))
Range("F1:F3") = Application.Filter(Range("RangeTwo"), Range("RangeOne") = Range("D1")) 'Runtime Error 13
Range("G1:G3") = Application.Filter(Range("RangeTwo"), Range("RangeOne"))
TestArray = Application.Filter(Range("RangeTwo"), Range("RangeOne"), Range("D1"))
TestArray = Application.Filter(Range("RangeTwo"), Range("RangeOne") = Range("D1")) 'Runtime Error 13
TestArray = Application.Filter(Range("RangeTwo"), Range("RangeOne"))
Range("H1:H3") = Application.Filter(Range("RangeTwo", "RangeOne"), Range("RangeOne"), Range("D1"))
TestArray = Application.Filter(Range("A1:B9"), Range("RangeOne"), "2")
End Sub
Column E returned the first 3 values from RangeTwo.
Column F has not been populated - This is because that line threw the Runtime error 13 - Type Mismatch
Column G returned the first 3 values from RangeTwo.
Column H returned the first 3 values from "A1:B9" (both ranges together) - specifically the first 3 values of column A.
I thought this was odd so I threw in an array to assign the values to rather than directly to the worksheet;
The first TestArray line and the third TestArray line both populated the array with the entire RangeTwo values;
I realised with the syntax of the first and third attempt at the WorksheetFunction.Filter, the entire range is returned (that being the first argument - Arg1 - range), but when trying to include the = Range("D1") , it returns the Type Mismatch error.
The final TestArray attempt being the same syntax as the Column H test, returned both columns in a 2D array (now TestArray(1 To 9, 1 To 2)).
I should note I couldn't find any documentation at all on WorksheetFunction.Filter so I'm assuming it does follow the same syntax as the Excel Sheet Function has.
If I find anything more on this topic I'll come back and edit it in, but for now it's looking like perhaps a solution using either loops or Index/Match functions also will need to happen to have the data returned in VBA.
I thought about perhaps writing the sheet formula to a cell and then grab that into an array or something but Excel inserts # into it now which only returns a single cell result, i.e.
Range("J1").Formula = "=FILTER(B1:B9, A1:A9 = D1)"
Would return in J1:
=#FILTER(B1:B9, A1:A9 = D1)
Which with our sample data, would only return 2 in J1 as opposed to the expected/desired 2, 3 and 4 in J1:J3.
I can't work out a way to remove the # as it is applied when the function is written to the cell unfortunately, but hopefully any of the above helps someone find a solution.
Just some comments to help you out.
If you are using the new FILTER() function from either a worksheet cell or within some VBA code, the first argument should be a range and the second argument should a a Boolean array. (if you don't enter something that can evaluate to a Boolean array, VBA may complain the the data type is wrong)
You would be best served (in VBA) if you:
explicitly declared a 2 dimensional, column-compatible, Boolean array
filled the array
used the array in the function call
Here is a super simple example. Say we want to filter the data from A1 to A6 to remove blanks. We could pick a cell and enter:
=FILTER(A1:A6,A1:A6<>"")
Looks like:
Now we want to perform the same activities with a VBA sub and put the result in a block starting with B9. The code:
Sub SingleColumn()
Dim r As Range, wf As WorksheetFunction, i As Long
Dim arr, s As String, dq As String, boo, rc As Long
Set wf = Application.WorksheetFunction
Set r = Range("A1:A6")
rc = r.Rows.Count
ReDim boo(1 To r.Rows.Count, 1 To 1) As Boolean
i = 1
For Each rr In r
If rr.Value = "" Then
boo(i, 1) = False
Else
boo(i, 1) = True
End If
i = i + 1
Next rr
arr = wf.Filter(r, boo)
MsgBox LBound(arr, 1) & "-" & UBound(arr, 1) & vbCrLf & LBound(arr, 2) & "-" & UBound(arr, 2)
Range("B9").Resize(UBound(arr, 1), UBound(arr, 2)) = arr
End Sub
Result:
On Excel version 15.0 (2013), I don't see Application.WorksheetFunction.Filter (tried with Show Hidden Members):
So maybe this is a newer function in later versions ?
My top Google search directs me to this question ;)
So, my answer is to avoid the function primarily from the point of view of backwards compatibility.
Alternate code options presented below returning e.g. a Range and a Variant.
Input:
Code:
Option Explicit
Sub Test()
Dim rngInput As Range
Dim rngFiltered As Range
Dim varFiltered As Variant
Dim varItem As Variant
Set rngInput = ThisWorkbook.Worksheets("Sheet1").Range("A2:B10")
' as range
Debug.Print "' Output as Range"
Set rngFiltered = MonstersInLevel_AsRange(rngInput, 2, 1, 2)
Debug.Print "' " & rngFiltered.Address ' expect B5, B6, B8
Debug.Print "' ---------------"
' as variant
Debug.Print "' Output as Variant"
varFiltered = MonstersInLevel_AsVariant(rngInput, 2, 1, 2)
For Each varItem In varFiltered
Debug.Print "' " & varItem ' expect 3, 4, 5
Next varItem
Debug.Print "' ---------------"
End Sub
Function MonstersInLevel_AsRange(rngToFilter As Range, _
ByVal lngLevel As Long, _
ByVal lngColIxToFilter As Long, _
ByVal lngColIxForValue As Long) As Range
Dim rngResult As Range
Dim lngRowIndex As Long
Dim lngResultIndex As Long
Set rngResult = Nothing
For lngRowIndex = 1 To rngToFilter.Rows.Count
If rngToFilter.Cells(lngRowIndex, lngColIxToFilter) = lngLevel Then
If rngResult Is Nothing Then
Set rngResult = rngToFilter.Cells(lngRowIndex, lngColIxForValue)
Else
Set rngResult = Union(rngResult, rngToFilter.Cells(lngRowIndex, lngColIxForValue))
End If
End If
Next lngRowIndex
Set MonstersInLevel_AsRange = rngResult
End Function
Function MonstersInLevel_AsVariant(rngToFilter As Range, _
ByVal lngLevel As Long, _
ByVal lngColIxToFilter As Long, _
ByVal lngColIxForValue As Long) As Variant
Dim varResult As Variant
Dim lngRowIndex As Long
Dim lngResultIndex As Long
lngResultIndex = 0
ReDim varResult(0)
For lngRowIndex = 1 To rngToFilter.Rows.Count
If rngToFilter.Cells(lngRowIndex, lngColIxToFilter) = lngLevel Then
lngResultIndex = lngResultIndex + 1
ReDim Preserve varResult(1 To lngResultIndex)
varResult(lngResultIndex) = rngToFilter.Cells(lngRowIndex, lngColIxForValue)
End If
Next lngRowIndex
MonstersInLevel_AsVariant = varResult
End Function
Test output:
' Output as Range
' $B$5:$B$6,$B$8
' ---------------
' Output as Variant
' 3
' 5
' 4
' ---------------
Based on Christian Buses answer (https://stackoverflow.com/a/65671334/16578424) I wrote a generic function to use the FILTER-function.
It returns a one-dimensional array with the filtered values.
Public Function getFILTERValuesFromRange(rgResult As Range, rgFilter As Range, varValue As Variant) As Variant
If rgResult.Columns.count > 1 Or rgFilter.Columns.count > 1 Then
Err.Raise vbObjectError + 512, , "Only ranges with one column are allowed."
ElseIf rgResult.Rows.count <> rgFilter.Rows.count Then
Err.Raise vbObjectError + 512, , "Both ranges have to be of the same size."
End If
Dim arr1 As Variant
With Application.WorksheetFunction
arr1 = .filter(rgResult, .XLookup(rgFilter, varValue, True, False))
End With
getFILTERValuesFromRange = getOneDimensionalArrayFromRangeArray(arr1)
End Function
Private Function getOneDimensionalArrayFromRangeArray(arr1 As Variant) As Variant
Dim arr2 As Variant
ReDim arr2(LBound(arr1, 1) To UBound(arr1, 1))
Dim i As Long
For i = 1 To UBound(arr1, 1)
arr2(i) = arr1(i, 1)
Next
getOneDimensionalArrayFromRangeArray = arr2
End Function

VBA check if whole row of multidimensional variant is empty without loops

Is there a quick way to check whether a whole row of a variant is empty?
My multi-dimensional array / variant has n-rows and m-columns.
The only way I can think of is to loop through the columns (of a specific row) and use the IsEmpty() function to determine if a cell is empty.
The variant only consists strings.
Do you know a faster way? Maybe something like this pseudo-code: IsEmpty(myarr(1,*))
this pseudocode would mean to check the all columns of the first row if they are empty.
You could try something like:
Sub Test()
Dim myarr() As Variant, indx As Long
myarr = Range("A8:C20").Value 'Or however you initialize your array.
indx = 1 'Or whichever row you would want to check.
With Application
Debug.Print Join(.Index(myarr, indx, 0), "") <> ""
End With
End Sub
Not sure if it will be faster than a loop though, since we call a worksheet application.
No, there isn't a faster way especially considering that arrays in VBA are stored column-wise in memory. The values on a single row are not stored adjacent in memory as it's the case with column values - you could easily test this by running a For Each loop on an array.
That being said, you should probably consider having a Function that checks if a specific row is empty so that you can call it repeatedly and maybe also check for null strings if needed. For example a range of formulas returning "" will not be empty but you might want to have the ability to consider them empty.
For example, you could use something like this:
Public Function Is2DArrayRowEmpty(ByRef arr As Variant _
, ByVal rowIndex As Long _
, Optional ByVal ignoreEmptyStrings As Boolean = False _
) As Boolean
Const methodName As String = "Is2DArrayRowEmpty"
'
If GetArrayDimsCount(arr) <> 2 Then
Err.Raise 5, methodName, "Array is not two-dimensional"
ElseIf rowIndex < LBound(arr, 1) Or rowIndex > UBound(arr, 1) Then
Err.Raise 5, methodName, "Row Index out of bounds"
End If
'
Dim j As Long
Dim v As Variant
'
For j = LBound(arr, 2) To UBound(arr, 2)
v = arr(rowIndex, j)
Select Case VBA.VarType(v)
Case VbVarType.vbEmpty
'Continue to next element
Case VbVarType.vbString
If Not ignoreEmptyStrings Then Exit Function
If LenB(v) > 0 Then Exit Function
Case Else
Exit Function
End Select
Next j
'
Is2DArrayRowEmpty = True 'If code reached this line then row is Empty
End Function
Public Function GetArrayDimsCount(ByRef arr As Variant) As Long
If Not IsArray(arr) Then Exit Function
'
Const MAX_DIMENSION As Long = 60
Dim dimension As Long
Dim tempBound As Long
'
'A zero-length array has 1 dimension! Ex. Array() returns (0 to -1)
On Error GoTo FinalDimension
For dimension = 1 To MAX_DIMENSION
tempBound = LBound(arr, dimension)
Next dimension
Exit Function
FinalDimension:
GetArrayDimsCount = dimension - 1
End Function
Notice that I haven't checked for IsObject as your values are coming from a range in Excel but you would normally check for that in a general case.
Your pseudocode IsEmpty(myarr(1,*)) could be translated to:
Is2DArrayRowEmpty(myarr, 1, False) 'Empty strings would not be considered Empty
or
Is2DArrayRowEmpty(myarr, 1, True) 'Empty strings would be considered Empty

VBA - StrComp type mismatch or subscript out of range

I am running a For Loop to find a string inside a variant array. I am using StrComp to compare the strings. The array that I am using as the following format.
I have tried two approaches which I will describe bellow.
1st approach returns that the subscript is out of range when I use col=0
Function IsInArray(stringToBeFound As String, arr As Variant, col As Integer) As Long
Dim i As Long
' default return value if value not found in array
IsInArray = -1
For i = LBound(arr) To UBound(arr)
If StrComp(stringToBeFound, arr(i, col), vbTextCompare) = 0 Then
IsInArray = i
Exit For
End If
Next i
End Function
2nd approach tells me type mismatch at the StrComp
Function IsIn1DArray(stringToBeFound As String, arr As Variant) As Long
Dim i As Long
' default return value if value not found in array
IsIn1DArray = -1
For i = LBound(arr) To UBound(arr)
If StrComp(stringToBeFound, arr(i)) = 0 Then
IsIn1DArray = i
Exit For
End If
Next i
End Function
I have been using the IsInArray and IsIn1DArray in the past and works but not for this case. Imagine I would like to search for the string "[TestHeader]" and return its index. How would you do it?
arr is an array of arrays, so you need:
If StrComp(stringToBeFound, arr(i)(col), vbTextCompare) = 0 Then
Note this may fail for some values as your sub arrays are not the same size, so you should test the Ubound of the subarray first.
Function IsIn1dArray(stringToBeFound As String, arr As Variant) As Long
IsIn1dArray = Application.WorksheetFunction.Match(stringToBeFound, arr, 0)
End Function

How to search for string in an array

Is there an easy (one-liner) to search for a string within an array in VBA? Or will I need to loop through each element and compare it with the target string?
EDIT:
It is a one-dimensional array. I only need to know IF a string is somewhere in the array.
IE:
names(JOHN, BOB, JAMES, PHLLIP)
How do I find out if "JOHN" is in the array, it needs to be minimal as it will be repeated around 5000 times and I don't want the function to slow the overall process down.
If you want to know if the string is found in the array at all, try this function:
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = (UBound(Filter(arr, stringToBeFound)) > -1)
End Function
As SeanC points out, this must be a 1-D array.
Example:
Sub Test()
Dim arr As Variant
arr = Split("abc,def,ghi,jkl", ",")
Debug.Print IsInArray("ghi", arr)
End Sub
(Below code updated based on comment from HansUp)
If you want the index of the matching element in the array, try this:
Function IsInArray(stringToBeFound As String, arr As Variant) As Long
Dim i As Long
' default return value if value not found in array
IsInArray = -1
For i = LBound(arr) To UBound(arr)
If StrComp(stringToBeFound, arr(i), vbTextCompare) = 0 Then
IsInArray = i
Exit For
End If
Next i
End Function
This also assumes a 1-D array. Keep in mind LBound and UBound are zero-based so an index of 2 means the third element, not the second.
Example:
Sub Test()
Dim arr As Variant
arr = Split("abc,def,ghi,jkl", ",")
Debug.Print (IsInArray("ghi", arr) > -1)
End Sub
If you have a specific example in mind, please update your question with it, otherwise example code might not apply to your situation.
Another option would be use a dictionary instead of an array:
Dim oNames As Object
Set oNames = CreateObject("Scripting.Dictionary")
'You could if need be create this automatically from an existing Array
'The 1 is just a dummy value, we just want the names as keys
oNames.Add "JOHN", 1
oNames.Add "BOB", 1
oNames.Add "JAMES", 1
oNames.Add "PHILIP", 1
As this would then get you a one-liner of
oNames.Exists("JOHN")
The advantage a dictionary provides is exact matching over partial matching from Filter. Say if you have the original list of names in an Array, but were looking for "JO" or "PHIL" who were actually two new people in addition to the four we started with. In this case, Filter(oNAMES, "JO") will match "JOHN" which may not be desired. With a dictionary, it won't.
Another option that enforces exact matching (i.e. no partial matching) would be:
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = Not IsError(Application.Match(stringToBeFound, arr, 0))
End Function
You can read more about the Match method and its arguments at
http://msdn.microsoft.com/en-us/library/office/ff835873(v=office.15).aspx
there is a function that will return an array of all the strings found.
Filter(sourcearray, match[, include[, compare]])
The sourcearray has to be 1 dimensional
The function will return all strings in the array that have the match string in them
Here's another answer. It works fast, reliably (see atomicules' answer) and has compact calling code:
' Returns true if item is in the array; false otherwise.
Function IsInArray(ar, item$) As Boolean
Dim delimiter$, list$
' Chr(7) is the ASCII 'Bell' Character.
' It was chosen for being unlikely to be found in a normal array.
delimiter = Chr(7)
' Create a list string containing all the items in the array separated by the delimiter.
list = delimiter & Join(ar, delimiter) & delimiter
IsInArray = InStr(list, delimiter & item & delimiter) > 0
End Function
Sample usage:
Sub test()
Debug.Print "Is 'A' in the list?", IsInArray(Split("A,B", ","), "A")
End Sub
more simple Function whichs works on Apple OS too:
Function isInArray(ByVal stringToBeFound As String, ByVal arr As Variant) As Boolean
Dim element
For Each element In arr
If element = stringToBeFound Then
isInArray = True
Exit Function
End If
Next element
End Function
This is my code, inspired by #atomicules
Public Function IsName(name As String) As Boolean
Dim names As Object
Set names = CreateObject("System.Collections.ArrayList")
names.Add "JOHN"
names.Add "BOB"
names.Add "JAMES"
names.Add "PHLLIP"
IsName = names.Contains(name)
End Function
And this is usage:
If IsName("JOHN") Then ...
You could use the following without the wrapper function, but it provides a nicer API:
Function IsInArray(ByVal findString as String, ByVal arrayToSearch as Variant) as Boolean
IsInArray = UBound(Filter(arrayToSearch,findString)) >= 0
End Function
The Filter function has the following signature:
Filter(sourceArray, stringToMatch, [Include As Boolean = True], [Compare as VbCompareMethod = vbBinaryCompare])
If it's a list of constants then you can use Select Case as follows:
Dim Item$: Item = "A"
Select Case Item
Case "A", "B", "C"
' If 'Item' is in the list then do something.
Case Else
' Otherwise do something else.
End Select
Completing remark to Jimmy Pena's accepted answer
As SeanC points out, this must be a 1-D array.
The following example call demonstrates that the IsInArray() function cannot be called only for 1-dim arrays,
but also for "flat" 2-dim arrays:
Sub TestIsInArray()
Const SearchItem As String = "ghi"
Debug.Print "SearchItem = '" & SearchItem & "'"
'----
'a) Test 1-dim array
Dim Arr As Variant
Arr = Split("abc,def,ghi,jkl", ",")
Debug.Print "a) 1-dim array " & vbNewLine & " " & Join(Arr, "|") & " ~~> " & IsInArray(SearchItem, Arr)
'----
'//quick tool to create a 2-dim 1-based array
Dim v As Variant, vals As Variant
v = Array(Array("abc", "def", "dummy", "jkl", 5), _
Array("mno", "pqr", "stu", "ghi", "vwx"))
v = Application.Index(v, 0, 0) ' create 2-dim array (2 rows, 5 cols)
'b) Test "flat" 2-dim arrays
Debug.Print "b) ""flat"" 2-dim arrays "
Dim i As Long
For i = LBound(v) To UBound(v)
'slice "flat" 2-dim arrays of one row each
vals = Application.Index(v, i, 0)
'check for findings
Debug.Print Format(i, " 0"), Join(vals, "|") & " ~~> " & IsInArray(SearchItem, vals)
Next i
End Sub
Function IsInArray(stringToBeFound As String, Arr As Variant) As Boolean
'Site: https://stackoverflow.com/questions/10951687/how-to-search-for-string-in-an-array/10952705
'Note: needs a "flat" array, not necessarily a 1-dimensioned array
IsInArray = (UBound(Filter(Arr, stringToBeFound)) > -1)
End Function
Results in VB Editor's immediate window
SearchItem = 'ghi'
a) 1-dim array
abc|def|ghi|jkl ~~> True
b) "flat" 2-dim arrays
1 abc|def|dummy|jkl|5 False
2 mno|pqr|stu|ghi|vwx True
A Case statement might suit some applications more simply:
select case var
case "a string", "another string", sVar
'do something
case else
'do something else
end select

Resources