I'm trying to duplicate a sheet. The user inputs the sheet name with an Input Box.
I'm trying to figure out how to interrupt the process if the user inputs a sheet name that already exists.
My plan is to use RegEx to match the string against all of the sheets in the workbook, and determine if the inputted name is already in use.
Here is my current code:
Dim NewSheetName As String
SheetInput: NewSheetName = InputBox("Insert the new sheet name.")
'Ends procedure if user does not input anything.
If NewSheetName = "" Then
Exit Sub
End If
'Ends procedure if the sheet name is already in use.
Dim RegEx As Object
Set RegEx = CreateObject("VBScript.RegExp")
With RegEx
.Pattern = NewSheetName
End With
For i = 1 To Sheets.Count
If RegEx.Test(Sheet(i).Name) = False Then
MsgBox ("This name is already used on a different sheet. Please use a different name.")
GoTo SheetInput
End If
Next i
I get the message
"Compile Error: Sub or Function not defined"
with Sheet in Sheet(i).Name highlighted. I assumed the name of the sheet would be inputted into the RegEx function as a string, but this doesn't seem to be happening.
There are many ways to check if a sheet exists. Here is one. I am sure there may be better ones.
Public Function worksheetExists(ByVal wb As Workbook, ByVal sheetNameStr As String) As Boolean
On Error Resume Next
worksheetExists = (wb.Worksheets(sheetNameStr).Name <> "")
On Error GoTo 0
End Function
Here are a couple of ways to call this function:
If worksheetExists(ThisWorkbook, "sheetNameOne") then ...
If Not worksheetExists(ThisWorkbook, "sheetNameOne") then ...
Adding a call to a couple of user defined functions that are reusable, and restructuring your code to use a Do Loop instead of GoTo see the following:
Public Sub AddNewWrkSht()
Dim SheetName As String
SheetName = GetValidSheetName()
Dim NewWrkSht As Worksheet
Set NewWrkSht = ThisWorkbook.Worksheets.Add
NewWrkSht.Name = SheetName
End Sub
Private Function GetValidSheetName() As String
Dim NewSheetName As String
'keep asking the user for a valid sheet name
'until it is valid
Do
NewSheetName = InputBox("Insert the new sheet name.")
Loop Until IsValidSheetName(NewSheetName)
GetValidSheetName = NewSheetName
End Function
Private Function IsValidSheetName(ByVal SheetName As String) As Boolean
If IsValueEmpty(SheetName) Then
MsgBox "You must Provide a sheet Name."
Exit Function
End If
If WorkSheetExists(SheetName) Then
MsgBox "This name is already used on a different sheet. " & _
"Please choose a different name."
Exit Function
End If
'if we make it here without exiting, then the sheet name is valid
IsValidSheetName = True
End Function
Private Function WorkSheetExists(ByVal SheetName As String) As Boolean
On Error Resume Next
WorkSheetExists = Not ActiveWorkbook.Worksheets(SheetName) Is Nothing
End Function
Private Function IsValueEmpty(ByVal varValue As Variant) As Boolean
IsValueEmpty = (Len(RemoveAllWhiteSpace(varValue)) = 0)
End Function
Private Function RemoveAllWhiteSpace(ByRef varStringIn As Variant, _
Optional ByRef RegExpIn As Object) As String
'Create if not instantiated
If RegExpIn Is Nothing Then Set RegExpIn = CreateObject("VBScript.RegExp")
With RegExpIn
.Pattern = "\s"
.MultiLine = True
.Global = True
RemoveAllWhiteSpace = CStr(.Replace(varStringIn, vbNullString))
End With
End Function
Related
Here's the pertinent code, which is giving different results on the regular expression test for the message body depending on whether I launch it using TestLaunchURL or the message is passed to it by Outlook when an incoming message arrives:
Public Sub OpenLinksMessage(olMail As Outlook.MailItem)
Dim Reg1 As RegExp
Dim AllMatches As MatchCollection
Dim M As Match
Dim strURL As String
Dim RetCode As Long
Set Reg1 = New RegExp
With Reg1
.Pattern = "(https?[:]//([0-9a-z=\?:/\.&-^!#$;_])*)"
.Global = True
.IgnoreCase = True
End With
PlayTheSound "Speech On.wav"
RetCode = Reg1.Test(olMail.Body)
MsgBox "The RetCode from Reg1.Test(olMail.Body) equals" + Str(RetCode)
' If the regular expression test for URLs in the message body finds one or more
If RetCode Then
PlayTheSound "chimes.wav"
' Use the RegEx to return all instances that match it to the AllMatches group
Set AllMatches = Reg1.Execute(olMail.Body)
For Each M In AllMatches
strURL = M.SubMatches(0)
' Don't activate any URLs that are for unsubscribing; skip them
If InStr(1, strURL, "unsubscribe") Then GoTo NextURL
' If the URL ends with a > from being enclosed in darts, strip that > off
If Right(strURL, 1) = ">" Then strURL = Left(strURL, Len(strURL) - 1)
' The URL to activate to accept must contain both of the substrings in the IF statement
PlayTheSound "tada.wav"
If InStr(1, strURL, ".com") Then
PlayTheSound "TrainWhistle.wav"
' Activate that link to accept the job
RetCode = ShellExecute(0, "Open", strURL)
Set Reg1 = Nothing
Exit Sub
End If
NextURL:
Next
End If
Set Reg1 = Nothing
End Sub
Private Sub TestLaunchURL()
Dim currItem As MailItem
Set currItem = ActiveExplorer.Selection(1)
OpenLinksMessage currItem
End Sub
The test IF Reg1.Test(olMail.Body) always returns a 0 when invoked from an Outlook rule on an incoming message and always returns a -1 when I use the debugger to trigger it for that same message from my inbox.
The code is acting almost as though it has a null message body when it is triggered by an Outlook rule versus having the message body when kicked off by me from exactly the same message once it's in my inbox.
I am completely flummoxed, as I can't understand how one and the same message, with one and the same body, can give 2 different results depending on who hands the message to the subroutine.
Additional Debugging Information:
Since the issue appears to surround the value of the Body of the message, I added the following code, that also examines the HTMLBody as well:
If IsNull(olMail.Body) Then
MsgBox "The message body is null!!"
Else
MsgBox "BODY: " + "|" + olMail.Body + "|"
End If
If IsNull(olMail.HTMLBody) Then
MsgBox "The message HTMLbody is null!!"
Else
MsgBox "BODY: " + "|" + olMail.HTMLBody + "|"
End If
When the script is triggered by the Outlook rule on a message with the content, and only the content, "http://britishtoolworks.com", when it arrives these are the two message boxes:
[I am being forbidden to post images for some reason. These show absolutely nothing between the two pipe characters for BODY and some text, but nothing with the URL in it, for the HTMLBody]
while these are the message boxes if I trigger the script via TestLaunchURL after that very same message is sitting in my inbox:
[Shows the actual expected content. I am forbidden from posting more images.]
If anyone can explain this discrepancy, please do.
Here is the code that finally works. It's clear that the .Body member of olMail is not available until some sort of behind the scenes processing has had time to occur and if you don't wait long enough it won't be there when you go to test using it. Focus on the Public Sub OpenLinksMessage which is where the problem had been occurring.
The major (and only) change that allowed the expected processing of olMail.Body to take place, apparently, was the addition of the line of code: Set InspectMail = olMail.GetInspector.CurrentItem. The time it takes for this set statement to run allows the .Body to become available on the olMail parameter that's passed in by the Outlook rule. What's interesting is that if you immediately display InspectMail.Body after the set statement it shows as empty, just like olMail.Body used to.
Option Explicit
Private Declare Function ShellExecute _
Lib "shell32.dll" Alias "ShellExecuteA" ( _
ByVal hWnd As Long, _
ByVal Operation As String, _
ByVal Filename As String, _
Optional ByVal Parameters As String, _
Optional ByVal Directory As String, _
Optional ByVal WindowStyle As Long = vbMinimizedFocus _
) As Long
Public Sub OpenLinksMessage(olMail As Outlook.MailItem)
Dim InspectMail As Outlook.MailItem
Dim Reg1 As RegExp
Dim AllMatches As MatchCollection
Dim M As Match
Dim strURL As String
Dim SnaggedBody As String
Dim RetCode As Long
' The purpose of the following Set statement is strictly to "burn time" so that the .Body member of
' olMail is available by the time it is needed below. Without this statement the .Body is consistently
' showing up as empty. What's interesting is if you use MsgBox to display InspectMail.Body immediately after
' this Set statement it shows as empty.
Set InspectMail = olMail.GetInspector.CurrentItem
Set Reg1 = New RegExp
With Reg1
.Pattern = "(https?[:]//([0-9a-z=\?:/\.&-^!#$;_])*)"
.Global = True
.IgnoreCase = True
End With
RetCode = Reg1.Test(olMail.Body)
' If the regular expression test for URLs in the message body finds one or more
If RetCode Then
' Use the RegEx to return all instances that match it to the AllMatches group
Set AllMatches = Reg1.Execute(olMail.Body)
For Each M In AllMatches
strURL = M.SubMatches(0)
' Don't activate any URLs that are for unsubscribing; skip them
If InStr(1, strURL, "unsubscribe") Then GoTo NextURL
' If the URL ends with a > from being enclosed in darts, strip that > off
If Right(strURL, 1) = ">" Then strURL = Left(strURL, Len(strURL) - 1)
' The URL to activate to accept must contain both of the substrings in the IF statement
If InStr(1, strURL, ".com") Then
' Activate that link to accept the job
RetCode = ShellExecute(0, "Open", strURL)
Set InspectMail = Nothing
Set Reg1 = Nothing
Set AllMatches = Nothing
Set M = Nothing
Exit Sub
End If
NextURL:
Next
End If
Set InspectMail = Nothing
Set Reg1 = Nothing
Set AllMatches = Nothing
Set M = Nothing
End Sub
Special thanks to niton for his patience and assistance on other questions that formed the basis for this one. He led me to the solution.
Addendum: Another individual assisting me elsewhere brought up something that deserves noting here, as I think she's got it right. I am using Gmail via IMAP access to download my messages. What appears to be happening is that once the header information is populated into the MailItem object, the Outlook Rule is immediately being triggered. The rest of the members of that object, including .Body, appear to be being populated asynchronously behind the scenes. The speed of processing in your script versus the speed of population processing can lead to situations where the script is triggered with the header information and gets to the point where it accesses the .Body before it's been populated by Outlook itself. What's interesting is when this occurred, and that was most of the time until this solution was found, .Body was not considered to be NULL. The IsNull test never passed, but the content when printed was nothing, as in absolutely nothing between the two pipe characters I used as delimiters. What is "nothing that takes up any characters" but that also is not NULL?
Clearly the whole MailItem passed would not pass the "Is Nothing" test, and I would not think to test an individual member of an object with "Is Nothing."
For myself, I consider this to be buggy. Before a MailItem object is ever handed off for script processing it would be the logical presumption that all Members of that object that can be prepopulated will be prepopulated by Outlook before the handoff. It just doesn't appear to be happening that way, and this is under Outlook 2010 on my machine and Outlook 2016 on another. If you get a member that has not yet been populated it should always have the NULL value, as that should be what everything is initialized to prior to the population process taking place.
I have a system that I can output a spreadsheet from. I then take this outputted spreadsheet and import it into MS Access. There, I run some basic update queries before merging the final result into a SharePoint 2013 Linked List.
The spreadsheet I output has an unfortunate Long Text field which has some comments in it, which are vital. On the system that hosts the spreadsheet, these comments are nicely formatted. When the spreadsheet it output though, the field turns into a long, very unpretty string like so:
09:00 on 01/03/2017, Firstname Surname. :- Have responded to request for more information. 15:12 on 15/02/2017, Firstname Surname. :- Need more information to progress request. 17:09 on 09/02/2017, Firstname Surname. :- Have placed request.
What I would like to do is run a query (either in MS Access or MS Excel) which can scan this field, detect occurrences of "##:## on ##/##/####, Firstname Surname. :-" and then automatically insert a line break before them, so this text is more neatly formatted. It would obviously skip the first occurrence of this format, as otherwise it would enter a new line at the start of the field. Ideal end result would be:
09:00 on 01/03/2017, Firstname Surname. :- Have responded to request
for more information.
15:12 on 15/02/2017, Firstname Surname. :- Need more information to progress request.
17:09 on 09/02/2017, Firstname Surname. :- Have placed request.
To be honest, I haven't tried much myself so far, as I really don't know where to start. I don't know if this can be done without regular expressions, or within a simple query versus VBA code.
I did start building a regular expression, like so:
[0-9]{2}:[0-9]{2}\s[o][n]\s[0-9]{2}\/[0-9]{2}\/[0-9]{4}\,\s
But this looks a little ridiculous and I'm fairly certain I'm going about it in a very unnecessary way. From what I can see from the text, detecting the next occurrence of "##:## on ##/##/####" should be enough. If I take a new line after this, that will suffice.
You have your RegExp pattern, now you need to create a function to append found items with your extra delimiter.
look at this function. It takes, your long string and finds your date-stamp using your pattern and appends with your delimiter.
Ideally, i would run each line twice and add delimiters after each column so you have a string like,
datestamp;firstname lastname;comment
you can then use arr = vba.split(text, ";") to get your data into an array and use it as
date-stamp = arr(0)
name = arr(1)
comment = arr(2)
Public Function FN_REGEX_REPLACE(iText As String, iPattern As String, iDelimiter As String) As String
Dim objRegex As Object
Dim allmatches As Variant
Dim I As Long
On Error GoTo FN_REGEX_REPLACE_Error
Set objRegex = CreateObject("vbscript.regexp")
With objRegex
.Multiline = True
.Global = True
.IgnoreCase = True
.Pattern = iPattern
If .test(iText) Then
Set allmatches = .Execute(iText)
If allmatches.count > 0 Then
For I = 1 To allmatches.count - 1 ' for i = 0 to count will start from first match
iText = VBA.Replace(iText, allmatches.item(I), iDelimiter & allmatches.item(I))
Next I
End If
End If
End With
FN_REGEX_REPLACE = Trim(iText)
Set objRegex = Nothing
On Error GoTo 0
Exit Function
FN_REGEX_REPLACE_Error:
MsgBox Err.description
End Function
use above function as
mPattern = "[0-9]{2}:[0-9]{2}\s[o][n]\s[0-9]{2}\/[0-9]{2}\/[0-9]{4}\,"
replacedText = FN_REGEX_REPLACE(originalText,mPattern,vbnewline)
Excel uses LF for linebreaks, Access uses CRLF.
So it should suffice to run a simple replacement query:
UPDATE myTable
SET LongTextField = Replace([LongTextField], Chr(10), Chr(13) & Chr(10))
WHERE <...>
You need to make sure that this runs only once on newly imported records, not repeatedly on all records.
Using Excel 2010. I want to only allow values in a cell that fit a given regex pattern. So I created a UDF module as follows:
Public re as RegExp
Public Function isValidRegex(rng As Range, pattern As String) As Boolean
If re Is Nothing Then
Set re = New RegExp
End If
re.pattern = pattern
isValidRegex = re.Test(rng.value)
End Function
I created a named range called THIS_CELL, so that the current cell can be passed to isValidRegex(), as follows:
=INDIRECT(ADDRESS(ROW(),COLUMN()))
I set a custom validation for the cell, using this formula:
=isValidRegex(THIS_CELL,"(my|regex)patt[ern]")
This generated the following error:
A named range you specified cannot be found.
According to this article, UDFs cannot be used in Custom validation formulas. The solution suggested in the article (putting the formula in another cell, making that cell into a named range, and referencing that cell in the Custom formula) won't work, because I need to be able to pass THIS_CELL as an argument to the function.
I also tried creating a named range called isValidRegexPattern, defining it as =isValidRegex(THIS_CELL,"(my|regex)patt[ern]"), and setting the Custom formula to =isValidRegexPattern, but this didn't work either; putting a breakpoint in isValidRegex() showed that the function wasn't even being called.
So, how can I use a UDF for cell validation?
You can use a static variable with the Worksheet_Change event to keep a snapshot of the prior values
The code below tracks the values in A1:A10 and uses a Regexp like yours to reject any non-numeric entries
The example below tries top copy and paste B1:B10 over A1:A10, only A6and A8 are allowed as they are numeric
to set the range initially change a cell outside the range of interest to trigger If IsEmpty(X) Then X = [a1:a10].Value2
change event
Private Sub Worksheet_Change(ByVal Target As Range)
Static X As Variant
Dim rng2 As Range
Dim rng3 As Range
If IsEmpty(X) Then X = [a1:a10].Value2
Set rng2 = Intersect([a1:a10], Target)
If rng2 Is Nothing Then Exit Sub
Application.EnableEvents = False
For Each rng3 In rng2
If Not isValidRegex(rng3, "\d+") Then rng3.Value = X(rng3.Row, 1)
Next
Application.EnableEvents = True
X = [a1:a10].Value2
End Sub
regexp
Function isValidRegex(rng As Range, pattern As String) As Boolean
Dim re As Object
Set re = CreateObject("vbscript.regexp")
re.pattern = pattern
isValidRegex = re.Test(rng.Value)
End Function
You seem to be reluctant to move over to a WorksheetChange event macro because you believe it does not 'capture the pre-change state of the cell'. That may be correct in the strictest definition but that doesn't mean you cannot capture the changed state, undo the change, determine whether the change is valid and only re-apply the change if it meets criteria.
I'm not going to produce a full regex validating function. This simply tests if the number typed into column E is less than zero or blank. If not then the cell reverts to its prechange state.
Private Sub Worksheet_Change(ByVal Target As Range)
If Target.Count > 1 Then Exit Sub
If Not Intersect(Target, Columns(5)) Is Nothing Then
If Not IsEmpty(Target) Then
On Error GoTo Safe_Exit
Application.EnableEvents = False
Dim vNEW As Variant
vNEW = Target.Value
Application.Undo
If bIs_It_Valid(vNEW) Then
Target = vNEW
Else
' put stuff like idiot warnings here
End If
End If
End If
Safe_Exit:
Application.EnableEvents = True
End Sub
Private Function bIs_It_Valid(val As Variant) As Boolean
If IsNumeric(val) Then _
bIs_It_Valid = CBool(val < 0)
Debug.Print bIs_It_Valid
End Function
That Worksheet_Change could probably be adjusted to work on a range of cells if pasting a number of values is important.
Here's how I accomplished this without using the Worksheet_Change event
Define a Public REGEX Function in a new Module
'Public REGEX Formula
Public Function REGEX(pattern As String, cel As Range) As Boolean
Dim re As New RegExp
re.pattern = pattern
REGEX = re.Test(cel.Value)
End Function
I added this Sub to a module I named Validations. This Sub requires not only the range to validate and the regular expression pattern, but also another range to apply the REGEX formula to. The actual validation applied actually only checks that separate cell for a True or False value. This is a simplified version that assumes the validationColumn is an entire column.
'Validations Module
Sub regexValidation(cells As Range, pattern As String, validationColumn As Range, defaultValue As String)
Dim cel As Range, regexFormula As String, validationCell As Range
cells.Value = defaultValue
'Need to match true on default value or validation will always fail
pattern = "(" & defaultValue & ")|(" & pattern & ")"
For Each cel In cells
regexFormula = "=REGEX(""" & pattern & """," & cel.address & ")"
Set validationCell = validationColumn.cells(cel.Row, 1)
validationCell.Formula = regexFormula
cel.Validation.Delete
cel.Validation.Add xlValidateCustom, Formula1:="=" & Trim(validationCell.address)
Next cel
End Sub
This is how I'm calling it. In my case, this is a UserForm with a TextBox called regexPattern that contains the regular expression to apply.
'Form
Private Sub applyRegexValidation(cells As Range)
Validations.regexValidation cells, regexPattern.text, validationColumn:=cells.Parent.Range("AA:AA"), defaultValue:="Required Custom"
End Sub
I have an Asp.Net web application to manage certain tables in the database. I'm using Grid to insert, update the Database. In addition to this, the requirement is that, user should be able to insert into database from Excel(by uploading the Excel, sort of like Import from Excel into Database).
So, I'm reusing the code for insertions(which i used for Insert in Grid) for each row in the Excel.
And I have Regular expression validators for certain fieldsin Grid in Asp.Net as follows:
Id: can be combination of numbers,alphabets. Regex is:"^[a-zA-Z0-9_]{1,50}$"
Formula: can have arithmetic operators and dot. Regex is: "^[ A-Za-z0-9%._(/*+)-]*$"
Sort Order: must be nuber with some max size Regex is: "^[0-9]{1,5}$"
Weight: real number with max size Regex is : "^[0-9]+(?:\.\d{1,2})?$"
Domain UserName: username with domain name Regex is: "^[a-zA-Z\\._]{1,200}$"
I wanted to have this validators in the Excel cells too. I've searched if Excel allows Regular expressions and found that it should be done through vba or any third party tool. I don't know Vb.net and neither want to use any external tool.
And i don't know much about Excel too. Is there any way to do the validations. If so, will there be some formats for setting formula for regex.
Can anyone suggest me how to do this. Thanks In Advance.
You can use the Regex engine that comes with VBScript:
Dim User_ID As String
User_ID = InputBox("Enter User ID:")
With CreateObject("VBScript.RegExp")
.Global = True
.Pattern = "^[\w]{1,50}$"
If .Test(User_ID) Then '// Check pattern matches User_ID string
Range("B" & Rows.Count).End(xlUp).Offset(1, 0).Value = User_ID
Else
MsgBox("Invalid ID, please try again!")
End If
End With
I got the answer. I've wrote worksheet_Change event with if else
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Target.Row = 1 Then Exit Sub '// Only look at header row
Application.EnableEvents = False '// Disable events, prevent infinite loop.
If Cells(1, Target.Column).Value = "Attribute_Id" Then
Target.Value = AttributeId(Target.Value)
ElseIf Cells(1, Target.Column).Value = "Attribute_Name" Then
Target.Value = AttributeName(Target.Value)
End If
Application.EnableEvents = True '// Turn Events back on
End Sub
And these are the functions:
Function AttributeId(Attribute_Id As String) As String
With CreateObject("vbscript.regexp")
.Global = True
.Pattern = "^[a-zA-Z0-9_]{1,50}$"
.IgnoreCase = True
If Not .Test(Attribute_Id) Then
MsgBox ("Invalid Attribute ID, please try again!")
Exit Function
End If
End With
AttributeId = Attribute_Id
End Function
And
Function AttributeName(Attribute_Name As String) As String
If Attribute_Name = "" Then MsgBox ("Attribute Name is a Mandatory field!")
AttributeName = Attribute_Name
End Function
No need to bind the functions to the cells.
-- Thank you #S O for the help..
Right now i'm making a program that allows you to mod a game more easier. In the regular game you have to open up files and navigate through the animations. I wanted to make it easier. I've already made the other parts of the program but go to the last part that I need help with. I want to be able to grab all forms of the first animation name and then the inside animation name, make that go along with it. So I can make an easy to use editor. I know this would most likely involve regex and I am fairly bad at it, I am also still trying to RE-learn VB.net after not toying with the language for ages. If someone could help me out, i'd be very thankful:
The file I am trying to load:
animation "idle0"
{
animation "idle_yoga";
};
animation "idle1"
{
animation "idle_pants";
};
Here you have a sample code performing what you are after:
Dim dict As Dictionary(Of String, String) = New Dictionary(Of String, String)()
Try
Dim sr As System.IO.StreamReader = New System.IO.StreamReader("path to the file")
Dim line As String
Dim started As Boolean = False
Dim inside As Boolean = False
Dim firstInput As String = ""
Do
line = sr.ReadLine()
If (line IsNot Nothing) Then
If (line.ToLower().Contains("animation")) Then
If (started AndAlso inside) Then
'Animation
Dim curItem As String = line.ToLower().Split(New String() {"animation"}, StringSplitOptions.None)(1).Trim()
If (curItem.Substring(curItem.Length - 1, 1) = ";") Then curItem = curItem.Substring(0, curItem.Length - 1)
curItem = curItem.Replace("""", "")
dict.Add(firstInput, curItem)
started = False
inside = False
ElseIf (Not inside) Then
'Group name
Dim curItem As String = line.ToLower().Split(New String() {"animation"}, StringSplitOptions.None)(1).Trim()
curItem = curItem.Replace("""", "")
firstInput = curItem
started = True
End If
ElseIf (started AndAlso line.Contains("{")) Then
inside = True
End If
End If
Loop Until line Is Nothing
sr.Close()
Catch
End Try
This code reads the information from a file as described (the code you posted line by line) and performs the grouping you want. Finally, I chose a Dictionary (ListBox is perhaps not the best control for that; you might consider to use a ListView better) because the whole point is showing you how can this situation be addressed. I guess that what the code does is pretty clear: you will have to extend/adapt it to your actual requirements, although the main structure should be something on these lines anyway.