1
0
pretty/analyze_structure.lua
Jon Michael Aanes 87b12e15b2 Fixed issues with analýse_structure.lua
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.
2017-04-14 13:06:43 +02:00

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 }