Answers - Beginning Lua Programming (2007)

Beginning Lua Programming (2007)

Appendix. Answers

Chapter 2

Exercise 1 Solution

Because the “p” in print needs to be lowercase.

Exercise 2 Solution

"ABC123"

Exercise 3 Solution

Because it reduces to the following, and the attempt to concatenate a Boolean value will trigger an error:

false .. 123

Exercise 4 Solution

One (the first one). More than one branch of an if is never executed. The first branch when a true test expression is executed, and after that, the if is exited.

Exercise 5 Solution

for N = 10, 2, -2 do

print(N)

end

Chapter 3

Exercise 1 Solution

function TypedToString(Val)

return type(Val) .. ": " .. tostring(Val)

end

Exercise 2 Solution

function SumProd(A, B)

return A + B, A * B

end

Exercise 3 Solution

It will print the following:

6 10 25

(SumProd(3, 3) returns 6, 9, which is adjusted to one value. SumProd(5, 5) returns 10, 25, and both values are used.

Exercise 4 Solution

It prints North America. (The Continent inside F is local—assigning to it has no effect on the global Continent.)

Exercise 5 Solution

For the functions returned to be closures (and thus have private state), Dots needs to be local. Because it's global, all the functions returned share the same state, so they all do the same thing. The fix is simply this:

function MakeDotter(N)

local Dots = ""

for I = 1, N do

Dots = Dots .. "."

end

return function(Str)

return Str .. Dots

end

end

As mentioned earlier, forgetting to make a variable local is a common source of bugs.

Chapter 4

Exercise 1 Solution

C

To figure this sort of thing out, work from left to right: figure out what D is, then what D.C is, and then what D.C["B"] is.

Exercise 2 Solution

The main trick to this is to compare things other than numbers and strings by their tostring value, as follows:

CompAll = function(A, B)

if type(A)~= type(B) then

-- If they're of different types, sort them by type:

A, B = type(A), type(B)

elseif type(A) ~= "number" and type(A) ~= "string" then

-- If they're something other than numbers or strings,

-- sort them by their tostring representation:

A, B = tostring(A), tostring(B)

end

return A < B

end

Exercise 3 Solution

table.concat does most of the hard work:

-- "String print" -- returns a string of the form output by

-- print:

function Sprint(... )

local Args = {...}

-- Using select and a numeric for makes sure that no nils

-- are missed:

local ArgCount = select("#", ...)

for I = 1, ArgCount do

Args[I] = tostring(Args[I])

end

return table.concat(Args, "\t") .. "\n"

end

Exercise 4 Solution

-- Rotates self N elements to the left:

function Methods:RotateL(N)

N = N or 1

if #self > 0 then

self.Pos = OneMod(self.Pos + N, #self)

end

end

Exercise 5 Solution

Here's one way to do it:

function SortedPairs(Tbl)

local Sorted = {} -- A (soon to be) sorted array of Tbl's keys.

for Key in pairs(Tbl) do

Sorted[#Sorted + 1] = Key -- Same as table.insert.

end

table.sort(Sorted, CompAll)

local I = 0

-- The iterator itself:

return function()

I = I + 1

local Key = Sorted[I]

return Key, Tbl[Key]

end

end

Chapter 5

Exercise 1 Solution

-- Returns an array of Str's characters:

function StrToArr(Str)

local Ret = {}

for I = 1, #Str do

Ret[I] = string.sub(Str, I, I)

end

return Ret

end

Exercise 2 Solution

Frmt = "%6s\n"

Exercise 3 Solution

-- Does a "dictionary order" less-than comparison:

function DictCmp(A, B)

-- "Fold" each argument by deleting all non-alphanumeric

-- characters then lowercasing it:

local FoldedA = string.lower(string.gsub(A, "%W+", ""))

local FoldedB = string.lower(string.gsub(B, "%W+", ""))

-- Only compare the folded versions if they're unequal:

if FoldedA ~= FoldedB then

return FoldedA < FoldedB

else

return A < B

end

end

Exercise 4 Solution

-- Reads lines, evaluates them as expressions, prints the

-- results:

function ExprInterp()

io.write("expression> ")

local Line = io.read()

while Line and Line ~= "quit" do

-- Convert the expression to a statement, convert the

-- statement to a function, call the function, and print

-- its results:

Line = "return " .. Line

local Fnc = loadstring(Line) -- Missing error check.

print(Fnc()) -- Missing error check.

io.write("expression> ")

Line = io.read()

end

end

It could also be written with io.lines, like this:

function ExprInterp()

io.write("expression> ")

for Line in io.lines() do

if Line == "quit" then

break -- QUIT.

end

-- Convert the expression to a statement, convert the

-- statement to a function, call the function, and print

-- its results:

Line = "return " .. Line

local Fnc = loadstring(Line) -- Missing error check.

print(Fnc()) -- Missing error check.

io.write("expression> ")

end

end

Exercise 5 Solution

Here's one way to do it:

-- Trims leading whitespace:

function TrimLeft(Str)

return (string.gsub(Str, "^%s+", ""))

end

Exercise 6 Solution

No. If there's whitespace starting at the subject's first character, it deletes it all and searches no more. If there's no whitespace there, it does nothing and searches no more.

Exercise 7 Solution

-- Turns $XYZ into the value of the global variable XYZ (XYZ

-- is any identifier):

function Interpolate(Str)

return (string.gsub(Str, "%$([%a_][%w_]*)",

function(Ident)

return tostring(getfenv()[Ident])

end))

end

Chapter 6

Exercise 1 Solution

The debug.traceback function is called (by Lua) while setting up the arguments to print, which is before the print function is actually called.

Exercise 2 Solution

Here is one way:

debug._traceback = debug.traceback

debug.traceback = function(Str)

return debug._traceback('Programmer error: ' .. Str)

end

Exercise 3 Solution

local function zpcall(Fnc, Err, ...)

local Args = {...}

local function Relay()

return Fnc(unpack(Args))

end

return xpcall(Relay, Err)

end

-- Test

local function Fnc(A, B, C)

print("Here in Fnc", A, B, C)

print(A + B + C)

end

print(zpcall(Fnc, debug.traceback, 1, nil, 3))

Chapter 7

Exercise 1 Solution

Here's one solution. Add the following line near the top of show.lua:

local Object = Object or {}

This assigns the local variable Object to a global value of that name if it exists; otherwise this assigns the variable to a newly created empty table.

Find this function header:

function ObjectDescribe(Tbl, Val, Key, TruncLen)

and change it to this:

function Object.Describe(Tbl, Val, Key, TruncLen)

Correspondingly, change ObjectShow to Object.Show, and ObjectDescribe to Object.Describe.

Finally, add the following to the bottom of the module:

return Object

This new version of show.lua can be used as follows:

local Obj = require("show")

Obj.Show(_G, "_G")

Exercise 2 Solution

The following error message is displayed:

lua: nomod.lua:1: cannot find module xyz

Lua first examines "?.lua", replacing "?" with "xyz". It doesn't find xyz.lua, so it then examines "nomod.lua", replacing all occurrences of "?" with "xyz". This results in "nomod.lua" (there are no "?" characters to replace), so it looks for nomod.lua, finds it, loads it, and runs it, resulting in the error message.

Note that Lua already has a superior mechanism to report an unfound module. The message it generates lists all of the filenames it attempted to locate before failing.

Chapter 8

Exercise 1 Solution

Meta = {

-- Returns a reverse-order copy of an array:

__unm = function(Arr)

local Ret = {}

for I = #Arr, 1, -1 do

Ret[#Ret + 1] = Arr[I]

end

return Ret

end}

Exercise 2 Solution

Makes Tbl read-only (and returns it):

function Protect(Tbl)

-- Tbl will be used as a proxy table; transfer all its

-- pairs to OrigTbl:

local OrigTbl = {}

for Key, Val in pairs(Tbl) do

OrigTbl[Key] = Val

Tbl[Key] = nil -- Clear the soon-to-be proxy.

end

-- __newindex prevents writes to the table; __index allows

-- reads from the (original) table; __metatable prevents

-- metatable trickery:

local ProtectMeta = {

__newindex = function()

error("attempt to assign to a protected table")

end,

__index = OrigTbl,

__metatable = true}

-- As well as setting Tbl's metatable, return it (for

-- convenience):

return setmetatable(Tbl, ProtectMeta)

end

These techniques can be used to rewrite ring.lua (from Chapter 4) to protect the object returned by MakeRing from tampering.

Chapter 9

Exercise 1 Solution

The script prints this:

0

1

1

2

3

5

8

13

These are the first eight numbers in the number series named after Leonardo Fibonacci, the medieval Italian mathematician. Beginning with 0, 1, each value is the sum of the two before it.

Exercise 2 Solution

This is one solution:

function JoinPairs(AList, BList)

local function Iter()

local Pos = 1

local A, B = AList[Pos], BList[Pos]

while A ~= nil and B ~= nil do

coroutine.yield(A, B)

Pos = Pos + 1

A, B = AList[Pos], BList[Pos]

end

end

return coroutine.wrap(Iter)

end

Notice that the iterator is generic in the sense that it doesn't care what value or type each array element is.

Exercise 3 Solution

Here is one solution:

local function LclConsumer(Evt)

print("Consumer: initialize")

while Evt ~= "end" do

print("Consumer: " .. Evt)

Evt = coroutine.yield()

end

print("Consumer: finalize")

end

local function LclProducer()

print("Producer: initialize")

local PutEvent = coroutine.wrap(LclConsumer)

-- Simulate event generation

local List = {"mouse", "keyboard", "keyboard", "mouse"}

for J, Val in ipairs(List) do

local Evt = string.format("Event %d (%s)", J, Val)

print("Producer: " .. Evt)

PutEvent(Evt)

end

PutEvent("end")

print("Producer: finalize")

end

LclProducer()

Notice how little the overall structure of the program is changed from the original.

Chapter 10

Exercise 1 Solution

Somehow, the character "\27" (an ASCII escape character) has been left at the beginning of the file, causing Lua to try (and fail) to treat it as bytecode.

Exercise 2 Solution

This function boils down to: See if it's a local; if not, then see if it's an upvalue; if not, then see if it's a global. It's getting all the details right that's hard. Here they are:

-- Gets the value of the named variable from its calling

-- scope:

function GetVar(Name)

local CallerInfo = debug.getinfo(2, "uf")

local Caller, Nups = CallerInfo.func, CallerInfo.nups

local Found, FoundVal

-- Look at the variables local to the caller; the last hit

-- (if any) will be the desired one:

local LclIndex = 1

local Loop = true

while Loop do

local MaybeName, MaybeVal = debug.getlocal(2, LclIndex)

if MaybeName then

if MaybeName == Name then

-- Found one hit (of possibly several):

Found, FoundVal = true, MaybeVal

end

LclIndex = LclIndex + 1

else

-- No more locals; break the loop:

Loop = false

end

end

-- If it wasn't found as a local, search the caller's

-- upvalues:

if not Found then

for UpIndex = 1, Nups do

local MaybeName, MaybeVal = debug.getupvalue(Caller,

UpIndex)

if MaybeName == Name then

-- FOUND IT! -- RECORD THE VALUE AND BREAK THE LOOP:

Found, FoundVal = true, MaybeVal

break

end

end

end

-- If it wasn't found as either a local or an upvalue, get

-- it as a global (from the caller's environment):

if not Found then

FoundVal = getfenv(2)[Name]

end

return FoundVal

end

Chapter 12

Exercise 1 Solution

The following script is one solution.

require "sqlite3"

local Cn = {}

Cn.DbStr = "test.db"

Cn.InitStr = [[

BEGIN TRANSACTION;

DROP TABLE IF EXISTS T1;

CREATE TABLE T1(A, B, C);

INSERT INTO T1 VALUES (12, 91, 40);

INSERT INTO T1 VALUES (27, 79, 5);

INSERT INTO T1 VALUES (32, 66, 53);

END TRANSACTION;

]]

local DbHnd, ErrStr = sqlite3.open(Cn.DbStr)

if DbHnd then

if DbHnd:exec(Cn.InitStr) then

DbHnd:set_function("format", -1, string.format)

print(DbHnd:first_cols("SELECT FORMAT('%05d %05d', 12, 34)"))

for Row in DbHnd:rows("SELECT FORMAT('A %05d, B %05d, C %05d', A, B, C) " ..

"AS Rec FROM T1") do

print(Row.Rec)

end

else

io.write("Error initializing ", Cn.DbStr, "\n")

end

DbHnd:close()

else

io.write("Error opening ", Cn.DbStr, ": ", tostring(ErrStr), "\n")

end

This simply registers string.format as a SQLite multivalue query function. The -1 value indicates a variable number of arguments. The string.format function meets the criteria for a lua-sqlite3 user function, so it didn't need to be wrapped in a user-written function.

Exercise 2 Solution

Copy the following script to a file named pngchunk.lua:

require "pack"

local PngFileStr = arg[1] or "logo.png"

local function LclFileGet(FileStr)

local Str

local Hnd, ErrStr = io.open(FileStr, "rb")

if Hnd then

Str = Hnd:read("*all")

Hnd:close()

end

return Str, ErrStr

end

local function printf(FrmtStr, ... )

io.write(string.format(FrmtStr, ...))

end

local PngStr, ErrStr = LclFileGet(PngFileStr)

if PngStr then

local PngSize = string.len(PngStr)

local DataLen, DataStr, HdrStr, Crc

-- Documented PNG signature

local SignatureStr = string.char(137, 80, 78, 71, 13, 10, 26, 10)

local Pos = string.len(SignatureStr)

if SignatureStr == string.sub(PngStr, 1, Pos) then

Pos = Pos + 1

printf("Header Length CRC\n")

printf("------------ ----------\n")

while Pos and Pos < PngSize do

Pos, DataLen, HdrStr = string.unpack(PngStr, ">LA4", Pos)

if Pos then

-- print("unpack_1", Pos, DataLen, HdrStr)

if DataLen > 0 then

Pos, DataStr, Crc = string.unpack(PngStr, ">A" .. DataLen .. "L", Pos)

else

Pos, Crc = string.unpack(PngStr, ">L", Pos)

DataStr = ""

end

-- print("unpack_2", Frmt, Pos, string.len(DataStr), Crc)

if Pos then

printf("%s %8u 0x%08X\n", HdrStr, DataLen, Crc)

if HdrStr == "IEND" then

Pos = nil -- End of loop

end

end

end

end

else

io.write("Error: ", PngFileStr, " does not have expected signature\n")

end

else

io.write("Error: ", ErrStr, "\n")

end

Pass the name of the PNG file as an argument to the script. For example, if the PNG file you want to summarize is named chart.png, run the script as follows:

lua pngchunk.lua chart.png

Note that this script actually reads each chunk's data field. To do this, it uses the A specifier concatenated with the data length. It implements a special case for zero-length data fields. An alternative method is to read the data length and chunk header fields in the first call to string.unpack, adjust the starting position value based on the data length, and call string.unpack again to retrieve the CRC field.

Chapter 13

Exercise 1 Solution

In the luaopen_ud_example function in ud_example.c, copy the following lines from the MetaMap array and place them unmodified into the Map array:

{"close", LclExampleClose},

{"get", LclExampleGet},

{"set", LclExampleSet},

Exercise 2 Solution

Here is the code with possible comments:

// Stk: ...

lua_newtable(L);

// Stk: ... Namespace

lua_newtable(L);

// Stk: ... Namespace Upvalue

lua_pushstring(L, "Rip Van Winkle");

// Stk: ... Namespace Upvalue NameStr

lua_setfield(L, -2, "Name");

// Stk: ... Namespace Upvalue

lua_pushvalue(L, -1);

// Stk: ... Namespace Upvalue Upvalue

lua_pushcclosure(L, FncA, 1);

// Stk: ... Namespace Upvalue FncA

lua_setfield(L, -3, "a");

// Stk: ... Namespace Upvalue

lua_pushcclosure(L, FncB, 1);

// Stk: ... Namespace FncB

lua_setfield(L, -2, "b");

// Stk: ... Namespace

lua_pushvalue(L, -1);

// Stk: ... Namespace Namespace

lua_setglobal(L, "test");

// Stk: ... Namespace

The code creates a global namespace table called test that includes two functions: "a" and "b". Each of these functions has one upvalue: a table that is accessible only to the two functions. Inside this table is the string "Rip Van Winkle" keyed by the string "Name". The namespace table is left on the stack as a possible return value.

Exercise 3 Solution

Here is one possible solution:

#include "lua.h"

#include "lualib.h"

#include "lauxlib.h"

/* * */

static int BitOr(

lua_State *L)

{ // BitOr

int Val, J, Top;

Top = lua_gettop(L);

Val = 0;

for (J = 1; J <= Top; J++) Val |= luaL_checkinteger(L, J);

lua_pushinteger(L, Val);

return 1;

} // BitOr

/* * */

static int BitAnd(

lua_State *L)

{ // BitAnd

int Val, J, Top;

Top = lua_gettop(L);

Val = Top > 0 ? -1 : 0;

for (J = 1; J <= Top; J++) Val &= luaL_checkinteger(L, J);

lua_pushinteger(L, Val);

return 1;

} // BitAnd

/* * */

int luaopen_bit(

lua_State *L)

{ // luaopen_bit

static const luaL_reg Map[] = {

{"_and", BitAnd},

{"and", BitAnd},

{"_or", BitOr},

{"or", BitOr},

{NULL, NULL}

};

luaL_register(L, "bit", Map);

return 1;

} // luaopen_bit

Chapter 14

Exercise 1 Solution

-- Converts a cursor to an array of associative tables:

function CursorToTbl(Cursor)

local Ret = {}

-- A wrapper for Cursor:fetch that lets it return a table

-- without being given one:

local function RowsIter()

local Row = {}

return Cursor:fetch(Row, "a") -- alphanumeric/associative

end

-- Get each row:

for Row in RowsIter do

table.insert(Ret, Row)

end

Cursor:close()

return Ret

end

Exercise 2 Solution

All three tables have an Id column. The database system doesn't know which one you want, so it returns the error message Column 'Id'-in-field list-is-ambiguous. You can fix this by specifying the following table:

SELECT Ord.Id, CustId, NameLast, ProductId, DescStr, Count

FROM Cust, Ord, Product

WHERE Cust.Id = Ord.CustId

AND Ord.ProductId = Product.Id

The column will still be called "Id" (not "Ord.Id") in the result. SQL offers a way to make it appear under another name—the AS keyword:

SELECT Ord.Id AS OrdId, CustId, NameLast, ProductId, DescStr, Count

FROM Cust, Ord, Product

WHERE Cust.Id = Ord.CustId

AND Ord.ProductId = Product.Id

That gives the following result:

OrdId CustId NameLast ProductId DescStr Count

----- ------ -------- --------- --------------- -----

6 2 Finn 4 thingamajig 2

1 3 Darcy 2 gizmo 52

2 3 Darcy 4 thingamajig 87

3 4 Bennet 1 whatchamacallit 12

4 4 Bennet 3 gewgaw 8

5 4 Bennet 5 widget 20

Chapter 15

Exercise 1 Solution

Save the following file as ex1.lua in your web server's cgi-bin directory:

#!/usr/local/bin/lua

local Cgi = require("cgi")

local Cn = {}

Cn.HdrStr = [[Content-Type: text/html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html>

<head>

<title>Greeting</title>

<link rel="stylesheet" href="/css/general.css" type="text/css" />

</head>

<body>

]]

Cn.FtrStr = "</body>\n\n</html>"

local Rec = os.date("*t")

local Hr = Rec.hour

local Str

if Hr >= 3 then

if Hr >= 12 then

if Hr >= 18 then

Str = "Good evening"

else

Str = "Good afternoon"

end

else

Str = "Good morning"

end

else

Str = "Good night"

end

local Prm = Cgi.Params()

Str = Str .. ", " .. (Prm.Get.Name or "Unknown user") .. "!"

io.write(Cn.HdrStr, '<p class="hdr shade">', Cgi.Escape(Str), '</p>', Cn.FtrStr)

Exercise 2 Solution

This solution calls the CGI script from the previous exercise using the GET method. Save the following file as ex2.html in your web server's document root directory:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html>

<head>

<title>Greeting Form</title>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

<link rel="stylesheet" href="/css/general.css" type="text/css" />

</head>

<body>

<h1>Greeting Form</h1>

<form action="/cgi-bin/ex1.lua" method="get">

<table class="shade" summary=""><tbody>

<tr>

<td class="label">Name</td>

<td><input type="text" size="20" name="Name" /></td>

<td><input type="submit" value="Submit" /></td>

</tr>

</tbody>

</table>

</form>

</body>

</html>

Exercise 3 Solution

Here is one solution. It uses one CGI script handle both the request form and the calendar presentation. Save the following file as ex3.lua in your web server's cgi-bin directory:

#!/usr/local/bin/lua

local Cgi = require("cgi")

local Cn = {}

Cn.HdrStr = [[Content-Type: text/html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html>

<head>

<title>Calendar</title>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

<link rel="stylesheet" href="/css/general.css" type="text/css" />

<style type="text/css">

table.data td {border-right : 1px solid; border-bottom : 1px solid;

border-color:inherit; color:#404040; background-color:#FFFFFF;

vertical-align:top; width:14%;}

table.data td.today {background-color:#f0f0ff;}

table.data td.name {font-weight:bold; text-align:center; color:#505060;

background-color:#f0f0ff;}

table.data td.edge {width:15%;}

</style>

</head>

<body>

]]

Cn.FtrStr = "</body>\n\n</html>"

local function CalendarShow(Year, Month)

local DaysInMonth = { 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }

-- For the specified month and year, this function returns the day of week of

-- the first day in the month, and the number of days in the month.

local function Calendar(Year, Month)

assert(Year > 1900 and Year < 2100, "Year out of range")

assert(Month >= 1 and Month <= 12, "Month out of range")

local Rec = os.date("*t", os.time{year = Year, month = Month, day = 1})

local DayMax = DaysInMonth[Rec.month]

if DayMax == 0 then

DayMax = math.mod(Rec.year, 4) == 0 and 29 or 28

end

return Rec.wday, DayMax

end

local Dow, DayMax = Calendar(Year, Month)

io.write(Cn.HdrStr)

io.write('<table class="data shade" border="0" cellspacing="0" ',

'cellpadding="5" summary="" width="100%">\n',

'<tbody>\n',

'<tr><th colspan="7">', os.date('%B %Y', os.time{year = Year,

month = Month, day = 1}), '</th></tr>\n',

'<tr>\n')

local Base = 8 - Dow

for Col = 1, 7 do

local Wd = (Col == 1 or Col == 7) and ' edge1 or ''

io.write('<td class="name', Wd, '">', os.date("%a",

os.time{year = Year, month = Month, day = Base + Col}),

'</td>\n')

end

io.write('</tr>\n')

local Day = 2 - Dow

while Day <= DayMax do

io.write('<tr>')

for Col = 1, 7 do

io.write('<td>', (Day >= 1 and Day <= DayMax) and Day or ' ',

'</td>\n')

Day = Day + 1

end

io.write("</tr>\n")

end

io.write('</tbody></table>\n')

io.write(Cn.FtrStr)

end

local function CalendarRequest(ScriptStr)

io.write(Cn.HdrStr, '<h1>Calendar Request</h1>\n',

'<form action="', ScriptStr, '" method="get">\n',

'<table class="shade" summary=""><tbody>\n',

'<tr>\n',

'<td class="label">Month</td>\n',

'<td>\n',

'<select name="Month" size="1">\n')

local Today = os.date("*t")

for J = 1, 12 do

local AttrStr = Today.month == J and ' selected="selected"' or ""

local MonthStr = os.date("%B", os.time{year=2000, month=J, day = 1})

io.write('<option', AttrStr, ' value="', J, '">', MonthStr, '</option>\n')

end

io.write('</select>\n',

'</td>\n',

'<td class="label">Year</td>\n',

'<td>\n',

'<select name="Year" size="1">\n')

for J = Today.year - 12, Today.year + 12 do

local AttrStr = Today.year == J and ' selected="selected"' or ""

io.write('<option', AttrStr, ' value="', J, '">', J, '</option>\n')

end

io.write('</select>\n',

'</td>\n',

'<td><input type="submit" value="Submit" /></td>\n',

'</tr>\n',

'</tbody></table>\n',

'</form>\n',

Cn.FtrStr)

end

-- If UserMonth and UserYear were passed in and are valid, present the

-- requested calendar, otherwise present a request form.

local Prm = Cgi.Params()

local UserMonth, UserYear

local ShowForm = true

UserMonth = tonumber(Prm.Get.Month)

if UserMonth and UserMonth >= 1 and UserMonth <= 12 then

UserYear = tonumber(Prm.Get.Year)

if UserYear and UserYear >= 1970 and UserYear < 2038 then

ShowForm = false

end

end

if ShowForm then

if Prm.SCRIPT_NAME then

CalendarRequest(Prm.SCRIPT_NAME)

else

io.write(Cn.HdrStr, "<p>Error: could not determine name of script.</p>\n",

Cn.FtrStr)

end

else

CalendarShow(UserYear, UserMonth)

end

Chapter 16

Exercise 1 Solution

The following is one of many possible solutions:

local ltn12 = require("ltn12")

local function Transform(Str, ... )

-- Source is specified string

local Src = ltn12.source.string(Str)

-- Chain all specified filters into one

local Filter = ltn12.filter.chain(...)

-- Send all data chunks to table

local Snk, Tbl = ltn12.sink.table()

-- Filter chunks before delivering to sink

Snk = ltn12.sink.chain(Filter, Snk)

-- Open the valve

local Code, ErrStr = ltn12.pump.all(Src, Snk)

return Code and table.concat(Tbl) or nil, ErrStr

end

-- Return the rot13 cipher of the character Ch

local function Rot13Char(Ch)

local Val = string.byte(Ch)

return string.char(Val < 65 and Val or

Val < 78 and Val + 13 or

Val < 91 and Val - 13 or

Val < 97 and Val or

Val < 110 and Val + 13 or

Val < 123 and Val - 13 or Val)

end

-- Filter that returns a rot13 cipher of the string Str

local function Rot13(Str)

return Str and string.gsub(Str, "(%a)", Rot13Char)

end

local Str = "The quick brown fox jumps over the lazy dog"

local CnvStr = Transform(Str, Rot13)

io.write(Str, "\n", CnvStr, "\n",

Str == Transform(CnvStr, Rot13) and "OK" or "Not OK", "\n",

Str == Transform(Str, Rot13, Rot13) and "OK" or "Not OK", "\n")

Exercise 2 Solution

The following is a possible solution:

local function LclBodyPrint(PopHnd, Pos)

local Str, Tbl

Str, Tbl = LclTransact(PopHnd, "retr " .. Pos, true)

if Str then

io.write(Str, "\n")

local Show

for K, Str in ipairs(Tbl) do

if Show then

io.write(Str, "\n")

elseif Str == "" then

Show = true

end

end

else

io.write('Error: ', Tbl, "\n") -- Tbl is error message

end

end

Exercise 3 Solution

The following script sends three lines, displays the echoed response, and sends an empty string to terminate the session:

local socket = require("socket")

local Cn = {SrvPort = 23032, SrvAddr = "localhost"}

local SckHnd = socket.connect(Cn.SrvAddr, Cn.SrvPort)

if SckHnd then

local RcvStr, ErrStr = SckHnd:receive()

if RcvStr then

io.write(RcvStr, "\n")

local SendList = {"One", "Two", "Three", ""}

for J, Str in ipairs(SendList) do

SckHnd:send(Str .. "\r\n")

local RcvStr, ErrStr = SckHnd:receive()

if RcvStr then

io.write(RcvStr, "\n")

elseif ErrStr ~= "closed" then

io.write("Error: ", ErrStr, "\n")

end

end

else

io.write("Error: ", ErrStr, "\n")

end

SckHnd:close()

else

io.write("Error creating client socket\n")

end

Chapter 17

Exercise Solution

It displays a 4×4 grid of goodies and baddies in random order.

Chapter 18

Exercise 1 Solution

Here is one solution:

-- star.lua

screen.clear()

local Wd, Ht = screen.mode()

gui.title("Star")

local X, Y = screen.pos()

screen.moveto(Wd / 2, Y + Ht / 10)

screen.heading(252)

for J = 1, 5 do

screen.walk(2 * Wd / 3)

screen.turn(144)

end

gui.event()

Exercise 2 Solution

A possible solution is as follows:

-- lua_expr.lua

screen.clear()

gui.title("Lua expression")

local X, Y

local Wd, Ht = screen.mode()

gui.label("Lua expression")

gui.nl()

local CtlExpr = gui.control{type = "field", lines = 1, columns = 24,

limit = 256, editable = true, underlined = true}

local CtlGo = gui.control{type = "button", text = "Go"}

gui.nl()

gui.label("Result")

gui.nl()

local CtlResult = gui.control{type = "field", lines = 1, columns = 28,

limit = 256, editable = false, underlined = false}

gui.setfocus(CtlExpr)

local Loop = true

while Loop do

local Evt, Id = gui.event()

if Evt == appStop then

Loop = false

elseif Evt == ctlSelect then

if Id == CtlGo then

local Fnc

local Str = gui.gettext(CtlExpr)

if string.len(Str) > 0 then

Fnc, Str = loadstring("return tostring(" .. Str .. ")")

if Fnc then

local Status

Status, Str = pcall(Fnc)

end

gui.settext(CtlResult, tostring(Str))

end

end

end

end

Exercise 3 Solution

Here is a solution:

-- barchart.lua

function Barchart(TitleStr, Tbl)

local Count = table.getn(Tbl) Lua 5.0

if Count > 0 then

local ScrWd, ScrHt, ScrDepth = screen.mode()

local ScrBase = ScrWd / 160

screen.clear()

gui.title(TitleStr)

local X, Top = screen.pos()

local BarWd, GapHrz, GapVrt = 3, 3, 4 * ScrBase

if Count * (BarWd + GapHrz) - GapHrz <= ScrWd then

BarWd = math.floor((ScrWd + GapHrz - (Count * GapHrz)) / Count)

local Lf = math.floor((ScrWd + GapHrz - Count * (BarWd + GapHrz)) / 2)

local Max = Tbl[1]

for J, Val in ipairs(Tbl) do

if Val > Max then

Max = Val

end

end

if Max > 0 then

local Mlt = (ScrHt - Top - GapVrt - GapVrt) / Max

local Clr = screen.rgb(128, 128, 128)

X = Lf

for J, Val in ipairs(Tbl) do

local Ht = math.floor(Val * Mlt)

screen.box(X, ScrHt - GapVrt - Ht, BarWd, Ht, Clr)

X = X + BarWd + GapHrz

end

gui.event()

else

gui.alert("Values must be positive")

end

else

gui.alert("Too many elements")

end

else

gui.alert("Table is empty")

end

end

Barchart("Example", {12, 34, 23, 25, 67, 45, 23, 7, 5, 54, 48, 50})

Nice extensions to this routine would include the following:

· Bar colors

· Interactive use, in which tapping a bar would result in detail information for the corresponding value

· A scale