Introduction
Understanding Practical Data Patterns
Character Classes for Email Patterns

Let's begin by extracting email addresses from text. An email address has two main components separated by an @ symbol: the username before the @ and the domain after it. Both parts contain letters, numbers, and potentially special characters like dots and hyphens. We need character classes that match these allowed characters precisely.

// Start with username pattern
// [\w.-]+ matches word characters, dots, or hyphens
const usernamePattern = /[\w.-]+/;

The pattern /[\w.-]+/ creates a character class that matches any word character (letters, digits, underscore), any dot, or any hyphen. Inside a character class, the dot loses its special "match anything" meaning and matches a literal dot character. The + quantifier means we match one or more of these characters, allowing usernames like "alice", "alice.smith", or "user-name123".

For the domain part, we need a similar approach but with an additional requirement: domains must end with a dot followed by an extension like "com" or "co.uk". We can match the domain name using the same character class, then explicitly match the dot and extension:

// Domain pattern includes the extension
// [\w.-]+ for the main domain name
// \. for the literal dot before extension
// \w+ for the extension (com, org, etc.)
const domainPattern = /[\w.-]+\.\w+/;

This pattern matches domains like "example.com", "mail.example.org", or "example.co.uk". The [\w.-]+ matches the main domain name, \. matches the required dot, and \w+ matches the extension. By breaking the pattern into username and domain components, we make it easier to understand and later modify if needed.

Extracting Email Components

Now let's combine these components into a complete function that extracts email addresses. We want to capture both the username and domain separately, so we'll use two capture groups joined by the @ symbol:

function extractEmails(text) {
  const pattern = /([\w.-]+)@([\w.-]+\.\w+)/g;
  const results = [];
  let match;
  while ((match = pattern.exec(text)) !== null) {
    results.push([match[1], match[2]]);
  }
  return results;
}

The pattern /([\w.-]+)@([\w.-]+\.\w+)/g has three parts. First, ([\w.-]+) is our first capture group matching the username. Second, @ matches the literal @ symbol. Third, ([\w.-]+\.\w+) is our second capture group matching the domain. The g flag at the end enables global matching, allowing us to find all email addresses in the text, not just the first one.

In JavaScript, we use the exec() method in a loop to collect all matches. Each call to exec() returns a match object where match[1] contains the first capture group (username) and match[2] contains the second capture group (domain). We manually build an array of results, pushing a two-element array for each match. This gives us structured data: instead of just finding email addresses, we extract them in a format that separates usernames from domains. This is valuable for many tasks: you might want to count how many emails belong to a specific domain, extract just the usernames for a contact list, or validate that domains follow certain rules. The capture groups transform unstructured text into organized data.

Non-Capturing Groups

Before we tackle price extraction, we need to introduce an important concept: non-capturing groups. So far, every time we've used parentheses in a pattern, we've created a capture group that extracts data. But sometimes, we need parentheses purely for structural reasons: to apply a quantifier to multiple characters, to define alternatives with |, or to organize complex patterns logically. In these cases, we don't want to capture the content; we just want to group it.

// Capturing group: extracts content
const pattern1 = /(\d{3})/;  // Captures three digits

// Non-capturing group: structures pattern without capturing
const pattern2 = /(?:\d{3})/;  // Groups three digits but doesn't capture

A non-capturing group starts with (?: instead of just (. The ?: at the beginning tells the regex engine: "treat this as a group for structural purposes, but don't remember its content." This is crucial for complex patterns where we need grouping for quantifiers or alternation but don't want the extra data in our results.

Why does this matter? Consider a price like "$1,299.00". We want to capture the entire number "1,299.00" as one group, but the pattern needs internal grouping to handle the optional thousands separators. If we use regular capturing groups for these internal components, our results would include nested captures with parts of the price, making the output messy and hard to use. Non-capturing groups solve this: they let us structure the pattern without affecting the output. Let's see this in action with price extraction.

Building Price Patterns Step by Step
Combining Pattern Elements for Prices

Now let's implement the complete price extraction function, placing a single capture group around the numeric portion of the pattern:

function extractPrices(text) {
  // Build up: \d{1,3} = 1-3 digits; (?:,\d{3})* = zero or more comma-groups
  // (?:\.\d{2})? = optional decimal; non-capturing (?:...) groups structure only
  const pattern = /\$(\d{1,3}(?:,\d{3})*(?:\.\d{2})?)/g;
  const results = [];
  let match;
  while ((match = pattern.exec(text)) !== null) {
    results.push(match[1]);
  }
  return results;
}

The crucial difference here is that we placed one capturing group around the entire number pattern: (\d{1,3}(?:,\d{3})*(?:\.\d{2})?). This means we capture everything after the dollar sign as a single string. The dollar sign itself is matched literally but not captured, which makes sense: we know all these are dollar amounts, so including the symbol in every result would be redundant.

Inside the main capture group, the two non-capturing groups (?:,\d{3})* and (?:\.\d{2})? structure the pattern without creating additional captures. We use exec() in a loop to find all matches, and for each match, we push match[1] (the first capture group) into our results array. This gives us a clean output format with simple strings like ['9.99', '1,299.00', '100'] rather than complex nested arrays. This clean output is exactly what we want for further processing: we can easily convert these strings to numbers, sum them for totals, or display them in reports.

Testing Both Functions
Conclusion and Next Steps

Excellent work mastering practical extraction patterns in JavaScript! You've learned to combine multiple regex concepts to solve real data extraction challenges. In this lesson, you discovered how to use character classes like [\w.-]+ to match common patterns in email addresses, how to structure complex patterns with capture groups for organized output, and, most importantly, how to use non-capturing groups (?:...) to build readable, maintainable patterns. You extracted emails as username-domain pairs and built a sophisticated price pattern that handles optional thousands separators and decimal places.

The techniques you've learned here form the foundation for most data extraction tasks. By breaking complex patterns into smaller, manageable pieces and building them up incrementally, you can tackle virtually any extraction challenge. The distinction between capturing and non-capturing groups is particularly powerful: it lets you structure patterns for clarity while keeping your results clean and simple. Combined with the named groups and backreferences from previous lessons, you now have a complete toolkit for extracting and validating data from unstructured text.

These skills have immediate practical applications. You can extract contact information from documents, parse price lists from websites, identify phone numbers in text files, or pull structured data from log files. As we continue through the course, you'll learn to transform extracted data with substitutions, optimize patterns for performance, and handle increasingly complex scenarios. Each new technique builds on your growing mastery of regular expressions.

Now it's time to apply what you've learned through hands-on practice! You'll extract social media handles from text, modify patterns to capture only specific components, write new patterns from scratch to extract website domains, and extend existing patterns to handle multiple currencies. These exercises will solidify your understanding and give you the confidence to tackle any data extraction challenge in your own projects!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal