Skip to the chapter
←JavaScriptGGroundworkJSJavaScript
JavaScript/Intermediate/I6
23 / 41 written chapters
/

The cover
  1. B1Setup & mental model
  2. B2Execution context
  3. B3var, let & const
  4. B4Scope
  5. B5Single thread
  6. B6In the browser
  7. B7Types & values
  8. B8Strings & Math
  9. B9Operators & flow
  10. B10Functions
  11. B11Objects & arrays
  12. B12this
  13. B13Closures
  14. B14DOM & events
  15. B15Callbacks & promises
  16. B16Errors & tools
  17. B17Guided project
  18. ★Cheat page
  1. I1Scope & functions, properly
  2. I2Objects deeply
  3. I3Prototypes & OOP
  4. I4Async, properly
  5. I5Modules & tooling
  6. I6Regex & dates
  7. I7Error handling
  8. I8Real-time connections
  9. I9Offline & storage
  10. I10Browser APIs
  11. I11Modern JS
  12. ★Cheat page
  1. A1Engine & memory
  2. A2Advanced async
  3. A3Metaprogramming
  4. A4Types & data
  5. A5Patterns & architecture
  6. A6Performance
  7. A7Security
  8. A8Ecosystem
  9. A9Testing
  10. A10Observability
  11. A11Interview bank
  12. A12Build it yourself
  13. ★Cheat page

JSI6 · Part 2 · Intermediate

Regex & dates

Two toolboxes every real app ends up reaching for, and the traps that come with each.

5 min read4 sections2 exercises
23 of 41

In this chapter

  • Regex — the essentials
  • Lookahead, lookbehind, and the ReDoS trap
  • The sticky flag — y vs g
  • Dates
about 5 min
04On this page
  1. Regex — the essentials
  2. Lookahead, lookbehind, and the ReDoS trap
  3. The sticky flag — y vs g
  4. Dates

Regex — the essentials

/abc/         // literal — matches "abc" exactly
/abc/i        // flag: i = case-insensitive
/abc/g        // flag: g = find ALL matches, not just the first
/(\w+)@(\w+)/  // ( ) = a capturing group — grabbed separately from the full match
const text = "contact: ana@example.com today";
const match = text.match(/(\w+)@(\w+)\.com/);
console.log(match[0]);   // the whole match — what happens?
console.log(match[1]);   // group 1 — what happens?
console.log(match[2]);   // group 2 — what happens?

"ana@example.com", then "ana", then "example" — the full match is always index 0, and every parenthesized group after it fills in one more slot, in order.

// Named groups — same idea, readable by name instead of position
const parsed = "2024-01".match(/(?<year>\d{4})-(?<month>\d{2})/);
parsed.groups.year;    // "2024"
parsed.groups.month;   // "01"

"2024-01-15".replace(/(\d+)-(\d+)-(\d+)/, "$3/$2/$1");   // "15/01/2024" — $1/$2/$3 refer back to the groups

[..."a1 b22 c333".matchAll(/[a-z](\d+)/g)].map((m) => m[1]);  // ["1", "22", "333"] — every match, not just the first
⚠ A /g regex remembers where it left off .test() and .exec() on a regex literal with the g flag mutate the regex object's own lastIndex — the next call resumes searching from there, not from the start of the string.
const stateful = /\d/g;
console.log(stateful.test("a1"));   // what happens?
console.log(stateful.test("a1"));   // SAME regex, same string — what happens?
console.log(stateful.test("a1"));   // what happens?

Dry run: three .test() calls on the same /g regex object

CalllastIndex beforeSearch starts atResultlastIndex after
1st stateful.test("a1")0index 0true — "1" found at index 12 (end of match)
2nd stateful.test("a1")2index 2 — past the end of a 2-character stringfalse — nothing left to searchreset to 0
3rd stateful.test("a1")0index 0 againtrue — same match as the 1st call2

true, false, true — alternating, on the exact same input. First call finds the digit and leaves lastIndex at 2; second call starts searching from index 2 in a 2-character string, finds nothing, and resets lastIndex back to 0; third call starts over and finds it again. Reusing one global-flagged regex object across unrelated calls is exactly how this bites — a fresh /\d/g literal each time, or dropping the g flag for a one-shot .test(), avoids it.

Lookahead, lookbehind, and the ReDoS trap

/\d+(?=px)/.exec("width: 240px")[0];      // "240" — lookahead: match digits, but only if "px" follows (not consumed)
/(?<=\$)\d+/.exec("Price: $99")[0];       // "99" — lookbehind: only if "$" precedes, also not consumed

Neither assertion becomes part of the match itself — that's the whole point of calling them assertions rather than groups. They let a pattern check context ("only if followed/preceded by X") without including that context in what gets extracted or replaced.

⚠ Some patterns can take exponential time — ReDoS A pattern with nested, overlapping repetition — /(a+)+b/ against a long string of just as with no trailing b — forces the engine to try an exponential number of ways to split the string among the repeated groups before giving up. Run against user-supplied input (a "validate this email" regex is the classic real-world case), this is a genuine denial-of-service vector — Regular expression Denial of Service, ReDoS — that has taken down real production servers with a single crafted string. The fix is rewriting the pattern to remove the nested ambiguity, or running untrusted-input matching behind a timeout.

The sticky flag — y vs g

const g = /\d+/g;
const y = /\d+/y;
const str = "12 34";

g.exec(str)[0];    // "12" — g can skip ahead to find the next match anywhere
y.lastIndex = 0;
y.exec(str)[0];    // "12" — same result here...
y.lastIndex = 1;
y.exec(str);       // what happens?

null — y demands a match starting exactly at lastIndex, nowhere later in the string; g is happy to scan forward and find the next match wherever it is. This matters for a hand-written tokenizer or parser, where "match right here or fail" is the entire point — scanning ahead silently would mean accepting a token that isn't actually adjacent to where the parser currently sits.

Dates

const d = new Date(2024, 0, 15);   // year, MONTH (0-indexed!), day
console.log(d.getMonth());   // what happens?
console.log(d.getDate());    // what happens?

0, then 15 — getMonth() is January-is-0, a decision baked into Date since the original Java date API it was modeled on in 1995, and never fixed since without breaking every existing script.

const start = new Date("2024-01-15");
const end = new Date("2024-02-15");
(end - start) / 86_400_000;   // 31 — subtracting Dates gives milliseconds; divide to get days
⚠ The two constructors don't parse in the same time zone new Date(2024, 0, 15) (year, month, day as separate numbers) builds midnight in the local time zone. new Date("2024-01-15") (a plain ISO date string, no time) is parsed as midnight UTC. In UTC+5:30, that ISO string's local clock time is 05:30, not midnight — a different instant from the numeric-args version, even though both print "15 January" if you only look at the date part. The subtraction above only comes out to a clean 31 because both operands went through the same UTC-parsing string form — mix a numeric-args Date with a string-parsed one and the day count can be off by one, depending on which side of midnight UTC the reader's time zone happens to sit.
⚠ Why almost nobody hand-rolls date math Time zones, daylight saving transitions, leap years, and leap seconds all make "add one day" genuinely harder than + 86400000 — a DST boundary can make that arithmetic land on the wrong calendar day entirely. This is the real reason libraries like date-fns exist, and why the language now has Temporal: not laziness, a correctness problem that's easy to get subtly wrong by hand.
Temporal.PlainDate.from("2024-01-31").add({ months: 1 }).toString();
// "2024-02-29" — calendar-aware: no milliseconds, no time zone, no DST surprise

Temporal is the built-in successor to Date. It ships in Firefox (139+) and Chrome (144+); check support for the rest of your audience, or load a polyfill, before relying on it.

new Intl.DateTimeFormat("en-IN", { dateStyle: "long" }).format(d);
// "15 January 2024" — locale-correct formatting, no manual string building

new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" }).format(150000);
// "₹1,50,000.00" — Indian digit grouping, handled for you

The next chapter picks up where this one leaves off — storage, the URL bar, and the event system in depth.

Interview answer, one sentence

"Regex is for patterns, not for parsing nested structure, and nested quantifiers can backtrack catastrophically (ReDoS); store dates as UTC timestamps and format them for display, because arithmetic on local time breaks across daylight-saving changes."

Before you move on
  • Write a regex with a named capture group and pull a value out by that name, not by index.
  • Explain what makes a regex vulnerable to catastrophic backtracking, and rewrite one to avoid it.
  • Explain the difference between the sticky (y) and global (g) flags, and when a sticky match fails where a global one would keep going.
  • Store and do arithmetic on a date as a UTC timestamp, and explain why local-time arithmetic breaks across a DST change.
  • Format a date and a number for a specific locale using Intl, without hand-building the string.

02Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 9 testsPull every hashtag out of a postintermediate
  • 9 testsMask a card number down to the last 4 digitsintermediate
All 33 for this level →
I6

Finished reading?

0 of 41 chapters read in this series

→Next stop

  1. I5Previous · I5Modules & tooling
  2. I6you are here
  3. Next · I7Error handling & debuggingPast what the beginner errors chapter covered — error chains, lost rejections and real debugging tools.6 min read

01On this page

4 sections · 0%

  1. 01Regex — the essentials
  2. 02Lookahead, lookbehind, and the ReDoS trap
  3. 03The sticky flag — y vs g
  4. 04Dates

[] chapters · t top