diff --git a/CHANGELOG.md b/CHANGELOG.md
index b039bf6..2b70590 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.10.0] - 2024-05-01
+
+### Added
+
+- `get_combinations(values, length, start, stop, step, offset, indexes)`: Computes combinations given a set of values and a length.
+- `minmax(*args)`: Returns with the min and the max value from the given arguments.
+- `quantity(quota, total)`: Gets a quantity with respect to a quota applied to a total.
+
+### Changed
+
+- Add options to JSONFile implementation (`sort_keys`, `skip_keys`, `ensure_ascii`, `separators`, `strict`).
+- Set the default CSV dialect to `'excel'` when writing (this reflects the default value from the Python library).
+- Set the default CSV dialect to `'auto'` when reading (the dialect will be sniffed from the first few rows).
+
+### Fixed
+
+- Fix the link to the documentation in the readme.
+- Inconsistent return value in the log action.
+- Too many branches and returns in the file checker.
+- A few linter issues.
+- Typo in the instructions for the installation in dev mode.
+
## [0.9.1] - 2023-10-27
### Fixed
diff --git a/README.md b/README.md
index 2fa7e6b..328d2ad 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ pip install --upgrade git+ssh://git@github.com/cerbernetix/py-toolbox.git@develo
`py-toolbox` offers several utilities per domain.
-Please refer to the [documentation](./docs/README.md) for more information.
+Please refer to the [documentation](https://github.com/cerbernetix/py-toolbox/blob/main/docs/README.md) for more information.
## Development
@@ -64,7 +64,7 @@ Then, create the virtual env and install the dependencies:
```sh
cd py-toolbox
python3 -m venv ".venv"
-source "./venv/bin/activate"
+source ".venv/bin/activate"
pip install -r requirements.txt
pip install -e .
```
diff --git a/docs/README.md b/docs/README.md
index f42f347..a78b1a9 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -23,6 +23,7 @@
- [`toolbox.logging.log_file`](./toolbox.logging.log_file.md#module-toolboxlogginglog_file): A custom logger that writes directly to a file.
- [`toolbox.math`](./toolbox.math.md#module-toolboxmath): A collection of Math related tools.
- [`toolbox.math.combination`](./toolbox.math.combination.md#module-toolboxmathcombination): A set of functions for working with combinations.
+- [`toolbox.math.utils`](./toolbox.math.utils.md#module-toolboxmathutils): A set of helper functions related to math.
- [`toolbox.testing`](./toolbox.testing.md#module-toolboxtesting): The `testing` package provides utilities for testing purpose.
- [`toolbox.testing.decorators`](./toolbox.testing.decorators.md#module-toolboxtestingdecorators): A collection of decorators for testing purpose.
- [`toolbox.testing.test_case`](./toolbox.testing.test_case.md#module-toolboxtestingtest_case): Extends the default Python TestCase with more assertions.
@@ -73,6 +74,9 @@
- [`config.setup_file_logging`](./toolbox.logging.config.md#function-setup_file_logging): Setup the application log to a file logger.
- [`combination.get_combination_from_rank`](./toolbox.math.combination.md#function-get_combination_from_rank): Gets the combination corresponding to a particular rank.
- [`combination.get_combination_rank`](./toolbox.math.combination.md#function-get_combination_rank): Gets the rank of a combination.
+- [`combination.get_combinations`](./toolbox.math.combination.md#function-get_combinations): Yields lists of combined values according to the combinations defined by the lengths.
+- [`utils.minmax`](./toolbox.math.utils.md#function-minmax): Returns with the min and the max value from the arguments.
+- [`utils.quantity`](./toolbox.math.utils.md#function-quantity): Gets a quantity with respect to a quota applied to a total.
- [`decorators.test_cases`](./toolbox.testing.decorators.md#function-test_cases): Creates a decorator for parametric test cases.
diff --git a/docs/toolbox.files.csv_file.md b/docs/toolbox.files.csv_file.md
index d239688..e827860 100644
--- a/docs/toolbox.files.csv_file.md
+++ b/docs/toolbox.files.csv_file.md
@@ -50,6 +50,7 @@ with file:
---------------
- **CSV_ENCODING**
- **CSV_DIALECT**
+- **CSV_AUTO**
- **CSV_SAMPLE_SIZE**
- **CSV_READER_PARAMS**
- **CSV_WRITER_PARAMS**
@@ -57,7 +58,7 @@ with file:
---
-
+
## function `read_csv_file`
@@ -65,7 +66,7 @@ with file:
read_csv_file(
filename: 'str',
encoding: 'str' = 'utf-8',
- dialect: 'str' = 'unix',
+ dialect: 'str' = 'auto',
iterator: 'bool' = False,
**kwargs
) → Iterable[dict | list]
@@ -81,7 +82,7 @@ The returned value can be either a list (default) or an iterator (when the itera
- `filename` (str): The path to the file to read.
- `encoding` (str, optional): The file encoding. Defaults to CSV_ENCODING.
- - `dialect` (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. Defaults to CSV_DIALECT.
+ - `dialect` (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. Defaults to CSV_AUTO.
- `iterator` (bool, optional): When True, the function will return an iterator instead of a list. Defaults to False.
- `delimiter` (str, optional): A one-character string used to separate fields. Defaults to ','.
- `doublequote` (bool, optional): Controls how instances of quotechar appearing inside a field should themselves be quoted. When True, the character is doubled. When False, the escapechar is used as a prefix to the quotechar. Defaults to True.
@@ -123,7 +124,7 @@ for row in read_csv_file('path/to/file', iterator=True):
---
-
+
## function `write_csv_file`
@@ -132,7 +133,7 @@ write_csv_file(
filename: 'str',
data: 'Iterable[dict | list]',
encoding: 'str' = 'utf-8',
- dialect: 'str' = 'unix',
+ dialect: 'str' = 'excel',
**kwargs
) → int
```
@@ -189,7 +190,7 @@ write_csv_file('path/to/file', csv_data, encoding='UTF-8', dialect='excel')
---
-
+
## function `read_zip_csv`
@@ -199,7 +200,7 @@ read_zip_csv(
filename: 'str' = None,
encoding: 'str' = 'utf-8',
decoding_errors: 'str' = 'ignore',
- dialect: 'str' = 'unix',
+ dialect: 'str' = 'auto',
iterator: 'bool' = False,
**kwargs
) → Iterable[dict | list]
@@ -217,7 +218,7 @@ The returned value can be either a list (default) or an iterator (when the itera
- `filename` (str, optional): The name of the file to extract from the zip If omitted, the first file having a '.csv' extension will be selected. Defaults to None.
- `encoding` (str, optional): The file encoding. Defaults to CSV_ENCODING.
- `decoding_errors` (str, optional): Controls how decoding errors are handled. If 'strict', a UnicodeError exception is raised. Other possible values are 'ignore', 'replace', and any other name registered via codecs.register_error(). See Error Handlers for details. Defaults to "ignore".
- - `dialect` (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. Defaults to CSV_DIALECT.
+ - `dialect` (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. Defaults to CSV_AUTO.
- `iterator` (bool, optional): When True, the function will return an iterator instead of a list. Defaults to False.
- `delimiter` (str, optional): A one-character string used to separate fields. Defaults to ','.
- `doublequote` (bool, optional): Controls how instances of quotechar appearing inside a field should themselves be quoted. When True, the character is doubled. When False, the escapechar is used as a prefix to the quotechar. Defaults to True.
@@ -265,7 +266,7 @@ with open('path/to/file.zip', 'rb') as file:
---
-
+
## class `CSVFile`
Offers a simple API for reading and writing CSV files.
@@ -310,7 +311,7 @@ with file(create=True):
csv = file.read_file()
```
-
+
### method `__init__`
@@ -322,7 +323,7 @@ __init__(
read: 'bool' = False,
write: 'bool' = False,
encoding: 'str' = 'utf-8',
- dialect: 'str' = 'unix',
+ dialect: 'str' = 'auto',
**kwargs
)
```
@@ -339,7 +340,7 @@ Creates a file manager for CSV files.
- `read` (bool, optional): Expect to also read the file. Defaults to False.
- `write` (bool, optional): Expect to also write to the file. Defaults to False.
- `encoding` (str, optional): The file encoding. Defaults to CSV_ENCODING.
- - `dialect` (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. Defaults to CSV_DIALECT.
+ - `dialect` (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. Defaults to CSV_AUTO for reading or to CSV_DIALECT for writing.
- `delimiter` (str, optional): A one-character string used to separate fields. Defaults to ",".
- `doublequote` (bool, optional): Controls how instances of quotechar appearing inside a field should themselves be quoted. When True, the character is doubled. When False, the escapechar is used as a prefix to the quotechar. Defaults to True.
- `escapechar` (str, optional): A one-character string used by the writer to escape the delimiter if quoting is set to QUOTE_NONE and the quotechar if doublequote is False. On reading, the escapechar removes any special meaning from the following character. Defaults to None, which disables escaping.
@@ -566,7 +567,7 @@ size = file.size
---
-
+
### method `close`
@@ -604,7 +605,7 @@ file.close()
---
-
+
### method `read`
@@ -648,7 +649,7 @@ csv_data = [row for row in file]
---
-
+
### method `read_file`
@@ -699,7 +700,7 @@ for row in file.read_file(iterator=True):
---
-
+
### method `write`
@@ -746,7 +747,7 @@ with file(create=True):
---
-
+
### method `write_file`
diff --git a/docs/toolbox.files.file.md b/docs/toolbox.files.file.md
index 81c7cc5..c73856c 100644
--- a/docs/toolbox.files.file.md
+++ b/docs/toolbox.files.file.md
@@ -9,7 +9,9 @@ A collection of utilities for accessing files.
**Examples:**
```python
-from cerbernetix.toolbox.files import fetch_content, get_file_mode, read_file, read_zip_file, write_file
+from cerbernetix.toolbox.files import (
+ fetch_content, get_file_mode, read_file, read_zip_file, write_file
+)
# get_file_mode() is used to build a file access mode.
# For example to create a text file:
@@ -43,7 +45,7 @@ content = read_zip_file(data)
---
-
+
## function `get_file_mode`
@@ -107,7 +109,7 @@ with open('path/to/file', get_file_mode(binary=True)) as file:
---
-
+
## function `read_file`
@@ -159,7 +161,7 @@ data = read_file('path/to/file', binary=True)
---
-
+
## function `write_file`
@@ -214,7 +216,7 @@ write_file('path/to/file', data, binary=True)
---
-
+
## function `fetch_content`
@@ -240,7 +242,8 @@ Under the hood, it relies on requests to process the query.
- `url` (str): The URL of the content to fetch.
- `binary` (bool): Tells if the content is binary (True) or text (False). When True, the function will return a bytes sequence, otherwise it will return a string sequence.
- `timeout` (int | tuple): The request timeout. Defaults to (6, 30).
- - `**kwargs`: Additional parameters for the GET request. For more info, see [requests/api](https://requests.readthedocs.io/en/latest/api/).
+ - `**kwargs`: Additional parameters for the GET request. For more info, see
+ - `[requests/api](https`: //requests.readthedocs.io/en/latest/api/).
@@ -277,7 +280,7 @@ data = fetch_content("http://example.com/data", binary=True)
---
-
+
## function `read_zip_file`
diff --git a/docs/toolbox.files.file_manager.md b/docs/toolbox.files.file_manager.md
index 15bbd3f..3af0f86 100644
--- a/docs/toolbox.files.file_manager.md
+++ b/docs/toolbox.files.file_manager.md
@@ -36,7 +36,7 @@ with file:
---
-
+
## class `FileManager`
Offers a simple API for reading and writing files.
@@ -74,7 +74,7 @@ with file(create=True):
content = file.read_file()
```
-
+
### method `__init__`
@@ -293,7 +293,7 @@ size = file.size
---
-
+
### method `check`
@@ -355,7 +355,7 @@ else:
---
-
+
### method `close`
@@ -393,7 +393,7 @@ file.close()
---
-
+
### method `create_path`
@@ -428,7 +428,7 @@ else:
---
-
+
### method `delete`
@@ -467,7 +467,7 @@ file.delete()
---
-
+
### method `exists`
@@ -499,7 +499,7 @@ if file.exists():
---
-
+
### method `open`
@@ -560,7 +560,7 @@ data = [dat for dat in file]
---
-
+
### method `read`
@@ -601,7 +601,7 @@ with file:
---
-
+
### method `read_file`
@@ -650,7 +650,7 @@ for data in file.read_file(iterator=True):
---
-
+
### method `write`
@@ -696,7 +696,7 @@ with file(create=True):
---
-
+
### method `write_file`
diff --git a/docs/toolbox.files.json_file.md b/docs/toolbox.files.json_file.md
index 7f75082..8d1e219 100644
--- a/docs/toolbox.files.json_file.md
+++ b/docs/toolbox.files.json_file.md
@@ -46,15 +46,25 @@ with file:
---------------
- **JSON_ENCODING**
- **JSON_INDENT**
+- **JSON_SEPARATORS**
+- **JSON_SORT_KEYS**
+- **JSON_SKIP_KEYS**
+- **JSON_ENSURE_ASCII**
+- **JSON_STRICT**
---
-
+
## function `read_json_file`
```python
-read_json_file(filename: str, encoding: str = 'utf-8', **kwargs) → Any
+read_json_file(
+ filename: str,
+ encoding: str = 'utf-8',
+ strict: bool = True,
+ **kwargs
+) → Any
```
Reads a JSON content from a file.
@@ -65,6 +75,7 @@ Reads a JSON content from a file.
- `filename` (str): The path to the file to read.
- `encoding` (str, optional): The file encoding. Defaults to JSON_ENCODING.
+ - `strict` (bool, optional): Whether or not to forbid control chars. Defaults to JSON_STRICT.
@@ -91,7 +102,7 @@ json_data = read_json_file('path/to/file', encoding='UTF-8')
---
-
+
## function `write_json_file`
@@ -101,6 +112,10 @@ write_json_file(
data: Any,
encoding: str = 'utf-8',
indent: int = 4,
+ separators: tuple = None,
+ sort_keys: bool = False,
+ skip_keys: bool = False,
+ ensure_ascii: bool = True,
**kwargs
) → int
```
@@ -115,6 +130,10 @@ Writes a JSON content to a file.
- `data` (Any): The content to write to the file.
- `encoding` (str, optional): The file encoding. Defaults to JSON_ENCODING.
- `indent` (int, optional): The line indent. Defaults to JSON_INDENT.
+ - `separators` (tuple, optional): The separators for key/values, a.k.a `(', ', ': ')`. Defaults to JSON_SEPARATORS.
+ - `sort_keys` (bool, optional): Whether or not to sort the keys. Defaults to JSON_SORT_KEYS.
+ - `skip_keys` (bool, optional): Whether or not to skip the keys not having an allowed type. Defaults to JSON_SKIP_KEYS.
+ - `ensure_ascii` (bool, optional): Whether or not to escape non-ascii chars. Defaults to JSON_ENSURE_ASCII.
@@ -146,7 +165,7 @@ write_json_file('path/to/file', json_data, encoding='UTF-8', indent=2)
---
-
+
## class `JSONFile`
Offers a simple API for reading and writing JSON files.
@@ -163,6 +182,11 @@ The read API reads all the content at once, and so do the write API too.
- `binary` (bool): The type of file, say text. It must always be False.
- `encoding` (str, optional): The file encoding.
- `indent` (int, optional): The line indent.
+ - `separators` (tuple, optional): The separators for key/values, a.k.a `(', ', ': ')`.
+ - `sort_keys` (bool, optional): Whether or not to sort the keys.
+ - `skip_keys` (bool, optional): Whether or not to skip the keys not having an allowed type.
+ - `ensure_ascii` (bool, optional): Whether or not to escape non-ascii chars.
+ - `strict` (bool, optional): Whether or not to forbid control chars.
@@ -187,7 +211,7 @@ with file(create=True):
json = file.read_file()
```
-
+
### method `__init__`
@@ -200,6 +224,11 @@ __init__(
write: bool = False,
encoding: str = 'utf-8',
indent: int = 4,
+ separators: tuple = None,
+ sort_keys: bool = False,
+ skip_keys: bool = False,
+ ensure_ascii: bool = True,
+ strict: bool = True,
**kwargs
)
```
@@ -217,6 +246,11 @@ Creates a file manager for JSON files.
- `write` (bool, optional): Expect to also write to the file. Defaults to False.
- `encoding` (str, optional): The file encoding. Defaults to JSON_ENCODING.
- `indent` (int, optional): The line indent. Defaults to JSON_INDENT.
+ - `separators` (tuple, optional): The separators for key/values, a.k.a `(', ', ': ')`. Defaults to JSON_SEPARATORS.
+ - `sort_keys` (bool, optional): Whether or not to sort the keys. Defaults to JSON_SORT_KEYS.
+ - `skip_keys` (bool, optional): Whether or not to skip the keys not having an allowed type. Defaults to JSON_SKIP_KEYS.
+ - `ensure_ascii` (bool, optional): Whether or not to escape non-ascii chars. Defaults to JSON_ENSURE_ASCII.
+ - `strict` (bool, optional): Whether or not to forbid control chars. Defaults to JSON_STRICT.
@@ -424,7 +458,7 @@ size = file.size
---
-
+
### method `read`
@@ -464,7 +498,7 @@ with file:
---
-
+
### method `write`
diff --git a/docs/toolbox.files.md b/docs/toolbox.files.md
index dce1f8f..a26f814 100644
--- a/docs/toolbox.files.md
+++ b/docs/toolbox.files.md
@@ -73,10 +73,16 @@ csv_data = file.read_zip_csv(data)
**Global Variables**
---------------
+- **CSV_AUTO**
- **CSV_DIALECT**
- **CSV_ENCODING**
- **JSON_ENCODING**
+- **JSON_ENSURE_ASCII**
- **JSON_INDENT**
+- **JSON_SEPARATORS**
+- **JSON_SKIP_KEYS**
+- **JSON_SORT_KEYS**
+- **JSON_STRICT**
diff --git a/docs/toolbox.files.pickle_file.md b/docs/toolbox.files.pickle_file.md
index 1cb4dc9..e8888eb 100644
--- a/docs/toolbox.files.pickle_file.md
+++ b/docs/toolbox.files.pickle_file.md
@@ -54,7 +54,7 @@ with file:
---
-
+
## function `read_pickle_file`
@@ -106,7 +106,7 @@ for obj in read_pickle_file('path/to/file', iterator=True):
---
-
+
## function `write_pickle_file`
@@ -156,7 +156,7 @@ write_pickle_file('path/to/file', data)
---
-
+
## class `PickleFile`
Offers a simple API for reading and writing pickle files.
@@ -200,7 +200,7 @@ file.write_file(data)
data = file.read_file()
```
-
+
### method `__init__`
@@ -439,7 +439,7 @@ size = file.size
---
-
+
### method `read`
@@ -483,7 +483,7 @@ data = [obj for obj in file]
---
-
+
### method `read_file`
@@ -534,7 +534,7 @@ for obj in file.read_file(iterator=True):
---
-
+
### method `write`
@@ -581,7 +581,7 @@ with file(create=True):
---
-
+
### method `write_file`
diff --git a/docs/toolbox.logging.log_file.md b/docs/toolbox.logging.log_file.md
index 48e1bc5..e0b51a1 100644
--- a/docs/toolbox.logging.log_file.md
+++ b/docs/toolbox.logging.log_file.md
@@ -40,7 +40,7 @@ logger.error('An error occurred: %s', error)
---
-
+
## class `LogFile`
Offers a similar API to the Python builtin loggers for logging to a custom file.
@@ -68,7 +68,7 @@ logger = LogFile()
logger.info('The received value is %d', value)
```
-
+
### method `__init__`
@@ -233,7 +233,7 @@ print(logger.name)
---
-
+
### method `close`
@@ -266,7 +266,7 @@ logger.close() # not necessary since it will be called automatically upon exit.
---
-
+
### method `debug`
@@ -306,7 +306,7 @@ logger.debug('For debug purpose: %d given to %s', value, action)
---
-
+
### method `delete`
@@ -335,7 +335,7 @@ logger.delete()
---
-
+
### method `error`
@@ -375,7 +375,7 @@ logger.error('An error occurred: %s', error)
---
-
+
### method `info`
@@ -415,7 +415,7 @@ logger.info('The received value is %d', value)
---
-
+
### method `log`
@@ -459,7 +459,7 @@ logger.log(logging.ERROR, 'An error occurred: %s', error)
---
-
+
### method `open`
@@ -492,7 +492,7 @@ logger.info('Something was done at %s', datetime.now())
---
-
+
### method `set_format`
@@ -527,7 +527,7 @@ logger.set_format('[%(asctime)s][%(levelname)s]: %(message)s')
---
-
+
### method `set_level`
@@ -562,7 +562,7 @@ logger.set_level(logging.DEBUG)
---
-
+
### method `warn`
diff --git a/docs/toolbox.math.combination.md b/docs/toolbox.math.combination.md
index 04592f7..cf9e851 100644
--- a/docs/toolbox.math.combination.md
+++ b/docs/toolbox.math.combination.md
@@ -9,19 +9,31 @@ A set of functions for working with combinations.
**Examples:**
```python
-from cerbernetix.toolbox.math import get_combination_rank, get_combination_from_rank
+from cerbernetix.toolbox.math import (
+ get_combination_rank,
+ get_combination_from_rank,
+ get_combinations,
+)
# Get the rank of a combination of 3 numbers
print(get_combination_rank([1, 3, 5]))
# Get the combination of 3 numbers ranked at position 5
-print(list(get_combination_from_rank(5, 3)))
+print(get_combination_from_rank(5, 3))
+
+# Get the combinations of 3 numbers from the list
+values = [1, 2, 4, 8, 16]
+print(list(get_combinations(values, 3)))
+
+# Get the combinations of 3 numbers out of 50 from rank 200 to 500
+values = [1, 2, 4, 8, 16]
+print(list(get_combinations(50, 3, start=200, stop=300)))
```
---
-
+
## function `get_combination_rank`
@@ -67,7 +79,7 @@ print(get_combination_rank([1, 3, 5]))
---
-
+
## function `get_combination_from_rank`
@@ -111,7 +123,93 @@ The rank must start at 0.
from cerbernetix.toolbox.math import get_combination_from_rank
# Get the combination of 3 numbers ranked at position 5
-print(list(get_combination_from_rank(5, 3)))
+print(get_combination_from_rank(5, 3))
+```
+
+
+---
+
+
+
+## function `get_combinations`
+
+```python
+get_combinations(
+ values: int | list | tuple | dict,
+ length: int = 2,
+ start: int = 0,
+ stop: int = None,
+ step: int = 1,
+ offset: int = 0,
+ indexes: list | tuple = None
+) → Iterator[list]
+```
+
+Yields lists of combined values according to the combinations defined by the lengths.
+
+Taking a list of values and the length of a combination, it yields each combination of length elements taken from the values.
+
+Note: Beware, the number of possible combinations grows fast with the lengths. For example, 3 out of 5 gives 10 possible combinations, but 3 out of 50 gives 19600...
+
+
+
+**Args:**
+
+ - `values` (int | list | tuple | dict): The list of values from which build the list of combinations. It can be either the length of a range of integers from 0, or a list of sparse values.
+ - `length` (int, optional): The length of each combination. Defaults to 2.
+ - `start` (int, optional): The rank of the first combination to generate. Defaults to 0.
+ - `stop` (int, optional): The rank of the last combination before what stop the generation. If omitted, the maximum number of combination is taken. Defaults to None.
+ - `step` (int, optional): The step between ranks. If start is higher than stop, the step is set to a negative value. Defaults to 1.
+ - `offset` (int, optional): An offset to add to the values if they must not start at 0. Defaults to 0.
+ - `indexes` (list | tuple, optional): A list of indexes for retrieving the values by position. Useful if the values are not indexed by sequential numbers or with a contiguous set like a dictionary or a spare array. Defaults to None.
+
+
+
+**Yields:**
+
+ - `Iterator[list]`: A list of combined values by the given length.
+
+
+
+**Examples:**
+ ```python
+from cerbernetix.toolbox.math import get_combinations
+
+# Get the combinations of 3 numbers from the list
+values = [1, 2, 4, 8, 16]
+print(list(get_combinations(values, 3)))
+# [[1, 2, 4],
+# [1, 2, 8],
+# [1, 4, 8],
+# [2, 4, 8],
+# [1, 2, 16],
+# [1, 4, 16],
+# [2, 4, 16],
+# [1, 8, 16],
+# [2, 8, 16],
+# [4, 8, 16]]
+
+# Get the combinations of 3 numbers from the list from rank 4 to 8
+values = {"1": 1, "2": 2, "4": 4, "8": 8, "16": 16}
+indexes = ["1", "2", "4", "8", "16"]
+print(list(get_combinations(values, 3, indexes=indexes, start=4, stop=8)))
+# [[1, 2, 16],
+# [1, 4, 16],
+# [2, 4, 16],
+# [1, 8, 16]]
+
+# Get combinations from a number of values
+print(list(get_combinations(5, 3, offset=1)))
+# [[1, 2, 3],
+# [1, 2, 4],
+# [1, 3, 4],
+# [2, 3, 4],
+# [1, 2, 5],
+# [1, 3, 5],
+# [2, 3, 5],
+# [1, 4, 5],
+# [2, 4, 5],
+# [3, 4, 5]]
```
diff --git a/docs/toolbox.math.md b/docs/toolbox.math.md
index 6335ba9..0f6dae9 100644
--- a/docs/toolbox.math.md
+++ b/docs/toolbox.math.md
@@ -8,18 +8,49 @@ A collection of Math related tools.
It contains:
- `get_combination_rank(combination, offset)`: Gets the rank of a combination.
- `get_combination_from_rank(rank, length, offset)`: Gets the combination corresponding to a particular rank.
+- `get_combinations(values, length, start, stop, step, offset, indexes)`: Yields lists of combined values according to the combinations defined by the lengths.
+- `minmax(*args)`: Returns with the min and the max value from the given arguments.
+- `quantity(quota, total)`: Gets a quantity with respect to a quota applied to a total.
**Examples:**
```python
-from cerbernetix.toolbox.math import get_combination_rank, get_combination_from_rank
+from cerbernetix.toolbox.math import (
+ get_combination_rank,
+ get_combination_from_rank,
+ get_combinations,
+)
# Get the rank of a combination of 3 numbers
print(get_combination_rank([1, 3, 5]))
# Get the combination of 3 numbers ranked at position 5
-print(list(get_combination_from_rank(5, 3)))
+print(get_combination_from_rank(5, 3))
+
+# Get the combinations of 3 numbers from the list
+values = [1, 2, 4, 8, 16]
+print(list(get_combinations(values, 3)))
+
+# Get the combinations of 3 numbers out of 50 from rank 200 to 500
+values = [1, 2, 4, 8, 16]
+print(list(get_combinations(50, 3, start=200, stop=300)))
+```
+
+```python
+from cerbernetix.toolbox.math import minmax
+
+mini, maxi = minmax(3, 2, 6, 4, 5) # 2, 6
+```
+
+```python
+from cerbernetix.toolbox.math import quantity
+
+# Gets a size from a percentage
+size = quantity(.2, 10) # 2
+
+# Gets a size from an absolute value
+size = quantity(6, 10) # 6
```
diff --git a/docs/toolbox.math.utils.md b/docs/toolbox.math.utils.md
new file mode 100644
index 0000000..adf0bf9
--- /dev/null
+++ b/docs/toolbox.math.utils.md
@@ -0,0 +1,96 @@
+
+
+
+
+# module `toolbox.math.utils`
+A set of helper functions related to math.
+
+Examples ```python
+from cerbernetix.toolbox.math import minmax
+
+mini, maxi = minmax(3, 2, 6, 4, 5) # 2, 6
+```
+
+```python
+from cerbernetix.toolbox.math import quantity
+
+# Gets a size from a percentage
+size = quantity(.2, 10) # 2
+
+# Gets a size from an absolute value
+size = quantity(6, 10) # 6
+```
+
+
+---
+
+
+
+## function `minmax`
+
+```python
+minmax(*args) → tuple
+```
+
+Returns with the min and the max value from the arguments.
+
+
+
+**Args:**
+
+ - `*args`: Arguments from which extract the min and the max.
+
+
+
+**Returns:**
+
+ - `tuple`: A tuple with first the min value, then the max value.
+
+Examples ```python
+from cerbernetix.toolbox.math import minmax
+
+mini, maxi = minmax(3, 2, 6, 4, 5) # 2, 6
+```
+
+
+---
+
+
+
+## function `quantity`
+
+```python
+quantity(quota: int | float, total: int) → int
+```
+
+Gets a quantity with respect to a quota applied to a total.
+
+
+
+**Args:**
+
+ - `quota` (int | float): The expected quota from the total. It can be either a percentage or an absolute value. The percentage is represented by a number between 0 and 1. An absolute value is represented by a number between 1 and the total included.
+ - `total` (int): The total number.
+
+
+
+**Returns:**
+
+ - `int`: The quantity computed from the quota applied to the total. It cannot exceeds the total, and it cannot be negative.
+
+Examples ```python
+from cerbernetix.toolbox.math import quantity
+
+# Gets a size from a percentage
+size = quantity(.2, 10) # 2
+
+# Gets a size from an absolute value
+size = quantity(6, 10) # 6
+```
+
+
+
+
+---
+
+_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._
diff --git a/docs/toolbox.testing.test_case.md b/docs/toolbox.testing.test_case.md
index 0892e5d..b903f88 100644
--- a/docs/toolbox.testing.test_case.md
+++ b/docs/toolbox.testing.test_case.md
@@ -23,7 +23,7 @@ class TestMyStuff(testing.TestCase)
---
-
+
## class `TestCase`
Test class with additional assertions.
@@ -33,7 +33,7 @@ Test class with additional assertions.
---
-
+
### method `assertListsAlmostEqual`
@@ -42,7 +42,7 @@ assertListsAlmostEqual(
first: Iterable[float],
second: Iterable[float],
places: int = 7
-)
+) → None
```
Asserts that 2 lists of float numbers are almost equal by the number of places.
@@ -74,7 +74,7 @@ class TestMyStuff(testing.TestCase)
---
-
+
### method `assertListsNotAlmostEqual`
@@ -83,7 +83,7 @@ assertListsNotAlmostEqual(
first: Iterable[float],
second: Iterable[float],
places: int = 7
-)
+) → None
```
Asserts that 2 lists of float numbers are not almost equal by the number of places.
diff --git a/pyproject.toml b/pyproject.toml
index 7d157fc..4d988bb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "cerbernetix.toolbox"
-version = "0.9.1"
+version = "0.10.0"
authors = [{ name = "Jean-Sébastien CONAN", email = "jsconan@gmail.com" }]
description = "A set of utilities for Python projects"
readme = "README.md"
diff --git a/src/cerbernetix/toolbox/files/__init__.py b/src/cerbernetix/toolbox/files/__init__.py
index 7c84fdf..e98bc30 100644
--- a/src/cerbernetix/toolbox/files/__init__.py
+++ b/src/cerbernetix/toolbox/files/__init__.py
@@ -64,7 +64,9 @@
csv_data = file.read_zip_csv(data)
```
"""
+
from cerbernetix.toolbox.files.csv_file import (
+ CSV_AUTO,
CSV_DIALECT,
CSV_ENCODING,
CSVFile,
@@ -82,7 +84,12 @@
from cerbernetix.toolbox.files.file_manager import FileManager
from cerbernetix.toolbox.files.json_file import (
JSON_ENCODING,
+ JSON_ENSURE_ASCII,
JSON_INDENT,
+ JSON_SEPARATORS,
+ JSON_SKIP_KEYS,
+ JSON_SORT_KEYS,
+ JSON_STRICT,
JSONFile,
read_json_file,
write_json_file,
diff --git a/src/cerbernetix/toolbox/files/csv_file.py b/src/cerbernetix/toolbox/files/csv_file.py
index e1ad18e..3adb4d4 100644
--- a/src/cerbernetix/toolbox/files/csv_file.py
+++ b/src/cerbernetix/toolbox/files/csv_file.py
@@ -39,6 +39,7 @@
first = file.read()
```
"""
+
from __future__ import annotations
import csv
@@ -52,7 +53,10 @@
CSV_ENCODING = "utf-8"
# The default CSV dialect
-CSV_DIALECT = "unix"
+CSV_DIALECT = "excel"
+
+# The value for auto-detecting the CSV dialect
+CSV_AUTO = "auto"
# The amount of bytes to read for auto-detecting the CSV dialect
CSV_SAMPLE_SIZE = 1024
@@ -139,7 +143,7 @@ def __init__(
read: bool = False,
write: bool = False,
encoding: str = CSV_ENCODING,
- dialect: str = CSV_DIALECT,
+ dialect: str = CSV_AUTO,
**kwargs,
):
r"""Creates a file manager for CSV files.
@@ -157,7 +161,7 @@ def __init__(
encoding (str, optional): The file encoding. Defaults to CSV_ENCODING.
dialect (str, optional): The CSV dialect to use. If 'auto' is given, the reader will
try detecting the CSV dialect by reading a sample at the head of the file.
- Defaults to CSV_DIALECT.
+ Defaults to CSV_AUTO for reading or to CSV_DIALECT for writing.
delimiter (str, optional): A one-character string used to separate fields.
Defaults to ",".
doublequote (bool, optional): Controls how instances of quotechar appearing inside a
@@ -400,7 +404,7 @@ def read(self) -> dict | list:
reader = csv.DictReader
dialect = self.dialect
- if dialect == "auto":
+ if dialect == CSV_AUTO:
dialect = csv.Sniffer().sniff(self._file.read(CSV_SAMPLE_SIZE))
self._file.seek(0)
@@ -461,7 +465,7 @@ def write(self, data: dict | list) -> int:
writer = csv.writer
dialect = self.dialect
- if dialect == "auto":
+ if dialect == CSV_AUTO:
dialect = CSV_DIALECT
self._writer = writer(self._file, dialect=dialect, **kwargs)
@@ -475,7 +479,7 @@ def write(self, data: dict | list) -> int:
def read_csv_file(
filename: str,
encoding: str = CSV_ENCODING,
- dialect: str = CSV_DIALECT,
+ dialect: str = CSV_AUTO,
iterator: bool = False,
**kwargs,
) -> Iterable[dict | list]:
@@ -489,7 +493,7 @@ def read_csv_file(
encoding (str, optional): The file encoding. Defaults to CSV_ENCODING.
dialect (str, optional): The CSV dialect to use. If 'auto' is given, the reader will
try detecting the CSV dialect by reading a sample at the head of the file.
- Defaults to CSV_DIALECT.
+ Defaults to CSV_AUTO.
iterator (bool, optional): When True, the function will return an iterator instead of a
list. Defaults to False.
delimiter (str, optional): A one-character string used to separate fields.
@@ -622,7 +626,7 @@ def read_zip_csv(
filename: str = None,
encoding: str = CSV_ENCODING,
decoding_errors: str = "ignore",
- dialect: str = CSV_DIALECT,
+ dialect: str = CSV_AUTO,
iterator: bool = False,
**kwargs,
) -> Iterable[dict | list]:
@@ -642,7 +646,7 @@ def read_zip_csv(
Defaults to "ignore".
dialect (str, optional): The CSV dialect to use. If 'auto' is given, the reader will
try detecting the CSV dialect by reading a sample at the head of the file.
- Defaults to CSV_DIALECT.
+ Defaults to CSV_AUTO.
iterator (bool, optional): When True, the function will return an iterator instead of a
list. Defaults to False.
delimiter (str, optional): A one-character string used to separate fields.
@@ -704,7 +708,7 @@ def read_zip_csv(
else:
reader_factory = csv.DictReader
- if dialect == "auto":
+ if dialect == CSV_AUTO:
dialect = csv.Sniffer().sniff(text[:CSV_SAMPLE_SIZE])
lines = re.split(r"[\r\n]+", text.strip("\r\n"))
diff --git a/src/cerbernetix/toolbox/files/file.py b/src/cerbernetix/toolbox/files/file.py
index 914d83a..5d064ae 100644
--- a/src/cerbernetix/toolbox/files/file.py
+++ b/src/cerbernetix/toolbox/files/file.py
@@ -2,7 +2,9 @@
Examples:
```python
-from cerbernetix.toolbox.files import fetch_content, get_file_mode, read_file, read_zip_file, write_file
+from cerbernetix.toolbox.files import (
+ fetch_content, get_file_mode, read_file, read_zip_file, write_file
+)
# get_file_mode() is used to build a file access mode.
# For example to create a text file:
@@ -33,6 +35,7 @@
content = read_zip_file(data)
```
"""
+
import os
import zipfile
from io import BytesIO
@@ -222,7 +225,8 @@ def fetch_content(
binary (bool): Tells if the content is binary (True) or text (False). When True,
the function will return a bytes sequence, otherwise it will return a string sequence.
timeout (int | tuple): The request timeout. Defaults to (6, 30).
- **kwargs: Additional parameters for the GET request. For more info, see [requests/api](https://requests.readthedocs.io/en/latest/api/).
+ **kwargs: Additional parameters for the GET request. For more info, see
+ [requests/api](https://requests.readthedocs.io/en/latest/api/).
Raises:
requests.RequestException: There was an ambiguous exception that occurred while handling
diff --git a/src/cerbernetix/toolbox/files/file_manager.py b/src/cerbernetix/toolbox/files/file_manager.py
index 5241064..dd4e711 100644
--- a/src/cerbernetix/toolbox/files/file_manager.py
+++ b/src/cerbernetix/toolbox/files/file_manager.py
@@ -25,6 +25,7 @@
print(file.read())
```
"""
+
from __future__ import annotations
import os
@@ -319,6 +320,7 @@ def open(
"""
self.close()
+ # pylint: disable-next=consider-using-with
self._file = open(
self.filename,
mode=get_file_mode(create, append, read, write, self.binary),
@@ -568,41 +570,26 @@ def check(
if must_exist and not exist:
return False
- if min_time is not None or max_time is not None:
- if not exist:
- return False
-
- file_time = self.date
-
- if min_time is not None and file_time <= min_time:
- return False
-
- if max_time is not None and file_time >= max_time:
- return False
-
- if min_age is not None or max_age is not None:
- if not exist:
- return False
-
- file_age = self.age
+ def fail(value: int, value_min: int, value_max: int) -> bool:
+ if value_min is not None or value_max is not None:
+ if not exist:
+ return True
- if min_age is not None and file_age <= min_age:
- return False
+ if value_min is not None and value <= value_min:
+ return True
- if max_age is not None and file_age >= max_age:
- return False
-
- if min_size is not None or max_size is not None:
- if not exist:
- return False
+ if value_max is not None and value >= value_max:
+ return True
+ return False
- file_size = self.size
+ if fail(self.date, min_time, max_time):
+ return False
- if min_size is not None and file_size <= min_size:
- return False
+ if fail(self.age, min_age, max_age):
+ return False
- if max_size is not None and file_size >= max_size:
- return False
+ if fail(self.size, min_size, max_size):
+ return False
return True
diff --git a/src/cerbernetix/toolbox/files/json_file.py b/src/cerbernetix/toolbox/files/json_file.py
index 58aefb2..06ea5b7 100644
--- a/src/cerbernetix/toolbox/files/json_file.py
+++ b/src/cerbernetix/toolbox/files/json_file.py
@@ -35,6 +35,7 @@
json_data = file.read()
```
"""
+
import json
from typing import Any
@@ -46,6 +47,21 @@
# The default indent for JSON files
JSON_INDENT = 4
+# The default separators for JSON files
+JSON_SEPARATORS = None
+
+# The default value for whether or not to sort the keys in JSON files
+JSON_SORT_KEYS = False
+
+# The default value for whether or not to skip the keys not having an allowed type in JSON files
+JSON_SKIP_KEYS = False
+
+# The default value for escaping non-ascii chars in JSON files
+JSON_ENSURE_ASCII = True
+
+# The default value for forbidding the control chars in JSON files
+JSON_STRICT = True
+
class JSONFile(FileManager):
"""Offers a simple API for reading and writing JSON files.
@@ -60,6 +76,11 @@ class JSONFile(FileManager):
binary (bool): The type of file, say text. It must always be False.
encoding (str, optional): The file encoding.
indent (int, optional): The line indent.
+ separators (tuple, optional): The separators for key/values, a.k.a `(', ', ': ')`.
+ sort_keys (bool, optional): Whether or not to sort the keys.
+ skip_keys (bool, optional): Whether or not to skip the keys not having an allowed type.
+ ensure_ascii (bool, optional): Whether or not to escape non-ascii chars.
+ strict (bool, optional): Whether or not to forbid control chars.
Examples:
```python
@@ -92,6 +113,11 @@ def __init__(
write: bool = False,
encoding: str = JSON_ENCODING,
indent: int = JSON_INDENT,
+ separators: tuple = JSON_SEPARATORS,
+ sort_keys: bool = JSON_SORT_KEYS,
+ skip_keys: bool = JSON_SKIP_KEYS,
+ ensure_ascii: bool = JSON_ENSURE_ASCII,
+ strict: bool = JSON_STRICT,
**kwargs,
):
"""Creates a file manager for JSON files.
@@ -108,6 +134,15 @@ def __init__(
Defaults to False.
encoding (str, optional): The file encoding. Defaults to JSON_ENCODING.
indent (int, optional): The line indent. Defaults to JSON_INDENT.
+ separators (tuple, optional): The separators for key/values, a.k.a `(', ', ': ')`.
+ Defaults to JSON_SEPARATORS.
+ sort_keys (bool, optional): Whether or not to sort the keys. Defaults to JSON_SORT_KEYS.
+ skip_keys (bool, optional): Whether or not to skip the keys not having an allowed type.
+ Defaults to JSON_SKIP_KEYS.
+ ensure_ascii (bool, optional): Whether or not to escape non-ascii chars.
+ Defaults to JSON_ENSURE_ASCII.
+ strict (bool, optional): Whether or not to forbid control chars.
+ Defaults to JSON_STRICT.
Examples:
```python
@@ -153,6 +188,11 @@ def __init__(
**kwargs,
)
self.indent = indent
+ self.separators = separators
+ self.sort_keys = sort_keys
+ self.skip_keys = skip_keys
+ self.ensure_ascii = ensure_ascii
+ self.strict = strict
def read(self) -> Any:
"""Reads the content from the file.
@@ -182,7 +222,7 @@ def read(self) -> Any:
if not data:
return None
- return json.JSONDecoder().decode(data)
+ return json.JSONDecoder(strict=self.strict).decode(data)
def write(self, data: Any) -> int:
"""Writes content to the file.
@@ -212,8 +252,11 @@ def write(self, data: Any) -> int:
"""
return super().write(
json.JSONEncoder(
- sort_keys=True,
+ skipkeys=self.skip_keys,
+ ensure_ascii=self.ensure_ascii,
+ sort_keys=self.sort_keys,
indent=self.indent,
+ separators=self.separators,
).encode(data)
)
@@ -221,6 +264,7 @@ def write(self, data: Any) -> int:
def read_json_file(
filename: str,
encoding: str = JSON_ENCODING,
+ strict: bool = JSON_STRICT,
**kwargs,
) -> Any:
"""Reads a JSON content from a file.
@@ -228,6 +272,7 @@ def read_json_file(
Args:
filename (str): The path to the file to read.
encoding (str, optional): The file encoding. Defaults to JSON_ENCODING.
+ strict (bool, optional): Whether or not to forbid control chars. Defaults to JSON_STRICT.
Raises:
OSError: If the file cannot be read.
@@ -243,7 +288,7 @@ def read_json_file(
json_data = read_json_file('path/to/file', encoding='UTF-8')
```
"""
- return JSONFile(filename, encoding=encoding, **kwargs).read_file()
+ return JSONFile(filename, encoding=encoding, strict=strict, **kwargs).read_file()
def write_json_file(
@@ -251,6 +296,10 @@ def write_json_file(
data: Any,
encoding: str = JSON_ENCODING,
indent: int = JSON_INDENT,
+ separators: tuple = JSON_SEPARATORS,
+ sort_keys: bool = JSON_SORT_KEYS,
+ skip_keys: bool = JSON_SKIP_KEYS,
+ ensure_ascii: bool = JSON_ENSURE_ASCII,
**kwargs,
) -> int:
"""Writes a JSON content to a file.
@@ -260,6 +309,13 @@ def write_json_file(
data (Any): The content to write to the file.
encoding (str, optional): The file encoding. Defaults to JSON_ENCODING.
indent (int, optional): The line indent. Defaults to JSON_INDENT.
+ separators (tuple, optional): The separators for key/values, a.k.a `(', ', ': ')`.
+ Defaults to JSON_SEPARATORS.
+ sort_keys (bool, optional): Whether or not to sort the keys. Defaults to JSON_SORT_KEYS.
+ skip_keys (bool, optional): Whether or not to skip the keys not having an allowed type.
+ Defaults to JSON_SKIP_KEYS.
+ ensure_ascii (bool, optional): Whether or not to escape non-ascii chars.
+ Defaults to JSON_ENSURE_ASCII.
Raises:
OSError: If the file cannot be written.
@@ -284,5 +340,9 @@ def write_json_file(
filename,
encoding=encoding,
indent=indent,
+ separators=separators,
+ sort_keys=sort_keys,
+ skip_keys=skip_keys,
+ ensure_ascii=ensure_ascii,
**kwargs,
).write_file(data)
diff --git a/src/cerbernetix/toolbox/files/pickle_file.py b/src/cerbernetix/toolbox/files/pickle_file.py
index 50ba8e2..ba5b63c 100644
--- a/src/cerbernetix/toolbox/files/pickle_file.py
+++ b/src/cerbernetix/toolbox/files/pickle_file.py
@@ -39,6 +39,7 @@
first = file.read()
```
"""
+
from __future__ import annotations
import pickle
@@ -134,17 +135,17 @@ def __init__(
pickle will try to map the new Python 3 names to the old module names used in Python 2,
so that the pickle data stream is readable with Python 2. Defaults to True.
encoding (str, optional): Tell pickle how to decode 8-bit string instances pickled by
- Python 2. The encoding can be ‘bytes’ to read these 8-bit string instances as bytes objects.
- Using encoding='latin1' is required for unpickling NumPy arrays and instances of datetime,
- date and time pickled by Python 2. Defaults to ‘ASCII’.
+ Python 2. The encoding can be ‘bytes’ to read these 8-bit string instances as bytes
+ objects. Using encoding='latin1' is required for unpickling NumPy arrays and instances
+ of datetime, date and time pickled by Python 2. Defaults to ‘ASCII’.
errors (str, optional): Tell pickle how to decode 8-bit string instances pickled by
Python 2. Defaults to ‘strict’.
buffers (optional): If buffers is None (the default), then all data necessary for
- deserialization must be contained in the pickle stream. This means that the buffer_callback
- argument was None when a Pickler was instantiated (or when dump() or dumps() was called). If
- buffers is not None, it should be an iterable of buffer-enabled objects that is consumed
- each time the pickle stream references an out-of-band buffer view. Such buffers have been
- given in order to the buffer_callback of a Pickler object.
+ deserialization must be contained in the pickle stream. This means that the
+ buffer_callback argument was None when a Pickler was instantiated (or when dump() or
+ dumps() was called). If buffers is not None, it should be an iterable of buffer-enabled
+ objects that is consumed each time the pickle stream references an out-of-band buffer
+ view. Such buffers have been given in order to the buffer_callback of a Pickler object.
buffer_callback (optional): If buffer_callback is None (the default), buffer views are
serialized into file as part of the pickle stream. If buffer_callback is not None, then
it can be called any number of times with a buffer view. If the callback returns a false
diff --git a/src/cerbernetix/toolbox/logging/log_file.py b/src/cerbernetix/toolbox/logging/log_file.py
index aab28dd..720241f 100644
--- a/src/cerbernetix/toolbox/logging/log_file.py
+++ b/src/cerbernetix/toolbox/logging/log_file.py
@@ -23,6 +23,7 @@
logger.error('An error occurred: %s', error)
```
"""
+
from __future__ import annotations
import atexit
@@ -290,7 +291,7 @@ def log(self, level: int, message: str, *args, **kwargs) -> LogFile:
```
"""
if level < self._level:
- return
+ return self
pathname = None
lineno = 0
diff --git a/src/cerbernetix/toolbox/math/__init__.py b/src/cerbernetix/toolbox/math/__init__.py
index c44772d..47e0285 100644
--- a/src/cerbernetix/toolbox/math/__init__.py
+++ b/src/cerbernetix/toolbox/math/__init__.py
@@ -4,16 +4,54 @@
- `get_combination_rank(combination, offset)`: Gets the rank of a combination.
- `get_combination_from_rank(rank, length, offset)`: Gets the combination corresponding to a
particular rank.
+- `get_combinations(values, length, start, stop, step, offset, indexes)`: Yields lists of combined
+values according to the combinations defined by the lengths.
+- `minmax(*args)`: Returns with the min and the max value from the given arguments.
+- `quantity(quota, total)`: Gets a quantity with respect to a quota applied to a total.
Examples:
```python
-from cerbernetix.toolbox.math import get_combination_rank, get_combination_from_rank
+from cerbernetix.toolbox.math import (
+ get_combination_rank,
+ get_combination_from_rank,
+ get_combinations,
+)
# Get the rank of a combination of 3 numbers
print(get_combination_rank([1, 3, 5]))
# Get the combination of 3 numbers ranked at position 5
-print(list(get_combination_from_rank(5, 3)))
+print(get_combination_from_rank(5, 3))
+
+# Get the combinations of 3 numbers from the list
+values = [1, 2, 4, 8, 16]
+print(list(get_combinations(values, 3)))
+
+# Get the combinations of 3 numbers out of 50 from rank 200 to 500
+values = [1, 2, 4, 8, 16]
+print(list(get_combinations(50, 3, start=200, stop=300)))
+```
+
+```python
+from cerbernetix.toolbox.math import minmax
+
+mini, maxi = minmax(3, 2, 6, 4, 5) # 2, 6
+```
+
+```python
+from cerbernetix.toolbox.math import quantity
+
+# Gets a size from a percentage
+size = quantity(.2, 10) # 2
+
+# Gets a size from an absolute value
+size = quantity(6, 10) # 6
```
"""
-from cerbernetix.toolbox.math.combination import get_combination_from_rank, get_combination_rank
+
+from cerbernetix.toolbox.math.combination import (
+ get_combination_from_rank,
+ get_combination_rank,
+ get_combinations,
+)
+from cerbernetix.toolbox.math.utils import minmax, quantity
diff --git a/src/cerbernetix/toolbox/math/combination.py b/src/cerbernetix/toolbox/math/combination.py
index 2dfc304..3baaa2e 100644
--- a/src/cerbernetix/toolbox/math/combination.py
+++ b/src/cerbernetix/toolbox/math/combination.py
@@ -2,17 +2,32 @@
Examples:
```python
-from cerbernetix.toolbox.math import get_combination_rank, get_combination_from_rank
+from cerbernetix.toolbox.math import (
+ get_combination_rank,
+ get_combination_from_rank,
+ get_combinations,
+)
# Get the rank of a combination of 3 numbers
print(get_combination_rank([1, 3, 5]))
# Get the combination of 3 numbers ranked at position 5
-print(list(get_combination_from_rank(5, 3)))
+print(get_combination_from_rank(5, 3))
+
+# Get the combinations of 3 numbers from the list
+values = [1, 2, 4, 8, 16]
+print(list(get_combinations(values, 3)))
+
+# Get the combinations of 3 numbers out of 50 from rank 200 to 500
+values = [1, 2, 4, 8, 16]
+print(list(get_combinations(50, 3, start=200, stop=300)))
```
"""
+
from math import comb
-from typing import Iterable
+from typing import Iterable, Iterator
+
+from cerbernetix.toolbox.data.mappers import passthrough
def get_combination_rank(combination: Iterable[int], offset: int = 0) -> int:
@@ -77,7 +92,7 @@ def get_combination_from_rank(rank: int, length: int = 2, offset: int = 0) -> li
from cerbernetix.toolbox.math import get_combination_from_rank
# Get the combination of 3 numbers ranked at position 5
- print(list(get_combination_from_rank(5, 3)))
+ print(get_combination_from_rank(5, 3))
```
"""
if rank < 0:
@@ -115,3 +130,116 @@ def get_combination_from_rank(rank: int, length: int = 2, offset: int = 0) -> li
combination[0] = rank - binomial + offset
return combination
+
+
+def get_combinations(
+ values: int | list | tuple | dict,
+ length: int = 2,
+ start: int = 0,
+ stop: int = None,
+ step: int = 1,
+ offset: int = 0,
+ indexes: list | tuple = None,
+) -> Iterator[list]:
+ """Yields lists of combined values according to the combinations defined by the lengths.
+
+ Taking a list of values and the length of a combination, it yields each combination of length
+ elements taken from the values.
+
+ Note: Beware, the number of possible combinations grows fast with the lengths.
+ For example, 3 out of 5 gives 10 possible combinations, but 3 out of 50 gives 19600...
+
+ Args:
+ values (int | list | tuple | dict): The list of values from which build the list of
+ combinations. It can be either the length of a range of integers from 0, or a list of
+ sparse values.
+ length (int, optional): The length of each combination. Defaults to 2.
+ start (int, optional): The rank of the first combination to generate. Defaults to 0.
+ stop (int, optional): The rank of the last combination before what stop the generation. If
+ omitted, the maximum number of combination is taken. Defaults to None.
+ step (int, optional): The step between ranks. If start is higher than stop, the step is set
+ to a negative value. Defaults to 1.
+ offset (int, optional): An offset to add to the values if they must not start at 0.
+ Defaults to 0.
+ indexes (list | tuple, optional): A list of indexes for retrieving the values by position.
+ Useful if the values are not indexed by sequential numbers or with a contiguous set like a
+ dictionary or a spare array. Defaults to None.
+
+ Yields:
+ Iterator[list]: A list of combined values by the given length.
+
+ Examples:
+ ```python
+ from cerbernetix.toolbox.math import get_combinations
+
+ # Get the combinations of 3 numbers from the list
+ values = [1, 2, 4, 8, 16]
+ print(list(get_combinations(values, 3)))
+ # [[1, 2, 4],
+ # [1, 2, 8],
+ # [1, 4, 8],
+ # [2, 4, 8],
+ # [1, 2, 16],
+ # [1, 4, 16],
+ # [2, 4, 16],
+ # [1, 8, 16],
+ # [2, 8, 16],
+ # [4, 8, 16]]
+
+ # Get the combinations of 3 numbers from the list from rank 4 to 8
+ values = {"1": 1, "2": 2, "4": 4, "8": 8, "16": 16}
+ indexes = ["1", "2", "4", "8", "16"]
+ print(list(get_combinations(values, 3, indexes=indexes, start=4, stop=8)))
+ # [[1, 2, 16],
+ # [1, 4, 16],
+ # [2, 4, 16],
+ # [1, 8, 16]]
+
+ # Get combinations from a number of values
+ print(list(get_combinations(5, 3, offset=1)))
+ # [[1, 2, 3],
+ # [1, 2, 4],
+ # [1, 3, 4],
+ # [2, 3, 4],
+ # [1, 2, 5],
+ # [1, 3, 5],
+ # [2, 3, 5],
+ # [1, 4, 5],
+ # [2, 4, 5],
+ # [3, 4, 5]]
+ ```
+ """
+ if isinstance(values, int):
+ nb_values = values
+ get_value = passthrough
+ else:
+ nb_values = len(values)
+ get_value = values.__getitem__
+
+ if indexes is None:
+ get_index = passthrough
+ else:
+ get_index = indexes.__getitem__
+
+ if nb_values == 0 or length == 0:
+ return
+
+ nb_comb = comb(nb_values, length)
+
+ if stop is None or stop > nb_comb:
+ stop = nb_comb
+
+ if start >= nb_comb:
+ start = nb_comb - 1
+
+ if start < 0 or stop < -1:
+ raise ValueError("A combination range cannot start or stop with a negative value")
+
+ if start > stop:
+ step = -abs(step)
+ else:
+ step = abs(step)
+
+ for rank in range(start, stop, step):
+ combination = get_combination_from_rank(rank, length)
+ yield [get_value(get_index(position)) + offset for position in combination]
diff --git a/src/cerbernetix/toolbox/math/utils.py b/src/cerbernetix/toolbox/math/utils.py
new file mode 100644
index 0000000..cefd185
--- /dev/null
+++ b/src/cerbernetix/toolbox/math/utils.py
@@ -0,0 +1,68 @@
+"""A set of helper functions related to math.
+
+Examples
+```python
+from cerbernetix.toolbox.math import minmax
+
+mini, maxi = minmax(3, 2, 6, 4, 5) # 2, 6
+```
+
+```python
+from cerbernetix.toolbox.math import quantity
+
+# Gets a size from a percentage
+size = quantity(.2, 10) # 2
+
+# Gets a size from an absolute value
+size = quantity(6, 10) # 6
+```
+"""
+
+
+def minmax(*args) -> tuple:
+ """Returns with the min and the max value from the arguments.
+
+ Args:
+ *args: Arguments from which extract the min and the max.
+
+ Returns:
+ tuple: A tuple with first the min value, then the max value.
+
+ Examples
+ ```python
+ from cerbernetix.toolbox.math import minmax
+
+ mini, maxi = minmax(3, 2, 6, 4, 5) # 2, 6
+ ```
+ """
+ return min(*args), max(*args)
+
+
+def quantity(quota: int | float, total: int) -> int:
+ """Gets a quantity with respect to a quota applied to a total.
+
+ Args:
+ quota (int | float): The expected quota from the total. It can be either a percentage or an
+ absolute value. The percentage is represented by a number between 0 and 1. An absolute
+ value is represented by a number between 1 and the total included.
+ total (int): The total number.
+
+ Returns:
+ int: The quantity computed from the quota applied to the total. It cannot exceeds the total,
+ and it cannot be negative.
+
+ Examples
+ ```python
+ from cerbernetix.toolbox.math import quantity
+
+ # Gets a size from a percentage
+ size = quantity(.2, 10) # 2
+
+ # Gets a size from an absolute value
+ size = quantity(6, 10) # 6
+ ```
+ """
+ if 0 < quota < 1:
+ return int(total * quota)
+
+ return min(abs(int(quota)), total)
diff --git a/src/cerbernetix/toolbox/testing/test_case.py b/src/cerbernetix/toolbox/testing/test_case.py
index 0fd7789..3f5c8d9 100644
--- a/src/cerbernetix/toolbox/testing/test_case.py
+++ b/src/cerbernetix/toolbox/testing/test_case.py
@@ -12,6 +12,7 @@ def test_dict(self):
self.assertListsAlmostEqual(create_dict(), {"value": 42.4242, "PI": 3.1415})
```
"""
+
import unittest
from typing import Iterable
@@ -19,9 +20,10 @@ def test_dict(self):
class TestCase(unittest.TestCase):
"""Test class with additional assertions."""
+ # pylint: disable-next=invalid-name
def assertListsAlmostEqual(
self, first: Iterable[float], second: Iterable[float], places: int = 7
- ):
+ ) -> None:
"""Asserts that 2 lists of float numbers are almost equal by the number of places.
Args:
@@ -46,7 +48,8 @@ def test_almost_equal(self):
second_is_iterable = isinstance(second, Iterable)
if not first_is_iterable and not second_is_iterable:
- return self.assertAlmostEqual(first, second, places)
+ self.assertAlmostEqual(first, second, places)
+ return
if not first_is_iterable or not second_is_iterable:
raise AssertionError("first != second")
@@ -76,9 +79,10 @@ def test_almost_equal(self):
self.assertListsAlmostEqual(left, right, places)
+ # pylint: disable-next=invalid-name
def assertListsNotAlmostEqual(
self, first: Iterable[float], second: Iterable[float], places: int = 7
- ):
+ ) -> None:
"""Asserts that 2 lists of float numbers are not almost equal by the number of places.
Args:
diff --git a/tests/files/test_csv_file.py b/tests/files/test_csv_file.py
index ad9bd67..498c3c7 100644
--- a/tests/files/test_csv_file.py
+++ b/tests/files/test_csv_file.py
@@ -1,10 +1,12 @@
"""Test the class for reading and writing CSV files."""
+
import unittest
import zipfile
from typing import Iterator
from unittest.mock import MagicMock, Mock, patch
from cerbernetix.toolbox.files import (
+ CSV_AUTO,
CSV_DIALECT,
CSV_ENCODING,
CSVFile,
@@ -25,14 +27,14 @@
["Jane", "Doe", "20", "Paris"],
]
CSV_LINES_STRING = [
- '"first_name","last_name","age","city"\n',
- '"John","Smith","18","London"\n',
- '"Jane","Doe","20","Paris"\n',
+ "first_name,last_name,age,city\r\n",
+ "John,Smith,18,London\r\n",
+ "Jane,Doe,20,Paris\r\n",
]
CSV_LINES_REDUCED = [
- '"first_name","last_name"\n',
- '"John","Smith"\n',
- '"Jane","Doe"\n',
+ "first_name,last_name\r\n",
+ "John,Smith\r\n",
+ "Jane,Doe\r\n",
]
CSV_STRING = "".join(CSV_LINES_STRING)
@@ -50,7 +52,7 @@ def test_construction_default(self):
self.assertEqual(file.filename, file_path)
self.assertFalse(file.binary)
- self.assertEqual(file.dialect, CSV_DIALECT)
+ self.assertEqual(file.dialect, CSV_AUTO)
self.assertEqual(file.encoding, CSV_ENCODING)
self.assertIsNone(file._file)
self.assertEqual(file._open_args, {"newline": ""})
@@ -230,7 +232,8 @@ def test_close_auto(self, mock_file_open):
CSV_LINES_HEADLESS,
],
["list", {"fieldnames": False}, CSV_LINES_STRING, CSV_LINES_LIST],
- ["auto", {"dialect": "auto"}, CSV_LINES_STRING, CSV_LINES_DICT],
+ ["auto", {"dialect": CSV_AUTO}, CSV_LINES_STRING, CSV_LINES_DICT],
+ ["dialect", {"dialect": CSV_DIALECT}, CSV_LINES_STRING, CSV_LINES_DICT],
]
)
def test_read_file(self, _, params, data, expected):
@@ -295,7 +298,8 @@ def test_read_file_iterator(self, mock_file_open):
CSV_LINES_LIST[1:],
"".join(CSV_LINES_STRING[1:]),
],
- ["auto", {"dialect": "auto"}, CSV_LINES_DICT, CSV_STRING],
+ ["auto", {"dialect": CSV_AUTO}, CSV_LINES_DICT, CSV_STRING],
+ ["dialect", {"dialect": CSV_DIALECT}, CSV_LINES_DICT, CSV_STRING],
]
)
def test_write_file(self, _, params, data, expected):
@@ -325,17 +329,24 @@ def write(line):
mock_file.write.assert_called()
mock_file.close.assert_called_once()
+ @test_cases(
+ [
+ [CSV_AUTO],
+ [CSV_DIALECT],
+ ]
+ )
@patch("builtins.open")
- def test_read(self, mock_file_open):
+ def test_read(self, dialect, mock_file_open):
"""Tests a file can be read line by line."""
file_path = "/root/folder/file"
mock_file = MagicMock()
mock_file.close = Mock()
mock_file.__iter__.return_value = CSV_LINES_STRING
+ mock_file.read.return_value = CSV_STRING
mock_file_open.return_value = mock_file
- file = CSVFile(file_path)
+ file = CSVFile(file_path, dialect=dialect)
self.assertRaises(ValueError, file.read)
@@ -426,6 +437,7 @@ def test_iterator(self, mock_file_open):
mock_file = MagicMock()
mock_file.close = Mock()
mock_file.__iter__.return_value = CSV_LINES_STRING
+ mock_file.read.return_value = CSV_STRING
mock_file_open.return_value = mock_file
file = CSVFile(file_path)
@@ -456,6 +468,7 @@ def test_read_csv_file(self, mock_file_open):
mock_file = MagicMock()
mock_file.close = Mock()
mock_file.__iter__.return_value = CSV_LINES_STRING
+ mock_file.read.return_value = CSV_STRING
mock_file_open.return_value = mock_file
result = read_csv_file(file_path)
@@ -473,6 +486,7 @@ def test_read_csv_file_iterator(self, mock_file_open):
mock_file = MagicMock()
mock_file.close = Mock()
mock_file.__iter__.return_value = CSV_LINES_STRING
+ mock_file.read.return_value = CSV_STRING
mock_file_open.return_value = mock_file
result = read_csv_file(file_path, iterator=True)
@@ -538,8 +552,8 @@ def write(line):
CSV_LINES_LIST,
],
[
- "dialect auto",
- {"dialect": "auto"},
+ "dialect",
+ {"dialect": CSV_DIALECT},
"FOO.CSV",
CSV_STRING,
CSV_LINES_DICT,
diff --git a/tests/files/test_json_file.py b/tests/files/test_json_file.py
index 6d7c2ab..2b42e59 100644
--- a/tests/files/test_json_file.py
+++ b/tests/files/test_json_file.py
@@ -1,10 +1,16 @@
"""Test the class for reading and writing JSON files."""
+
import unittest
from unittest.mock import Mock, patch
from cerbernetix.toolbox.files import (
JSON_ENCODING,
+ JSON_ENSURE_ASCII,
JSON_INDENT,
+ JSON_SEPARATORS,
+ JSON_SKIP_KEYS,
+ JSON_SORT_KEYS,
+ JSON_STRICT,
JSONFile,
read_json_file,
write_json_file,
@@ -12,6 +18,16 @@
JSON_DATA = {"name": "test", "level": 20, "keywords": ["one", "two"], "enabled": True}
JSON_STRING = """{
+ "name": "test",
+ "level": 20,
+ "keywords": [
+ "one",
+ "two"
+ ],
+ "enabled": true
+}"""
+JSON_STRING_PACKED = """{"name":"test","level":20,"keywords":["one","two"],"enabled":true}"""
+JSON_STRING_SORTED = """{
"enabled": true,
"keywords": [
"one",
@@ -36,6 +52,11 @@ def test_construction_default(self):
self.assertEqual(file.filename, file_path)
self.assertFalse(file.binary)
self.assertEqual(file.indent, JSON_INDENT)
+ self.assertEqual(file.separators, JSON_SEPARATORS)
+ self.assertEqual(file.sort_keys, JSON_SORT_KEYS)
+ self.assertEqual(file.skip_keys, JSON_SKIP_KEYS)
+ self.assertEqual(file.ensure_ascii, JSON_ENSURE_ASCII)
+ self.assertEqual(file.strict, JSON_STRICT)
self.assertEqual(file.encoding, JSON_ENCODING)
self.assertIsNone(file._file)
self.assertEqual(file._open_args, {})
@@ -45,13 +66,33 @@ def test_construction_params(self):
file_path = "/root/folder/file"
encoding = "ascii"
indent = 2
+ separators = (",", ":")
+ sort_keys = True
+ skip_keys = True
+ ensure_ascii = True
+ strict = True
newline = "\n"
- file = JSONFile(file_path, encoding=encoding, indent=indent, newline=newline)
+ file = JSONFile(
+ file_path,
+ encoding=encoding,
+ indent=indent,
+ separators=separators,
+ sort_keys=sort_keys,
+ skip_keys=skip_keys,
+ ensure_ascii=ensure_ascii,
+ strict=strict,
+ newline=newline,
+ )
self.assertEqual(file.filename, file_path)
self.assertFalse(file.binary)
self.assertEqual(file.indent, indent)
+ self.assertEqual(file.separators, separators)
+ self.assertEqual(file.sort_keys, sort_keys)
+ self.assertEqual(file.skip_keys, skip_keys)
+ self.assertEqual(file.ensure_ascii, ensure_ascii)
+ self.assertEqual(file.strict, strict)
self.assertEqual(file.encoding, encoding)
self.assertIsNone(file._file)
self.assertEqual(file._open_args, {"newline": newline})
@@ -217,6 +258,44 @@ def test_write_file(self, mock_file_open):
mock_file.write.assert_called_with(JSON_STRING)
mock_file.close.assert_called_once()
+ @patch("builtins.open")
+ def test_write_file_sorted_keys(self, mock_file_open):
+ """Tests a file can be written at once."""
+ file_path = "/root/folder/file"
+
+ count = len(JSON_STRING)
+ mock_file = Mock()
+ mock_file.write = Mock(return_value=count)
+ mock_file.close = Mock()
+ mock_file_open.return_value = mock_file
+
+ file = JSONFile(file_path, sort_keys=True)
+
+ self.assertEqual(file.write_file(JSON_DATA), count)
+
+ mock_file_open.assert_called_once()
+ mock_file.write.assert_called_with(JSON_STRING_SORTED)
+ mock_file.close.assert_called_once()
+
+ @patch("builtins.open")
+ def test_write_file_packed(self, mock_file_open):
+ """Tests a file can be written at once."""
+ file_path = "/root/folder/file"
+
+ count = len(JSON_STRING)
+ mock_file = Mock()
+ mock_file.write = Mock(return_value=count)
+ mock_file.close = Mock()
+ mock_file_open.return_value = mock_file
+
+ file = JSONFile(file_path, indent=None, separators=(",", ":"))
+
+ self.assertEqual(file.write_file(JSON_DATA), count)
+
+ mock_file_open.assert_called_once()
+ mock_file.write.assert_called_with(JSON_STRING_PACKED)
+ mock_file.close.assert_called_once()
+
@patch("builtins.open")
def test_read(self, mock_file_open):
"""Tests a file can be read."""
diff --git a/tests/math/test_combination.py b/tests/math/test_combination.py
index 2adcd2c..41316b8 100644
--- a/tests/math/test_combination.py
+++ b/tests/math/test_combination.py
@@ -1,7 +1,13 @@
"""Test the set of functions for working with combinations."""
+
import unittest
+from typing import Iterator
-from cerbernetix.toolbox.math import get_combination_from_rank, get_combination_rank
+from cerbernetix.toolbox.math import (
+ get_combination_from_rank,
+ get_combination_rank,
+ get_combinations,
+)
from cerbernetix.toolbox.testing import test_cases
@@ -205,3 +211,138 @@ def test_get_combination_from_rank_error(self):
"""Test get_combination_from_rank errors."""
self.assertRaises(ValueError, lambda: get_combination_from_rank(-1, 2))
self.assertRaises(ValueError, lambda: get_combination_from_rank(0, -1, 4))
+
+ def test_get_combinations_int(self):
+ """Test get_combinations."""
+ self.assertIsInstance(get_combinations(5, 3), Iterator)
+ self.assertEqual(next(get_combinations(5, 3)), [0, 1, 2])
+ self.assertEqual(
+ list(get_combinations(5, 3)),
+ [
+ [0, 1, 2],
+ [0, 1, 3],
+ [0, 2, 3],
+ [1, 2, 3],
+ [0, 1, 4],
+ [0, 2, 4],
+ [1, 2, 4],
+ [0, 3, 4],
+ [1, 3, 4],
+ [2, 3, 4],
+ ],
+ )
+
+ def test_get_combinations_offset(self):
+ """Test get_combinations."""
+ self.assertIsInstance(get_combinations(5, 3, offset=1), Iterator)
+ self.assertEqual(next(get_combinations(5, 3, offset=1)), [1, 2, 3])
+ self.assertEqual(
+ list(get_combinations(5, 3, offset=1)),
+ [
+ [1, 2, 3],
+ [1, 2, 4],
+ [1, 3, 4],
+ [2, 3, 4],
+ [1, 2, 5],
+ [1, 3, 5],
+ [2, 3, 5],
+ [1, 4, 5],
+ [2, 4, 5],
+ [3, 4, 5],
+ ],
+ )
+
+ def test_get_combinations_values(self):
+ """Test get_combinations."""
+ values = [1, 2, 4, 8, 16]
+ self.assertIsInstance(get_combinations(values, 3), Iterator)
+ self.assertEqual(next(get_combinations(values, 3)), [1, 2, 4])
+ self.assertEqual(
+ list(get_combinations(values, 3)),
+ [
+ [1, 2, 4],
+ [1, 2, 8],
+ [1, 4, 8],
+ [2, 4, 8],
+ [1, 2, 16],
+ [1, 4, 16],
+ [2, 4, 16],
+ [1, 8, 16],
+ [2, 8, 16],
+ [4, 8, 16],
+ ],
+ )
+
+ def test_get_combinations_indexes(self):
+ """Test get_combinations with a dictionary and a list of indexes."""
+ values = {"1": 1, "2": 2, "4": 4, "8": 8, "16": 16}
+ indexes = ["1", "2", "4", "8", "16"]
+ self.assertIsInstance(get_combinations(values, 3, indexes=indexes), Iterator)
+ self.assertEqual(next(get_combinations(values, 3, indexes=indexes)), [1, 2, 4])
+ self.assertEqual(
+ list(get_combinations(values, 3, indexes=indexes)),
+ [
+ [1, 2, 4],
+ [1, 2, 8],
+ [1, 4, 8],
+ [2, 4, 8],
+ [1, 2, 16],
+ [1, 4, 16],
+ [2, 4, 16],
+ [1, 8, 16],
+ [2, 8, 16],
+ [4, 8, 16],
+ ],
+ )
+
+ def test_get_combinations_range(self):
+ """Test get_combinations with a range."""
+ values = [1, 2, 4, 8, 16]
+ self.assertIsInstance(get_combinations(values, 3, start=2, stop=8, step=2), Iterator)
+ self.assertEqual(
+ list(get_combinations(values, 3, start=2, stop=8, step=2)),
+ [
+ [1, 4, 8],
+ [1, 2, 16],
+ [2, 4, 16],
+ ],
+ )
+ self.assertEqual(
+ list(get_combinations(values, 3, start=8, stop=2, step=2)),
+ [
+ [2, 8, 16],
+ [2, 4, 16],
+ [1, 2, 16],
+ ],
+ )
+ self.assertEqual(
+ list(get_combinations(values, 3, start=20, stop=-1)),
+ [
+ [4, 8, 16],
+ [2, 8, 16],
+ [1, 8, 16],
+ [2, 4, 16],
+ [1, 4, 16],
+ [1, 2, 16],
+ [2, 4, 8],
+ [1, 4, 8],
+ [1, 2, 8],
+ [1, 2, 4],
+ ],
+ )
+ self.assertEqual(
+ list(get_combinations(values, 3, start=20)),
+ [
+ [4, 8, 16],
+ ],
+ )
+
+ def test_get_combinations_empty(self):
+ """Test get_combinations returns empty."""
+ self.assertEqual(list(get_combinations(0, 3)), [])
+ self.assertEqual(list(get_combinations(5, 0)), [])
+
+ def test_get_combinations_error(self):
+ """Test get_combinations errors."""
+ self.assertRaises(ValueError, lambda: next(get_combinations(5, start=-10)))
+ self.assertRaises(ValueError, lambda: next(get_combinations(5, stop=-10)))
diff --git a/tests/math/test_utils.py b/tests/math/test_utils.py
new file mode 100644
index 0000000..3b73efb
--- /dev/null
+++ b/tests/math/test_utils.py
@@ -0,0 +1,28 @@
+"""Test the set of helper functions related to math."""
+
+import unittest
+
+from cerbernetix.toolbox.math import minmax, quantity
+
+
+class TestUtils(unittest.TestCase):
+ """Test suite for the set of helper functions related to math."""
+
+ def test_minmax(self):
+ """Test minmax."""
+ self.assertEqual(minmax([1]), (1, 1))
+ self.assertEqual(minmax([1, 2]), (1, 2))
+ self.assertEqual(minmax([2, 1]), (1, 2))
+ self.assertEqual(minmax(*[1, 2]), (1, 2))
+ self.assertEqual(minmax(*[2, 1]), (1, 2))
+ self.assertEqual(minmax(1, 2), (1, 2))
+ self.assertEqual(minmax(2, 1), (1, 2))
+ self.assertEqual(minmax(3, 2, 6, 5, 4), (2, 6))
+
+ def test_quantity(self):
+ """Test quantity."""
+ self.assertEqual(quantity(5, 10), 5)
+ self.assertEqual(quantity(0.1, 10), 1)
+ self.assertEqual(quantity(1.5, 10), 1)
+ self.assertEqual(quantity(-0.2, 10), 0)
+ self.assertEqual(quantity(30, 10), 10)