Skip to content
Lesson 4 of 18

Step 1 of 5 · Reading · ~4 min

Learn

Strings

String Basics

A Lua string is an immutable sequence of bytes. Nothing you call on a string ever changes it — every string function hands back a brand-new string. Internalise that one fact and a whole family of bugs disappears.

✗ result thrown away

local s = "hello"
s:upper()
print(s)

--> hello

✓ result kept

local s = "hello"
s = s:upper()
print(s)

--> HELLO

Building and measuring

.. joins strings; # measures one.

local first = "Grace"
local last = "Hopper"
local full = first .. " " .. last
print(full)
print(#full)

--> Grace Hopper
--> 12

# counts bytes, not characters. For plain ASCII the two numbers agree. For anything else they do not, because Lua strings are byte strings and your source file is UTF-8.

print(#"cafe")
print(#"café")

--> 4
--> 5

When you genuinely need character counts, utf8.len(s) does it.

Slicing with sub

Lua indexes strings from 1, and sub includes both endpoints. Negative positions count back from the end, with -1 meaning the last byte.

local s = "shipthatcode"
print(s:sub(1, 4))
print(s:sub(5, 8))
print(s:sub(9))
print(s:sub(-4))

--> ship
--> that
--> code
--> code

Two habits to unlearn if you come from C, Python, Java or JavaScript: there is no index 0, and the second argument is the last position you want — not one past it.

Two ways to call the same function

string.upper(s) and s:upper() are the same call. The colon quietly passes the string as the first argument, so method style reads better when you chain.

print(string.upper("hi"))
print(("hi"):upper())
print(("ab"):rep(3))
print(string.find("shipthatcode", "that"))

Output (that last gap is a TAB — find returns two values):

HI
HI
ababab
5	8

The parentheses in ("hi"):upper() are required; "hi":upper() will not parse.

How do I split a line into words?

Lua has no split function. The idiom is string.gmatch, which returns an iterator — a function the loop calls repeatedly, getting the next match each time and nil when there are none left.

The pattern "%S+" means "one or more non-space bytes", so it yields each run of visible characters and skips the gaps between them, however wide they are.

local words = {}
for w in string.gmatch("to be  or not", "%S+") do
    table.insert(words, w)
end
print(#words)
print(words[1])
print(words[4])

--> 4
--> to
--> not

table.insert(words, w) appends to the end of the list. You will meet tables properly soon; for now, treat words as a numbered list starting at 1.

How do I join a list back into a string?

table.concat(list, separator) glues a list of strings together in a single pass.

✓ one allocation

local parts = {"to", "be", "or", "not"}
print(table.concat(parts, " "))

--> to be or not

✗ appending in a loop

local out = ""
for i = 1, #parts do
    out = out .. parts[i] .. " "
end

Because strings are immutable, every .. builds a new string and copies everything accumulated so far, so that loop costs O(n2)O(n^2) — and it leaves a trailing space you then have to trim off. table.concat does neither.

Walking a list backwards

A counted loop takes a start, a stop, and an optional step. A step of -1 counts down.

local parts = {"to", "be", "or", "not"}
local out = {}
for i = #parts, 1, -1 do
    table.insert(out, parts[i])
end
print(table.concat(out, " "))

--> not or be to

Both ends are inclusive, so #parts down to 1 really does visit every element. Leave the -1 off and the loop counts upward from #parts to 1 — a range that is already finished before it starts, so the body never runs at all.

Your exercise

Reverse Words reads one line and prints the same words in the opposite order, separated by single spaces.

The starter has already split the line for you: words holds each word in original order. Your job is the walk back down plus the join.

Two mistakes the grader will catch. First, joining without reversing — table.concat(words, " ") returns the line untouched, and the visible single-word test hello still passes, so it is easy to believe you are done. Second, the loop bounds: start at #words, because starting at #words - 1 silently drops the word that should be printed first, and keep the -1 step, because without it the loop body never executes and you print an empty line.

Up nextString FormattingStrings

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…