526 lines
17 KiB
Python
526 lines
17 KiB
Python
from random import Random
|
|
from typing import *
|
|
|
|
class KeyNotFoundException(Exception):
|
|
def __init__(self, message):
|
|
self.message = message
|
|
super().__init__(self.message)
|
|
|
|
class StringComparer:
|
|
def __init__(self):
|
|
pass
|
|
def compare(self, left: str, right: str) -> int:
|
|
if left < right:
|
|
return -1
|
|
if left > right:
|
|
return 1
|
|
return 0
|
|
|
|
class KeyStringComparer(StringComparer):
|
|
def __init__(self, key_selector: Callable[[str], str]):
|
|
self.key_selector = key_selector
|
|
super().__init__()
|
|
def compare(self, left: str, right: str) -> int:
|
|
leftkey = self.key_selector(left)
|
|
rightkey = self.key_selector(right)
|
|
if leftkey < rightkey:
|
|
return -1
|
|
if leftkey > rightkey:
|
|
return 1
|
|
return 0
|
|
|
|
class AnonymousStringComparer(StringComparer):
|
|
def __init__(self, compare_func: Callable[[str, str], int]):
|
|
self.compare_func = compare_func
|
|
super().__init__()
|
|
def compare(self, left: str, right: str) -> int:
|
|
return self.compare_func(left, right)
|
|
|
|
class StringList:
|
|
def __init__(self, sort_comparer: StringComparer = StringComparer(), keep_sorted: bool = False):
|
|
self.values = []
|
|
self.set_sort_comparer(sort_comparer)
|
|
self._keep_sorted = keep_sorted
|
|
|
|
def get_keep_sorted(self):
|
|
"""
|
|
Indicates whether sorted data should be maintained during all operations.
|
|
"""
|
|
return self._keep_sorted
|
|
def set_keep_sorted(self, value):
|
|
if self._keep_sorted != value:
|
|
if value:
|
|
self._quicksort(0, len(self.values) - 1, self._sort_comparer)
|
|
self._keep_sorted = value
|
|
keep_sorted = property(get_keep_sorted, set_keep_sorted, lambda: None)
|
|
|
|
def __getitem__(self, index):
|
|
"""
|
|
Provides an indexer for direct access to the elements of the .value property.
|
|
"""
|
|
return self.values[index]
|
|
def __setitem__(self, index, value):
|
|
"""
|
|
Provides an indexer for direct access to the elements of the .value property.
|
|
"""
|
|
self.values[index] = value
|
|
|
|
def set_sort_comparer(self, sort_comparer: StringComparer):
|
|
"""
|
|
Overrides the default string comparison object.
|
|
The string comparison object is used when sorting and searching.
|
|
"""
|
|
self._sort_comparer = sort_comparer
|
|
return self
|
|
def assign(self, string_list):
|
|
"""
|
|
Tells the StringList to take on all the data and behaviors of the specified secondary StringList.
|
|
In other words, turn yourself into a copy of it.
|
|
"""
|
|
self.values = list(string_list.values)
|
|
self._keep_sorted = string_list.keep_sorted
|
|
self._sort_comparer = string_list._sort_comparer
|
|
return self
|
|
def add_string_list(self, string_list):
|
|
"""
|
|
Append the StringList with a set of strings from a second StringList.
|
|
"""
|
|
self.values.extend(string_list.values)
|
|
self.maintain_sort_order(string_list.count())
|
|
return self
|
|
def clone(self):
|
|
"""
|
|
Create a new StringList instance which is identical to this one.
|
|
"""
|
|
return StringList().assign(self)
|
|
def add(self, item: str):
|
|
"""
|
|
Adds a single string to the end of the StringList.
|
|
"""
|
|
self.values.append(item)
|
|
self.maintain_sort_order(1)
|
|
return self
|
|
def add_strings(self, items: List[str]):
|
|
"""Adds all elements from a list to the end of this StringList."""
|
|
self.values.extend(items)
|
|
self.maintain_sort_order(len(items))
|
|
return self
|
|
def push(self, value: str) -> str:
|
|
"""
|
|
Treats the StringList like a stack. Appends a value to the end of the list.
|
|
"""
|
|
self.add(value)
|
|
self.maintain_sort_order(1)
|
|
return self
|
|
def pop(self) -> str:
|
|
"""
|
|
Treats the StringList like a stack. Removes the last value from the list and returns it.
|
|
"""
|
|
value = self.values[-1:][0]
|
|
self.values = self.values[:-1]
|
|
return value
|
|
def push_bottom(self, value: str) -> str:
|
|
"""
|
|
Treats the StringList like an upside-down stack. Inserts a new value to the beginning of the list.
|
|
"""
|
|
self.insert(0, value)
|
|
self.maintain_sort_order(1)
|
|
return self
|
|
def pop_bottom(self) -> str:
|
|
"""
|
|
Treats the StringList like an upside-down stack. Removes the first value from the list and returns it.
|
|
"""
|
|
value = self.values[0]
|
|
self.values = self.values[1:]
|
|
return value
|
|
def enqueue(self, value: str) -> str:
|
|
"""
|
|
Inserts an element at the beginning of the list.
|
|
"""
|
|
self.insert(0, value)
|
|
self.maintain_sort_order(1)
|
|
return self
|
|
def dequeue(self) -> str:
|
|
"""
|
|
Removes the last element of the list and returns it.
|
|
"""
|
|
return self.pop()
|
|
def clear(self):
|
|
"""
|
|
Removes all elements from the StringList.
|
|
"""
|
|
self.values = []
|
|
return self
|
|
def count(self) -> int:
|
|
"""
|
|
Returns the number of elements in the StringList.
|
|
"""
|
|
return len(self.values)
|
|
def load_from_file(self, filename: str):
|
|
"""
|
|
Loads all lines from a text file into the StringList.
|
|
"""
|
|
f = open(filename, "r")
|
|
try:
|
|
self.values = f.readlines()
|
|
self.maintain_sort_order(self.count())
|
|
finally:
|
|
f.close()
|
|
return self
|
|
def strip_line_endings(self):
|
|
"""
|
|
Removes all line endings from each string in the StringList. This is useful after loading from a file.
|
|
"""
|
|
self.values = [value.rstrip("\r\n") for value in self.values]
|
|
return self
|
|
def save_to_file(self, filename: str, line_ending:str = "\n"):
|
|
"""
|
|
Saves the contents of the StringList to a text file.
|
|
"""
|
|
f = open(filename, "w")
|
|
try:
|
|
for s in self.values:
|
|
f.write(s + line_ending)
|
|
finally:
|
|
f.close()
|
|
return self
|
|
def insert(self, index: int, value: str):
|
|
"""
|
|
Insert a single string value into the StringList at the specified 0-based index instead of the end.
|
|
"""
|
|
self.values.insert(index, value)
|
|
self.maintain_sort_order(1)
|
|
return self
|
|
def insert_range(self, index: int, values: List[str]):
|
|
"""
|
|
Insert a list of strings into the StringList at the specified 0-based index instead of the end.
|
|
"""
|
|
list = []
|
|
list.extend(values)
|
|
list.extend(self.values)
|
|
self.maintain_sort_order(len(values))
|
|
|
|
self.values = list
|
|
def insert_string_list(self, index: int, string_list):
|
|
"""
|
|
Insert the contents of a second StringList into the StringList at the specified 0-based index instead of the end.
|
|
"""
|
|
list = []
|
|
list.extend(string_list.values)
|
|
list.extend(self.values)
|
|
self.maintain_sort_order(len(string_list.values))
|
|
|
|
self.values = list
|
|
def remove(self, index: int, count: int = 1):
|
|
"""
|
|
Removes one or more values from the StringList beginning at the specified 0-based index.
|
|
"""
|
|
del self.values[index: index + count]
|
|
return self
|
|
def parse(self, text: str, delimiter="\n"):
|
|
"""
|
|
Read a string, split it by delimter and load the elements into the StringList. The opposite of .text().
|
|
"""
|
|
self.values = text.split(delimiter)
|
|
self.maintain_sort_order(self.count())
|
|
return self
|
|
def text(self, delimiter: str = ""):
|
|
"""
|
|
Returns all lines from the StringList concatenated as a single string. The opposite of .parse().
|
|
"""
|
|
return delimiter.join(self.values)
|
|
def contains_key(self, key: str, delimiter: str = "=", key_comparer = lambda s1, s2: s1 == s2):
|
|
"""
|
|
Determines if any line in the StringList contains a key/value pair with the given key.
|
|
"""
|
|
for line in self.values:
|
|
line_key = self.key_of_pair(line, delimiter)
|
|
if key_comparer(line_key, key):
|
|
return True
|
|
return False
|
|
def value_of_key(self, key: str, delimiter: str = "=", key_comparer = lambda s1, s2: s1 == s2):
|
|
"""
|
|
Returns the value part of a key-value pair from the StringList.
|
|
Any string can qualify as a key-value pair if the delimiter is present in the string.
|
|
"""
|
|
for line in self.values:
|
|
line_key = self.key_of_pair(line, delimiter)
|
|
if key_comparer(line_key, key):
|
|
return self.value_of_pair(line, delimiter)
|
|
raise KeyNotFoundException(f"Key not found in StringList: '{key}'")
|
|
def value_of_pair(self, pair: str, delimiter: str = "="):
|
|
"""
|
|
Parses the provided key/value pair and returns the value part (the second part).
|
|
"""
|
|
parts = pair.split(delimiter)
|
|
if len(parts) >= 2:
|
|
return parts[1]
|
|
return ""
|
|
def key_of_pair(self, pair: str, delimiter: str = "="):
|
|
"""
|
|
Parses the provided key/value pair and returns the key part (the first part).
|
|
"""
|
|
parts = pair.split(delimiter)
|
|
if len(parts) >= 1:
|
|
return parts[0]
|
|
return ""
|
|
def all_keys(self, delimiter: str = "="):
|
|
"""
|
|
Returns the key part of all key-value pairs in the StringList.
|
|
Any string can qualify as a key-value pair if the delimiter is present in the string.
|
|
"""
|
|
return [self.key_of_pair(line, delimiter) for line in self.values]
|
|
def all_values(self, delimiter: str = "="):
|
|
"""
|
|
Returns the value part of all key-value pair from the StringList.
|
|
Any string can qualify as a key-value pair if the delimiter is present in the string.
|
|
"""
|
|
return [self.value_of_pair(line, delimiter) for line in self.values]
|
|
def all_values_of_key(self, key: str, delimiter: str = "=", key_comparer = lambda s1, s2: s1 == s2):
|
|
"""
|
|
Returns the value part of a key-value pair from the StringList.
|
|
This method assumes there may be duplicate keys.
|
|
Any string can qualify as a key-value pair if the delimiter is present in the string.
|
|
"""
|
|
return [self.value_of_pair(line, delimiter) for line in self.values if key_comparer(self.key_of_pair(line, delimiter), key)]
|
|
def to_dictionary(self, delimiter: str = "="):
|
|
"""
|
|
Determines all key-value pairs within the StringList and returns them collectively as a new dictionary.
|
|
"""
|
|
tuple_pairs = [(self.key_of_pair(line), self.value_of_pair(line)) for line in self.values]
|
|
return dict(tuple_pairs)
|
|
def to_indexed_dictionary(self, delimiter: str = "="):
|
|
"""
|
|
Determines all key-value pairs within the StringList and returns them collectively as a new dictionary.
|
|
"""
|
|
tuple_pairs = [(self.key_of_pair(x[1]), (x[0], self.value_of_pair(x[1]))) for x in [x for x in zip(range(0, len(self.values)), self.values)]]
|
|
return dict(tuple_pairs)
|
|
def _quicksort(self, partition_left: int, partition_right: int, comparer: StringComparer = None):
|
|
"""
|
|
Sort a large unordered list using the QuickSort algorithm. If the list is small, it will delegate a more efficient algorithm instead.
|
|
"""
|
|
if comparer == None:
|
|
comparer = self._sort_comparer
|
|
|
|
left = partition_left
|
|
right = partition_right
|
|
pivot = right
|
|
|
|
while left < pivot:
|
|
left_value = self.values[left]
|
|
right_value = self.values[pivot]
|
|
|
|
if comparer.compare(left_value, right_value) >= 0:
|
|
|
|
swapper = self.values[left]
|
|
self.values[left] = self.values[pivot - 1]
|
|
self.values[pivot - 1] = swapper
|
|
|
|
swapper = self.values[pivot - 1]
|
|
self.values[pivot - 1] = self.values[pivot]
|
|
self.values[pivot] = swapper
|
|
|
|
pivot -= 1
|
|
else:
|
|
left += 1
|
|
|
|
if partition_left < pivot:
|
|
self._quicksort(partition_left, pivot - 1, comparer)
|
|
if partition_right > pivot:
|
|
self._quicksort(pivot + 1, partition_right, comparer)
|
|
def _insertionsort(self, comparer: StringComparer = None):
|
|
"""
|
|
Performs an Insertion Sort on the list data. This is best when sorting a nearly-sorted list or when very few values need to be moved.
|
|
"""
|
|
if comparer == None:
|
|
comparer = self._sort_comparer
|
|
|
|
if len(self.values) > 1:
|
|
lbound = 1
|
|
count = len(self.values)
|
|
j: int = 1
|
|
while j <= count - 1:
|
|
k: int = j
|
|
while k >= lbound:
|
|
if comparer.compare(self.values[k-1], self.values[k]) <= 0:
|
|
break
|
|
swapper = self.values[k-1]
|
|
self.values[k-1] = self.values[k]
|
|
self.values[k] = swapper
|
|
k -= 1
|
|
lbound + 1
|
|
j += 1
|
|
def maintain_sort_order(self, lines_changed_count: int = 1, comparer: StringComparer = None):
|
|
"""
|
|
Attempts to make minimal effort to maintain a previously sorted list when small changes are made, or proper full sort when large changes are made or a previously unsorted StringList is suddenly marked as sorted.
|
|
"""
|
|
if self._keep_sorted:
|
|
if lines_changed_count > 50:
|
|
self._quicksort(0, self.count(), comparer)
|
|
else:
|
|
self._insertionsort(comparer)
|
|
def sort(self, comparer: StringComparer = None):
|
|
"""
|
|
Performs a Quicksort on the elements in the StringList.
|
|
"""
|
|
self._quicksort(0, self.count() - 1, comparer)
|
|
def is_sorted(self, comparer: StringComparer = None):
|
|
"""
|
|
Determines if the element in the StringList are currently in order.
|
|
"""
|
|
if comparer == None:
|
|
comparer = self._sort_comparer
|
|
if self.count() > 0:
|
|
for index in range(1, self.count()):
|
|
if comparer.compare(self.values[index-1], self.values[index]) > 0:
|
|
return False
|
|
return True
|
|
else:
|
|
return True
|
|
|
|
def shuffle(self):
|
|
"""
|
|
Randomizes the order of the elements in the StringList.
|
|
"""
|
|
rnd: Random = Random()
|
|
count: int = self.count()
|
|
lbound: int = 0
|
|
ubound: int = count -1
|
|
for index in range(0, count):
|
|
swap_index = rnd.randint(lbound, ubound)
|
|
swapper = self.values[index]
|
|
self.values[index] = self.values[swap_index]
|
|
self.values[swap_index] = swapper
|
|
|
|
def reverse(self):
|
|
"""
|
|
Reverses the order of the elements in the StringList."
|
|
"""
|
|
self.values.reverse()
|
|
self.maintain_sort_order(self.count())
|
|
|
|
def _binary_search(self, text):
|
|
"""
|
|
Performs a binary search on the StringList. This method requires that the list be sorted.
|
|
Otherwise, invalid index could be returned or the algorithm may enter an infinite loop.
|
|
The application guarantees this to be teh case when calling it. If you decide to call it yourself,
|
|
ensure that you have first called sort() or checked that is_sorted() is True.
|
|
"""
|
|
count: int = self.count()
|
|
left: int = 0
|
|
right: int = count - 1
|
|
|
|
if count == 0:
|
|
return -1
|
|
|
|
while (right - left) >= 2:
|
|
pivot: int = int((right - left) /2 + left)
|
|
i: int = self._sort_comparer.compare(text, self.values[pivot])
|
|
|
|
if (i == 0):
|
|
return pivot
|
|
if (i < 0):
|
|
right = pivot - 1
|
|
if (i > 0):
|
|
left = pivot + 1
|
|
|
|
if (right - left) == 1:
|
|
if self._sort_comparer.compare(text, self.values[left]) == 0:
|
|
return left
|
|
if self._sort_comparer.compare(text, self.values[right]) == 0:
|
|
return right
|
|
|
|
return -1
|
|
|
|
def index_of(self, text: str):
|
|
"""
|
|
Returns the 0-based index of the specified string.
|
|
If the list is kept sorted (see "keep_sorted" property), a binary search is performed.
|
|
"""
|
|
if self._keep_sorted:
|
|
index: int = self._binary_search(text)
|
|
return index
|
|
else:
|
|
i: int = 0
|
|
count: int = self.count()
|
|
while i <= count - 1:
|
|
if self._sort_comparer.compare(text, self.values[i]) == 0:
|
|
return i
|
|
i += 1
|
|
return -1
|
|
def swap(self, index_1: int, index_2:int):
|
|
"""
|
|
Exchanges the elements at the two specified indices with each other.
|
|
"""
|
|
if (index_1 != index_2):
|
|
self.values[index_2], self.values[index_1] = self.values[index_1], self.values[index_2]
|
|
return self
|
|
|
|
def select(self, projection: Callable[[int, str], str]):
|
|
"""
|
|
Modifies the StringList to translate each of its strings using the specified projection function.
|
|
"""
|
|
pairs = zip(range(0, self.count()), self.values)
|
|
new_values = [projection(pair[0], pair[1]) for pair in pairs]
|
|
self.values = new_values
|
|
|
|
def where(self, predicate: Callable[[int, str], bool]):
|
|
"""
|
|
Modifies the StringList to contain only strings which pass the specified predicate function.
|
|
"""
|
|
pairs = zip(range(0, self.count()), self.values)
|
|
new_values = [pair[1] for pair in pairs if predicate(pair[0], pair[1])]
|
|
self.values = new_values
|
|
|
|
def skip(self, line_count: int = 1):
|
|
"""
|
|
Modifies the StringList to remove the first line_count elements from the StringList.
|
|
"""
|
|
self.values = self.values[line_count:]
|
|
return self
|
|
def take(self, line_count: int = 1):
|
|
"""
|
|
Modifies the StringList to keep only the first line_count elements from the StringList.
|
|
"""
|
|
self.values = self.values[:line_count]
|
|
return self
|
|
def first(self, default = ""):
|
|
"""
|
|
Returns the first string in the StringList or the value of the "default" parameter if the StringList is empty.
|
|
"""
|
|
if self.count() > 0:
|
|
return self.values[0]
|
|
return default
|
|
def last(self, default=""):
|
|
"""
|
|
Returns the last string in the StringList or the value of the "default" parameter if the StringList is empty.
|
|
"""
|
|
if self.count() > 0:
|
|
return self.values[len(self.values)-1]
|
|
return default
|
|
def distinct(self):
|
|
"""
|
|
Modifies the list to contain only a single instance of each unique string.
|
|
"""
|
|
seen_before = set()
|
|
seen_add = seen_before.add
|
|
new_values = [line for line in self.values if not (line in seen_before or seen_add(line))]
|
|
self.values = new_values
|
|
def union(self, second_list: List[str]):
|
|
"""
|
|
Adds any missing elements from a second list to the StringList.
|
|
"""
|
|
missing_list = [new_line for new_line in second_list if not (new_line in self.values)]
|
|
self.add_strings(missing_list)
|
|
def exclude(self, second_list: List[str]):
|
|
"""
|
|
Removes any elements from the StringList which are found in the second list.
|
|
"""
|
|
new_values = [line for line in self.values if not (line in second_list)]
|
|
self.values = new_values
|
|
def zip(self, second_list: List[str], projection: Callable[[int, str, str], str] = lambda index, left, right: left + right):
|
|
if len(second_list) != self.count():
|
|
raise Exception(f"StringList has {self.count()} elements but zip list has {len(second_list)}.")
|
|
new_list = list(zip(range(0, len(second_list)), zip(self.values, second_list)))
|
|
self.values = [projection(newline[0], newline[1][0], newline[1][1]) for newline in new_list]
|