How do I work with regular expressions?

← Getting Started · Ref: Q33

EK9 has a built-in RegEx type with /pattern/ literal syntax (same as JavaScript), avoiding double-escaping.

REGEX LITERALS

  digitPattern <- /\d+/
  emailPattern <- /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

No string escaping needed: /\d+/ is what you mean. Java requires Pattern.compile("\\d+").

MATCHING

Bidirectional 'matches' operator:

  require 'hello123' matches /[a-z]+\d+/
  require /[a-z]+\d+/ matches 'hello123'

Negate with 'not matches':

  require 'hello' not matches /\d+/

SPLITTING STRINGS

Split with regex delimiter, works both directions:

  parts <- 'one:two:three'.split(/:/)
  alsoParts <- /:/.split('one:two:three')

Both produce List of String.

GROUP EXTRACTION

Capture groups extract matched portions via split():

  groups <- text.split(/\{(.*?)(\d+)(.*)/)

If matched, groups is List of String per capture group. If not matched, groups is unset (see Q29).
Access: groups.getOrDefault(0, "") or stream: cat groups | skip 1 | head 1 | collect as String

ESCAPING IN LITERALS

/\\/ matches literal backslash. /\// matches literal forward slash.
Shortcuts: \d \w \s . Quantifiers: ? * + {n} {n,m}. Anchors: ^ $ \b. Classes: [a-z] [^0-9].

REGEX COMPOSITION

Build patterns with + operator:

  combined <- /^Hello/ + / / + /World$/

Extend with +=:

  pattern <- /^Start/
  pattern += /.*End$/

COMPARISON AND IDENTITY

== and <> compare patterns. ? checks if set. $ converts to string. length returns pattern length.

Use 'ek9 -h RegEx' and 'ek9 -h String' for the full API.

See Q37 for string operations. See Q29 for tri-state and unset group extraction.

Example

defines module qa.regex

  defines program
    RegExDemo()
      stdout <- Stdout()

      // === REGEX LITERALS ===

      // Regex literals use /pattern/ syntax - like JavaScript
      // No double-escaping needed (unlike Java's Pattern.compile("\\d+"))
      digitPattern <- /\d+/
      emailPattern <- /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
      stdout.println(`Digit pattern: ${digitPattern}`)
      require "user@example.com" matches emailPattern
      require "not-an-email" not matches emailPattern

      // === MATCHING ===

      // The 'matches' operator works in both directions
      require "hello123" matches /[a-z]+\d+/
      require /[a-z]+\d+/ matches "hello123"

      // 'not matches' for negation
      require "hello" not matches /\d+/

      // Match name variants with character classes and alternation
      namePattern <- /[S|s]te(?:ven?|phen)/
      require "Steve" matches namePattern
      require "steve" matches namePattern
      require "Stephen" matches namePattern
      require "Steven" matches namePattern
      require "Stephene" not matches namePattern

      // Character class with quantifier
      sixChars <- /[a-zA-Z0-9]{6}/
      require sixChars matches "arun32"
      require sixChars not matches "kkvarun32"
      require sixChars not matches "arun$2"

      // === SPLITTING STRINGS ===

      // Split works both directions: string.split(regex) or regex.split(string)
      colonRegEx <- /:/
      colonDelimited <- "one:two:three:four:five"

      fromString <- colonDelimited.split(colonRegEx)
      fromRegEx <- colonRegEx.split(colonDelimited)
      require fromString == fromRegEx
      stdout.println(`Split: ${fromString}`)
      require $fromString == "one,two,three,four,five"

      // === GROUP EXTRACTION ===

      // Capture groups in the pattern extract matched portions
      text <- "This is a sample Text 1234 with numbers in between."
      groupPattern <- /\{(.*?)(\d+)(.*)/
      groups <- text.split(groupPattern)

      stdout.println(`Groups: ${groups}`)
      require length groups == 3
      require groups.getOrDefault(0, "") == "This is a sample Text "
      require groups.getOrDefault(2, "") == " with numbers in between."

      // Extract specific group using stream pipeline
      justNumber <- cat groups | skip 1 | head 1 | collect as String
      require $justNumber == "1234"
      stdout.println(`Extracted number: ${justNumber}`)

      // Convert extracted text to Integer
      asInteger <- Integer(justNumber)
      require asInteger == 1234
      stdout.println(`As integer: ${asInteger}`)

      // Get last two groups
      lastTwo <- cat groups | tail 2 | collect as List of String
      require length lastTwo == 2

      // === NO MATCH RETURNS UNSET ===

      noNumbers <- "No digits here at all."
      noMatch <- noNumbers.split(groupPattern)
      require ~noMatch?
      stdout.println(`No match isSet: ${noMatch?}`)

      // === ESCAPING IN LITERALS ===

      // Backslash in regex literal: /\\/ matches a literal backslash
      backslashPattern <- /^Some\\Thing$/
      require backslashPattern matches "Some\Thing"

      // Forward slash in regex literal: /\// matches a literal forward slash
      slashPattern <- /^Some\/Thing$/
      require slashPattern matches "Some/Thing"

      // Fraction matching with escaped slash
      fractionPattern <- /.*\/.*/
      require fractionPattern matches "3/4"

      // === REGEX COMPOSITION ===

      // Build patterns with + operator
      prefix <- /^Hello/
      combined <- prefix + / / + /World$/
      require combined matches "Hello World"
      stdout.println(`Combined: ${combined}`)

      // += to extend a pattern
      growing <- /^Start/
      growing += /.*End$/
      require growing matches "Start and End"

      // === COMMON PATTERNS ===

      // Phone number
      require "555-1234" matches /^\d{3}-\d{4}$/

      // UK postcode (simplified)
      require "SW1A 1AA" matches /^[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}$/

      // ISO date format
      require "2024-06-15" matches /^\d{4}-\d{2}-\d{2}$/

      stdout.println("All regex tests passed")

Common mistakes

E50001 — EK9 rejects variable names that shadow operator keywords. 'matches' is a reserved operator keyword. Use descriptive names like 'digitPattern' or 'emailPattern'. See ek9 -h E50001 for details.

Incorrect:

matches <- /\d+/

Correct:

digitPattern <- /\d+/
Other ways to ask this
  • Does EK9 have regex support?
  • How do I match patterns in strings?
  • How do I split strings with a regex in EK9?
  • How do I extract groups from a regex match?

Coming from another language?

Java: Pattern.compile() double-escaping, Pattern/Matcher ceremony. Python: re.compile(r'pattern'). JavaScript: /pattern/ (EK9 follows this). Rust/Go: no literal syntax. EK9: /pattern/ literal, bidirectional matches, split both directions, no double-escaping.

Keywords: match, group, escape, quantifier, expression, start, beginner, regular, class, character, split, migrate, anchor, capture, literal, pattern, first, intro, regex, matches