Skip to content

Instantly share code, notes, and snippets.

@moteus
Created June 21, 2013 08:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save moteus/5829768 to your computer and use it in GitHub Desktop.
Save moteus/5829768 to your computer and use it in GitHub Desktop.
Finds longest prefix from prefix list for given string
------------------------------------------------
-- Общие функции для работы с деревом --
------------------------------------------------
local default_compare = function(lhs, rhs) return lhs == rhs end;
local default_char_set = {'0','1','2','3','4','5','6','7','8','9'};
local INVALID_VALUE_ALWAYS_NIL = {}
--Удаляет все пустые ветви в дереве
local pack_empty
pack_empty = function(t)
for k,v in pairs(t) do
if type(k) ~= 'boolean' then
if not pack_empty(v) then
t[k] = nil
end
end
end
if next(t) ~= nil then
return true
end
end
-- Поглащение длинных префиксов более короткими
local pack_dup = function(t, compare_value)
compare_value = compare_value or default_compare
local do_work
do_work = function(t, v, val_prefix, prefix)
prefix = prefix or ''
val_prefix = val_prefix or ''
if t[true] ~= nil then
if compare_value(v, t[true], val_prefix, prefix) then
t[true] = nil
else
v = t[true]
val_prefix = prefix
end
end
for k,r in pairs(t) do
if type(k) ~= 'boolean' then
do_work(r, v, val_prefix, prefix .. k)
end
end
end
do_work(t)
end
-- "Сворачивает" ветки поддерева
--[[
@param pack_percent
Если есть следующие префиксы : 10,11,12,13,14,15,16,17,18 с одинаковыми значениями то их
возможно заменить на один префикс: 1, но при этом необходимо создать префикс 19.
Значение этого префикса должно быть либо значением префикса 1(если он существовал до сворачивания)
либо принемать невалидное значение. При этом если невалидное значение отсутствует, то свертка
оказывается невозможной.
Такая замена имеет смысл если результирующее число префиксов будет меньше и если
существует возможность создания невалидных значений.
Этот параметр указывает какой процент префиксов сворачивается (в примере сворачивается 90 процентов)
если установить 100, то будут сворачиватся только все префиксы и невалидные значения вставлятся не будут
@param invalid_value
невалидное значение
если оно равно INVALID_VALUE_ALWAYS_NIL, то это приведет к запрету переноса значения с короткого префикса
на длинный. Например, если рассмотреть предыдущий пример, и предположить что префикс 1 имеет
некоторое значение, то в случае установки этого значения сворачивание будет невозможно. Это предотвращает
создание ошибочных ситуаций когда номер равный короткому префиксу(1) до сворачивания имел одно значение,
а после сворачивания другой
@param compare_value
функция сравнения двух значенй. По умолчанию применяется operator==
@param char_set
полное множество значений узлов если у узла есть подчиненные узлы которые покрывают это множество
и эти узлы имеют одинаковое значение то это значение можно перенести на этот узел.
По умолчанию цыфры от 0 до 9
@note
Следует учитывать следующие эффект
Если есть префиксы : 1[0..9] с одинаковыми значениями, то номеру '1' не соответствует ни одно значение.
После сворачивания в префикс 1 этот номер принимает действительное значение.
Тот же вариант если есть префикс 1 с одним значениеим и префиксы 11[0..9] с другим, то до свертки номеру 11 соответствует
значение префикса 1, а после свертки значение префикса 11.
В таком случае создается еще одно значение [false] которое соответствует старому значению для этого номера.
Это относится к номеру равному префиксу к которому производится свертка. Свертка может быть произведена если
этот номер не является верным.
--]]
local pack_roll = function(t, pack_percent, invalid_value, compare_value, char_set)
pack_percent = tonumber(pack_percent)
if pack_percent == nil then
return
end
compare_value = compare_value or default_compare
char_set = char_set or default_char_set
local do_work
do_work = function(t, prfx, val)
for k,v in pairs(t) do
if type(k) ~= 'boolean' then
do_work(v, (prfx or '') .. k, v[true] or val)
end
end
if prfx == nil then
return
end
-- таблица для подсчета количества узлов для разных значений
local node = {}
-- в node заносим количество префиксов для каждого значения
for _, key in ipairs(char_set) do repeat
if (t[key] == nil) or (t[key][true] == nil) then
break --continue
end
local value = t[key][true]
local flag = false
for i,j in pairs(node)do
if compare_value(value, i) then
node[i] = node[i] + 1
flag = true
end
end
-- Нет такого значения в node
if not flag then
node[value] = 1;
end
until true end
-- Находим значение с максимальным числом повторений
local max_val
for i,j in pairs(node)do
if max_val == nil then
max_val = i
elseif node[max_val] < j then
max_val = i
end;
end;
--Если свертка не целесообразна
if
(max_val == nil) or
(node[max_val] < (#char_set * (pack_percent / 100 )))
then
return nil
end
-- Предотвращает создание длинных префиксов
if invalid_value == INVALID_VALUE_ALWAYS_NIL then
val = nil
end
-- если нет 'верхнего' значения то можно сворачивать только целиком.
-- например есть префиксы 1[0..9]->aaa то их можно свернуть в 1->aaa
-- но если есть только 1[0..8]->aaa и для 1 нет значения и не определено invalid_value
-- то свертка 1->aaa невозможна
-- но если определен еще префикс 19->bbb то возможно 1->aaa 19->bbb
local total_ = 0
table.foreach(node,function(_,v) total_ = total_ + v end)
if val == nil and total_ < #char_set then
return
end
t[true] = max_val
-- Это значение для полного соответствия префиксу
t[false] = val
--свертка
for _, key in ipairs(char_set) do repeat
-- Нет поддерева - создаем
if t[key] == nil then
t[key] = {}
end
-- Нет значения - присваиваем значение более короткого префикса
if t[key][true] == nil then
assert(pack_percent < 100)
assert(val)
t[key][true] = val
break --continue
end
--Удаляем префиксы с максимальным количеством повторений
if compare_value(t[key][true], max_val) then
t[key][true] = nil;
end
until true end
return true
end
do_work (t, nil, invalid_value)
pack_empty(t)
end
-- Возвращает поддерево
local function sub_tree(t, key)
for i = 1, #key do
local b = string.sub(key, i, i)
t = t[b]
if not t then
return nil
end
end
return t
end
-- Итератор
local function for_each(t, func)
if type(func) ~= 'function' then
return
end
local do_work
do_work = function (t, name)
if t[true] ~= nil then
func(name, t[true], true)
end
if t[false] ~= nil then
func(name, t[false], false)
end
for k, r in pairs(t) do
if type(k) ~= 'boolean' then
do_work(r, name .. k)
end
end
end
do_work(t, '')
end
-- Итератор
local function for_each_sort(t, char_set, func)
if (type(char_set) == 'function')and(func == nil) then
func = char_set
char_set = default_char_set
end
if type(func) ~= 'function' then
return
end
local do_work
do_work = function (t, name)
if t[true] ~= nil then
func(name, t[true], true)
end
if t[false] ~= nil then
func(name, t[false], false)
end
for _, k in ipairs(char_set) do
assert(type(k) ~= 'boolean')
local r = t[k]
if r then
assert(type(r) == 'table')
do_work(r, name .. k)
end
end
end
do_work(t, '')
end
-- Итератор
local function transform(t, func)
if type(func) ~= 'function' then
return
end
local do_work
do_work = function (t, name)
if t[true] ~= nil then
t[true] = func(name, t[true], true)
end
if t[false] ~= nil then
t[false] = func(name, t[false], false)
end
for k, r in pairs(t) do
if type(k) ~= 'boolean' then
do_work(r, name .. k)
end
end
end
do_work(t, '')
end
---
-- Индексированный поиск по префиксам
-- при вводе полного слова осуществляется поиск значения соответствующий
-- сомому длинному префиксу
--
local tree_index = {}
---
-- Добавление префикса и значения
-- существующее значение перезаписывается
-- flag - true - значение для префикса
-- - false - префикс это не префикс а полная строка
function tree_index:add( key, val, flag )
if flag == nil then flag = true end
assert(type(flag) == 'boolean')
local t = self.data
for i = 1, #key do
local b = key:sub(i,i)
if not t[b] then
t[b] = {}
end
t = t[b]
end
t[flag] = val
end
---
-- Поиск значения на совпадение самого длинного ключа
-- use_false - по умолчанию включен
-- если у префикса есть 2 значения.
-- брать второе если ключ полностью соответствует префиксу
-- и первое если ключ длиннее префикса.
-- при use_false = false всегда берется только первое значение
-- return value, prefix, flag
function tree_index:find( key , use_false)
if use_false == nil then
use_false = true
end
local real_key = ''
local flag = true
local t = self.data
local value = t[true]
for i = 1, #key do
local b = string.sub(key, i, i)
t = t[b]
if not t then
break
end
if use_false and i == #key and t[false] ~= nil then
value = t[false]
flag = false
real_key = string.sub(key, 1, i)
elseif t[true] ~= nil then
value = t[true]
flag = true
real_key = string.sub(key, 1, i)
end
end
if (value == nil) or (value == self.invalid_value) then
return nil
end
return value, real_key, flag
end
---
-- Удаление значения
function tree_index:del( key )
local t = self.data
for i = 1, #key do
local b = string.sub(key, i, i)
t = t[b]
if not t then
return false
end
end
t[true] = nil
return true
end
---
-- Итератор
function tree_index:for_each( func )
for_each(self.data, func)
return self
end
function tree_index:for_each_sort( char_set_or_func, func_or_nil )
for_each_sort(self.data, char_set_or_func, func_or_nil)
return self
end
function tree_index:pack_dup(compare_value)
return pack_dup(self.data, compare_value)
end
function tree_index:pack_empty()
return pack_empty(self.data)
end
function tree_index:pack_roll(pack_percent, compare_value, char_set)
return pack_roll(
self.data,
pack_percent,
self.invalid_value,
compare_value,
char_set
)
end
---
-- Минимизирует префиксное поле
function tree_index:pack( pack_percent, compare_value, char_set )
self:pack_dup(compare_value)
self:pack_empty()
self:pack_roll(pack_percent, compare_value, char_set)
end
---
-- возаращает часть дерева
-- копирования не происходит
function tree_index:sub_tree( key )
local data = sub_tree(self.data, key)
if not data then
return data
end
local t = {data = data}
table.foreach(self, function(k,v)
if t[k] == nil then
t[k] = v
end
end)
return t
end
function tree_index:transform(func)
transform(self.data, func)
return self
end
function tree_index:clear()
self.data = {}
end
---
--
function tree_index:keys()
local t = {}
self:for_each(function(prefix)
table.insert(t,prefix)
end)
return t
end
---
-- возвращает множество ключей
function tree_index:key_set(val)
val = (val ~= nil) and val or true
local t = {}
self:for_each(function(prefix)
t[prefix] = val
end)
return t
end
---
--
function tree_index:values()
local t = {}
self:for_each(function(prefix,val)
t[prefix] = val
end)
return t
end
---
--
function tree_index:set_values(t)
self:clear()
for prefix,value in pairs(t)do
self:add(prefix,value)
end
return t
end
---
--
function tree_index:exists( key )
local _, pfx = self:find(key)
return pfx == key
end
---
-- расширяет по возможности набор ключей до
-- указоного списка
-- @param clone = функция для клонирования значения
--
function tree_index:expand( keys, clone )
if clone then
for _,newkey in ipairs(keys) do
local val, key = self:find(newkey)
if val and (newkey ~= key) then
self:add(newkey, clone(val))
end
end
else
for _,newkey in ipairs(keys) do
local val, key = self:find(newkey)
if val and (newkey ~= key) then
self:add(newkey, val)
end
end
end
end
-------------------------------------------------------------------
-- Реализация загрузки префиксов из файла
-------------------------------------------------------------------
require 'lpeg'
require 're'
local function try(f, catch_f)
local ok, exception = pcall(f)
if not ok then
if catch_f then catch_f(exception) else error(exception) end
end
end
local function fitstr(str, ch, width)
if #str >= width then
return str
end
return string.rep(ch,width-#str) .. str
end
---
-- Уменьшает диапазон удаляя последние символы из префиксов
-- если они покрывают весь диапазон
--
-- 74957252300-74957252399 -> 749572523
--
local function pack_range(be, en, first_cahr, last_cahr)
if #be ~= #en then
return be, en
end
first_cahr = first_cahr or '0'
last_cahr = last_cahr or '9'
local last_be, last_en = string.sub(be, -1, -1), string.sub(en, -1, -1)
while(last_be == first_cahr) do
if last_en == last_cahr then
be, en = string.sub(be, 1, -2), string.sub(en, 1, -2)
last_be, last_en = string.sub(be, -1, -1), string.sub(en, -1, -1)
else
break
end
end
return be, en
end
local DecodePrefixList
local DecodeAppend
do
local function list(t)
t.subprefix=nil
return {
name = "list";
value = t;
}
end
local function append(t)
t.subprefix=nil
return {
name = "append";
value = t;
}
end
local function append_tst(t)
t.subprefix=nil
return {
name = "append";
value = t;
}
end
function serialize(...)
require "sys"
sys.serialize({...},"SERIALIZE")
end
local PrefixList_pat = re.compile(
[=============================================================================[
mian <- <sp>* <str> <sp>* <eos>
str <- (<list> (<lstdelim> <list>)*) ->{}
list <- <list1> ->{}
list1 <- (
<applist> ->{} ->append
<listelem> ->{} ->list
) / -- 7 4 9 5,6 -> append{7,4,9} list{5,6}
(<applist> <elem>) ->{} ->append / -- 7 495 -> append{7,495}
<listelem> ->{} ->list / -- 7,495 -> list{7,495}
<elem> ->{} ->list -- 7495 -> list{7495}
applist <- ( <elem> <appdelim>)+ -- список главных префиксов. может существовать
-- только при наличии подчененных элементов
listelem <- ('('<sp>* <listelem1> <sp>*')') / <listelem1> -- список элементов
listelem1 <- ((<elem> <elmdelim>)+ <elem>)
elem <- ('(' <sp>* <elem1> <sp>* ')') / <elem1> -- элемент списка диапазон или одиночный элемент
elem1 <- <range> / <single>
range <- {:subprefix: %a+(%d+%a+)+ / (%a*) :}
(
{:beg:%d+:}
<sp>* [-] <sp>*
{:sub:=subprefix:} {:en:%d+:}
) -> {}
single <- {%w*}
appdelim <- <sp> !(<elmdelim> / <lstdelim> / <eos>) -- Отделяет главные префиксы
lstdelim <- (<sp>* [;] <sp>*) -- Отделяет независимые списки
elmdelim <- (<sp>* [,] <sp>*) -- Отделяет элементы списка
sp <- ' '+
eos <- !.
]=============================================================================],
{list=list,append=append,appendtst=append_tst,print=serialize})
function DecodePrefixList(str)
local t = lpeg.match(PrefixList_pat, str)
if t then
for i = 1,#t do
for j = 1, #t[i] do
local name = t[i][j].name
local value = t[i][j].value
t[i][name] = value;
t[i][j] = nil
end
end
end
return t
end
function DecodeAppend(append,pack_range)
local new_append = {''}
if append == nil then
return new_append
end
for i,prefix in ipairs(append) do
if type(prefix) == 'string' then
-- добавляем новый префикс ко всем предыдущим
for k,v in ipairs(new_append) do
new_append[k] = v .. prefix
end
else
local sub_prefix, beg, en = assert(prefix.sub), assert(prefix.beg), assert(prefix.en)
if pack_range and (append[i+1] == nil) then
beg, en = pack_range(beg, en)
end
local len = math.max(#beg, #en)
if len > 0 then
local t = {}
for i = beg, en do
local s = sub_prefix .. fitstr(tostring(i), '0', len)
for k,v in ipairs(new_append) do
table.insert(t,v..s)
end
end
new_append = t
end
end
end
return new_append
end
do -- test --
local cmp_t
local function cmp_v(v1,v2)
local flag = true
if type(v1) == 'table' then
if type(v2) == 'table' then
flag = cmp_t(v1, v2)
else
flag = false
end
else
flag = (v1 == v2)
end
return flag
end
function cmp_t(t1,t2)
for k in pairs(t2)do
if t1[k] == nil then
return false
end
end
for k,v in pairs(t1)do
if not cmp_v(t2[k],v) then
return false
end
end
return true
end
local function dump (pat)
require "sys"
local t = DecodePrefixList(pat)
sys.serialize(t,'"' .. pat .. '"')
io.write"\n"
end
local function dump2 (pat)
require "sys"
local t = DecodePrefixList(pat)
t = DecodeAppend(assert(t[1].append),pack_range)
sys.serialize(t,'"' .. pat .. '"')
io.write"\n"
end
-- dump"7 4 9 (5,6) 7"
-- dump2"999 1-5 7-8"
local tests = {}
local tests_index={}
local function tclone(t)
local res = {}
for k,v in pairs(t)do res[k]=v end
return res
end
local test = function(str, result)
local t
if type(result) == 'string' then
local res = assert(tests_index[str])
t = {result, result = res.result}
assert(result ~= str)
tests_index[result] = t;
else
t = {str,result=result}
tests_index[str] = t;
end
return table.insert(tests,t)
end
--test = function()end
test("7 4 9 5,6",
{{
append = {"7","4","9"};
list = {"5","6"};
}}
)
test("7 4 9 5,6", "7 4 9 (5,6)" )
test("7 4 9 5,6", "7 4 9 (5 ,6)" )
test("7 495",
{{
append = {"7","495"};
}}
)
test("7495",
{{
list = {"7495"};
}}
)
test("7,495",
{{
list = {"7","495"};
}}
)
test("749 5 - 6",
{{
append={
[1]="749";
[2]={
beg="5";
sub="";
en="6";
};
};
}}
)
test("749 5 - 6", "749 (5 - 6)")
test("749 5 - 6", "749 (5-6)" )
test("749 5 - 6", "749 ( 5-6 )")
test("7 5 1-5 7-9",
{{
append={
[1]="7";
[2]="5";
[3]={
sub="";
en="5";
beg="1";
};
[4]={
sub="";
en="9";
beg="7";
};
};
}}
);
test("7 5 1-5 7-9", "7 5 1- 5 7-9")
test("7 5 1-5 7-9", "7 5 1 -5 7-9")
test("7 5 1-5 7-9", "7 5 1 - 5 7-9")
test("7 5 1-5 7-9", "7 5 1 - 5 7 - 9")
test("7 5 1-5 7-9", "7 5 1-5 7 - 9")
test("7 5 1-5 7-9", "7 5 (1-5) 7-9")
test("7 5 1-5 7-9", "7 5 ( 1-5) 7-9")
test("7 5 1-5 7-9", "7 5 ( 1-5 ) 7-9")
test("7 5 1-5 7-9", "7 5 ( 1 - 5 ) 7-9")
test("7 5 1-5 7-9", "7 5 ( 1 - 5 ) (7-9)")
----------------------------------
test"7 4 9 (5,6) 7"
test"7 4 9 (5,6),7"
for _,test_case in ipairs(tests)do
local t = DecodePrefixList(test_case[1])
assert(cmp_v(t, test_case.result ), test_case[1])
-- проверяем разделение на списки
local str = test_case[1] .. ';' .. test_case[1]
t = DecodePrefixList(str)
local res
if test_case.result ~= nil then
local n = #test_case.result
res = {}
for i = 1,n do
res[i] = test_case.result[i]
res[2*i] = test_case.result[i]
end
assert(#res == 2*n)
end
assert(cmp_v(t, res ), str)
end
end -- test
end
local function ProcessPrefixes(functor, t, value, prefix_str, pack_range)
for _, prefix in ipairs(t.list) do
if type(prefix) == 'string' then
for _,main_prefix in ipairs(t.append) do
functor(main_prefix .. prefix, value, prefix_str)
end
else
local sub_prefix, beg, en = assert(prefix.sub), assert(prefix.beg), assert(prefix.en)
if pack_range then
beg, en = pack_range(beg, en)
end
local len = math.max(#beg, #en)
for i = beg, en do
for _,main_prefix in ipairs(t.append) do
local s = main_prefix .. sub_prefix .. fitstr(tostring(i), '0', len)
functor(s, value, prefix_str)
end
end
end
end
end
local function LoadPrefixFromFile_impl(FileName_or_file, functor, pack_range)
local data_pat = re.compile"[ ]*(({[^\t]+}[\t]?)/({[^\t]*}[\t])){.*}"
local file, do_close
if type(FileName_or_file) == 'string' then
file = assert(io.open(FileName_or_file,'r'),'Can not open file "' .. FileName_or_file .. '"!')
do_close = true
else
file = assert(FileName_or_file)
end
try(function()
for str in file:lines() do
local prefixes, value = lpeg.match(data_pat, str)
if prefixes then -- строка не пустая
local t = assert(DecodePrefixList(prefixes), "Error prefix: " .. (prefixes or '<EMPTY>'))
for _,p in ipairs(t) do
if p.list == nil then
p.list = {''}
p.append = DecodeAppend(p.append, pack_range)
else
p.append = DecodeAppend(p.append)
end
ProcessPrefixes(functor, p, value, prefixes, pack_range)
end
end
end
if do_close then file:close() end
end,
function(e) --catch
if do_close then file:close() end
error(e)
end)
end
---
-- fn pack_range - функция для сжатия диапазонов
-- bool ret_list - возвращать список префикс => строка из которой он сформирован
--
function tree_index:load(FileName, pack_range, ret_list)
local prefix_list_t
local add_prefix
if ret_list then
prefix_list_t = {}
add_prefix = function(prefix, value, list)
self:add(prefix,value)
prefix_list_t[prefix] = list
end
else
add_prefix = function(prefix, value)self:add(prefix,value)end
end
LoadPrefixFromFile_impl(FileName, add_prefix, pack_range)
return prefix_list_t
end
local function LoadPrefixFromFile(...)
local tree = tree_index:new()
local prefix_list_t = tree:load(...)
return tree, prefix_list_t
end
---
-- cnt
function tree_index:new()
local t = setmetatable({
data = {};
invalid_value = {};
char_set = default_char_set;
}, {__index = self})
return t;
end
---
-- serialize
function tree_index:serialize(packer)
return packer{
data = self.data;
invalid_value = self.invalid_value;
char_set = self.char_set;
}
end
---
-- deserialize
function tree_index:deserialize(unpacker, str)
local t = unpacker(str)
if not(t.data and t.char_set) then
return nil, "invalid format"
end
return setmetatable({
data = t.data;
invalid_value = t.invalid_value;
char_set = t.char_set;
}, {__index = self})
end
local type, assert =
type, assert
local _M = {}
function _M.new()
return tree_index:new()
end
function _M.deserialize(...)
return tree_index:deserialize(...)
end
_M.for_each, _M.sub_tree, _M.pack_dup, _M.pack_empty, _M.pack_roll, _M.transform =
for_each, sub_tree, pack_dup, pack_empty, pack_roll, transform
_M.pack_range =
pack_range
_M.INVALID_VALUE_ALWAYS_NIL = INVALID_VALUE_ALWAYS_NIL
_M.LoadPrefixFromFile = LoadPrefixFromFile
return _M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment