Chapter 4: String Craft
Strings are everywhere — user input, file paths, URLs, log messages, generated code. Tova gives you powerful string interpolation, a rich set of string functions, and pattern matching on strings. This chapter makes you fluent in text processing.
By the end, you'll build a mini template engine.
String Types
Tova has several string forms, each designed for different situations. Understanding when to use each one makes your code cleaner and avoids escaping headaches.
Double-Quoted Strings (Interpolation)
The most common form. Expressions inside {braces} are evaluated and inserted:
name = "Alice"
greeting = "Hello, {name}!" // "Hello, Alice!"
math = "2 + 2 = {2 + 2}" // "2 + 2 = 4"
nested = "Score: {user.scores[0]}" // Access nested dataDouble-quoted strings process escape sequences like \n, \t, and \\.
Single-Quoted Strings (Literal)
Single quotes create literal strings — no interpolation, no escape processing:
template = 'Hello, {name}' // Literally: Hello, {name}
regex_str = '\d+\.\d+' // Backslashes are literal
json_key = '{"key": "value"}' // Braces are literalUse single quotes when you need literal curly braces, backslashes, or when the string shouldn't be processed in any way.
Raw Strings
Prefix with r to disable all escape processing while keeping double-quote syntax:
windows_path = r"C:\Users\Alice\Documents"
regex = r"\d{3}-\d{4}"Raw strings are perfect for Windows file paths, regex patterns, and template literals.
Triple-Quoted Multiline Strings
For multi-line text, use triple double quotes. They automatically dedent — common leading whitespace is stripped:
html = """
<div>
<h1>Hello</h1>
<p>Welcome to Tova</p>
</div>
"""
// The 2-space indent common to all lines is removedTriple-quoted strings support interpolation:
fn render_card(title, body) {
"""
<div class="card">
<h2>{title}</h2>
<p>{body}</p>
</div>
"""
}Escape Sequences Reference
Double-quoted strings support these escape sequences:
| Escape | Character |
|---|---|
\n | Newline |
\t | Tab |
\r | Carriage return |
\\ | Literal backslash |
\" | Literal double quote |
\' | Literal single quote |
\{ | Literal opening brace (prevents interpolation) |
\} | Literal closing brace |
print("Column1\tColumn2\tColumn3") // Tab-separated
print("She said \"hello\"") // Escaped quotes
print("Price: \{not interpolated\}") // Literal bracesChoosing a String Type
| Need | Use |
|---|---|
| Most strings | "double quotes" |
| No interpolation needed | 'single quotes' |
| File paths, regex | r"raw strings" |
| Multi-line text, templates | """triple quotes""" |
String Interpolation
Tova strings use {expression} for interpolation — no dollar signs, no special syntax, just curly braces:
name = "Alice"
print("Hello, {name}!")
// Any expression works inside the braces
items = [1, 2, 3]
print("You have {len(items)} items totaling {sum(items)}")
// Object access
user = { name: "Bob", role: "admin" }
print("{user.name} is a {user.role}")When NOT to Interpolate
If you're building a string incrementally in a loop, collect parts in an array and join() them. Interpolation is best for final, human-readable output.
String Concatenation
Use interpolation to join strings:
greeting = "Hello, World!"
// Building paths
base = "/api"
version = "/v2"
endpoint = "/users"
url = "{base}{version}{endpoint}"
// "/api/v2/users"For joining many strings, join() is cleaner:
parts = ["2026", "03", "05"]
date = join(parts, "-") // "2026-03-05"
words = ["Tova", "is", "great"]
sentence = join(words, " ") // "Tova is great"Essential String Functions
Here are the string functions you'll use daily:
Searching
text = "Hello, World!"
contains(text, "World") // true
startsWith(text, "Hello") // true
endsWith(text, "!") // true
indexOf(text, "World") // 7
lastIndexOf(text, "l") // 10 (last occurrence)
charAt(text, 0) // "H" (character at index)Transforming
upper("hello") // "HELLO"
lower("HELLO") // "hello"
trim(" hello ") // "hello"
trimStart(" hello ") // "hello "
trimEnd(" hello ") // " hello"
replace("foo bar", "bar", "baz") // "foo baz"
replaceFirst("aaa", "a", "b") // "baa" (only first occurrence)
isEmpty("") // true
isEmpty("hello") // falseExtracting
text = "Hello, World!"
substr(text, 0, 5) // "Hello"
substr(text, 7) // "World!"
len(text) // 13
chars(text) // ["H", "e", "l", "l", "o", ...]Splitting
split("a,b,c", ",") // ["a", "b", "c"]
split("hello world", " ") // ["hello", "world"]
split("a::b::c", "::") // ["a", "b", "c"]Padding and Repetition
padStart("42", 5, "0") // "00042"
padEnd("hi", 10, ".") // "hi........"
repeat("-", 30) // "------------------------------"
repeat("ab", 3) // "ababab"Formatting
The fmt() function creates formatted strings with {} placeholders, filled in order from the remaining arguments. Think of it as a lightweight alternative to interpolation when your template comes from a variable or config:
fmt("Hello, {}! You have {} messages.", "Alice", 5)
// "Hello, Alice! You have 5 messages."
fmt("{} + {} = {}", 2, 3, 5)
// "2 + 3 = 5"
// Useful when the template is dynamic
template = "Welcome back, {}! Last login: {}"
print(fmt(template, "Bob", "March 5"))Centering
The center() function pads a string on both sides to center it within a given width. The default pad character is a space:
center("hello", 11) // " hello "
center("title", 20, "-") // "-------title--------"
// Great for building formatted output
heading = center(" Report ", 40, "=")
print(heading)
// "================ Report ================"Counting Substrings
The countOf() function counts how many times a substring appears in a string:
countOf("banana", "an") // 2
countOf("hello", "l") // 2
countOf("aaa", "aa") // 1 (non-overlapping)
// Handy for text analysis
csv_line = "alice,bob,charlie,diana"
num_commas = countOf(csv_line, ",")
print("Fields: {num_commas + 1}") // "Fields: 4"String Pattern Matching
One of Tova's unique features is matching strings with the ++ concat pattern:
fn parseUrl(url) {
match url {
"https://" ++ domain => { secure: true, domain: domain }
"http://" ++ domain => { secure: false, domain: domain }
_ => { secure: false, domain: url }
}
}
print(parseUrl("https://tova.dev"))
print(parseUrl("http://localhost"))This is incredibly useful for routing and parsing:
fn handle_command(input) {
match trim(input) {
"help" => show_help()
"quit" => exit()
"open " ++ filename => open_file(filename)
"search " ++ query => search_for(query)
"set " ++ rest => {
parts = split(rest, " ")
set_config(parts[0], parts[1])
}
_ => print("Unknown command: {input}")
}
}Building Strings Efficiently
For building strings from collections, join() is preferred:
// Building a CSV line
fields = ["Alice", "30", "Portland", "Engineer"]
csv_line = join(fields, ",")
// Building a table
fn format_row(cells, widths) {
formatted = zip(cells, widths)
|> map(fn(pair) padEnd(toString(pair[0]), pair[1]))
join(formatted, " | ")
}
header = format_row(["Name", "Age", "City"], [10, 5, 12])
row1 = format_row(["Alice", "30", "Portland"], [10, 5, 12])
row2 = format_row(["Bob", "25", "Seattle"], [10, 5, 12])
print(header)
print(repeat("-", 33))
print(row1)
print(row2)Common Text Processing Patterns
Title Case
fn titleCase(text) {
split(text, " ")
|> map(fn(word) {
if len(word) == 0 { "" }
else {
first = upper(substr(word, 0, 1))
rest = substr(word, 1)
"{first}{rest}"
}
})
|> join(" ")
}
print(titleCase("hello world from tova"))
// "Hello World From Tova"Slugify
fn slugify(text) {
text
|> lower()
|> replace(" ", "-")
|> replace("'", "")
}
print(slugify("Hello World It's Tova"))
// "hello-world-its-tova"Built-in Stdlib Function
slugify is available as a stdlib function — you don't need to define it yourself. The implementation above shows how it works internally. Just call slugify(text) directly.
Truncate with Ellipsis
fn truncate(text, max_len) {
if len(text) <= max_len { text }
else {
prefix = substr(text, 0, max_len - 3)
"{prefix}..."
}
}
print(truncate("A very long description that goes on and on", 20))
// "A very long descr..."Built-in Stdlib Function
truncate is available as a stdlib function — call truncate(text, max_len) directly without defining it.
Regular Expressions
For complex text patterns, Tova has regex literals and a full regex toolkit:
Regex Literals
// Regex literals use /pattern/flags syntax
email_pattern = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/
phone_pattern = /\d{3}-\d{3}-\d{4}/Testing Patterns
regexTest("alice@example.com", email_pattern) // true
regexTest("not-an-email", email_pattern) // false
regexTest("555-123-4567", phone_pattern) // trueMatching and Capturing
// Find the first match (returns Result)
result = regexMatch("Order 42-7", /(\d+)-(\d+)/)
// Ok({ match: "42-7", groups: ["42", "7"] })
// Find all matches
all = regexFindAll("I have 3 cats and 12 dogs", /\d+/)
// [{ match: "3", ... }, { match: "12", ... }]
// Named captures (returns Result)
parsed = regexCapture("2026-03-05", /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/)
// Ok({ year: "2026", month: "03", day: "05" })Replacing with Regex
// Simple replacement
clean = regexReplace("hello world foo", /\s+/, " ")
// "hello world foo"
// Replace all digits with #
masked = regexReplace("Card: 4111-2222-3333-4444", /\d/, "#")
// "Card: ####-####-####-####"Splitting with Regex
// Split on any whitespace
parts = regexSplit("hello world\tfoo\nbar", /\s+/)
// ["hello", "world", "foo", "bar"]
// Split on comma with optional spaces
items = regexSplit("a, b , c,d", /\s*,\s*/)
// ["a", "b", "c", "d"]Building Regex Patterns
For complex patterns, regex_builder provides a fluent API to construct regex step by step:
// Build a URL validation pattern
url_regex = regexBuilder()
.literal("https://")
.oneOf("a-zA-Z0-9.-")
.oneOrMore()
.literal(".")
.oneOf("a-zA-Z")
.oneOrMore()
.build()
regexTest("https://tova.dev", url_regex) // true
regexTest("not-a-url", url_regex) // falseregex_builder returns a builder object with chainable methods: .literal(s), .digits(n), .word(), .space(), .any(), .oneOf(chars), .group(name), .endGroup(), .optional(), .oneOrMore(), .zeroOrMore(), .startOfLine(), .endOfLine(), .flags(f), .build(), .test(s), and .match(s). Quantifier methods like .oneOrMore() apply to the preceding element. This is more readable than writing raw regex strings for complex patterns.
String Validation Helpers
Tova includes common validation functions:
isEmail("alice@test.com") // true
isUrl("https://tova.dev") // true
isUuid("550e8400-e29b-41d4-a716-446655440000") // true
isNumeric("42.5") // true
isAlpha("hello") // true
isAlphanumeric("abc123") // true
isHex("ff00aa") // trueWhen to Use Regex vs. String Functions
Use string functions (contains, startsWith, split) for simple patterns. Use regex for complex patterns involving repetition, alternation, or capturing groups. String functions are faster and more readable for simple cases.
Additional String Functions
Tova also provides case conversion utilities:
snakeCase("helloWorld") // "hello_world"
camelCase("hello_world") // "helloWorld"
kebabCase("helloWorld") // "hello-world"
capitalize("hello world") // "Hello world"
titleCase("hello world") // "Hello World"And text manipulation tools:
words("hello world foo") // ["hello", "world", "foo"]
lines("line1\nline2\nline3") // ["line1", "line2", "line3"]
reverseStr("hello") // "olleh"
wordWrap("long text...", 40) // Wraps at 40 chars
indentStr("hello", 4) // " hello"
dedent(" hello") // "hello"
escapeHtml("<script>") // "<script>"
unescapeHtml("<script>") // "<script>"Project: Mini Template Engine
Let's build a template engine that replaces %placeholder% markers with actual values:
fn render_template(template, data) {
var tpl = template
for entry in entries(data) {
placeholder = "%{entry[0]}%"
tpl = replace(tpl, placeholder, toString(entry[1]))
}
tpl
}
// Use it
greeting = render_template(
"Hello, %name%! You have %count% new messages.",
{ name: "Alice", count: 5 }
)
print(greeting)
// "Hello, Alice! You have 5 new messages."
// Generate HTML
card = render_template(
"<div class='card'><h2>%title%</h2><p>%body%</p></div>",
{ title: "Welcome", body: "Getting started with Tova" }
)
print(card)Exercises
Exercise 4.1: Write a caesar_cipher(text, shift) function that shifts each letter by shift positions in the alphabet. Handle both uppercase and lowercase. Non-letter characters stay unchanged. Then write caesar_decipher that reverses it.
Exercise 4.2: Write a count_vowels(text) function and a count_consonants(text) function. Then write text_stats(text) that returns an object with { vowels, consonants, spaces, digits, other }.
Exercise 4.3: Write a formatNumber(n) function that adds comma separators: formatNumber(1234567) returns "1,234,567". Handle negative numbers too.
Challenge
Build a Markdown to plain text converter that handles:
- Headers (
# text→TEXT,## text→text) - Bold (
**text**→text) - Italic (
*text*→text) - Links (
[text](url)→text (url)) - Code blocks (
``text``→text)
Process the text line by line, using string pattern matching and the string functions you've learned. Test it with a sample Markdown document.
← Previous: Mastering Collections | Next: Pattern Matching Power →