Heading

This is some text inside of a div block.
This is some text inside of a div block.
This is some text inside of a div block.
min read

Get any sentence range

Word • Macros • Editing • Functions
Peter Ronhovde
39
min read

We create a function to get a sentence range around on a given location in the document. It naturally accounts for double quotes or parentheses based on whether they are present on both sides of the text.

Thanks for your interest

This content is part of a paid plan.

Get any sentence range

Editing functions and macros should work intuitively and not make us clean up the text or a selection after they're done. With this in mind, they should should play nice with common document elements, and dialog is … let's use a fancy word, ubiquitous in fiction.

A function tackles a specific task which is generally useful in other macros, and finding a sentence range is a general enough task that it makes a good candidate function. Such a function should intelligently provide a sentence range while taking the dialog text into account if it's present. That is, should a sentence range include the dialog tag or just the regular sentence text? A similar argument applies to the parentheses in other documents.

Create the empty function

Open the VBA editor with Alt+F11 in Word for Windows (or Option+F11 on a Mac) and create the following empty function. It's much the same as others we've created except we add an optional object parameter.

Function GetSentenceRange(Optional TargetRange As Range = Nothing) As Range
' Returns the range of the sentence around the given target range
' Defaults to the current position in the document
' Include function steps ...

Set GetSentenceRange = Nothing ' Placeholder result
End Function

All functions require the Function keyword followed by a unique name. Any external data are listed as parameters inside the parentheses. Functions always return a value, so we need to specify its returned data type at the end. The initial comment lines clearly describe what the function does.

What is the parameter?

We want the sentence range around a given target range, so we specify a parameter name TargetRange and give it a data type As Range.

TargetRange As Range ' Not done ...

A common use case is just getting the range around the current cursor position, so we'll make the argument Optional and assume the current position if it isn't given when the function is used.

Optional TargetRange As Range ' Still not done ...

Optional parameters require a fixed default value. Since a Range is an object, the only valid default value is Nothing.

Optional TargetRange As Range = Nothing

Nothing is the value assigned to objects not yet assigned to a valid document element. See our introduction to functions and subroutines article for more information about optional parameters.

Other parameters?

If we want the function to be even more general, we could include two more optional parameters to control whether the function trims any dialog tag or parentheses. These are omitted from the current function for brevity.

What is the returned result?

We’re returning the sentence range, so we add As Range at the end of the header line. The empty function above includes a placeholder result assignment to be clear.

Set GetSentenceRange = Nothing ' Placeholder assignment

Nothing is a value VBA assigns to object variables not yet associated with a valid document element (or whatever it represents). The returned result is assigned to the function name, but it's temporarily set to Nothing since we do not yet have a sentence range.

What’s the plan?

Roughly speaking, the function should work as follows:

  • We may begin the function with a given target range around which we want to know the sentence range.
  • If a range is not given, the function defaults to the current selection or insertion point position in the document.
  • Trim any paragraph marks and spaces from the right side of the range.
  • Trim parentheses if they occur only on one side of the range. Similarly, trim any dialog tags or a double quote if both left and right double quotes are not present in the range.

Word automatically includes any trailing paragraph marks in automatic sentence range extensions, but we remove them because they shouldn’t be part of a sentence range. Spaces are also trimmed because we need to check for parentheses on either side which is easier to do after removing the spaces. The logic checking for double quotes is more complicated because we're further allowing dialog tags in the sentence range when appropriate.

Assign the initial working range

We need a separate working range variable inside the function. If we used the target range parameter directly, any changes to it would affect the argument on the outside of the function because object parameters are always passed to the function as a memory location (called "by reference").

Declare the working variables

We declare a working Range variable which we call simply r to make it easier to type.

' Declare several working variables
Dim r As Range

Dim is the VBA keyword to declare a variable. We'll also declare several plain text variables to clarify the steps below.

Dim sFirst As String, sLast As String, sText As String

Each variable needs its own data type, or it will default to a Variant type than can store anything. Using a variable would not be a problem in this function, but specifying a String type is clearer. Each variable is explained when it is used. Often variables are declared just before they are used the first time, but it seemed cleaner to group them.

What is the initial working range?

The initial working range depends on what was provided to the function as the target range. A rough conditional statement looks like:

' Assign an initial working range based on the target range
If target range is not assigned to a document range then
' Target range is invalid, so assign the initial document position
' or selection as the working range
Otherwise
' Target range is valid, so assign a duplicate to the working range
End the initial range assignment

We need to know whether the target range is valid. It may seem awkward to include the invalid case as the first condition, but this structure is easier to read later.

What is Nothing?

No, we're not delving into metaphysics or vacuum energy fluctuations (although, that's cool too).

VBA literally assigns a value called Nothing to any object not yet assigned to a valid document element. If a target range argument is not provided when this function is used, the target range parameter will default to Nothing.

Check for a default target range

Since the target range is an object parameter, we need to use the keyword "Is," rather than an equals = sign, to compare it to Nothing.

' Is the target range assigned to a valid document range?
' True indicates an unassigned target range
TargetRange Is Nothing

This is a True or False (Boolean) condition which we can use in a conditional statement to make a decision. More specifically, True indicates the target range is not assigned. This condition also catches an invalid target range argument if the user provides an unassigned range variable.

Assign the default working range

When the target range is Nothing, we assign the working range based on the Range property of the Selection.

Set r = Selection.Range

Set is required in VBA when assigning any object variables. The Selection is a VBA object, kind of like a special Range, that represents the current selection or insertion point in the document. We need to refer to its Range property since the Selection is more than just a range of document content. In Word, an insertion point is just an empty Selection spanning no content meaning it's Start and End positions in the document are the same.

Assign the given target range

When the target range is valid, we use the Duplicate method to create a copy for our working range.

Set r = TargetRange.Duplicate

We need to duplicate the target range argument into a separate variable, or the two variable names will refer to the same range. That may seem like what we want with a copy, but they will not be independent range variables. Any changes made to the working range inside the function would still affect the target range variable outside the function. Using Duplicate ensures they are independent of each other.

Extend the range over the sentence

After the above assignments, the working range r is valid in either case, so we can proceed. We now extend it over the sentence range at its location.

Collapse the range (optional)

If the working range spans any text, the expansion below could extend the range across multiple sentences or even paragraphs depending on the starting range. While it's not inherently wrong, we can avoid any logical issues by simply using the Collapse method.

' Collapse the starting range to avoid any extension issues
r.Collapse ' Optional

How does this help?

After the collapse, we know the working range is empty and positioned at the beginning of the initial target range. Only one sentence is possible for the following expansion step.

Allowing more than one sentence

If you prefer to allow multiple sentences, just omit the collapse command. Neither approach is wrong, but it could result in some extra gotchas. For the remainder of the function, we assume a single sentence range.

Expand over the sentence range

Now, we Expand the range over the sentence.

' Extend the range both directions over the sentence
r.Expand Unit:=wdSentence

Expand accepts a Unit option. The default unit is by word, so we assign the sentence constant wdSentence (from a table of Word constants). All option assignments require a colon equals := symbol. The Expand command extends the range both directions to the respective beginning or ending of the given unit but no farther.

Omit any paragraph marks (suggested)

By default, Word includes an ending paragraph mark in an automatic sentence range extension (or selection) if it exists after a sentence. In fact, Word will include every trailing paragraph mark (empty paragraphs) until it encounters some regular text. This behavior isn’t intuitive, so let’s get rid of them.

The majority of the time, only one paragraph mark will be spanned, but we can remove all of them with a single command using the MoveEndWhile method.

' Remove all paragraph marks from the sentence range
r.MoveEndWhile Cset:=vbCr, Count:=wdBackward

This command literally moves the End position of the range backward or forward in the document. It requires a set of characters to include or exclude (depending on the direction) in any order or combination. A paragraph mark vbCr is a special character from a table of miscellaneous constants, so we assign that character to the Cset option.

An additional Count option specifies how many characters to check. The default is any number of characters forward (up to about a billion). We need to retract the End position backward, so we instead assign the constant wdBackward to the Count option. The two options must be separated by a comma.

Why omit paragraph marks?

A paragraph mark is a paragraph mark. It's in the name. In my opinion, omitting them from a sentence range is more intuitive and consistent with the meaning of a sentence within a paragraph.

Trim any ending spaces

Word automatically extends ranges over any trailing spaces. Detecting any parentheses on either side of the range will be easier if we trim any spaces. We again use the MoveEndWhile method.

r.MoveEndWhile Cset:=" ", Count:=wdBackward

This time, we assigned a space " " to the character set option. The double quotes are required to indicate a literal space character as a plain text string.

Wait a second …

This uses the same MoveEndWhile command as before, even moving backward, except it trims any spaces.

Combine with trimming any paragraph marks step

We can just combine this command with the previous one by including a space along with the paragraph mark in the character search string.

' Combine the above two movement commands
r.MoveEndWhile Cset:=" " + vbCr, Count:=wdBackward

We concatenate the two characters together using the plus + sign to create a revised Cset search string. This just jams the two strings together to make a new, longer string.

A possible gotcha is this revised command will trim the two characters from the end of the range in any combination, but this behavior is still reasonable for this function.

Trim any starting spaces (optional)

Most automatic range extensions (or selections) do not include any preceding spaces, but it can happen for the first sentence of a paragraph. Just in case, we'll trim them using the MoveStartWhile method.

' Trim any spaces at the start of the range (unusual)
r.MoveStartWhile Cset:=" "

MoveStartWhile works just like MoveEndWhile mentioned above except it moves the Start position of the range. The default Word behavior for including any preceding spaces seems unintuitive, so if these spaces are present, we will not restore them to the working range as part of the function result.

Allow parentheses

Accounting for parenthetical text is easier than adjusting the sentence range for dialog, so we'll begin by intelligently selecting a sentence with or without any parentheses. More specifically, we'll exclude a parenthesis if it occurs only on one side of a sentence, specifically at the beginning or ending of the range. We'll extend the ideas below to return a general sentence range that may include dialog text.

We are not attempting to identify parenthetical text alone as is done in a separate macro. This function focuses on sentence ranges where the sentence may or may not be inside parentheses.

Examples of parenthetical text ranges

For illustration purposes, let's assume another macro uses this function to identify the sentence range and then selects the range. What are we expecting?

No parentheses in the text

If we're spanning a regular novel sentence without any parentheses, the selection would be:

This is a regular sentence in a document. Another regular sentence follows that one.

The function should just return the individual sentence range like normal, so the steps to handle the parentheses shouldn’t cause any problems with the regular case.

Single sentence inside parentheses

If the entire sentence is parenthetical text, our function should span all of it including the parentheses on both sides.

A sentence precedes some text. (This is a whole sentence of parenthetical text.) The text continues afterward.

Parentheses inside a sentence

If the sentence just includes some parenthetical text, this function should not adjust the sentence range based on it.

A sentence says something important. This sentence includes some parenthetical text (this is some text inside parentheses). The text continues afterward.

The sentence range is not affected by typical parenthetical text.

Multiple sentences of parenthetical text

If the text consists of multiple sentences, then the function should just return the sentence range inside the parentheses.

(This is the first sentence of some parenthetical text. Another sentence of text follows it inside the parentheses.)

Regardless of which sentence is selected, it would have at most one parenthesis bordering it. The function should span only the current sentence range and exclude the parenthesis.

Trim parentheses logic

A rough conditional statement to detect a parenthesis only on the left side of the working range would look like:

If only an open parenthesis is found then
' Found only an open parenthesis, so trim it from the range
End the open parenthesis detection

Unfortunately, the open parenthesis test must be performed by itself because it will be trimmed from the left side of the range. The close parenthesis test is separate.

If only a close parenthesis is found then
' Found only a close parenthesis, so trim it from the range
End the close parenthesis detection

For both tests, we need to know the first and last characters of the range, but the respective conditions are different.

Check whether to trim an open parenthesis

We'll start with the open parenthesis and adapt it for a close parenthesis check.

Get the first and last characters

We need to get the characters at each end of the range using the Characters collection. The First property gives use the range of the first character in the range.

r.Characters.First ' Not done ...

We need the text for the comparisons below, so we reference its Text property. We then store this character in a string variable sFirst.

sFirst = r.Characters.First.Text

Similarly, we get the Last character of the range and store it in a conveniently named variable.

sLast = r.Characters.Last.Text

Presumably, the working range r is not empty.

Open parenthesis condition

We check whether the first character is a left double quote.

' Is the first character an open parenthesis?
sFirst = "("

This statement will be interpreted as a True-False (Boolean) value when it's used in an If statement to make a decision. The overlapping notation between assignments and conditions in VBA is unfortunate, but we’re stuck with it.

Missing close parenthesis condition

For the second condition, we need to know whether the close parenthesis is missing.

' Is the last character not a close parenthesis?
sLast <> ")"

The not equals <> symbol literally reads as less than or greater than which is … not equal to something. For text, it checks whether the two strings are not exactly the same.

Compound parentheses condition

If the range only contains an open parenthesis, both of the above conditions must be True. We use “And” between them to create a compound condition for our budding conditional statement.

If sFirst = "(" And sLast <> ")" Then
' Remove the open parenthesis from the range ...
End If
Trim the close parenthesis from the range

The command to remove the open parenthesis from the left side of the range is the MoveStart method:

' Trim a single character from the left side of the range
r.MoveStart

As its name implies, the MoveStart command moves the Start position of the range. The default movement is forward by one character. That is exactly what we need at this point, so we can omit both the Unit and the Count options. The command does not change the End position unless Start exceeds the End position.

Since the If statement is relatively simple, we can condense it to one line.

' Check whether only an open parenthesis exists and remove it if so
If sFirst = "(" And sLast <> ")" Then r.MoveStart

Check whether to trim a close parenthesis

Similarly, we may need remove close parenthesis. We need to make sure the open parenthesis is not present at the start, but a close parenthesis is present at the end. If so, we trim the close parenthesis from the range.

If sFirst <> "(" And sLast = ")" Then
' Remove the close parenthesis from the range ...
End If

Trim the close parenthesis from the range

We can trim a close parenthesis using the MoveEnd method.

' Trim one character from the right side of the range
r.MoveEnd Count:=-1

The MoveEnd command moves the End position of the range without changing the Start position (unless End precedes Start). The negative Count value moves backward in the document by 1 Unit. The default movement unit is by a character, so we can omit the Unit option.

The If statement to remove a close parenthesis, if appropriate, is:

' Check whether only a close parenthesis exists and remove it if so
If sFirst <> "(" And sLast = ")" Then r.MoveEnd Count:=-1

Allow dialog

A typical novel contains thousands of lines of dialog at about half of the total word count. If we're manipulating sentences, the macros should properly interpret whether it's a regular sentence, all dialog, or just one of several sentences inside the double quotes. We want this function to perform the grunt work when identifying the appropriate sentence range. The above section covers a simpler solution for parentheses which are more likely in other documents.

Use double quote constants

To make this function easier to read, we’ll use the double quote constants mentioned in a separate article. We'll refer to the left and right double quote characters as LeftDQ and RightDQ, respectively.

If you prefer not to use module-level constants (just constants declared at the top of the macro file outside any function or subroutine), copy and paste the constants into the function somewhere near the top or type and copy the actual text characters from a typical Word document. The latter approach is the most direct, but the characters are similar to straight double quotes, so it's easy to misread the strings.

Example dialog text selections

For sentence variations including dialog, we have several possibilities. For illustration purposes, let's view the function result as a selection.

No dialog in the text

If the sentence is just regular novel text with no dialog included, just span the sentence as normal.

This is a regular sentence in a novel. Another regular sentence follows that one.

Single sentence of dialog

If the entire sentence is dialog, our function should span all of it including the double quotes on both sides.

Some regular text precedes a dialog sentence. “This is dialog text inside a pair of double quotes.”

Multiple sentences of dialog

If the text consists of multiple sentences of dialog, then the function should just return the current sentence inside the double quotes.

This is the first sentence of some dialog text. Another sentence of dialog text follows it inside the double quotes like this.”

Regardless of which sentence is selected, it would have at most one double quote bordering it. The function should span only the current sentence range and exclude the double quote.

Sentences with dialog tags

If the text contains any dialog tags, the same ideas from above apply, but we need some extra work to detect the respective double quote inside the sentence.

Dialog tag appears with a single sentence

The dialog tag often follows the text.

Harry takes an action. “This is an entire sentence of dialog text with a dialog tag on the right,” he said.

The dialog tag could also precede the text.

Harry whispered, “This is the first sentence of some dialog text with a dialog tag on the left.” He does something related to the scene.

In either example, the function should include the dialog tag in the sentence range.

If the dialog tag is inside the sentence, we still select the whole sentence.

“This is the first sentence of some dialog text with a dialog tag in the middle," Harry whispered, “and some more dialog text follows it.” He does something else related to the scene.

This variation takes advantage of a side effect our text searches in this function. It isn't checking for the number of left or right double quotes, so the validations just want to know they both exist.

Dialog tags with more than one sentence

The dialog could include more than on sentence. In this case, we only include the dialog text.

Monica whispered, “This is the first sentence of some dialog text with a dialog tag on the left. Something else is said between the double quotes.” He does something related to the scene.

What about action tags?

As shown above, action tags (any character action taken during dialog) are treated like normal sentences because they are. They are not dialog text in the proper sense.

It gets a little tricky

See where this gets tricky?

We need to the detect the differences and span the range accordingly for every case (without messing up the others). Also, see the gotchas below for an exception where it gets even trickier.

Trim double quotes logic

A rough conditional statement to detect a double quote only on the left side of the range would look like:

If only a left double quote is found then
' Found only a left double quote, so trim all text up to and
' including it from the range ...
End the left quote detection

Unfortunately, the left double quote test must be performed separately from the right double quote text because the trimmed text is different for each case.

If only a right double quote is found then
' Found only a right double quote, so trim all text including
' and after it from the range ...
End the right quote detection

In the earlier parenthetical conditions, we only needed the first and last characters of the range, but dialog can include tags. The generality makes the logic messier, but it follows a similar pattern.

Check whether to trim a left double quote

We'll start with a left double quote and then adapt the logic for a right double quote.

Is this text Like the other text?

The simplest way in VBA to detect some text within a another longer string of text is probably using Like.

' Example use of Like for a text search
SearchText Like SearchPattern

For Like, we provide text to search on the left and the search pattern on the right. Both are plain text strings. The search text is often stored in a String variable, and the pattern is often just given in double quotes. Although, more complicated patterns might be stored in a String variable. Search patterns can be somewhat general but not as complex as we can do with an advanced Find search.

Why use Like?

Like automatically returns a True or False (Boolean) value that we can immediately use in a conditional statement to make a decision about our sentence range. Other commands such as MoveUntil, Find, or even a standard string function InStr(…) would also work, but each is a little more complicated than what we need for this function.

  • The MoveUntil method would normally be a good solution since we're already working with a Range variable, but in this function, the decision logic would require more steps to avoid any gotchas. It would require an extra working range variable, and the command could the move it outside the original sentence.
  • Word Range variables include a Find property, but it's like taking a hammer to a fly for simple text searches. We need to be clear about several Find settings to avoid any gotchas, and we're only searching for a single character.
  • InStr(…) is a standard VBA function that searches for a substring in another string. It's perfectly functional (ha, ha, get it … a programming joke), but it's also clunky compared to using Like unless we also want to know where the double quote was found in the text.

None of the above are incorrect, but Like does what we need out of the box since we (mostly) just need a yes or no (True or False) result for our decision logic.

What is the search text?

We're looking for a left double quote in the range text, so the search text is found using its Text property.

Dim SearchText As String ' Optional
SearchText = r.Text ' Store the working range text

We don't actually need an extra variable, but it makes the searches clearer.

What is the search pattern?

We previously defined a constant LeftDQ, so it might seem like we could just use Like like so:

' Search expression for a left double quote using Like?
SearchText Like LeftDQ ' Does not work

Like compares the SearchText using LeftDQ as the pattern. The problem is it checks whether the search text is exactly a left double quote. Since this rarely happens, we need to allow for other characters on either side. A common wildcard character is an asterisk * which allows any number (zero or more) of plain text characters wherever the asterisk occurs in the pattern. Some special characters like a paragraph mark are not matched by an asterisk.

Left double quote condition

We include an asterisk as plain text in straight double quotes "*" on both sides to allow any characters on either side of the left double quote.

' Is a left double quote anywhere in the search text?
SearchText Like "*" + LeftDQ + "*"

We concatenated the asterisks with the LeftDQ character using a plus + sign which just jams them together into a three-character string.

We could also use the plain text left double quote character search pattern "*“*" with the left double quote character.

' Also works but is easy to misread (not used)
SearchText Like "*“*"

This version is simpler, which is nice, but it could also easily be misread in a text editor. The potential confusion isn't worth the saved characters unless you prefer extra concise steps. Plus, if we happen to delete and retype the left double quote on the inside, it the VBA editor will replace it with a straight double quote. Just step around the mud puddle rather than trying to walk through it while keeping your shoes clean.

Missing right double quote condition

The right double quote pattern is almost the same.

' Is a right double quote anywhere in the search text?
SearchText Like "*" + RightDQ + "*"
Clean up on aisle five (simplify the conditions)

The conditional logic quickly gets messy because we need two conditions for each check. I don't like to bloat a function, but it will be clearer if we define two intermediate variables to store the search results.

' Store the search results for clarity below
Dim FoundLeftDQ As Boolean, FoundRightDQ As Boolean ' Optional
FoundLeftDQ = SearchText Like "*" + LeftDQ + "*"
FoundRightDQ = SearchText Like "*" + RightDQ + "*"

The variables are Boolean data types because Like returns True or False values. I prefer to precede Boolean variables with a "B", but it seemed clumsy here.

Technically, this also avoids doing the searches four times in the conditional statements below, but the extra two searches would literally take less than a millisecond, so it's not the motivation for our function.

Check for a missing right double quote

One catch is we actually want to know if a right double quote is not present, so we precede the condition with a Not operator.

' Is a right double quote not in the search text?
Not FoundRightDQ

Not does exactly what it implies. It reverses the Boolean result that follows it. True becomes False and vice versa. This breaks the Englishy VBA phrasing, but correct is better.

Compound left double quote condition

If the range only contains a left double quote, both of the above conditions must be True. We use “And” between them to create a compound condition.

If FoundLeftDQ And Not FoundRightDQ Then
' Trim all text up to and including the left double quote
' from the range ...
End If
Trim the text up to the left double quote from the range

How do we trim all text up to the left double quote?

Trim the other text from the left side of the range

The MoveStartUntil method moves the Start position of a range based on the characters at that position.

' Trim text from the left side of the range up to a left double quote
r.MoveStartUntil Cset:=LeftDQ

MoveStartUntil works similarly to MoveStartWhile except it trims (or includes if moving backward) all text until it finds any character in the character set. We assign a LeftDQ character to the Cset option. Forward is the default direction, so we can omit the Count option. It does not change the End position unless Start exceeds the End position.

MoveStartUntil stops immediately at a left double quote

Technically, MoveStartUntil will move forward in the document until it finds the character even it moves outside the original range. In this function, we've verified it exists inside the range before using this command.

Also important for the logic of this function, if a left double quote begins the search text, the MoveStartUntil method will find it immediately and not move the Start position. Thus, the following trim step for the left double quote character still works as expected. In other words, we effectively inherit the original, simpler logic used with the parentheses above without needing any messy correction steps.

Trim the left double quote from the left side of the range

Then we can trim the left double quote from the left side of the range using the MoveStart method:

' Trim a single character from the left side of the range
r.MoveStart

The MoveStart command moves the Start position of the range. The default movement is forward by one character. That is what we need at this step, so we can omit the Unit and the Count options.

Check whether to trim a right double quote

We may need remove the text after a right double quote if a left double quote is not present. Phrasing this in a rough conditional statement, we get:

If Not FoundLeftDQ And FoundRightDQ Then
' Trim all text including and after the right double quote
' from the range ...
End If
Trim the other text from the right side of the range

We trim the text after the right double quote using the MoveEndUntil method.

' Trim text from the right side of the range back to a right
' double quote
r.MoveEndUntil Cset:=RightDQ, Count:=wdBackward

MoveEndUntil works similarly to MoveEndWhile except it moves the End position across any text until it finds a character in the character set. We assign a RightDQ character to the Cset option. We need to move the End position backward, so we also assign the wdBackward constant to the Count option.

Trim the right double quote from the range

We trim the right double quote using the MoveEnd method.

' Trim one character from the right side of the range
r.MoveEnd Count:=-1

MoveEnd moves the End position of the range. The negative Count value moves backward in the document by one Unit. The default movement unit is by a character, so we can omit the Unit option.

Extend over any ending spaces

Since Word normally includes spaces at the end of an automatic selection or a range extension, our sentence range function should mimic that behavior. We again use the MoveEndWhile method, but we need to extend over all spaces at the end of the working range.

' Re-include any spaces at the end of the range
r.MoveEndWhile Cset:=" "

We assigned a space " " character to the Cset option. The default movement is forward, so we can omit the Count option. Writers will probably expect this behavior in Word even if they’re not conscious of it.

Return the function result

The working range r now spans the intended sentence range, so we can assign it to the function name as the result.

' Assign the working range as the function result
Set GetSentenceRange = r

Gotchas

We should get in the habit of considering potential problems, or skip to the final macro if you prefer to avoid the muddy details.

What if an initial selection exists?

This should probably be an automatic consideration in any editing function or macro. After assigning an initial working range based on a given (or omitted) target range, we immediately collapsed the range to avoid any logical issues, so no problem exists.

What if we like allowing multiple sentences?

If we omit the above collapse command, which is my preferred approach, the Expand method could span multiple sentences if the initial target range (or selection) crossed a sentence boundary.

Is this a problem?

Allowing multiple sentences is more of an advantage than a problem because we can leverage the function in more circumstances, but being more general also introduces more chances for gotchas that might cause trouble later. While useful, this extension is beyond the scope of this article.

What about the double quotes order?

Like only tells us whether the search pattern found a match. In this version, the function does not validate the order or number of the left and right double quotes. We could pivot to a different approach, but Like was used for its simplicity. The additional work required to snip this small gotcha is not difficult, but it is outside the scope of this article.

What if the working range ends the macro empty?

During the function, we remove several different characters. If the range ends up being empty by the time the macro finishes, is that a problem?

An empty range result by itself would just indicate no sentence was found, so it seems like a valid result on the surface.

The function would end and return the empty range as normal. The result would be a little strange but also reasonable, so adding any extra logic to catch an empty range seems superfluous. In fact, the returned empty range serves as a clue to the user that something did not work as expected, but this function isn’t responsible for correcting any external logical errors. It should just do what it does correctly without returning gibberish.

A similar argument applies to a final range that includes just spaces.

No problem exists here, but that doesn’t mean we shouldn’t consider it when creating the macro because sometimes issues like this can circle around to bite us.

What if the expanded range is only whitespace?

What happens if the expanded range contains only whitespace?

If the function begins with the target range in an empty paragraph or one containing only whitespace, the expansion step will extend the working range over whitespace. Ignoring tabs to keep the logic simple, the subsequent command to remove paragraph marks and spaces will remove all those characters. In fact, it will move the empty range backward to the end of a previous paragraph. It's an unintuitive result, so we need to correct for it.

We previously created a function to check whether a range contains only whitespace. If so, we want to exit the function. A reasonable return value is just the collapsed working range.

' Check whether the expanded range is empty or whitespace and
' return a collapsed range
If IsRangeWhitespace(r) Then
r.Collapse
Set GetSentenceRange = r
Exit Function
End If

The collapsed range will indicate the function encountered some trouble in identifying a reasonable sentence range. This seems better than returning Nothing since Nothing indicates an invalid range rather than an inconclusive one. This conditional statement should be placed after the range expansion but before the step to trim paragraph marks and spaces.

We could alternatively check whether the initial paragraph is only whitespace. It's more direct since the condition can be checked at the top of the function, but it's also a little messier, so I used the above version.

Final function to get a sentence range

Putting it all together, the function to get the sentence range while accounting for dialog or parentheses is:

Function GetSentenceRange(Optional TargetRange As Range = Nothing) As Range
' Return the sentence range corresponding to a given target range
' while excluding any paragraph marks or preceding spaces
' Includes parentheses if they are on both sides of the range
' Includes double quotes and any associated dialog tags if they are
' in the range on both sides
' Default target range is the current document Selection range
' Does not check for double quote order or count

' Declare several working variable (optional)
Dim r As Range
Dim sFirst As String, sLast As String, sText As String
Dim FoundLeftDQ As Boolean, FoundRightDQ As Boolean

' Assign the working range r based on the target range or default
' to the current Selection range
If TargetRange Is Nothing Then
Set r = Selection.Range ' Default to the Selection
Else
Set r = TargetRange.Duplicate ' Use the target range
End If

' Extend the working range over the local sentence
r.Collapse ' Avoid any expansion issues (optional)
r.Expand Unit:=wdSentence

' Check whether the initial paragraph is empty or only whitespace
' and exit if so. Return a collapsed range.
If IsRangeWhitespace(r) Then
r.Collapse
Set GetSentenceRange = r
Exit Function
End If

' Trim any paragraph marks or spaces from the end of the range
r.MoveEndWhile Cset:=" " + vbCr, Count:=wdBackward
' Trim any spaces from the beginning of the range
r.MoveStartWhile Cset:=" "

' Store the search results
sText = r.Text ' Search all text in the range
FoundLeftDQ = sText Like "*" + LeftDQ + "*"
FoundRightDQ = sText Like "*" + RightDQ + "*"
' Trim all text up to and including a left double quote if no
' right double quote is found
If FoundLeftDQ And Not FoundRightDQ Then
r.MoveStartUntil Cset:=LeftDQ
r.MoveStart ' Trim the left double quote
End If
' Trim all text including and after a right double quote if no
' left double quote is found
If Not FoundLeftDQ And FoundRightDQ Then
r.MoveEndUntil Cset:=RightDQ, Count:=wdBackward
r.MoveEnd Count:=-1 ' Trim the right double quote
End If

' Get the first and last characters for testing below
sFirst = r.Characters.First.Text
sLast = r.Characters.Last.Text
' Trim an open parenthesis if no close parenthesis is found
If sFirst = "(" And sLast <> ")" Then r.MoveStart
' Trim a close parenthesis if no open parenthesis is found
If sFirst <> "(" And sLast = ")" Then r.MoveEnd Count:=-1

' Re-include any spaces at the end of the range
r.MoveEndWhile Cset:=" "

Set GetSentenceRange = r
End Function

This function does not specifically return parenthetical or dialog text. Rather, it identifies sentences and validates whether to include any parentheses, double quotes, or dialog tags based on the included punctuation.

This function requires a separate function to check whether a range contains only whitespace. A separate article mentioned how to declare the LeftDQ and RightDQ constants for all macros in a module. If you prefer not to use the double quote constants, a quick and dirty solution is to replace every LeftDQ constant with a literal left double quote character "“" in straight double quotes and Every right double quote RightDQ constant with its plain text character "”". These strings are easy to misread, so the function uses the constants.

What are the uses?

A simpler version of this function was used with the move sentence macros. Where else might it be used?

We also created a delete sentence macro, and we could simplify and generalize that macro at the same time. We could further create macros to select, cut, copy, and italicize (for internal dialog) sentences. These additional macros would be nearly trivial to create using this workhorse function.

Examples of using the function

If we're creating a bigger macro, we can use this function as follows. We'll assume our macro has a valid range variable named SomeRange.

' Assume a working range is already declared in the main macro
Dim SomeRange As Range
' Store the current sentence range in the SomeRange variable
Set SomeRange = GetSentenceRange

Parentheses are not required after the function name GetSentenceRange because no range argument was provided. The target range parameter was declared as Optional meaning we can omit the argument. In this case, we designed the function to assume the working range is the current position in the document.

We could also provide a range already assigned somewhere else in the macro. Without a lot of explanation, suppose we want to get the range of the last sentence of the current paragraph while adjusting for possible dialog or parentheses. We first define a current paragraph variable to keep the commands reasonably short. We then target the last sentence using the Last property of the Sentences collection.

' Range variable declaration is required to pass it to the function
' Paragraph variable is just for clarity
Dim AnotherRange As Range, MyParagraph As Paragraph
' Store the current paragraph for convenience
Set MyParagraph = Selection.Paragraphs.First
' Store the range of the last sentence of the current paragraph
Set AnotherRange = MyParagraph.Range.Sentences.Last
Set SomeRange = GetSentenceRange(AnotherRange)

The parentheses are required around AnotherRange because we assign the function result to a variable. The details are a little trickier than it seems here. The argument must explicitly be a Range variable or some other Range result because we declared the TargetRange parameter As Range.

Since the last character range is already a VBA Range, we could skip the intermediate range assignment.

' Or just provide the sentence range directly to the function
Set SomeRange = GetSentenceRange(MyParagraph.Range.Sentences.Last)

It's a long command, but it's not hard to read. We could even skip the intermediate paragraph assignment, but the command becomes even longer.

Improvements

What could we do better?

Correct for double quote order or count

The above function uses Like for the text searches, so it does not check whether the right and left double quotes are in the correct order. It also does not catch multiple double quotes inside the same sentence. The order issue could be solve using the InStr function along with some messier logic. Catching multiple quotes would require even more steps. Both are good ideas for a foolproof function, but the work is beyond the scope of this article.

Catch sentence punctuation problems

Word's default sentence parsing algorithm will choke on common abbreviations and mistake them as the end of a sentence.

  • Professional titles like Dr. Sally Doe will split a sentence.
  • Latin abbreviations can be problematic like etc. or e.g.
  • Many common abbreviations are also suspect such as Vol. or Inc.
  • Geographical or address related abbreviations like Ave. or D.C. will do the same.
  • Time or measurement based abbreviations like a.m. or ft. still include a pesky period.
  • Names with middle initials will also cause trouble, but these are more difficult to catch, in general.

The list is probably much longer than you realized. We could include steps to correct the sentence range inconsistencies, and they would be neatly contained in the function.

We would need some logic to catch an incomplete sentence and correct the sentence range. The solution will probably stretch out more than you might think. The cleanest way to implement it would be with another function which would then be used with each desired abbreviation, but that's a lesson for another day and probably one for intermediate users.

Some of these may be worth the effort in certain work-related documents if a particular job deals with them frequently. Correcting for all of them every time would be cumbersome which is probably why Word does not do so by default.

Affiliate Links

If you're interested in using Word or another tool related to the article, check out these affiliate links. I may make a small commission if you purchase when using them, but there is no increase in cost for you, and it helps to support this site and associated content.

I've been using Microsoft for Business for commercial use (that's us writers) on one of the lower pricing tiers for years. I get to use my macros, have online storage, and don't have to worry about software updates.