voxelcore/res/modules/search_utils.lua
2026-08-30 22:11:08 +05:00

98 lines
3 KiB
Lua

local M = {}
-- Оценивает схожесть строк и возвращает совпадение (>= 0) и если переданы
-- параметры color_matched и color_normal, возвращает также исходную строку,
-- в которой совпадающие части обрамлены в color_matched и color_normal
---@param sample str где ищем совпадения
---@param pattern str какие совпадения ищем
---@param color_matched? str
---@param color_normal? str
---@return int match
---@return str? colored_sample
function M.fuzzy_score(sample, pattern, color_matched, color_normal)
if pattern == "" then return 0 end
local do_coloring = color_matched and color_normal
local colored_sample
if do_coloring then
colored_sample = ""
end
local sample_lower = string.lower(sample)
local pattern_lower = string.lower(pattern)
local sample_len = #sample_lower
local pattern_len = #pattern_lower
if pattern_len > sample_len then return 0 end
local pattern_idx = 1
local consecutive = 0
local max_consecutive = 0
local matched_chars = 0
local total_distance = 0
local last_match_pos = -1
for i = 1, sample_len do
if pattern_idx <= pattern_len and (
sample_lower:sub(i, i) ==
pattern_lower:sub(pattern_idx, pattern_idx)) then
matched_chars = matched_chars + 1
if consecutive == 0 and do_coloring then
colored_sample = colored_sample .. color_matched
end
consecutive = consecutive + 1
if consecutive > max_consecutive then
max_consecutive = consecutive
end
if last_match_pos ~= -1 then
total_distance = total_distance + (i - last_match_pos - 1)
end
last_match_pos = i
pattern_idx = pattern_idx + 1
else
if consecutive ~= 0 and do_coloring then
colored_sample = colored_sample .. color_normal
end
consecutive = 0
end
if do_coloring then
colored_sample = colored_sample .. sample:sub(i, i)
end
end
if consecutive ~= 0 and do_coloring then
colored_sample = colored_sample .. color_normal
end
if pattern_idx <= pattern_len then
return 0
end
local coverage = matched_chars / pattern_len * 20
local consecutive_bonus = max_consecutive * 10
local start_bonus = 0
if sample_lower:sub(1, 1) == pattern_lower:sub(1, 1) then
start_bonus = 15
end
local prefix_bonus = 0
if sample_lower:find(pattern_lower, 1, true) == 1 then
prefix_bonus = 25
end
local distance_penalty = total_distance * 2
local score = coverage + consecutive_bonus + start_bonus + prefix_bonus - distance_penalty
if sample_lower:find(pattern_lower, 1, true) then
score = score + 30
end
return math.max(0, score), colored_sample
end
return M