StringListPy/stringlist/stringlist.py
2025-02-15 20:42:57 -07:00

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]