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 the current sentence range

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

We create a function to get the sentence range at the cursor position in the document. It naturally accounts for double quotes or parentheses based on whether they are present on one or both sides of the sentence.

Thanks for your interest

This content is part of a paid plan.

Get the current sentence range

A function tackles a specific task and is ideally also useful in other macros, but another advantage is we can tweak or improve them while keeping any complexity tucked neatly out of sight. Getting the current sentence range is a general enough task that it makes a good candidate function.

It's important for editing macros to play nice with common document elements. For fiction authors in particular, it makes sense for sentence ranges to account for double quotes. That is, if the sentence is all dialog, then the double quotes should be included in the returned range. If the sentence only has a double quote on one side, it is just one sentence in the dialog, so it should be handled separately from the double quotes. A similar argument applies to the parentheses.

Move sentence article series

The previous move sentence article uses several utility functions to streamline the task and correct several spacing issues along the way.

Each function is general enough to be useful in other macros, so they make great additions to an editing macro toolbox.

What’s the plan?

Before throwing some VBA stuff onto the screen, it’s a good practice to think through some of the details. What are the essential subtasks? Are there any potential hiccups?

We won't get everything exactly right, but we’ll begin the process with a plan which is usually better than winging it. Whoa … a life lesson beckons, but I shall restrain myself.

Roughly speaking, the function should work as follows:

  • The macro begins with the cursor anywhere inside a sentence. The sentence could consist of just text, text within dialog, or text between parentheses. No assumption is made about whether we begin with a selection or an insertion point.
  • Assign a working sentence range based on the current selection or insertion point (a blinking I-bar with no text selected).
  • Trim any paragraph marks or spaces from the end of the range.
  • Trim double quotes or parentheses if they occur only on one side of the range.

We trim any paragraph marks because they shouldn’t be part of a sentence range. Word includes paragraph marks in automatically extended sentence ranges (or selections) by default, but doing so is counterintuitive for a sentence. After all, it's called a paragraph mark. We also temporarily trim any spaces because we need to check the punctuation at the end of the working range.

Create the empty function

Open the VBA editor with Alt+F11 in Word for Windows (or Option+F11 on a Mac) and type the following empty function.

Function GetCurrentSentenceRange() As Range
' Get the range of the sentence at the cursor position in the document
' or the first sentence if text is selected
' Include the function steps ...

' Return the sentence range
Set GetCurrentSentenceRange = Nothing ' Placeholder value
End Function

The initial lines inside the function start with a single quote (called a "comment character") to explain what the function does.

No parameters are needed

We want the current sentence range, so no parameters are needed. Instead of receiving any data input, our starting range will be the initial selection or insertion point in the document. The empty parentheses after the name are still required for the function definition.

Assign a returned data type and result

We’re returning the sentence range after properly accounting for double quotes or parentheses, so we need to include the return data type  “As Range” at the end of the header line.

Set GetCurrentSentenceRange = Nothing

Nothing is a value VBA assigns to object variables not yet associated with a valid document element (or whatever it represents). In VBA, the returned result is assigned to the function name, but we temporarily set it to Nothing since we have not yet identified the current document position.

Get the initial sentence range

The most important feature of this function is to get the current sentence range.

Declare the working range variable

We declare a working Range variable.

' Declare the working range simply named r
Dim r As Range ' Optional

Dim is the VBA keyword to declare a variable. We used a simple name like r because we only need one range, and some of the later command lines will be long. At this point in the macro, the variable is not yet assigned to a valid document range, so it has a value of Nothing.

Assign the initial sentence range

We assign the initial working range based on the starting selection or insertion point in the document. The Selection object represents the document selection we see on screen whether any content is selected or not. We refer to its Range property and store that information in our working range variable.

Set r = Selection.Range

In Word, an insertion point just means no selection exists in the document. An empty Selection has Start and End its positions at the same position, but this macro will work even it the macro runs with an initial selection. This assignment stores the extent of the content in the document not the content.

Collapse the working range (optional)

We can avoid any logical uncertainties with the function logic by collapsing the range before doing anything else. The appropriate method is obviously named Collapse.

' Collapse the working range to ensure we get the current sentence
r.Collapse ' Optional

Why collapse the range?

We are now sure the range is empty, so it cannot span more than one sentence. This is more of a logical preference because it gives us a consistent starting point for the rest of the macro. However, the function would easily work across contiguous sentences with no changes while only introducing a tiny extra gotcha. In fact, I prefer the little utility bump from allowing consecutive sentences, so I would omit this step. See the last gotcha below for some additional explanation of the issues involved.

Expand the range over the sentence

We expand the working range over the current sentence using the Expand method.

r.Expand Unit:=wdSentence

The default expansion unit is by a word, so we need the sentence unit constant, wdSentence, from a table of unit constants. We assign this constant to the Unit option using a colon equals := symbol.

How does expand work?

Expand will extend the range both directions until it spans the entirety of the given unit but no more even if the method is run again.

If the above Collapse method is omitted, Expand will extend the initial range both directions to the respective beginning or ending of the nearest sentence. This would naturally work for an initial selection that spanned a sentence boundary. I like this extra perk, so my version would just use Expand.

Remove the paragraph mark(s) from the range

By default, Word includes an ending paragraph mark if it follows a sentence. Of course, this occurs when a sentence ends a paragraph.

In the majority of use cases, only one paragraph mark will be present on the right side of a sentence range, but if any empty paragraphs follow the last sentence, the range will span those as well. This behavior isn’t intuitive, in my opinion, so let’s trim them from the range using the MoveEndWhile method.

' Remove all paragraph marks from end of the sentence range
' But wait, there's more ... we merge it with a similar one below
r.MoveEndWhile Cset:=vbCr, Count:=wdBackward

MoveEndWhile moves the End position of the range across any specified characters. It requires a character set as plain text, and any characters listed in the set will be included or excluded from the range depending on the movement direction. In VBA, a paragraph mark is a special character, vbCr, found in another miscellaneous constants table, so we set the character cset option Cset equal to it.

We need to move the End position backward, so we also assign the constant wdBackward (from yet another constants table) to the Count option. The constant is a large negative number that tells the command to keep moving backward until all paragraph marks are removed from the range (up to a billion or so). The options need to be separated with a comma.

In my opinion, omitting paragraph marks from a sentence selection is more intuitive and consistent with the meaning of a sentence within a paragraph. It's one of the cases where I do not emulate Word's standard actions with my own macros.

Simple get current sentence range function

Putting the above steps together, we have a simpler version of the function.

Function GetCurrentSentenceRangeSimple() As Range
' Get the current sentence range excluding any paragraph marks

' Assign a working range r based on the current Selection
Dim r As Range ' Optional
Set r = Selection.Range

' Extend the working range over the current sentence
r.Collapse ' Restrict to the first sentence (optional)
r.Expand Unit:=wdSentence
' Remove any paragraph marks from the end of the range (preference)
r.MoveEndWhile Cset:=vbCr, Count:=wdBackward

Set GetCurrentSentenceRangeSimple = r
End Function

If you do not care about dialog or parenthetical text, this version works nicely. It encapsulates several steps which we can run as a single step in any macro.

Account for dialog with the sentence range

Macros should work as intuitively as possible not make us clean up the text after they run. For fiction authors, dialog is so common that our sentence manipulation macros should account for it naturally. More specifically, we include the double quotes only if they occur on both sides of the sentence.

A similar effect occurs with parentheses in non-fiction documents. We probably want to exclude a parenthesis if it occurs only on one side of a sentence. For the remainder of this article, we’ll work with double quotes since the logic extends trivially to parentheses.

Use double quote constants

To make the function easier to read, we're using the double quote constants defined in a separate article. These characters are very common in fiction editing macros, so defining them for all macros is useful. In this function, we'll refer to left and right double quote characters as LeftDQ and RightDQ.

Not using straight double quotes

In macros dealing with dialog, we work mostly with left and right double quotes, but a straight double quote could pop up occasionally. Unless the setting is turned off, Word automatically corrects straight double quotes to the "smart" quote varieties making them uncommon in typical documents. It's not difficult to account for all three, but it clutters the macro, so we'll assume only left and right double quotes are used.

Declare double quotes (alternative)

If you prefer to define the double quote characters within this macro, VBA has a standard function ChrW(…) that allows us to get the plain text version of many special characters. Cross reference the desired character(s) in an external standard Unicode character table. Use the assigned number in the character function ChrW(…). The left and right double quote characters are respectively:

  • Straight double quote " → ChrW(34) [not used in this function for brevity]
  • Left double quote “ → ChrW(8220)
  • Right double quote ” → ChrW(8221)

These are special characters but still just plain text. We can assign them to a pair of variables for later use.

' Alternative approach to declare double quote character variables
Dim LeftDQ As String, RightDQ As String
LeftDQ = ChrW(8220)
RightDQ = ChrW(8221)

Copy these assignments somewhere early in the macro. The ChrW(…) function will not work for constant assignments. While they technically return a fixed plain text character based on the given number, it is nevertheless a function call.

What do we do with dialog?

How do we handle the double quotes around a sentence? They could occur on none, one, or both sides of the sentence range.

  1. No double quotes → Span the sentence as normal.
  2. One one double quote (either side) → Extend over the sentence but exclude the double quote. The sentence is only part of the dialog, so we should include only the sentence text in the range.
  3. Double quotes on both sides → Select the sentence including the double quotes. The entire sentence is dialog, so we should logically include all of it.

A similar logic applies to open and close parentheses in place of of left and right double quotes, respectively.

Trim spaces from the right side of the range

Word automatically includes any trailing spaces with the expanded range, so we temporarily trim them from the end of the range using the MoveEndWhile method. Trimming the spaces makes testing for the punctuation marks easier.

' Trim any spaces from the right side of the range (but ...)
r.MoveEndWhile Cset:=" ", Count:=wdBackward

We assigned a space " " to the character set option Cset. We're trimming them from the range, so we move the End position backward by assigning wdBackward to the Count option. This looks familiar … it uses the same MoveEndWhile command as above, even moving backward, except it removes spaces now.

Combine with removing the paragraph marks

We're already trimming any paragraph marks, so we can just combine the two commands by adding a paragraph mark to the space character:

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

We add the two characters together, literally with a plus sign " " + vbCr, to create a revised Cset search string. This is called concatenation which we explained in more detail in a previous article.

Trim spaces from the left side of the range (optional)

The only case where Word will automatically includes any spaces on the left side of a range is when it extends over a sentence at the beginning of a paragraph. It's an unusual case, but it only takes a handful of microseconds to correct for it. We move the Start position of the range using the MoveStartWhile method.

' Trim any spaces from the left side of the range (optional)
r.MoveStartWhile Cset:=" "

The character set option works the same as MoveEndWhile, trimming any spaces from the range, but we omit the Count option because forward movement is the default.

Get the first and last characters

We begin by declaring two character variables for clarity. We define them as strings, but they will only contain a single character.

' Declare the working comparison characters
Dim sFirst As String, sLast As String ' Optional

We use brief variable names, sFirst and sLast, to shorten the upcoming conditional statements. The "s" prefix is just a personal shorthand for a string variable, but VBA does not care as long as we follow its naming rules (no keywords allowed; use only alphanumeric characters or an underscore).

Now, we need the characters at the beginning of the range. We begin by referring to its Characters collection.

r.Characters.First ' Not done ...

The First property returns the character range, so we need to reference its Text property to get the plain text character. We store it in the sFirst variable for later use.

sFirst = r.Characters.First.Text

Similarly, we get the last character of the range using the Last property.

sLast = r.Characters.Last.Text

The range will not be empty in most use cases, but the logic below still handles everything correctly even if the range ends up empty after trimming the spaces or paragraph marks as we did above. If you're especially concerned about it, we provide an extra validation step in the gotchas below.

Do we remove the left double quote?

Currently any double quotes are included in the expanded sentence range. Do we keep or remove a left double quote?

Our goal was to remove a double quote if it only exists on one side of the trimmed working range. We need to be specific with our steps, so we start by detecting a lone left double quote. We've trimmed any preceding spaces or paragraph marks, so we need to compare the first character to a left double quote and the last character to a right double quote.

Tentative quote removal conditional statement

If we structure the decision a little more logically, we might have something like:

If the left character is a left double quote but
the right character is not a right double quote then
' Remove the left double quote ...
End the character checks

That's a doozy, but it gets us closer to a VBA If statement. We just need to work out the conditions.

First character condition

The initial condition is whether the first character is a left double quote. Exact matches for strings are compared with an equals = sign just like for numbers.

sFirst = LeftDQ

For our decision logic, this condition will evaluate to a True or False (Boolean) value in an If statement. It is unfortunate that VBA has overlapping notation between assigning a value and determining a Boolean result (some other languages use double equals == for Boolean values), but we’re stuck with it.

Last character condition

We only remove the left double quote if a right double quote is missing at the end of the range. That is, the last character is not a right double quote.

sLast <> RightDQ

The not equals <> symbol is the plain text computer version of the mathematical not equals symbol ≠. The VBA version literally reads, less than or greater than … which is not equal to something. For a single plain text character, the not equal to symbol checks checks whether it is not the exact same character.

See … programming is easy—okay, maybe that's going too far for most readers, but at least the symbol makes sense. If the characters are not exactly matched (even case matters for alphabetic characters), they are not equal.

Compound quote removal condition

Both of these conditions must be True before we remove the left double quote. This corresponds to an And operator.

sFirst = LeftDQ And sLast <> RightDQ

And takes two Boolean conditions and returns True only if both are True. Otherwise, it returns False.

Remove the left double quote

If the compound condition is True, we remove the left double quote. The left double quote is a single character on the left side of the range, so we only need to move the Start position of the range. The appropriate method is MoveStart.

r.MoveStart ' Remove the left double quote

The default movement unit is by character, and the default number of units is one, so we can omit both options.

Left double quote removal conditional statement

Combining the above conditions along with the character removal command, the If statement is:

' Remove the left double quote if it only appears on the left side
If sFirst = LeftDQ And sLast <> RightDQ Then
r.MoveStart
End If

For more explanation, see a previous introduction to conditional statements in VBA.

It's relatively simple but a tad messy, so we can condense it into one line.

' Remove the left double quote if it only appears on the left side
If sFirst = LeftDQ And sLast <> RightDQ Then r.MoveStart

The line is a little long and dense, but it's more concise than the three-line version.

Do we remove the right double quote?

Similarly, we may need remove the right double quote. The basic idea is the same, so we'll summarize it.

Compound condition to remove the right double quote

The conditions are reversed. If a right double quote only exists on the right side of the range, then remove it.

sFirst <> LeftDQ ' Is the first character not a left double quote?
sLast = RightDQ ' Is the last character a right double quote?

We again combine them into a compound conditional statement using the And operator.

' Compound condition to detect only a right double quote on the end
sFirst <> LeftDQ And sLast = RightDQ

Remove the right double quote character

To remove the right double quote character from the right side of the range, we need the MoveEnd method.

' Remove a right double quote from the end of the range
r.MoveEnd Count:=-1

The MoveEnd command moves the End position of the range without changing the Start position (unless End moves before Start). The default move unit is a character, so we can omit the unit option, but we need to move backward by a single character, so we assign -1 to the Count option.

Remove the right double quote conditional statement

Putting the conditions and the command together, we again use a condensed, one-line If statement to be concise:

' Remove the right double quote if it only appears on the right side
If sFirst <> LeftDQ And sLast = RightDQ Then r.MoveEnd Count:=-1

Allowing parentheses

We can do the same thing for parentheses simply by swapping out the respective left and right characters in the above conditions for an open parenthesis "(" and a close parenthesis ")".

' Remove the open parenthesis if it only appears on the left side
If sFirst = "(" And sLast <> ")" Then r.MoveStart

' Remove the close parenthesis if it only appears on the right side
If sFirst <> "(" And sLast = ")" Then r.MoveEnd Count:=-1

We want the same macro to handle both dialog and parentheses, so both sets of punctuation checks are included.

Include ending spaces

Since Word normally includes any trailing spaces after an extended selection, our range should probably mimic that behavior. That’s what users expect in Word even if they’re not aware of it.

' Extend over any spaces at the end of the range
r.MoveEndWhile Cset:=" "

For example, if the user selects the returned range in an external macro, it will look much like a typical range Word selects with the exception of any punctuation or paragraph mark characters we trimmed. Despite the default Word behavior for sentence selections, we do not include any trailing paragraph marks since that is counterintuitive to the idea of a sentence range.

Gotchas

We should get into the habit of considering potential problems. If you prefer to avoid any mud puddles today, skip to the final macro.

What if the range ends the macro empty?

We remove spaces, paragraph marks, and possibly a double quote or a parenthesis. If the range ends up being empty by the time the macro finishes, is that a problem?

Not really because the function returns the empty range and exits as normal. It 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.

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 initial paragraph is empty?

This is a special case of the above gotcha. What happens if the initial range begins empty or perhaps includes only whitespace?

If the macro begins with the cursor in an empty paragraph, the command to remove paragraph marks will move the range backward into the previous paragraph. It's not technically an error, but it's an awkward end result, so we should probably correct for it.

Without a lot of background explanation, we created two functions in separate articles to check these conditions. After the range assignment, the conditions are:

If both conditions are True, then we just want to exit the function. A reasonable return value is just the initial working range which was set to the initial document selection.

' Check whether the initial paragraph is empty (small gotcha)
If IsRangeEmpty(r) And IsParagraphEmpty(r.Paragraphs.First) Then
Set GetCurrentSentenceRange = r
Exit Function
End If

We insist on both conditions because an empty starting range is a common use case. Checking only for an empty first paragraph would mostly work, but it would not catch the unusual case of an initial multi-paragraph selection that just happened to have an empty first paragraph. Checking both conditions covers all the bases, the infield, the outfield … and the parking lot, and the hot dog stand, and the—but it's a tad safer.

We could instead use the whitespace versions of the functions given in the same articles, but that is more of a preference.

If you do not want to include two more functions, a quick and dirty solution that accomplishes the same validation is:

' Check whether the initial paragraph is empty (small gotcha)
Dim sParagraph As String
sParagraph = r.Paragraphs.First.Range.Text
If r.Start = r.End And sParagraph = vbCr Then
Set GetCurrentSentenceRange = r
Exit Function
End If

Again, we're not stepping through the commands with a lot of explanation, but this conditional statement would solve the gotcha without using any extra functions.

What if an initial selection exists?

This is a typical question in many Word VBA functions or editing macros. After identifying the initial sentence range, we quickly collapsed the range to avoid any logical issues. The stock macro avoids any such problems, but … what if we like the ability to selection multiple sentences?

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

Is this a problem?

It's more of an advantage because we can leverage the function in more circumstances, but … a few extra gotchas are introduced based on what content the initial selection spans.

  • The initial selection could span a paragraph mark. This sidesteps the intent of the function in returning a sentence range, not a multi-paragraph range. It could also cause unsightly changes to the paragraph structure depending on what the main macro does with the returned range.
  • It could span the end of the dialog (the right double quote) or a close parenthesis. Either case would confound the punctuation detection steps.

Wha are some recommended solutions?

These gotchas a little more complicated to catch and correct. How we handle such gotchas also depends more on preference.

  • We could ignore the issue and attribute any wonky identified ranges to user error. This is viable since running a sentence range function with a selection spanning excessive content (such as multiple paragraphs) is a little strange and somewhat counter to the intent of the function.
  • We could add some steps to automatically retract the End position of the range to the first paragraph mark or right punctuation mark. These extra steps would need to be balanced with some extra logic to ensure it still works for the regular cases.

The punctuation corrections in the current function just trim the unnecessary character, so it's not a significant issue. If the initial selection was problematic, the user would just receive a range including an extra punctuation mark.

The corrections will definitely bloat the function. A puffy macro is annoying, but I usually consider the extra steps a fair trade-off for a function since the complexity is neatly tucked out of sight. Given the extra complexity, we will defer handling this to the next level member function variation.

Final get current sentence range function

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

Function GetCurrentSentenceRange() As Range
' Get the range of the current sentence excluding any paragraph marks
' Double quotes or parentheses are excluded if they only occur
' on one side of range

' Declare some simplifying character variables (optional)
Dim sFirst As String, sLast As String, sParagraph As String

' Assign the working range r based on current Selection
Dim r As Range ' Optional
Set r = Selection.Range

' Exit the function if the initial paragraph is empty (small gotcha)
sParagraph = r.Paragraphs.First.Range.Text
If r.Start = r.End And sParagraph = vbCr Then
Set GetCurrentSentenceRange = r
Exit Function
End If

' Extend the range over the current sentence
r.Collapse ' Optional
r.Expand Unit:=wdSentence
' Remove paragraph marks or spaces from the end of the range
r.MoveEndWhile Cset:=" " + vbCr, Count:=wdBackward
' Remove spaces from the beginning of the range
r.MoveStartWhile Cset:=" " ' Just in case

' Get first and last characters for testing below
sFirst = r.Characters.First.Text
sLast = r.Characters.Last.Text

' Remove left double quote if only on one side
If sFirst = LeftDQ And sLast <> RightDQ Then r.MoveStart
' Remove right double quote if only on one side
If sFirst <> LeftDQ And sLast = RightDQ Then r.MoveEnd Count:=-1

' Remove left parenthesis if only on one side
' Reassign the characters just to avoid any tiny gotchas
sFirst = r.Characters.First.Text
sLast = r.Characters.Last.Text
If sFirst = "(" And sLast <> ")" Then r.MoveStart
' Remove right parenthesis if only on one side
If sFirst <> "(" And sLast = ")" Then r.MoveEnd Count:=-1

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

Set GetCurrentSentenceRange = r
End Function

These functions require the LeftDQ and RightDQ constants from a separate article. They are conveniently declared for the whole module (a group of macros in the VBA editor). If you prefer not to use them, we assigned similar variables above, but it clutters the function. The small gotcha near the top of the function is mentioned above which includes links to other functions, if you like, that simplify the conditional statement.

How do we use the function?

Using the function inside another macro would look like:

' Example of storing the current sentence range for later use
Dim rSentence As Range ' Optional
Set rSentence = GetCurrentSentenceRange

In VBA, we can omit the ending empty parentheses on the function call because no arguments are required.

What are the uses?

This function is used with the aforementioned move sentence macros. Where else might it be used?

We’ve previously created a delete sentence macro. We could generalize that macro using this function and extend it to make macros that select, cut, copy, and italicize (for internal dialog) the current sentence. Moreover, these macros would inherit the ability to work with dialog or parenthetical text, and they would be nearly trivial to create now that we have this workhorse function.

Improvements?

What could we do better?

New trim character functions?

The final macro isn't short, so we could extract the steps to trim the double quotes or parentheses into a separate function. This would condense and even generalize the steps. It’s not a bad idea, and I would probably do so with my own macros, but it would be mostly for aesthetics, so I omitted it from the final version above.

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.