Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 33 additions & 47 deletions pyflowlauncher/string_matcher.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from bisect import bisect_left
from dataclasses import dataclass, field
from functools import lru_cache
from typing import List
Expand Down Expand Up @@ -49,9 +50,13 @@ def string_matcher(query: str, text: str, ignore_case: bool = True,
full_text_lower: str = text.lower() if ignore_case else text
query_lower: str = query.lower() if ignore_case else query

query_substrings: List[str] = query_lower.split(' ')
query_substrings: List[str] = query_lower.split()
if not query_substrings:
return MatchData(False, query_search_precision)
query_substrings_length: int = len(query_substrings)
current_query_substring_index: int = 0
current_query_substring = query_substrings[current_query_substring_index]
current_query_substring_length = len(current_query_substring)
current_query_substring_char_index = 0

first_match_index = -1
Expand All @@ -63,24 +68,27 @@ def string_matcher(query: str, text: str, ignore_case: bool = True,

index_list: List[int] = []
space_indices: List[int] = []
for text_index in range(len(full_text_lower)):
if (current_acronym_query_index >= len(query_lower) or
(current_acronym_query_index >= len(query_lower) and all_query_substrings_matched)):
query_length = len(query_lower)
for text_index, text_char in enumerate(full_text_lower):
if current_acronym_query_index >= query_length:
break

if full_text_lower[text_index] == SPACE_CHAR and current_query_substring_char_index == 0:
if text_char == SPACE_CHAR and current_query_substring_char_index == 0:
space_indices.append(text_index)

if is_acronym(text, text_index):
if full_text_lower[text_index] == query_lower[current_acronym_query_index]:
acronym_char = is_acronym_char(text, text_index)
acronym_number = is_acronym_number(text, text_index)

if acronym_char or acronym_number:
if text_char == query_lower[current_acronym_query_index]:
acronym_match_data.append(text_index)
acronyms_matched += 1
current_acronym_query_index += 1

if is_acronym_count(text, text_index):
if is_acronym_count(text, text_index, acronym_char, acronym_number):
acronyms_total_count += 1

if all_query_substrings_matched or (full_text_lower[text_index]
if all_query_substrings_matched or (text_char
!= current_query_substring[current_query_substring_char_index]):
match_found_in_previous_loop = False
continue
Expand All @@ -107,18 +115,18 @@ def string_matcher(query: str, text: str, ignore_case: bool = True,

current_query_substring_char_index += 1

if current_query_substring_char_index == len(current_query_substring):
if current_query_substring_char_index == current_query_substring_length:
all_substrings_contained_in_text = match_found_in_previous_loop and all_substrings_contained_in_text

current_query_substring_index += 1

all_query_substrings_matched = all_query_substrings_matched_func(
current_query_substring_index, len(query_substrings))
all_query_substrings_matched = current_query_substring_index >= query_substrings_length

if all_query_substrings_matched:
continue

current_query_substring = query_substrings[current_query_substring_index]
current_query_substring_length = len(current_query_substring)
current_query_substring_char_index = 0

if acronyms_matched > 0 and acronyms_matched == len(query):
Expand Down Expand Up @@ -167,52 +175,37 @@ def calculate_search_score(query: str, text: str, first_index: int, space_indice
def get_updated_index_list(start_index_to_verify: int,
current_query_substring_char_index: int,
first_matched_index_in_word: int, index_list: List[int]):
updated_list: List[int] = []

for idx, item in enumerate(index_list):
if item >= first_matched_index_in_word:
index_list.pop(idx)

updated_list.extend(index_list)
updated_list: List[int] = [item for item in index_list if item < first_matched_index_in_word]

for i in range(current_query_substring_char_index):
updated_list.append(start_index_to_verify + i)
updated_list.extend(range(start_index_to_verify,
start_index_to_verify + current_query_substring_char_index))

return updated_list


def all_query_substrings_matched_func(current_query_substring_index: int, query_substrings_length: int) -> bool:
return current_query_substring_index >= query_substrings_length


def all_previous_chars_matched(start_index_to_verify: int,
current_query_substring_char_index: int,
full_text_lower: str, current_query_substring: str) -> bool:
all_match = True
for i in range(current_query_substring_char_index):
if full_text_lower[start_index_to_verify + i] != current_query_substring[i]:
all_match = False

return all_match
end_index = start_index_to_verify + current_query_substring_char_index
return (full_text_lower[start_index_to_verify:end_index]
== current_query_substring[:current_query_substring_char_index])


def is_acronym(text: str, text_index: int) -> bool:
if is_acronym_char(text, text_index) or is_acronym_number(text, text_index):
return True
return False
return is_acronym_char(text, text_index) or is_acronym_number(text, text_index)


def is_acronym_count(text: str, text_index: int) -> bool:
if is_acronym_char(text, text_index):
def is_acronym_count(text: str, text_index: int, acronym_char: bool, acronym_number: bool) -> bool:
if acronym_char:
return True
if is_acronym_number(text, text_index):
if acronym_number:
return text_index == 0 or text[text_index - 1] == SPACE_CHAR

return False


def is_acronym_char(text: str, text_index: int) -> bool:
return text[text_index].isupper() or text_index == 0 or text[text_index - 1] == SPACE_CHAR
return text_index == 0 or text[text_index - 1] == SPACE_CHAR or text[text_index].isupper()


def is_acronym_number(text: str, text_index: int) -> bool:
Expand All @@ -221,12 +214,5 @@ def is_acronym_number(text: str, text_index: int) -> bool:

def calculate_closest_space_index(space_indices: List[int], first_match_index: int) -> int:

closest_space_index = -1

for i in space_indices:
if i < first_match_index:
closest_space_index = i
else:
break

return closest_space_index
position = bisect_left(space_indices, first_match_index)
return space_indices[position - 1] if position > 0 else -1
15 changes: 14 additions & 1 deletion tests/unit/test_string_matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,17 @@ def test_subtext():
index_list=[4, 5, 6],
score=119
)
assert string_matcher.string_matcher("bar", "foo bar baz", query_search_precision=50) == match_data
assert string_matcher.string_matcher("bar", "foo bar baz", query_search_precision=50) == match_data


def test_consecutive_spaces_in_query():
match_data = string_matcher.string_matcher("foo bar", "foo baz")
assert match_data.matched is False


def test_index_list_pruned_on_word_restart():
# The second query word restarts mid-word in the text; stale indices from
# the abandoned partial match must all be pruned from the index list.
match_data = string_matcher.string_matcher("v vl", "ynbgdbzvqsbpdibcjvaydzdtgsgfvla")
assert match_data.matched is True
assert match_data.index_list == [28, 29]