Calling custom RegEx function in VBA - excel

I wanted to clean up my excel file, which is using a regular expression query like this. Unfortunately the approach mentioned there via data validation and CTRL-F3 "managed names" does conflict with a Worksheet_Change() sub in VBA. Meaning when applying data validation on the cell in question, Worksheet_Change() gets somehow overlooked.
Anyway, not much of a VBA regular by a long shot. I have a modul with the function RegExpMatch as follows (from above website, I hope this function can even be used for this purpose):
Public Function RegExpMatch(input_range As Range, pattern As String, Optional match_case As Boolean = True) As Variant
...
When using this as a formula (i.e. =RegExpMatch(Sheet1!A1, "^[A-Z]{3}-\d{3}$")) it works quite well. However due to above mentioned conflict I would like to call it like:
Private Sub Worksheet_Change(...)
....
If RegExpMatch(<target>, <pattern>) Then
...
End If
End Sub
I had some issues with special characters before and I find this in general quite confusing. Not sure if the pattern is a problem or how I am trying to call RegExpMatch, but I somehow need this to work and its giving me a run-time error 424. I understand, that there is a somewhat build-in RegEx possibility (see Tools->References), but this file needs to be distributed across different machines and therefore I dont want to make it reliant on special or outdated settings.
EDIT: Please see below MWE. The goal is to save the file when the cell value matches a RegEx. The error occurs when calling the RegExpMatch-Function within the If loop in the last code example.
I have a module called RegExpMatch as follows:
Public Function RegExpMatch(input_range As Range, pattern As String, Optional match_case As Boolean = True) As Variant
Dim arRes() As Variant 'array to store the results
Dim iInputCurRow, iInputCurCol, cntInputRows, cntInputCols As Long 'index of the current row in the source range, index of the current column in the source range, count of rows, count of columns
On Error GoTo ErrHandl
RegExpMatch = arRes
Set regex = CreateObject("VBScript.RegExp")
regex.pattern = pattern
regex.Global = True
regex.MultiLine = True
If True = match_case Then
regex.ignorecase = False
Else
regex.ignorecase = True
End If
cntInputRows = input_range.Rows.Count
cntInputCols = input_range.Columns.Count
ReDim arRes(1 To cntInputRows, 1 To cntInputCols)
For iInputCurRow = 1 To cntInputRows
For iInputCurCol = 1 To cntInputCols
arRes(iInputCurRow, iInputCurCol) = regex.Test(input_range.Cells(iInputCurRow, iInputCurCol).Value)
Next
Next
RegExpMatch = arRes
Exit Function
ErrHandl:
RegExpMatch = CVErr(xlErrValue)
End Function
and am trying to utilize that in my worksheet code as follows:
Public Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("J3")) Is Nothing Then
' The following shows run-time error 424
If RegExpMatch(Range("J3"), "^[A-Z]{3}-\d{3}$") Then
ActiveWorkbook.SaveAs Filename:=Range("J3").value
End If
End If
End Sub

Try removing the row
RegExpMatch = arRes
right after the
On Error Goto ErrHandl
statement. The array still is undefined in this moment and this most likely causes the error.

Not an answer (in regards of explaining the error), but as #FunThomas pointed out, I am using the built-in VBScript.RegEx anyway. So I wrote a simplier custom function RegExpMatch and it works as needed.
Public Function RegExpMatch(ByVal value as String, ByVal pattern As String)
Set regex = CreateObject("VBScript.RegExp")
regex.pattern = pattern
RegExpMatch = regex.Test(value)
End Function
and called this in my Workbook_Change sub as follows:
...
Dim value As String
value = Target.value
Dim pattern As String
pattern = "^[A-Z]{3}-\d{3}$"
If RegExpMatch(value, pattern) Then
...
End If
...

Related

How can I make removal of Strikethrough text in Excel cells using XML parsing more robust?

I have a complex spreadsheet with many cells of text containing random mixtures of normal text and text with strikethrough. Before I scan a cell for useful information, I have to remove the struck through text. I intially achieved this (with VBA) using the Characters object, but it was so slow as to be totally impractical, for business purposes. I was then kindly supplied with some code (on this site) that parses the XML encoding. This was 1000's of times faster, but it occassionally causes the following error:
"The parameter node is not a child of this node".
So far, it only happens in heavily loaded cells (1000's of characters), otherwise it works fine. I cannot see anything wrong in the code or the XML structure of the problem cells, although I am a total newbie to XML. Using the VBA debugger, I know the error is occurring when RemoveChild() is called, typically when it has already worked without error on a few struck through sections of a cell's text.
Is there a way I could make the following code more robust?
Public Sub ParseCellForItems(TargetCell As Excel.Range, ItemsInCell() As String)
Dim XMLDocObj As MSXML2.DOMDocument60
Dim x As MSXML2.IXMLDOMNode
Dim s As MSXML2.IXMLDOMNode
Dim CleanedCellText As String
On Error GoTo ErrorHandler
Call UnstrikeLineBreakCharsInCell(TargetCell)
Set XMLDocObj = New MSXML2.DOMDocument60
'Add some namespaces.
XMLDocObj.SetProperty "SelectionNamespaces", "xmlns:ss='urn:schemas-microsoft-com:office:spreadsheet' " & _
"xmlns:ht='http://www.w3.org/TR/REC-html40'"
'Load the cell data as XML into XMLDOcObj.
If XMLDocObj.LoadXML(TargetCell.Value(xlRangeValueXMLSpreadsheet)) Then
Set x = XMLDocObj.SelectSingleNode("//ss:Data") 'Cell content.
If Not x Is Nothing Then
Set s = x.SelectSingleNode("//ht:S") 'Struck through cell content.
Do While Not s Is Nothing
x.RemoveChild s
Set s = x.SelectSingleNode("//ht:S")
Loop
CleanedCellText = XMLDocObj.Text
'Parse CleanedCellText for useful information.'
'...
End If
End If
Set XMLDocObj = Nothing
'Presumably don't have to 'destroy' x and s as well, as they were pointing to elements of XMLObj.
Exit Sub
ErrorHandler:
Call RaiseError(Err.Number, Err.Source, "ParseCellForItems()", Err.Description, Erl)
End Sub
Public Sub UnstrikeLineBreakCharsInCell(TargetCell As Excel.Range)
Dim mc As MatchCollection
Dim RegExObj1 As RegExp
Dim Match As Variant
On Error GoTo ErrorHandler
Set RegExObj1 = New RegExp
RegExObj1.Global = True
RegExObj1.IgnoreCase = True
RegExObj1.Pattern = "\n" 'New line. Equivalent to vbNewLine.
Set mc = RegExObj1.Execute(TargetCell.Value)
For Each Match In mc
TargetCell.Characters(Match.FirstIndex + 1, 1).Font.Strikethrough = False
Next Match
Set mc = Nothing
Set RegExObj1 = Nothing
Exit Sub
ErrorHandler:
Call RaiseError(Err.Number, Err.Source, "UnstrikeLineBreakCharsInCell()", Err.Description, Erl)
End Sub
Yep, as per Tim Williams' comment, making sure you're calling RemoveChild() from its immediate parent fixes the problem:
Set s = x.SelectSingleNode("//ht:S")
Do While Not s Is Nothing
s.ParentNode.RemoveChild s
Set s = x.SelectSingleNode("//ht:S")
Loop

Match Not working Excel: Error 1004 Unable to get the Match Property

Sub Sales_Summary_Macro()
Dim strMake, strModel, strCount As String
Dim makeLoc, modelLoc, countLoc As Integer
strMake = Application.InputBox("Make")
strModel = Application.InputBox("Model")
strCount = Application.InputBox("Count")
If strMake <> False Then
Debug.Print strMake
Debug.Print strModel
Debug.Print strCount
makeLoc = WorksheetFunction.Match(strMake, Range("A1:A10"), 0)
Debug.Print makeLoc
End If
End Sub
I just want to take the string input of the user on three different variables and find the column that contains each variable. I have tried Application.Match() and Match() alone and neither seem to work.
Not going full technical and will not post code. However, three things:
One, make sure your ranges are always fully qualified. For example, Range("A1:A10") is not nearly enough. You should specify on which sheet this should be located. If you are calling this macro from another sheet, it will give you a wrong result or throw an error.
Two, without going to too much details:
Application.Match returns an error value if there's no match found. This can be handled using IsError, which is what simoco did in his answer.
WorksheetFunction.Match throws a 1004 error when it doesn't find an error. This is not the same as returning a value. As such, this is (slightly) harder to handle.
Best practice is to always use the first one.
Three, the immediate window in VBE is your best friend. A simple ?Application.Match("FindMe", [A1:A10], 0) in the window can help you check if your formula is netting a similarly intended result.
As shown in the screenshot above, no string is found and an error value is returned.
Hope this helps!
UPD:
Is it possible to get it to return the cell reference like C1 and then use that cell reference in other functions
Sub Sales_Summary_Macro()
Dim strMake As String, strModel As String, strCount As String
Dim makeLoc, modelLoc As Integer, countLoc As Integer
Dim res As Range
strMake = Application.InputBox("Make")
strModel = Application.InputBox("Model")
strCount = Application.InputBox("Count")
If strMake <> "False" Then
Debug.Print strMake
Debug.Print strModel
Debug.Print strCount
On Error Resume Next
'Set res = Range("A1:Z1").Find(What:=strMake, LookAt:=xlWhole, MatchCase:=False)
Set res = Application.Index(Range("A1:A10"), Application.Match(strMake, Range("A1:A10"), 0))
On Error GoTo 0
If res Is Nothing Then
MsgBox "Nothing found!"
Exit Sub
End If
'Print address of result
Debug.Print res.Address
makeLoc = res.Value
Debug.Print makeLoc
End If
End Sub
BTW,
when you are using Dim strMake, strModel, strCount As String, only strCount has type String, but strMake, strModel are Variant.
The same thing with Dim makeLoc, modelLoc, countLoc As Integer - only countLoc has Integer type.
This is not a direct answer to the OP, but people (like me) may find this question helpful when trying to TRAP an error with vba Match. Typically I would use this to test if a value exists in an array.
It's quite maddening when using Application.Worksheetfunction.Match and being unable to capture a True with IsError when a value doesn't exist. Even the WorksheetFunction error handlers (iserr, isNA, etc) will not capture this as True and instead throws the VBA error of 1004 Unable to get the Match Property.
This is resolved by using Application.Match instead of Application.WorksheetFunction.Match. This is most counterintuitive as Match doesn't appear in the intellisense after typing Application. nor does Application.Match( display prompts for what fields to enter.
Meanwhile using Application.WorksheetFunction.Match does auto-populate with prompts which understandably can inspire users to take this approach and then be confused why they can't successfully trap an error.

Test if range exists in VBA

I have a dynamically defined named range in my excel ss that grabs data out of a table based on a start date and an end date like this
=OFFSET(Time!$A$1,IFERROR(MATCH(Date_Range_Start,AllDates,0)-1,MATCH(Date_Range_Start,AllDates)),1,MATCH(Date_Range_End,AllDates)-IFERROR(MATCH(Date_Range_Start,AllDates,0)-1,MATCH(Date_Range_Start,AllDates)),4)
But if the date range has no data in the table, the range doesn't exists (or something, idk). How can I write code in VBA to test if this range exists or not?
I have tried something like
If Not Range("DateRangeData") Is Nothing Then
but I get "Runtime error 1004, method 'Range' of object '_Global' failed."
Here is a function I knocked up to return whether a named range exists. It might help you out.
Function RangeExists(R As String) As Boolean
Dim Test As Range
On Error Resume Next
Set Test = ActiveSheet.Range(R)
RangeExists = Err.Number = 0
End Function
You can replicate the match in your VBA to count before using the range how many rows you would have, or you can use error handling:
On Error Resume Next
Debug.Print range("DateRangeData").Rows.Count
If Err = 1004 Then
MsgBox "Range Empty"
Exit Sub
Else
MsgBox "Range full"
End If
Err.Clear
On Error GoTo 0
This is another approach. It has the advantage to take the container and the name you want to test. That means you can test either Sheets Names or Workbook Names for example.
Like this:
If NamedRangeExists(ActiveSheet.Names, "Date") Then
...
Else
...
End If
or
If NamedRangeExists(ActiveWorkbook.Names, "Date") Then
...
Else
...
End If
Public Function NamedRangeExists(ByRef Container As Object, item As String) As Boolean
Dim obj As Object
Dim value As Variant
On Error GoTo NamedRangeExistsError:
value = Container(item)
If Not InStr(1, CStr(value), "#REF!") > 0 Then
NamedRangeExists = True
End If
Exit Function
Exit Function
NamedRangeExistsError:
NamedRangeExists = False
End Function
Depending on the application you're doing, it's good to consider using a Dictionary. They're especially useful when you wanna check whether something exists.
Take this example:
Dim dictNames as Scripting.Dictionary
Sub CheckRangeWithDictionary()
Dim nm As Name
'Initially, check whether names dictionary has already been created
If Not dictNames Is Nothing Then
'if so, dictNames is set to nothing
Set dictNames = Nothing
End If
'Set to new dictionary and set compare mode to text
Set dictNames = New Scripting.Dictionary
dictNames.CompareMode = TextCompare
'For each Named Range
For Each nm In ThisWorkbook.Names
'Check if it refers to an existing cell (bad references point to "#REF!" errors)
If Not (Strings.Right(nm.RefersTo, 5) = "#REF!") Then
'Only in that case, create a Dictionary entry
'The key will be the name of the range and the item will be the address, worksheet included
dictNames(nm.Name) = nm.RefersTo
End If
Next
'You now have a dictionary of valid named ranges that can be checked
End Sub
Within your main procedure, all you need to do is do an existence check before using the range
Sub CopyRange_MyRange()
CheckRangeWithDictionary
If dictNames.exists("MyRange") then
Sheets(1).Range("MyRange").Copy
end if
End Sub
While loading the dictionary may look a little longer, it's extremely fast to process and search. It also becomes much simpler to check whether any named range referring to a valid address exists, without using error handlers in this simple application.
Please note that when using names at sheet level rather than workbook level, it is necessary to use more elaborate keys to guarantee uniqueness. From the way the dictionary was created, if a key is repeated, the item value is overwritten. That can be avoided by using the same Exists method as a check in the key creation statement. If you need a good reference on how to use dictionaries, use this one.
Good luck!
This is an old post, but none of the rated answers has a dynamic solution to test if a name exists in a workbook or worksheet. This function below will accomplish that:
Function pg_Any_Name(thename As String) As Boolean
Dim n As Name, t As String
For Each n In ThisWorkbook.Names
t = Mid(n.Name, InStr(1, n.Name, "!", vbTextCompare) + 1, 999)
If UCase(thename) = UCase(t) Then
pg_Any_Name = True
Exit Function
End If
Next n
End Function
Worth noting that this would not have worked for this specific question because OP had a dynamic defined range. This question would have been more accurately titled Test if Name is a Valid Range because the name always existed as a formula, the issue was if it was a valid RANGE. To address this question with a solution that checks both workbook and sheets... this function would work:
Function PG_Range_Name(thename As String) As Boolean
Dim n As Name, t As String
For Each n In ThisWorkbook.Names
t = Mid(n.Name, InStr(1, n.Name, "!", vbTextCompare) + 1, 999)
If UCase(thename) = UCase(t) Then
On Error Resume Next
PG_Range_Name = n.RefersToRange.Columns.Count > 0
Exit Function
End If
Next n
End Function

VBA FileFolderExists pass variable

I found this function on a web
Private Function FileFolderExists(strFullPath As String) As Boolean
On Error GoTo EarlyExit
If Not Dir(strFullPath, vbDirectory) = vbNullString then
FileFolderExists = True
End If
EarlyExit:
On Error GoTo 0
End Function
And I want to pass string variable like this
Dim lineText As String
...
ElseIf FileFolderExists(lineText) = False Then
I am getting compile error "byref argument type mismatch"
When I put byval before strFullPath, it doesn't seem to work properly.
I also tried playing with Dir function, it works if I pass literal like "C:\test", but it doesn't work if I pass the variable.
Does anyone have function that check for folder existence and accepts the string variable as parameter ?
Thanks in advance
The problem seems to be that Word adds CR character to every paragraph, or, to be more exact, that the Text property of the Paragraph object returns the paragraph text plus the CR character.
AFAIK, this is the Word's behaviour for every paragraph, even for the last one.
How can this cause a compile error, I do not have a clue. If I take Milan's example:
Private Sub FirstLineFolder()
Dim lineText As String
lineText = ActiveDocument.Paragraphs(1).Range.Text
lineText = Left(lineText, Len(lineText) - 1) 'see below
MsgBox DoesFolderExist("C:\")
MsgBox DoesFolderExist(lineText)
End Sub
it returns true, true if the first line of the document is a valid folder. If I comment the marked line, the program still compiles and runs and returns true, false (with the same document).
There is some info about it on MSDN website
Try this:
Function FolderExists(folderPath As String) As Boolean
Dim f As Object
Set f = CreateObject("Scripting.FileSystemObject")
On Error GoTo NotFound
Dim ff As Object
Set ff = f.GetFolder(folderPath)
FolderExists = True
Exit Function
NotFound:
FolderExists = False
On Error GoTo 0
End Function
I used the following to test it:
Sub Tst()
Dim b As Boolean
Dim s As String
s = "c:\temp"
b = FolderExists(s)
End Sub
And it works as expected.
Generally, I used Scripting.FileSystemObject for all file-related operation in VBA, the native functions are too cumbersome.
It should be also noted that my function all checks for folders, while the original function -- judging by its name -- perhaps also tried to check for existence of files.
New code, it explains exactly what I need, it should be easier for you to try.
I am expecting folder in first line of the Word document, then I have to check if it exists.
Private Sub FirstLineFolder()
Dim lineText As String
lineText = ActiveDocument.Paragraphs(1).range.Text
MsgBox DoesFolderExists("C:\") ' this works
MsgBox DoesFolderExists(lineText) ' this doesnt work, when same folder passed
End Sub
Both my and Martin's function are throwing compiling error I wrote in my first post.
If it matters : Word is 2010, "option explicit" isn't written (I inherited the code, I can't change that)

Excel UDF detecting page breaks?

I'm trying to write a UDF that returns whether the cell is at a page break.
So far I have this:
Function pbreak() As Boolean
' Application.Volatile
pbreak = False
Dim ra As Range
Set ra = Application.Caller
With ra
For i = 1 To .Worksheet.HPageBreaks.Count
If .Worksheet.HPageBreaks(i).Location.Row = .Row Then
pbreak = True
End If
Next
End With
End Function
This returns a #VALUE error. I've tried debugging it, HPageBreaks.Count returns 3 (and there are 3 page breaks), but HPageBreaks(i) yields an "index out of range"-error for all pagebreaks that are below the current cell .
Is this a bug (ie .Count is wrong), or is there some special behavior with page breaks that I am missing?
Is there a way to fix this (preferably without resorting to on error resume next)?
Thanks
Martin
Option Explicit
Function pbreak() As Boolean
' Application.Volatile
Dim i As Integer 'the missing line
pbreak = False
Dim ra As Range
Set ra = Application.Caller
With ra
For i = 1 To .Worksheet.HPageBreaks.Count
If .Worksheet.HPageBreaks(i).Location.Row <= .Row Then
If .Worksheet.HPageBreaks(i).Location.Row = .Row Then
pbreak = True
'exit the function once a page break is found.
Exit Function
End If
Else
Exit Function
End If
Next
End With
End Function
EDIT: Always use Option Explicit & compile the code before using it.
Use of Exit Function inside the loop is to prevent the code from running it further, once the result is known.

Resources