Troubleshooters.Com and Code Corner Present

Litt's Lua Laboratory:
Lua Tables
(With Snippets)

Copyright (C) 2011 by Steve Litt



Debug like a Ninja

Contents

  • Introduction
  • Hello World
  • Primarily Numeric Tables
  • Tables With Named Keys
  • Generic For Loops
  • "Indexing" and Sorting Tables
  • Introduction

    A Lua table is a group of key<=>value pairs. Think hashes in Perl and Ruby or Dictionaries in Python. But Lua is built from the ground up to be fast and efficient with its tables. Lookup is quick. So unlike Perl, Python and Ruby, with Lua it's efficient to use a table as an array.

    And because functions are just data, it's perfectly reasonable to use a function as one value in a table. And what do you get when you bundle data and functions together? You get an object.

    Ever program in C? Remember structs? Each field could represent an integer, a string (pointer to char), an array, another struct, a pointer to another struct, or even a pointer to function. Well guess what -- you can use a table to do all of that -- the key is the struct's element name, and the value is the value of the struct's element. And even cooler, unlike C, with Lua tables you can add elements to the struct at runtime.

    Because Lua's syntax is so much easier than other languages, you can build up a huge data structure using tables, and that data structure will be intuitively obvious. You can have many, many dimensions and levels of abstraction.

    Tables can also serve as very simple objects, but Lua OOP doesn't really come into its own until you combine tables with closures, so we'll delay that discussion.

    I remember working in Perl. You'd refer to an array as @myarray but its elements would be $myarray[9]. You'd refer to a hash as %myhash but its elements would be $myhash[key]. And when you instantiated references to these things, the syntax would get even stranger. All that's gone with Lua. You instantiate a Lua table as:
    myarray = {"one", "two", "three", "four"}
    or
    mystruct = {fname="Barack", lname="Obama", job="President}

    or
    myemptytable = {}
    You can then add elements by name, or with numbered elements, by number or by appending:
    mystruct["party"] = "Democratic"
    Note the key is surrounded by quotes in the preceding. If it weren't, party would be taken to be a variable. Lua provides an alternate syntax to make things easier (alternate syntaxes to make things easier are called "syntactic sugar"):
    mystruct.party = "Democratic"
    As far as numbers, you can use either the 2 argument insert() or use array notation with square brackets.

    Hello World

    Here's your first table:
    #!/usr/bin/lua 

    mytable = {} -- make empty table
    mytable[1] = "one" -- assign "one" to its element 1
    print(mytable[1]) -- print element number 1
    You create an empty table, assign element 1, and then print element 1, giving you exactly the output you expect:
    slitt@mydesk:~$ ./test.lua
    one
    slitt@mydesk:~$
    In the preceding example, you had to make the empty table. If you'd just tried to assign element 1 to a non-table variable, you'd have gotten a "attempt to index global 'mytable' (a nil value)" error.

    Actually you don't have to create an empty table. You could have put some elements in at creation time:
    #!/usr/bin/lua 

    mytable = {"one", "two", "three"}
    print(mytable[2]) -- print element number 1

    Primarily Numeric Tables

    The following program shows three ways to store by numeric key:
    1. During initialization
    2. Explicit subscript
    3. Appending with two arg insert command
    The following also shows a way to iterate throught a table's numeric keys, the pairs command:
    #!/usr/bin/lua 

    myarray = {"one", "two", "three", "four"}
    myarray[5] = "five"
    table.insert(myarray, "six")

    for k, v in pairs(myarray) do
    print(string.format("k=%s, v=%s", tostring(k), tostring(v)))
    end
    The preceding prints the following:
    slitt@mydesk:~$ ./test.lua
    k=1, v=one
    k=2, v=two
    k=3, v=three
    k=4, v=four
    k=5, v=five
    k=6, v=six
    slitt@mydesk:~$

    Tables With Named Keys

    Tables are simply sets of key/value pairs. The previous article discussed tables whose keys were integers. This one discusses tables with named keys:
    president = {fname = "Barack", lname = "Obama", from = 2009, to = nil}
    Let's show a few presidents:
    presidents = {
    {fname = "Barack", lname = "Obama", from = 2009, to = nil},
    {fname = "George", lname = "Bush", from = 2001, to = 2008},
    {fname = "Bill", lname = "Clinton", from = 1993, to = 2000},
    {fname = "George", lname = "Bush", from = 1989, to = 1992}
    }
    Now let's add another president:
    presidents.insert(
    {fname = "Ronald", lname = "Reagan", from = 1981, to = 1988}
    )
    Does this look like a database table to you? It sure does to me. But it's implemented as a table of tables, where the top level table has numeric keys 1 through 5, and each of the lower level tables has the same four keys, but with different values for the keys. Because the top level table has all integer keys, you can sort it with table.sort() using fname, lname, from or to to sort by. You can also iterate through the top table and then print each "column" for each element of the top level table.

    You cannot use table.sort() to sort a table with named keys. Not directly, anyway. But you can put each key and value into an element of a different table with a numeric key, and then sort the one with numeric keys. Watch this:
    #!/usr/bin/lua 
    sf = string.format

    -- DEFINE TABLE WITH WORD KEYS
    earthquakes = {
    date8 = "1992/01/17",
    date7 = "1971/02/09",
    date6 = "2010/04/04",
    date5 = "1987/10/19"
    }

    -- CONVERT TO TABLE WITH NUMERIC KEYS
    earthquakes_numeric = {}
    for k,v in pairs(earthquakes) do
    table.insert(earthquakes_numeric, {key=k, value=v})
    end

    -- READ THEM BACK
    for i, v in ipairs(earthquakes_numeric) do
    print(sf("Row %d, key=%s, value=%s",i, v.key, v.value))
    end

    -- SORT THEM
    table.sort(earthquakes_numeric, function(a,b) return a.value < b.value end)
    print("=====================")

    -- READ THEM BACK AGAIN
    for i, v in ipairs(earthquakes_numeric) do
    print(sf("Row %d, key=%s, value=%s",i, v.key, v.value))
    end
    The preceding code produces the following output:
    slitt@mydesk:~$ ./test.lua
    Row 1, key=date6, value=2010/04/04
    Row 2, key=date8, value=1992/01/17
    Row 3, key=date5, value=1987/10/19
    Row 4, key=date7, value=1971/02/09
    =====================
    Row 1, key=date7, value=1971/02/09
    Row 2, key=date5, value=1987/10/19
    Row 3, key=date8, value=1992/01/17
    Row 4, key=date6, value=2010/04/04
    slitt@mydesk:~$
    So that's it. That's how you can sort a table with named keys -- you simply make each key/value pair an element in a table with numeric keys.

    Generic For Loops

    Generic for loops iterate through anything for which an iterator is available or can be made. The rest of this article is about generic for loops using two iterators: pairs() and ipairs(), both of which iterate through tables.

    The ipairs() iterator iterates, in numeric order, all elements with positive integer keys, from 1 until the first nonexistant or nil-valued key is encountered. Let me show you what I mean:
    #!/usr/bin/lua

    local mytable = {"one", "two", "three"}
    mytable[4] = "four"
    mytable[6] = "six" --notice the gap at 5
    mytable["name"] = "Steve" --notice this is a string, not an integer
    mytable[1.5] = "one-point-five" --notice this is a float, not integer

    for k, v in ipairs(mytable) do
    print(string.format("k=%s, v=%s",tostring(k),tostring(v)))
    end
    The preceding creates a table with keys 1 through 4, 6 (skipping 5), float 1.5 and string "name". What it should do is iterate through keys 1 through 4, skipping 1.5 because 1.5 is not an integer, and stopping at 4 because 5 is undefined. It also doesn't iterate key "name" because it's a string, not an integer. So the preceding should print "one", "two", "three" and "four". Let's see if it does:
    slitt@mydesk:/d/websites/tjunct/codecorn/lua$ ./test.lua
    k=1, v=one
    k=2, v=two
    k=3, v=three
    k=4, v=four
    slitt@mydesk:/d/websites/tjunct/codecorn/lua$
    There it is -- ipairs() worked just as specified.

    The other iterator you need to know about is pairs(). This iterator is guaranteed to iterate over every key of every kind as long as it has a non-nil value. However, it doesn't iterate in any particular order, so if you need it sorted you need to sort it yourself. In the previous code, delete the i in ipairs() to make pairs(), and try it again:
    slitt@mydesk:/d/websites/tjunct/codecorn/lua$ ./test.lua
    k=1, v=one
    k=2, v=two
    k=3, v=three
    k=4, v=four
    k=6, v=six
    k=name, v=Steve
    k=1.5, v=one-point-five
    slitt@mydesk:/d/websites/tjunct/codecorn/lua$
    You can see it printed every key and value. It just so happens the numeric keys were printed first and printed in order. DON'T COUNT ON THAT, IT WON'T ALWAYS WORK!!!

    "Indexing" and Sorting Tables

    Lua tables are a lot like database tables. If there are just a few rows in the database (elements in the array), it's quick and easy to to locate a row (element) with a certain property just by sequentially going through the whole table. But if you have thousands, or even millions of rows (elements). Even with a small number of elements, using a Lua index is algorithmically easier than sequentially searching the table.

    So let's say you have the following table of tables:
    presidents = {
    {lname = "Obama", fname = "Barack", from = 2009, to = nil},
    {lname = "Bush", fname = "George W", from = 2001, to = 2008},
    {lname = "Bush", fname = "George HW", from = 1989, to = 1992},
    {lname = "Clinton", fname = "Bill", from = 1993, to = 2000}
    }
    You could set up an index table whose each element is has the "from" year as a key, and the record number in the presidents table as the value. So you look for the year as a key in the index table, and look in the presidents table for the element whose key is the value from the index table, and bang, you've looked it up by year. Of course this works only if each element's from year is unique. Otherwise you'd need to do things differently.

    About sorting: the table.sort() function works only on tables whose keys are consecutive integers starting with 1. In other words, a table essentially functioning as an array. Fortunately, no matter what a table looks like, it's always fairly easy to build an array-like table such that each element of the array-like table is a "column" such as from in the presidents table. Then you can sort the array like table, and work through the index table as shown in the following program:
    #!/usr/bin/lua 
    sf = string.format
    ts = tostring

    -- MAKE LIST OF LAST FOUR PRESIDENTS IN NO SPECIAL ORDER
    presidents = {
    {lname = "Obama", fname = "Barack", from = 2009, to = nil},
    {lname = "Bush", fname = "George W", from = 2001, to = 2008},
    {lname = "Bush", fname = "George HW", from = 1989, to = 1992},
    {lname = "Clinton", fname = "Bill", from = 1993, to = 2000}
    }

    -- CREATE A LOOKUP INDEX FOR from YEAR
    pres_from_idx = {}
    for k, v in ipairs(presidents) do
    pres_from_idx[v.from] = k
    end


    -- PRINT EVERYTHING FOR THE GUY WHO SERVED FROM 1993
    print("==== FOLLOWING IS INFO FOR PRESIDENT STARTING IN 1993 ====")
    prez = presidents[pres_from_idx[1993]]
    for k, v in pairs(prez) do
    print(sf(" %s=%s",ts(k), ts(v)))
    end

    -- MAKE INTEGER KEY TABLE OF FROM YEARS
    year_array = {}
    for k, v in pairs(pres_from_idx) do
    table.insert(year_array, k)
    end

    -- SORT THE INTEGER KEY TABLE
    table.sort(year_array, function(a,b) return a < b end)

    -- PRINT PRESIDENTS IN CHRONOLOGICAL ORDER
    print("\n==== FOLLOWING IS CHRONOLOGICAL LISTING OF PRESIDENTS ====")
    for k, v in ipairs(year_array) do
    yearstarted = v
    recno = pres_from_idx[yearstarted]
    prez = presidents[recno]
    print(sf(" %s %s served from %s to %s",
    prez.fname, prez.lname, ts(prez.from), ts(prez.to)))
    end
    The preceding code outputs the following:
    slitt@mydesk:~$ test.lua
    ==== FOLLOWING IS INFO FOR PRESIDENT STARTING IN 1993 ====
    fname=Bill
    to=2000
    lname=Clinton
    from=1993

    ==== FOLLOWING IS CHRONOLOGICAL LISTING OF PRESIDENTS ====
    George HW Bush served from 1989 to 1992
    Bill Clinton served from 1993 to 2000
    George W Bush served from 2001 to 2008
    Barack Obama served from 2009 to nil
    slitt@mydesk:~$
    Of course, when there is a possibility of duplicates, you must make different arrangements. For instance, perhaps each key in the index will have a value that is an array-like table of all the record numbers in the original table that have that key. Or, perhaps you can have an array like index table, with each element being a table with a key and a record number, and then sort that array like index table. These possibilities will remain as exercises for you. The point is, any time you need quick lookup on a table, especially a large one, you can create a second table that serves as an index to the first. And one way or another, you can build another table sorted on a specific information piece from the first table.


     [ Troubleshooters.com| Code Corner | Email Steve Litt ]

    Copyright (C) 2011 by Steve Litt --Legal