---
url: /luau-basics/strings-and-libraries.md
description: >-
  Interpolation, patterns, and the string and math libraries for everyday text
  and numbers.
---

# Strings and Libraries

*Written: {{ $frontmatter.date }}*

## Learning objectives

* You interpolate with backticks and escape safely.
* You format with string.format verbs.
* You search and replace with string patterns.
* You split and slice with sub and split.
* You apply math essentials with confidence.
* You tour the table library for string-adjacent work.

## Prerequisites

* [Strings Numbers Nil](../programming-fundamentals/strings-numbers-nil.md)
* [Variables and Values](../programming-fundamentals/variables-and-values.md)
* [Tables Part 1](../programming-fundamentals/tables-part-1.md)
* [Tables in Depth](./tables-in-depth.md)

## Backtick interpolation builds readable text

Backtick strings let you drop expressions into text. Wrap the literal in backticks. Put each expression in single braces. Luau evaluates the braces and inserts the results. The string guide in [strings](https://create.roblox.com/docs/luau/strings) shows the core shapes.

Run this pure snippet in the Command Bar.

```luau
--!strict
local name = "Mara"
local wins = 3
local line = `{name} has {wins} wins`
print(line)
```

```txt
Mara has 3 wins
```

Two locals go into one literal. Braces accept any expression, including math and calls. Prefer this over long `..` chains for messages, labels, and logs.

Run this pure snippet in the Command Bar.

```luau
--!strict
local price = 25
print(`Total: {price * 2} coins`)
```

```txt
Total: 50 coins
```

The brace holds `price * 2`. Luau computes 50 and inserts it. The sentence stays intact.

Backticks do not accept {{ as an escaped brace. Two opening braces in a row are a parse error. This rule stops confusion with languages that double braces to escape them. The restriction is documented in the [syntax tour](https://luau.org/syntax). You insert a literal brace with an escape instead.

## Escapes keep special characters literal

Inside backticks, a backslash escapes the next character: backslash backtick for a literal backtick, backslash brace for a literal brace, and double backslash for a literal backslash. You can also escape a newline to continue a long literal.

Run this pure snippet in the Command Bar.

```luau
--!strict
local shown = `Use \`backticks\` around code`
print(shown)
```

```txt
Use `backticks` around code
```

Each backslash backtick pair produces one visible backtick. The string stays one literal. You use the same habit for braces when you show template syntax to players.

Quoted strings use their own escapes. Double-quoted strings escape inner doubles with backslash. Single-quoted strings escape inner singles the same way. Newlines use `\n` and tabs use `\t`. These rules also live in [strings](https://create.roblox.com/docs/luau/strings).

String indices are 1-based everywhere. The first character is position 1. Negative positions count back from the end, so negative 1 is the last character. You carry this habit from tables into every slice and search.

## string.format handles verbs and layout

Use `string.format` when you need verbs, padding, or precision. The first argument is a template with verbs. Later arguments fill the verbs in order. Common verbs include `%s` for strings, `%d` and `%i` for integers, `%f` for floats, `%q` for quoted re-readable strings, `%x` for hex, and `%%` for a literal percent sign.

Run this pure snippet in the Command Bar.

```luau
--!strict
print(string.format("%s has %d coins", "Mara", 42))
```

```txt
Mara has 42 coins
```

`%s` takes the name. `%d` takes the count. You keep the sentence shape while Luau converts each value.

Run this pure snippet in the Command Bar.

```luau
--!strict
print(string.format("Price: %.2f", 3.5))
print(string.format("Hex: %x", 255))
print(string.format("Safe: %q", "say hi"))
print(string.format("%d%% sure", 100))
```

```txt
Price: 3.50
Hex: ff
Safe: "say hi"
100% sure
```

`%.2f` rounds to two decimals for shop prices. `%x` prints lowercase hex for colors and ids. `%q` adds quotes and escapes so you can paste the value back into code. `%%` emits one percent sign. You pick format for layout work and backticks for plain insertion.

## Patterns find shapes, not just text

A pattern describes a shape in text. `%d` matches any digit. `%a` matches any letter. `%s` matches whitespace including spaces and newlines. `%w` matches letters and digits. `%l` matches lowercase, `%u` matches uppercase, `%p` matches punctuation, `%x` matches hex digits. Uppercase versions invert the class, so `%D` means non-digit. The full class table sits in [strings](https://create.roblox.com/docs/luau/strings).

Modifiers widen a class. `+` means one or more. `*` means zero or more. `-` means as few as possible. `?` means zero or one. Anchors pin the match: `^` pins to the start, `$` pins to the end. Sets group choices in brackets. Captures in parentheses save pieces for later.

Run this pure snippet in the Command Bar.

```luau
--!strict
print(string.match("Room 42B", "%d+"))
print(string.match("hello world", "%a+"))
```

```txt
42
hello
```

`%d+` grabs the digit run `42`. `%a+` grabs the letter run `hello`. Each call returns the first match. You add captures when you need pieces.

`string.find` returns start and end positions instead of text. It returns nil when nothing matches. Pass an `init` position to start later in the string. Use true as the fourth argument for a plain search with no pattern magic.

Run this pure snippet in the Command Bar.

```luau
--!strict
local s = "hi there, hi again"
local a, b = string.find(s, "hi", 4)
print(a)
print(b)
print(string.find(s, "bye"))
```

```txt
10
11
nil
```

The search starts at 4 and finds the second `hi` at 10 through 11. The missing word returns nil. You guard nil before you slice with the positions.

`string.gsub` replaces every match and returns the new string plus a count. It accepts a string, a table, or a function as the replacement. Captures `%1` through `%9` reuse saved pieces.

Run this pure snippet in the Command Bar.

```luau
--!strict
local out, n = string.gsub("I have 2 cats", "%d+", "3")
print(out)
print(n)
```

```txt
I have 3 cats
1
```

The digit run becomes `3`. The count reports one substitution. You cap replacements with a fourth numeric argument when only the first hits should change.

`string.sub` slices by byte positions. Both ends have defaults: start defaults to 1, finish defaults to negative 1. Out-of-range ends clamp. A start past the finish returns an empty string.

Run this pure snippet in the Command Bar.

```luau
--!strict
local file = "script.lua"
local dot = string.find(file, "%.")
print(dot)
print(string.sub(file, dot + 1))
print(string.sub(file, 1, 6))
```

```txt
7
lua
script
```

Escape the dot as `%.` because a bare dot means any character. The dot sits at 7. Slicing after it gives `lua`, and slicing 1 through 6 gives `script`. Combine find plus sub for extensions, tags, and prefixes.

`string.split` chops on a separator and returns an array. The default separator is a comma. Empty slices survive as empty strings. Surrounding whitespace stays put.

Run this pure snippet in the Command Bar.

```luau
--!strict
local parts = string.split("a,b,c", ",")
print(#parts)
print(table.concat(parts, "|"))
local kept = string.split("x,,y", ",")
print(#kept)
print(kept[2] == "")
```

```txt
3
a|b|c
3
true
```

Three names split into three entries. The double comma keeps an empty middle entry, so the second table also has length 3 with an empty string at 2. Split suits CSV settings, chat commands, and simple configs. Use patterns when the separator varies.

## Math essentials cover games

The math library holds rounding, clamping, roots, powers, randomness, and constants. Call it through the `math` table. Gameplay code rarely needs more.

Run this pure snippet in the Command Bar.

```luau
--!strict
print(math.floor(4.9))
print(math.ceil(4.1))
print(math.round(4.5))
print(math.abs(-7))
print(math.min(3, 9, 1))
print(math.max(3, 9, 1))
```

```txt
4
5
5
7
1
9
```

Floor drops down, ceil lifts up, round goes to the nearest integer away from zero at halves. Abs strips sign. Min and max take any number of arguments. You use these for grid snaps, damage bounds, and leaderboard extremes.

Run this pure snippet in the Command Bar.

```luau
--!strict
print(math.clamp(15, 1, 10))
print(math.sqrt(16))
print(math.pow(2, 3))
print(math.pi)
```

```txt
10
4
8
3.1415926535898
```

Clamp pins 15 into 1 through 10. Sqrt inverts squares. Pow raises 2 to 3. Pi ships as a constant for circles and waves. You pair clamp with health, speed, and UI ranges.

Run this pure snippet in the Command Bar.

```luau
--!strict
print(math.random(1, 6))
```

```txt
4
```

The output shows one possible die roll. Your run prints a value from 1 through 6. Call `math.randomseed` only when you need repeatable sequences for tests. Most games accept the default seed and call `math.random` directly.

`tonumber("42")` returns 42 and `tonumber("hi")` returns nil. Math on a non-numeric string errors, so validate with `tonumber` before you compute on player input.

## Table helpers support string work

String work often ends in tables. Split returns arrays. Captures fill arrays. You join results with `table.concat` and search them with `table.find`. The deep tour lives in [Tables in Depth](./tables-in-depth.md) and the reference in [tables](https://create.roblox.com/docs/luau/tables).

Run this pure snippet in the Command Bar.

```luau
--!strict
local line = "tank,healer,dps"
local team = string.split(line, ",")
print(table.find(team, "healer"))
print(table.concat(team, " + "))
```

```txt
2
tank + healer + dps
```

One line splits into three roles. Find locates `"healer"` at 2. Concat joins with a readable separator. This split, find, join loop covers most lobby and chat parsing.

## Common mistakes

Wrong: you double braces to escape in backticks.

```luau
--!strict
local x = 1
print(`{{x}}`)
```

Studio reports a parse error on {{. Fix it with a backslash escape for a literal brace, or drop the extra brace.

```luau
--!strict
local x = 1
print(`\{x} is {x}`)
```

```txt
{x} is 1
```

Wrong: you forget that a dot is magic in patterns.

```luau
--!strict
print(string.match("go to roblox#com", "roblox.com"))
```

This matches `roblox#com` because `.` means any character. Fix it by escaping the dot.

```luau
--!strict
print(string.match("go to roblox.com", "roblox%.com"))
```

```txt
roblox.com
```

Wrong: you index strings from 0.

```luau
--!strict
print(string.sub("hello", 0, 1))
```

Luau clamps 0 to 1 and returns `h`, which confuses zero-based readers. Fix it by starting at 1.

```luau
--!strict
print(string.sub("hello", 1, 1))
```

```txt
h
```

Wrong: you do math on raw player text.

```luau
--!strict
local raw = "ten"
print(raw + 5)
```

Studio raises `attempt to perform arithmetic (add) on string and number`. Fix it with `tonumber` and a nil guard.

```luau
--!strict
local raw = "10"
local n = tonumber(raw)
print(if n ~= nil then n + 5 else 0)
```

```txt
15
```

## Exercises

1. Interpolate a welcome line with a name and a level. Include a literal brace via escape.
2. Format a shop price with two decimals, a hex color id, and a quoted item name.
3. Extract all numbers from a sentence with `%d+` and join them with `-`.
4. Split a comma list, find one entry, replace a word with gsub, and clamp a math result.

::: details Solution 1
Run this pure snippet in the Command Bar.

```luau
--!strict
local who = "Mara"
local lvl = 6
print(`Welcome \{hero\} {who}, level {lvl}`)
```

```txt
Welcome {hero} Mara, level 6
```

You escape the first brace pair so it prints literally. The real values interpolate normally.
:::

::: details Solution 2
Run this pure snippet in the Command Bar.

```luau
--!strict
print(string.format("Sword: %.2f coins", 12.5))
print(string.format("Tint: %x", 255))
print(string.format("Name: %q", "Oak Blade"))
```

```txt
Sword: 12.50 coins
Tint: ff
Name: "Oak Blade"
```

Two decimals suit money and hex suits tints, while the quoted form suits names you may paste back into code.
:::

::: details Solution 3
Run this pure snippet in the Command Bar.

```luau
--!strict
local text = "Wave 3 spawns 12 bots in 4 s"
local hits = {}
for token in string.gmatch(text, "%d+") do
	table.insert(hits, token)
end
print(table.concat(hits, "-"))
```

```txt
3-12-4
```

You loop matches with gmatch, collect each digit run, and join with dashes.
:::

::: details Solution 4
Run this pure snippet in the Command Bar.

```luau
--!strict
local roles = string.split("tank,healer,dps", ",")
print(table.find(roles, "dps"))
local msg, n = string.gsub("hp low, hp low", "low", "ok")
print(msg)
print(n)
print(math.clamp(99, 0, 50))
```

```txt
3
hp ok, hp ok
2
50
```

Find locates `"dps"` at 3. Gsub replaces both hits. Clamp pins 99 down to 50.
:::

## Beyond string and math: utf8, vector, buffer, os

Four libraries from the [standard library](https://luau.org/library/) cover the gaps this chapter leaves open.

**utf8** handles international text by codepoint, not by byte. `string.len` counts bytes and breaks on multibyte names; `utf8.len` counts codepoints and returns nil plus the first bad byte on malformed input. Use `utf8.codes` to iterate, `utf8.offset` to find byte positions, and `utf8.char`/`utf8.codepoint` to convert.

```luau
--!strict
local name = "Mara✓"
print(string.len(name))
print(utf8.len(name))
for bytePos, code in utf8.codes(name) do
	print(bytePos, code)
end
```

**vector** is the immutable Luau vector type with `.x`/`.y`/`.z` components. The library offers `create`, `magnitude`, `normalize`, `dot`, `cross`, `angle`, `floor`, `ceil`, `abs`, `sign`, `clamp`, `min`, `max`, plus `zero` and `one`. There is no `vector.lerp` in the reference, blend manually or use `Vector3:Lerp()` at the Engine boundary.

```luau
--!strict
local a = vector.create(0, 0, 0)
local b = vector.create(10, 0, 0)
local mid = a + (b - a) * 0.5
print(mid.X)
print(vector.magnitude(b - a))
print(vector.dot(vector.one, vector.one))
```

**buffer** is a fixed-size mutable binary block for compact protocols and MemoryStore payloads. Create with `buffer.create` or `buffer.fromstring`, then use typed `read*`/`write*` calls with byte offsets in little-endian order, plus `readstring`/`writestring`, `copy`, and `fill`. Reads or writes outside the block throw.

```luau
--!strict
local buf = buffer.create(8)
buffer.writeu32(buf, 0, 12345)
buffer.writestring(buf, 4, "hi")
print(buffer.readu32(buf, 0))
print(buffer.readstring(buf, 4, 2))
print(buffer.len(buf))
```

**os** covers time and profiling. `os.clock` returns a high-precision timestamp for measuring duration (use it with the MicroProfiler chapter). `os.time`/`os.date`/`os.difftime` handle wall-clock dates; prefer Roblox `DateTime` for timezone-safe persistence of player data.

```luau
--!strict
local start = os.clock()
local sum = 0
for i = 1, 1000 do
	sum += i
end
print(os.clock() - start)
print(os.date("%Y-%m-%d"))
```

## Summary

* Backticks interpolate expressions. Escape backticks and braces. {{ is a parse error.
* string.format verbs handle layout. Patterns with `%d`, `%a`, `%s`, and friends find shapes.
* find, gsub, sub, and split cover search, replace, slice, and chop. String positions start at 1.
* math covers rounding, bounds, roots, powers, and randomness. Tables hold string results.
* Next: [Advanced Functions](./functions-advanced.md).
