Jon Michael Aanes
155c877987
This one is based on the representative width of the table. Not only does this produce better results, but it's also more futureproof.
255 lines
7.1 KiB
Lua
255 lines
7.1 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 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_map (t)
|
|
local k, count = nil, -1
|
|
repeat
|
|
k, count = next(t, k), count + 1
|
|
until not k
|
|
return count
|
|
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 has_seq (t)
|
|
if not contains_only_nice_number_indexes(t) then return false end
|
|
-- Contain list of keys
|
|
local keys = {[0] = 0}
|
|
for i, _ in pairs(t) do keys[#keys+1] = i end
|
|
table.sort(keys)
|
|
-- Check to see that no indice jumps more than 2
|
|
for indice_i = 1, #keys do
|
|
if keys[indice_i - 1] < keys[indice_i] - 2 then return false end
|
|
end
|
|
return true, (#keys ~= keys[#keys])
|
|
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.has_seq, info.has_holes = has_seq(t)
|
|
info.has_map = key_types.nr_types > (key_types.number and 1 or 0)
|
|
info.is_set = is_set(t)
|
|
info.is_tabular = is_tabular(t)
|
|
info.is_short = is_short_table(t)
|
|
info.nr_elems = nr_elements_in_map(t) -- TODO: Use this for something.
|
|
|
|
-- 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
|