Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Regular Expressions  Module 4 – Quantifiers: greedy, lazy and possessive

Repetition with *, +, ? and {m,n}

How *, +, ? and braces repeat the atom before them, where {,n} and large counts differ by flavour, and why x* returns empty matches.

  • Beginner
  • 30 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7, Go 1.27.1 and PHP 8.5.11
  • By MySmartCoPilot
Free lesson, Pro answers: 30 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Predict which words *, +, ?, {n}, {n,} and {n,m} accept, and name the atom each quantifier repeats
  • Group several characters with (?:...) when a quantifier must repeat all of them
  • Distinguish six digits in a row from a six-digit number, and add the anchors or boundaries a check needs
  • Compare how JavaScript, Python, PCRE2, PHP and Go read {,n}, spaces inside braces and counts above 1,000
  • Choose + over * when collecting runs, and explain the empty matches that x* returns in each flavour

Before you start

What you’ll learn · Before you start

What you will learn

  • Predict which words *, +, ?, {n}, {n,} and {n,m} accept, and name the atom each quantifier repeats
  • Group several characters with (?:...) when a quantifier must repeat all of them
  • Distinguish six digits in a row from a six-digit number, and add the anchors or boundaries a check needs
  • Compare how JavaScript, Python, PCRE2, PHP and Go read {,n}, spaces inside braces and counts above 1,000
  • Choose + over * when collecting runs, and explain the empty matches that x* returns in each flavour

Before you start

On this page

Search an address for \d{6} and it returns the PIN code 411001, and also 900001, the first six digits of the phone number 9000012345. A quantifier: The part of a pattern that says how many times the atom before it may repeat: * for zero or more, + for one or more, ? for zero or one, or a count in braces such as {2,5}. such as {6} says how many times something repeats, but not which part of the pattern repeats or where the run has to stop. The engines do not even agree on how to write one: Python reads x{,3} as up to three x, JavaScript without a flag takes the five characters x{,3} as plain text, and Go refuses any count above 1,000.

The sections below show what a quantifier attaches to and how a non-capturing group widens it, take the six forms on words whose results you can read, and separate a count from a boundary. Then the brace forms run through JavaScript, Python, PCRE2, PHP and Go, and the last section explains the empty matches that * returns. The exercise asks for three formats with the right counts: a vehicle-plate shape, a phone extension and a laugh.

A quantifier repeats the atom just before it

Take the word banana and the pattern bana+. It looks like “bana, repeated”, but the + belongs to the last a alone: the pattern asks for b, a and n once each, then one or more a. Every quantifier repeats exactly one atom: The single item a quantifier repeats, written right before it: one character, the dot, an escape, a character class or a group. In ab+ the atom is b; in (?:ab)+ it is the group ab., the item written right before it. To repeat the pair na, wrap it in a non-capturing group: Parentheses written (?:...), which make the part inside one atom, for a quantifier or an alternation, without saving the text it matched. Plain parentheses group the same way and also capture that text., (?:na), and the quantifier takes the whole group as its atom. Step through both patterns, first on banana, then on banaaaa:

Interactive · step by step What the + repeats in bana+ and in ba(?:na)+ Step through two patterns that differ only in what their + is attached to, first on banana, then on banaaaa.
What the + repeats in bana+ and in ba(?:na)+, step 7 of 7. Showing b, a, n, a+ (took four a), bana+ matches (banaaaa), b, a, (?:na)+ (took na once), ba(?:na)+ matches (bana). In focus: a+, (?:na)+.bonceaoncenoncea+took four abana+ matchesbanaaaabonceaonce(?:na)+took na onceba(?:na)+ matchesbana

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Four atoms, one quantifier. bana+ is four atoms in a row, b, a, n and a, and the + is attached to the last a alone. So the pattern asks for b, a and n once each, then one or more a.
  2. The + stops at the n. On banana, b, a and n match the first three letters, and a+ takes the a at position 3. The next character is n, not a, so the + has nothing more to take.
  3. The first match is bana. bana+ matches bana, four of the six letters. The second na is never repeated, because the + only ever looks for another a.
  4. Group the pair first. ba(?:na)+ puts n and a inside a non-capturing group. The group is one atom, so the + repeats the pair: b and a once, then na one or more times.
  5. The group repeats twice. On banana the group matches na at positions 2 and 3, then na again at 4 and 5. The text ends there, so the repetition stops after two rounds.
  6. The second match is banana. ba(?:na)+ matches the whole word, banana. The two patterns read the same letters, but the + repeats a different atom in each.
  7. On banaaaa the roles swap. On banaaaa, a+ takes all four a, so bana+ matches the whole word. The group takes na once, and the a at position 4 cannot start another na, so ba(?:na)+ stops at bana.

The two patterns trade results: bana+ takes all of banaaaa but only bana of banana, and ba(?:na)+ does the opposite. The program below runs both, then sets a class beside the group, then puts one quantifier after each kind of atom. Press Run, then Edit: add the word banananana to text and run it again, and the group repeats four times.

JavaScript: what one quantifier repeats JavaScript · atoms-js/atoms.mjs
// A quantifier repeats the one atom right before it: in bana+ that is the last a, in ba(?:na)+ the group na.
const text = 'banana banaaaa';
for (const re of [/bana+/g, /ba(?:na)+/g]) {
  console.log(`${String(re).padEnd(13)} ${JSON.stringify(text.match(re))}`);
}

// A class repeats one character from a set, in any mix; a group repeats its whole sequence.
const words = 'banaan baaaa banana';
for (const re of [/ba[na]+/g, /ba(?:na)+/g]) {
  console.log(`${String(re).padEnd(13)} on ${JSON.stringify(words)} ${JSON.stringify(words.match(re))}`);
}

// An atom can be one character, the dot, an escape, a class or a group.
console.log('');
const atoms = [
  [/colou?r/g, 'color colour colouur'],
  [/.{3}/g, 'abcdefgh'],
  [/\.{3}/g, 'wait... what.'],
  [/[0-9]{4}/g, 'in 2026, 7 or 1999'],
  [/(?:ab){2}/g, 'ab abab ababab'],
];
for (const [re, subject] of atoms) {
  console.log(`${String(re).padEnd(13)} on ${JSON.stringify(subject).padEnd(24)} ${JSON.stringify(subject.match(re))}`);
}

// A quantifier cannot follow another quantifier: group the first one to repeat it.
console.log('');
for (const source of ['x{2}{3}', '(?:x{2}){3}']) {
  try {
    const whole = new RegExp(`^${source}$`);
    console.log(`${source.padEnd(13)} matches "xxxxxx": ${whole.test('xxxxxx')}`);
  } catch (e) {
    console.log(`${source.padEnd(13)} ${e.name}`);
  }
}

Output

/bana+/g      ["bana","banaaaa"]
/ba(?:na)+/g  ["banana","bana"]
/ba[na]+/g    on "banaan baaaa banana" ["banaan","baaaa","banana"]
/ba(?:na)+/g  on "banaan baaaa banana" ["bana","banana"]

/colou?r/g    on "color colour colouur"   ["color","colour"]
/.{3}/g       on "abcdefgh"               ["abc","def"]
/\.{3}/g      on "wait... what."          ["..."]
/[0-9]{4}/g   on "in 2026, 7 or 1999"     ["2026","1999"]
/(?:ab){2}/g  on "ab abab ababab"         ["abab","abab"]

x{2}{3}       SyntaxError
(?:x{2}){3}   matches "xxxxxx": true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node atoms.mjs

The middle lines show the kinds of atom. colou?r accepts color and colour, since ? makes the one u optional, and refuses colouur. .{3} takes any three characters at a time, while \.{3} takes three real dots. [0-9]{4} repeats the class of Character classes, ranges and negation, so 2026 and 1999 both match although their digits differ. (?:ab){2} finds abab twice, once in abab and once at the start of ababab, whose last ab is left over. The last two lines show what the grammar refuses, a count written right after another count: x{2}{3} is a SyntaxError, and the group in (?:x{2}){3} says it properly, six x. A ? right after a quantifier, as in x+?, is not a second count: every flavour here reads it as a change in how the first one matches, and Python, PCRE2 and PHP read a + there the same way, while JavaScript and Go refuse it. The next two lessons of this module explain both.

A class where a group was meant

Wrong: ba[na]+ to repeat the syllable na. A class is one character from a set, so [na]+ takes n and a in any order, and the run shows ba[na]+ accepting banaan and baaaa as well as banana. Right: ba(?:na)+, which repeats the two characters in their order.

Plain parentheses group too, and they also save the text they matched as a group: A part of a pattern wrapped in parentheses whose matched text the engine remembers separately, for reuse or extraction. More in “What a regular expression is and where it runs”. That changes what some functions return: Python’s re.findall hands back the saved text instead of the whole match.

Python: findall with a non-capturing and a capturing group Python · atoms-py/atoms.py
"""Grouping before a quantifier in Python: findall returns what a capturing group saved, not the whole match."""

import re

text = "banana banaaaa"
for pattern in [r"ba(?:na)+", r"ba(na)+"]:
    print(f"{pattern:11} findall {re.findall(pattern, text)}")

print()
for m in re.finditer(r"ba(na)+", text):
    print(f"match {m.group(0)!r:9} group 1 {m.group(1)!r}")

Output

ba(?:na)+   findall ['banana', 'bana']
ba(na)+     findall ['na', 'na']

match 'banana'  group 1 'na'
match 'bana'    group 1 'na'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 atoms.py

With (?:na) the list holds banana and bana; with (na) it holds na twice, the text of the last repetition each match saved. Capturing groups, their numbers and their names belong to a later module. When the only aim is to repeat several characters, (?:...) is the group to write.

Key idea

A quantifier repeats the single atom right before it: one character, the dot, an escape, a class or a group. To repeat several characters, make them one atom with (?:...).

Checkpoint

Check: What a quantifier repeats

  1. Question 1 of 2 Which of these words does /^colou?r$/ accept?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • colour
    • color

    A quantifier repeats only the atom right before it. In colou?r that atom is the u, so the pattern accepts color and colour and nothing else: two u are one too many, and every other letter must be there once.

  2. Question 2 of 2 Which pattern repeats the pair ab, so that it matches ab, abab and ababab as whole words?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: ^(?:ab)+$

    To repeat several characters, make them one atom with (?:...). ab+ repeats the b alone, and a class repeats one character from its set in any mix, which is the mistake the lesson's run shows with ba[na]+.

Six quantifiers set a lowest and a highest count

Four words differ only in how many o they hold: gal has none, goal one, gooal two and gooooal four. Each quantifier allows a range of repetitions between two repetition bound: The lowest or the highest count a quantifier allows, such as 3 and 5 in {3,5}. A missing highest bound, as in {3,}, means no limit, though each engine caps how large a written count may be., a lowest and a highest count, and a missing highest bound means no limit. The six forms, on the o between g and al:

Pattern Lowest Highest Whole words it accepts
go*al 0 no limit gal, goal, gooal, gooooal
go+al 1 no limit goal, gooal, gooooal
go?al 0 1 gal, goal
go{2}al 2 2 gooal
go{2,}al 2 no limit gooal, gooooal
go{1,2}al 1 2 goal, gooal

The program checks every row of the table against the four words, then confirms that the one-character forms are short for braces: * is {0,}, + is {1,} and ? is {0,1}. Press Edit, add goooal, with three o, to words, and decide which patterns will accept it before you run it again:

JavaScript: six quantifiers on the o of go…al JavaScript · six-js/six.mjs
// Six quantifiers on the o of go_al: which of four words does each one accept as a whole word?
const words = ['gal', 'goal', 'gooal', 'gooooal'];
console.log('words:', words.map((w) => `${w} (${w.length - 3} o)`).join(', '));
console.log('');

const forms = [
  ['go*al', 'zero or more'],
  ['go+al', 'one or more'],
  ['go?al', 'zero or one'],
  ['go{2}al', 'exactly two'],
  ['go{2,}al', 'two or more'],
  ['go{1,2}al', 'one or two'],
];
const accepts = (pattern) => words.filter((w) => new RegExp(`^${pattern}$`).test(w));
for (const [pattern, says] of forms) {
  console.log(`${pattern.padEnd(10)} ${says.padEnd(13)} ${accepts(pattern).join(' ')}`);
}

// The three one-character quantifiers are short forms of braces.
console.log('');
for (const [short, braces] of [['go*al', 'go{0,}al'], ['go+al', 'go{1,}al'], ['go?al', 'go{0,1}al']]) {
  const same = accepts(short).join() === accepts(braces).join();
  console.log(`${braces.padEnd(10)} accepts the same words as ${short}: ${same}`);
}

Output

words: gal (0 o), goal (1 o), gooal (2 o), gooooal (4 o)

go*al      zero or more  gal goal gooal gooooal
go+al      one or more   goal gooal gooooal
go?al      zero or one   gal goal
go{2}al    exactly two   gooal
go{2,}al   two or more   gooal gooooal
go{1,2}al  one or two    goal gooal

go{0,}al   accepts the same words as go*al: true
go{1,}al   accepts the same words as go+al: true
go{0,1}al  accepts the same words as go?al: true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node six.mjs

All the flavours of this track write the six forms the same way (ECMA-262 §22.2.1, Python’s re, pcre2pattern and Go’s regexp/syntax). A bound counts repetitions of the atom, not characters of the text: [0-9]{4} is four characters, and (?:ab){2} is also four, made of two repetitions of ab. Within its bounds a quantifier takes as many repetitions as it can and gives some back only when the rest of the pattern fails, by the backtracking that How an engine finds a match, step by step shows; Go’s documentation writes “prefer more” beside *, + and the brace ranges, and “prefer one” beside ?. How a quantifier settles on its count, and the lazy forms that prefer fewer, are the subject of the next lesson, Greedy and lazy quantifiers.

Reading the star as anything at all

Wrong: go*al to accept every word that starts with go and ends with al, as a star does in a shell’s file pattern, where it matches any string (POSIX §2.14.2). In a regex * repeats the atom before it, so go*al allows only o between g and al, and refuses gopal. Right: go.*al when anything may stand there, or a class such as go[a-z]*al.

The table and the run agree on every word, and goooal, if you added it, lands in exactly the three rows whose range holds 3: go*al, go+al and go{2,}al.

Key idea

Each quantifier is a range of counts of its atom: * is 0 or more, + 1 or more, ? 0 or 1, and braces write any other range as {n}, {n,} or {n,m}.

Checkpoint

Check: The six quantifier forms

  1. Question 1 of 2 How many of the words gal, goal, gooal and gooooal does ^go{1,3}al$ accept?

    Type a number.

    A hint

    Count the o in each word and keep the words whose count lies from 1 to 3.

    Show the answer: Question 1 of 2

    Answer: 2 words

    gal has 0 o, goal 1, gooal 2 and gooooal 4. The range {1,3} holds 1 and 2 but neither 0 nor 4, so the pattern accepts goal and gooal: two words.

  2. Question 2 of 2 Which brace form means the same as the quantifier ?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: {0,1}

    The three one-character quantifiers are short forms of braces: * is {0,}, + is {1,} and ? is {0,1}. The six example checks each pair on the same four words.

A count finds six digits inside longer numbers too

Each line below ends in a six-digit PIN code, and two of them hold a longer number as well: a ten-digit phone number and a seven-digit order number. \d{6} reads like “a six-digit number”, but it asks only for six digits in a row, starting at any position. The program below runs it, then the same count with \b on each side:

JavaScript: six digits in a row, or a six-digit number JavaScript · pin-js/pin.mjs
// \d{6} asks for six digits in a row at some position: it does not ask that the run of digits stops after six.
const lines = [
  'Asha Rao, 14 MG Road, Pune 411001, phone 9000012345',
  'Order 7300412 for Bengaluru 560001',
  'Ravi Sen, 3 Park Street, Kolkata 700016',
];
for (const re of [/\d{6}/g, /\b\d{6}\b/g]) {
  console.log(String(re));
  for (const line of lines) console.log(`  ${JSON.stringify(line.match(re))}`);
}

// Checking one field: test() succeeds as soon as six digits appear anywhere in it.
console.log('');
for (const field of ['411001', '4110012', 'PIN 411001']) {
  console.log(`${JSON.stringify(field).padEnd(13)} /\\d{6}/ ${String(/\d{6}/.test(field)).padEnd(5)}  /^\\d{6}$/ ${/^\d{6}$/.test(field)}`);
}

Output

/\d{6}/g
  ["411001","900001"]
  ["730041","560001"]
  ["700016"]
/\b\d{6}\b/g
  ["411001"]
  ["560001"]
  ["700016"]

"411001"      /\d{6}/ true   /^\d{6}$/ true
"4110012"     /\d{6}/ true   /^\d{6}$/ false
"PIN 411001"  /\d{6}/ true   /^\d{6}$/ false

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node pin.mjs

\d{6} found 900001 inside 9000012345 and 730041 inside 7300412, numbers that nobody wrote. A count says how many repetitions to take and nothing about what stands before or after them. \b, the word boundary of the previous module, holds between a digit and a space or a comma but not between two digits, so \b\d{6}\b fails inside the longer numbers and returns each PIN code alone. The last three lines test one form field at a time: /\d{6}/ is true for 4110012 and for PIN 411001, because six digits stand somewhere in each, while the anchored ^\d{6}$ accepts only a field that is six digits and nothing else.

Python counts the same way, but its \d takes the decimal digits of every script (Shorthand classes for digits, words and spaces), so \d{6} also accepts ४११००१, the same PIN code in Devanagari digits:

Python: the same counts on Devanagari digits Python · pin-py/pin.py
"""The same count in Python: \\d{6} takes six decimal digits of any script, [0-9]{6} only ASCII ones."""

import re

lines = [
    "Asha Rao, 14 MG Road, Pune 411001, phone 9000012345",
    "पुणे ४११००१",  # Pune and its PIN code, written in Devanagari digits
]
for pattern in [r"\d{6}", r"\b\d{6}\b", r"\b[0-9]{6}\b"]:
    print(f"{pattern:13}", [re.findall(pattern, line) for line in lines])

Output

\d{6}         [['411001', '900001'], ['४११००१']]
\b\d{6}\b     [['411001'], ['४११००१']]
\b[0-9]{6}\b  [['411001'], []]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pin.py

Python’s \b treats those digits as word characters too, so \b\d{6}\b still returns ४११००१. Only the class [0-9], which names the ten ASCII digits, leaves it out.

Checking a field with a count alone

Wrong: /\d{6}/.test(pin) to accept a PIN code field. It is true for 4110012 and for PIN 411001. Right: /^\d{6}$/ in JavaScript, and re.fullmatch(r"[0-9]{6}", pin) in Python, where a bare \d would also let the digits of other scripts through.

Both fixes keep the count as it was, {6}. What changes is the pattern around it: anchors or boundaries decide that the run of digits starts and stops exactly there.

Key idea

A count says how many repetitions, never where they stop: when the whole run must have that length, put \b or anchors around it.

Checkpoint

Check: A count is not a boundary

  1. Question 1 of 2 What does "Call 9000012345".match(/\d{6}/g) return in JavaScript?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: ["900001"]

    A count says how many repetitions, not where the run stops. \d{6} takes the first six digits of the phone number, as the lesson's PIN example shows with 900001 inside 9000012345.

  2. Question 2 of 2 A form field must hold a six-digit code and nothing else. Which JavaScript check does exactly that?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: /^\d{6}$/.test(s)

    A field check needs the whole value to be the run: ^\d{6}$ in JavaScript. Boundaries suit a search inside a longer text, where other words are expected around the number.

Braces that some flavours read as text

x{2,3} means two or three x in every flavour of this track. The forms around it travel less well: x{,3} leaves out the lowest bound, x{1, 3} has a space, x{,} has no number at all, and each engine decides for itself whether such a brace is a quantifier, an error or plain text. JavaScript alone gives two answers, one without a flag and one with u or v. Press Run, then Edit and add a form of your own to forms, such as x{ 2}:

JavaScript: brace forms with no flag, u and v JavaScript · braces-js/braces.mjs
// Brace forms at the edge of the grammar, in JavaScript with no flag, with u and with v.
// "literal text" means the pattern matched its own characters; otherwise it says how many x it accepts.
const forms = ['x{2,3}', 'x{,3}', 'x{1, 3}', 'x{3-5}', 'x{,}', 'x{2,1}', 'x{1001}'];

function reading(source, flags) {
  let whole;
  try {
    whole = new RegExp(`^(?:${source})$`, flags);
  } catch (e) {
    return e.name;
  }
  if (whole.test(source)) return 'literal text';
  const counts = [];
  for (let n = 0; n <= 1100; n++) if (whole.test('x'.repeat(n))) counts.push(n);
  const [low, high] = [counts[0], counts.at(-1)];
  return low === high ? `x ${low} times` : `x ${low} to ${high} times`;
}

console.log(`${'pattern'.padEnd(9)} ${'no flag'.padEnd(16)} ${'u'.padEnd(16)} v`);
for (const source of forms) {
  const cells = ['', 'u', 'v'].map((flags) => reading(source, flags));
  console.log(`${source.padEnd(9)} ${cells[0].padEnd(16)} ${cells[1].padEnd(16)} ${cells[2]}`);
}

Output

pattern   no flag          u                v
x{2,3}    x 2 to 3 times   x 2 to 3 times   x 2 to 3 times
x{,3}     literal text     SyntaxError      SyntaxError
x{1, 3}   literal text     SyntaxError      SyntaxError
x{3-5}    literal text     SyntaxError      SyntaxError
x{,}      literal text     SyntaxError      SyntaxError
x{2,1}    SyntaxError      SyntaxError      SyntaxError
x{1001}   x 1001 times     x 1001 times     x 1001 times

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node braces.mjs

Without a flag, a brace that does not complete a quantifier is plain text: Annex B.1.2 of ECMA-262 lets { and } stand for themselves outside Unicode mode, so x{,3} matches only the five characters x{,3} as text. With u or v the stricter grammar of §22.2.1 applies, and the same forms are SyntaxErrors. x{2,1} fails in every mode, because the first number may not exceed the second (§22.2.1.1), and x{1001} runs, since the grammar sets no upper limit on a count. The other four engines ran the same forms:

Python, Go, PHP and PCRE2: the same brace forms

Python 3.14.8 · braces/braces.py

"""Brace forms in Python's re: what each one means, and the largest count re accepts.

"literal text" means the pattern matched its own characters; otherwise the line says how many x it accepts, testing
every count from 0 to 1,100. A count too large to test that way is reported as compiled."""

import re


def reading(source: str) -> str:
    try:
        whole = re.compile(f"(?:{source})")
    except re.error as e:
        return f"error: {e.msg}"
    except OverflowError as e:
        return f"OverflowError: {e}"
    if whole.fullmatch(source):
        return "literal text"
    counts = [n for n in range(1101) if whole.fullmatch("x" * n)]
    if not counts:
        return "compiles"
    if counts[-1] == 1100:
        return f"x {counts[0]} or more times"
    low, high = counts[0], counts[-1]
    return f"x {low} times" if low == high else f"x {low} to {high} times"


forms = ["x{2,3}", "x{,3}", "x{1, 3}", "x{3-5}", "x{,}", "x{2,1}", "x{1001}", "x{4294967294}", "x{4294967295}"]
for source in forms:
    print(f"{source:14} {reading(source)}")

Output

x{2,3}         x 2 to 3 times
x{,3}          x 0 to 3 times
x{1, 3}        literal text
x{3-5}         literal text
x{,}           x 0 or more times
x{2,1}         error: min repeat greater than max repeat
x{1001}        x 1001 times
x{4294967294}  compiles
x{4294967295}  OverflowError: the repetition number is too large

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 braces.py

Go 1.27.1 · braces/braces.go

// Brace forms in Go's regexp package (RE2 syntax), and its limit of 1000 on every count, nested counts included.
// "literal text" means the pattern matched its own characters; otherwise the line says how many x it accepts.
package main

import (
	"fmt"
	"regexp"
	"strings"
)

func reading(source string) string {
	whole, err := regexp.Compile("^(?:" + source + ")$")
	if err != nil {
		return "error: " + strings.TrimPrefix(err.Error(), "error parsing regexp: ")
	}
	if whole.MatchString(source) {
		return "literal text"
	}
	low, high := -1, -1
	for n := 0; n <= 1100; n++ {
		if whole.MatchString(strings.Repeat("x", n)) {
			if low < 0 {
				low = n
			}
			high = n
		}
	}
	if low == high {
		return fmt.Sprintf("x %d times", low)
	}
	return fmt.Sprintf("x %d to %d times", low, high)
}

func main() {
	forms := []string{"x{2,3}", "x{,3}", "x{1, 3}", "x{3-5}", "x{,}", "x{2,1}", "x{1000}", "x{1001}", "(?:x{100}){10}", "(?:x{100}){11}"}
	for _, source := range forms {
		fmt.Printf("%-15s %s\n", source, reading(source))
	}
}

Output

x{2,3}          x 2 to 3 times
x{,3}           literal text
x{1, 3}         literal text
x{3-5}          literal text
x{,}            literal text
x{2,1}          error: invalid repeat count: `{2,1}`
x{1000}         x 1000 times
x{1001}         error: invalid repeat count: `{1001}`
(?:x{100}){10}  x 1000 times
(?:x{100}){11}  error: invalid repeat count: `{11}`

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run braces.go

PHP 8.5.11 · braces/braces.php

<?php
// Brace forms in PHP's preg functions, which run the PCRE2 library bundled with PHP.
// "literal text" means the pattern matched its own characters; otherwise the line says how many x it accepts.
function reading(string $source): string
{
    $whole = '/^(?:' . $source . ')$/';
    if (@preg_match($whole, '') === false) {
        $message = error_get_last()['message'] ?? 'unknown';
        error_clear_last();
        return 'error: ' . preg_replace('/^preg_match\(\): Compilation failed: | at offset \d+$/', '', $message);
    }
    if (preg_match($whole, $source)) {
        return 'literal text';
    }
    $counts = [];
    for ($n = 0; $n <= 1100; $n++) {
        if (preg_match($whole, str_repeat('x', $n))) {
            $counts[] = $n;
        }
    }
    if (!$counts) {
        return 'compiles';
    }
    [$low, $high] = [$counts[0], end($counts)];
    return $low === $high ? "x $low times" : "x $low to $high times";
}

printf("PCRE2 in this PHP: %s\n\n", explode(' ', PCRE_VERSION)[0]);
foreach (['x{2,3}', 'x{,3}', 'x{1, 3}', 'x{ 1 , 3 }', 'x{3-5}', 'x{,}', 'x{2,1}', 'x{1001}', 'x{65535}', 'x{65536}'] as $source) {
    printf("%-11s %s\n", $source, reading($source));
}

Output

PCRE2 in this PHP: 10.44

x{2,3}      x 2 to 3 times
x{,3}       x 0 to 3 times
x{1, 3}     x 1 to 3 times
x{ 1 , 3 }  x 1 to 3 times
x{3-5}      literal text
x{,}        literal text
x{2,1}      error: numbers out of order in {} quantifier
x{1001}     x 1001 times
x{65535}    compiles
x{65536}    error: number too big in {} quantifier

Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php braces.php

PCRE2 10.49 · braces/braces_pcre2.py

"""Brace forms in PCRE2 10.49 through pcre2test.

Each pattern is anchored with ^ and $, so a subject line either matches as a whole (" 0:" and the match) or prints
"No match". A pattern that does not compile prints "Failed:" with PCRE2's error number and message, and pcre2test
skips its subject lines."""

from pcre2_session import run

run(r'''
/^x{,3}$/
    xxx
    x{,3}

/^x{1, 3}$/
    xx
    x{1, 3}

/^x{3-5}$/
    xxx
    x{3-5}

/^x{,}$/
    xx
    x{,}

/x{2,1}/
    xx

/x{65535}/
    x

/x{65536}/
    x
''')

Output

$ pcre2test -q patterns.txt   (PCRE2 10.49)
/^x{,3}$/
    xxx
 0: xxx
    x{,3}
No match

/^x{1, 3}$/
    xx
 0: xx
    x{1, 3}
No match

/^x{3-5}$/
    xxx
No match
    x{3-5}
 0: x{3-5}

/^x{,}$/
    xx
No match
    x{,}
 0: x{,}

/x{2,1}/
Failed: error 104 at offset 5: numbers out of order in {} quantifier
        here: x{2,1 |<--| }
    xx

/x{65535}/
    x
No match

/x{65536}/
Failed: error 105 at offset 7: number too big in {} quantifier
        here: x{65536 |<--| }
    x

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 braces_pcre2.py

braces/pcre2_session.py

"""Runs a script of patterns and subjects through pcre2test, the test program of the PCRE2 10.49 library, and prints
what it printed.

pcre2test reads a pattern line such as /a.b/ (with modifiers after the closing slash, for example /a.b/dotall,utf),
then one subject per indented line, and prints each subject with " 0:" and the matched text (" 1:" and on for groups)
or "No match". A line starting with # is a comment and is printed as it is. Escapes in a subject such as \\r, \\n or
\\x{2028} stand for those characters (pcre2test's own subject escapes), so invisible characters can be tested.

The script is given in the example file itself, so the example's run record changes whenever the script does:

    from pcre2_session import run
    run(r'''
    /a.b/
        a\\rb
    ''')

The version banner and its release date are not printed (pcre2test -q); the session checks the version itself and
stops if this computer's pcre2test is not PCRE2 10.49, so a record never shows another version's behaviour.
"""

import re
import subprocess
import sys
import tempfile
from pathlib import Path

VERSION = "10.49"
_CANDIDATES = ("/opt/homebrew/bin/pcre2test", "/usr/local/bin/pcre2test")
_ENV = {"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8", "LANG": "C.UTF-8", "TZ": "UTC"}


def _tool() -> str:
    for candidate in _CANDIDATES:
        if Path(candidate).is_file():
            return candidate
    sys.exit("pcre2test (PCRE2 10.49) was not found")


def _check_version(tool: str) -> None:
    first = subprocess.run([tool, "-C"], capture_output=True, text=True, env=_ENV).stdout.splitlines()[:1]
    found = re.match(r"PCRE2 version (\S+)", first[0]) if first else None
    if not found or found.group(1) != VERSION:
        sys.exit(f"this session needs PCRE2 {VERSION}, found: {first[0] if first else 'nothing'}")


def run(script: str) -> None:
    """Prints the pcre2test transcript of the script (patterns, subjects and results)."""
    tool = _tool()
    _check_version(tool)
    lines = script.strip("\n").splitlines()
    text = "\n".join(_dedent(lines)) + "\n"
    with tempfile.TemporaryDirectory() as folder:
        source = Path(folder) / "patterns.txt"
        source.write_text(text, encoding="utf-8")
        done = subprocess.run([tool, "-q", str(source)], capture_output=True, text=True, encoding="utf-8", env=_ENV)
    print(f"$ pcre2test -q patterns.txt   (PCRE2 {VERSION})")
    sys.stdout.write(done.stdout)
    if done.stderr:
        sys.stdout.write(done.stderr)
    if done.returncode != 0:
        print(f"[exit status {done.returncode}]")
    sys.stdout.flush()


def _dedent(lines: list[str]) -> list[str]:
    """Removes the indentation the whole script shares, so it can sit indented inside a Python call."""
    widths = [len(line) - len(line.lstrip(" ")) for line in lines if line.strip()]
    cut = min(widths) if widths else 0
    return [line[cut:] for line in lines]

Python reads a missing lowest bound as 0 and a missing highest bound as no limit, as its documentation says, so x{,3} is zero to three x and x{,} is the same as x*. PCRE2 10.49, and PHP 8.5.11 with the PCRE2 10.44 it bundles, read x{,3} as x{0,3} and allow spaces inside the braces, yet keep the {,} of x{,} as three characters of text (pcre2pattern, Repetition). Go reads all three as text. Put together, in the flavour: A particular regular-expression syntax and feature set, such as ECMAScript, Python re, PCRE2 or RE2; a pattern is written for one of them. More in “What a regular expression is and where it runs” of this track:

Form A quantifier in Plain text or an error in
x{,3} Python; PCRE2 10.43 or later; PHP 8.4 or later Go: text; JavaScript: text, or a SyntaxError with u or v
x{1, 3} PCRE2 10.43 or later; PHP 8.4 or later Python and Go: text; JavaScript: text, or a SyntaxError with u or v
x{,} Python, as x{0,} PCRE2, PHP and Go: text; JavaScript: text, or a SyntaxError with u or v
x{2,1} none every engine: an error

Since PCRE2 10.43 and PHP 8.4

Perl 5.34.0 began to read {,3} as {0,3}, and PCRE2 followed in 10.43, which also allowed spaces after {, before } and around the comma (its ChangeLog, items 29 and 30 of 10.43). PHP 8.4 bundles PCRE2 10.44, so in PHP a pattern such as \d{,8} changed meaning: before 8.4 its braces were plain text.

Each engine also caps the count you may write, and the caps differ widely. The runs above reached each one:

Engine Largest count it accepts
JavaScript no limit in the grammar: x{1001} runs
Python re 4,294,967,294; one more is an OverflowError
PCRE2 and PHP 65,535; one more fails with “number too big”
Go regexp 1,000, also for a count that nesting multiplies

Go’s documentation says its counting forms reject any form that creates a minimum or maximum count above 1,000 (regexp/syntax), and a nested count creates one: (?:x{100}){10} is accepted, a thousand x, while (?:x{100}){11} is refused at the outer count.

Key idea

Write both bounds and no spaces, x{0,3} rather than x{,3} or x{0, 3}, and keep counts at 1,000 or less where Go may run the pattern: that form means the same in every flavour.

Checkpoint

Check: Brace forms and limits by flavour

  1. Question 1 of 2 What does new RegExp("x{,3}", "u") do in JavaScript?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It throws a SyntaxError

    The braces example shows x{,3} as literal text with no flag and a SyntaxError with u and with v. Writing both bounds, x{0,3}, works in every mode.

  2. Question 2 of 2 Which of these patterns does Go's regexp package refuse to compile?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • (?:x{100}){11}
    • x{1001}

    Go's regexp/syntax documentation rejects counting forms that create a minimum or maximum count above 1,000, and a nested count multiplies: (?:x{100}){10} is the largest of these that Go accepts.

A quantifier that can match nothing returns empty matches

x* allows zero repetitions, so it succeeds even at a position where no x stands, by matching nothing. Such an empty match: A match of length zero: the pattern succeeds at a position without taking any character, as x* does where no x stands. Global searches, splits and replacements report such matches, each engine by its own rules. is a real match whose length is zero, and a global search in JavaScript over axxb finds three of them besides xx. Step through the search one position at a time:

Interactive · step by step How a global search for x* walks the text axxb Step through the search one position at a time: where each match starts, why some are empty, and which one Go leaves out.
How a global search for x* walks the text axxb, step 8 of 8. Showing Position 0, Position 1, Position 2 (inside match 2), Position 3, Position 4, Match 2: xx. In focus: Match 2: xx.Position 0before aPosition 1before xPosition 2inside match 2Position 3before bPosition 4the endMatch 2: xxpositions 1 and 2

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Four characters, five positions. axxb has four characters and five positions, 0 to 4: one before each character and one at the end. A global search tries them from the left, as every search does.
  2. Zero x at position 0. At position 0 the character is a. x* may repeat x zero times, so it succeeds there without taking a character: match 1 is the empty string at position 0.
  3. The search moves one on. A search that began at position 0 again would find the same empty match for ever. So after an empty match the next search starts one position on: JavaScript adds one to lastIndex.
  4. x* takes both x. At position 1, x* takes both x characters, as many as it can. Match 2 is xx, which covers positions 1 and 2 and ends at position 3.
  5. No search starts at 2. The next search starts where match 2 ended, at position 3. Position 2 lies inside xx, so no search ever starts there.
  6. An empty match after xx. At position 3 the character is b, so x* matches empty again: match 3. JavaScript, Python, PHP and PCRE2 keep it. Go ignores an empty match that touches the match before it, so Go returns three matches.
  7. The end of the text. Position 4 is the end of the text, and x* matches empty there too: match 4. Position 5 would lie past the end, so the search stops with four matches: empty, xx, empty, empty.
  8. With x+ only xx is left. x+ needs at least one x, so it can never match empty. Its only match in axxb is xx at position 1, which is why + is the quantifier for collecting runs.

The JavaScript run prints the four matches with their positions, then what split and replace make of them, and then the same search with x+. Press Edit, change 'axxb' to 'xaxx', and work out the matches before you run it again:

JavaScript: empty matches of x*, and x+ for contrast JavaScript · empty-js/empty.mjs
// x* may repeat x zero times, so it also matches the empty string at every position where no x starts.
const text = 'axxb';
for (const source of ['x*', 'x+']) {
  const global = new RegExp(source, 'g');
  const found = [...text.matchAll(global)].map((m) => `${m.index}:${JSON.stringify(m[0])}`);
  console.log(`/${source}/ on "${text}"`);
  console.log(`  matchAll  ${found.join(' ')}`);
  console.log(`  split     ${JSON.stringify(text.split(new RegExp(source)))}`);
  console.log(`  replace   ${JSON.stringify(text.replace(global, '-'))}`);
}

// Collecting the numbers of a line: \d* also returns an empty string at every position without a digit.
const line = 'Order 42, qty 7';
const star = line.match(/\d*/g);
console.log('');
console.log(`\\d* on "${line}": ${star.length} strings, ${star.filter((s) => s === '').length} of them empty`);
console.log(`\\d+ on "${line}": ${JSON.stringify(line.match(/\d+/g))}`);

// A whole-field check written with * also accepts a field with nothing in it.
console.log('');
for (const re of [/^[0-9]*$/, /^[0-9]+$/]) {
  console.log(`${String(re).padEnd(10)} accepts the empty string: ${re.test('')}`);
}

Output

/x*/ on "axxb"
  matchAll  0:"" 1:"xx" 3:"" 4:""
  split     ["a","b"]
  replace   "-a--b-"
/x+/ on "axxb"
  matchAll  1:"xx"
  split     ["a","b"]
  replace   "a-b"

\d* on "Order 42, qty 7": 15 strings, 13 of them empty
\d+ on "Order 42, qty 7": ["42","7"]

/^[0-9]*$/ accepts the empty string: true
/^[0-9]+$/ accepts the empty string: false

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node empty.mjs

replace writes a dash at each of the four matches, so -a--b- has two dashes between a and b: one for xx and one for the empty match after it. split gives ["a","b"], because JavaScript skips an empty match at the place where the last piece ended (ECMA-262 §22.2.6.14). After an empty match a global search moves one position on (§22.2.6.8), so it never finds the same empty match twice. The other engines agree on most of this, but not all of it:

Python, Go, PHP and PCRE2: empty matches of x* and x+

Python 3.14.8 · empty/empty.py

"""Empty matches in Python's re: finditer, findall, split and sub with x* and x+."""

import re

text = "axxb"
for pattern in [r"x*", r"x+"]:
    found = " ".join(f"{m.start()}:{m.group()!r}" for m in re.finditer(pattern, text))
    print(f"{pattern} on {text!r}")
    print(f"  finditer  {found}")
    print(f"  findall   {re.findall(pattern, text)}")
    print(f"  split     {re.split(pattern, text)}")
    print(f"  sub       {re.sub(pattern, '-', text)!r}")

line = "Order 42, qty 7"
star = re.findall(r"\d*", line)
print()
print(f"\\d* on {line!r}: {len(star)} strings, {star.count('')} of them empty")
print(f"\\d+ on {line!r}: {re.findall(r'\d+', line)}")

Output

x* on 'axxb'
  finditer  0:'' 1:'xx' 3:'' 4:''
  findall   ['', 'xx', '', '']
  split     ['', 'a', '', 'b', '']
  sub       '-a--b-'
x+ on 'axxb'
  finditer  1:'xx'
  findall   ['xx']
  split     ['a', 'b']
  sub       'a-b'

\d* on 'Order 42, qty 7': 15 strings, 13 of them empty
\d+ on 'Order 42, qty 7': ['42', '7']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 empty.py

Go 1.27.1 · empty/empty.go

// Empty matches in Go's regexp package: the FindAll functions ignore an empty match that abuts the match before it.
package main

import (
	"fmt"
	"regexp"
	"strings"
)

func main() {
	text := "axxb"
	for _, source := range []string{`x*`, `x+`} {
		re := regexp.MustCompile(source)
		found := []string{}
		for _, m := range re.FindAllStringIndex(text, -1) {
			found = append(found, fmt.Sprintf("%d:%q", m[0], text[m[0]:m[1]]))
		}
		fmt.Printf("%s on %q\n", source, text)
		fmt.Printf("  FindAllStringIndex  %s\n", strings.Join(found, " "))
		fmt.Printf("  FindAllString       %q\n", re.FindAllString(text, -1))
		fmt.Printf("  Split               %q\n", re.Split(text, -1))
		fmt.Printf("  ReplaceAllString    %q\n", re.ReplaceAllString(text, "-"))
	}

	line := "Order 42, qty 7"
	star := regexp.MustCompile(`\d*`).FindAllString(line, -1)
	empty := 0
	for _, s := range star {
		if s == "" {
			empty++
		}
	}
	fmt.Println()
	fmt.Printf("\\d* on %q: %d strings, %d of them empty\n", line, len(star), empty)
	fmt.Printf("\\d+ on %q: %q\n", line, regexp.MustCompile(`\d+`).FindAllString(line, -1))
}

Output

x* on "axxb"
  FindAllStringIndex  0:"" 1:"xx" 4:""
  FindAllString       ["" "xx" ""]
  Split               ["a" "b"]
  ReplaceAllString    "-a-b-"
x+ on "axxb"
  FindAllStringIndex  1:"xx"
  FindAllString       ["xx"]
  Split               ["a" "b"]
  ReplaceAllString    "a-b"

\d* on "Order 42, qty 7": 13 strings, 11 of them empty
\d+ on "Order 42, qty 7": ["42" "7"]

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run empty.go

PHP 8.5.11 · empty/empty.php

<?php
// Empty matches in PHP's preg functions: preg_match_all, preg_split and preg_replace with x* and x+.
$text = 'axxb';
foreach (['/x*/', '/x+/'] as $pattern) {
    preg_match_all($pattern, $text, $found, PREG_OFFSET_CAPTURE);
    $at = array_map(fn ($m) => $m[1] . ':' . json_encode($m[0]), $found[0]);
    printf("%s on \"%s\"\n", $pattern, $text);
    printf("  preg_match_all  %s\n", implode(' ', $at));
    printf("  preg_split      %s\n", json_encode(preg_split($pattern, $text)));
    printf("  preg_replace    %s\n", json_encode(preg_replace($pattern, '-', $text)));
}

$line = 'Order 42, qty 7';
preg_match_all('/\d*/', $line, $star);
preg_match_all('/\d+/', $line, $plus);
$empty = count(array_filter($star[0], fn ($s) => $s === ''));
printf("\n\\d* on \"%s\": %d strings, %d of them empty\n", $line, count($star[0]), $empty);
printf("\\d+ on \"%s\": %s\n", $line, json_encode($plus[0]));

Output

/x*/ on "axxb"
  preg_match_all  0:"" 1:"xx" 3:"" 4:""
  preg_split      ["","a","","b",""]
  preg_replace    "-a--b-"
/x+/ on "axxb"
  preg_match_all  1:"xx"
  preg_split      ["a","b"]
  preg_replace    "a-b"

\d* on "Order 42, qty 7": 15 strings, 13 of them empty
\d+ on "Order 42, qty 7": ["42","7"]

Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php empty.php

PCRE2 10.49 · empty/empty_pcre2.py

"""Empty matches in PCRE2 10.49 through pcre2test.

The g modifier finds every match, as a global search does in Perl: after an empty match, pcre2test searches again from
the same point with the PCRE2_NOTEMPTY_ATSTART option, which refuses a second empty match there. aftertext prints the
rest of the subject after each match on a " 0+" line, which shows where the match ended."""

from pcre2_session import run

run(r'''
/x*/g,aftertext
    axxb

/x+/g,aftertext
    axxb
''')

Output

$ pcre2test -q patterns.txt   (PCRE2 10.49)
/x*/g,aftertext
    axxb
 0: 
 0+ axxb
 0: xx
 0+ b
 0: 
 0+ b
 0: 
 0+ 

/x+/g,aftertext
    axxb
 0: xx
 0+ b

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 empty_pcre2.py

Engine Matches of x* split Replace with -
JavaScript 4: at 0, 1, 3, 4 a, b -a--b-
Python 4: at 0, 1, 3, 4 '', a, '', b, '' -a--b-
PHP 4: at 0, 1, 3, 4 '', a, '', b, '' -a--b-
Go 3: at 0, 1, 4 a, b -a-b-

Go ignores an empty match that touches the match before it (the regexp documentation), so the empty match at position 3, right after xx, is not reported, and its replacement has one dash between a and b. Python’s and PHP’s split cut at every match, empty ones included, so their lists start and end with an empty string. pcre2test finds the same four matches as PHP: after an empty match it searches again at the same point for one that is not empty, which is what Perl does (pcre2test, Finding all matches), and its 0+ lines show where each match ended.

The difference matters as soon as you collect. On Order 42, qty 7, \d* returns 15 strings, 13 of them empty, in JavaScript, Python and PHP, and 13 strings with 11 empty in Go; \d+ returns 42 and 7 everywhere. The same zero lets a check through: the last lines of the JavaScript run show ^[0-9]*$ accepting the empty string, which ^[0-9]+$ refuses.

Collecting with a star

Wrong: line.match(/\d*/g) to collect the numbers of a line; it returns an empty string at every position without a digit. Right: line.match(/\d+/g), which can only match runs of at least one digit.

A quantifier whose lowest bound is 0 can always match nothing, and ? is such a quantifier too, so colou?r never matches empty but u? alone would.

Key idea

Use + when you collect, split or replace runs, and keep * and ? for places where nothing at all is a valid answer: wherever a pattern can match nothing, a global search returns empty matches.

Checkpoint

Check: Empty matches of * and ?

  1. Question 1 of 2 How many strings does "a1b".match(/\d*/g) return in JavaScript?

    Type a number.

    A hint

    Walk the positions 0 to 3 and ask at each one what \d* can take there.

    Show the answer: Question 1 of 2

    Answer: 4 strings

    At position 0 the a is no digit, so \d* matches empty. At 1 it takes the 1. At 2, right after it, the b gives an empty match, and at 3, the end of the text, there is one more: "", "1", "", "", four strings.

  2. Question 2 of 2 For x* on axxb, Go's FindAllString returns three strings where JavaScript returns four. Which match does Go leave out?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The empty match at position 3, right after xx

    The regexp documentation says the All functions ignore empty matches abutting a preceding match. The empty example's Go tab prints 0:"", 1:"xx" and 4:"", and its ReplaceAllString gives -a-b- with one dash between a and b.

Practise: three formats with the right counts

The exercise asks for three patterns, each for a format the exercise itself defines, not the rules of any authority: a vehicle-plate shape with optional spaces, a phone extension of three to five digits, and a laugh made of two or more ha. Each starter pattern carries one typical slip, and the page can run the sample tests on your answer, written in JavaScript or in Python.

Exercise · Easy · JavaScript, Python

Three formats with the right counts

Write three patterns, one for each format below. The formats are defined by this exercise, not by the rules of any registration authority or telephone system. Solve them in JavaScript (patterns.mjs, a regular expression literal for each name) or in Python (patterns.py, the pattern text for each name). The JavaScript tests call test() on one whole field at a time, so each pattern must say where the field starts and ends; the Python tests call re.fullmatch(), which already asks for the whole field.

  1. PLATE: a simplified vehicle-plate shape. Two capital letters A to Z, then two digits, then one or two capital letters, then four digits, such as MH 12 AB 1234 or KA05M0001. Between two neighbouring groups there may be one space or none: no other character, and never two spaces.
  2. EXTENSION: a phone extension of three to five digits 0 to 9, such as 123 or 89012, and nothing else.
  3. LAUGH: two or more ha in a row, in lower case, such as haha or hahaha. A single ha is not a laugh here, and neither is haa.

The starter code holds one common slip in every pattern. The sample tests list what each pattern must accept and what it must refuse.

JavaScript · Starter code · patterns.mjs

// Each name holds a regular expression. The tests run it with test() on one whole field at a time.
// Every pattern below has one common slip: the sample tests show which inputs it gets wrong.

/** An exercise-defined plate shape: two letters, two digits, one or two letters, four digits; one space or none between groups. */
export const PLATE = /^[A-Z]{2}\s*[0-9]{2}\s*[A-Z]*\s*[0-9]{4}$/;

/** A phone extension: three to five digits 0 to 9, and nothing else. */
export const EXTENSION = /\d{3,5}/;

/** A laugh: two or more ha in a row, in lower case. */
export const LAUGH = /^ha+$/;
The sample tests · patterns.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { EXTENSION, LAUGH, PLATE } from './patterns.mjs';

// A copy without g or y, so that test() never carries a position over from one call to the next.
const fresh = (re) => new RegExp(re.source, re.flags.replace(/[gy]/g, ''));

function check(name, re, accept, refuse) {
  const r = fresh(re);
  for (const s of accept) assert.ok(r.test(s), `${name} should accept ${JSON.stringify(s)}`);
  for (const s of refuse) assert.ok(!r.test(s), `${name} should refuse ${JSON.stringify(s)}`);
}

test('PLATE: two letters, two digits, one or two letters, four digits', () => {
  check('PLATE', PLATE, ['MH12AB1234', 'KA05M0001', 'DL01C7777'], [
    'MH12AB123', 'MH12AB12345', 'MH12ABC1234', 'M12AB1234', 'MH1AB1234', 'MH121234', 'mh12ab1234', '',
  ]);
});

test('PLATE: one space or none between two groups, and nothing else', () => {
  check('PLATE', PLATE, ['MH 12 AB 1234', 'KA 05 M 0001', 'MH12 AB1234', 'TN 09C 4321'], [
    'MH  12 AB 1234', 'MH 12 AB  1234', 'MH-12-AB-1234', 'MH\t12 AB 1234', ' MH 12 AB 1234', 'MH 12 AB 1234 ', 'MH 1 2 AB 1234',
  ]);
});

test('EXTENSION: three to five digits 0 to 9, and nothing else', () => {
  check('EXTENSION', EXTENSION, ['123', '4567', '89012', '000'], ['12', '123456', '12a4', '', ' 123', 'ext 123', '१२३']);
});

test('LAUGH: two or more ha in a row, in lower case', () => {
  check('LAUGH', LAUGH, ['haha', 'hahaha', 'hahahahaha'], ['ha', 'haa', 'hah', 'hahah', 'HAHA', 'ahah', 'haha!', '']);
});

Python · Starter code · patterns.py

"""Each name holds the text of a pattern. The tests compile it with re and call fullmatch() on one whole field at a
time, so a pattern needs no anchors here.

Every pattern below has one common slip: the sample tests show which inputs it gets wrong.
"""

# An exercise-defined plate shape: two letters, two digits, one or two letters, four digits; one space or none between
# groups.
PLATE = r"[A-Z]{2}\s*[0-9]{2}\s*[A-Z]*\s*[0-9]{4}"

# A phone extension: three to five digits 0 to 9, and nothing else.
EXTENSION = r"\d{3,5}"

# A laugh: two or more ha in a row, in lower case.
LAUGH = r"ha+"
The sample tests · test_patterns.py
import re
import warnings

from patterns import EXTENSION, LAUGH, PLATE


def compiled(pattern):
    """Compiles the pattern with every warning turned into an error, so a FutureWarning fails the test."""
    with warnings.catch_warnings():
        warnings.simplefilter("error")
        return re.compile(pattern)


def check(name, pattern, accept, refuse):
    r = compiled(pattern)
    for s in accept:
        assert r.fullmatch(s), f"{name} should accept {s!r}"
    for s in refuse:
        assert not r.fullmatch(s), f"{name} should refuse {s!r}"


def test_plate_groups():
    """PLATE: two letters, two digits, one or two letters, four digits"""
    check("PLATE", PLATE, ["MH12AB1234", "KA05M0001", "DL01C7777"],
          ["MH12AB123", "MH12AB12345", "MH12ABC1234", "M12AB1234", "MH1AB1234", "MH121234", "mh12ab1234", ""])


def test_plate_spaces():
    """PLATE: one space or none between two groups, and nothing else"""
    check("PLATE", PLATE, ["MH 12 AB 1234", "KA 05 M 0001", "MH12 AB1234", "TN 09C 4321"],
          ["MH  12 AB 1234", "MH 12 AB  1234", "MH-12-AB-1234", "MH\t12 AB 1234", " MH 12 AB 1234", "MH 12 AB 1234 ",
           "MH 1 2 AB 1234"])


def test_extension():
    """EXTENSION: three to five digits 0 to 9, and nothing else"""
    check("EXTENSION", EXTENSION, ["123", "4567", "89012", "000"], ["12", "123456", "12a4", "", " 123", "ext 123", "१२३"])


def test_laugh():
    """LAUGH: two or more ha in a row, in lower case"""
    check("LAUGH", LAUGH, ["haha", "hahaha", "hahahahaha"], ["ha", "haa", "hah", "hahah", "HAHA", "ahah", "haha!", ""])
A hint

For each group of characters, decide its lowest and its highest count, then write exactly that range: something that may be missing or appear once is ?, and three to five of something is {3,5}. \s* takes tabs and runs of spaces, while a single space followed by ? takes one space or none. A quantifier repeats only the atom right before it, so a syllable that repeats needs a group. In Python, \d also takes the digits of other scripts.

The sample tests run on this device, in your browser (QuickJS, Pyodide): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: JavaScript (about 0.6 MB) or Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Regex Tester Try the forms of this lesson on text of your own. The tester runs your browser's JavaScript engine: with its default g flag, x{,3} is plain text there, and with the u flag it is an error, as in the braces run above.

Then check the five ideas with the self-check below. It starts with the idea you miss most and explains each choice the moment you pick it:

Practice that adapts to you

Practise this lesson

17 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 17 In the pattern bana+, what does the + repeat?

    Choose one answer.

    Show the answer: Question 1 of 17

    Answer: The last a

    bana+ is b, a and n once each, then one or more a. That is why it takes all of banaaaa but only bana of banana in the atoms example.

  2. Question 2 of 17 What does re.findall(r"ba(na)+", "banana") return in Python?

    Choose one answer.

    Show the answer: Question 2 of 17

    Answer: ['na']

    Plain parentheses capture. findall then returns what the group saved instead of the whole match, and for a repeated group that is the last repetition: na. The atoms example prints both forms side by side.

  3. Question 3 of 17 A whole field must be a run of x whose length is a multiple of three: xxx, xxxxxx and so on. Which pattern does that in JavaScript and Python alike?

    Choose one answer.

    Show the answer: Question 3 of 17

    Answer: ^(?:x{3})+$

    To repeat a count, group it first, as (?:x{2}){3} does in the atoms example. A + written right after {3} never repeats the three x: JavaScript and Go refuse it, and Python and PCRE2 read it as the possessive form of the count.

  4. Question 4 of 17 Which quantifier allows zero or more repetitions of its atom?

    Choose one answer.

    Show the answer: Question 4 of 17

    Answer: *

    • accepts gal, goal, gooal and gooooal in the six example, because zero o is as good as any number of them.
  5. Question 5 of 17 Which of these patterns accept goooal, with three o, as a whole word?

    Choose every answer that is right.

    Show the answer: Question 5 of 17

    Answer:

    • go+al
    • go{2,}al
    • go*al

    A word is accepted when its count of o lies between the two bounds of the quantifier. Three lies inside 0 to no limit, 1 to no limit and 2 to no limit, and outside 1 to 2 and 0 to 1.

  6. Question 6 of 17 A teammate writes report*.csv as a regex, meaning file names such as report-2026.csv and report.csv. Which regex says what they meant?

    Choose one answer.

    Show the answer: Question 6 of 17

    Answer: ^report.*\.csv$

    In a shell's file pattern a star matches any string (POSIX 2.14.2), but in a regex * repeats the atom before it. Anything at all is .*, which repeats the dot.

  7. Question 7 of 17 What does \d{6} require of the text it searches?

    Choose one answer.

    Show the answer: Question 7 of 17

    Answer: Six digits in a row, starting at any position

    The PIN example shows the difference: \d{6} finds 900001 and 730041 inside longer numbers, while \b\d{6}\b finds only the six-digit PIN codes.

  8. Question 8 of 17 How many matches does /\b\d{6}\b/g find in "Pune 411001, phone 9000012345"?

    Type a number.

    A hint

    Where does a word boundary stand inside the phone number?

    Show the answer: Question 8 of 17

    Answer: 1 matches

    411001 has a space before it and a comma after it, so both boundaries hold. Inside 9000012345 no boundary stands after the sixth digit, because another digit follows: one match, 411001.

  9. Question 9 of 17 A Python service checks codes with re.fullmatch(r"\d{6}", code). Which code does it accept that re.fullmatch(r"[0-9]{6}", code) refuses?

    Choose one answer.

    Show the answer: Question 9 of 17

    Answer: ४११००१, six Devanagari digits

    The count is the same in both patterns; what differs is the atom. The Python PIN example shows \d{6} taking ४११००१ and [0-9]{6} leaving it out.

  10. Question 10 of 17 How does Python's re read x{,3}?

    Choose one answer.

    Show the answer: Question 10 of 17

    Answer: As zero to three x

    The braces example prints x 0 to 3 times for x{,3} in Python, and also reads x{,} as zero or more x, the same as x*.

  11. Question 11 of 17 Which of these engines read \d{1, 3}, with a space after the comma, as one to three digits?

    Choose every answer that is right.

    Show the answer: Question 11 of 17

    Answer:

    • PHP 8.5
    • PCRE2 10.49

    Spaces inside braces are a quantifier only in PCRE2 10.43 and later and in PHP 8.4 and later. Writing the bounds without spaces, \d{1,3}, gives the same meaning everywhere.

  12. Question 12 of 17 What is the largest count that PCRE2 and PHP accept inside braces?

    Type a number.

    A hint

    One more fails with number too big in {} quantifier.

    Show the answer: Question 12 of 17

    Answer: 65535

    pcre2pattern says the numbers must be no greater than 65,535, and the braces example shows x{65535} compiling while x{65536} fails in both PCRE2 10.49 and PHP 8.5.11.

  13. Question 13 of 17 One pattern must mean zero to three x in JavaScript (with and without u), Python, PCRE2, PHP and Go. Which do you write?

    Choose one answer.

    Show the answer: Question 13 of 17

    Answer: x{0,3}

    The flavour table of the lesson shows every shortcut failing somewhere. The full form x{0,3} is read the same way by all of them.

  14. Question 14 of 17 Which of these patterns can produce an empty match?

    Choose every answer that is right.

    Show the answer: Question 14 of 17

    Answer:

    • x{0,2}
    • x*
    • x?

    A quantifier whose lowest bound is 0 can match the empty string, so a global search with it reports empty matches wherever its atom is missing.

  15. Question 15 of 17 What does re.split(r"x*", "axxb") return in Python?

    Choose one answer.

    Show the answer: Question 15 of 17

    Answer: ['', 'a', '', 'b', '']

    The empty example's Python tab prints ['', 'a', '', 'b', ''], and PHP's preg_split gives the same list. JavaScript and Go both return ["a", "b"].

  16. Question 16 of 17 What does "axxb".replace(/x*/g, "-") return in JavaScript?

    Choose one answer.

    Show the answer: Question 16 of 17

    Answer: -a--b-

    replace writes the replacement once per match. With x* the matches are "", "xx", "" and "", so the two dashes between a and b come from xx and from the empty match after it.

  17. Question 17 of 17 A form checks its quantity field with /^[0-9]*$/. Someone sends the form with the field left blank. What happens, and what is the fix?

    Choose one answer.

    Show the answer: Question 17 of 17

    Answer: The check accepts the blank field; write /^[0-9]+$/ so at least one digit is needed

    A lowest bound of 0 lets a check pass on nothing at all. When a value is required, the quantifier must ask for at least one repetition: + or {1,}.

Interview questions

Warm-up: what does the plus repeat in ab+ and in (?:ab)+, and what do *, + and ? each allow?

A quantifier repeats the one atom written right before it. In ab+ that atom is b, so the pattern matches an a and then one or more b: ab, abb, abbb. In (?:ab)+ the non-capturing group makes ab a single atom, so it matches ab, abab, ababab. * allows zero or more repetitions, + one or more and ? zero or one; they are short for {0,}, {1,} and {0,1}, and braces state any other range, such as {2,4}. Two points show real understanding. A count is not a boundary: \d{6} also finds six digits inside a longer number unless anchors or \b surround it. And * and ? can match the empty string, so a global search with them returns empty matches, which is why + is the safer choice for collecting runs.

Two longer questions follow from the brace and limit sections: one moves a pattern with a missing lower bound between engines, the other meets Go’s cap of 1,000 in rules that people other than the developers write. Sketch an answer to each, then compare it with its page:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    An invoice field is checked with ^INV-\d{,6}$ in a Python service, and its tests succeed. The browser copy of the rule refuses every invoice number people type, and after a PHP server moved from 8.3 to 8.4 it began to accept INV- with no digits. Explain all three, and write one rule that means the same everywhere.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    Administrators of a log-search product write their own match rules, which a Go service compiles. Two rules that worked in the Python prototype now fail with invalid repeat count: [^,]{0,2000} for a field of up to 2,000 characters, and (?:[0-9]{3}-){400}. Why does Go refuse them, and how should the product let administrators express such rules?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Which part of a pattern does a quantifier repeat?

    Only the one atom right before it: a character, the dot, an escape, a class or a group. bana+ repeats the last a, while ba(?:na)+ repeats the pair na.

  2. Which range of counts does each of *, +, ? and the brace forms allow?

    * allows 0 or more, + 1 or more and ? 0 or 1. Braces write any other range: {n} exactly n, {n,} at least n, {n,m} from n to m.

  3. Why does \d{6} return 900001 when it searches a line holding the phone number 9000012345?

    A count fixes how many repetitions, not where the run stops, so six digits inside a longer number match too. Use \b\d{6}\b to search, or anchors such as ^\d{6}$ to check a field.

  4. Why is ba[na]+ the wrong way to repeat the syllable na?

    A class matches one character from a set, so [na]+ takes n and a in any order and also accepts banaan and baaaa. ba(?:na)+ repeats the two characters in their order.

  5. Does go*al match gopal?

    No. In a regex the star repeats the atom before it, so go*al allows only more o's between g and al. A shell's star means any string; in a regex write go.*al, or go[a-z]*al.

  6. Which way of writing a brace count means the same in every flavour of the track?

    Both bounds and no spaces, such as x{0,3}, with counts of at most 1,000. x{,3} is plain text in Go and in JavaScript without a flag, and Go refuses any count above 1,000.

  7. Why does line.match(/\d*/g) return empty strings among the numbers?

    \d* can match nothing, so a global search reports an empty match at each position without a digit. Collect runs with \d+, which needs at least one digit.

Key takeaways

  • A quantifier repeats only the atom right before it, so bana+ repeats the last a while ba(?:na)+ repeats the pair na; a non-capturing group is how several characters become one atom.
  • The six forms are ranges of counts: * is 0 or more, + 1 or more, ? 0 or 1, and {n}, {n,} and {n,m} write any range out, the same way in every flavour of the track.
  • A count is not a boundary: \d{6} finds 900001 inside 9000012345, so a field check needs anchors and a search needs \b around the count.
  • The shortcuts x{,3} and x{1, 3} are quantifiers in PCRE2 10.43 and later, and x{,3} also in Python, but plain text in Go and in JavaScript without a flag; the largest count is 1,000 in Go, 65,535 in PCRE2 and PHP and far higher in Python, while x{0,3} means the same everywhere.
  • A quantifier that can match nothing returns empty matches: JavaScript, Python and PHP also report the one right after a non-empty match, Go leaves it out, and collecting with + avoids them in every engine.

Terms in this lesson

atom
The single item a quantifier repeats, written right before it: one character, the dot, an escape, a character class or a group. In ab+ the atom is b; in (?:ab)+ it is the group ab.
empty match
A match of length zero: the pattern succeeds at a position without taking any character, as x* does where no x stands. Global searches, splits and replacements report such matches, each engine by its own rules.
non-capturing group
Parentheses written (?:...), which make the part inside one atom, for a quantifier or an alternation, without saving the text it matched. Plain parentheses group the same way and also capture that text.
quantifier
The part of a pattern that says how many times the atom before it may repeat: * for zero or more, + for one or more, ? for zero or one, or a count in braces such as {2,5}.
repetition bound
The lowest or the highest count a quantifier allows, such as 3 and 5 in {3,5}. A missing highest bound, as in {3,}, means no limit, though each engine caps how large a written count may be.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.