AutoHotkey Tip of the Week: Dynamic Regular Expressions (RegEx) for Math Calculating Hotstrings

An AutoHotkey Classic, the Dynamic Hotstrings() Function Makes Instant RegEx Replacements Possible—Now, You Can Do Math with Your Hotstrings!

Anyone who reads my blog on a routine basis knows how I love Regular Expressions (RegEx). They make feasible all kinds of capabilities not practical by any other method. While not necessarily easy for a beginner to grasp, RegEx provides a mechanism for matching text when you don’t know exactly which characters you need (wildcards). (That’s why I wrote the book A Beginner’s Guide to Using Regular Expressions in AutoHotkey.) Although you may encounter a bit of a learning curve, RegEx gives you the ability to accomplish some pretty fancy tasks. This time I plan to demonstrate a couple of Hotstring techniques that might amaze you—they did me!

AutoHotkey AutoHotkey Library Deal!

While doing research for last week’s blog, I came across an alternative technique for capitalizing the first letter in a sentence. It uses a function called Hotstrings() originally written back in the AutoHotkey stone age (started by polyethene, Feb 4, 2007, and found on the old archived AutoHotkey forum—not to be confused with recently-new built-in AutoHotkey Hotstring() function). I later discovered the version discussed in this blog in the same archived forum (updated by Edd, Sep 9, 2014, the post “RegEx Dynamic Hotstrings). Other variations of “Dynamic Hotstrings” have appeared since that time but I found that, with the arrival of the new built-in Hotstring() function, I didn’t need the additional complexity. I only planned to use the Regular Expressions (RegEx) capability.

Note: Perhaps the name of this function should change to something similar to “RegExHotstrings()”—saving confusion with the build-in AutoHotkey Hotstring() function. In fact, for the purposes of this blog (and future reference), I have done just that.

The reply posted by flyingDman to the question “Auto Capitalize First Letter of Sentence” put me on the trail to this useful Dynamic RegEx Hotstring function.

The Special Hotstrings() Function

Cover 200You don’t need to understand how the RegExHotstrings() function works. (I only partially grasp what it does.) You only need to include it in your script or make it available in one of your libraries. (I plan to address function libraries soon.) Suffice it to say, the function appears to monitor all of the keys on your keyboard by turning each into an individual Hotkey looking for RegEx matches (similar to a keylogger). As the user types, the function watches the keystrokes. When the function finds a match, it fires a text replacement or subroutine.

I could only find a few examples of how to use the function, so it took me a good bit of fooling around to determine what works and what doesn’t. After reading this blog, you should understand how to make RegExHotstrings() work for your scripts.

Note: Others may have written even more versions of this RegExHotstrings() function with better features, but (other than the one I mentioned previously) I don’t know where to find them.

Capitalize Sentences with RegExHotstrings()

In the original answer to the sentence-capitalization question, flyingDman used the Hotstrings() function in the following manner:

Hotstrings("\.\s*(\w)", "Label")
Return

Label:
  Stringupper, $1, $1
  Sendinput, % ". " $1
return

The Regular Expression appears in the first parameter and the subroutine Label fills in the second parameter. (If AutoHotkey doesn’t find a matching Label in the script, it uses the second parameter as replacement text.)

The RegEx breaks down as follows:

  1. The \. expression represents the period at the end of a sentence. Normally, the dot acts as a wildcard for any character. Placing the backslash in front of it changes it to a mere dot.
  2. The \s matches any space, tab or any other single whitespace character (i.e. newlines).
  3. The asterisk * tells the RegEx engine to continue matching none or more occurrences of the preceding \s. I changed this to the plus sign + (one or more) since the asterisk capitalized file extensions (i.e. RegExHotstrings.Ahk). Using the \s+ allows for multiple spaces between the period and the first letter of the next sentence, although the subroutine only returns one space.
  4. The \w looks for any letter or digit. Putting a set of parentheses around the expression (\w) saves the match as a subpattern for later use in the results variable $1.

Making that one correction and changing the name of the function yields:

RegExHotstrings("\.\s+(\w)", "Label")

Label:
  Stringupper, $1, $1
  Sendinput, % ". " $1
Return

The Label subroutine uses the subpattern result $1 to convert the first letter of the sentence into uppercase, then executes the SendInput command—inserting the replacement results (a period, one space, and the uppercase letter) into the document or text editing field.

This RegEx tolerates multiple spaces between the period and the first letter of the sentence. Plus, as shown in the download RegExHotstringsApp.ahk script, I easily add the question mark and exclamation point to the capitalization RegEx by replacing \. with (\.|\?|!). (This RegEx implementation matches any one of the three sentence terminators. The script requires adjustments in the original subroutine since now it employs both $1 and $2.)

In my last blog, you might complain about the over 100 Hotstrings generated by the built-in AutoHotkey Hotstring() function loop. On the other hand, this RegExHotstrings() function turns every key on your keyboard into a Hotkey. I can’t say which I prefer or if it makes much of a difference—except small adjustments to the RegEx in RegExHotstrings() can add significantly more Hotstring-like actions to a single function call.

The opportunities for creating unique dynamic Hotstrings with RegExHotstrings()—starting with the next example for converting a fraction into a percentage on-the-flyintrigue me most.

A Truly Dynamic Hotstring for Calculating Percentages

The RegExHotstringsApp.ahk script posted at the Free AutoHotkey Scripts page contains both the RegExHotstrings() function and a number of examples. Most of the examples execute text replacements—which you can often replace with the built-in AutoHotkey Hotstring() function and the X option (for running functions and subroutines). However, calculating percentages with standard Hotstrings would present a challenge.

This next function call for calculating percentages from raw numbers immediately caught my attention:

RegExHotstrings("(\d+)/(\d+)%", "Percent") ; try: 4/50%

Percent:
p := Round($1 / $2 * 100,1)
SendInput, %p%`%
Return

The Regular Expressions above contains the following components:

  1. The two \d+ expressions match one or more numeric digits (0-9).
  2. The /  and % match a single forward-slash ( / ) and a single percent sign ( % ), respectively. You’ll find some non-AutoHotkey RegEx references (including the original example here) place a backslash in front of the forward-slash ( \/ ). You don’t need the backslash here since the forward-slash holds no special meaning in AutoHotkey Regular Expressions.
  3. The two sets of parentheses (…) save the subpattern results to the variables $1 and $2, respectively. AutoHotkey uses these for calculating the sum in the Percent subroutine.

For example, after typing the character string “345/5673%” and hitting the % key, a match occurs. The subroutine Percent dynamically replaces the original text with the calculated “6.1%” string:

 345/5673%  ⇒  6.1%

This means we can add percentages directly to documents without first breaking out a calculator.

Addition and Subtraction Hotstrings

Since I wanted to demonstrate at least one additional use for this Dynamic RegEx Hotstring technique, I chose to write a routine to add/subtract values in a text string. First, I used Ryan’s RegEx Tester to write an expression that recognizes an addition/subtraction string:

RegExHotstrings("(\d+(\.\d+)?(\+|-)(\d+(\.\d+)?(\+|-))*\d+(\.\d+)?)="
                ,"Add") ; add/subtract

Not as complicated as it looks!

While this RegEx uses \d+ in the same manner as the last example for matching any series of numeric digits, you see a few more needed additions:

  1. The expression (\.\d+)? looks for any decimal point followed by one or more numerical digits. The question mark ( ? ) makes the match optional—it may or may not appear. This makes it possible to mix both integers and floating-point numbers in the same string.
    Note: If you would like the previous percent calculator to accept non-integer values (containing a decimal point) insert (?:\.\d+)?—including parentheses—just after each \d+ expression (inside parentheses). The modifier ?: prevents the new sets of parentheses from assigning a new subpattern variable, thwarting the possible misalignment of $1 and $2 with the new values. You can find this form of the RegEx in the RegExHotstringsApp.ahk script.
  2. The expression (\+|-) matches either the plus sign (escaped \+) or the minus sign ( – ). The vertical bar ( | )  acts like the logical “or” in expressions.
  3. The asterisk ( * ) tells the RegEx engine to continue matching until it reaches the last number followed by a + or – sign—then include the last number
  4. The entire match activates by pressing the equal sign ( = )—the last character for the matching RegEx.

This time the parentheses capture the total addition/subtraction string (without the equals sign) as the first subpattern value $1 (e.g. “23+34+56-78+90”). Unlike the built-in AutoHotkey RegExReplace() function, when using the RegExHotstrings() function $0 does not yield the entire match. Rather, you must use $1, $2, $3, … for consecutive sets of parentheses. In other words, when capturing data for your subroutines, don’t try to get too fancy. (I did not find anything documenting this nuance.)

I wrote the following Add subroutine to parse the string ($1) and calculate the sum:

Add:
  AddArray := StrSplit($1 ,"+")
  Total := 0
  Loop % AddArray.MaxIndex()
  {
    If InStr(AddArray[A_Index],"-")
    {
      SubArray := StrSplit(AddArray[A_Index],"-")
      SubTotal := SubArray[1]
      Loop % SubArray.MaxIndex()-1
      {
        SubTotal := SubTotal - SubArray[A_Index+1]
      }
      AddArray[A_Index] := SubTotal
    }
    Total := Total + AddArray[A_Index]
  }
  SendInput, % Format("{:g}", Total)
Return

Type a row of addition/subtraction numbers with the appropriate intervening signs (no spaces), then hit the equal sign ( = ). It replaces the string with the calculated total:

23+34+56-78+90= replaced with ⇒  125

This routine only calculates addition and subtraction. While I could probably write a more complicated routine to include multiplication and division, a number of years ago Pulover of Pulover’s Macro Creator posted the Eval() function which does all these math calculations (and much more). I plan to demonstrate how to use Eval() in next week’s tip.

Include the RegExHotstrings() function in your script or one of the AutoHotkey function libraries. This function appears in both the RegExHotstringsApp.ahk script and separately as RegExHotstrings.ahk function file found at the Free AutoHotkey Scripts page:

RegExHotstrings(k, a = "", Options:="")
{
    static z, m = "~$", m_ = "*~$", s, t, w = 2000, sd, d = "Left,Right,Up,Down,Home,End,RButton,LButton", f = "!,+,^,#", f_="{,}"
    global $
    If z = ; init
    {
        RegRead, sd, HKCU, Control Panel\International, sDecimal
        Loop, 94
        {
            c := Chr(A_Index + 32)
            If A_Index between 33 and 58
                Hotkey, %m_%%c%, __hs
            else If A_Index not between 65 and 90
                Hotkey, %m%%c%, __hs
        }
        e = 0,1,2,3,4,5,6,7,8,9,Dot,Div,Mult,Add,Sub,Enter
        Loop, Parse, e, `,
            Hotkey, %m%Numpad%A_LoopField%, __hs
        e = BS,Shift,Space,Enter,Return,Tab,%d%
        Loop, Parse, e, `,
            Hotkey, %m%%A_LoopField%, __hs
        z = 1
    }
    If (a == "" and k == "") ; poll
    {
        q:=RegExReplace(A_ThisHotkey, "\*\~\$(.*)", "$1")
        q:=RegExReplace(q, "\~\$(.*)", "$1")
        If q = BS
        {
            If (SubStr(s, 0) != "}")
                StringTrimRight, s, s, 1
        }
        Else If q in %d%
            s =
        Else
        {
            If q = Shift
            return
            Else If q = Space
                q := " "
            Else If q = Tab
                q := "`t"
            Else If q in Enter,Return,NumpadEnter
                q := "`n"
            Else If (RegExMatch(q, "Numpad(.+)", n))
            {
                q := n1 == "Div" ? "/" : n1 == "Mult" ? "*" : n1 == "Add" ? "+" : n1 == "Sub" ? "-" : n1 == "Dot" ? sd : ""
                If n1 is digit
                    q = %n1%
            }
            Else If (GetKeyState("Shift") ^ !GetKeyState("CapsLock", "T"))
                StringLower, q, q
            s .= q
        }
        Loop, Parse, t, `n ; check
        {
            StringSplit, x, A_LoopField, `r
            If (RegExMatch(s, x1 . "$", $)) ; match
            {
                StringLen, l, $
                StringTrimRight, s, s, l
                if !(x3~="i)\bNB\b")        ; if No Backspce "NB"
                    SendInput, {BS %l%}
                If (IsLabel(x2))
                    Gosub, %x2%
                Else
                {
                    Transform, x0, Deref, %x2%
                    Loop, Parse, f_, `,
                        StringReplace, x0, x0, %A_LoopField%, ¥%A_LoopField%¥, All
                    Loop, Parse, f_, `,
                        StringReplace, x0, x0, ¥%A_LoopField%¥, {%A_LoopField%}, All
                    Loop, Parse, f, `,
                        StringReplace, x0, x0, %A_LoopField%, {%A_LoopField%}, All
                    SendInput, %x0%
                }
            }
        }
        If (StrLen(s) > w)
            StringTrimLeft, s, s, w // 2
    }
    Else ; assert
    {
        StringReplace, k, k, `n, \n, All ; normalize
        StringReplace, k, k, `r, \r, All
        Loop, Parse, t, `n
        {
            l = %A_LoopField%
            If (SubStr(l, 1, InStr(l, "`r") - 1) == k)
                StringReplace, t, t, `n%l%
        }
        If a !=
            t = %t%`n%k%`r%a%`r%Options%
    }
    Return
    __hs: ; event
    RegExHotstrings("", "", Options)
    Return
}

Click the Follow button at the top of the sidebar on the right of this page for e-mail notification of new blogs. (If you’re reading this on a tablet or your phone, then you must scroll all the way to the end of the blog—pass any comments—to find the Follow button.)

jack

This post was proofread by Grammarly
(Any other mistakes are all mine.)

(Full disclosure: If you sign up for a free Grammarly account, I get 20¢. I use the spelling/grammar checking service all the time, but, then again, I write a lot more than most people. I recommend Grammarly because it works and it’s free.)

Find my AutoHotkey books at ComputorEdge E-Books!

Buy Jack a Cappuccino!

If you found a blog particularly useful, show your appreciation by buying Jack a cup of coffee! Or, if you prefer something for yourself, check out his books.

$4.95

3 thoughts on “AutoHotkey Tip of the Week: Dynamic Regular Expressions (RegEx) for Math Calculating Hotstrings

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s