Jon Michael Aanes
87b12e15b2
The most significant was a misclassification of tables with number exclusive keys, with huge jumps between each one. Example: { 1, [300] = 300 } This was caused by logic faults in `get_table_info`, which has been fixed. When solving above issue, a two new issues appeared: 1. Due to a logic fault, the count of number of elements in a table was wrong, leading to issues with tables with boolean keys. This was fixed with simple logic changes. 2. Some very small tables (1 element) would be classified as sets, and thus rendered wrongly. This was fixed by adding a MINIMUM_NUMBER_OF_SET_ELEMENTS constant.
258 lines
7.2 KiB
Lua
258 lines
7.2 KiB
Lua
|
|
local TABLE_TYPE
|
|
do
|
|
local thispath = ... and select('1', ...):match('.+%.') or ''
|
|
was_loaded, TABLE_TYPE = pcall(require, thispath..'table_type')
|
|
assert(was_loaded, '[pretty]: Could not load vital library: table_type')
|
|
end
|
|
|
|
local RESERVED_LUA_WORDS = {
|
|
['and'] = true,
|
|
['break'] = true,
|
|
['do'] = true,
|
|
['else'] = true,
|
|
['elseif'] = true,
|
|
['end'] = true,
|
|
['false'] = true,
|
|
['for'] = true,
|
|
['function'] = true,
|
|
['if'] = true,
|
|
['in'] = true,
|
|
['local'] = true,
|
|
['nil'] = true,
|
|
['not'] = true,
|
|
['or'] = true,
|
|
['repeat'] = true,
|
|
['return'] = true,
|
|
['then'] = true,
|
|
['true'] = true,
|
|
['until'] = true,
|
|
['while'] = true,
|
|
['goto'] = true,
|
|
}
|
|
|
|
local SIMPLE_VALUE_TYPES = {
|
|
['nil'] = true,
|
|
['boolean'] = true,
|
|
['number'] = true,
|
|
}
|
|
|
|
local SHORT_STRING_MAX_LEN = 7
|
|
local MINIMUM_NUMBER_OF_SET_ELEMENTS = 2
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function is_identifier(str)
|
|
-- An identier is defined in the lua reference guide
|
|
|
|
return str:match('^[_%a][_%w]*$') and not RESERVED_LUA_WORDS[str]
|
|
end
|
|
|
|
local function get_key_types (t)
|
|
local types = { nr_types = -1 }
|
|
for key, _ in pairs(t) do
|
|
types[type(key)] = true
|
|
end
|
|
--
|
|
for type_name, _ in pairs(types) do
|
|
types.nr_types = types.nr_types + 1
|
|
end
|
|
return types
|
|
end
|
|
|
|
local function get_value_types (t)
|
|
local types = { nr_types = -1 }
|
|
for _, value in pairs(t) do
|
|
types[type(value)] = true
|
|
end
|
|
--
|
|
for type_name, _ in pairs(types) do
|
|
types.nr_types = types.nr_types + 1
|
|
end
|
|
return types
|
|
end
|
|
|
|
local function largest_number_index (t)
|
|
-- Returns the largest number index in t.
|
|
|
|
local max_index = 0
|
|
for k,v in pairs(t) do
|
|
if type(k) == 'number' then
|
|
max_index = math.max(max_index, k)
|
|
end
|
|
end
|
|
return max_index
|
|
end
|
|
|
|
local function nr_elements_in_table (t)
|
|
local k, count = nil, -1
|
|
repeat
|
|
k, count = next(t, k), count + 1
|
|
until k == nil
|
|
return count
|
|
end
|
|
|
|
local function nr_elements_in_seq (t)
|
|
local i, last_elem_i, nr_elems, has_holes = 0, 0, 0, false
|
|
while i <= last_elem_i + 2 do
|
|
i = i + 1
|
|
if t[i] ~= nil then
|
|
last_elem_i, nr_elems = i, nr_elems + 1
|
|
else
|
|
has_holes = true
|
|
end
|
|
end
|
|
return nr_elems, has_holes
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function contains_only_nice_string_keys (t)
|
|
-- Predicate: Does t contain only string keys which could be used as
|
|
-- identifiers, eg.
|
|
|
|
for k, _ in pairs(t) do
|
|
if type(k) ~= 'string' or not is_identifier(k) then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function contains_only_nice_number_indexes (t)
|
|
-- Predicate: Does t contain only number keys, all of which are integer,
|
|
-- larger than/equal 1 and less than the maximum index.
|
|
|
|
local max_index = largest_number_index(t)
|
|
for k, v in pairs(t) do
|
|
if type(k) ~= 'number' or k < 1 or max_index < k or k ~= math.floor(k) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return #t > 0
|
|
end
|
|
|
|
local function is_set (t)
|
|
-- Predicate: Does t contain only boolean values.
|
|
local value_types = get_value_types(t)
|
|
return value_types.boolean and value_types.nr_types == 1
|
|
end
|
|
|
|
local function is_tabular (t)
|
|
-- Predicate: Does t contain sub-tables with identical substructure.
|
|
|
|
-- Quick return if table is empty, or not conctaining only values of type table.
|
|
local value_types = get_value_types(t)
|
|
if not value_types.table or value_types.nr_types ~= 1 then
|
|
return false
|
|
end
|
|
|
|
-- Determine keys of first child.
|
|
local children_keys = {}
|
|
for key, _ in pairs(t[next(t)]) do
|
|
children_keys[key] = true
|
|
end
|
|
|
|
-- Make sure every other child has exact same sub-structure.
|
|
for _, child in pairs(t) do
|
|
for key, _ in pairs(children_keys) do
|
|
if not child[key] then return false end
|
|
end
|
|
for key, _ in pairs(child) do
|
|
if not children_keys[key] then return false end
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local is_short_table, is_simple_value
|
|
|
|
function is_short_table (value)
|
|
-- Predicate: value is either an empty table, or one with a single simple
|
|
-- non-function element.
|
|
|
|
if type(value) ~= 'table' then
|
|
error(('[pretty/internal]: Only tables allowed in function analyze_structure.is_short_table, but was given %s (%s)'):format(value, type(value)), 2)
|
|
end
|
|
|
|
local first_key = next(value, nil)
|
|
if not first_key then
|
|
return true
|
|
elseif not next(value, first_key) == nil then
|
|
return false
|
|
end
|
|
|
|
return type(value[first_key]) ~= 'table'
|
|
and is_simple_value( value[first_key] )
|
|
end
|
|
|
|
function is_simple_value (value)
|
|
-- Predicate: value is either nil, a boolean, a number, a short string or a
|
|
-- short table.
|
|
|
|
return SIMPLE_VALUE_TYPES[ type(value) ]
|
|
or type(value) == 'string' and #value <= SHORT_STRING_MAX_LEN
|
|
or type(value) == 'table' and is_short_table(value)
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function get_table_info (t)
|
|
local key_types = get_key_types(t)
|
|
|
|
local info = {}
|
|
info.nr_elems = nr_elements_in_table(t)
|
|
info.seq_elems, info.has_holes = nr_elements_in_seq(t)
|
|
info.map_elems = info.nr_elems - info.seq_elems
|
|
info.has_seq = info.seq_elems > 0
|
|
info.has_map = info.map_elems > 0
|
|
info.is_set = is_set(t) and info.nr_elems >= MINIMUM_NUMBER_OF_SET_ELEMENTS
|
|
info.is_tabular = is_tabular(t)
|
|
info.is_short = is_short_table(t) -- TODO: Remove this. It's not used for anything.
|
|
|
|
-- Determine type of table
|
|
if not info.has_seq and not info.has_map then info.type = TABLE_TYPE.EMPTY
|
|
elseif info.has_seq and not info.has_map then info.type = TABLE_TYPE.SEQUENCE
|
|
elseif info.is_set then info.type = TABLE_TYPE.SET
|
|
elseif info.has_seq then info.type = TABLE_TYPE.MIXED
|
|
elseif contains_only_nice_string_keys(t) then info.type = TABLE_TYPE.STRING_MAP
|
|
else info.type = TABLE_TYPE.PURE_MAP
|
|
end
|
|
|
|
return info
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function visit_table (t, info, visited)
|
|
|
|
-- Who've been visited? Bookkeeping
|
|
visited[t] = (visited[t] or 0) + 1
|
|
if visited[t] == 2 then
|
|
info[t].marker, info.next_mark = info.next_mark, info.next_mark + 1
|
|
end
|
|
if visited[t] >= 2 then return end
|
|
|
|
-- Get table info
|
|
info[t] = get_table_info(t)
|
|
|
|
-- Visit children.
|
|
for k,v in pairs(t) do
|
|
if type(k) == 'table' then visit_table(k, info, visited) end
|
|
if type(v) == 'table' then visit_table(v, info, visited) end
|
|
end
|
|
end
|
|
|
|
local function analyze_structure (t)
|
|
local info, visited = { root = t, next_mark = 1 }, {}
|
|
visit_table(t, info, visited)
|
|
info.next_mark = nil
|
|
return info
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
return { analyze_structure, get_table_info }
|