Part of Learn Lua with NeoVim

Learn Lua in Neovim: Table Array Operations — Sort, Concat & Custom Comparators | Episode 18

Sandy LaneSandy Lane

Video: Learn Lua in Neovim: Table Array Operations — Sort, Concat & Custom Comparators | Episode 18 by Taught by Celeste AI - AI Coding Coach

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

Lua Table Array Operations: sort, concat, and Custom Comparators

table.sort(t) for ascending sort. table.sort(t, fn) with a custom comparator for any order. table.concat(t, sep) joins to a string. Build a to-do list.

Last episode covered creating and modifying arrays. Today we cover the three high-leverage operations on them: sorting, joining, and inserting at specific positions.

Sort: ascending by default

local names = { "Charlie", "Alice", "Eve", "Bob", "Diana" }

table.sort(names)

for i = 1, #names do
  print(names[i])
end

Output:

Alice
Bob
Charlie
Diana
Eve

table.sort(t) sorts the table in place (the original is modified). Default order is ascending by <. For strings, that's lexicographic. For numbers, numeric.

Sorts in place. If you need the original order preserved, copy first:

local copy = {}
for i, v in ipairs(names) do copy[i] = v end
table.sort(copy)

Concat: join to a string

local names = { "Alice", "Bob", "Charlie" }
print("Joined: " .. table.concat(names, ", "))

Output:

Joined: Alice, Bob, Charlie

table.concat(t, sep) builds one string by joining the array elements with sep between them. The separator is optional:

print(table.concat(names))         -- "AliceBobCharlie"
print(table.concat(names, " | "))  -- "Alice | Bob | Charlie"

table.concat only works on string and number values. Booleans and tables error.

Custom sort comparator

local scores = { 85, 92, 78, 95, 88 }

table.sort(scores)
print("Ascending: " .. table.concat(scores, ", "))
-- Ascending: 78, 85, 88, 92, 95

table.sort(scores, function(a, b)
  return a > b
end)
print("Descending: " .. table.concat(scores, ", "))
-- Descending: 95, 92, 88, 85, 78

Pass a function as the second argument: table.sort(t, comparator). The comparator takes two elements a and b and returns:

  • true if a should come before b.
  • false otherwise.

Default comparator is function(a, b) return a < b end. To reverse, return a > b.

Sorting tables of objects

local people = {
  { name = "Charlie", age = 30 },
  { name = "Alice", age = 25 },
  { name = "Bob", age = 35 },
}

-- Sort by name (alphabetical)
table.sort(people, function(p, q) return p.name < q.name end)

-- Sort by age (youngest first)
table.sort(people, function(p, q) return p.age < q.age end)

-- Sort by age descending
table.sort(people, function(p, q) return p.age > q.age end)

The comparator can reach into any field. This generalises to any "sort by X" need.

Stable vs unstable sort

table.sort is not a stable sort — equal elements may swap order. For most cases this is invisible. When stability matters:

-- Add an "original index" to break ties stably
for i, p in ipairs(people) do p.orig_idx = i end
table.sort(people, function(p, q)
  if p.age ~= q.age then return p.age < q.age end
  return p.orig_idx < q.orig_idx
end)

Verbose but reliable.

A to-do list

local todos = { "Buy groceries", "Clean kitchen" }

table.insert(todos, "Walk the dog")              -- append at end
table.insert(todos, 1, "Wake up early")          -- insert at position 1

print("To-Do:")
for i = 1, #todos do
  print("  " .. i .. ". " .. todos[i])
end
-- Output:
--   1. Wake up early
--   2. Buy groceries
--   3. Clean kitchen
--   4. Walk the dog

table.remove(todos, 2)   -- complete and remove item 2

print("After completing #2:")
for i = 1, #todos do
  print("  " .. i .. ". " .. todos[i])
end
-- Output:
--   1. Wake up early
--   2. Clean kitchen
--   3. Walk the dog

table.insert(todos, 1, "...") shifts everything down. Useful for "add at top" semantics; expensive for big lists (O(n) shifts).

For "first in, first out" queue behaviour, append at the end with plain table.insert(t, v) and remove from the front with table.remove(t, 1). For LIFO (stack), append and table.remove(t) at the end.

table.unpack: spread a table into args

local args = {1, 2, 3}
print(table.unpack(args))   -- 1   2   3

The opposite of {...}. Useful when you have a table and want to pass its contents to a function:

local args = {2, 8, 5, 1, 9}
print(math.max(table.unpack(args)))   -- 9

In Lua 5.1, this was just unpack (global). Lua 5.2+ moved it to table.unpack. Use the namespaced version in modern code.

Common stumbles

Sorting in place when you wanted to preserve original. table.sort(t) mutates t. Copy first if you need both.

Comparator must return strict less-than. Returning a <= b violates the strict-ordering requirement and can cause undefined behaviour. Always use < or >.

table.concat on non-string values. {1, true, "hi"} errors because of true. Convert first: table.concat({tostring(true), "hi"}, " ").

Off-by-one in inserts. table.insert(t, 0, x) doesn't add to "before the first" — Lua clamps. To prepend, use position 1.

Mutating during iteration. Sorting a list while iterating it produces undefined results. Mutate first, iterate after.

What's next

Episode 19: tables as dictionaries. Same {} syntax, but with string keys: { name = "Alice", age = 30 }. Dot notation vs bracket notation. Adding, modifying, and deleting keys.

Recap

table.sort(t) for in-place ascending sort. table.sort(t, function(a, b) return a > b end) for custom order. Comparator must be strict less-than. table.concat(t, sep) joins to a string (string/number values only). table.insert(t, pos, v) for positional inserts; table.remove(t, pos) to shift things out. table.unpack(t) spreads a table back into multiple args.

Next episode: tables as dictionaries.

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.