-
Notifications
You must be signed in to change notification settings - Fork 0
/
Jade.lua
276 lines (231 loc) · 7.19 KB
/
Jade.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
local Jade = {}
local Jade_mt = { __index = Jade }
-- private function to capitalize string
local function upper(str)
return (str:gsub("^%l", string.upper))
end
-- private function to test for number
local function isNum(n) return tonumber(n) and true or false end
-- tiny helper method which just converts underscores to spaces
function Jade.str(s) return s:gsub("_", " ") end
-- create a brand new 2da file
-- createNew(filename, { label1, label2, label3, etc })
-- don't forget to sync afterwards
function Jade.createNew(fn, cols)
local me = { data = {}, filename = fn }
me.columns = cols
return setmetatable(me, Jade_mt)
end
-- load a 2da file
function Jade.load(fn)
local me = { data = {}, filename = fn }
local file = io.open(fn, "r")
if not file then
error("Could not open file: " .. fn)
end
-- ensure we have the correct prologue
local prologue = file:read("*line")
-- strip leading and trailing whitespace
prologue = prologue:match( "^%s*(.-)%s*$" )
if not prologue or prologue ~= "2DA V2.0" then
error("Invalid 2DA file format.")
end
-- mandatory empty line after the prologue
file:read("*line")
-- parse the column labels
local labels = file:read("*line")
me.columns = {}
for label in labels:gmatch("%S+") do
table.insert(me.columns, label)
end
-- parse the actual data
for line in file:lines() do
local values = {}
local rowIndex
-- extract first column as the index for the row
local ri = line:match("(%S+)")
rowIndex = isNum(ri) and tonumber(ri) or ri
if rowIndex then
local i = 1
values["__id"] = rowIndex
for value in line:gmatch("%S+") do
if i > 1 and me.columns[i - 1] then
values[me.columns[i - 1]] = value == "****" and false or value
-- if it's a number, then uh.. convert it into one
if isNum(value) then
values[me.columns[i - 1]] = tonumber(value)
end
end
i = i + 1
end
-- set the values for this row
me.data[rowIndex] = values
end
end
file:close()
return setmetatable(me, Jade_mt)
end
-- this method gets added to rows to follow a reference to another table
local function _ref(self, rs, label)
if self[label] then
return rs:fetch(self[label])
else
print("Can't find the key '" .. self[label] .. "' in the provided resultset")
return false
end
end
-- fetches a row specified by its key/id
function Jade:fetch(id)
if self.data[id] then
self.data[id].ref = _ref
return self.data[id]
end
return false
end
-- simply dumps the records for the given id to stdout
-- no real use. Was using this to check output when working on the initial parsing.
function Jade:dump(id)
if self.data[id] then
print("")
local label = "RECORDS FOR index (" .. id .. ")"
print(label)
for i=1,#label do
io.write("-")
end
print("")
for k,v in pairs(self.data[id]) do
print(k .. " | " .. v)
end
print("")
end
return false
end
-- removes an item from the resultset
-- returns true on success, false on failure (id does not exist)
-- requires an item record
function Jade:remove(item)
if self.data[item.__id] then
self.data[item.__id] = nil
return true
end
return false
end
-- adds a new record to the dataset
function Jade:add(key, tbl)
--local res = {}
local newIndex = isNum(key) and tonumber(key) or key
self.data[newIndex] = { __id = newIndex }
for _,v in ipairs(self.columns) do
if tbl[v] then
local val = tbl[v]
if type(val) == "string" then val = val:gsub(" ", "_") end
self.data[newIndex][v] = val
else
self.data[newIndex][v] = "****"
end
end
return self.data[newIndex]
end
function Jade:enumerate(fn)
for _,row in pairs(self.data) do
if type(row) ~= "function" then fn(row) end
end
end
-- find is essentially just a shortcut for :search():first()
-- used when you are just expecting a single result
function Jade:find(tbl)
local res = self:search(tbl)
if #res > 0 then return res:first() end
return false
end
-- private function for condition matching, if it wasn't obvious from the name
local function matchCondition(row, condition)
for label, value in pairs(condition) do
if value:sub(-1) == "%" then
value = value:sub(1, -2)
if not string.find(row[label], "^" .. value) then
return false
end
else
if row[label] ~= value then
return false
end
end
end
return true
end
-- returns a resultset of any rows matching the specified conditions
function Jade:search(tbl)
local results = {}
if type(tbl[1]) == "table" then
-- multiple conditions
for index, row in pairs(self.data) do
local match = true
for _, condition in ipairs(tbl) do
if not matchCondition(row, condition) then
match = false
break
end
end
if match then
local id = row.__id
if id then
table.insert(results, self:fetch(id))
end
end
end
else
-- single condition
for index, row in pairs(self.data) do
if matchCondition(row, tbl) then
local id = row.__id
if id then
table.insert(results, self:fetch(id))
end
end
end
end
-- pick off the first result
function results:first()
local f = results[1]
f.ref = _ref
return f
end
-- iterate over each row. a little cleaner than doing a for loop yourself imho
-- other benefit: this can also be chained onto the search resultset
function results:enumerate(fn)
for _,row in pairs(self) do
if type(row) ~= "function" then fn(row) end
end
end
function results:count() return #self end
return results
end
-- sync will write the content of self.data to the filename given in .load
-- if you've made any changes to records you'd like to keep, you should do this
function Jade:sync()
if not self.filename then
print("Error: No filename specified for sync.")
return
end
local file = io.open(self.filename, "w")
if not file then
print("Error: Could not open file for writing.")
return
end
file:write("2DA V2.0\n\n")
-- write labels
local labels = " " .. table.concat(self.columns, " ")
file:write(labels .. "\n")
-- now the actual data rows
for rowIndex, values in pairs(self.data) do
-- Create a line for each row
local line = values.__id
for _, column in ipairs(self.columns) do
line = line .. " " .. (values[column] or "****")
end
file:write(line .. "\n")
end
file:close()
end
return Jade