Text in depth
Joining and measuring text, pulling pieces out with sub and find, and laying values out neatly with string.format.
Joining and measuring
Two dots join two strings into one: first .. " " .. last. This is called concatenation, and it is how a Lua program builds a sentence from parts.
The # in front of a string is its length in characters, so the length of "hello" is five. The same sign gives the length of an array, which you meet in the next chapter.
Cutting text up
string.sub takes a piece of a string. Give it a start and an end position, and both are included: the first three letters of "hello" are hel.
A negative position counts from the end, so the last two letters of "hello" come from position -2. string.find answers where a piece first appears, giving back the start and end positions of the match.
Laying values out
string.format builds a string from a template. In the template, %s stands for a string, %d for a whole number, and %.2f for a number rounded to two digits after the point.
A width in front of the letter pads the value, so %5d uses five columns. This is how a column of numbers lines up under itself instead of wandering.
Strings are values, and they do not change
Every string is a value with fixed contents. string.upper(name) does not edit name; it builds a second string and hands it back. That is why you have to keep the result: name = string.upper(name) replaces the name's value, while a bare call throws the new string away.
The colon form is the same call written from the string's side: name:upper() means string.upper(name). It works for every function in the string library, and it reads left to right.
Positions, bytes and patterns
Lua counts string positions from one, and a negative position counts back from the end. string.sub(s, 2, -2) drops the first and last characters. The count is in bytes, not letters in the everyday sense, so an accented character stored as two bytes counts as two.
string.find treats its pattern as a Lua pattern, not as plain text: % escapes a special character, . matches any character, and - is lazy repetition. When you want the literal characters, pass true as the fourth argument. When nothing matches, find returns nil rather than an error, so check before you use the positions.
Patterns, in small doses
string.find accepts a pattern, and a pattern is a tiny language of its own. %a matches a letter, %d a digit, %s a space, and a plus means one or more of the thing before it. string.match returns the piece that matched rather than its position, which is often what you wanted.
Brackets capture part of a match: string.match(date, "(%d+)-(%d+)") hands back the two runs of digits separately. string.gmatch walks every match in a string, which is how the word-counting example in the maps chapter splits a sentence without a split function. Patterns are not regular expressions, so look up the set before borrowing a regex from another language.
Joining text without copying it all
Every .. builds a new string and copies both sides into it. In a loop that appends a word on each pass, the first pass copies one word, the second copies two, and the total work grows with the square of the count. For a dozen words that is invisible; for a hundred thousand it is the whole run time.
table.concat(list, ", ") joins a list in one pass and allocates once. Build the pieces in a table, then join at the end. That is the same advice as the loop chapter’s accumulator, applied to text: collect first, combine once.
The everyday string tools
local name = "Lua"
print(#name)
print(string.upper(name))
print(string.sub("hello world", 1, 5))
local start, stop = string.find("hello world", "world")
print("found at " .. start .. " to " .. stop)
print(string.format("%s has %d letters", name, #name))
print(string.format("%.2f", 3.14159))
print(string.format("%5d|", 42))Three mistakes with text. Using a find result without checking for nil, then joining nil and getting an error. Forgetting that % and . are pattern characters when you meant them literally. And expecting # to count letters: it counts bytes, which is why non-ASCII text can surprise a loop over positions.
Try these next
- Format three rows with
%8sand%6.2fso the columns line up. - Find the position of a full stop in a sentence with a literal search, then with a pattern.
- Take the first and last letters off a word with one
string.subcall.
Measuring and reshaping text
Run it and read each line against the string it came from. Then build a line of your own with string.format, and find the position of your own name inside a sentence.
A tiny report, aligned
Run it, then add a fourth name and price to both lists. Keep the columns straight by giving every name and number the same width.
A percent sign in a format string is special, so it starts a placeholder. To print a literal one, write it twice, and the pair is shown as a single percent sign.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does the # in front of a string give?
