← Back to tools
Utility

Regex Tester

Test and debug regular expressions in real-time. Highlights matches, captures groups, and explains patterns in plain English.

Implementation

interface RegexMatch {
  fullMatch: string;
  index: number;
  groups: { name: string | number; value: string }[];
}

interface RegexResult {
  matches: RegexMatch[];
  totalMatches: number;
  executionTime: number;
  error: string | null;
}

function testRegex(pattern: string, flags: string, input: string): RegexResult {
  const start = performance.now();
  try {
    const re = new RegExp(pattern, flags);
    const matches: RegexMatch[] = [];

    if (flags.includes('g')) {
      let match: RegExpExecArray | null;
      while ((match = re.exec(input)) !== null) {
        const groups: { name: string | number; value: string }[] = [];
        // Numbered capture groups (skip index 0 = full match)
        for (let i = 1; i < match.length; i++) {
          if (match[i] !== undefined) {
            groups.push({ name: i, value: match[i] });
          }
        }
        // Named capture groups
        if (match.groups) {
          for (const [name, value] of Object.entries(match.groups)) {
            if (value !== undefined) {
              groups.push({ name, value });
            }
          }
        }
        matches.push({ fullMatch: match[0], index: match.index, groups });
        if (match[0].length === 0) re.lastIndex++; // prevent infinite loop
      }
    } else {
      const match = re.exec(input);
      if (match) {
        const groups: { name: string | number; value: string }[] = [];
        for (let i = 1; i < match.length; i++) {
          if (match[i] !== undefined) {
            groups.push({ name: i, value: match[i] });
          }
        }
        if (match.groups) {
          for (const [name, value] of Object.entries(match.groups)) {
            if (value !== undefined) {
              groups.push({ name, value });
            }
          }
        }
        matches.push({ fullMatch: match[0], index: match.index, groups });
      }
    }

    return {
      matches,
      totalMatches: matches.length,
      executionTime: performance.now() - start,
      error: null,
    };
  } catch (err) {
    return {
      matches: [],
      totalMatches: 0,
      executionTime: performance.now() - start,
      error: (err as Error).message,
    };
  }
}

function explainRegex(pattern: string): string {
  const explanations: { token: string; meaning: string }[] = [];
  const rules: [RegExp, string][] = [
    [/\^/, 'Start of string anchor'],
    [/\$/, 'End of string anchor'],
    [/\\./, 'Any single character'],
    [/\\d/, 'Any digit (0-9)'],
    [/\\D/, 'Any non-digit'],
    [/\\w/, 'Any word character (a-z, A-Z, 0-9, _)'],
    [/\\W/, 'Any non-word character'],
    [/\\s/, 'Any whitespace (space, tab, newline)'],
    [/\\S/, 'Any non-whitespace character'],
    [/\\b/, 'Word boundary'],
    [/\\B/, 'Non-word boundary'],
    [/\+/, 'One or more of the preceding'],
    [/\*/, 'Zero or more of the preceding'],
    [/\?/, 'Zero or one of the preceding (optional)'],
    [/\{(\d+)\}/, 'Exactly $1 of the preceding'],
    [/\{(\d+),(\d*)\}/, 'Between $1 and $2 of the preceding'],
    [/\\(\d+)/, 'Backreference to group $1'],
    [/\(\?:/, 'Non-capturing group'],
    [/\(\?=/, 'Positive lookahead'],
    [/\(\?!/, 'Negative lookahead'],
    [/\(\?<=/, 'Positive lookbehind'],
    [/\(\?<!/, 'Negative lookbehind'],
    [/\(\?<(\w+)>/, 'Named capture group "$1"'],
    [/\(/, 'Capture group start'],
    [/\)/, 'Group end'],
    [/\[\^/, 'Negated character class'],
    [/\[/, 'Character class start'],
    [/\]/, 'Character class end'],
    [/\|/, 'OR — alternation'],
  ];

  for (const [regex, meaning] of rules) {
    if (regex.test(pattern)) {
      explanations.push({ token: regex.source, meaning });
    }
  }

  if (explanations.length === 0) {
    return 'Matches the literal text: "' + pattern + '"';
  }

  return explanations.map(e => `  ${e.meaning}`).join('\n');
}

// Usage
const result = testRegex(
  '(\\d{4})-(\\d{2})-(\\d{2})',
  'g',
  'Dates: 2024-01-15 and 2024-12-31'
);
console.log(result.matches);
// => [
//   { fullMatch: '2024-01-15', index: 7, groups: [{name:1,value:'2024'},{name:2,value:'01'},{name:3,value:'15'}] },
//   { fullMatch: '2024-12-31', index: 22, groups: [{name:1,value:'2024'},{name:2,value:'12'},{name:3,value:'31'}] }
// ]

console.log(explainRegex('\\d{4}-\\d{2}'));
// => "Any digit (0-9)\n  Exactly 4 of the preceding"

Usage

Call testRegex(pattern, flags, input) to find all matches with group captures. Use explainRegex(pattern) to get a human-readable breakdown.

Examples

Input: testRegex("(\\w+)@(\\w+\\.\\w+)", "g", "a@b.com c@d.org")
Output: 2 matches with captured user and domain groups
Input: testRegex("(?<year>\\d{4})-(?<month>\\d{2})", "g", "2024-01")
Output: Named groups: year="2024", month="01"
Input: explainRegex("^\\d{3}-\\d{4}$")
Output: "Start of string anchor, Any digit, Exactly 3 of the preceding, ..."