Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Text Processing Recipes

String manipulation and regex patterns for common tasks.

Suji’s regex support answers one question — does this match? There are no capture groups, no regex replace and no regex split, so extraction is done with index_of, slicing (s[a;b]) and split.

Email Validation

Validate email addresses with regex.

import std:println

email_pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

validate_email = |email| email ~ email_pattern

println(validate_email("user@example.com"))  # true
println(validate_email("user@localhost"))    # false
println(validate_email("not an email"))      # false

URL Extraction

Extract URLs from text by splitting into words and matching each one.

import std:println

extract_urls = |text| {
    words = text::replace("\n", " ")::replace("\t", " ")::split(" ")
    words::filter(|w| w::length() > 0 && w ~ /^https?:\/\/.+$/)
}

text = "Visit https://example.com or http://test.org for more info"
urls = extract_urls(text)

println(urls::length())     # 2
println(urls::join(", "))   # https://example.com, http://test.org

Trailing punctuation is part of the word, so trim it when the text is prose:

import std:println

clean = |w| w::trim(".,;:!?)")
println(clean("https://example.com."))  # https://example.com

Log Parsing

Parse structured log entries. Locate the delimiters with index_of, then slice.

import std:println

parse_log_line = |line| {
    close = line::index_of("]")
    close < 0 && return nil

    timestamp = line[1;close]
    rest = line[(close + 2);]

    sep = rest::index_of(": ")
    sep < 0 && return nil

    {
        "timestamp": timestamp,
        "level": rest[0;sep],
        "message": rest[(sep + 2);],
    }
}

line = "[2024-01-15 10:30:00] ERROR: Database connection failed"
entry = parse_log_line(line)

println(entry:level)      # ERROR
println(entry:message)    # Database connection failed
println(entry:timestamp)  # 2024-01-15 10:30:00

A malformed line returns nil, so check before reading fields:

import std:println

describe = |entry| match {
    entry == nil => "unparsable line",
    _ => entry:level,
}

println(describe(nil))                     # unparsable line
println(describe({ level: "WARN" }))       # WARN

Template Generation

Generate text from templates. string::replace takes plain strings, so {{name}}-style placeholders are the easiest scheme.

import std:println

render = |tmpl, data| {
    result = tmpl
    loop through data with key, value {
        result = result::replace("{{${key}}}", value::to_string())
    }
    result
}

email_template = """Hello {{name}},

Your order #{{order_id}} has been shipped.

Total: \${{total}}"""

message = render(email_template, {
    name: "Alice",
    order_id: "12345",
    total: "99.99",
})

println(message)

Output:

Hello Alice,

Your order #12345 has been shipped.

Total: $99.99

Note the \$ escape: ${...} is string interpolation everywhere in Suji, so a literal dollar sign in front of a brace must be escaped.

Text Search and Replace

Collapsing runs of a separator is the same shape every time: split on it, drop the empty pieces, and join. No character loop is needed.

import std:println

normalize_whitespace = |text| text
    ::replace("\n", " ")
    ::replace("\t", " ")
    ::split(" ")
    ::filter(|w| w::length() > 0)
    ::join(" ")

messy = "  too    many \n\t spaces  "

println(normalize_whitespace(messy))  # too many spaces

When the rule is per-character, strings are not iterable — call ::to_list() for a list of single-character strings. Map each character, then collapse with the same split-filter-join:

import std:println

slugify = |text| {
    out = ""
    loop through text::lower()::to_list() with ch {
        out = out + match {
            ch ~ /^[a-z0-9]$/ => ch,
            _ => "-",
        }
    }
    out::split("-")::filter(|p| p::length() > 0)::join("-")
}

println(slugify("Hello World!"))              # hello-world
println(slugify("  Suji: A Small Language"))  # suji-a-small-language

Plain substring replacement needs no loop at all:

import std:println

println("2024-01-15"::replace("-", "/"))          # 2024/01/15
println("a,b,,c"::split(",")::filter(|p| p::length() > 0)::join("|"))  # a|b|c

Complete Example: Markdown Parser

Simple line-based markdown to HTML converter (headers + paragraphs).

import std:println

wrap = |tag, text| "<${tag}>${text}</${tag}>"

markdown_to_html = |md| {
    out = []

    loop through md::split("\n") with line {
        match {
            line::starts_with("## ") => { out::push(wrap("h2", line[3;])) },
            line::starts_with("# ") => { out::push(wrap("h1", line[2;])) },
            line::trim()::length() == 0 => {},
            _ => { out::push(wrap("p", line)) },
        }
    }

    out::join("\n")
}

md = "# Title\n\nAn opening paragraph.\n\n## Section\n\nMore text."
println(markdown_to_html(md))

Output:

<h1>Title</h1>
<p>An opening paragraph.</p>
<h2>Section</h2>
<p>More text.</p>

See Also