--#!/home/richard/bin/lua -i
--
-- (c) 2006 Richard Simes
-- You may distribute this program under the terms of the
-- GNU General Public License. See COPYING for details.
--
-- should really configure this from Makefile
--package.path="./?.lua;./?.lc;/usr/share/luacalc/?.lua"
luacalc={
-- functions for use in sheet cells
functions = {},
-- the actual sheet with values.
sheet = {},
dependant_cells = {},
-- Different views/masks
html_view = {}, -- TODO
display_view = {},
csv_view = {},
selection = {},
selections = {}, -- standard place to store selections
inputmode="normal", -- the input mode
pastedata={},
-- whether the file has been changed or not.
dirty = false,
-- input cell locaton
x = 1,
y = 1,
-- macro
macro_running = false,
-- version number
version="0.7.0"
}
require"stringaux"
require"lcstack"
-- import luacalc cell functions
require"lcfunctions"
-- plotting
require"plot"
-- no operation
function nop(...) end
debug = print
--[[
debug = nop
--]]
luacalc.record_access = nop
luacalc.super_cell = {x = nil, y=nil}
-- Sheet or view iterator
-- unfortunately ipairs() doesn't work
-- for views as they don't contain any data.
-- and a get-and-check iterator won't work either
-- as we are using semi auto-magic/self expanding tables.
function luacalc.foreach(tab, func, eolfunc)
for x=1, #luacalc.sheet do
for y=1, #luacalc.sheet[x] do
func(tab[x][y], x , y)
end
if eolfunc then
eolfunc(x)
end
end
end
function luacalc.debug_table(t, indent)
indent = indent or ""
for k, v in next, t do
print(indent..k, v)
if type(v) == "table" then
luacalc.debug_table(v, indent.."\t")
end
end
end
-- Sheet or view iterator.
-- alternate ordering to luacalc.foreach
function luacalc.forall(tab, func, eolfunc)
for y = 1, #luacalc.sheet do
for x = 1, #luacalc.sheet[1] do
func(tab[x][y], x, y)
end
if eolfunc then
eolfunc(x)
end
end
end
-- Convert a string such as A, AB... to an index.
-- doesn't do any error checking - use with caution
function luacalc.string_to_index(s)
if string.len(s) == 1 then
return string.byte(s, 1)-string.byte("A", 1) + 1
else
return luacalc.string_to_index(string.sub(s, 1, 1))*26 + luacalc.string_to_index(string.sub(s, 2))
end
end
--WARNING: only valid up to Z (until i need it higher...
function luacalc.index_to_string(i)
print"WARNING: only valid up to Z (until i need it higher...)"
local res = ""
res = res..string.char(string.byte"A"+i-1)
return res
end
--
function luacalc.add_super_cell(i, j, cell)
debug('adding super cell of (', cell.x, cell.y, ') to (', i, j, ')')
luacalc.dependant_cells[i][j] = luacalc.dependant_cells[i][j] or {sub_cells = {}, super_cells = {}}
table.insert(luacalc.dependant_cells[i][j].super_cells, cell)
end
function luacalc.calculate_sub_cells(i, j)
debug("calculating sub+super cells for "..i.." : "..j)
luacalc.super_cell = {x=i, y=j}
local sub_cells = {}
luacalc.record_access = function(x, y)
debug("access ", x, y)
if not (x==i and y==j) then
table.insert(sub_cells, {x=x, y=y})
luacalc.add_super_cell(x, y, luacalc.super_cell)
end
end
-- access cell (i, j)
get(j, i)
-- stop recording access
luacalc.record_access = nop
end
-- returns the value s unless it is a function:
-- if s is a string, starting with '='
-- the string will be loaded as a lua function
-- and run with the result returned.
function luacalc.raw_value(s, x, y)
luacalc.record_access(x, y)
if type(s)=="string" and string.sub(s, 1, 1) == "=" then
-- deal with ':' here. need to parse for text outside of quotes
local form = luacalc.parse_formula(s, x, y)
--print(s)
--
local f, err = loadstring("return "..string.sub(form, 2))
if err then
luacalc.sheet[x][y]=("'"..s )
return ("'"..s)
end
luacalc.sheet[x][y] = {text=s, func=f}
return f()
elseif type(s)=="table" then
return s.func()
elseif type(s)=="boolean" then
return IF(s, "true", "false")
else
return s
end
end
--
-- parse the command for the range operator, ':' and replace with a
-- call returning a table with all the values in the selection
-- also replaces $x and $y with the x, y cell reference
function luacalc.parse_formula(str, x, y)
if string.len(str) == 0 then return str end
-- find quotes
local start, finish, quoted = string.find(str, '(".-")')
if start and finish then
-- parse parts before and after the quoted string
local bef = string.sub(str, 1, start-1)
local aft = string.sub(str, finish+1, string.len(str))
return luacalc.parse_formula(bef, x, y)..quoted..luacalc.parse_formula(aft, x, y)
end
-- coordinate substitution
str = string.gsub(str, "$x", x)
str = string.gsub(str, "$y", y)
-- deal with colons - no quoted strings should be here
start, finish =string.find(str, "%u+%d+[:]%u+%d+")
if start and finish then
local colon = string.find(str, ":")
local x1,y1 = luacalc.cellindex(string.sub(str, start, colon-1))
local x2,y2 = luacalc.cellindex(string.sub(str, colon+1, finish))
local res = string.sub(str, 1, start-1)
res = res.."luacalc.getselection{"..x1..","..y1..","..x2..","..y2.."}"
res = res..string.sub(str, finish+1, string.len(str))
return res
end
-- String contains no colon, so just return it.
return str
end
--
-- returns the (x, y) index of a cell given a string of the form A1
--
function luacalc.cellindex(str)
local letter=""
local number=""
string.foreach(str, function(ind, char)
-- surely there is a more efficient check for capitals?
if string.byte(char) >= string.byte"A" and string.byte(char) <= string.byte"Z" then
letter=letter..char
else
number=number..char
end
end)
local rowind = luacalc.string_to_index(letter)
local ind = tonumber(number)
return rowind, ind
end
function luacalc.getselection(tab)
local x1 = tab.x1 or tab[1]
local y1 = tab.y1 or tab[2]
local x2 = tab.x2 or tab[3]
local y2 = tab.y2 or tab[4]
local res = {}
if x1 == x2 then
for i=y1, y2 do
table.insert(res,luacalc.display_view[x1][i])
end
elseif y1==y2 then
for i=x1, x2 do
table.insert(res,luacalc.display_view[i][y1])
end
else
for j = x1, x2 do
local inner = {}
for i=y1, y2 do
table.insert(inner,luacalc.display_view[j][i])
end
table.insert(res, inner)
end
end
return res
end
function luacalc.select(tab)
assert(#tab==4)
return tab
end
-- Automagic tables.
-- If a non existant column is indexed with a number, add it
-- If we have indexed with a string, convert to a colum no where
-- A is 1, B is 2.... AA is 27
setmetatable(luacalc.sheet, {
__index = function(table, key)
if type(key) == "number" then
table[key] = {}
return table[key]
-- if key is an all uppercase string
elseif type(key) == "string" and string.find(key, "^%u+$") then
-- return a whole column
return table[luacalc.string_to_index(key)]
end
end
}
)
setmetatable(luacalc.dependant_cells, {
__index = function(table, key)
if type(key) == "number" then
table[key] = {}
return table[key]
end
end
}
)
-- ok....
-- here we set a metatable of a view, so it returns a table containing
-- nothing but the key used to reference it. the table returned has a
-- metatable, with the __index function as given in 'func'
function luacalc.metasetup(lctab, func)
setmetatable(lctab, {
__index = function(tab, key)
local res = {index=key}
setmetatable(res, {__index=func
})
return res
end
}
)
end
-- not really csv view anymore - serial view as well
luacalc.metasetup(luacalc.csv_view,
function(t, k)
local val = luacalc.sheet[k][t.index]
if type(val)=="table" then
val = val.text
end
if type(val)=="string" then
val = string.format("%q", val)
end
if val then
val = val..","
end
return val
end
)
-- save the sheet to the file given
-- would be good if string.concat could
-- be used, but we need to quote strings
-- and get the function text.
function luacalc.save_file(fname)
local f = io.open(fname, "w")
luacalc.forall(luacalc.csv_view,
function(val)
if val then
f:write(val)
end
end,
function()
f:write("\n")
end
)
f:close()
luacalc.dirty=false
end
-- 'normal' unquoted, empty valued csv files
function luacalc.load_file_csv(fname)
y= 1
local f = io.open(fname, 'r')
for line in f:lines() do
x=1
for _, word in ipairs(luacalc.split(line, "[,]+")) do
if word then
luacalc.sheet[x][y] = tonumber(word) or word
x=x+1
end
end
y = y+1
end
f:close()
luacalc.dirty=false
end
-- 'proper' csv files.
-- auto translated to lua, so each value must be a valid lua value
-- thanks to lhf for the help with this.
function luacalc.load_file(fname)
local state=1
local file = io.open(fname)
local f = function()
if state==1 then
state=2
return "luacalc._load{"
end
if state==2 then
local line = file:read()
if line then
return "{"..line.."},"
else
state=3
return "}"
end
end
if state==3 then
return nil
end
end
luacalc.dirty=false
return assert(load(f))()
end
--[[
--sets the table given as the current sheet
--]]
function luacalc._load(tab)
luacalc.sheet=tab
setmetatable(luacalc.sheet, {
__index = function(table, key)
if type(key) == "number" then
table[key] = {}
return table[key]
end
end
}
)
end
-- display the calculated value
luacalc.metasetup(luacalc.display_view,
function (tab, k)
local val = luacalc.sheet[k][tab.index]
return luacalc.raw_value(val, k, tab.index)
end
)
-- import selection functions
require"lcselect"
-- index function for the global metatable.
-- converts variable in form A1 to luacalc.sheet[1][1]
-- references only - no assignments
-- this discourages assigning globals from a spreadsheet.
local function gmt(table, key)
-- only interested in things that start
-- with upper case letters, possibly
-- followed by numbers
if string.find(key, "^%u+%d*$") then
-- this is a cell reference
-- print("Looking up global")
-- print(key)
local letter=""
local number=""
string.foreach(key, function(ind, char)
-- surely there is a more efficient check for capitals?
if string.byte(char) >= string.byte"A" and string.byte(char) <= string.byte("Z") then
letter=letter..char
else
number=number..char
end
end)
local rowind = luacalc.string_to_index(letter)
local ind = tonumber(number)
local row = luacalc.sheet[ind]
if ind then
-- need to convert functions to values
return luacalc.raw_value(row[rowind], ind, rowind)
else
-- return the whole row
return luacalc.getselection{rowind, 1, rowind, #luacalc.sheet}
end
end
end
--
-- Sort works by sorting a table that stores (index, row) couples
-- where the index is the orignal luacalc.sheet index.
-- the comparator takes the original index and finds the values in the
-- given column
--
--
function luacalc.sort(col)
luacalc.dirty=true
tosort = {}
for i=1, #luacalc.sheet do
table.insert(tosort, {index=i, row=luacalc.sheet[i]})
end
table.sort(tosort,
function(a, b)
local val_a = luacalc.sheet[a.index][col]
val_a = luacalc.raw_value(val_a, a.index, col)
local val_b = luacalc.sheet[b.index][col]
val_b = luacalc.raw_value(val_b , b.index, col)
if val_a and val_b then
val_a = tonumber(val_a) or val_a
val_b = tonumber(val_b ) or val_b
if type(val_a)=="string" or type(val_b )=="string" then
return tostring(val_a) < tostring(val_b )
else
return val_a < val_b
end
else
return val_a
end
end
)
local i = 1
for _, v in next, tosort do
luacalc.sheet[i] = v.row
i = i+1
end
end
-- undo the last command (not all actions
function luacalc.undo()
local u, f = commandstack:undo()
-- if f then
debug("f", f)
luacalc.debug_table(f)
-- end
return u, f
end
-- redo the last command
function luacalc.redo()
return commandstack:redo()
end
-- start/end copy are used so that undo can cope with copy/paste.
function luacalc.startpaste()
assert(luacalc.inputmode=="normal")
luacalc.inputmode="paste"
end
-- stop collecting commands for a paste.
function luacalc.endpaste()
-- make a local copy of paste data
local pastedata = {}
for k, v in ipairs(luacalc.pastedata) do
table.insert(pastedata, v)
end
commandstack:push{
undo = function()
-- undo each in fifo order
for i=#pastedata, 1, -1 do
pastedata[i].undo()
end
end,
redo = function()
-- redo each action
for k, v in ipairs(pastedata) do
v.redo()
end
end
}
luacalc.inputmode="normal"
luacalc.pastedata={}
end
-- start recording a macro
-- overwrites any previously stored macro
function luacalc.startmacro(x, y)
luacalc.x, luacalc.y = x, y
luacalc.macro_running = true
luacalc.macro = macrostack()
end
-- stops macro recording
function luacalc.stopmacro()
luacalc.macro_running = false
end
--replay the currently loaded macro
function luacalc.replay()
for _, v in luacalc.macro:ipairs() do
v.func()
-- print(v.text)
end
luacalc.macro_running = false
end
-- record a movement
function luacalc.move(x, y)
luacalc.macro:append{
text="luacalc._move("..x..", "..y..")",
func = function()
luacalc.x = luacalc.x+x
luacalc.y = luacalc.y+y
end
}
end
-- replace a movement (shouldn't be directly called)
function luacalc._move(x, y)
luacalc.x = luacalc.x+x
luacalc.y = luacalc.y+y
end
-- save the current macro
function luacalc.savemacro(fname)
local f = io.open(fname, "w")
for _, v in luacalc.macro:ipairs() do
f:write(v.text)
f:write("\n")
end
f:close()
end
-- load a previously saved macro (or a lua script)
function luacalc.loadmacro(fname)
local text = io.open(fname):read("*all")
--print(text)
luacalc.macro = macrostack()
luacalc.macro:append{
text=text,
func = loadstring(text)
}
luacalc.macro_running=false
end
-- for the wxWidgets gui
-- cause I'm no good at using the lua C api
function get(i, j)
local success, res = pcall(function() return luacalc.display_view[i][j] end)
if not success then
return "#error: "..tostring(res)
else
return tostring(res or '')
end
end
function getformula(i, j)
local val = luacalc.sheet[i][j]
if type(val)=="table" then
return val.text
else
return val
end
end
-- set the value at (i, j) to val
-- can log the requests to allow for undo/redo
function set(i, j, val)
local old = luacalc.sheet[i][j]
luacalc.dirty=true
local action = {
undo = function()
luacalc.sheet[i][j]=old
end,
redo= function()
luacalc.sheet[i][j]=val
end,
i = i,
j = j
}
if luacalc.inputmode == "normal" and #luacalc.pastedata == 0 then
commandstack:push(action)
elseif luacalc.inputmode == "normal" then
error("Invalid state; there should be no paste data")
else
table.insert(luacalc.pastedata, action)
end
if luacalc.macro_running then
luacalc.macro:append{
text="set(luacalc.x, luacalc.y, "..(tonumber(val) or string.format("%q",string.trim(val)))..")",
func=function()
set(luacalc.x, luacalc.y, val)
end
}
end
-- finally, do the actual assignment.
luacalc.sheet[i][j]=val
luacalc.calculate_sub_cells(i, j)
local tab = luacalc.dependant_cells[i]
if tab and #tab > 0 and tab[j] then
debug("i, j is", i, j)
luacalc.debug_table(tab)
return tab[j].super_cells
else
return {}
end
end
function save(fname)
luacalc.save_file(fname)
end
function open(fname)
luacalc.load_file(fname)
end
-- input/show for non-graphical version:
input = input or function(v)
io.write(v..">")
return io.read()
end
show = show or print
-- disable this for debugging.
-- I'm sure this "shouldn't be done" - changing the behaviour
-- of all globals can't be good for modularity.
setmetatable(_G, {__index=gmt})
--function to test cpp binding.
function hello()
-- print("LuaCalc started")
--show"welcome to luacalc"
--show("***"..input("Enter your name").."**")
end