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