Regular expressions helped me work with phone-number normalization, voice-route matching and trunk translations in Microsoft Lync environments. The syntax is compact; the important work is making the rule reflect the intended dialing behavior.

This revision develops my original Lync-era article into a clearer guide. The examples retain its sample numbering plan. They illustrate string transformations, not a current national numbering specification or a configuration to apply unchanged to a live service.

Separate the match from the translation

A normalization rule has two parts. The pattern decides which input qualifies. The replacement describes the output, using any captured digits. Keep these separate when reasoning about a rule: a match alone does not confirm that the translated number is correct.

The examples below use .NET regular-expression syntax. They use [0-9] to mean ASCII dialing digits and \A / \z to bound the entire input string. Check the supported syntax and rule evaluation behavior of the actual voice platform before adopting a rule.

Walk through a four-digit extension

Input:        3045
Pattern:      \A([0-9]{4})\z
Replacement:  +2023303$1
Output:       +20233033045
  1. \A begins at the start of the string.
  2. ([0-9]{4}) captures exactly four ASCII digits as group 1.
  3. \z requires the end of the string.
  4. The replacement joins the fixed prefix +2023303 with the value of $1: 3045.

A five-digit input, letters or a trailing newline should not match this rule. Test these rejected inputs as deliberately as the accepted one.

A consistent set of dial-plan examples

The original examples use Egypt’s country code 20, local-area prefixes and organization-specific extension mappings. Numbers are illustrative. The access code 48 is a sample organizational prefix; it is not presented as Egypt’s standard international dialing prefix.

Illustrative rules, with pattern and replacement shown separately
UsePatternReplacementInput → output
Four-digit extension\A([0-9]{4})\z+2023303$13045
→ +20233033045
Five-digit extension beginning with 3\A3([0-9]{4})\z+2023308$133045
→ +20233083045
Cairo local number\A([0-9]{8})\z+202$133033045
→ +20233033045
Alexandria local number\A([0-9]{7})\z+203$13303045
→ +2033303045
Banha local number\A([0-9]{7})\z+2013$13303045
→ +20133303045
Egypt number already beginning with 20\A(20[0-9]{9,10})\z+$120233033045
→ +20233033045
Organization-specific international access prefix 48\A48([1-9][0-9]{7,14})\z+$148914412345678
→ +914412345678
Operator shortcut\A0\z+202330330450
→ +20233033045
Sharkeya site prefix 611\A611([0-9]{4})\z+2055234$16111234
→ +20552341234
Mansoura site prefix 622\A622([0-9]{4})\z+2050234$16221234
→ +20502341234
Luxor site prefix 633\A633([0-9]{4})\z+2095234$16331234
→ +20952341234

The Alexandria and Banha examples deliberately have the same seven-digit match. They represent alternative local contexts, not two distinguishable rules for the same input in one undifferentiated plan. Use an appropriate user or site context, or an explicit prefix, to remove ambiguity.

A syntactically matching result is not proof that a telephone number is assigned, reachable or authorized for the caller. Numbering policy and routing need their own validation.

The syntax you need most often

Capturing and non-capturing groups

(...) captures a matched portion. (?:...) groups alternatives or repeated elements without allocating a capture. For example, \A(?:611|622)([0-9]{4})\z captures the extension after either site prefix, but does not capture the prefix itself.

Patterns and replacements use different notation

Within a .NET pattern, \1 matches the text captured by group 1. In a .NET replacement, $1 inserts group 1. Use ${1} when the next literal character is a digit and the group reference would otherwise be ambiguous.

Pattern:      \A([0-9]{2})-\1\z
Matches:      12-12
Rejects:      12-34

Replacement example:
Pattern:      \A([0-9]{4})\z
Replacement:  +2023303${1}

Quantifiers

* means zero or more; + means one or more; ? means optional. Braces set a count: {4}, {2,5} or {3,}. For structured numbers, bounded counts usually communicate the intended length more clearly than .*.

Greedy quantifiers prefer longer matches; adding ? makes a quantifier lazy. A lazy quantifier changes matching preference, not the definition of a valid telephone number.

Character classes, alternatives and escaping

[0-9] matches one ASCII digit; [^0-9] matches a character outside that range. (?:611|622) selects either prefix. A dot is a wildcard, so use \. for a literal dot and \+ for a literal plus sign.

In .NET, \d includes Unicode decimal digits, while \w covers more than ASCII letters and digits. Neither \w nor \s is a substitute for a clearly defined dialing alphabet.

Test both the rule and the plan

Start with a table of expected inputs, rejected inputs and translated outputs. Then test how the rules interact in the actual platform. A broad local-number rule may overlap with a site-prefix rule; use platform testing to verify precedence and scope.

# PowerShell / .NET: test the transformation locally
$pattern = '\A([0-9]{4})\z'
$replacement = '+2023303$1'
$dialed = '3045'

if ([regex]::IsMatch($dialed, $pattern)) {
    [regex]::Replace($dialed, $pattern, $replacement)
}
# Expected: +20233033045
  • Test the shortest and longest accepted lengths.
  • Test an input that nearly matches: an extra digit, a missing digit or an unexpected prefix.
  • Check leading and trailing whitespace explicitly.
  • Compare every output with the intended destination format.
  • Test overlapping rules in their intended site and user context.
  • Review special destinations separately, including emergency and operator handling.

The PowerShell example evaluates a string transformation only. It does not configure Lync, alter a dial plan or place a call.

Original article and technical references

This is a revised edition of my 15 January 2012 technical article. The historical version remains available on WordPress.

The original also cited Wikipedia and NET VX documentation. The revision corrects example lengths, country-code capture, full-match behavior and the distinction between backreferences and replacement groups.