Part of Learn Lua with NeoVim

Learn Lua in Neovim: Math & String Libraries — floor, random, sub, format & Dice Roller | Episode 23

Sandy LaneSandy Lane

Video: Learn Lua in Neovim: Math & String Libraries — floor, random, sub, format & Dice Roller | Episode 23 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

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 %.2f for fixed decimal places).
  • %x — lowercase hex; %X for 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:

  1. Roll count dice of sides sides each.
  2. Track each roll in a table.
  3. Sum them.
  4. Format with string.format for the brackets and totals.
  5. Use table.concat(rolls, ", ") to make the comma-separated list.
  6. 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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.