Learn Lua in Neovim: Math & String Libraries — floor, random, sub, format & Dice Roller | Episode 23
Video: Learn Lua in Neovim: Math & String Libraries — floor, random, sub, format & Dice Roller | Episode 23 by Taught by Celeste AI - AI Coding Coach
Lua Math and String Libraries: floor, random, sub, format
math.floor,math.ceil,math.abs,math.max/min,math.pi,math.random.string.sub,string.find,string.format,string.byte/char. Build a dice roller.
Lua's standard library is intentionally small, but the math and string namespaces cover most everyday needs. Today we tour both and build a dice roller.
Math basics
print(math.floor(3.7)) -- 3 (round down)
print(math.ceil(3.2)) -- 4 (round up)
print(math.abs(-15)) -- 15 (absolute value)
print(math.max(5, 9, 3)) -- 9 (largest of all args)
print(math.min(5, 9, 3)) -- 3 (smallest)
Five core utilities. math.max and math.min accept any number of arguments — useful when you don't know how many values you have in advance.
For "round to nearest," combine: math.floor(x + 0.5) (standard rounding) or math.floor(x + 0.5) for positive numbers; for negatives the rules differ depending on what you want.
math.pi and circle area
local radius = 5
local area = math.pi * radius ^ 2
print("Circle area: " .. string.format("%.2f", area)) -- "78.54"
math.pi is the constant 3.141592653589793.... Use it directly, no need to redefine.
string.format("%.2f", value) formats to two decimal places. We'll see more string.format patterns shortly.
Random numbers
math.randomseed(os.time())
print(math.random(1, 10)) -- random integer 1..10
print(math.random(1, 10)) -- another
print(math.random()) -- random float in [0, 1)
Three forms:
math.random()— float in [0, 1).math.random(n)— integer 1..n.math.random(a, b)— integer a..b (inclusive both).
math.randomseed(seed) initialises the generator. Without it, every run produces the same sequence (the default seed). Use os.time() for time-based seeding (different every second), or a fixed seed for reproducible tests.
In Lua 5.4, math.random is a much better generator (xoshiro256). In 5.1–5.3, it's whatever the C rand() happens to be — uniform but not great for serious work.
More math: power, log, trig
print(math.sqrt(16)) -- 4.0
print(math.exp(1)) -- 2.718... (e)
print(math.log(100, 10)) -- 2 (log base 10)
print(math.sin(math.pi / 2)) -- 1.0
print(math.cos(0)) -- 1.0
print(math.tan(math.pi / 4)) -- 1.0
Standard math: sqrt, exp, log, sin, cos, tan (and inverses asin, acos, atan). Trig is in radians. For degrees, multiply: math.sin(math.rad(45)).
String basics
local text = "Hello, Lua World!"
print(string.sub(text, 1, 5)) -- "Hello"
print(string.sub(text, 8)) -- "Lua World!" (from index 8 to end)
string.sub(s, i, j) extracts a substring from index i to j (inclusive). Both indices are 1-based. Omit j to go to the end. Negative indices count from the end:
print(string.sub(text, -6)) -- "World!" (last 6 chars)
print(string.sub(text, -6, -1)) -- same
string.find: locate a substring
local start, stop = string.find(text, "Lua")
print("find Lua: " .. start .. " to " .. stop) -- "8 to 10"
Returns the start and end indices of the first match. Returns nil if not found:
local start = string.find(text, "Python")
if not start then
print("Not found")
end
string.find also supports patterns (covered in episode 25). For literal-only matching, pass true as the fourth arg: string.find(s, pattern, init, plain).
string.format: printf for Lua
print(string.format("Name: %s, Age: %d", "Alice", 30))
print(string.format("Pi: %.4f", math.pi))
print(string.format("Hex: %x", 255))
Output:
Name: Alice, Age: 30
Pi: 3.1416
Hex: ff
C-style format specifiers:
%s— string.%d— integer.%f— float (use%.2ffor fixed decimal places).%x— lowercase hex;%Xfor uppercase.%e— scientific notation.%%— literal%.
Width and padding:
%5d— pad to width 5.%-5d— left-align.%05d— zero-pad.
Use string.format whenever you need controlled output — money, padded columns, hex dumps.
byte and char: ASCII conversions
print(string.byte("A")) -- 65
print(string.char(65)) -- "A"
string.byte(s, i) returns the numeric value of the character at index i. Default is index 1.
string.char(n) returns the one-character string for byte value n.
Useful for bit manipulation, custom encoding, or comparing characters numerically.
The dice roller
local function roll_dice(count, sides)
local rolls = {}
local total = 0
for i = 1, count do
local roll = math.random(1, sides)
rolls[i] = roll
total = total + roll
end
return string.format("[%s] = %d", table.concat(rolls, ", "), total)
end
print("3d6: " .. roll_dice(3, 6)) -- e.g. "[4, 1, 6] = 11"
print("2d20: " .. roll_dice(2, 20)) -- e.g. "[15, 8] = 23"
Six pieces:
- Roll
countdice ofsidessides each. - Track each roll in a table.
- Sum them.
- Format with
string.formatfor the brackets and totals. - Use
table.concat(rolls, ", ")to make the comma-separated list. - Return one neat string.
Don't forget to math.randomseed(os.time()) once at startup, otherwise every run produces the same dice rolls.
Other useful string functions
print(string.upper("hello")) -- "HELLO"
print(string.lower("HELLO")) -- "hello"
print(string.len("hello")) -- 5 (same as #"hello")
print(string.rep("-", 10)) -- "----------"
print(string.reverse("hello")) -- "olleh"
The full string library has more — gmatch, gsub, match are pattern-based and covered in episode 25.
Method-call syntax
local s = "Hello"
print(s:upper()) -- "HELLO" (same as string.upper(s))
print(s:sub(1, 3)) -- "Hel"
s:method() is sugar for string.method(s, ...) — passes s as the implicit first argument. We dive into colon syntax in episode 30. For now, both work; pick one for consistency.
Common stumbles
Forgetting math.randomseed. Same sequence every run unless you seed with something time-varying.
Trig in degrees. math.sin(45) is not sin(45°) — convert with math.rad(45).
%.2f produces wrong output for huge numbers. string.format("%.2f", 1e20) works but the result is huge. For currency, format the float, not the string.
string.find returning two values. local pos = string.find(s, "x") discards the second return. That's fine, but be aware.
Confusing 1-based and 0-based when porting code. Lua's string.sub starts at 1; C's substr starts at 0. Adjust when translating.
What's next
Episode 24: modules and require. Splitting code into reusable files. local M = {}, attach functions, return M. Then require("modulename") from another file.
Recap
math library: floor, ceil, abs, max, min, sqrt, pi, random (with randomseed). string library: sub (1-based, negative indices count from end), find (returns start, stop or nil), format (printf-style), byte/char (ASCII conversion), upper/lower/rep/reverse. string.format is the right tool for any controlled output. Method syntax s:upper() works alongside string.upper(s).
Next episode: modules and require.