When creating custom natural language voice command scripts in Dragon Professional, we’ll eventually need to interpret numbers of document elements to act on such as in command phrases “delete five words” or “move that selection up three paragraphs”. Identifying the number allows more general command scripts. A previous article covered identifying relevant text keywords using the Like operator in VBA, but the process is a little more complicated when identifying numbers. We create a function to facilitate the process.
Thanks for your interest
This content is part of a paid plan.
Parsing numbers from a command phrase in a custom Dragon Professional script
A previous article parsed document related keywords. The next step in customizing our own natural language voice commands is detecting any spoken numbers. While Dragon Professional already includes list variables for such tasks, they aren’t general enough unless we create a long list. We're extending the idea to allow any whole number.
Using keywords in command parsing
Identifying keywords is a simple form of parsing text. There are better ways to interpret general text, but voice commands are often relatively simple where keywords can adequately summarize the instructions. Thus, we gain a lot of phrasing flexibility with a much easier approach.
Our number parsing goal
We’re imagining a voice command something like:
That doesn’t seem too bad, right?
A previous article talked about how to get the “paragraph” document element and the “up” direction parts of the command, but now we need to interpret the number.
Uhhh …
And it needs to work with any reasonable number (up to ten would be practical for most use cases but probably too restrictive in general), but we get bonus points if we allow any whole (non-negative) number.
Hold on.
I need some iced tea before we continue. Go ahead and get yourself a coffee and maybe a donut. I’ll be back in a minute.
…
Okay, Here we go.
We’ll start with interpreting numbers up to 10, but that is a little restrictive, so we’ll backtrack and add larger numbers in the extended content.
Function Skeleton
Checking for the same number matches over and over in various command scripts is repetitive, so it’s convenient to extract the steps into a function. When we need to interpret a number in a script, we just “pass” the voice command phrase to the function (generally referred to as "calling" the function). Then we store whatever result the function returns in a variable for later use.
Most of our keyword parsing functions are similar in structure, so we’ll use the following function skeleton:
Let's break down the details a little bit.
What parameter?
The function receives the command phrase as a plain text String variable sPhrase.
An upcoming article will cover how to get the command phrase from the Dragon Professional voice command script, but for now we’ll assume it’s stored in the sPhrase text variable.
Return result
To return the value, we assign any determined result to the function name anywhere within the function. For example, we might assign a function result by the following somewhere inside the function:
Default value
The document direction is already determined by a previous parsing function, so we will not interpret negative numbers in this function. Therefore, zero makes a reasonable default value when no number is stated explicitly in the command.
Data type
The return type of the function is given As Long as opposed to As Integer since Integers in VBA have a maximum value around 32,000. In practice for a voice command when writing, even this large of a number would probably never be needed, but we might as well be general about it (perhaps for character or word commands in a novel). Long values require slightly more computer memory than Integer values, but in practice, we would almost never notice the difference in a script or macro.
Store the detected number
We need to store the number for later use if it is present in the command text. As mentioned above, two of the basic number types are Integers and a larger number Long. To keep things simple and general, we’ll use Long.
Long and Integer data types are actual numbers in VBA as opposed to text versions of numbers like “two” or “2”.
I like to precede any variable names for numbers with an “n” as a reminder of what type of data it stores. We’re calling the variable “nElements” rather than something like “nParagraphs” since we plan to use the number function with different document elements across several scripts.
Using Like in text searches
“Text? I thought we were checking for—”
Just hold on.
We’re building something. Plus, we can write numbers as words also, right?
Search expressions using Like embrace VBAs inclination toward easily understandable commands (speaking of VBA macro steps not a Dragon voice command). They are usually clean and easy to read, and they allow a few wildcard characters in the search patterns to match varied text. Wildcard characters provide some flexibility, but we’ll stick with simpler search patterns for keywords or numbers.
The previous article covers some elementary concepts in more detail when using Like to parse a short command phrase, so we’ll swim out into deeper water a little more quickly in this article.
If you’re interested in more comprehensive search techniques, look up regular expressions including our brief introduction to using them in VBA.
Basic syntax when searching with Like
As a brief review, the basic syntax using Like is:
The text variable to search comes first followed by the Like VBA reserved word (meaning we can’t declare a variable in VBA named Like). Lastly, we add a plain text match pattern in double quotes. We could also use a String variable for the search pattern if it helps, but we’ll just stick with explicit search patterns.
Asterisk wildcard characters
The asterisks on either side of “two” indicate that Like should allow any or no characters on either side of the word. If one were omitted, such as “two*”, this pattern would require “two” to begin the sPhrase string, so we need both asterisks.
Like statement result
A search expression using Like will result in a True-False (Boolean) value. For example, our test expression “*two*” is True if the word “two” is found anywhere within the sPhrase text variable.
Using Like in conditional statements
Since Like gives us a True or False result, we can use them directly in conditional statements to make decisions in the script (see previous article for a brief introduction).
We’re checking against the noun form of “2” since Dragon Professional will automatically convert spoken text in a <dictation> list variable (see below) to the word form when the number is less than or equal to ten.
Storing the numerical result
Like only tells us whether a match was found, so we need to manually store the intended number inside the If statement.
The variable nElements stores the actual number value not a text version of it.
Using Like for text numbers as digits
In Dragon Professional scripts, the command phrase will be all text even if any numbers are stated explicitly. The number “2” will probably be automatically transcribed as the noun form “two” for any values ten or below (customizable in Dragon settings), but we should also interpret the text number version just in case.
Incidentally, even a “2” in a text variable is still text even though it looks like a number to human eyes. In VBA, it’s not a number 2 in the conventional sense until we convert it to a numerical value. We’ll work on number conversions from text in the extended content.
Numerical text search pattern
How do we match numerical digits as text?
We’re already checking for the noun form "two", so we explicitly add a check for the digit form “2” as text. If either condition is a match, then we know the stated number and can assign the value to nElements.
When matching “2”, our search pattern is literally “*2*”.
Not using “#” in the search pattern (yet)
If you’re familiar with the search patterns, you may notice that I omitted the more generic numerical digit match “*#*”. I don’t want to match a single-digit number because Like doesn’t tell me what number was matched. It only indicates that a match occurred, and I also need to test for noun forms like “*two*” anyhow. See the extended content below where we leverage the general match pattern for larger numbers.
Compound conditions
We’re matching both the noun “two” as well as the number “2” as text.
When several conditions are equivalent
Since either match could be True and still indicate a numerical value of 2 for the voice command, we use an Or operator.
The Or operator gives us a True result is either condition A or B is True. Applying it to our Like searches, we have:
In this compound condition, we won’t know which match was True (or both), just that one of the matches was successful; but in this statement, both matches are equivalent to the same numerical value.
Our number conditional statement
We put this compound condition into a conditional statement.
In one sense, it’s a little clunky, and it will get even longer below, but it’s not as awkward as it seems at first. No matter what, we would need to interpret any usage of “two” in a command phrase as a noun whether it is done directly or as part of a separate function.
Other solutions?
Alternatively, we could get more detailed information on a match using another string function InStr(…). It detects the presence of a search text and tells us where it’s located in the string.
In principle, we could then extract the number and convert it to a value from the string, but InStr(…) looks for a specific text with no wildcards possible. If we knew what text number to search for, we wouldn’t need to search for it.
A regular expression could easily capture which value the user said at a given position in the command using a “group”, called a “submatch” in VBA parlance. We could use that information somewhere else in the script, but that is a subject of a later article.
Solving with Like
Fortunately, if we’re willing to sacrifice conciseness in our script, Like provides an easy and clear solution.
Detect which number matches using ElseIf
How do we distinguish between numbers one through ten?
We can extend what we did for the number “two” to the numbers one through ten. If we find a match, we assign the appropriate number to our nElements variable.
The direct approach is to chain the necessary conditional matches together using ElseIf statements. An ElseIf statement is basically a continued If statement, so we chain ten of them together to handle whole numbers ten or less.
Each ElseIf statement tests the condition and runs the code below it if the condition is True. If the condition is False, it checks the next ElseIf statement until none are left or until it finds an Else statement. After any match in the chain is found and the corresponding steps are run, the overall If statement ends. See this previous article for more explanation about conditional statements in VBA.
Default value
We need a way to set a default value since a number may not be stated in some cases. If no numbers are found, we assume a value of zero and assign it in the Else part of the long If statement.
Getting the number
Now the initial solution.
As we did with search for “*two*” above, we use Like to check for a series of specific numbers in either the text or numerical form and then assign the known value to the nElements repeating for all numbers up to ten.
Including ordinal numbers
You could add ordinal numbers such as “*first*” by tacking on an additional check.
We only need to consider the unique ordinal forms like “first”, “second”, “third”, “fifth”, or “ninth”. The others like “fourth” will be matched by “*four*” since “four” is a substring of “fourth”.
Number parsing conditional statement skeleton
Repeat the compound conditional statements for “one”, “two”, up to “ten”. That’s why we sacrificed conciseness if we use Like directly. If no number matches are found, we’ll assume a value of 0.
An abbreviated set of steps to interpret the move number is:
Now that we have the skeleton, let’s create our function.
Basic numbers phrase function
We’ve been storing the identified numerical value in the nElements variable, but now we’ll return the number as our function result by storing it in the function name GetPhraseNumber instead.
Really? That’s clunky
Yeah, that’s … kind of long. We stopped at ten just to keep it shorter, but we still need to account for numbers bigger than ten.
Arghhh.
Can you say regular expressions, please?
Yeah, I agree, it really is easier if we switch, so we could—
Excuse me?
Oh, sorry. You’re right. I suppose we shouldn’t get ahead of ourselves. Yeah, let’s save regex for another article and stick with Like for now [sad face].
Example use
When using the above function, we store the result in the nElements variable we defined earlier. We “pass” the command phrase sPhrase as an argument in parentheses, then set the returned result equal to nElements.
Given the function, we know we get a Long value from 0 up to 10. See the extended content below if you want to allow any whole number up to about two billion, but the process is more programmy.
Gotchas?
Where could all this go wrong?
Ambiguous numbers
There are some issues with more colloquial phrasings like “this one”.
I admit the wording is a little awkward, but “one” in this phrase is a pronoun referring to the current paragraph rather than a numerical value. The above function would not be able to distinguish between “three” and “one” without some additional validation checks. Solving this issue is left to later articles.
Just don’t say it like that
One could argue, “just don’t say it like that.”
I suppose you could enforce that restriction on yourself, but you’ll be surprised how much your spoken command varies as you try to conveniently give command.
It’s happened to me more than once. You’re writing, and you need to instruct the computer to do something. Your brain knows what you want to say and the meaning of the command, but your verbal processing doesn’t quite get the command out correctly, at least not without some additional effort.
Writing and speech use different parts of the brain. It’s not that you can’t train yourself to efficiently give the voice command in a specific way, but why not let the command script make that process easier? We’ll expand more on natural language voice commands in upcoming articles at two different levels.
Improved Number Phrase Function Using Like
Let’s tackle interpreting numbers larger than ten.
Like with numbers
The wildcard character to find numbers with a Like search pattern is “#”. Assuming it is part of a larger search pattern, the hashtag symbol specifically matches a single number digit at the given location in the search pattern.
Detecting one-digit numbers
How do we get a general one-digit number anywhere in a text string using Like?
Using our previous command phrase variable sPhrase, we could try a pattern match such as:
This pattern asks Like to match any single-digit number in the text regardless of what is on either side (numbers, spaces, or other text). For example, “This is a 1” would work fine but so would “How about 23” and “Your ID is A4X”. We can take advantage of the former and correct for the latter below.
However, using Like only tells us if a number is present. We don’t get the numeric value or even its position in the sPhrase text variable.
Ughhh, can’t they ever make it easy for us?
What do we do?
Leave single-digit numbers explicit
We can detect whether a single digit number exists in the text, but in practice when parsing voice commands, using a pattern “*#*” doesn’t solve our problem of interpreting numbers.
We don’t get the specific number or even its position in the phrase, and we would still need to account for noun forms of the numbers “one” through “ten” (and the unique ordinal forms if you wish to include them). Most <dictation> list variable results in Dragon Professional scripting will automatically convert spoken numbers ten or below into their noun form, so we can’t leave them out.
It’s easier to just use explicit matches “*1*”, “*one*”, “*first*”, etc. with single-digit numbers, and tackle larger numbers with the hashtag wildcard character.
Aside from a different solution entirely (like regex … yeah, I’ll shut up about it for now), we can still solve this problem for larger numbers, but we will need to lean on some additional tools to generalize our solution.
Detecting two-digit numbers
For larger numbers, we need to know if at least two consecutive numerical digits appear together anywhere in the text. Using Like, we can detect them with more than one hashtag in the search pattern such as “##”, but we need the wildcard characters “*”. Each asterisk * allows for any or no character in that position, so a search pattern “*##*” using Like will match any two number digits side by side anywhere in the searched text. A few examples include:
The first two examples work because they include at least a two-digit number, “12” or “103”, respectively. The pattern also matches “… 103 …” because the “3” in the number falls into the second asterisk * wildcard character since a "3" counts as any character. The last example only includes a single digit number “5”, so it is not matched by “*##*” which requires at least two digit characters side by side.
Not quite enough …
What’s the problem?
Again, Like only tells us whether a match was found. It returns no information about the match, so we still need to dig deeper to extract the stated number from the command.
Match conditional statement
So we can detect a general two or more digit number in the command phrase using the Like search pattern “*##*”. Using our assumed command phrase sPhrase, the conditional statement looks like:
Combining this with our previous conditional statement from the basic script above, we get:
Are we done?
Oh. That’s all? That was easy—
Uh …
Probably not if you scroll down and see how much of the article is left.
What’s wrong?
As previously mentioned, we don’t know which number was stated. The above Like expression only tells us if a two or more digit number is present.
Ughhh.
This is why regex really is a better solution for more complicated text searches, but let’s trudge ahead.
Test each word of the command phrase
We can split the command text up into individual words and check whether each word is a number.
Yeah, it’s clunky, but it’s completely general for any whole number mentioned in the command.
Store the command words
We need a variable to store all the words in sPhrase. We’ll call it AllWords.
We’re being lazy with the return type because we haven’t talked about arrays (basically a sequential list of values in computer memory) yet, and the Split function below will give us an array of words. Using a Variant type for AllWords allows VBA to handle the details for us. I don’t like being sloppy like this, but I do not want to introduce arrays now.
Using the Split function
The Split function is a standard VBA function that takes a plain text string and splits it based on any desired separation character such as commas, spaces, dashes, etc. The string to split is the first argument, and the character used to break it into pieces (generally called “substrings”) is the second argument.
In our case, we need to split the command phrase sPhrase into individual words, so we specify the split character as a space “ ” in double quotes (remember plain text strings must be set off by double quotes). The spaces are discarded in the splitting process, but this doesn’t matter for our solution.
The return type for the Split function is an array of text strings, but we’re not interested in the gritty details more than what was stated above. The AllWords words variable just holds a sequence of words for us.
Checking for numbers using IsNumeric
We’ve split sPhrase into its individual words. Presumably most of them will be regular words and not numbers; so if I select a given word, say “the”, from command phrase, how do I determine whether it is a number? And when I find a text number, how do I convert it to an actual number?
Let’s assume our word is stored in a variable named sWord (see below). The IsNumeric function is a standard VBA function that tests whether a text string is a possible number in disguise.
The function does not convert the word to a number for us. It just checks whether sWord has a possible numerical value and gives us a True or False (Boolean) result.
Using IsNumeric in a conditional statement
Since IsNumeric returns a Boolean value, we can immediately use it in a conditional statement.
Presumably inside the If statement, we would convert the validated number into its corresponding numerical value.
Converting numerical text into a number
We can convert a known text number to a VBA Long number type using the standard VBA function CLng(…). We apply it to sWord and then store the result in our previous variable nElements.
This function only works if we know sWord is a text form of a numerical value such as “1”, “25”, etc. Otherwise, VBA will give an error and crash your script which isn’t fun.
If you prefer to use Integer numbers, there is a corresponding VBA function CInt(sWord).
CLng only works for numbers
Unfortunately, neither IsNumeric nor CLng will work with noun forms of numbers “one”, “twelve”, etc., but we’ve already manually accounted for these numbers at least up to “ten” in the basic script above.
Number conversion
Putting it together, our numerical test along with the number conversion is:
But since the command is so short, I prefer the more compact form:
Unfortunately, this does start looking more like programming than a typical VBA macro or script.
Check all command words
We now want to check each word in the AllWords variable and convert the spoken number. A For Each loop will iterate over each item in an array or collection and allow us to run checks (or whatever else we wish to do) on it.
General For Each loop structure
A general For Each loop has the structure:
The reserved words “For”, “Each”, and “In” are required. SomeLoopVariable must be a Variant type for an array like we're using, but it can be an Object type if you're looping over object-like things such as Ranges, Paragraphs, etc. This variable changes its value as we loop over the items in SomeCollection.
The SomeCollection variable can be any array or collection VBA understands natively. Word or Dragon automatically detects how many items are in the collection and loops over them one by one.
The loop must be ended by Next. If you prefer to be extra clear, the loop variable name can be placed after the Next, but it is optional.
All steps inside the loop will be repeated for each item in the collection. If SomeCollection contains no items, then the loop does nothing. A nice thing about a For Each loop is we don’t need to know how many items are in the collection.
Define a loop variable for each command word
We need a loop variable which we’ll name sWord. This variable will take on the value of each individual word in the command phrase as the For Each loop iterates over each word stored in the AllWords variable.
You might consider declaring sWord as a String type since we know the words from the command phrase are plain text, but VBA requires the Loop variable of a For Each loop to be a Variant type because we’re looping over an array.
Loop over the command words
Now loop over all the words in AllWords and perform the number check. Following the For Each loop structure above, we use:
sWord must be the loop variable, and AllWords must be the array of words where we’re looping over.
Allows (almost) any size number
Split separated our command phrase into words based on the spaces “ ” between them. Even though we initially matched a two-digit number using Like, the Split function automatically separates out the whole number (no pun intended), so the conversion works for any size number up to the maximum size of a Long variable (over two billion).
Final improved get number function
Combining the earlier basic function results with our new multi-digit number parser, we the following improved script. Again, we replace references to nElements mentioned above with the function name GetPhraseNumber to assign the return value of the function.
This version will not catch noun forms of numbers above ten, but it catches them for ten or below because we explicitly test for them.
The way the two or more digit number check and conversion is done, only the last number mentioned in the command phrase will be returned. Any other numbers mentioned are ignored.