A powerful string list class for Python, inspired by Delphi's TStringList class.
Go to file
2025-02-15 20:42:57 -07:00
stringlist Initial commit. 2025-02-15 20:42:57 -07:00
stringlist_tests Initial commit. 2025-02-15 20:42:57 -07:00
.editorconfig Initial commit. 2025-02-15 20:42:57 -07:00
.gitignore Initial commit. 2025-02-15 20:42:57 -07:00
LICENSE.txt Initial commit. 2025-02-15 20:42:57 -07:00
pyproject.toml Initial commit. 2025-02-15 20:42:57 -07:00
README.md Initial commit. 2025-02-15 20:42:57 -07:00
runtests.py Initial commit. 2025-02-15 20:42:57 -07:00

StringList

A powerful string list class for Python, inspired by Delphi's TStringList class.

Construction

There are no parameters necessary to create an instance.

list1 = StringList()

It is often useful to initialize a list. Using a fluent approach, you can use the add() or add_strings() methods to to this.

list1 = StringList().add("1")
list2 = StringList().add_strings(["1", "2", "3"])

Optional constructor parameters:

  • sort_comparer: SortComparer: Provides an instance of a SortComparer if overriding the default instance. This is used in sorting and searching (see Sorting and Ordering Data).
  • keep_sorted: bool: Sets the _keep_sorted member (see Sorting and Ordering Data).

Managing Instances

StringList wants to be a wrapper around your list. It is not the star of the show. It gives you a lot of control over instances of it so that you can focus on your data, not your StringList.

These methods are available to manage your instances:

  • assign(): Tells your StringList to adopt the data, settings and behaviors from another StringList. Like an inverse clone().
  • clone(): Tells your StringList to create a new Stringlist which is identical to itself.

Managing Values

The values Property

StringList gives you full access to your list data in the .values property. You have direct read and write access to it. No code inside of StringList requires you to maintain a particular instance of the list object. In other words, no side-effects.

Example of reading and directly accessing the list.

list1 = StringList().add_strings("one", "two", "three")
list1.values.append("four")
del list1.values(0)

Example of directly writing the list.

list2 = ["one", "two", "three"]
list1 = StringList()
list1.values = list2

The Indexer

The StringList defines an indexer. This allows you to read and write the elements of the list by index at the StringList object level.

Example read and write using the indexer.

list1 = StringList().add_strings("a", "b", "c")
print(list1[0])
print(list1[1])
print(list1[2])

list1[0] = "one"
list1[1] = "two"
list1[2] = "three"

print(list1[0])
print(list1[1])
print(list1[2])

Count of Elements

The StringList .count() method returns the number of elements in the StringList. It is a convenience method to provide a more fluent approach than len(StringList.values).

Example of count():

list1 = StringList().add_strings(["one", "two", "three"])

for j in range(0, list1.count()):
	print(f"Item {j}: {list1[j]}")

Adding and Removing Items From the List

The StringList provides these methods to add and remove items from the list.

  • add(): Adds a single string to the end of the list.
  • add_strings(): Adds the contents of a second list to the end of the list.
  • add_string_list(): Adds the contents of a second StringList to the end of the list.
  • insert(): Adds a string to the beginning of the list.
  • insert_range(): Adds the contents of a second list to the beginning of the list.
  • insert_string_list(): Adds the contents of a second list to the beginning of the list.
  • remove(): Removes the string or range of strings at the specified index from the list.
  • clear(): Remove all strings and leaves you with an empty list.

Example of add():

list1 = StringList().add("one")
print(list1.values)

Example of add_strings():

list1 = StringList().add_strings(["one", "two", "three"])
print(list1.values)

Example of add_string_list():

list1 = StringList().add_strings(["one", "two", "three"])
list2 = StringList().add_strings(["four", "five", "six"])
list2.add_string_list(list1)
print(list2.values)

Example of insert():

list1 = StringList().add_strings(["two", "three"])
list1.insert(0, "one")
print(list1.values)

Example of insert_range():

range1 = ["one", "two", "three"]
range2 = ["four", "five", "six"]
list1 = StringList().add_strings(range2)
list1.insert_range(0, range1)
print(list1.values)

Example of insert_string_list():

range1 = ["one", "two", "three"]
range2 = ["four", "five", "six"]
list1 = StringList().add_strings(range1)
list2 = StringList().add_strings(range2)
list2.insert_string_list(0, list1)
print(list2.values)

Example of remove():

list1 = StringList().add_strings(["zero", "one", "two", "three"])
list1.remove(0)
print(list2.values)

Example of clear():

list1 = StringList().add_strings(["zero", "one", "two", "three"])
list1.clear
print(len(list2.values))

Key / Value Stores

StringList has support for treating its contents as key / value pairs.

These simple methods operate on a single string parameter, not on the list itself. They are used to support the list-level methods below.

  • key_of_pair(): Given a delimiter and a key/value pair as a string, extracts the first token (the key of the pair) and returns it. If there is no delimiter found, the whole string is returned.
  • value_of_pair(): Given a delimiter and a key/value pair as a string, extracts the second token (the value of the pair) and returns it. If there is no delimiter found, an empty string is returned.

Here are the key / value methods that can operate on the list itself..

  • value_of_key(): Finds the first key/value pair in the list with the specified key and returns its value.
  • all_keys(): Returns the keys extracted from all the lines in the list without their values.
  • all_values(): Returns the values extracted from all the lines in the list without their keys.
  • all_values_of_key(): Returns the values from every line with the specified key. This assumes that there may be duplicate keys.
  • to_dictionary(): Creates a dictionary from the keys and values found in the list. This assumes there are no duplicate keys. The dictionary key will be the key of the pair. The dictionary value will be the value of the pair.
  • to_indexed_dictionary(): Creates a dictionary with indexed values from the keys and values found in the list. This assumes there are no duplicate keys. The dictionary key will be the key of the pair. The dictionary value will be a tuple containing the list index and the value of the pair.

Example of key_of_pair() and value_of_pair():

list1 = StringList().add_strings(["a=1", "b=2", "c=3"])
should_be_a = list1.key_of_pair(list1[0])
should_be_1 = list1.value_of_pair(list1[0])

# Output should be: ('a', '1')
print((should_be_a, should_be_1))

Example of all_keys():

list1 = StringList().add_strings(["a=1", "b=2", "c=3"])
result = list1.all_keys()

# Output should be: ['a', 'b', 'c']
print(result)

Example of all_values():

list1 = StringList().add_strings(["a=1", "b=2", "c=3"])
result = list1.all_values()

# Output should be: ['1', '2', '3']
print(result)

Example of all_values_of_key():

list1 = StringList().add_strings(["a=1", "a=2", "c=3"])
result = list1.all_values_of_key("a")

# Output should be: ['1', '2']
print(result)

Example of to_dictionary:

list1 = StringList().add_strings(["a=1", "b=2", "c=3"])
table1 = list1.to_dictionary()

# Output should be: {'a': '1', 'b': '2', 'c': '3'}
print(table1)

Example of to_indexed_dictionary:

list1 = StringList().add_strings(["a=1", "b=2", "c=3"])
table1 = list1.to_indexed_dictionary()

# Output should be: {'a': (0, '1'), 'b': (1, '2'), 'c': (2, '3')}
print(table1)

Parsing Text

StringList can split a string by delimiter and load the elements into its list in a single step using the parse() method. It can also do the inverse and return the concatenation of all strings in the list joined by a specified delimiter.

Example of .parse() using the default delimiter ("\n"):

text: str = """
one
two
three
"""

list1 = StringList().parse(text)

# Output should be: ['', 'one', 'two', 'three', '']
print(list1.values)

Example of .parse() using a specified delimiter:

text: str = "one, two, three"
list1 = StringList().parse(text, ",")

# Output should be: ['one', ' two', ' three']
print(list1.values)

Example of .text() with no delimiter:

text: str = "one, two, three"
list1 = StringList().add_strings(["one", "two", "three"])

# Output should be: 'onetwothree'
print(list1.text())

Example of .text() with a specified delimiter:

text: str = "one, two, three"
list1 = StringList().add_strings(["one", "two", "three"])

# Output should be: 'one, two, three'
print(list1.text(", "))

Text Files

For your convenience, StringList can read and write all its content directly to text files with the load_from_file() and save_to_file() methods.

Example using load_from_file():

filename: str = "c:\users\Phil\testfile.txt"
list1 = StringList().load_from_file(filename)
print(list1.values)

Example using load_from_file():

filename: str = "c:\users\Phil\testfile.txt"
list1 = StringList().add_strings([f"Line {i}" for i in range(0, 100)])
list1.save_to_file(filename)

Stacks

StringList has methods that allow you to treat the list as a LIFO stack.

  • push(): Add en element to the top of the stack.
  • pop(): Retrieve and remove the element at the top of the stack.
  • push_bottom(): Add an element to the bottom of the stack (upside-down stack).
  • pop_bottom(): Retrieve and remove the element at the bottom fo the stack (upside-down stack).

Queues

StringList has methods that allow you to treat the list as a FIFO queue.

  • enqueue(): Insert an element into the beginning of the queue.
  • dequeue(): Extract the next available element out of the end of the queue.

Linq-ish Support

For those familiar with .NET's Linq, there are two methods added for convenience:

  • where(): Filters out unwanted strings, keeping only the ones tha pass the provided predicate function.
  • select(): Translates each string using the provided projection function.
  • skip(): Keeps a subset of the string with the first specified number of rows removed.
  • take(): Keeps a subset of the string with only the first specified number of rows kept.
  • first(): Returns the first string in the list.
  • last(): Returns the last string in the list.
  • distinct(): Keeps only a single instance of all the string.
  • union(): Creats a distinct product of the the strings herein and a given second list.
  • exclude(): Removes any strings which also exist in the given second list.
  • zip(): Replaces all strings with a projection combining each string in this list a string in a second given list with the same corresponding index.

Example of where():

list1 = StringList().add_strings([f"{i}" for i in range(0, 5)])
list1.where(lambda index, line: index % 2 == 0)

# Output should be: ['0', '2', '4']
print(list1.values)

Example of select():

squares = StringList().add_strings([f"{i}" for i in range(0, 5)])
squares.select(lambda index, line: f"{int(line)} x {int(line)} = {str(int(line) * int(line))}")

# Output should be: ['0 x 0 = 0', '1 x 1 = 1', '2 x 2 = 4', '3 x 3 = 9', '4 x 4 = 16']
print(squares.values)

Sorting and Ordering Data

Sorting

You can use the sort() method to sort the list at any time.

Example of sort on demand:

rnd = Random()
list1 = StringList() \
	.add_strings([f"Line {rnd.randint(0, 100)}" for i in range(0, 5)])
list1.sort()

Other Ways To Control Order

  • You can use the is_sorted() method to check if the list is already sorted.
  • You can use the shuffle() method to randomize the order of the data in the list.
  • You can use the reverse() method to reverse the order of the data in the list.
  • You can use the swap() method to transpose two elements in the list.

Example of is_sorted():

list1 = StringList().add_strings(["1", "2", "0"])

# Output should be: False
print(list1.is_sorted())

list1.sort()

# Output should be: True
print(list1.is_sorted())

Example of shuffle():

list1 = StringList().add_strings([f"{chr(i + 65)}" for i in range(0, 5)])
# Output should be: ['A', 'B', 'C', 'D', 'E']
print(list1.values)

# Output should be: True
print(list1.is_sorted())

list1.shuffle()

# Output should be: False
print(list1.is_sorted())

# Output should be five upper-case letters in random order.
print(list1.values)

Example of reverse():

list1 = StringList().add_strings([f"{chr(i + 65)}" for i in range(0, 5)])
# Output should be: ['A', 'B', 'C', 'D', 'E']
print(list1.values)

list1.reverse()

# Output should be: ['E', 'D', 'C', 'B', 'A']
print(list1.values)

Example of swap():

list1 = StringList().add_strings([f"{chr(i + 65)}" for i in range(0, 5)])
# Output should be: ['A', 'B', 'C', 'D', 'E']
print(list1.values)

list1.swap(1, 3)

# Output should be: ['A', 'D', 'C', 'B', 'E']
print(list1.values)

StringComparer

Sorts and searches use a StringComparer object to compare strings. StringList uses a simple lexicographical string comparer by default. You can override it by providing your own instance of a StringComparer and passing it to the StringList constructor or as a parameter to any of the sort-related methods.

The following StringComparer classes are provided:

  • StringComparer: A basic lexicographical comparer. This is the default object used.
  • AnonymousComparer: Allows you to specify the comparison function as a lambda expression so you don't have to create your own class to override the StringComparer.
  • KeyStringComparer: A class which lets you provide a lambda expression to transforming the string prior to comparison. This is similar to the sort of comparison used when calling the sort method on a list.

Example of a sort using a custom numeric sort comparison:

# When sorting lexicographically, this list is not considered sorted because the string "10" 
# is considered to preceed the string "9", even though the number 9 preceeds the number 10.
# If we use a custom StringComparer that compares the values numerically, it will then be
# considered sorted.

list1 = StringList() \
	.add_strings([f"{i}" for i in range(0, 15)])
comparer: StringComparer = AnonymousStringComparer(lambda left, right: int(left) - int(right))

list1.sort(comparer)
print(list1.values)

Maintaining Constant Sort Order

The StringList class is capable of maintaining sorted content at all times. This behavior is turned off by default. To turn it on or off, use the keep_sorted property. You can write to it at any time or specify its value as a parameter in the StringList constructor.

When the keep_sorted property is set to True, StringList will operate as a Maintained List. That is, a list that is sorted at all times. If there is already data in the StringList, it will be immediately sorted when this property is set to True.

The Maintained List behavior is more efficient than constantly sorting the list. It is aware of the scope of the changes for a given operation and uses a secondary Insertion Sort algorithm on almost-sorted data and uses a primary QuickSort algorithm for larger or unordered data. If you plan to modify sorted data and use it along the way, it makese sense to use a Maintained List. If you plan to build up the data all at once before using it, it makes more sense to sort it once with sort().

Example of a Maintained list:

list1 = StringList(keep_sorted=True)
list1.add("This will be the last line.")
list1.add("Adding this will go to the beginning of the list, not the end.")

Binary searches will be performed against Maintained Lists.

list1 = StringList(keep_sorted=True) \
	.add_strings([f"Line {i}" for i in range(0, 5)])

binary_searched_index: int = list1.index_of("Line 55")
found: bool = binary_searched_index != -1

Order Disruption

Using the keep_sorted feature will disrupt behavior of many functions. Where you would normally expect these functions to modify the list by changing elements are certain locations, those locations will be affected by the sorting. Here are some examples of functions whose behaviors will be affected by sorting:

  • .add()
  • .add_strings()
  • .add_string_list()
  • .push()
  • .enqueue()

Searching

The index_of() method will search for a value in the list. If the list is a Maintained List, a binary search will be performed. Otherwise, a linear search is performed. If the value is not found, the method returns -1.

Exmaple index_of():

sorted_list = StringList(keep_sorted=True).add_strings(["a", "b", "c"])
unsorted_list = StringList(keep_sorted=False).add_strings(["a", "b", "c"])

binary_search_result = sorted_list.index_of("b")
linear_search_result = unsorted_list.index_of("b")
unfound_binary_search_result = sorted_list.index_of("z")
unfound_linear_search_result = unsorted_list.index_of("z")

# Output should be: [1, 1, -1, -1]
print([
	binary_search_result,
	linear_search_result,
	unfound_binary_search_result,
	unfound_linear_search_result
])

Unit Tests

This StringList class is fully unit-tested. You can run the unit tests as a module using the universal approach like this:

python -m unittest -v

This project is also set up to run the unit tests as an application. You can run the unit test application like this:

python runtests.py -v