Initial commit.
This commit is contained in:
commit
13b4044907
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@ -0,0 +1,21 @@
|
||||
root=true
|
||||
|
||||
[*.cs]
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
|
||||
[*.xml]
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
|
||||
[*.py]
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
|
||||
[{*.yml, *.yaml}]
|
||||
tab_width = 2
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
177
.gitignore
vendored
Normal file
177
.gitignore
vendored
Normal file
@ -0,0 +1,177 @@
|
||||
# My custom ignores
|
||||
.vscode/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) [year] [fullname]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
503
README.md
Normal file
503
README.md
Normal file
@ -0,0 +1,503 @@
|
||||
# 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
|
||||
```
|
||||
20
pyproject.toml
Normal file
20
pyproject.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "stringlist"
|
||||
version = "0.0.1"
|
||||
authors = [{ name = "Phil Gilmore", email="spgilmore+pypi@gmail.com"}]
|
||||
description = "A powerful string list class for Python, inspired by Delphi's TStringList class."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.6"
|
||||
classifiers = [
|
||||
"Programming Language :: Python",
|
||||
"Natural Language :: English",
|
||||
"License :: OSI Approved :: MIT License"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://git.pillidar.com/PillidarPublic/StringListPy"
|
||||
Issues = "https://git.pillidar.com/PillidarPublic/StringListPy/issues"
|
||||
5
runtests.py
Normal file
5
runtests.py
Normal file
@ -0,0 +1,5 @@
|
||||
import unittest
|
||||
from stringlist_tests.test_stringlist import *
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
13
stringlist/__init__.py
Normal file
13
stringlist/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
from .stringlist import StringList
|
||||
from .stringlist import KeyNotFoundException
|
||||
from .stringlist import StringComparer
|
||||
from .stringlist import AnonymousStringComparer
|
||||
from .stringlist import KeyStringComparer
|
||||
|
||||
__all__ = [
|
||||
"StringList",
|
||||
"KeyNotFoundException",
|
||||
"StringComparer",
|
||||
"AnonymousStringComparer",
|
||||
"KeyStringComparer"
|
||||
]
|
||||
525
stringlist/stringlist.py
Normal file
525
stringlist/stringlist.py
Normal file
@ -0,0 +1,525 @@
|
||||
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]
|
||||
0
stringlist_tests/__init__.py
Normal file
0
stringlist_tests/__init__.py
Normal file
832
stringlist_tests/test_stringlist.py
Normal file
832
stringlist_tests/test_stringlist.py
Normal file
@ -0,0 +1,832 @@
|
||||
import unittest
|
||||
from os import path, remove, environ
|
||||
from typing_extensions import List
|
||||
from stringlist import *
|
||||
|
||||
class StringListTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test_can_construct(self):
|
||||
list = StringList()
|
||||
self.assertIsNotNone(list)
|
||||
self.assertIs(list.count(), 0)
|
||||
|
||||
def test_count_works(self):
|
||||
list = StringList()
|
||||
expected = 0
|
||||
actual = list.count()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
list.values.extend([1, 2, 3])
|
||||
|
||||
expected = 3
|
||||
actual = list.count()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_clear_works(self):
|
||||
list = StringList()
|
||||
list.values.extend([1, 2, 3])
|
||||
self.assertNotEqual(len(list.values), 0)
|
||||
|
||||
list.clear()
|
||||
|
||||
self.assertEqual(len(list.values), 0)
|
||||
|
||||
def test_add_works(self):
|
||||
list: StringList = StringList()
|
||||
expected: List[str] = ["abc", "123"]
|
||||
|
||||
list.add(expected[0])
|
||||
list.add(expected[1])
|
||||
|
||||
actual: List[str] = ["abc", "123"]
|
||||
self.assertIsNotNone(list.values)
|
||||
self.assertListEqual(expected, actual)
|
||||
|
||||
def test_save_and_load_from_file(self):
|
||||
userprofile_path = environ.get("userprofile")
|
||||
if not path.exists(userprofile_path):
|
||||
raise Exception(f"Cannot find path ({userprofile_path})")
|
||||
filename: str = path.abspath(path.join(userprofile_path, "testfile.txt"))
|
||||
if path.exists(filename):
|
||||
raise Exception(f"Test file already exists: ({filename})")
|
||||
|
||||
expected_lines: str = ["There once", "was a man", "from Kantuckeh"]
|
||||
list1: StringList = StringList().add_strings(expected_lines)
|
||||
list1.save_to_file(filename)
|
||||
try:
|
||||
list2: StringList = StringList()
|
||||
list2.load_from_file(filename)
|
||||
finally:
|
||||
remove(filename)
|
||||
|
||||
#self.assertListEqual(list1.values, list2.values)
|
||||
self.assertEqual(list2.count(), len(expected_lines))
|
||||
self.assertEqual(list2.count(), 3)
|
||||
self.assertEqual(list2.values[0], expected_lines[0]+"\n")
|
||||
self.assertEqual(list2.values[1], expected_lines[1]+"\n")
|
||||
self.assertEqual(list2.values[2], expected_lines[2]+"\n")
|
||||
|
||||
def test_strip_line_endings_after_load_from_file(self):
|
||||
userprofile_path = environ.get("userprofile")
|
||||
if not path.exists(userprofile_path):
|
||||
raise Exception(f"Cannot find path ({userprofile_path})")
|
||||
filename: str = path.abspath(path.join(userprofile_path, "testfile.txt"))
|
||||
if path.exists(filename):
|
||||
raise Exception(f"Test file already exists: ({filename})")
|
||||
|
||||
expected_lines: str = ["There once", "was a man", "from Kantuckeh"]
|
||||
list1: StringList = StringList().add_strings(expected_lines)
|
||||
list1.save_to_file(filename)
|
||||
try:
|
||||
list2: StringList = StringList()
|
||||
list2.load_from_file(filename)
|
||||
finally:
|
||||
remove(filename)
|
||||
|
||||
list2.strip_line_endings()
|
||||
|
||||
self.assertListEqual(list1.values, list2.values)
|
||||
|
||||
def test_remove_with_single_item(self):
|
||||
before = list(range(0, 10))
|
||||
list1: StringList = StringList() \
|
||||
.add_strings(before) \
|
||||
.remove(4)
|
||||
expected = [0, 1, 2, 3, 5, 6, 7, 8, 9]
|
||||
actual = list1.values
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_remove_first_item(self):
|
||||
before = list(range(0, 10))
|
||||
list1: StringList = StringList() \
|
||||
.add_strings(before) \
|
||||
.remove(0)
|
||||
expected = [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
actual = list1.values
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_remove_last_item(self):
|
||||
before = list(range(0, 10))
|
||||
list1: StringList = StringList() \
|
||||
.add_strings(before) \
|
||||
.remove(9)
|
||||
expected = [0, 1, 2, 3, 4, 5, 6, 7, 8]
|
||||
actual = list1.values
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_remove_first_three_items(self):
|
||||
before = list(range(0, 10))
|
||||
list1: StringList = StringList() \
|
||||
.add_strings(before) \
|
||||
.remove(0, 3)
|
||||
expected = [3, 4, 5, 6, 7, 8, 9]
|
||||
actual = list1.values
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_remove_last_three_items(self):
|
||||
before = list(range(0, 10))
|
||||
list1: StringList = StringList() \
|
||||
.add_strings(before) \
|
||||
.remove(7, 3)
|
||||
expected = [0, 1, 2, 3, 4, 5, 6]
|
||||
actual = list1.values
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_remove_middle_three_items(self):
|
||||
before = list(range(0, 10))
|
||||
list1: StringList = StringList() \
|
||||
.add_strings(before) \
|
||||
.remove(4, 3)
|
||||
expected = [0, 1, 2, 3, 7, 8, 9]
|
||||
actual = list1.values
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_remove_before_first_item(self):
|
||||
before = list(range(0, 10))
|
||||
|
||||
list1: StringList = StringList() \
|
||||
.add_strings(before) \
|
||||
.remove(-1, 3)
|
||||
|
||||
# This is what actually happens.
|
||||
expected = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
# This is what I REALLY wanted to happen. :/
|
||||
#expected = [2, 3, 4, 5, 6, 7, 8, 9]
|
||||
actual = list1.values
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_remove_beyond_last_item(self):
|
||||
before = list(range(0, 10))
|
||||
|
||||
list1: StringList = StringList() \
|
||||
.add_strings(before) \
|
||||
.remove(9, 3)
|
||||
|
||||
#expected = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
expected = [0, 1, 2, 3, 4, 5, 6, 7, 8]
|
||||
actual = list1.values
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_parse_empty(self):
|
||||
list1: StringList = StringList()
|
||||
|
||||
list1.parse("")
|
||||
|
||||
self.assertEqual(1, list1.count())
|
||||
|
||||
def test_parse_single_token(self):
|
||||
list1: StringList = StringList()
|
||||
input = "A monkey's your uncle."
|
||||
expected = [input]
|
||||
|
||||
list1.parse(input, ",")
|
||||
|
||||
actual = list1.values
|
||||
self.assertEqual(len(actual), 1)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_parse_multiple_tokens(self):
|
||||
list1: StringList = StringList()
|
||||
input = "A monkey's your uncle."
|
||||
expected = ["A", "monkey's", "your", "uncle."]
|
||||
|
||||
list1.parse(input, " ")
|
||||
|
||||
actual = list1.values
|
||||
self.assertListEqual(expected, actual)
|
||||
self.assertEqual(4, len(actual))
|
||||
|
||||
def test_text_empty(self):
|
||||
list1: StringList = StringList()
|
||||
list1.clear()
|
||||
expected = ""
|
||||
actual = list1.text()
|
||||
self.assertEqual(expected, actual)
|
||||
self.assertEqual(0, len(actual))
|
||||
|
||||
def test_text_single_value(self):
|
||||
list1: StringList = StringList()
|
||||
input = "A monkey's your uncle."
|
||||
list1.add(input)
|
||||
expected = input
|
||||
actual = list1.text()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_text_multiple_values(self):
|
||||
list1: StringList = StringList()
|
||||
input = ["A", "monkey's", "your", "uncle."]
|
||||
list1.add_strings(input)
|
||||
expected = "A monkey's your uncle."
|
||||
|
||||
actual = list1.text(" ")
|
||||
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_value_of_missing_key_is_handled(self):
|
||||
list1: StringList = StringList()
|
||||
list1.add("key=value")
|
||||
|
||||
encountered_expected_exception: bool = False
|
||||
try:
|
||||
list1.value_of_key("missing_key")
|
||||
except KeyNotFoundException as e:
|
||||
encountered_expected_exception = True
|
||||
|
||||
self.assertTrue(encountered_expected_exception)
|
||||
|
||||
def test_contains_key_missing_on_list_size_of_1(self):
|
||||
list1: StringList = StringList()
|
||||
list1.add("key=value")
|
||||
actual: bool = list1.contains_key("missing")
|
||||
self.assertFalse(actual)
|
||||
|
||||
def test_contains_key_missing_on_list_size_of_10(self):
|
||||
list1: StringList = StringList()
|
||||
list1.add_strings([f"key{i}=value{i}" for i in range(0, 10)])
|
||||
actual: bool = list1.contains_key("missing")
|
||||
self.assertFalse(actual)
|
||||
|
||||
def test_contains_key_missing_on_empty_list(self):
|
||||
list1: StringList = StringList()
|
||||
actual: bool = list1.contains_key("missing")
|
||||
self.assertFalse(actual)
|
||||
|
||||
def test_contains_key_present_on_list_size_of_1(self):
|
||||
list1: StringList = StringList()
|
||||
list1.add("key=value")
|
||||
actual: bool = list1.contains_key("key")
|
||||
self.assertTrue(actual)
|
||||
|
||||
def test_contains_key_present_on_list_size_of_10(self):
|
||||
list1: StringList = StringList()
|
||||
list1.add_strings([f"key{i}=value{i}" for i in range(0, 10)])
|
||||
actual: bool = list1.contains_key("key1")
|
||||
self.assertTrue(actual)
|
||||
|
||||
def test_value_of_key(self):
|
||||
list1: StringList = StringList()
|
||||
list1.add("key=value")
|
||||
expected = "value"
|
||||
|
||||
actual = list1.value_of_key("key")
|
||||
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_all_keys_empty(self):
|
||||
list1: StringList = StringList()
|
||||
expected = []
|
||||
|
||||
actual = list1.all_keys()
|
||||
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_all_keys(self):
|
||||
list1: StringList = StringList()
|
||||
list1.add_strings([f"key{i}=value{i}" for i in range(0, 3)])
|
||||
expected = ["key0", "key1", "key2"]
|
||||
|
||||
actual = list1.all_keys()
|
||||
|
||||
self.assertListEqual(expected, actual)
|
||||
|
||||
def test_all_values_of_key(self):
|
||||
list1: StringList = StringList()
|
||||
list1.add_strings([f"key{i%2}=value{i}" for i in range(0, 10)])
|
||||
expected = ["value0", "value2", "value4", "value6", "value8"]
|
||||
|
||||
actual = list1.all_values_of_key("key0")
|
||||
|
||||
self.assertListEqual(expected, actual)
|
||||
|
||||
def test_assign(self):
|
||||
list1: StringList = StringList().add_strings([f"string {i}" for i in range(0, 3)])
|
||||
list2: StringList = StringList()
|
||||
list2.assign(list1)
|
||||
self.assertListEqual(list1.values, list2.values)
|
||||
|
||||
def test_assign_empty(self):
|
||||
list1: StringList = StringList()
|
||||
list2: StringList = StringList().add_strings([f"string {i}" for i in range(0, 3)])
|
||||
expected = []
|
||||
|
||||
list2.assign(list1)
|
||||
|
||||
actual = list2.values
|
||||
self.assertListEqual(expected, actual)
|
||||
|
||||
def test_add_strings(self):
|
||||
list1: StringList = StringList().add_strings([f"string {i}" for i in range(0, 3)])
|
||||
list2: StringList = StringList().add_strings(["a", "b", "c"])
|
||||
expected = ["a", "b", "c", "string 0", "string 1", "string 2"]
|
||||
|
||||
list2.add_string_list(list1)
|
||||
|
||||
actual = list2.values
|
||||
self.assertEqual(6, list2.count())
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_add_strings_empty(self):
|
||||
list1: StringList = StringList()
|
||||
list2: StringList = StringList().add_strings(["a", "b", "c"])
|
||||
expected = ["a", "b", "c"]
|
||||
|
||||
list2.add_string_list(list1)
|
||||
|
||||
actual = list2.values
|
||||
self.assertEqual(3, list2.count())
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_clone_empty(self):
|
||||
expected = []
|
||||
list1: StringList = StringList()
|
||||
list2: StringList = list1.clone()
|
||||
|
||||
actual = list2.values
|
||||
self.assertEqual(expected, actual)
|
||||
self.assertEqual(0, list2.count())
|
||||
|
||||
def test_clone(self):
|
||||
expected = ["a", "b", "c"]
|
||||
list1: StringList = StringList().add_strings(expected)
|
||||
|
||||
list2: StringList = list1.clone()
|
||||
|
||||
actual = list2.values
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_push(self):
|
||||
list1: StringList = StringList()
|
||||
self.assertEqual([], list1.values)
|
||||
|
||||
list1.push("a")
|
||||
self.assertEqual(["a"], list1.values)
|
||||
list1.push("b")
|
||||
self.assertEqual(["a", "b"], list1.values)
|
||||
list1.push("c")
|
||||
self.assertEqual(["a", "b", "c"], list1.values)
|
||||
|
||||
def test_pop(self):
|
||||
list1: StringList = StringList() \
|
||||
.push("a") \
|
||||
.push("b") \
|
||||
.push("c")
|
||||
|
||||
value1 = list1.pop()
|
||||
value2 = list1.pop()
|
||||
value3 = list1.pop()
|
||||
|
||||
self.assertEqual(0, list1.count())
|
||||
self.assertEqual(["c", "b", "a"], [value1, value2, value3])
|
||||
|
||||
def test_enqueue(self):
|
||||
list1: StringList = StringList()
|
||||
self.assertEqual([], list1.values)
|
||||
|
||||
list1.enqueue("a")
|
||||
self.assertEqual(["a"], list1.values)
|
||||
list1.enqueue("b")
|
||||
self.assertEqual(["b", "a"], list1.values)
|
||||
list1.enqueue("c")
|
||||
self.assertEqual(["c", "b", "a"], list1.values)
|
||||
|
||||
def test_dequeue(self):
|
||||
list1: StringList = StringList() \
|
||||
.push("c") \
|
||||
.push("b") \
|
||||
.push("a")
|
||||
|
||||
value1 = list1.dequeue()
|
||||
value2 = list1.dequeue()
|
||||
value3 = list1.dequeue()
|
||||
|
||||
self.assertEqual(0, list1.count())
|
||||
self.assertEqual(["a", "b", "c"], [value1, value2, value3])
|
||||
|
||||
def test_keep_sorted_property(self):
|
||||
list1: StringList = StringList()
|
||||
self.assertEqual(False, list1.keep_sorted)
|
||||
|
||||
list1.keep_sorted = True
|
||||
self.assertEqual(True, list1.keep_sorted)
|
||||
|
||||
def test_keep_sorted_property(self):
|
||||
list1: StringList = StringList()
|
||||
self.assertEqual(False, list1.keep_sorted)
|
||||
|
||||
list1.keep_sorted = True
|
||||
self.assertEqual(True, list1.keep_sorted)
|
||||
|
||||
def test_insertion_sort(self):
|
||||
list1: StringList = StringList()
|
||||
list1.keep_sorted = False
|
||||
list1.values = [0, 1, 2, 3, 4, 5]
|
||||
self.assertTrue(list1.is_sorted())
|
||||
list1.reverse()
|
||||
self.assertFalse(list1.is_sorted())
|
||||
list1._insertionsort()
|
||||
self.assertTrue(list1.is_sorted())
|
||||
|
||||
def test_reverse(self):
|
||||
list1: StringList = StringList().add_strings([f"{i}" for i in range(0, 3)])
|
||||
expected = ["2", "1", "0"]
|
||||
|
||||
list1.reverse()
|
||||
actual = list1.values
|
||||
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_is_sorted(self):
|
||||
list1: StringList = StringList()
|
||||
list1.keep_sorted = False
|
||||
sorted1: bool = list1.is_sorted(list1._sort_comparer)
|
||||
|
||||
list1.add_strings([f"{chr(i+65)}" for i in range(0, 25)])
|
||||
sorted2: bool = list1.is_sorted()
|
||||
|
||||
list1.reverse()
|
||||
sorted3: bool = list1.is_sorted()
|
||||
|
||||
self.assertEqual(True, sorted1)
|
||||
self.assertEqual(True, sorted2)
|
||||
self.assertEqual(False, sorted3)
|
||||
|
||||
def test_shuffle(self):
|
||||
list1: StringList = StringList()
|
||||
list1.keep_sorted = False
|
||||
|
||||
ordered_list: List[str] = [f"{chr(i+65)}" for i in range(0, 25)]
|
||||
list1.add_strings(ordered_list)
|
||||
not_expected = ordered_list
|
||||
|
||||
list1.shuffle()
|
||||
actual = list1.values
|
||||
|
||||
self.assertNotEqual(not_expected, actual)
|
||||
|
||||
def test_sort(self):
|
||||
list1: StringList = StringList()
|
||||
list1.keep_sorted = False
|
||||
list1.add_strings([f"{i}" for i in range(0, 10000)])
|
||||
list1.shuffle()
|
||||
|
||||
list1._quicksort(0, list1.count() - 1)
|
||||
|
||||
self.assertTrue(list1.is_sorted())
|
||||
|
||||
def test_numeric_comparer(self):
|
||||
list1 = StringList().add_strings([f"{i}" for i in range(0, 15)])
|
||||
comparer: StringComparer = AnonymousStringComparer(lambda left, right: int(left) - int(right))
|
||||
expected = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14"]
|
||||
list1.sort(comparer)
|
||||
|
||||
actual = list1.values
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_lexical_comparer(self):
|
||||
list1 = StringList().add_strings([f"{i}" for i in range(0, 15)])
|
||||
expected = ["0", "1", "10", "11", "12", "13", "14", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||
list1.sort()
|
||||
|
||||
actual = list1.values
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_binary_search_present_0_to_99(self):
|
||||
list1 = StringList().add_strings([f"{i:03}" for i in range(0, 100)])
|
||||
list1.keep_sorted = True
|
||||
|
||||
for j in range(0, 100):
|
||||
i: int = list1.index_of(str(f"{j:03}"))
|
||||
self.assertEqual(j, i, f"{j}")
|
||||
|
||||
def test_binary_search_present_0_to_98(self):
|
||||
list1 = StringList().add_strings([f"{i:03}" for i in range(0, 99)])
|
||||
list1.keep_sorted = True
|
||||
|
||||
for j in range(0, 99):
|
||||
i: int = list1.index_of(str(f"{j:03}"))
|
||||
self.assertEqual(j, i, f"{j}")
|
||||
|
||||
def test_binary_search_present_0_to_2(self):
|
||||
list1 = StringList().add_strings([f"{i:03}" for i in range(0, 2)])
|
||||
list1.keep_sorted = True
|
||||
|
||||
for j in range(0, 2):
|
||||
i: int = list1.index_of(str(f"{j:03}"))
|
||||
self.assertEqual(j, i, f"{j}")
|
||||
|
||||
def test_binary_search_present_0_to_1(self):
|
||||
list1 = StringList().add_strings([f"{i:03}" for i in range(0, 1)])
|
||||
list1.keep_sorted = True
|
||||
|
||||
for j in range(0, 1):
|
||||
i: int = list1.index_of(str(f"{j:03}"))
|
||||
self.assertEqual(j, i, f"{j}")
|
||||
|
||||
def test_binary_search_present_single_element(self):
|
||||
list1 = StringList().add_strings([f"{i:03}" for i in range(0, 0)])
|
||||
list1.keep_sorted = True
|
||||
|
||||
for j in range(0, 0):
|
||||
i: int = list1.index_of(str(f"{j:03}"))
|
||||
self.assertEqual(j, i, f"{j}")
|
||||
|
||||
def test_binary_search_missing_empty(self):
|
||||
list1 = StringList()
|
||||
list1.keep_sorted = True
|
||||
|
||||
i: int = list1.index_of("missing")
|
||||
self.assertEqual(-1, i)
|
||||
|
||||
def test_binary_search_missing_0_to_99(self):
|
||||
list1 = StringList().add_strings([f"{i:03}" for i in range(0, 100)])
|
||||
list1.keep_sorted = True
|
||||
|
||||
for j in range(0, 100):
|
||||
i: int = list1.index_of(f"{j:03}5")
|
||||
self.assertEqual(-1, i, f"{j}")
|
||||
|
||||
def test_binary_search_missing_0_to_98(self):
|
||||
list1 = StringList().add_strings([f"{i:03}" for i in range(0, 99)])
|
||||
list1.keep_sorted = True
|
||||
|
||||
for j in range(0, 99):
|
||||
i: int = list1.index_of(f"{j:03}5")
|
||||
self.assertEqual(-1, i, f"{j}")
|
||||
|
||||
def test_binary_search_missing_0_to_2(self):
|
||||
list1 = StringList().add_strings([f"{i:03}" for i in range(0, 2)])
|
||||
list1.keep_sorted = True
|
||||
|
||||
for j in range(0, 2):
|
||||
i: int = list1.index_of(f"{j:03}5")
|
||||
self.assertEqual(-1, i, f"{j}")
|
||||
|
||||
def test_binary_search_missing_0_to_1(self):
|
||||
list1 = StringList().add_strings([f"{i:03}" for i in range(0, 1)])
|
||||
list1.keep_sorted = True
|
||||
|
||||
for j in range(0, 1):
|
||||
i: int = list1.index_of(f"{j:03}5")
|
||||
self.assertEqual(-1, i, f"{j}")
|
||||
|
||||
def test_binary_search_missing_single_element(self):
|
||||
list1 = StringList().add_strings([f"{i:03}" for i in range(0, 0)])
|
||||
list1.keep_sorted = True
|
||||
|
||||
for j in range(0, 0):
|
||||
i: int = list1.index_of(f"{j:03}5")
|
||||
self.assertEqual(-1, i, f"{j}")
|
||||
|
||||
def test_linear_search_empty(self):
|
||||
list1 = StringList()
|
||||
|
||||
self.assertEqual(-1, list1.index_of(""))
|
||||
self.assertEqual(-1, list1.index_of("missing"))
|
||||
|
||||
def test_linear_search_one_element(self):
|
||||
list1 = StringList().add("1")
|
||||
|
||||
self.assertEqual(-1, list1.index_of(""))
|
||||
self.assertEqual(-1, list1.index_of("missing"))
|
||||
self.assertEqual(0, list1.index_of("1"))
|
||||
|
||||
def test_linear_search_five_elements(self):
|
||||
list1 = StringList().add_strings([f"{i}" for i in range(0, 5)])
|
||||
|
||||
self.assertEqual(-1, list1.index_of(""))
|
||||
self.assertEqual(-1, list1.index_of("missing"))
|
||||
|
||||
for j in range(0, 5):
|
||||
self.assertEqual(j, list1.index_of(str(j)), f"{j}")
|
||||
|
||||
def test_swap(self):
|
||||
list1 = StringList().add_strings([f"{i}" for i in range(0, 5)])
|
||||
|
||||
list1.swap(1, 3)
|
||||
|
||||
expected = ["0", "3", "2", "1", "4"]
|
||||
actual = list1.values
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_to_dictionary(self):
|
||||
list1 = StringList().add_strings([f"{i+1}={chr(i + 65)}" for i in range(0, 5)])
|
||||
table1 = list1.to_dictionary()
|
||||
|
||||
self.assertEqual(5, len(table1))
|
||||
self.assertEqual(table1["1"], "A")
|
||||
self.assertEqual(table1["2"], "B")
|
||||
self.assertEqual(table1["3"], "C")
|
||||
self.assertEqual(table1["4"], "D")
|
||||
self.assertEqual(table1["5"], "E")
|
||||
|
||||
def test_to_indexed_dictionary(self):
|
||||
list1 = StringList().add_strings([f"{i+1}={chr(i + 65)}" for i in range(0, 5)])
|
||||
table1 = list1.to_indexed_dictionary()
|
||||
|
||||
self.assertEqual(5, len(table1))
|
||||
self.assertEqual(table1["1"], (0, "A"))
|
||||
self.assertEqual(table1["2"], (1, "B"))
|
||||
self.assertEqual(table1["3"], (2, "C"))
|
||||
self.assertEqual(table1["4"], (3, "D"))
|
||||
self.assertEqual(table1["5"], (4, "E"))
|
||||
|
||||
def test_indexer_read(self):
|
||||
list1 = StringList().add_strings([f"{i}" for i in range(0, 5)])
|
||||
|
||||
for j in range(0, 5):
|
||||
expected = str(j)
|
||||
actual = list1[j]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_indexer_write(self):
|
||||
values = [f"{i}" for i in range(0, 5)]
|
||||
list1 = StringList().add_strings(["", "", "", "", ""])
|
||||
|
||||
for j in range(0, 5):
|
||||
expected = str(values[j])
|
||||
list1[j] = expected
|
||||
actual = list1.values[j]
|
||||
self.assertNotEqual("", actual)
|
||||
self.assertEqual(expected, actual, f"{j}")
|
||||
|
||||
def test_maintain_sort_add(self):
|
||||
list1 = StringList(keep_sorted=True)
|
||||
self.assertTrue(list1.is_sorted())
|
||||
|
||||
list1.add("c")
|
||||
self.assertTrue(list1.is_sorted())
|
||||
list1.add("b")
|
||||
self.assertTrue(list1.is_sorted())
|
||||
list1.add("a")
|
||||
self.assertTrue(list1.is_sorted())
|
||||
|
||||
def test_maintain_sort_add_range(self):
|
||||
list1 = StringList(keep_sorted=True)
|
||||
list1.add_strings(["x", "y", "z"])
|
||||
self.assertTrue(list1.is_sorted())
|
||||
|
||||
list1.add_strings(["c", "b", "a"])
|
||||
self.assertTrue(list1.is_sorted())
|
||||
|
||||
def test_maintain_sort_enqueue(self):
|
||||
list1 = StringList(keep_sorted=True)
|
||||
self.assertTrue(list1.is_sorted())
|
||||
|
||||
list1.enqueue("a")
|
||||
self.assertTrue(list1.is_sorted())
|
||||
list1.enqueue("b")
|
||||
self.assertTrue(list1.is_sorted())
|
||||
list1.enqueue("c")
|
||||
self.assertTrue(list1.is_sorted())
|
||||
|
||||
def test_maintain_sort_push(self):
|
||||
list1 = StringList(keep_sorted=True)
|
||||
self.assertTrue(list1.is_sorted())
|
||||
|
||||
list1.push("c")
|
||||
self.assertTrue(list1.is_sorted())
|
||||
list1.push("b")
|
||||
self.assertTrue(list1.is_sorted())
|
||||
list1.push("a")
|
||||
self.assertTrue(list1.is_sorted())
|
||||
|
||||
def test_stack(self):
|
||||
list1 = StringList()
|
||||
|
||||
list1.push("1")
|
||||
self.assertListEqual(["1"], list1.values)
|
||||
list1.push("2")
|
||||
self.assertListEqual(["1", "2"], list1.values)
|
||||
|
||||
actual = list1.pop()
|
||||
self.assertEqual("2", actual)
|
||||
self.assertListEqual(["1"], list1.values)
|
||||
actual = list1.pop()
|
||||
self.assertEqual("1", actual)
|
||||
self.assertListEqual([], list1.values)
|
||||
|
||||
def test_bottom_stack(self):
|
||||
list1 = StringList()
|
||||
|
||||
list1.push_bottom("1")
|
||||
self.assertListEqual(["1"], list1.values)
|
||||
list1.push_bottom("2")
|
||||
self.assertListEqual(["2", "1"], list1.values)
|
||||
|
||||
actual = list1.pop_bottom()
|
||||
self.assertEqual("2", actual)
|
||||
self.assertListEqual(["1"], list1.values)
|
||||
actual = list1.pop_bottom()
|
||||
self.assertEqual("1", actual)
|
||||
self.assertListEqual([], list1.values)
|
||||
|
||||
def test_where(self):
|
||||
list1 = StringList().add_strings([f"{i}" for i in range(0, 10)])
|
||||
|
||||
list1.where(lambda index, line: index % 2 == 0)
|
||||
|
||||
self.assertListEqual(["0", "2", "4", "6", "8"], list1.values)
|
||||
|
||||
def test_select(self):
|
||||
base_values = [f"{i}" for i in range(0, 5)]
|
||||
list1 = StringList().add_strings(base_values)
|
||||
|
||||
list1.select(lambda index, line: str(int(line) + 1))
|
||||
|
||||
expected = [f"{i+1}" for i in range(0, 5)]
|
||||
actual = list1.values
|
||||
self.assertListEqual(expected, actual)
|
||||
|
||||
def test_distinct(self):
|
||||
input = ["a", "b", "c", "d", "b", "c"]
|
||||
expected = ["a", "b", "c", "d"]
|
||||
list1 = StringList().add_strings(input)
|
||||
|
||||
list1.distinct()
|
||||
actual = list1.values
|
||||
|
||||
self.assertListEqual(expected, actual)
|
||||
|
||||
def test_union(self):
|
||||
list_a = ["a", "b", "c", ]
|
||||
list_b = ["b", "c", "d", "e"]
|
||||
list1 = StringList().add_strings(list_a)
|
||||
|
||||
list1.union(list_b)
|
||||
|
||||
expected = ["a", "b", "c", "d", "e"]
|
||||
actual = list1.values
|
||||
self.assertListEqual(expected, actual)
|
||||
|
||||
def test_exclude(self):
|
||||
list_a = ["a", "b", "c"]
|
||||
list_b = ["b", "c", "d", "e"]
|
||||
list1 = StringList().add_strings(list_a)
|
||||
|
||||
list1.exclude(list_b)
|
||||
|
||||
expected = ["a"]
|
||||
actual = list1.values
|
||||
self.assertListEqual(expected, actual)
|
||||
|
||||
def test_zip(self):
|
||||
list_a = ["a", "b", "c"]
|
||||
list_b = ["1", "2", "3"]
|
||||
list1 = StringList().add_strings(list_a)
|
||||
|
||||
list1.zip(list_b, lambda index, left, right: f"{index}:({left},{right})")
|
||||
#print(list1.values)
|
||||
|
||||
expected = ["0:(a,1)", "1:(b,2)", "2:(c,3)"]
|
||||
actual = list1.values
|
||||
self.assertListEqual(expected, actual)
|
||||
|
||||
def test_first(self):
|
||||
list1 = StringList().add_strings([f"{i}" for i in range(0, 5)])
|
||||
actual = list1.first()
|
||||
expected = "0"
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_first_empty(self):
|
||||
list1 = StringList()
|
||||
actual = list1.first("default")
|
||||
expected = "default"
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_last(self):
|
||||
list1 = StringList().add_strings([f"{i}" for i in range(0, 5)])
|
||||
actual = list1.last()
|
||||
expected = "4"
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_last_empty(self):
|
||||
list1 = StringList()
|
||||
actual = list1.last("default")
|
||||
expected = "default"
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_skip(self):
|
||||
list1 = StringList().add_strings([f"{i}" for i in range(0, 5)])
|
||||
actual = list1.skip().values
|
||||
expected = ["1", "2", "3", "4"]
|
||||
self.assertListEqual(expected, actual)
|
||||
|
||||
def test_skip_2(self):
|
||||
list1 = StringList().add_strings([f"{i}" for i in range(0, 5)])
|
||||
actual = list1.skip(2).values
|
||||
expected = ["2", "3", "4"]
|
||||
self.assertListEqual(expected, actual)
|
||||
|
||||
def test_take(self):
|
||||
list1 = StringList().add_strings([f"{i}" for i in range(0, 5)])
|
||||
actual = list1.take().values
|
||||
expected = ["0"]
|
||||
self.assertListEqual(expected, actual)
|
||||
|
||||
def test_take_2(self):
|
||||
list1 = StringList().add_strings([f"{i}" for i in range(0, 5)])
|
||||
actual = list1.take(2).values
|
||||
expected = ["0", "1"]
|
||||
self.assertListEqual(expected, actual)
|
||||
Loading…
Reference in New Issue
Block a user