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.

JavaScript (ES2026)  Module 7 – Strings, Unicode and regular expressions

Regular expressions in JavaScript

Write JavaScript regular expressions as literals or with RegExp, choose the right flags, and use test, exec, match, matchAll, replace and split safely.

  • Intermediate
  • 25 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Create regexes with literals and the RegExp constructor
  • Choose the flags a task needs and predict what each one changes
  • Use test, exec, match, matchAll, replace and split for the job each one does best
  • Avoid the lastIndex trap of global and sticky regexes

Before you start

What you’ll learn · Before you start

What you will learn

  • Create regexes with literals and the RegExp constructor
  • Choose the flags a task needs and predict what each one changes
  • Use test, exec, match, matchAll, replace and split for the job each one does best
  • Avoid the lastIndex trap of global and sticky regexes

Before you start

On this page

A regular expression, or regex, is a short pattern that describes text: /^\d{6}$/ means “six digits and nothing else”. In JavaScript a regex is a RegExp object. You write it as a literal between two slashes, with optional flags such as g or i after the closing slash, or you build it while the program runs with new RegExp(pattern, flags). Two groups of methods use it: the regex’s own test and exec, and the string methods match, matchAll, replace, replaceAll, split and search. Together they check that input has the right shape, pull fields out of a log line, rewrite text and cut it into pieces. This lesson shows how to create a regex, what each flag changes, which method fits which job, and the lastIndex trap: a regex with the g or y flag remembers where its last match ended, so the same call with the same kind of input can answer differently the second time.

A literal or the RegExp constructor

A literal is the pattern itself between slashes, /\d{6}/. It is read together with the rest of the code, so a broken pattern, such as a bracket that is never closed, stops the script with a SyntaxError before any line of it runs. Each time the literal is evaluated, for example on every call of a function that contains it, it creates a new RegExp object.

new RegExp(pattern, flags) takes the pattern as a string. That is what you need when part of the pattern is known only at run time: a field name read from a configuration file, a word someone searches for. The cost of the string is the backslash. In a string literal '\d' is simply the letter d, so the backslash that belongs to the pattern has to be written twice, '\\d'.

The same pattern as a literal and from a string JavaScript · literal-and-constructor.mjs
// The same pattern written two ways: six digits in a row.
const literal = /\d{6}/;
const built = new RegExp('\\d{6}'); // inside a string, the pattern's backslash is written twice

console.log(literal.source, '|', built.source);
console.log(literal.test('Pune 411001'), built.test('Pune 411001'));

// A pattern that is only known when the program runs: build it from a string.
const field = 'pin';
const fieldLine = new RegExp(`^${field}=(\\d+)$`, 'm');
console.log(fieldLine.source, '| flags:', fieldLine.flags);
console.log(fieldLine.exec('name=Asha\npin=411001\ncity=Pune')[1]);

// A copy with other flags: pass a regex and the new flags. The original keeps its own.
const everyMatch = new RegExp(literal, 'g');
console.log(everyMatch.source, '| flags:', everyMatch.flags, '| original flags:', JSON.stringify(literal.flags));

// The usual mistake: '\d' in a string is just 'd', so this pattern looks for six letters d.
const wrong = new RegExp('\d{6}');
console.log(wrong.source, wrong.test('411001'), wrong.test('dddddd'));

Output

\d{6} | \d{6}
true true
^pin=(\d+)$ | flags: m
411001
\d{6} | flags: g | original flags: ""
d{6} false true

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

Both ways produce the same kind of object: source holds the pattern and flags the flags. Passing an existing regex and new flags to the constructor makes a copy with those flags, and the original keeps its own. The last line is the mistake almost everyone makes once: new RegExp('\d{6}') received the string d{6}, so it rejects six digits and accepts six letters d, without any error. A template literal is a handy way to assemble a pattern from pieces, but a piece that comes from outside the program, such as a search term, must be escaped first, or its ., ( and + turn into pattern syntax. RegExp.escape, which does that safely, is in the next lesson of this module.

Regex Tester Paste a pattern and some sample text to see every match, its groups and the result of a replacement as you type.

The pattern language, briefly

The building blocks below cover most patterns in everyday code. Characters without a special meaning match themselves, and the rest combine freely.

Pattern Matches
abc the characters a, b and c, in that order
. any one character except a line break (any at all with the s flag)
\d, \w, \s a digit 0 to 9; an ASCII letter, digit or underscore; a space, tab, line break or other white space
\D, \W, \S any character that \d, \w or \s does not match
[aeiou], [A-Z0-9], [^,] one character from a set or a range; a ^ at the start of the brackets inverts the set
^, $ the start and the end of the text (of every line with the m flag)
\b a word boundary: a position between a \w character and one that is not
x*, x+, x? zero or more, one or more, zero or one x, as many as possible
x{6}, x{2,4}, x{2,} exactly 6, from 2 to 4, at least 2
x+?, x*? as few as possible instead of as many as possible (lazy)
(…), (?:…), (?<name>…) a numbered group that captures its text, a group that captures nothing, a named group
a|b a or b
\., \(, \\ the character itself: a backslash removes a special meaning

Lookahead and lookbehind, Unicode property escapes such as \p{L} and the syntax added in the newest editions are covered in the next lesson.

Flags change how the whole pattern matches

Flags come after the closing slash, /x/gi, or as the second argument of the constructor. Each one has a read-only property with a longer name.

  • d (hasIndices): each result also gives the start and end of every group, in indices.
  • g (global): look for every match; test and exec start where the last match ended.
  • i (ignoreCase): capital and small letters match each other.
  • m (multiline): ^ and $ match at the start and end of every line, not only of the text.
  • s (dotAll): . matches line breaks as well.
  • u (unicode): the text is read by code point, so an emoji is one character; the syntax is stricter, and \p{…} escapes become available.
  • v (unicodeSets): everything u does, plus set operations inside character classes.
  • y (sticky): a match must start exactly at lastIndex.
What i, m, s and u change, and the flags property JavaScript · flags.mjs
const notes = 'Line one\nline two\nLINE THREE';

// i ignores case; m makes ^ and $ match at every line; g finds every match.
console.log(JSON.stringify(notes.match(/^line \w+/gim)));
console.log(JSON.stringify(notes.match(/^line \w+/gi)));

// s lets . match a line break too.
console.log(/one.line/.test(notes), /one.line/s.test(notes));

// u reads the pattern and the text by code point, so one emoji is one character.
console.log('🚆'.length, /^.$/.test('🚆'), /^.$/u.test('🚆'));

// The flags property lists the flags in a fixed order, whatever order you wrote them in.
const re = new RegExp('x', 'yigm');
console.log(re.flags, re.global, re.ignoreCase, re.multiline, re.sticky);

// Unknown, repeated or clashing flags are refused when the regex is made.
for (const flags of ['gg', 'q', 'uv']) {
  try {
    new RegExp('x', flags);
  } catch (e) {
    console.log(JSON.stringify(flags), e.name);
  }
}

Output

["Line one","line two","LINE THREE"]
["Line one"]
false true
2 false true
gimy true true true true
"gg" SyntaxError
"q" SyntaxError
"uv" SyntaxError

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

With i and m together, ^line \w+ finds a match at the start of each of the three lines whatever their case; without m, ^ means only the start of the whole text, so only the first line matches. A dot refuses the line break between “one” and “line” until the s flag allows it. The train emoji is two UTF-16 code units, so /^.$/ sees two characters, while the u flag reads it as the one code point it is. Whatever order you type the flags in, flags lists them in one fixed order, dgimsuvy. A repeated flag, an unknown one, or u together with v is refused with a SyntaxError when the regex is created.

Which method for which job

The regex methods and the string methods overlap. Pick by what you need back:

You need The call, and what it returns
Whether the text matches re.test(text): true or false
The first match, with its groups and position re.exec(text), or text.match(re) without g: a match array, or null
The text of every match text.match(re) with g: an array of strings, or null
Every match, with its groups and position text.matchAll(re), which requires g: an iterator of match arrays
Where the first match starts text.search(re): an index, or -1
A changed copy of the text text.replace(re, …) or text.replaceAll(re, …): a new string
The text cut into pieces text.split(re): an array of strings

search and split ignore the g flag and lastIndex, and leave them as they found them; the others are covered one by one below.

Checking that a whole string has a shape

test answers yes or no. To check a value rather than search inside it, the pattern must be anchored: ^ at the start and $ at the end, so that nothing may come before or after the shape.

The shape of an IFSC

The Reserve Bank of India describes the IFSC (Indian Financial System Code), which identifies a bank branch for NEFT transfers, as 11 characters: the first four are letters for the bank, the fifth is always the digit 0, and the last six identify the branch. The regex /^[A-Z]{4}0[A-Z0-9]{6}$/ says exactly that.

Checking the shape of a bank branch code JavaScript · ifsc-check.mjs
// The shape of an IFSC: 4 letters for the bank, the digit 0, then 6 letters or digits for the branch.
const IFSC = /^[A-Z]{4}0[A-Z0-9]{6}$/;

function ifscShape(input) {
  const code = input.trim().toUpperCase();
  return IFSC.test(code) ? `${code} has the shape of an IFSC` : `${JSON.stringify(input)} is not an IFSC`;
}

for (const input of ['ABCD0123456', ' wxyz0a1b2c3 ', 'ABCD0I23456', 'ABCDO123456', 'ABCD012345', 'ABCD01234567', 'AB1D0123456']) {
  console.log(ifscShape(input));
}

// Without ^ and $, test() is true when the shape appears anywhere in the text.
const unanchored = /[A-Z]{4}0[A-Z0-9]{6}/;
console.log(unanchored.test('XABCD0123456789'), IFSC.test('XABCD0123456789'));

Output

ABCD0123456 has the shape of an IFSC
WXYZ0A1B2C3 has the shape of an IFSC
ABCD0I23456 has the shape of an IFSC
"ABCDO123456" is not an IFSC
"ABCD012345" is not an IFSC
"ABCD01234567" is not an IFSC
"AB1D0123456" is not an IFSC
true false

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

The function cleans the input before testing it: trim() removes the spaces a form often adds, and toUpperCase() lets someone type the code in small letters while the pattern stays strict about the stored value. Two lines of the output deserve a second look. ABCD0I23456 passes, because a capital I is a valid branch character: a regex checks the shape of a value, never whether a branch with that code exists. ABCDO123456, with the letter O in place of the zero, is rejected, which is exactly what the fifth position is for. The last line shows why the anchors matter: without them, test is happy to find a valid-looking code inside a longer, invalid string.

IFSC Code Finder & Validator Once a code has the right shape, look up whether it belongs to a real bank branch.

Getting the parts: exec and match

exec returns the first match as an array: the whole matched text at index 0, then one entry per numbered group, plus a few properties. index is where the match starts, input is the text that was searched, and groups is an object with one property per named group (it is undefined when the pattern has no named groups). When nothing matches, the result is null.

exec and match on a food-order message JavaScript · exec-and-match.mjs
const sms = 'Order 4521: 2 x masala chai, ready. Order 4522: 1 x samosa, on its way.';
const ORDER = /Order (?<id>\d+): (?<qty>\d+) x (?<item>[a-z ]+)/;

// exec returns the first match with its groups and position, or null.
const m = ORDER.exec(sms);
console.log(m[0]);
console.log(m[1], m.groups.item, 'at index', m.index);

// match without the g flag returns the same kind of result as exec.
const { id, qty } = sms.match(ORDER).groups;
console.log(id, Number(qty) * 20);

// With the g flag, match returns only the matched texts: no groups, no positions.
console.log(JSON.stringify(sms.match(/Order \d+/g)));

// No match gives null, so read .length or a group only after checking.
console.log(sms.match(/refund/));
console.log((sms.match(/refund/g) ?? []).length);

Output

Order 4521: 2 x masala chai
4521 masala chai at index 0
4521 40
["Order 4521","Order 4522"]
null
0

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

Without the g flag, text.match(re) gives back the same kind of result as re.exec(text), which makes destructuring the groups convenient. With g, match changes its answer: an array of the matched texts only, with no groups and no positions. In both forms a text with no match gives null, not an empty array, so code such as text.match(re).length throws a TypeError on the first message that does not match; ?? [] turns the null into an empty array.

Every match with its groups: matchAll

matchAll returns an iterator that produces one full match array per match, each with its groups and index. It is the method for reading many records out of one text, such as the lines of a log.

Counting statuses and slow requests in a log JavaScript · log-parser.mjs
const log = `GET /api/trains/12951 200 38ms
POST /api/bookings 201 120ms
GET /api/pnr/4521 404 12ms
GET /api/trains/12002 200 41ms
POST /api/bookings 500 1503ms
GET /health 200 2ms`;

// One named group per field; m makes ^ and $ work per line, g lets matchAll find every line.
const LINE = /^(?<method>GET|POST) (?<path>\S+) (?<status>\d{3}) (?<ms>\d+)ms$/gm;

const perStatus = new Map();
const slow = [];
for (const { groups, index } of log.matchAll(LINE)) {
  perStatus.set(groups.status, (perStatus.get(groups.status) ?? 0) + 1);
  if (Number(groups.ms) >= 100) slow.push(`${groups.method} ${groups.path}, ${groups.ms} ms, at character ${index}`);
}
console.log([...perStatus].map(([status, n]) => `${status}: ${n}`).join(', '));
console.log(slow.join('\n'));

// matchAll works on a copy of the regex, so LINE.lastIndex is still 0 afterwards.
console.log('lastIndex', LINE.lastIndex);

// Without the g flag, matchAll refuses to start.
try {
  log.matchAll(/GET/);
} catch (e) {
  console.log(e.name);
}

Output

200: 3, 201: 1, 404: 1, 500: 1
POST /api/bookings, 120 ms, at character 31
POST /api/bookings, 1503 ms, at character 118
lastIndex 0
TypeError

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

The pattern has the m flag so that ^ and $ hold at each line, and the g flag because matchAll refuses to start without it: it throws a TypeError rather than quietly return a single match. The loop destructures groups and index straight from each match. matchAll copies the regex before it starts, so the lastIndex of LINE is still 0 afterwards and the same regex can be used again safely. The iterator is lazy and finds the next match only when the loop asks for it; [...text.matchAll(re)] turns it into an array when you need all of them at once.

Replacing text

replace returns a new string and leaves the original as it was. What it replaces depends on the pattern: a string pattern replaces its first occurrence only, a regex without g its first match, and a regex with g every match. replaceAll replaces every occurrence of a string, and with a regex it insists on the g flag.

Strings, regexes, replacement patterns and a function JavaScript · replace.mjs
const note = 'Platform 4 for train 12951, platform 4 again for train 12953';

// A string pattern replaces the first occurrence only; replaceAll replaces every one.
console.log(note.replace('4', 'four'));
console.log(note.replaceAll('4', 'four'));

// A regex without g replaces the first match; with g, every match.
console.log(note.replace(/platform/i, 'Track'));
console.log(note.replace(/platform/gi, 'Track'));

// In the replacement text, $& is the whole match and $<name> a named group.
console.log(note.replace(/\b\d{5}\b/g, '[$&]'));
console.log('Rao, Asha'.replace(/^(?<last>\w+), (?<first>\w+)$/, '$<first> $<last>'));

// A function receives the match and its groups and returns the replacement.
const camel = 'seat_number_and_coach'.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
console.log(camel);
const masked = 'Account 123456789012, IFSC ABCD0123456'.replace(/\b\d{8,}\b/g, (digits) => '•'.repeat(digits.length - 4) + digits.slice(-4));
console.log(masked);

// replaceAll with a regex demands the g flag.
try {
  note.replaceAll(/platform/i, 'Track');
} catch (e) {
  console.log(e.name);
}

Output

Platform four for train 12951, platform 4 again for train 12953
Platform four for train 12951, platform four again for train 12953
Track 4 for train 12951, platform 4 again for train 12953
Track 4 for train 12951, Track 4 again for train 12953
Platform 4 for train [12951], platform 4 again for train [12953]
Asha Rao
seatNumberAndCoach
Account ••••••••9012, IFSC ABCD0123456
TypeError

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

The replacement text may refer to the match:

In the replacement Inserts
$& the whole match
$1, $2 … the text of a numbered group
$<name> the text of a named group
$` and $' the text before and after the match
$$ one dollar sign

When the new text needs logic, pass a function instead of a string. It is called once per match with the matched text, then the text of each numbered group, the position and the whole string (and an object of named groups, when the pattern has any); whatever it returns is inserted. The example converts snake_case to camelCase that way and masks an account number so that only its last four digits remain readable.

Splitting with a regex

split accepts a regex as the separator, which handles separators that vary, such as a comma or a semicolon with any number of spaces around it.

Separators that vary, kept separators and a limit JavaScript · split.mjs
// Split on a run of separators with optional spaces around them.
console.log(JSON.stringify('chai, samosa;vada pav ,  idli'.split(/\s*[,;]\s*/)));

// A capture group in the separator puts the separators into the result too.
console.log(JSON.stringify('12+30-5'.split(/([+-])/)));

// A separator at the start gives an empty first piece.
console.log(JSON.stringify(',a,,b'.split(/,/)));

// The second argument limits how many pieces come back.
console.log(JSON.stringify('a1b2c3d'.split(/\d/, 2)));

Output

["chai","samosa","vada pav","idli"]
["12","+","30","-","5"]
["","a","","b"]
["a","b"]

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

A capture group in the separator puts the separators themselves into the result, between the pieces, which is a quick way to keep the operators of a sum. A separator at the very start produces an empty first piece, and two separators in a row produce an empty piece between them. The second argument caps the number of pieces returned.

The lastIndex trap

A regex with the g flag or the y flag is stateful. test and exec start reading at the regex’s lastIndex property, set it to the end of the match when they find one, and put it back to 0 when they find none. That is what lets a loop of exec calls walk through a text match by match. It is also a classic bug when one global regex is shared and used with test on different strings.

Four test() calls on one /^\d+$/g regex alternate true and false, because each call starts at the lastIndex the previous call left.One regex with the g flag, four callsconst DIGITS = /^\d+$/g;1. DIGITS.test('411001')411001start 0reads from index 0match: truelastIndex: 0 → 62. DIGITS.test('560001')560001start 6starts at the endno match: falselastIndex: 6 → 03. DIGITS.test('110001')110001start 0reads from index 0match: truelastIndex: 0 → 64. DIGITS.test('600001')600001start 6starts at the endno match: falselastIndex: 6 → 0Each call starts where the last match ended;a failed call puts lastIndex back to 0.

Why a regex with the g flag answers differently on the next call

Text description of the diagram

The regex is const DIGITS = /^\d+$/g, and it is called four times with four six-digit codes. Each call is drawn as a row of six character boxes with an arrow under the position where the search starts.

  • Call 1, DIGITS.test('411001'): the search starts at index 0, the whole code matches, the result is true, and lastIndex goes from 0 to 6.
  • Call 2, DIGITS.test('560001'): the search starts at index 6, which is already the end of the new code, so the boxes are greyed out. Nothing matches, the result is false, and lastIndex goes back from 6 to 0.
  • Call 3, DIGITS.test('110001'): the same as call 1: start at 0, true, lastIndex 0 to 6.
  • Call 4, DIGITS.test('600001'): the same as call 2: start at 6, false, lastIndex 6 to 0.

The note at the bottom says that each call starts where the last match ended, and that a failed call puts lastIndex back to 0. Every code is valid, yet only every other call says so.

One shared regex with g, four valid codes JavaScript · lastindex-trap.mjs
// The g flag makes a regex remember where its last match ended (lastIndex).
const DIGITS = /^\d+$/g;

for (const code of ['411001', '560001', '110001', '600001']) {
  const result = DIGITS.test(code);
  console.log(code, result, 'lastIndex is now', DIGITS.lastIndex);
}

// The same bug inside filter: half of the valid codes are dropped.
const codes = ['411001', '560001', '110001', '600001'];
console.log(codes.filter((c) => DIGITS.test(c)).length, 'of', codes.length, 'kept');

// Fix 1: a regex that is only used with test() needs no g flag.
const ALL_DIGITS = /^\d+$/;
console.log(codes.filter((c) => ALL_DIGITS.test(c)).length, 'of', codes.length, 'kept');

// Fix 2: when a g regex has to be reused, set lastIndex back to 0 before every call.
const again = codes.map((c) => {
  DIGITS.lastIndex = 0;
  return DIGITS.test(c);
});
console.log(again.join(' '));

Output

411001 true lastIndex is now 6
560001 false lastIndex is now 0
110001 true lastIndex is now 6
600001 false lastIndex is now 0
2 of 4 kept
4 of 4 kept
true true true true

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

The second call starts at index 6, which is already the end of the next six-digit code, so it fails and resets lastIndex to 0; the third call succeeds again. Inside filter, that drops half of the valid codes, and the bug depends on what was checked before, which makes it hard to reproduce. The fixes, in order of preference:

  • Leave out g when a regex is only used with test or for a single exec. It brings nothing there.
  • Create the regex inside the function that uses it: a literal makes a new object, with lastIndex 0, every time it is evaluated.
  • When one global regex has to be reused, set lastIndex = 0 before each call.
  • Prefer match, matchAll, replace and split for global work. match and replace start from 0 and leave lastIndex at 0, matchAll works on a copy, and split and search ignore it.

The y flag uses the same property on purpose. A sticky regex matches only if the match starts exactly at lastIndex, never further along, which is what a tokenizer needs: every character must belong to a token, and an unknown one must stop it rather than be skipped.

A tokenizer for arithmetic built on the y flag JavaScript · tokenizer.mjs
// y (sticky): every match must start exactly at lastIndex, so nothing can be skipped.
const TOKEN = /\s*(?:(?<number>\d+(?:\.\d+)?)|(?<op>[-+*/()]))\s*/y;

function tokenize(source) {
  const tokens = [];
  TOKEN.lastIndex = 0;
  while (TOKEN.lastIndex < source.length) {
    const at = TOKEN.lastIndex;
    const m = TOKEN.exec(source);
    if (m === null) throw new SyntaxError(`unexpected ${JSON.stringify(source[at])} at position ${at}`);
    tokens.push(m.groups.number ?? m.groups.op);
  }
  return tokens;
}

console.log(tokenize('12 + 3.5*(4 - 1)').join(' | '));
console.log(tokenize(' 7/2 ').join(' | '));
try {
  tokenize('2 + x * 3');
} catch (e) {
  console.log(`${e.name}: ${e.message}`);
}

// With g instead of y, exec searches on from lastIndex: it jumps over the x and finds the 3.
const searching = /\d+/g;
searching.lastIndex = 4;
console.log(searching.exec('2 + x * 3')[0], 'found at', searching.lastIndex - 1);

Output

12 | + | 3.5 | * | ( | 4 | - | 1 | )
7 | / | 2
SyntaxError: unexpected "x" at position 4
3 found at 8

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

Each exec call reads one token where the previous one ended, so the loop stops with a precise error at the x. The last two lines show the difference the flag makes: with g, exec searches onward from lastIndex and silently passes over the x to find the 3.

Exercise

Rupee amounts

The exercise reads amounts in Indian rupees: the sign ₹ or the abbreviation Rs, digits grouped in the Indian style (12,34,567) or the international one (1,234,567), and paise, the hundredth part of a rupee, after a full stop. It returns whole numbers of paise, so that no fraction is ever added up.

Exercise · Medium · JavaScript

Pull every rupee amount out of a message

Write extractAmounts(text) in amounts.mjs. It returns every rupee amount written in text, in the order they appear, as whole numbers of paise (100 paise make one rupee), so that no fractions are ever added up.

An amount is:

  • a currency mark: the sign ₹, or Rs with or without a full stop after it, in any case (Rs., rs, RS);
  • optionally one space;
  • the rupees: digits, which may be grouped with commas in the Indian style (12,34,567) or the international style (1,234,567);
  • optionally a full stop and one or two digits of paise (.5 is 50 paise).

A number without a currency mark is not an amount, and neither is a currency mark without digits after it.

extractAmounts('Paid ₹1,234.50 to Asha and ₹99 to Ravi')  →  [123450, 9900]
extractAmounts('Refund of Rs. 12,34,567.5 approved')       →  [123456750]
extractAmounts('No amounts here, only 42 and ₹ alone')     →  []

Use one regex with the g flag and matchAll, with a named group for the rupees and one for the paise.

Starter code · amounts.mjs

/** Every rupee amount in the text, in paise (100 paise = 1 rupee), in the order they appear. */
export function extractAmounts(text) {
  // Replace these lines with your code. This version stops reading each amount at its first comma.
  const found = text.match(/₹\s?[\d,.]+/g) ?? [];
  return found.map((amount) => Math.round(parseFloat(amount.slice(1)) * 100));
}
The sample tests · amounts.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { extractAmounts } from './amounts.mjs';

test('the first example of the prompt', () => {
  assert.deepEqual(extractAmounts('Paid ₹1,234.50 to Asha and ₹99 to Ravi'), [123450, 9900]);
});

test('Indian digit grouping and one digit of paise', () => {
  assert.deepEqual(extractAmounts('Refund of Rs. 12,34,567.5 approved'), [123456750]);
});

test('Rs in any case, with or without a full stop and a space', () => {
  assert.deepEqual(extractAmounts('rs 40 for tea, RS.15 for biscuits, Rs.7.25 for the bus'), [4000, 1500, 725]);
});

test('numbers without a currency mark, and a mark without digits, are not amounts', () => {
  assert.deepEqual(extractAmounts('No amounts here, only 42 and ₹ alone'), []);
  assert.deepEqual(extractAmounts(''), []);
});

test('the same result every time the function is called', () => {
  const text = '₹10 now and ₹20 later';
  assert.deepEqual(extractAmounts(text), [1000, 2000]);
  assert.deepEqual(extractAmounts(text), [1000, 2000]);
});
A hint

Start the pattern with (?:₹|\bRs\.?)\s?: the sign, or Rs with an optional full stop, then an optional space; the i flag lets Rs match in any case. After it, put the rupees in a named group that allows a comma between runs of digits, and make the paise optional with (?:…)?. Take the commas out with replaceAll(',', '') before converting with Number; a group that did not match is undefined.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 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.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 What does this module print?

    Read the code, then choose one answer.

    const word = /tea/g;
    console.log(word.test('tea'), word.test('tea'), word.test('tea'));
    Show the answer to question 1

    Answer: true false true

    The g flag makes test() start at lastIndex and set it after a match. The first call matches and sets lastIndex to 3; the second call starts at index 3 of 'tea', finds nothing, returns false and resets lastIndex to 0; the third call starts from 0 again. Without the g flag every call would print true.

  2. Question 2 of 5 You need every match in a long text together with its named groups and its position. Which call gives you that?

    Choose one answer.

    Show the answer to question 2

    Answer: text.matchAll(re), with the g flag on re

    matchAll returns an iterator of full match arrays, each with its groups and index. match with the g flag returns only the matched strings, exec without g returns only the first match, and test returns true or false.

  3. Question 3 of 5 What does 'a-b-c'.replace('-', '+') return?

    Choose one answer.

    Show the answer to question 3

    Answer: 'a+b-c'

    With a string as the pattern, replace changes only the first occurrence. replaceAll('-', '+'), or replace with the regex /-/g, would change both.

  4. Question 4 of 5 What does this print?

    Read the code, then choose one answer.

    const re = new RegExp('^\d+$');
    console.log(re.source, re.test('411001'));
    Show the answer to question 4

    Answer: ^d+$ false

    In a string literal, \d is not an escape the string knows, so it is just the letter d, and the pattern becomes ^d+$: one or more letters d. Write the backslash twice in a string, '^\\d+$', or use the literal /^\d+$/.

  5. Question 5 of 5 Which flags make test() and exec() start at lastIndex and update it?

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • g
    • y

    Only the global and sticky flags make a regex stateful. With g the search starts at lastIndex and may move on from there; with y the match must start exactly at lastIndex. The other flags change what matches, not where a search starts.

Interview questions

Why does calling test() twice on a regex with the g flag return true and then false? With the g or y flag a regex keeps state in its lastIndex property. test and exec start reading at lastIndex, and after a match they set it to the position where the match ended. The second call therefore starts part of the way into the next string, or past its end, finds nothing, returns false and sets lastIndex back to 0, so a third call succeeds again. A regex that is only used with test should not have the g flag. If a global regex must be shared, reset lastIndex to 0 before each call, or create the regex inside the function that uses it. match, matchAll, replace and split do not have this problem.

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

  1. Mid to senior

    A Node.js script reads a 700 MB access log with readFileSync(file, 'utf8') and collects every URL with text.match(/https?:\/\/\S+/g). It crashes before matching anything, and on a 300 MB log it uses gigabytes of memory. Explain both failures and rewrite the approach so that memory stays flat.

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

  2. Fresher to mid

    An import feature splits each line of a CSV file with line.split(','), and a row such as Asha,"Pune, MH",42 comes out with four fields instead of three. A teammate proposes a cleverer regex for split. Explain the bug, whether a regex can fix it, and what you would ship.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Write a regex as a literal when the pattern is fixed, and with new RegExp(string, flags) when it is built at run time; in a string, the pattern’s backslashes are written twice.
  • Flags change the whole match: i for case, m for line anchors, s for dots across lines, u or v for Unicode, d for group positions, and g or y for searching on from lastIndex.
  • Anchor a validation pattern with ^ and $, and remember that a regex checks a shape, not that the value exists.
  • exec and match without g give the first match with its groups; match with g gives only texts; matchAll gives every match with its groups.
  • replace with g, or replaceAll, changes every match; $&, $1 and $<name> reuse the match, and a function can compute each replacement.
  • A shared regex with g or y carries lastIndex from call to call: leave the flag out for test, create the regex where it is used, or reset lastIndex.

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.