In VBA, how to check if two arrays contain the same element? - vba

I suspect I might need a function for that. I would like to return a Boolean. E.g., firstarray = ("haha", "ya", "test") and secondarray = ("haha", "no", "stop"), then return True. Thanks

Are There Any Matching Elements in Two Arrays? (One-Liner)
A safer yet less efficient way would probably be to loop through each element in one array and, in another (inner) loop, compare it to each element of the other array exiting the loops when a match is found.
Option Explicit
Sub Test()
Dim FirstArray() As Variant
FirstArray = Array("A", "ya", CVErr(xlErrNA), "test")
Dim SecondArray() As Variant
SecondArray = Array("sa", "aha", CVErr(xlErrNA), "stop")
If FoundMatchingElement(FirstArray, SecondArray) Then
Debug.Print "True"
Else
Debug.Print "False"
End If
' Result:
' False ' ... since error values are excluded from the comparison
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Returns a boolean indicating whether two arrays ('Arr1', 'Arr2')
' have a same element.
' Remarks: 'Application.Match' is case-insensitive i.e. 'A = a'.
' Error values will not be considered.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function FoundMatchingElement( _
Arr1() As Variant, _
Arr2() As Variant) _
As Boolean
FoundMatchingElement _
= Application.Count(Application.Match(Arr1, Arr2, 0)) > 0
End Function

Please, use the next function. It does what you want without any iteration. It returns True if any of their elements are common to both of them:
Function MatchArrayElement(arr1, arr2) As Boolean
Dim arrMtch: arrMtch = Application.IfError(Application.match(arr1, arr2, 0), "x") 'place "x" instead of error (not matching elements)
MatchArrayElement = UBound(filter(arrMtch, "x", False)) > -1
End Function
It can be used in the next way:
Sub testMatchArrEl()
Dim arr1, arr2
arr1 = Array("hah", "ya", "test")
arr2 = Array("haha", "no", "stop")
Debug.Print MatchArrayElement(arr1, arr2)
End Sub
But if you need checking if a specific element/string exists in an array, you can use the following function:
Function arrElementMatch(El, arr) As Boolean
Dim mtch: mtch = Application.match(El, arr, 0)
arrElementMatch = Not IsError(mtch)
End Function
It can be checked in the next way:
Debug.Print arrElementMatch("haha", arr1), arrElementMatch("hah", arr1)
To check if the element exists in both array, try using:
Debug.Print arrElementMatch("haha", arr1) And arrElementMatch("haha", arr2)
Please, send some feedback after using it.
If something not clear enough, do not hesitate to ask for clarifications...

Related

Find anything but a number or "C"

I have this formula (below) where I am trying to find a space in C1. Instead of this, I would like to update this formula to look for anything except for "C" or any number and not only find a space.
LEFT(C1, find("" "", C1, 1)-1)
For e.g.
if C1 has - "C1234 - XXX" or "C1234-XXX" or "C1234:XXX", I always want the above function to find anything except for "C" and "1234" (i.e. numbers).
P.S.: I would want to use the find function only with improvements to meet the above conditions.
Please suggest.
Perhaps this:
'To create a new string from a source string which will or will not contain the characters present within the source string
'Examples of string of characters: 0123456789 -OR- {}[]<>\/|+*=-_(),.:;?!##$%^&™®©~'" OR - combination of various characters
Public Function getNewStringFromString(ByVal strSource As Variant, ByVal strChars As Variant, Optional isInString As Boolean = True) As String
Dim strArr As Variant, iChar As Variant
getNewStringFromString = ""
If VarType(strSource) = vbString And VarType(strChars) = vbString Then
strSource = Trim(strSource): strChars = Trim(strChars)
If Len(strSource) > 0 And Len(strChars) > 0 Then
strArr = Split(StrConv(strSource, vbUnicode), vbNullChar)
For Each iChar In strArr
If (isInString Xor isInArray(iChar, strChars)) = False Then getNewStringFromString = getNewStringFromString + iChar
Next iChar
Erase strArr
End If
End If
End Function
Use as the following:
MsgBox getNewStringFromString(CStr(Range("C1")), "C0123456789")
Forgot to give you the code for the isInArray function. Here it is:
'To check if an element is within a specific Array, Object, Range, String, etc.
Public Function isInArray(ByVal itemSearched As Variant, ByVal aArray As Variant) As Boolean
Dim item As Variant
If VarType(aArray) >= vbArray Or VarType(aArray) = vbObject Or VarType(aArray) = vbDataObject Or TypeName(aArray) = "Range" Then
For Each item In aArray
If itemSearched = item Then
isInArray = True
Exit Function
End If
Next item
isInArray = False
ElseIf VarType(aArray) = vbString Then
isInArray = InStr(1, aArray, itemSearched, vbBinaryCompare) > 0 'Comparing character by character
Else
On Error Resume Next
isInArray = Not IsError(Application.Match(itemSearched, aArray, False))
Err.Clear: On Error GoTo 0
End If
End Function
Given your data format, where
C is always the first character
subsequent values are all digits
You want to return the C followed by the digits
Try:
="C" & LOOKUP(9E+307,VALUE(MID(A1,2,{1,2,3,4,5,6,7})))
If there might be more than 7 digits, you can either extend the array constant, or use a formula to create a larger array.
The formula looks for the largest integer in the string, starting with position 2. So it will stop at the last non-digit, since anything including a non-digit will return an error.
If the "non-digit" might be your decimal or thousands separator, you will need to replace it with something else, with a nested SUBSTITUTE
Replace . , and space with -
=LOOKUP(1E+307,--SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(MID(A1,2,{1,2,3,4,5,6,7}),",","-"),".","-"),".","-"))
For a VBA solution, I would use regular expressions.
Option Explicit
Function getCnum(str As String)
Dim RE As Object
Const sPat As String = "(^C\d+).*"
Set RE = CreateObject("vbscript.regexp")
With RE
.Global = False
.MultiLine = True
.ignorecase = True
.Pattern = sPat
getCnum = .Replace(str, "$1")
End With
End Function
Note that this also validates the string by checking that the first letter is, in fact, a C (or c). If you want it to be case-sensitive, make the obvious change.

UBound Array Definition

Having trouble understanding how to use UBound.
If I have a dynamic column A where the rows count is constantly changing, but I want to grab the upper bound to define in a for loop - how would I go about doing so? I keep getting a runtime error with the below method.
arr = Worksheets("Sheet1").Range("A1").CurrentRegion.Value
For i = 1 to UBound(arr, 1)
etc.
Next i
When setting an array equal to a range value, it automatically sets it as a two dimensional array, even if the range is only one column. Look at the example below:
Private Sub workingWithArrayBounds()
Dim Arr As Variant
Dim RowIndex As Long
Dim ColumnIndex As Long
Arr = Worksheets("Sheet1").Range("A1").CurrentRegion.Value
'VBA FUNCTION FOR CHECKING IF SOMETHING IS AN ARRAY
If IsArray(Arr) Then
'FIRST LOOP GOES THROUGH THE ROWS
For RowIndex = LBound(Arr, 1) To UBound(Arr, 1)
'THE SECOND LOOP GOES THROUGH THE COLUMNS
For ColumnIndex = LBound(Arr, 2) To UBound(Arr, 2)
'NOW YOU HAVE ACCESS TO EACH ELEMENT USING THE ROWINDEX AND THE COLUMN INDEX
Debug.Print Arr(RowIndex, ColumnIndex)
Next
Next
End If
End Sub
If there is only one column of data, the bounds would look something like (1 to x, 1 to 1). So you would need to pass in the second bounds to it. To be safe, it is always recommended to use both the LBound and UBound when looping through arrays as these can vary from array to array.
Another thing to check is if the array is actually allocated. In the scenario of setting an array equal to the value of a range, you can first check if there are values actually in your range to retrieve.
An alternative is to use a function to check and see if array is empty or not. Cpearson has a good function for that as well as many other helpful functions # http://www.cpearson.com/excel/vbaarrays.htm
Public Function IsArrayEmpty(Arr As Variant) As Boolean
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' IsArrayEmpty
' This function tests whether the array is empty (unallocated). Returns TRUE or FALSE.
'
' The VBA IsArray function indicates whether a variable is an array, but it does not
' distinguish between allocated and unallocated arrays. It will return TRUE for both
' allocated and unallocated arrays. This function tests whether the array has actually
' been allocated.
'
' This function is really the reverse of IsArrayAllocated.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Dim LB As Long
Dim UB As Long
Err.Clear
On Error Resume Next
If IsArray(Arr) = False Then
' we weren't passed an array, return True
IsArrayEmpty = True
End If
' Attempt to get the UBound of the array. If the array is
' unallocated, an error will occur.
UB = UBound(Arr, 1)
If (Err.Number <> 0) Then
IsArrayEmpty = True
Else
''''''''''''''''''''''''''''''''''''''''''
' On rare occassion, under circumstances I
' cannot reliably replictate, Err.Number
' will be 0 for an unallocated, empty array.
' On these occassions, LBound is 0 and
' UBoung is -1.
' To accomodate the weird behavior, test to
' see if LB > UB. If so, the array is not
' allocated.
''''''''''''''''''''''''''''''''''''''''''
Err.Clear
LB = LBound(Arr)
If LB > UB Then
IsArrayEmpty = True
Else
IsArrayEmpty = False
End If
End If
End Function

function to use an array

I am trying to write a function that uses the upper and lower bound of an array and returns the number of elements. It is confusing because of the lack of experience I have with functions but nonetheless I have come up with this code below:
Function numelement(LBound(array1), UBound(array2)) as Integer
numelement = UBound(array1) - LBound(array2)
End Function
Is this code correct, I know its simple but thats what the purpose of this code is to be.
If you realy want to have a function (you can do it easily without it), then try the code below:
Function numelement(array1 As Variant) As Long
numelement = (UBound(array1) - LBound(array1)) + 1 '<-- since array iz zero based
End Function
Sub Code to test the Function:
Sub TestFunc()
Dim Arr As Variant
Arr = Array("Shai", "Rado", "Tel-Aviv")
MsgBox numelement(Arr)
End Sub
Note: instead of the function, you can use:
Dim array1 As Variant
array1 = Array("Shai", "Rado", "Tel-Aviv")
MsgBox "number of elements in array are " & UBound(array1 ) + 1
You don't need a function for this. Ubound +1 works nicely for the total number of elements, if you initalize array like this Array(). It is a built-it function in VBA, it works great:
Option Explicit
Sub TestMe()
Dim myArr As Variant
myArr = Array("A", "B", "C")
Debug.Print LBound(myArr)
Debug.Print UBound(myArr)
End Sub
Unless you decide to initialize an array like this:
Dim arr(3 To 4)
arr(3) = 1
arr(4) = 2
Then you should use the following:
debug.print UBound(myArr)-LBound(myArr)+1

Excel VBA Custom Function Remove Words Appearing in One String From Another String

I am trying to remove words appearing in one string from a different string using a custom function. For instance:
A1:
the was why blue hat
A2:
the stranger wanted to know why his blue hat was turning orange
The ideal outcome in this example would be:
A3:
stranger wanted to know his turning orange
I need to have the cells in reference open to change so that they can be used in different situations.
The function will be used in a cell as:
=WORDREMOVE("cell with words needing remove", "cell with list of words being removed")
I have a list of 20,000 rows and have managed to find a custom function that can remove duplicate words (below) and thought there may be a way to manipulate it to accomplish this task.
Function REMOVEDUPEWORDS(txt As String, Optional delim As String = " ") As String
Dim x
'Updateby20140924
With CreateObject("Scripting.Dictionary")
.CompareMode = vbTextCompare
For Each x In Split(txt, delim)
If Trim(x) <> "" And Not .exists(Trim(x)) Then .Add Trim(x), Nothing
Next
If .Count > 0 Then REMOVEDUPEWORDS = Join(.keys, delim)
End With
End Function
If you can guarantee that your words in both strings will be separated by spaces (no comma, ellipses, etc), you could just Split() both strings then Filter() out the words:
Function WORDREMOVE(ByVal strText As String, strRemove As String) As String
Dim a, w
a = Split(strText) ' Start with all words in an array
For Each w In Split(strRemove)
a = Filter(a, w, False, vbTextCompare) ' Remove every word found
Next
WORDREMOVE = Join(a, " ") ' Recreate the string
End Function
You can also do this using Regular Expressions in VBA. The version below is case insensitive and assumes all words are separated only by space. If there is other punctuation, more examples would aid in crafting an appropriate solution:
Option Explicit
Function WordRemove(Str As String, RemoveWords As String) As String
Dim RE As Object
Set RE = CreateObject("vbscript.regexp")
With RE
.ignorecase = True
.Global = True
.Pattern = "(?:" & Join(Split(WorksheetFunction.Trim(RemoveWords)), "|") & ")\s*"
WordRemove = .Replace(Str, "")
End With
End Function
My example is certainly not the best code, but it should work
Function WORDREMOVE(FirstCell As String, SecondCell As String)
Dim FirstArgument As Variant, SecondArgument As Variant
Dim FirstArgumentCounter As Integer, SecondArgumentCounter As Integer
Dim Checker As Boolean
WORDREMOVE = ""
FirstArgument = Split(FirstCell, " ")
SecondArgument = Split(SecondCell, " ")
For SecondArgumentCounter = 0 To UBound(SecondArgument)
Checker = False
For FirstArgumentCounter = 0 To UBound(FirstArgument)
If SecondArgument(SecondArgumentCounter) = FirstArgument(FirstArgumentCounter) Then
Checker = True
End If
Next FirstArgumentCounter
If Checker = False Then WORDREMOVE = WORDREMOVE & SecondArgument(SecondArgumentCounter) & " "
Next SecondArgumentCounter
WORDREMOVE = Left(WORDREMOVE, Len(WORDREMOVE) - 1)
End Function

VBA string interpolation syntax

What is the VBA string interpolation syntax? Does it exist?
I would to to use Excel VBA to format a string.
I have a variable foo that I want to put in a string for a range.
Dim row as Long
row = 1
myString = "$row:$row"
I would like the $row in the string to be interpolated as "1"
You could also build a custom Format function.
Public Function Format(ParamArray arr() As Variant) As String
Dim i As Long
Dim temp As String
temp = CStr(arr(0))
For i = 1 To UBound(arr)
temp = Replace(temp, "{" & i - 1 & "}", CStr(arr(i)))
Next
Format = temp
End Function
The usage is similar to C# except that you can't directly reference variables in the string. E.g. Format("This will {not} work") but Format("This {0} work", "will").
Public Sub Test()
Dim s As String
s = "Hello"
Debug.Print Format("{0}, {1}!", s, "World")
End Sub
Prints out Hello, World! to the Immediate Window.
This works well enough, I believe.
Dim row as Long
Dim s as String
row = 1
s = "$" & row & ":$" & row
Unless you want something similar to Python's or C#'s {} notation, this is the standard way of doing it.
Using Key\Value Pairs
Another alternative to mimic String interpolation is to pass in key\value pairs as a ParamArray and replace the keys accordingly.
One note is that an error should be raised if there are not an even number of elements.
' Returns a string that replaced special keys with its associated pair value.
Public Function Inject(ByVal source As String, ParamArray keyValuePairs() As Variant) As String
If (UBound(keyValuePairs) - LBound(keyValuePairs) + 1) Mod 2 <> 0 Then
Err.Raise 5, "Inject", "Invalid parameters: expecting key/value pairs, but received an odd number of arguments."
End If
Inject = source
' Replace {key} with the pairing value.
Dim index As Long
For index = LBound(keyValuePairs) To UBound(keyValuePairs) Step 2
Inject = Replace(Inject, "{" & keyValuePairs(index) & "}", keyValuePairs(index + 1), , , vbTextCompare)
Next index
End Function
Simple Example
Here is a simple example that shows how to implement it.
Private Sub testingInject()
Const name As String = "Robert"
Const age As String = 31
Debug.Print Inject("Hello, {name}! You are {age} years old!", "name", name, "age", age)
'~> Hello, Robert! You are 31 years old!
End Sub
Although this may add a few extra strings, in my opinion, this makes it much easier to read long strings.
See the same simple example using concatenation:
Debug.Print "Hello, " & name & "! You are " & age & " years old!"
Using Scripting.Dicitionary
Really, a Scripting.Dictionary would be perfect for this since they are nothing but key/value pairs. It would be a simple adjustment to my code above, just take in a Dictionary as the parameter and make sure the keys match.
Public Function Inject(ByVal source As String, ByVal data As Scripting.Dictionary) As String
Inject = source
Dim key As Variant
For Each key In data.Keys
Inject = Replace(Inject, "{" & key & "}", data(key))
Next key
End Function
Dictionary example
And the example of using it for dictionaries:
Private Sub testingInject()
Dim person As New Scripting.Dictionary
person("name") = "Robert"
person("age") = 31
Debug.Print Inject("Hello, {name}! You are {age} years old!", person)
'~> Hello, Robert! You are 31 years old!
End Sub
Additional Considerations
Collections sound like they would be nice as well, but there is no way of accessing the keys. It would probably get messier that way.
If using the Dictionary method you might create a simple factory function for easily creating Dictionaries. You can find an example of that on my Github Library Page.
To mimic function overloading to give you all the different ways you could create a main Inject function and run a select statement within that.
Here is all the code needed to do that if need be:
Public Function Inject(ByVal source As String, ParamArray data() As Variant) As String
Dim firstElement As Variant
assign firstElement, data(LBound(data))
Inject = InjectCharacters(source)
Select Case True
Case TypeName(firstElement) = "Dictionary"
Inject = InjectDictionary(Inject, firstElement)
Case InStr(source, "{0}") > 0
Inject = injectIndexes(Inject, CVar(data))
Case (UBound(data) - LBound(data) + 1) Mod 2 = 0
Inject = InjectKeyValuePairs(Inject, CVar(data))
Case Else
Err.Raise 5, "Inject", "Invalid parameters: expecting key/value pairs or Dictionary or an {0} element."
End Select
End Function
Private Function injectIndexes(ByVal source As String, ByVal data As Variant)
injectIndexes = source
Dim index As Long
For index = LBound(data) To UBound(data)
injectIndexes = Replace(injectIndexes, "{" & index & "}", data(index))
Next index
End Function
Private Function InjectKeyValuePairs(ByVal source As String, ByVal keyValuePairs As Variant)
InjectKeyValuePairs = source
Dim index As Long
For index = LBound(keyValuePairs) To UBound(keyValuePairs) Step 2
InjectKeyValuePairs = Replace(InjectKeyValuePairs, "{" & keyValuePairs(index) & "}", keyValuePairs(index + 1))
Next index
End Function
Private Function InjectDictionary(ByVal source As String, ByVal data As Scripting.Dictionary) As String
InjectDictionary = source
Dim key As Variant
For Each key In data.Keys
InjectDictionary = Replace(InjectDictionary, "{" & key & "}", data(key))
Next key
End Function
' QUICK TOOL TO EITHER SET OR LET DEPENDING ON IF ELEMENT IS AN OBJECT
Private Function assign(ByRef variable As Variant, ByVal value As Variant)
If IsObject(value) Then
Set variable = value
Else
Let variable = value
End If
End Function
End Function
Private Function InjectCharacters(ByVal source As String) As String
InjectCharacters = source
Dim keyValuePairs As Variant
keyValuePairs = Array("n", vbNewLine, "t", vbTab, "r", vbCr, "f", vbLf)
If (UBound(keyValuePairs) - LBound(keyValuePairs) + 1) Mod 2 <> 0 Then
Err.Raise 5, "Inject", "Invalid variable: expecting key/value pairs, but received an odd number of arguments."
End If
Dim RegEx As Object
Set RegEx = CreateObject("VBScript.RegExp")
RegEx.Global = True
' Replace is ran twice since it is possible for back to back patterns.
Dim index As Long
For index = LBound(keyValuePairs) To UBound(keyValuePairs) Step 2
RegEx.Pattern = "((?:^|[^\\])(?:\\{2})*)(?:\\" & keyValuePairs(index) & ")+"
InjectCharacters = RegEx.Replace(InjectCharacters, "$1" & keyValuePairs(index + 1))
InjectCharacters = RegEx.Replace(InjectCharacters, "$1" & keyValuePairs(index + 1))
Next index
End Function
I have a library function SPrintF() which should do what you need.
It replaces occurrences of %s in the supplied string with an extensible number of parameters, using VBA's ParamArray() feature.
Usage:
SPrintF("%s:%s", 1, 1) => "1:1"
SPrintF("Property %s added at %s on %s", "88 High St, Clapham", Time, Date) => ""Property 88 High St, Clapham added at 11:30:27 on 25/07/2019"
Function SprintF(strInput As String, ParamArray varSubstitutions() As Variant) As String
'Formatted string print: replaces all occurrences of %s in input with substitutions
Dim i As Long
Dim s As String
s = strInput
For i = 0 To UBound(varSubstitutions)
s = Replace(s, "%s", varSubstitutions(i), , 1)
Next
SprintF = s
End Function
Just to add as a footnote, the idea for this was inspired by the C language printf function.
I use a similar code to that of #natancodes except that I use regex to replace the occurances and allow the user to specifiy description for the placeholders. This is useful when you have a big table (like in Access) with many strings or translations so that you still know what each number means.
Function Format(ByVal Source As String, ParamArray Replacements() As Variant) As String
Dim Replacement As Variant
Dim i As Long
For i = 0 To UBound(Replacements)
Dim rx As New RegExp
With rx
.Pattern = "{" & i & "(?::(.+?))?}"
.IgnoreCase = True
.Global = True
End With
Select Case VarType(Replacements(i))
Case vbObject
If Replacements(i) Is Nothing Then
Dim Matches As MatchCollection
Set Matches = rx.Execute(Source)
If Matches.Count = 1 Then
Dim Items As SubMatches: Set Items = Matches(0).SubMatches
Dim Default As String: Default = Items(0)
Source = rx.Replace(Source, Default)
End If
End If
Case vbString
Source = rx.Replace(Source, CStr(Replacements(i)))
End Select
Next
Format = Source
End Function
Sub TestFormat()
Debug.Print Format("{0:Hi}, {1:space}!", Nothing, "World")
End Sub