Explainer

Regular expressions

A regular expression is a compact text pattern that describes the shape of a text, such as an invoice number or an IBAN, so that Excel can test, extract or replace it.

Last updated: 23 October 2026

Explains: regular expression

What it is: A regular expression is a text pattern in a compact notation. ^INV-[0-9]{6}$ stands for "INV-, exactly six digits, nothing else".

Why it matters: Reference numbers follow schemes, and bank text and exports hide codes inside longer texts. A wildcard cannot say "exactly six digits" or "INV or CN"; a regular expression can. In Microsoft 365 you use it to validate invoice numbers and IBANs, pull keys out of bank descriptions and look up on a pattern.

What a regular expression is

A regular expression (regex) is a text pattern in a compact notation that describes the shape of a text. The pattern ^INV-[0-9]{6}$ says: the text starts with INV-, then comes exactly six digits, and nothing follows. A wildcard can only say "any characters"; a regular expression can say how many, of which kind, and which alternatives are allowed.

An auditor meets regular expressions wherever a code has to follow a scheme or sits inside free text:

  • reference schemes: invoice numbers, Dutch IBANs, VAT numbers (NL, 9 digits, B, 2 digits) and postcodes
  • bank statement lines, where the customer types the invoice number anywhere in the description
  • exports that pack several fields into one cell, such as VRK|25-0388|D1002|21%

Excel reads the PCRE2 flavor of regular expressions, in Microsoft 365 only. Three rules decide most results:

  • the pattern may match anywhere in the text; only ^ and $ pin it to the start and the end
  • the match is case sensitive unless you ask otherwise
  • inside an Excel string a backslash stays a single \, and a double quote is written as ""

The tokens that matter in finance data

A pattern is built from literal characters, which match themselves, and tokens, which have a special meaning. The table lists the tokens you need for reference numbers and bank text. A character class such as [A-Z] or [0-9] matches one character from the set; \d is another way to write a digit.

A token applies to the item directly before it. In [0-9]{6} the {6} counts the digits of the class; in (INV|CN) the brackets make INV|CN one item.

TokenMeaningExample patternMatches
^Start of the text^INVINV-250412, but not XINV-250412
$End of the text412$INV-250412, but not INV-4125
\dOne digit, the same as [0-9]25-\d\d25-04 inside 25-0412, but not 25-AB
[A-Z]One character from the set or range[CD]2001C2001 and D2001, but not E2001
[^A-Z]Not: any one character that is not in the set (the ^ must be the first character inside the brackets)[^A-Za-z0-9]The space, dot and hyphen in NL91 ABNA.0417-1643, so REGEXREPLACE can delete every separator in one pass
{n}Exactly n times the item before it[0-9]{6}250412, but not 25041 (five digits)
{n,m}Between n and m times^INV-[0-9]{4,6}$INV-2504 and INV-250412, but not INV-250 or INV-2504123
.Any one character25.0425-04, 25X04 and also 25.04
+One or more times^INV-[0-9]+$INV-2 and INV-250412, but not INV-
?The item before it is optional (zero or one time)^[1-9][0-9]{3} ?[A-Z]{2}$A postcode with or without the space: 1011 AB and 1011AB
|Or: the left side or the right sideVRK|MEMAny text that contains VRK or MEM
( )Groups items; REGEXEXTRACT and REGEXREPLACE can pick up the group^(INV|CN)-[0-9]{6}$INV-250412 and CN-250412
\.A real dot (the backslash removes the special meaning)^[0-9]+\.[0-9]{2}$1250.00, but not 1250,00 or 125000
(?i)At the start of a pattern: ignore upper and lower case(?i)^inv-[0-9]{6}$INV-250412, inv-250412 and Inv-250412

Wildcards versus regular expressions

A wildcard pattern and a regular expression look alike but work differently. A wildcard pattern is compared with the whole text, so INV* means "starts with INV". A regular expression is searched anywhere in the text, so INV already means "contains INV" and you add ^ and $ when the whole text must fit.

The table shows what each can say. The three rows from "Exactly six digits" to "An optional space" are out of reach for wildcards.

You wantWildcardRegular expression
Any run of characters*.*
Exactly one character?.
Contains INV*INV*INV
Starts with INVINV*^INV
The whole text must fitAlways the caseAdd ^ and $
Exactly six digitsCannot be said[0-9]{6}
INV or CNCannot be said(INV|CN)
An optional spaceCannot be said[ ]?
Upper and lower caseIgnoredMatters; (?i) ignores it
A real asterisk~*\*

Where regular expressions work in Excel

Watch out: Everything on this page is Microsoft 365 only. In Excel 2021 or 2024 XLOOKUP and XMATCH return #VALUE! for match mode 3, and the three REGEX functions are not available.

Five functions read a regular expression. The criteria of SUMIFS, COUNTIFS and the other conditional functions do not: they read only wildcards. Older Excel versions need Power Query or VBA for patterns.

Function or toolReads a pattern?How
REGEXTESTYesThe second argument; TRUE when the pattern is found anywhere in the text.
REGEXEXTRACTYesThe second argument; returns the first match, all matches or the groups, always as text.
REGEXREPLACEYesThe second argument; replaces the matches, and $1 and $2 insert groups.
XLOOKUP, XMATCHYes, with match mode 3The lookup value is the pattern; case sensitive unless it starts with (?i).
SUMIFS, COUNTIFSNoWildcards only. Count with SUMPRODUCT and REGEXTEST instead.
FILTERNot directlyUse REGEXTEST as the include argument.
Find and Replace (Ctrl+F, Ctrl+H)NoWildcards only. Use REGEXREPLACE in a helper column.

Test: does the text follow the scheme?

=REGEXTEST(B2, "^INV-[0-9]{6}$")
=REGEXTEST(B2; "^INV-[0-9]{6}$")

TRUE when B2 is INV- followed by exactly six digits, and nothing else.

Extract: pull a number out of text.

=VALUE(REGEXEXTRACT(B2, "[0-9]{6}"))
=VALUE(REGEXEXTRACT(B2; "[0-9]{6}"))

The first run of six digits in B2. REGEXEXTRACT returns text, so VALUE turns it into a number.

Replace: rearrange with groups.

=REGEXREPLACE(B2, "^INV-([0-9]{2})([0-9]{4})$", "$1-$2")
=REGEXREPLACE(B2; "^INV-([0-9]{2})([0-9]{4})$"; "$1-$2")

INV-250412 becomes 25-0412: group 1 holds the first two digits, group 2 the last four.

Look up: the lookup value is a pattern.

=XLOOKUP("^INV-10(02|04)$", A2:A7, B2:B7, "Not found", 3)
=XLOOKUP("^INV-10(02|04)$"; A2:A7; B2:B7; "Not found"; 3)

Match mode 3 is the fifth argument in XLOOKUP and the third in XMATCH. The first cell of A2:A7 that is INV-1002 or INV-1004 wins.

Reading a pattern step by step

Read a pattern from left to right and cut it into pieces at each token. A Dutch IBAN is NL, 2 digits, 4 letters and 10 digits, 18 characters in all. As a pattern: ^NL[0-9]{2}[A-Z]{4}[0-9]{10}$.

The pattern checks the shape, not the content: it does not test the check digits, so a mistyped digit still passes. A lowercase nl91... fails because of the case sensitivity, and an IBAN printed in groups (NL91 ABNA 0417 1643 00) or with dots and hyphens fails because of the separators. Clean it first with a negated class: [^A-Za-z0-9] is "any character that is not a letter or digit", so REGEXREPLACE with that pattern deletes every separator in one pass, whichever ones a client used, and UPPER takes care of the case.

PieceReads asIn NL91ABNA0417164300
^The text starts hereNothing before the N
NLThe two letters NL, as typedNL
[0-9]{2}Exactly two digits91, the check digits
[A-Z]{4}Exactly four capital lettersABNA, the bank code
[0-9]{10}Exactly ten digits0417164300, the account number
$The text ends hereNothing after the last 0
=REGEXTEST(B2, "^NL[0-9]{2}[A-Z]{4}[0-9]{10}$")
=REGEXTEST(B2; "^NL[0-9]{2}[A-Z]{4}[0-9]{10}$")

TRUE for NL91ABNA0417164300: 2 + 2 + 4 + 10 = 18 characters in the right order.

=REGEXTEST(UPPER(REGEXREPLACE(B2, "[^A-Za-z0-9]", "")), "^NL[0-9]{2}[A-Z]{4}[0-9]{10}$")
=REGEXTEST(UPPER(REGEXREPLACE(B2; "[^A-Za-z0-9]"; "")); "^NL[0-9]{2}[A-Z]{4}[0-9]{10}$")

Also TRUE for nl91 abna.0417-1643 00: the negated class deletes the spaces, dot and hyphen, UPPER fixes the case, and only then is the shape tested.

Worked examples

Example1 of 3Beginner

Check that invoice numbers follow the INV- scheme

The sales ledger of Veldhuis Installatie BV numbers its invoices as INV- followed by six digits. Before you test the completeness of the numbering, you want to flag every number in the export that does not follow that scheme.

Column C shows TRUE or FALSE per invoice number.

Sheet excerpt: C2:C8 holds the formula
RowABCD
1Invoice no.CustomerValid?
2INV-250412Dekker Installatie BVTRUE=REGEXTEST(A2, "^INV-[0-9]{6}$")=REGEXTEST(A2; "^INV-[0-9]{6}$")
3INV-25412Van Rijn Vastgoed BVFALSE=REGEXTEST(A3, "^INV-[0-9]{6}$")=REGEXTEST(A3; "^INV-[0-9]{6}$")
4inv-250413Hofman AannemersFALSE=REGEXTEST(A4, "^INV-[0-9]{6}$")=REGEXTEST(A4; "^INV-[0-9]{6}$")
5INV-2504140Mulder HorecaFALSE=REGEXTEST(A5, "^INV-[0-9]{6}$")=REGEXTEST(A5; "^INV-[0-9]{6}$")
6INV 250415Ouwerkerk LogistiekFALSE=REGEXTEST(A6, "^INV-[0-9]{6}$")=REGEXTEST(A6; "^INV-[0-9]{6}$")
7INV-250416Kuipers Techniek BVTRUE=REGEXTEST(A7, "^INV-[0-9]{6}$")=REGEXTEST(A7; "^INV-[0-9]{6}$")
8INV-25O417Smeets GroothandelFALSE=REGEXTEST(A8, "^INV-[0-9]{6}$")=REGEXTEST(A8; "^INV-[0-9]{6}$")
9
Formula cell (C2:C8)

Formula in C2

=REGEXTEST(A2, "^INV-[0-9]{6}$")
=REGEXTEST(A2; "^INV-[0-9]{6}$")

Result: TRUE

Step by step
  1. The pattern is ^INV-[0-9]{6}$. ^ fixes the start, INV- must be typed exactly like this in capitals, [0-9]{6} is exactly six digits and $ fixes the end. REGEXTEST looks for the pattern anywhere in the text, so the two anchors are what turn this into a check on the whole cell.
  2. A2 holds INV-250412: it starts with INV-, then come the six digits 2, 5, 0, 4, 1 and 2, and then the text ends. TRUE.
  3. A3 holds INV-25412: only five digits, and the pattern needs six before the end. FALSE. A4 holds inv-250413: the pattern is case sensitive by default, so inv- does not fit INV-. FALSE.
  4. A5 holds INV-2504140: after six digits (250414) a seventh digit follows, but $ needs the end of the text. FALSE. A6 holds INV 250415: a space where the pattern needs a hyphen. FALSE.
  5. A7 holds INV-250416: TRUE. A8 holds INV-25O417, with the capital letter O instead of a zero in the third position after the hyphen. A letter is not in [0-9]. FALSE.
  6. C2 shows TRUE; copied down, C3 to C8 show FALSE, FALSE, FALSE, FALSE, TRUE and FALSE. Two of the seven numbers follow the scheme, so five go on your list of exceptions.

Watch out: Leave out the anchors, as in INV-[0-9]{6}, and A5 (INV-2504140) passes: six digits are found inside the seven. Test on the whole cell with ^ and $.

More on this example: why it works, 1 more pitfall, 2 variations

Why it works

REGEXTEST asks whether the pattern occurs in the text, not whether the text equals the pattern. ^ and $ close both ends, and {6} fixes the number of digits, which a wildcard cannot do.

The check says whether the number has the right form. It does not say that the invoice exists.

More to watch out for

  • Look-alike characters fail the test on purpose: a letter O for a zero, an en dash for a hyphen. The result is FALSE and the cell looks right, so check the flagged numbers character by character.

Variations

=REGEXTEST(A2, "^INV-[0-9]{6}$", 1)
=REGEXTEST(A2; "^INV-[0-9]{6}$"; 1)

The third argument 1 ignores case. A4 (inv-250413) now passes, so the numbers that follow the scheme go from 2 to 3. Writing (?i) at the start of the pattern does the same.

=SUMPRODUCT(--REGEXTEST(A2:A8, "^INV-[0-9]{6}$"))
=SUMPRODUCT(--REGEXTEST(A2:A8; "^INV-[0-9]{6}$"))

Counts the numbers that follow the scheme in one cell: the result is 2 (A2 and A7).

Example2 of 3Intermediate

Find the customer behind a bank statement line

A customer paid Veldhuis Installatie BV and typed the invoice number somewhere in the bank description. You want to know which customer the line belongs to, using the debtor list of the client.

E1 holds the bank description. The invoice number can sit anywhere in it, so a fixed position (LEFT, MID) does not work.

Sheet excerpt: E2 holds the formula
RowABCDEF
1Invoice no.CustomerBank descriptionSEPA OVERBOEKING HOFMAN AANNEMERS BETALING FACTUUR INV-250418 DD 14-10-2025
2INV-250412Dekker Installatie BVCustomerHofman Aannemers=XLOOKUP(REGEXEXTRACT(E1, "INV-[0-9]{6}"), A2:A7, B2:B7, "Not found")=XLOOKUP(REGEXEXTRACT(E1; "INV-[0-9]{6}"); A2:A7; B2:B7; "Not found")
3INV-250415Van Rijn Vastgoed BV
4INV-250418Hofman Aannemers
5INV-250421Ouwerkerk Logistiek
6INV-250427Mulder Horeca
7INV-250433Kuipers Techniek BV
8
Formula cell (E2)Input

Formula in E2

=XLOOKUP(REGEXEXTRACT(E1, "INV-[0-9]{6}"), A2:A7, B2:B7, "Not found")
=XLOOKUP(REGEXEXTRACT(E1; "INV-[0-9]{6}"); A2:A7; B2:B7; "Not found")

Result: Hofman Aannemers

Step by step
  1. Excel evaluates the inner function first: REGEXEXTRACT on E1 with the pattern INV-[0-9]{6}, that is INV- and six digits. The fourth argument of XLOOKUP is not involved yet. There are no anchors, because the number is inside a longer text and REGEXEXTRACT has to find it, not test the whole text.
  2. The words SEPA, OVERBOEKING, HOFMAN, AANNEMERS, BETALING and FACTUUR contain no INV-. In FACTUUR INV-250418 the pattern fits: INV-, then 2, 5, 0, 4, 1, 8 are six digits. The next character is a space, so the match ends there. The default return mode 0 returns the first match as text: INV-250418. The date 14-10-2025 has no INV- and is never reached.
  3. XLOOKUP now looks up the text INV-250418 in A2:A7 with the default exact match. A2 (INV-250412): no. A3 (INV-250415): no. A4 (INV-250418): yes, the third item.
  4. XLOOKUP returns the third item of B2:B7, which is B4: Hofman Aannemers.
  5. Type BETALING INV-250433 KUIPERS TECHNIEK in E1 and REGEXEXTRACT returns INV-250433, the sixth item, so the result is Kuipers Techniek BV. The formula follows the number wherever it sits in the text.

Watch out: A bank line without an invoice number, such as TERUGBETALING BTW 3E KWARTAAL, makes REGEXEXTRACT return #N/A. The error from REGEXEXTRACT is what you see: the Not found message is not shown, because it only covers a key that is missing from the list. Wrap the formula in IFNA (first variation).

More on this example: why it works, 1 more pitfall, 2 variations

Why it works

REGEXEXTRACT turns free text into a clean key, and XLOOKUP does what it always does with a key: an exact lookup. The two jobs stay separate, so you can check the extracted key on its own.

The text before and after the number does not matter. Only the shape INV- plus six digits is searched.

More to watch out for

  • A longer digit string is cut after six digits. FACT INV-2504181 HOFMAN gives INV-250418 and so the wrong customer. End the pattern with (?![0-9]), which means "not followed by another digit": INV-[0-9]{6}(?![0-9]) then finds no match in that text, and you see the #N/A.

Variations

=IFNA(XLOOKUP(REGEXEXTRACT(E1, "INV-[0-9]{6}"), A2:A7, B2:B7), "No invoice number found")
=IFNA(XLOOKUP(REGEXEXTRACT(E1; "INV-[0-9]{6}"); A2:A7; B2:B7); "No invoice number found")

IFNA catches the #N/A from REGEXEXTRACT as well as from XLOOKUP. With the bank line above the result is still Hofman Aannemers; with TERUGBETALING BTW 3E KWARTAAL it shows the message.

=VALUE(REGEXEXTRACT(E1, "[0-9]{6}"))
=VALUE(REGEXEXTRACT(E1; "[0-9]{6}"))

Only the number, as a real number: 250418. REGEXEXTRACT returns text, so VALUE is needed before you calculate with it. The first run of six digits is 250418; the date digits come in groups of 2 and 4.

Example3 of 3Advanced

Find the amount of the last sales entry of a customer

An export of Veldhuis Installatie BV packs the journal, the entry number, the relation and the VAT rate into one key per line, separated by pipes: VRK|25-0407|D1002|21%.

You want the amount of the last sales entry (journal VRK) of the customer in E1. A memorial entry (MEM) for the same customer comes later in the list and must not count.

Sheet excerpt: E2 holds the formula
RowABCDEF
1Journal keyAmountCustomerD1002
2VRK|25-0388|D1002|21%€1,210.00€ 1.210,00Amount€2,420.00€ 2.420,00=XLOOKUP("^VRK\|[0-9]{2}-[0-9]{4}\|" & E1 & "\|", A2:A8, B2:B8, "Not found", 3, -1)=XLOOKUP("^VRK\|[0-9]{2}-[0-9]{4}\|" & E1 & "\|"; A2:A8; B2:B8; "Not found"; 3; -1)
3VRK|25-0391|D1005|21%€605.00€ 605,00
4VRK|25-0395|D1002|9%€218.00€ 218,00
5INK|25-0402|C2001|21%€363.00€ 363,00
6VRK|25-0407|D1002|21%€2,420.00€ 2.420,00
7VRK|25-0409|D1020|21%€726.00€ 726,00
8MEM|25-0414|D1002|21%€150.00€ 150,00
9
Formula cell (E2)Input

Formula in E2

=XLOOKUP("^VRK\|[0-9]{2}-[0-9]{4}\|" & E1 & "\|", A2:A8, B2:B8, "Not found", 3, -1)
=XLOOKUP("^VRK\|[0-9]{2}-[0-9]{4}\|" & E1 & "\|"; A2:A8; B2:B8; "Not found"; 3; -1)

Result: €2,420.00€ 2.420,00

Step by step
  1. E1 holds D1002. The three pieces join into the pattern ^VRK\|[0-9]{2}-[0-9]{4}\|D1002\|. Read it in parts: ^VRK the key starts with VRK; \| a real pipe, because a bare | would mean "or"; [0-9]{2}-[0-9]{4} an entry number such as 25-0407; D1002 the customer; \| a real pipe after it.
  2. The fifth argument 3 is the match mode for regular expressions: the lookup value is read as a pattern, and a cell matches when the pattern is found in it. The sixth argument -1 searches from the last cell to the first.
  3. XLOOKUP starts at the bottom of A2:A8. A8 (MEM|25-0414|D1002|21%): the customer fits but the key starts with MEM, and ^VRK fails. No. A7 (VRK|25-0409|D1020|21%): VRK and an entry number fit, but after the pipe the pattern needs D1002| and the text has D1020|. No.
  4. A6 (VRK|25-0407|D1002|21%): VRK, entry number 25-0407, D1002 and a pipe all fit. Yes, and the search stops. A6 is the fifth item of A2:A8, so XLOOKUP returns the fifth item of B2:B8, which is B6: €2,420.00€ 2.420,00.
  5. Without the sixth argument the search runs from the top and stops at A2 (VRK|25-0388|D1002|21%): €1,210.00€ 1.210,00, the first entry instead of the last. Without ^VRK in the pattern the last hit would be the memorial entry A8: €150.00€ 150,00.
  6. Change E1 to D1005 and only A3 fits (VRK|25-0391|D1005|21%): the result is €605.00€ 605,00, whichever direction you search.

Watch out: An unescaped pipe changes the pattern into alternatives. ^VRK|[0-9]{2}-[0-9]{4}|D1002| ends with an empty alternative that fits every text, so A8 is found first from the bottom and the result is €150.00€ 150,00. Always write \| for a real pipe.

More on this example: why it works, 2 more pitfalls, 2 variations

Why it works

In match mode 3 the cell has to contain the pattern, so the anchor ^ and the closing pipe decide which cells count: the journal at the start and the whole customer code, so D1002 never fits D10025.

The search mode still works as usual: -1 returns the last match. Binary search (2 or -2) makes no sense with a pattern, because the list is not sorted by what the pattern describes.

More to watch out for

  • Mode 3 is case sensitive. With d1002 in E1 nothing fits and you see Not found. Start the pattern with (?i) to ignore case, as in "(?i)^VRK...".
  • In Excel 2021 or 2024 this formula returns #VALUE!, because match mode 3 exists only in Microsoft 365. Check the version of everyone who opens the file.

Variations

=XMATCH("^VRK\|[0-9]{2}-[0-9]{4}\|" & E1 & "\|", A2:A8, 3, -1)
=XMATCH("^VRK\|[0-9]{2}-[0-9]{4}\|" & E1 & "\|"; A2:A8; 3; -1)

The position instead of the amount: 5, the fifth cell of A2:A8. In XMATCH the match mode is the third argument and the search mode the fourth.

=XLOOKUP("^VRK\|[0-9]{2}-[0-9]{4}\|" & E1 & "\|", A2:A8, B2:B8, "Not found", 3)
=XLOOKUP("^VRK\|[0-9]{2}-[0-9]{4}\|" & E1 & "\|"; A2:A8; B2:B8; "Not found"; 3)

Without the search mode the first match counts: €1,210.00€ 1.210,00, the entry in A2.

Common mistakes

Common mistakes
SymptomCauseFix
REGEXTEST returns TRUE for a number that is too longINV-2504140 (seven digits) passes the test INV-[0-9]{6}.The pattern is searched anywhere in the text, and six digits do occur inside seven. Add ^ at the start and $ at the end when the whole cell must fit.
Lowercase invoice number is rejectedThe pattern ^INV-[0-9]{6}$ gives FALSE for inv-250413.Regular expressions are case sensitive by default, unlike wildcards. Start the pattern with (?i), or set the case sensitivity argument of the REGEX functions to 1.
A pattern matches more than expected25.04 also matches 25X04, and VRK|25-0388 is read as two alternatives.A . stands for any character and a | means "or". Write \. for a real dot and \| for a real pipe. The same goes for (, ), [, +, *, ? and $.
A sum over extracted numbers gives 0SUM over cells filled by REGEXEXTRACT returns 0, although the cells show numbers.REGEXEXTRACT always returns text. Wrap it in VALUE to get a number: VALUE(REGEXEXTRACT(...)).
A wildcard-style pattern fits texts without INVYou write INV* as you would for a wildcard, and REGEXTEST says TRUE for BIN 12.In a regular expression * repeats the item before it, so INV* means IN followed by zero or more V. For "INV- and anything" write INV-.*.
#VALUE! from XLOOKUP or XMATCHA formula with match mode 3 works for you and returns #VALUE! for a colleague.Match mode 3 and the REGEX functions exist only in Microsoft 365. Excel 2021 and 2024 do not support them. Check the version of every user, or use a different construction.

Back to the glossary