As we utilize our editing tools, we’ll inevitably want a more comprehensive set. Toward that end, we generalize an earlier macro to quickly delete an introductory phrase or main clause of a sentence.
Thanks for your interest
This content is part of a paid plan.
Delete Sentence Introductory Phrase
We’re deleting the introductory phrase or main clause of a sentence with a keystroke. While we’re at it, our new macro might as well remove any conjunction and capitalize the new first word of the sentence. Macros excel at repetitive tasks. It’s why we use them, and today’s multi-step task is easy for a computer to accomplish without additional human input other than starting inside the intended sentence.
Why bother?
Sometimes it is handy to be able to tell Word to just do something simple. No fuss. Just get it done.
If you’ve been following along with our articles, we’ve implemented many macros to complete similar tasks. Sometimes, including this one today, the knee jerk reaction might be … but why?
After all, you have fingers, right? And you’ve been doing it “this way” for years, so why bother?
I hear you. Surely, we can leave some editing to the human, right?
Well, yes, but you’ve got plenty to do. Things that require a human brain, so why not let Word handle the tedious details.
Just try it.
You’ll be surprised at how often you’ll start wanting these quick and dirty macros to accomplish menial but necessary tasks.
A few notes
Believe it or not, the general term for either an introductory phrase or a main clause of a compound sentence is an “introducer.” Yeah, it sounds a little odd, but I’ll use that term because it’s accurate and shorter than repeating “introductory phrase or main clause.”
This macro also utilizes two previous simple functions that define single and double quotes because they are different characters between Word for Mac or Windows.
Function skeleton
We’ll initially implement a function to return the range of the introducer. We can then easily specialize it to deleting the identified text, but we’ll also extend it to related tasks when we’re done. The function skeleton is:
We're not creating a function to carry out the delete or select the text because we want to apply it to several different tasks each of which needs the introducer range as a starting point. I'm not saying ranges are a silver bullet but get used to thinking in terms of them, and it will often (not always) help frame up and even generalize the solution to apply to similar tasks.
The following naming stuff isn’t strictly necessary but notice the descriptive function name elements.
- “Get” often means … well, get something. How is that for profound?
- We then include the document element category of “Sentence”.
- A “what” of “Introducer”
- Finally, the word “Range” implies what the function returns to whoever uses it.
The idea is so using the function reads a little more like English, so we can understand how it’s being used in a macro. I don’t always follow the above pattern, but it also helps with organizing your macros and functions.
What parameters?
We accept a target range which should be positioned somewhere inside a sentence that contains the introducer. The target range does not have to start inside the introducer.
Return value
We return the identified sentence introducer range. For concreteness, we’ll exclude initial quotes, an open parenthesis, or a left square bracket. We’ll include a comma or semicolon in the returned range as well as any conjunction (see extended content) if we’re returning a main clause range.
The latter two details could be set up to depend on optional parameters, but I’ve found that I haven’t needed to reverse either assumption yet.
Create working range duplicate
We first create a working range r as a duplicate of the target range. It’s named r just to keep the name short.
Now r is an independent range we can manipulate as desired.
If we were to set it equal to the target range instead, they would essentially be the same range. Any changes we would make to our working range r would immediately be reflected in the target range which is usually not the desired result.
Collapse the range
Since a given target range could span more than one sentence, it’s safest to collapse the initial range to avoid any unexpected issues with the upcoming steps.
Collapsing the range ensures we have a well-defined starting point for the rest of the macro.
Get the beginning of the sentence
Since we want the sentence introducer range, we move the working range to the start of the current sentence.
The advantage of using the StartOf method rather than a Move method is the range doesn’t move if we’re already at the beginning of the sentence. The Unit wdSentence is from a standard word units table.
Notice how we’re making choices that give us a clear state as we move forward.
Exclude initial sentence punctuation
We want to exclude single or double quotes, an open parenthesis, or a left square bracket from the range. Technically, our working range doesn’t span any text yet, but we’ll extend it toward a comma or semicolon in the next step, so we move past any initial punctuation marks before we do that.
Toward this end, we use the range’s MoveWhile method. This method keeps moving the collapsed range while it finds any of the indicated characters.
Cset is an option that stores a plain text string of characters to skip (different methods use Cset different ways). We include a left square bracket, an open parenthesis, and a straight single quote. Since these are already plain text, we can just include them in straight double quotes to define our character set. We then add other relevant characters using a literal plus + sign to mimic adding numbers (an ampersand & also works).
We include the two simple functions LeftDQ and LeftSQ that define single and double quotes since they are different characters in Word for Windows or Mac. Similarly, Chr(…) is a standard VBA character function. The number 34 corresponds to a straight double quote character. We use the Chr(…) function since we defined the initial Cset string using straight double quotes, so we need a way to specify it as a search character also.
Extend range over to a comma or semicolon
We’re positioned at the start of the sentence, possibly just to the right of some initial sentence punctuation marks. We now need to extend the range forward to the next comma.
Since we’re also allowing a main clause, it takes almost no extra work to include a possible semicolon.
A range has Start and End positions (among other properties), so MoveEndUntil literally moves the End position of the range until it finds any character listed in the Cset option. The Start position of the range is unchanged, so any text in between the Start and End positions are now spanned by the range variable r.
In the extended content later, we’ll further verify whether a comma or semicolon exists before committing to this step.
Include the comma and trailing spaces
Now we need to include the comma or semicolon in the range. We use MoveEnd because we know we’re extending the End position of the range by one character.
We further include any trailing spaces to mimic how Word works when automatically selecting text. Using the MoveEndWhile method is appropriate because we want to keep extending the End of the range while the next character is a space.
The character set Cset is just a space. This step is probably redundant in most cases, but I like to be clear.
In the extended content, we’ll further include a possible conjunction if we happen to be spanning a main clause instead of an introductory phrase.
Final Macro
Putting the steps together, we have our function to get the range of the sentence introducer:
We should add some checks to detect whether the target range exists or whether the sentence actually has an introductory phrase or main clause before continuing. Further checking for a possible coordinating conjunction joining two independent clauses is a convenient addition even if a tertiary effect. See the extended content below for both topics since the explanations stretch out a little.
Improved Function
Now we include two different validation checks, so we catch the problems before continuing (often resulting in our macro or function crashing). We also incorporate any coordinating conjunction between two independent clauses just to be complete.
Validation checks
We need to view any input from the user with suspicion. This is really “programming 101” (not that I call what we’re doing “programming” very often; just don’t tell anyone what we’re really doing).
Granted, in our current context we’re probably the ones both creating the macro and giving it any input, but we’re not perfect either (right?). In the interest of saving ourselves some trouble later, we should add some validation checks to our function.
First, what if the target range isn’t a valid document range? Second, what if the sentence doesn’t contain an introducer? After validating both, we'll feel more confident we’re working with a valid sentence range that includes an introducer.
Check if target range is valid
Is the given rTarget range assigned to a range in the document? It sounds like a non-sensical question at first. Of course, it refers to a document range—
Not always.
The rTarget argument given by the user could be Nothing. “Nothing” is a literal value assigned to an object when it is not yet assigned to anything in the document. We check this with the condition:
“Is” is used to compare whether two objects in VBA (ranges are objects with properties and methods) refer to the same thing, but we usually use Is in this validation context. This condition is True if the target range is not yet assigned to something in the document. Our conditional statement for the range validation check is roughly:
Invalidate return result if target range is invalid
If so, we need to tell the user that the target range was invalid. While we can’t return that specific information, we can at least set the return result to Nothing (using the function name).
We must Set a range even when assigning Nothing since a range is not a plain value like a number or text. When the user notices the Nothing value of the returned range, they know something went wrong.
Exit if the target range is invalid
Since we know there is no valid result in this case, we immediately exit the function:
Putting it together, our fool proof check to exit if we’re not given a valid target range is:
Validate introductory phrase or main clause
If the target sentence has no comma or at least a semicolon, it cannot have an introducer. If one of these characters doesn’t exist in the sentence, we can immediately exit the function.
How do we check this?
We previously introduced the Like operator (in a different context) which takes search text and searches it using a provided search pattern. Both the search text and the search pattern can be defined explicitly with double quotes or refer to a string variable. The basic format of a search looks like:
The result is a True or False (Boolean) value indicating whether the pattern was found in the searched text. We can then use this result in a conditional statement to decide whether we exit the function in our case.
What text do we want to search?
We need the full first sentence regardless of whether the initial target range spans all of it. To get it, we use the Sentences collection of the target range.
Sentences is a collection of sentence ranges in the rTarget range, and First references the first sentence range of that collection.
We need the text to search, so we use that range’s Text property. To make the following statements easier to read, we’ll declare a plain text variable and store the sentence text in it.
Strings are just values (mostly) in VBA, so we can just use an equals = sign to store it in the sFirstSentence variable.
What is our search pattern?
We want to know if a comma exists in the sentence. Obviously, our template search pattern is something like “,”, but we need to allow for any characters on either side (otherwise it looks for only a comma). A wildcard asterisk * character allows any or no characters in that search pattern position, so our search pattern becomes “*,*” since we want to allow any characters on either side of the sentence text.
Combining the pattern with our Like statement, we have:
Exit if we do not find a comma
Our conditional statement is roughly:
Like will tell us if a comma is found, but we only want to exit the function if a comma is Not found. Again, the Not operator reverses True or False values of the Like search result.
If no comma is detected, we need to tell the user no introducer was found. Since we haven’t checked anything yet, there is no introducer range to return. In VBA terms, this means the range is literally Nothing. As mentioned earlier, Nothing corresponds to an object not assigned to anything in the document yet.
Returning this result is somewhat of a judgement call. You may prefer to return an empty range at the beginning of the sentence as your indicator of no introducer being found. In that case, the check would need to come after the sentence start is determined. We'll continue with returning Nothing in this function when no comma is found.
We then exit the function using:
Putting it together, our fool proof check to exit the function if no comma is found is:
Exit if we do not find a semicolon
If we’re allowing for main clauses as a special case, we should probably also check for a semicolon just to be complete. Like finding a comma, the search pattern for finding a semicolon anywhere in the search text is “*;*”.
Either case could be valid for us to proceed with the function. This condition corresponds to an Or statement. Putting it together with the comma check, we have:
This compound condition tells us if either a comma or semicolon is found in the searched text. We only want to exit the function if one of them is not found, so we again need a Not operator to reverse the True or False result.
We included parentheses around the compound condition because we want to reverse the overall result. If we don’t like how that looks, use Not with each condition instead, but we must switch the Or to an And to make the new compound condition work out correctly.
There is some general Boolean logic to explain this compound condition, but basically for our case, we only want to exit the function if both a comma and a semicolon are missing. That corresponds to an And operator.
Combine foolproof checks
It is convenient to combine the target range validation with the punctuation checks all into one conditional statement. After all, both sets of conditions give the same result of Nothing and then exit the function. Putting them together looks like:
Remember an underscore _ continues a line, so the VBA editor looks at it as a single line as far as the command is concerned, but humans can read it more easily.
But be careful when using objects ...
We're okay here but be careful if the latter conditions also involve the target range variable. Unfortunately, VBA isn’t smart enough about how it checks the conditions to do this. All conditions are evaluated regardless, so if rTarget happened to be Nothing, another other tests referencing a property or method of rTarget would cause an error. For example, it would not be able to reference a Sentences collection of an invalid range. The collection would not exist since the range is not yet assigned to anything in the document yet, and the function would crash. Not nice.
Regular Visual Basic has some extra Boolean operators, specifically OrElse for our case here, to make this easier; but VBA does not inherit it which is a shame.
What's the solution?
If the following validation conditions depend on the target range, just do the other tests strictly after the target range is validated (we know it is assigned to some document range).
Including a conjunction in the range
Let’s account for a possible coordinating conjunction if we’re spanning a main clause. Of course, this is optional, but I prefer it, so we don't need to manually remove the conjunction if we delete the introducer.
Once the function has spanned text up to and including a comma, we can check whether the next word after that position is a coordinating conjunction.
Get the next word text
To make the following easier to read, we define a plain text variable sWord.
We need the literal next word after the working range r. Ranges have a Next method that returns a range after it depending on whichever Unit we specify.
We needed to include the Unit inside parentheses, so we could reference the Text property of the next word range.
Store the text in the sWord variable.
Trim any text spaces
The word Text will probably include a trailing space (it is the default action for Word) which will confound our checks for a conjunction below, so we should trim any spaces off the Text. VBA has a standard function called Trim(…) to do just this.
We can assign this trimmed text back to the same word variable sWord just to keep things simple.
Technically, two more standard VBA functions RTrim or LTrim exist to trim spaces off only the right or left sides, respectively, but I’m just using the regular function.
More concise trimmed word text
Alternatively, we could have skipped the intermediate assignment to sWord and saved a step.
I prefer this version, but it does look messier if you’re not used to looking at it.
Check for a coordinating conjunction
Check if the next word is a coordinating conjunction using a conditional statement something like:
Basically, we check if sWord contains the text of any of the following: “but”, “and”, “yet”, “or”, “nor”, “so”. If so, we extend the End of the working range over the word.
Since we know exactly what the next word is and what the word should be, we can just compare our stored word to the conjunctions directly using an equals = sign. Remember in VBA, using an equal with strings requires an exact match—no missing or extra characters, and even the case of every letter must match.
Any of the conjunctions could match (result in a True condition for the search), so this corresponds to a series of Or statements all chained together.
Remember Or results in True if any of the conditions are True.
Extend the working range over a conjunction
After we detect a conjunction, we move the End position of the working range forward over that word.
The word Unit constant wdWord is from a table of standard Word Units. Combining this with our conditional statement, we have:
Revised introducer function
This version includes some validation checks as well as incorporating a trailing coordinating conjunction after an independent clause.
For those newer to programming, the advantage of creating our working macros using this function is all four versions inherit our new improvements without any extra work.
Working macros using the function
We need a plain macro without any parameters if we plan to assign it to a keyboard shortcut, a custom ribbon button, or a quick launch button. This restriction does not apply to functions or macros called from a Dragon Professional script (which is a separate paid application).
Now that we have the base function, let’s apply it to several relevant macros. Specifically, we will select or delete the sentence introducer, or we will cut or copy it to the clipboard.
We probably won’t assign all these the keyboard shortcuts in Word since there are only so many key combinations available, but it’s nice to have them easily accessible for any simple voice commands we might set up in Dragon Professional scripting, but that is a different lesson.
Declare a working range
In each of the macros below, we define a working range.
Since r is declared in each macro separately from the above function, it is considered a completely separate variable by VBA despite the similarity in the variable names (in fancy terms, they have a different "scope"). VBA will not get them confused.
Get introducer range
In each of the macros below, we need the range of the introducer at the cursor location. We defined the previous GetSentenceIntroducerRange function to accept a target range to keep things general. Now we need to give the function the range of the current sentence. The user’s current selection or insertion point in the active document is literally called Selection (with a capital "S") in VBA, but it is not strictly a Range, so we need to reference its Range property.
Then we “pass” this range to the GetSentenceIntroducerRange function and Set it equal to our above working range r.
The first steps above are the same for every macro below which is part of the reason for creating the workhorse of a function.
Run the appropriate method
Now that the working range corresponds to the sentence introducer, what we do now depends on the specific macro. Pick the corresponding method for the macro.
Validate the range (again?)
We should probably validate whether the returned range exists, so we like in the extended content above, we need to check whether we were given Nothing by the function.
All the validations may seem annoying. You could take your chances and omit them, but it will probably pay off at some point in the future. (I used to skip style validations, for example, but those macros kept crashing in other documents until I got tired of it ... then I started validating the styles.)
In these macros, the chance of encountering a problem with Selection.Range being invalid is very small. If it is invalid, you have more serious problems with your document. However, since these macros deal with an introductory phrase, there is a very reasonable chance once does not exist in the current sentence.
Since the command is so short, we can use the more compact notation for two of the macros. We’ll use the Select version as an example.
Capitalize first word of sentence
For the Cut and Delete versions of the macros, it is convenient to have the macro go ahead and capitalize the first word of the sentence for us. May as well let VBA save us the manual work.
Since we just removed the introducer, the range is now positioned precisely at the new first word of the sentence. We can control the case of the word using the Case property of the working range r.
The wdTitleWord character case constant if from a standard word case table. Another other case constant wdTitleSentence also make sense, but wdTitleWord is quick and easy considering our range r is already at the beginning of the sentence. We do not need to extend the range over the word since where already positioned there.
Of course, we only capitalize the word if we have a valid range, so it would go inside the If statement.
Working macros
Putting it together, we get our initial two intended macros.
All four macros can be useful at various times, and they take very little effort to implement after we’ve set up the workhorse function.
With the extra validation check, these macros look a little more like programming, but it’s safer to make sure the range exists before trying to use it.
Watch out for (figurative) shortcuts
If you’ve been paying close attention, you might be tempted to skip declaring the working range r. It does make the macro simple. The idea is the function already returns a range:
So why not just go ahead and select it here by referring to the Select method of the returned range.
Why bother with the extra step of storing the result in a range variable r only to use r.Select in the very next step?
Almost a gold star …
What could go wrong?
Honestly, this should work 87.238% of the time (ha, ha … math humor). If that’s good enough for you, then go for it. Here is the revised uber-simple macro:
Isn’t that beautiful simplicity?
Come on … admit it.
The problem is it crashes when the range isn’t valid. The way I coded the improved function, we get a result of Nothing if no sentence introducer is found. That makes sense, but it causes this simplistic version of our macro to crash. You could modify the function to return an empty range at the beginning of the sentence instead, so this simpler version of the macro doesn’t crash. However, this could cause side effects if you're not careful since methods like r.Delete would still do something, so you would need to account for the change in other places also. I'm not trying to scare you. Just trying to keep you safe.
It’s a judgment call. I just liked the clarity of the function returning a Nothing result to tell me it didn’t find anything.
Power of functions
Suppose you come back a week later and decide you also want to implement this introducer stuff for the Word spike (let me know if you use the spike). It’s almost trivial given our setup.
Remember Word's spike automatically cuts the text from the document, so I also included the extra step of capitalizing first word. I think the spike is more useful in VBA than in actual writing (at least for authors), but your mileage may vary.