diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c9162b9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 4 +insert_final_newline = true +end_of_line = lf + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 diff --git a/.github/.templateMarker b/.github/.templateMarker new file mode 100644 index 0000000..5e3a3e0 --- /dev/null +++ b/.github/.templateMarker @@ -0,0 +1 @@ +KOLANICH/python_project_boilerplate.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..89ff339 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-type: "all" diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..7fe33b3 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,15 @@ +name: CI +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - name: typical python workflow + uses: KOLANICH-GHActions/typical-python-workflow@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4bd614c --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__ +*.pyc +*.pyo +/*.egg-info +/build +/dist +/.eggs +/monkeytype.sqlite3 +/.coverage +*.py,cover diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..552f17f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,50 @@ +image: registry.gitlab.com/kolanich-subgroups/docker-images/fixed_python:latest +stages: + - build + - trigger + - test + +variables: + DOCKER_DRIVER: overlay2 + SAST_ANALYZER_IMAGE_TAG: latest + SAST_DISABLE_DIND: "true" + +include: + - template: SAST.gitlab-ci.yml + #- template: DAST.gitlab-ci.yml + #- template: License-Management.gitlab-ci.yml + #- template: Container-Scanning.gitlab-ci.yml + #- template: Dependency-Scanning.gitlab-ci.yml + - template: Code-Quality.gitlab-ci.yml + +build: + tags: + - shared + - linux + stage: build + variables: + GIT_DEPTH: "1" + PYTHONUSERBASE: ${CI_PROJECT_DIR}/python_user_packages + + before_script: + - export PATH="$PATH:$PYTHONUSERBASE/bin" # don't move into `variables` + + cache: + paths: + - $PYTHONUSERBASE + + script: + - python3 setup.py bdist_wheel + - mkdir wheels + - mv ./dist/*.whl ./wheels/rangeslicetools-0.CI-py3-none-any.whl + - pip3 install --user --upgrade ./wheels/rangeslicetools-0.CI-py3-none-any.whl + - coverage run --branch --source=rangeslicetools -m pytest --junitxml=./rspec.xml ./tests/tests.py + - coverage report -m || true + - coverage xml + + artifacts: + paths: + - wheels + reports: + junit: rspec.xml + cobertura: ./coverage.xml diff --git a/Code_Of_Conduct.md b/Code_Of_Conduct.md new file mode 100644 index 0000000..2b781c7 --- /dev/null +++ b/Code_Of_Conduct.md @@ -0,0 +1 @@ +No codes of conduct! diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..20f0fa8 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include UNLICENSE +include *.md +include tests +include .editorconfig diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 0000000..9cb4070 --- /dev/null +++ b/ReadMe.md @@ -0,0 +1,88 @@ +rangeslicetools.py [![Unlicensed work](https://raw.githubusercontent.com/unlicense/unlicense.org/master/static/favicon.png)](https://unlicense.org/) +================== +~~[wheel (GHA via nightly.link)](https://nightly.link/KOLANICH-libs/rangeslicetools.py/workflows/CI/master/rangeslicetools-0.CI-py3-none-any.whl)~~ +~~[wheel (GitLab)](https://gitlab.com/KOLANICH/rangeslicetools.py/-/jobs/artifacts/master/raw/dist/rangeslicetools-0.CI-py3-none-any.whl?job=build)~~ +~~[![GitLab Build Status](https://gitlab.com/KOLANICH/rangeslicetools.py/badges/master/pipeline.svg)](https://gitlab.com/KOLANICH/rangeslicetools.py/pipelines/master/latest)~~ +~~![GitLab Coverage](https://gitlab.com/KOLANICH/rangeslicetools.py/badges/master/coverage.svg)~~ +~~[![GitHub Actions CI](https://github.com/KOLANICH-libs/rangeslicetools.py/workflows/CI/badge.svg)](https://github.com/KOLANICH-libs/rangeslicetools.py/actions/)~~ +![N∅ dependencies](https://shields.io/badge/-N%E2%88%85_deps!-0F0) +[![Code style: antiflash](https://img.shields.io/badge/code%20style-antiflash-FFF.svg)](https://github.com/KOLANICH-tools/antiflash.py) + +This is a library to manipulate python `range` and `slice` objects. The objects of these classes have the same internal structure but a bit different semantics and set of available methods. Unfortunately these objects include no methods to manipulate them and unfortunately they cannot be subclassed. + +So I have implemented a set of functions to manipulate these objects. Their names follow the following conventions: + +* All these **functions** names begin from `s` which stands there for `slice`, even though they will work for ranges too. + +* If a name ends with `_`, it is a generator, otherwise it returns a `list`. + +**WARNING: FOR NEGATIVE-DIRECTED `slice`s/`range`s `step` is MANDATORY. It is BY DESIGN of python and we follow this convention too. Always set `step` for all the ranges if you may deal with negative-directed ones.** + +For the info on usage see the docstrings and tests. And READ the source code, it is SMALL ENOUGH. + +Features +-------- +Notation and terms: + +* For briefness we `r = range` and `s = slice` +* When we say `range`, it also works for a `slice` and in the opposite direction too. + +Conventions: + +* When we say `range` or `slice`, it usually works also for a sequence of them. See type annotations to check if a specific function supports sequences of ranges. +* There may be undefined behavior (UB): + + * negative-directed ranges without negative `step` is always UB; + * non-integer numbers usage is always UB;; + * operations on the ranges having different `abs(step)`; + * empty ranges (ranges of zero length) produced **may** (but not guaranteed) be eliminated; + * operations on the ranges having opposite direction may be UB. It should be stated in docstrings if it is the case. + +* basic operations + + * type conversion: + * `sAny2Type(s(1, 10), r) -> r(1, 10)` `sAny2Type(r(1, 10), s) -> s(1, 10)` + * `range2slice` and `slice2range` do the same. + + * get a length of a range `slen(s(2, 4)) -> 2`. For usual ranges just `len` works, but our func works also for slices and seqs. + * get direction (a director vector in fact) of a slice `sdir(r(10, 1, -2)) -> -1` + * reverse direction of a slice: `srev(r(0, 10)) -> r(9, -1, -1)` + * make 2 `slice`s of the same direction: `sdirect(r(25, 5, -5), s(1, 10)) -> slice(9, 0, -1)` + * make a slice positive-directed: `snormalize(r(25, 5, -5)) -> range(10, 30, 5)` + +* checking conditions about ranges: + + * check if one range is fully within another range `swithin(r(0, 10), r(1, 5)) -> true` + * check if one range is overlaps another range `soverlaps(r(0, 10), r(2, -6, -1)) -> true` + +* splitting + + * sprit a range at certain points: `ssplit(r(5, 13), (7, 8, 12)) -> [r(5, 7), r(7, 8), r(8, 12), r(12, 13)]` + * split a range into pieces of certain lengths `soffset_split(r(5, 13), (2, 3, 7)) -> [r(5, 7), r(7, 8), r(8, 12), r(12, 13)]` + * split a range into pieces of a certain length `schunks(r(5, 13), 3) -> [r(5, 8), r(8, 11), r(11, 13)]` + * split multiple sequences of ranges of the same total length into the chunks of equal length, in other words - align split points of all the sequence - see the docs for `salign` function. + +* join/merge **adjacent** (non-overlapping!) ranges into one: `sjoin([r(0, 8), r(8, 9), r(9, 10), r(12, 15)]) -> [r(0, 10), r(12, 15)]` + +* set operations + + * compute a diff of 2 ranges: `sdiff` + * subtract 2 ranges: `ssub(r(1, 10), r(5, -10, -1)) -> [r(6, 10)]` + * union 2 ranges: `sunion(r(1, 10), r(7, 20)) -> [r(1, 20)]` + +* intersections querying via a [range tree](https://en.wikipedia.org/wiki/Range_tree) +* remapping via a `SliceSequence` +* visualization + + +Examples +-------- +* https://codeberg.org/kaitaiStructCompile/Endianness.py +* tests + + +Similar projects +---------------- + +* [intervaltree](https://github.com/chaimleib/intervaltree) +* [rangetree](https://github.com/nanobit/rangetree) diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..efb9808 --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e830973 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["setuptools>=61.2.0", "wheel", "setuptools_scm[toml]>=3.4.3"] +build-backend = "setuptools.build_meta" + +[project] +name = "rangeslicetools" +authors = [{name = "KOLANICH"}] +description = "A library to manipulate ranges and slices" +readme = "ReadMe.md" +keywords = ["range", "slice"] +license = {text = "Unlicense"} +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Development Status :: 4 - Beta", + "Environment :: Other Environment", + "Intended Audience :: Developers", + "License :: Public Domain", + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Python Modules", +] +requires-python = ">=3.4" +dynamic = ["version"] + +[project.urls] +Homepage = "https://codeberg.org/KOLANICH-libs/rangeslicetools.py" +"Bug Tracker" = "https://codeberg.org/KOLANICH-libs/rangeslicetools.py/issues" + +[tool.setuptools] +zip-safe = true +packages = ["rangeslicetools"] + +[tool.setuptools_scm] diff --git a/rangeslicetools/__init__.py b/rangeslicetools/__init__.py new file mode 100644 index 0000000..778618b --- /dev/null +++ b/rangeslicetools/__init__.py @@ -0,0 +1,61 @@ +import sys +import types +import typing +from functools import wraps + +from . import diff, utils + + +def _createWrapped(f: typing.Callable) -> typing.Callable: + @wraps(f) + def f1(*args, **kwargs): + return tuple(f(*args, **kwargs)) + + f1.__annotations__["return"] = utils.SliceRangeListT + return f1 + + +def _wrapModuleProp(module, k, v, _all_) -> None: + if k[0] != "_": + _all_.append(k) + + if k[0] == "s" and k[-1] == "_": + if "return" not in v.__annotations__: + raise ValueError("Annotate the return type in " + v.__qualname__ + "!") + + modName = k[:-1] + if v.__annotations__["return"] is module.SliceRangeSeqT and modName not in module.__dict__: + module.__dict__[modName] = _createWrapped(v) + _all_.append(modName) + + module.__dict__[k] = v + + +def _wrap(module) -> None: + _all_ = getattr(module, "__all__", None) + if _all_ is None: + _all_ = [] + for k, v in tuple(module.__dict__.items()): + _wrapModuleProp(module, k, v, _all_) + _all_.append(k) + else: + _all_ = list(_all_) + for k in list(_all_): + v = getattr(module, k) + _wrapModuleProp(module, k, v, _all_) + _all_ = tuple(sorted(_all_)) + + module.__all__ = tuple(_all_) + + sys.modules[module.__name__] = module + + +_wrap(utils) +_wrap(diff) + + +# pylint: disable=wrong-import-position +from .utils import * # noqa +from .diff import * # noqa +from .tree import * # noqa +from .viz import * # noqa diff --git a/rangeslicetools/diff.py b/rangeslicetools/diff.py new file mode 100644 index 0000000..39de726 --- /dev/null +++ b/rangeslicetools/diff.py @@ -0,0 +1,219 @@ +import typing +from enum import IntFlag + +from .utils import SliceRangeSeqT, SliceRangeT, _isNegative, _sdirect, sjoin_, slen, snormalize + +__all__ = ("SDiffAutomata", "sdiff", "sdiffSelectPred_", "sdiffSelect_", "ssub2_", "ssub", "sunion_", "sgap", "sdist") + +# pylint: disable=too-few-public-methods + + +class SDiffAutomata: + """ + Stores partiton of the space into the areas where ranges overlap or not. + """ + + __slots__ = ("state",) + + class State(IntFlag): + """ + |<---| + ee | en | ne + + |--->| + ne | en | ee + """ + + notEntered = 0 + entered = 1 + exited = 2 + + def __init__(self) -> None: + self.state = self.__class__.State.notEntered + + def process(self, p: int, isExit: bool) -> None: + if self.state & self.__class__.State.entered: + if isExit: + self.state |= self.__class__.State.exited + else: + raise ValueError((p, isExit, self.state)) + else: + if not isExit: + self.state |= self.__class__.State.entered + else: + raise ValueError((p, isExit, self.state)) + + +sdiffBackDirRemap = { + SDiffAutomata.State.entered | SDiffAutomata.State.exited: SDiffAutomata.State.notEntered, + SDiffAutomata.State.entered: SDiffAutomata.State.entered, + SDiffAutomata.State.notEntered: SDiffAutomata.State.entered | SDiffAutomata.State.exited, +} + + +IntersectionStateT = typing.Tuple[SDiffAutomata.State, SDiffAutomata.State] + + +class DiffFSMPoint: + __slots__ = ("pos", "rangeId", "isEnd") + + def __init__(self, pos, rangeId, isEnd): + self.pos = pos + self.rangeId = rangeId + self.isEnd = isEnd + + @property + def tuple(self): + return tuple(getattr(self, k) for k in self.__class__.__slots__) + + def __repr__(self): + return self.__class__.__name__ + repr(self.tuple) + + def __eq__(self, another): + return self.tuple == another.tuple + + def __hash__(self): + return hash(self.tuple) + + def __gt__(self, another): + return self.pos > another.pos + + +def _computeEndpointRepresentation(rngs: SliceRangeSeqT): + points = [] + for i, el in enumerate(rngs): + points.extend(( + DiffFSMPoint(el.start, i, False), + DiffFSMPoint(el.stop, i, True), + )) + points.sort() + return points + + +def _endpointsToMatrix(rs, points): + matrix = {} + + az = [SDiffAutomata() for i in range(len(rs))] + + state = tuple(a.state for a in az) + + for pt in points: + if state not in matrix: + matrix[state] = [None, None] + matrix[state][1] = pt.pos + az[pt.rangeId].process(pt.pos, pt.isEnd) + state = tuple(a.state for a in az) + if state not in matrix: + matrix[state] = [None, None] + matrix[state][0] = pt.pos + + del matrix[(SDiffAutomata.State.notEntered, SDiffAutomata.State.notEntered)] + del matrix[(SDiffAutomata.State.entered | SDiffAutomata.State.exited, SDiffAutomata.State.entered | SDiffAutomata.State.exited)] + return matrix + + +def getDirectorRangeIndex(s0: SDiffAutomata.State, s1: SDiffAutomata.State) -> int: + if (s1 & SDiffAutomata.State.entered and not s1 & SDiffAutomata.State.exited) and not (s0 & SDiffAutomata.State.entered and not s0 & SDiffAutomata.State.exited): + return 1 + return 0 + + +def _postProcessMatrix(rs, matrix): + newMatrix = type(matrix)() + + shouldRemapComp = _isNegative(rs) + + for k in matrix: + el = matrix[k] + if el[0] == el[1]: + continue + + directorIdx = getDirectorRangeIndex(*k) + dR = rs[directorIdx] + if shouldRemapComp[directorIdx]: + el = (el[1] - 1, el[0] - 1) + + k = tuple((sdiffBackDirRemap[comp] if shouldRemapComp[i] else comp) for i, comp in enumerate(k)) + + newMatrix[k] = dR.__class__(el[0], el[1], dR.step) + return newMatrix + + +def sdiff(s0: SliceRangeT, s1: SliceRangeT) -> typing.Dict[IntersectionStateT, SliceRangeT]: + """Computes a difference of 2 slices/ranges. More than 2 is not yet implemented, though planned.""" + # pylint: disable=too-many-locals + rs = (s0, s1) + canonicalized = snormalize(rs) + endpoints = _computeEndpointRepresentation(canonicalized) + return _postProcessMatrix(rs, _endpointsToMatrix(rs, endpoints)) + + +def sdiffSelectPred_(s1: SliceRangeT, s2: SliceRangeT, pred) -> SliceRangeSeqT: + """Computes differences and takes states.""" + res = sdiff(s1, s2) + for k, v in res.items(): + if pred(k): + yield v + + +def sdiffSelect_(s1: SliceRangeT, s2: SliceRangeT, keys: list, neg: bool = False) -> SliceRangeSeqT: + """Computes differences and takes states.""" + res = sdiff(s1, s2) + for k in keys: + v = res.get(k, None) + if (v is not None) != neg: + yield res[k] + + +def ssub2_(s1: SliceRangeT, s2: SliceRangeT) -> SliceRangeSeqT: + """Subtracts 2 ranges""" + S = SDiffAutomata.State + return sdiffSelect_(s1, s2, [(S.entered, S.notEntered), (S.entered, S.entered | S.exited)]) + + +def ssub(s1: SliceRangeT, *rest: typing.Iterable[SliceRangeT]) -> SliceRangeSeqT: + """Subtracts n >= 1 ranges""" + res = (s1,) + for i, el2 in enumerate(rest): + res1 = () + for el1 in res: + res1 += ssub2(el1, el2) # pylint:disable=undefined-variable + res = res1 + return res + + +def sunion_(s1: SliceRangeT, s2: SliceRangeT) -> SliceRangeSeqT: + """Unions 2 ranges. + See also `shull`, which is faster, but dumber. + """ + S = SDiffAutomata.State + + def pred(k): + a, b = k + return (a & S.entered and not a & S.exited) or (b & S.entered and not b & S.exited) + + toJoin = sorted(sdiffSelectPred_(s1, s2, pred), key=lambda e: snormalize(e).start) + return sjoin_(toJoin) + + +def sgap(s1: SliceRangeT, s2: SliceRangeT) -> typing.Optional[SliceRangeT]: + """Returns a gap between 2 ranges""" + S = SDiffAutomata.State + + def pred(k): + a, b = k + return (a & S.exited or not a & S.entered) and (b & S.exited or not b & S.entered) + + try: + return next(sdiffSelectPred_(s1, s2, pred)) + except StopIteration: + return None + + +def sdist(s1: SliceRangeT, s2: SliceRangeT) -> int: + """Returns length of a gap between 2 ranges""" + gap = sgap(s1, s2) + if gap: + return slen(gap) + + return 0 diff --git a/rangeslicetools/py.typed b/rangeslicetools/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/rangeslicetools/tree.py b/rangeslicetools/tree.py new file mode 100644 index 0000000..ab8684e --- /dev/null +++ b/rangeslicetools/tree.py @@ -0,0 +1,494 @@ +import typing +from abc import abstractmethod +from collections.abc import Mapping + +from .diff import SDiffAutomata, sdiff, sdist, ssub +from .utils import SliceRangeListT +from .utils import SliceRangeT, _scollapse, isInstArg, salign, salign_, sAny2Type, sjoin, sjoin_, slen, slice2range, soverlaps # pylint: disable=no-name-in-module + +__all__ = ("IndexProto", "KeyLeaf", "ValueLeaf", "_RangesTree", "RangesTree", "SliceSequence", "mergeRangesInTreeLookupResult", "FuzzySingleLookupResult", "SingleLookupResult") + + +# pylint: disable=too-few-public-methods +class IndexProto(Mapping): + __slots__ = () + + # in fact a slot + #@property + #@abstractmethod + #def index(self): + # raise NotImplementedError() + + def keys(self): + for n in self: + yield n.index + + def values(self): + for n in self: + yield n.indexee + + def items(self): + for n in self: + yield n.index, n.indexee + + def __iter__(self) -> typing.Iterator["ValueLeaf"]: + yield self + + def __len__(self) -> int: + return 1 + + +class NodeProto(IndexProto): + __slots__ = () + + @property + @abstractmethod + def indexee(self): + raise NotImplementedError() + + +class ILeaf(NodeProto): + + """A regular leaf.""" + + __slots__ = ("index",) + + def __init__(self, index: SliceRangeT) -> None: + self.index = index + + def __repr__(self): + return repr(self.index) + ":[" + repr(self.indexee) + "]" + + def cmpTuple(self) -> typing.Tuple[SliceRangeT, SliceRangeT]: + return (self.indexee, self.index) + + def __eq__(self, other: "KeyLeaf") -> bool: + return self.cmpTuple() == other.cmpTuple() + + @property + def indexee(self) -> SliceRangeListT: + return self.index + + def __getitem__(self, q: SliceRangeT) -> "LookupResult": + if soverlaps(self.index, q): + yield self + + def getPath(self, q: SliceRangeT, path: typing.Tuple[int, ...] = ()) -> typing.Iterator["SingleLookupResult"]: + if soverlaps(self.index, q): + yield SingleLookupResult(self, path) + + +LookupResult = typing.Iterable["ILeaf"] +LookupPath = typing.Iterable[int] + + +class SingleLookupResult: + __slots__ = ("node", "path") + + def __init__(self, node: ILeaf, path: LookupPath) -> None: + self.node = node + self.path = path + + @property + def dist(self) -> int: + return 0 + + @property + def query(self) -> SliceRangeT: + return self.node.index + + def toTuple(self) -> typing.Tuple[ILeaf, LookupPath, int, typing.Optional[SliceRangeT]]: + return (self.node, self.path, self.dist, self.query) + + def __eq__(self, other: "SingleLookupResult") -> bool: + return self.toTuple() == other.toTuple() + + def __repr__(self) -> str: + return self.__class__.__name__ + "(" + repr(self.node) + ", " + repr(self.path) + ")" + + +class FuzzySingleLookupResult(SingleLookupResult): + __slots__ = ("query", "dist") + + def __init__(self, node: ILeaf, path: LookupPath, dist: int, query: typing.Optional[SliceRangeT] = None) -> None: + super().__init__(node, path) + self.dist = dist + self.query = query + + def __repr__(self): + return super().__repr__()[:-1] + ", " + repr(self.dist) + ", " + repr(self.query) + ")" + + +class KeyLeaf(ILeaf): + __slots__ = () + + @property + def indexee(self) -> SliceRangeListT: + return self.index + + +class ValueLeaf(ILeaf): + __slots__ = ("_indexee",) + + KEY_LEAF_TYPE = KeyLeaf + + def __init__(self, index: SliceRangeT, indexee: SliceRangeT) -> None: + super().__init__(index) + self._indexee = indexee + + @property + def indexee(self) -> LookupResult: + return self._indexee + + @indexee.setter + def indexee(self, v): + self._indexee = v + + +def get_lowest_metered(cur: "RangesTree", metric: typing.Callable) -> LookupResult: + cur = (cur, None) + path = () + while not isinstance(cur[0], ILeaf): + cur = min( + ( + (el, metric(el.index), i) for i, el in enumerate(cur[0].children) + ), + key=lambda p: p[1] + ) + path += (cur[2], ) + return FuzzySingleLookupResult(cur[0], path, cur[1]) + + +class _RangesIndexTree(IndexProto): + + """Allows to store sequences of slices and then query the slices overlapping with the given slice. Returns the whole slices, not their parts.""" + + __slots__ = ("_left", "_right", "index") + + INDEX_NODE = ValueLeaf + + def __init__(self) -> None: + self._left = None + self._right = None + self.index = None + + def updateRange(self) -> None: + if self._left is not None: + if self._right is not None: + ress = sjoin((self._left.index, self._right.index)) + self.index = type(ress[0])(ress[0].start, ress[-1].stop, ress[0].step) + else: + self.index = self._left.index + else: + if self._right is not None: + self.index = self._right.index + else: + raise ValueError("All nodes are empty, cannot compute tree range") + + @property + def left(self): + return self._left + + @left.setter + def left(self, v): + self._left = v + self.updateRange() + + @property + def right(self): + return self._right + + @right.setter + def right(self, v): + self._right = v + self.updateRange() + + @property + def children(self): + return (self.left, self.right) + + @children.setter + def children(self, v): + self.left, self.right = v + + def setChild(self, idx: int, newV: IndexProto) -> None: + if idx: + self.right = newV + else: + self.left = newV + + def __iter__(self) -> None: + for el in self.children: + yield from el + + def __len__(self) -> int: + return sum(len(el) for el in self.children if el) + + def __repr__(self) -> str: + return ( + repr(self.index) + "[" + + repr(self.left) + + ", " + + repr(self.right) + + "]" + ) + + def get_lowest_metered(self, metric: typing.Callable) -> LookupResult: + return get_lowest_metered(self, metric) + + def get_closest(self, q: SliceRangeT) -> typing.List[SingleLookupResult]: + intersecting = tuple(self.getPath(q)) + res = list(intersecting) + fuzzyToMatch = ssub(q, *tuple(el.node.index for el in intersecting)) + for el in fuzzyToMatch: + + def comparator(m): + return sdist(m, q) + + resPart = self.get_lowest_metered(comparator) + resPart.query = el + res.append(resPart) + return res + + @classmethod + def build(cls, index: SliceRangeListT, data: typing.Iterable[typing.Any]) -> typing.Union[ILeaf, "RangesTree"]: + assert len(index) == len(data) + return cls._build(index=index, data=data) + + @classmethod + def _build(cls, index: SliceRangeListT, data: typing.Optional[typing.Iterable[typing.Any]] = None) -> typing.Union[KeyLeaf, "_RangesIndexTree"]: + #ic(__class__.__name__ + ".build", index, data) + + if not isinstance(index, isInstArg): + count = len(index) + root = cls() + if count > 1: + mid = count // 2 + leftIdx, rightIdx = index[:mid], index[mid:] + if len(leftIdx) == 1: + leftIdx = leftIdx[0] + if len(rightIdx) == 1: + rightIdx = rightIdx[0] + + #ic(mid, ranges) + if data is not None: + leftRngs, rightRngs = data[:mid], data[mid:] + #ic(leftRngs, rightRngs) + else: + leftRngs, rightRngs = None, None + + root.left = cls._build(index=leftIdx, data=leftRngs) + root.right = cls._build(index=rightIdx, data=rightRngs) + return root + else: + index = (index,) + + if data: + data = _scollapse(data) + return cls.INDEX_NODE(index[0], data) + + return cls.INDEX_NODE.KEY_LEAF_TYPE(index[0]) + + def getPath(self, q: SliceRangeT, path: typing.Tuple[int, ...] = ()) -> None: + if soverlaps(self.index, q): + for i, ch in enumerate(self.children): + yield from ch.getPath(q, path + (i,)) + + def __getitem__(self, q: SliceRangeT) -> LookupResult: + for el in self.getPath(q): + yield el.node + + def getByPath(self, path): + cur = self + for el in path: + cur = cur.children[el] + return cur + + def getNodesInPath(self, path: typing.Tuple[int]) -> typing.Iterator["RangesTree"]: + cur = self + yield cur + for el in path: + cur = cur.children[el] + yield cur + yield cur + + def _insertRelated1(self, parent, others, nod): + other = others[0] + for othersEl in others: + newParent = self.__class__() + if k == v: + newLeaf = KeyLeaf(k) + else: + newLeaf = ValueLeaf(k, v) + if nod.index.start < other.start: + nod.index = nod.index.__class__(nod.index.start, other.start) + newParent.children = (nod, newLeaf) + else: + nod.index = nod.index.__class__(other.end, nod.index.end) + newParent.children = (newLeaf, nod) + + parent.setChild(el.path[-1], newParent) + + def _insertRelated2(self, others, nod, kLeft, vLeft, kRight, vRight): + newParent1 = self.__class__() + newParent1.right = newParent2 = self.__class__() + + if kLeft == vLeft: + newLeafLeft = KeyLeaf(kLeft) + else: + newLeafLeft = ValueLeaf(kLeft, vLeft) + + if kRight == vRight: + newLeafRight = KeyLeaf(kRight) + else: + newLeafRight = ValueLeaf(kRight, vRight) + newLeafLeft.index = others[0] + newLeafRight.index = others[1] + middle = nod + middle.index = k + middle.data = v + + def __setitem__(self, k: SliceRangeT, v: SliceRangeT) -> None: + els = self.get_closest(k) + # print("found", els) + if len(els) > 1: + raise NotImplementedError("Value set overlaps multiple leaves: " + repr(els) + ". Not yet implemented, set the leaves individually.") + + if len(els) == 1: + for el in els: + path = tuple(self.getNodesInPath(el.path[:-1])) + parent = path[-1] + nod = el.node + + if isinstance(el, FuzzySingleLookupResult): + # intersecting leaf is not found, found closest leaf. We spawn a new node and create a subtree. + + newParent = self.__class__() + if k == v: + newLeaf = KeyLeaf(k) + else: + newLeaf = ValueLeaf(k, v) + if nod.index.start < k.start: + newParent.children = (nod, newLeaf) + else: + newParent.children = (newLeaf, nod) + + parent.setChild(el.path[-1], newParent) + + for pathComp in reversed(path): + pathComp.updateRange() + elif isinstance(el, SingleLookupResult): + if el.node.index == k: + if isinstance(el.node, ValueLeaf): + el.node.indexee = v + elif sinstance(el.node, KeyLeaf): + replacementLeaf = ValueLeaf(k, v) + parent.setChild(el.path[-1], replacementLeaf) + else: + raise NotImplementedError("Something strange happened: only leaves must be found") + else: + if soverlaps(el.index, k): + others = ssub(k, el.index) + if others: + if len(others) > 1: + self._insertRelated1(parent, others, nod) + elif len(others) == 2: + self._insertRelated2(others, nod, kLeft, vLeft, kRight, vRight) + else: + raise NotImplementedError("Too many `others`: " + repr(others)) + else: + el.index = k + el.indexee = v + else: + raise NotImplementedError("Something strange happened: len(els) == " + repr(len(els))) + #if len(index) == 1: + # els[0].indexee = + #print("aligned", index, ranges, k) + + +class _RangesTree(_RangesIndexTree): + + """Allows to store sequences of slices and then query the slices overlapping with the given slice. Returns the whole slices, not their parts.""" + + __slots__ = () + + @classmethod + def build(cls, index: SliceRangeListT, data: typing.Optional[SliceRangeListT] = None) -> typing.Union[ILeaf, "RangesTree"]: + if data: + rangesIsRange = isinstance(data, isInstArg) + indexIsRange = isinstance(index, isInstArg) + if (indexIsRange != rangesIsRange) or ((not indexIsRange or not rangesIsRange) and len(data) != len(index)): + index, data = salign((index, data)) + #print("aligned", index, ranges) + + return cls._build(index=index, data=data) + + +class RangesTree(_RangesTree): + __slots__ = () + + def __getitem__(self, q: typing.Union[SliceRangeT, int]) -> LookupResult: + if isinstance(q, int): + q = type(self.index)(q, q + 1) + return super().__getitem__(q) + + +class _SliceSequence: + __slots__ = ("tree",) + + def __init__(self, tree: RangesTree) -> None: + self.tree = tree + + def __getitem__(self, q: SliceRangeT) -> LookupResult: + res = list(self.tree[q]) + #ic(res) + idxz = [] + valuez = [] + for el in res: + if isinstance(el, ValueLeaf): + idx = el.index + val = el.indexee + else: + idx = val = el + + #print("sdiff(", idx, ",", q, ")") + diff = sdiff(idx, q) + #ic(diff) + + idx1 = diff[(SDiffAutomata.State.entered, SDiffAutomata.State.entered)] + idxz.append(idx1) + idx1Size = slen(idx1) + + leftWasteIdx = (SDiffAutomata.State.entered, SDiffAutomata.State.notEntered) + if leftWasteIdx in diff: + #print("leftWaste", diff[leftWasteIdx]) + leftWasteSize = slen(diff[leftWasteIdx]) + else: + leftWasteSize = 0 + + #print(leftWasteSize, ":", leftWasteSize+idx1Size, ) + val = sAny2Type(slice2range(val)[leftWasteSize : leftWasteSize + idx1Size], val.__class__) + #ic(val) + valuez.append(val) + + #ic(idx, idxz, valuez) + return (ValueLeaf(*p) for p in zip(idxz, valuez)) + + +class SliceSequence(_SliceSequence): + + """Allows to associate one sequence of slices to another one and then lookup subslices of the second one using subslices in the first one as keys. Useful for ranges rewriting like endianness transformations.""" + + __slots__ = () + + def __init__(self, index: SliceRangeListT, data: typing.Optional[SliceRangeT] = None) -> None: + #ic("SliceSequence.__init__", data, index) + super().__init__(RangesTree.build(index=index, data=data)) + + +def mergeRangesInTreeLookupResult(lookupResults: LookupResult) -> LookupResult: + idxz, valuez = zip(*((s.index, s.indexee) for s in lookupResults)) + idxz = sjoin(idxz) + valuez = sjoin_(valuez) + idxz, valuez = salign_((idxz, valuez)) + return (ValueLeaf(*p) for p in zip(idxz, valuez)) diff --git a/rangeslicetools/utils.py b/rangeslicetools/utils.py new file mode 100644 index 0000000..1c42e0f --- /dev/null +++ b/rangeslicetools/utils.py @@ -0,0 +1,420 @@ +import heapq +import itertools +import typing +from collections.abc import Sequence +from functools import wraps + +__all__ = ("SliceRangeT", "SliceRangeTypeT", "SliceRangeSeqT", "SliceRangeListT", "sAny2Type", "range2slice", "slice2range", "slen", "sdir", "svec", "srev", "sdirect", "snormalize", "ssplit_1_", "ssplit_1", "ssplit_", "ssplit", "schunks_", "schunks", "soffset_split_", "soffset_split", "sjoin_", "swithin", "soverlaps", "teeSliceSequences", "salign_", "sPointIn", "ssegments_", "ssegments", "shull") + +isInstArg = (range, slice) +SliceRangeT = typing.Union[isInstArg] +SliceRangeTypeT = typing.Union[tuple(typing.Type[el] for el in isInstArg)] +SliceRangeSeqT = typing.Iterable[SliceRangeT] +SliceRangeListT = typing.Sequence[SliceRangeT] +SliceRangeOptListT = typing.Union[SliceRangeT, SliceRangeListT] + + +def _getStepForComputation(slc: SliceRangeT) -> int: + """Returns a `step` that is a number""" + if slc.step is not None: + return slc.step + + if slc.start <= slc.stop: + return 1 + + raise ValueError("start < end, so if step is not explicitly defined, it is undefined! Setup the step explicitly (you would likely need -1)!") + + +def sign(n: int) -> int: + """Signum func FOR OUR PURPOSES""" + if n is None or n >= 0: + return 1 + + return -1 + + +def _scollapse(slc: SliceRangeOptListT) -> SliceRangeOptListT: + """Collapses a sequence of ranges into a range, if it contains only a 1 range""" + if not isinstance(slc, isInstArg) and len(slc) == 1: + return slc[0] + return slc + + +def sAny2Type(rng: SliceRangeT, tp: SliceRangeTypeT) -> SliceRangeT: + """Creates a new /range/slice with needed type""" + return tp(rng.start, rng.stop, _getStepForComputation(rng)) + + +def range2slice(rng: SliceRangeT) -> slice: + """Clones into a slice.""" + return sAny2Type(rng, slice) + + +def slice2range(slc: SliceRangeT) -> range: + """Clones into a range.""" + return sAny2Type(slc, range) + + +def _slen(slc: SliceRangeT) -> int: + return len(slice2range(slc)) + + +def slen(slcs: SliceRangeSeqT) -> int: + """Returns length of a range/slice.""" + if isinstance(slcs, isInstArg): + return _slen(slcs) + total = 0 + for s in slcs: + total += _slen(s) + return total + + +def sdir(slc: SliceRangeT) -> int: + """Returns director of a range/slice.""" + return sign(slc.stop - slc.start) + + +def svec(slc: SliceRangeT) -> int: + return sdir(slc) * slen(slc) + + +def srev(slc: SliceRangeT) -> SliceRangeT: + """Reverses direction of a range/slice.""" + step = _getStepForComputation(slc) + newStep = -1 * step + assert isinstance(slc, range) or newStep >= -1, "Negative-directed slices with `step`s other -1 don't work!" + return slc.__class__(slc.stop - step, slc.start - step, newStep) + + +def _isNegative(slcs: SliceRangeListT) -> typing.Iterable[bool]: + return slcs.__class__(el.stop < el.start for el in slcs) + + +def _sdirect(donorNegative: bool, acceptor: SliceRangeOptListT) -> SliceRangeOptListT: + if not isinstance(acceptor, isInstArg): + if not isinstance(donorNegative, bool): + return acceptor.__class__(_sdirect(*el) for el in zip(donorNegative, acceptor)) + + return acceptor.__class__(_sdirect(donorNegative, el) for el in acceptor) + + if donorNegative != (acceptor.stop < acceptor.start): + return srev(acceptor) + + return acceptor + + +def sPointIn(s: SliceRangeT, pt: int) -> bool: + #return (((s.step is None or s.step > 0) and s.start <= pt < s.stop) or (s.start >= pt > s.stop)) + return pt in slice2range(s) + + +def snormalize(slc: SliceRangeOptListT) -> SliceRangeOptListT: + """Returns range/slice that points forward. If the range is positive-directed with the step 1, removes the step.""" + res = _sdirect(False, slc) + if isinstance(res, isInstArg): + return sAny2Type(res, slc.__class__) + + return res.__class__(sAny2Type(el, el.__class__) for el in res) + + +def sdirect(donor: SliceRangeT, acceptor: SliceRangeT) -> SliceRangeT: + """Makes direction of an `acceptor` the same as a direction of a `donor.""" + return _sdirect(donor.stop < donor.start, acceptor) + + +class InBandSignal: + __slots__ = () + + +newMacroGroup = InBandSignal() + + +def _createWrappedWithnewMacroGroup(f: typing.Callable) -> typing.Callable: + @wraps(f) + def f1(*args, **kwargs): + bigRes = [] + res = [] + secCtor = kwargs.get("_secCtor", tuple) + + def genericAppend(): + nonlocal res + if len(res) == 1: + res = res[0] + else: + res = secCtor(res) + + bigRes.append(res) + res = [] + + for el in f(*args, **kwargs): + #ic(el) + if el is not newMacroGroup: + res.append(el) + else: + genericAppend() + + if res: + genericAppend() + + bigRes = secCtor(bigRes) + + return bigRes + + f1.__annotations__["return"] = typing.Iterable[SliceRangeOptListT] + return f1 + + +def ssplit_1_(slc: SliceRangeT, splitPts: typing.Union[int, typing.Iterable[int]]) -> SliceRangeSeqT: + """Splits the slices by split points, which are ABSOLUTE POSITIONS OF POINTS on axis.""" + tp = slc.__class__ + if isinstance(splitPts, int): + splitPts = (splitPts,) + for p in splitPts: + if p != slc.start: + yield tp(slc.start, p, slc.step) + yield newMacroGroup + slc = tp(p, slc.stop, slc.step) + yield slc + + +ssplit_1 = _createWrappedWithnewMacroGroup(ssplit_1_) + + +def ssplit_(slc: SliceRangeSeqT, splitPts: typing.Iterable[int]) -> SliceRangeSeqT: + """Splits the slices by split points, which are ABSOLUTE POSITIONS OF POINTS on axis.""" + + if isinstance(slc, isInstArg): + slc = (slc,) + if isinstance(splitPts, int): + splitPts = (splitPts,) + + splitPts = iter(splitPts) + + try: + pt = next(splitPts) + except StopIteration: + yield from slc + return + + pts2split = [] + + for s in slc: + while pt is not None and sPointIn(s, pt): + pts2split.append(pt) + try: + pt = next(splitPts) + except StopIteration: + pt = None + + if pts2split: + yield from ssplit_1_(s, pts2split) + pts2split = [] + else: + yield s + + +ssplit = _createWrappedWithnewMacroGroup(ssplit_) + + +def schunks_(slc: SliceRangeT, chunkLen: int) -> SliceRangeSeqT: + """Splits the slice into slices of length `chunkLen` (which is in `slc.step`s!!!)""" + cl = chunkLen * _getStepForComputation(slc) + return ssplit_(slc, range(slc.start + cl, slc.stop, cl)) + + +schunks = _createWrappedWithnewMacroGroup(schunks_) + + +def soffset_split_(slc: SliceRangeSeqT, splitPts: typing.Iterable[int]) -> SliceRangeSeqT: + """Splits the slices by split points, which are OFFSETS FROM RANGE BEGINNING.""" + if isinstance(slc, isInstArg): + slc = (slc,) + if isinstance(splitPts, int): + splitPts = (splitPts,) + + splitPts = iter(splitPts) + + try: + pt = next(splitPts) + except StopIteration: + yield from slc + return + + cumLen = 0 + cumLenPrev = None + pts2split = [] + + for s in slc: + cumLenPrev = cumLen + cumLen += slen(s) + while pt is not None and cumLenPrev <= pt < cumLen: + pts2split.append(s.start + (pt - cumLenPrev) * _getStepForComputation(s)) + try: + pt = next(splitPts) + except StopIteration: + pt = None + + if pts2split: + yield from ssplit_1_(s, pts2split) + pts2split = [] + else: + yield s + + +soffset_split = _createWrappedWithnewMacroGroup(soffset_split_) + + +def _posHull(first: SliceRangeT, slcs: SliceRangeSeqT) -> SliceRangeT: + mi, ma = first.start, first.stop + + for slc in slcs: + mi = min(mi, slc.start) + ma = max(slc.stop, ma) + + return first.__class__(mi, ma, first.step) + + +def _negHull(first: SliceRangeT, slcs: SliceRangeSeqT) -> SliceRangeT: + ma, mi = first.start, first.stop + + for slc in slcs: + mi = min(mi, slc.stop) + ma = max(slc.start, ma) + + return first.__class__(ma, mi, first.step) + + +def shull(slcs: SliceRangeSeqT) -> SliceRangeT: + """Returns the range covering all the ranges provided. Every item must be of the same direction! + See also `sunion`, which is slower, but takes into account direction. + """ + + slcs = iter(slcs) + first = next(slcs) + + if first.start < first.stop: + return _posHull(first, slcs) + + return _negHull(first, slcs) + + +def sjoin_(slcs: SliceRangeSeqT) -> SliceRangeSeqT: + """Merges adjacent or overlapped ranges. All the ranges must be of the same direction. If the direction is negative, the sequence MUST be reversed! The sequence MUST be sorted. The type is taken from the type of the first range in the input.""" + slcs = iter(slcs) + wholeDir = 0 + while not wholeDir: + try: + prevSlc = next(slcs) + except StopIteration: + return + wholeDir = sdir(prevSlc) + wholeDir = wholeDir > 0 # type: bool + tp = prevSlc.__class__ + + for s in slcs: + #assert (prevSlc.start <= prevSlc.stop) == (s.start <= s.stop) + if prevSlc.step == s.step: + if s.start == prevSlc.stop: + prevSlc = tp(prevSlc.start, s.stop, prevSlc.step) + else: + curDir = prevSlc.start <= s.start + if (swithin(prevSlc, s) or swithin(s, prevSlc)) or curDir == wholeDir and soverlaps(prevSlc, s): + prevSlc = shull((prevSlc, s)) + else: + yield prevSlc + prevSlc = s + else: + yield prevSlc + prevSlc = s + yield prevSlc + + +def swithin(haystack: SliceRangeT, needle: SliceRangeT) -> bool: + """Answers if needle is fully within haystack (including boundaries).""" + hsn = snormalize(haystack) + nn = snormalize(needle) + return _swithin(hsn, nn) + + +def soverlaps(haystack: SliceRangeT, needle: SliceRangeT) -> bool: + """Answers if needle is at least partially overlaps haystack (including boundaries).""" + hsn = snormalize(haystack) + nn = snormalize(needle) + return _soverlaps(hsn, nn) + + +_normalizationSkippedWarning = " Normalization is skipped." + + +def _swithin(haystack: SliceRangeT, needle: SliceRangeT) -> bool: + res = needle.start >= haystack.start and needle.stop <= haystack.stop + #ic("_swithin", haystack, needle, needle.start >= haystack.start, needle.stop < haystack.stop, res) + return res + + +_swithin.__doc__ = swithin.__doc__ + _normalizationSkippedWarning + + +def _soverlaps(haystack: SliceRangeT, needle: SliceRangeT) -> bool: + #ic("_soverlaps", haystack, needle, needle.start <= haystack.start < needle.stop, needle.start < haystack.stop < needle.stop) + return _swithin(haystack, needle) or needle.start <= haystack.start < needle.stop or needle.start < haystack.stop < needle.stop + + +_soverlaps.__doc__ = soverlaps.__doc__ + _normalizationSkippedWarning + + +def _teeSliceSequences(sliceSequences: typing.Iterable[SliceRangeSeqT], count: int = 2) -> typing.Iterator[typing.Tuple[itertools._tee, itertools._tee]]: + for s in sliceSequences: + if isinstance(s, isInstArg): + s = (s,) + yield itertools.tee(s, count) + + +def teeSliceSequences(sliceSequences: typing.Iterable[SliceRangeSeqT], count: int = 2) -> zip: + return zip(*(_teeSliceSequences(sliceSequences, count))) + + +def _integrator(chunkLens: typing.Iterable[int]) -> typing.Iterable[int]: + cumLen = 0 + for s in chunkLens: + cumLen += s + yield cumLen + + +def _uniq(it: typing.Iterable[typing.Any]) -> typing.Iterable[typing.Any]: + it = iter(it) + try: + prev = next(it) + yield prev + except StopIteration: + return + for el in it: + if prev == el: + continue + prev = el + yield el + + +def _mergeAndDedup(intSeqs: typing.Iterable[typing.Iterable[int]]) -> typing.Iterable[int]: + return _uniq(sorted(heapq.merge(*intSeqs))) + + +def _deduplicatedIntegrator(*chunksLens: typing.Iterable[typing.Iterable[int]]) -> typing.Iterable[int]: + return _mergeAndDedup(map(_integrator, chunksLens)) + + +def ssegments_(slc: SliceRangeT, chunkLens: typing.Iterable[int]) -> SliceRangeSeqT: + """Splits the slice into slices of lengths `chunkLen` (which is in `slc.step`s!!!)""" + return soffset_split_(slc, _deduplicatedIntegrator(chunkLens)) # pylint: disable=undefined-variable + + +ssegments = _createWrappedWithnewMacroGroup(ssegments_) + + +def salign_(sliceSequences: typing.Iterable[SliceRangeSeqT]) -> SliceRangeSeqT: + """"Aligns" seqs of ranges/slices OF THE SAME TOTAL LENGTH, returning ones with additional split points, so that all the sequences have segments of equal lengths between split points with the same indexes. See the test for more insight on what it does.""" + slcsPoints, slcsSplit = teeSliceSequences(sliceSequences, 2) + + splitPoints = tuple(_deduplicatedIntegrator(*(map(_slen, ss) for ss in slcsPoints))) + for ss in slcsSplit: + yield soffset_split(ss, splitPoints) # pylint: disable=undefined-variable diff --git a/rangeslicetools/viz.py b/rangeslicetools/viz.py new file mode 100644 index 0000000..f2e5ef6 --- /dev/null +++ b/rangeslicetools/viz.py @@ -0,0 +1,111 @@ +import typing +from collections import defaultdict + +from .diff import SDiffAutomata, sdiff +from .utils import SliceRangeListT, SliceRangeT, sdir, slen, snormalize + + +def _pointsAndD(r: range) -> typing.Tuple[int, int, int]: + d = sdir(r) + r = snormalize(r) + return r.start, r.stop, d + + +def _drawArea(r: range, f: typing.Callable, layer: str, ruler: typing.Mapping[int, int]) -> str: + sp, ep, d = _pointsAndD(r) + startX = ruler[sp] + endX = ruler[ep] + pixLen = endX - startX + + nS = str(ep - sp) + + e, s, filler = f(d) + + iL = len(e) + len(s) + len(nS) + margin = pixLen - iL + mh = margin // 2 + msh = margin - mh + + if d >= 0: + e = filler * mh + e + s = s + filler * msh + else: + s += filler * mh + e = filler * msh + e + + curImg = s + nS + e + return layer + (curImg) + + +def sviz(ranges: SliceRangeListT) -> str: + """Draws ranges with ASCII art.""" + c = len(ranges) + ruler = "" + scale = "" + ranges = sorted(ranges, key=lambda r: r.start) + res = sdiff(*ranges) + res = sorted((p for p in res.items()), key=lambda p: snormalize(p[1]).start) + minf = -float("inf") + ruler = defaultdict(lambda: minf) + + offset = len(str(res[0][1].start)) + maxStartPxPos = offset + maxEndPxPos = maxStartPxPos + + for state, r in res: + sp, ep, d = _pointsAndD(r) + l = ep - sp + + numW = len(str(sp)) + maxStartPxPos = maxEndPxPos + numW + 3 # for arrow + ruler[sp] = max(maxStartPxPos - numW // 2, ruler[sp]) + + maxEndPxPos = maxStartPxPos + len(str(l)) + maxEndPxPos += 3 # for arrow + ruler[ep] = max(maxEndPxPos, ruler[sp]) + + e = "" + s = "" + layers = [] + + S = SDiffAutomata.State + + for ln in range(len(ranges)): + drawn = False + layer = " " * offset + for k, r in res: + ls = k[ln] + if not ls & S.entered or ls & S.exited: + + def lam(d): + return "...", "...", "." + + layer = _drawArea(r, lam, layer, ruler) + else: + + def lam(d): + if d >= 0: + e = "~>]" + s = "[~~" + else: + s = "[<~" + e = "~~]" + return e, s, "~" + + layer = _drawArea(ranges[ln], lam, layer, ruler) + layers.append(layer) + break + + layers = ("".join(l) for l in layers) + + ruler = sorted((p for p in ruler.items()), key=lambda p: p[0]) + rulerScale = " " * offset + rulerImg = "" + for x, px in ruler: + margin = px - len(rulerImg) + ns = str(x) + rulerImg += ns + "." * margin + rulerScale += "|" + "." * (margin + len(ns) // 2) + res = "\n".join(layers) + res += "\n" + rulerScale + "\n" + rulerImg + return res diff --git a/tests/tests.py b/tests/tests.py new file mode 100755 index 0000000..54c745b --- /dev/null +++ b/tests/tests.py @@ -0,0 +1,1025 @@ +#!/usr/bin/env python3 +import typing +import os, sys +import unittest +import itertools +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).absolute().parent.parent)) + +from rangeslicetools import * +from rangeslicetools.utils import _getStepForComputation, isInstArg + + +def constructNestedRangeSliceSeq(ctor, seq): + if seq: + if isinstance(seq[0], int): + return ctor(*seq) + else: + return seq.__class__(constructNestedRangeSliceSeq(ctor, el) for el in seq) + return seq + + +cnss = constructNestedRangeSliceSeq + +#@unittest.skip +class TestTestUtils(unittest.TestCase): + def test_constructNestedRangeSliceSeq(self): + pairs = { + (1, 2): range(1, 2), + ((1, 2), (3, 4)): (range(1, 2), range(3, 4)), + (((1, 2),), ((3, 4),(5, 6))): ((range(1, 2),), (range(3, 4), range(5, 6))) + } + for challenge, response in pairs.items(): + with self.subTest(challenge=challenge, response=response): + self.assertEqual(constructNestedRangeSliceSeq(range, challenge), response) + + +#@unittest.skip +class Tests(unittest.TestCase): + def test_getStepForComputation(self) -> None: + pairs = { + (0, 16,): 1, + (0, 16, 1): 1, + (0, 16, 2): 2, + (15, -1, -1): -1, + (15, -1, -2): -2, + } + for ctor in isInstArg: + for challenge, response in pairs.items(): + challenge = ctor(*challenge) + + with self.subTest(challenge=challenge, response=response): + self.assertEqual(_getStepForComputation(challenge), response) + + def _test_any2any(self, ctorSrc: typing.Union[typing.Type[slice], typing.Type[range]], ctorDst: typing.Union[typing.Type[slice], typing.Type[range]], convertor: typing.Callable) -> None: + testRanges = [ + (0, 16, 1), + (17, -1, -2), + ] + + for r in testRanges: + src = ctorSrc(*r) + exp = ctorDst(*r) + with self.subTest(src=src, ctorSrc=ctorSrc, convertor=convertor): + self.assertEqual(convertor(src), exp) + + def test_slice2range(self) -> None: + self._test_any2any(slice, range, slice2range) + self._test_any2any(range, range, slice2range) + + def test_range2slice(self) -> None: + self._test_any2any(range, slice, range2slice) + self._test_any2any(slice, slice, range2slice) + + def test_slen(self) -> None: + testRanges = [ + (0, 16, 1), + (17, -1, -2), + (15, -1, -1), + (0, 16), + (17, -1, -1), + (15, -1, -2), + ] + + for ctor in isInstArg: + sumLen = 0 + + for r in testRanges: + r = cnss(ctor, r) + with self.subTest(r=r): + l = len(list(slice2range(r))) + self.assertEqual(slen(r), l) + sumLen += l + + with self.subTest(r=testRanges): + self.assertEqual(slen(cnss(ctor, testRanges)), sumLen) + + def test_sPointIn(self) -> None: + pairs = { + ((0, 16, 1), 15): True, + ((0, 16, 1), 18): False, + ((0, 16, 1), 16): False, + ((0, 16, 1), 0): True, + + ((15, -1, -1), 15): True, + ((15, -1, -1), 18): False, + ((15, -1, -1), 16): False, + ((15, -1, -1), 0): True, + } + + for ctor in isInstArg: + sumLen = 0 + + for challenge, response in pairs.items(): + r, p = challenge + r = cnss(ctor, r) + with self.subTest(challenge=challenge, response=response): + self.assertEqual(sPointIn(r, p), response) + + def test_svec(self) -> None: + pairs = ( + ((0, 16, 1), 16), + ((14, -2, -2), -8) + ) + for ctor in isInstArg: + for s, orientedLen in pairs: + s = cnss(ctor, s) + with self.subTest(s=s): + self.assertEqual(svec(s), orientedLen) + + def test_srev(self) -> None: + pairs = ( + ((0, 16, 1), (15, -1, -1)), + ((0, 16, 2), (14, -2, -2)) + ) + for ctor in isInstArg: + for s, rev in pairs: + s = cnss(ctor, s) + rev = cnss(ctor, rev) + resS = srev(rev) + + with self.subTest(s=s, rev=rev, resS=resS): + self.assertEqual(list(slice2range(resS)), list(slice2range(s))) + self.assertEqual(resS, s) + + try: + resRev = srev(s) + except BaseException: + continue + with self.subTest(s=s, rev=rev, resRev=resRev): + self.assertEqual(list(slice2range(resRev)), list(slice2range(rev))) + self.assertEqual(resRev, rev) + + def test_sdir(self) -> None: + pairs = ( + ((0, 16, 1), 1), + ((14, -2, -2), -1), + ((0, 16), 1), + ((14, -2), -1), + ) + for ctor in isInstArg: + for s, exp in pairs: + s = cnss(ctor, s) + + with self.subTest(s=s): + self.assertEqual(sdir(s), exp) + + def test_sdirect(self) -> None: + pairs = { + ((0, 16, 1), (1, 2, 1)): (1, 2, 1), + ((0, 16, 1), (1, 0, -1)): (1, 2, 1), + ((16, 0, -1), (1, 2, 1)): (1, 0, -1), + ((16, 0, -1), (1, 0, -1)): (1, 0, -1), + } + for ctor in isInstArg: + for ctor2 in isInstArg: + for (donor, acceptor), expected in pairs.items(): + donor = cnss(ctor, donor) + acceptor = ctor2(*acceptor) + expected = ctor2(*expected) + + with self.subTest(donor=donor, acceptor=acceptor): + self.assertEqual(sdirect(donor, acceptor), expected) + + def test_snormalize(self) -> None: + testRanges = [ + (0, 16, 1), + (0, 16, 2) + ] + + for ctor in isInstArg: + r = ctor(0, 16) + with self.subTest(r=r): + self.assertEqual(snormalize(r), ctor(0, 16, 1)) + + for ctor in isInstArg: + for r in testRanges: + r = cnss(ctor, r) + with self.subTest(r=r): + self.assertEqual(snormalize(r), r) + + try: + rev = srev(r) + except AssertionError: + continue + with self.subTest(rev=rev): + self.assertEqual(snormalize(rev), r) + + +@unittest.skip +class RelationTests(unittest.TestCase): + def _testRelation(self, pairs, func: typing.Callable) -> None: + for ctor in isInstArg: + for initialRanges, expectedResult in pairs.items(): + initialRanges = cnss(ctor, initialRanges) + + with self.subTest(initialRanges=initialRanges): + self.assertEqual(func(*initialRanges), expectedResult) + + def test_swithin(self) -> None: + pairs = { + ((0, 10), (1, 5)): True, + ((0, 10), (-5, 3)): False, + ((0, 10), (7, 15)): False, + + ((9, -1, -1), (1, 5)): True, + ((9, -1, -1), (-5, 3)): False, + ((9, -1, -1), (7, 15)): False, + + ((0, 10), (4, 0, -1)): True, + ((0, 10), (2, -6, -1)): False, + ((0, 10), (14, 6, -1)): False, + + ((9, -1, -1), (4, 0, -1)): True, + ((9, -1, -1), (2, -6, -1)): False, + ((9, -1, -1), (14, 6, -1)): False, + + + ((0, 10), (11, 15)): False, + ((0, 10), (-15, -11)): False, + + ((9, -1, -1), (11, 15)): False, + ((9, -1, -1), (-15, -11)): False, + + ((0, 10), (14, 10, -1)): False, + ((0, 10), (-12, -16, -1)): False, + + ((9, -1, -1), (14, 10, -1)): False, + ((9, -1, -1), (-12, -16, -1)): False, + } + self._testRelation(pairs, swithin) + + def test_soverlaps(self) -> None: + pairs = { + ((0, 10), (1, 5)): True, + ((0, 10), (-5, 3)): True, + ((0, 10), (7, 15)): True, + + ((9, -1, -1), (1, 5)): True, + ((9, -1, -1), (-5, 3)): True, + ((9, -1, -1), (7, 15)): True, + + ((0, 10), (4, 0, -1)): True, + ((0, 10), (2, -6, -1)): True, + ((0, 10), (14, 6, -1)): True, + + ((9, -1, -1), (4, 0, -1)): True, + ((9, -1, -1), (2, -6, -1)): True, + ((9, -1, -1), (14, 6, -1)): True, + + + ((0, 10), (11, 15)): False, + ((0, 10), (-15, -11)): False, + + ((9, -1, -1), (11, 15)): False, + ((9, -1, -1), (-15, -11)): False, + + ((0, 10), (14, 10, -1)): False, + ((0, 10), (-12, -16, -1)): False, + + ((9, -1, -1), (14, 10, -1)): False, + ((9, -1, -1), (-12, -16, -1)): False, + + + ((0, 4, 1), (2, 4, 1)): True, + ((1, 2), (2, 4, 1)): False, + } + self._testRelation(pairs, soverlaps) + + +#@unittest.skip +class SplitMergeTests(unittest.TestCase): + def _testSplit(self, pairs, func: typing.Callable) -> None: + for ctor in isInstArg: + for argsTuple, expectedResult in pairs.items(): + initialRange = argsTuple[0] + otherArgs = argsTuple[1:] + initialRange = cnss(ctor, initialRange) + expectedResult = cnss(ctor, expectedResult) + + with self.subTest(initialRange=initialRange, otherArgs=otherArgs): + self.assertEqual(func(initialRange, *otherArgs), expectedResult) + + def test_ssplit(self) -> None: + pairs = { + ((0, 8), (2, 3, 7)): ((0, 2), (2, 3), (3, 7), (7, 8)), + ((0, 8), (2, 8)): ((0, 2), (2, 8)), + ((7, -1, -1), 2): ((7, 2, -1), (2, -1, -1)), + #(((0, 4), (4, 8), (8, 12), (12, 16)), 8): (((0, 4), (4, 8)), ((8, 12), (12, 16))), + } + self._testSplit(pairs, ssplit) + + def test_ssplit_1(self) -> None: + pairs = { + ((0, 8), (2, 3, 7)): ((0, 2), (2, 3), (3, 7), (7, 8)), + ((0, 8), (2, 8)): ((0, 2), (2, 8), (8, 8)), + ((7, -1, -1), 2): ((7, 2, -1), (2, -1, -1)), + } + self._testSplit(pairs, ssplit_1) + + def test_schunks(self) -> None: + pairs = { + ((0, 8), 8): ((0, 8),), + ((0, 8), 9): ((0, 8),), + ((0, 8), 2): ((0, 2), (2, 4), (4, 6), (6, 8)), + ((1, 9, 1), 4): ((1, 5, 1), (5, 9, 1)), + ((1, 9, 4), 8): ((1, 9, 4),), + ((7, -1, -1), 2): ((7, 5, -1), (5, 3, -1), (3, 1, -1), (1, -1, -1)), + } + self._testSplit(pairs, schunks) + + def test_ssegments(self) -> None: + pairs = { + ((0, 8), (2, 3, 3)): ((0, 2), (2, 5), (5, 8)), + ((0, 8), (2, 3, 7)): ((0, 2), (2, 5), (5, 8)), + ((0, 8), (2, 6)): ((0, 2), (2, 8)), + ((7, -1, -1), (2,)): ((7, 5, -1), (5, -1, -1)), + } + self._testSplit(pairs, ssegments) + + def test_soffset_split(self) -> None: + pairs = { + ((0, 8, 1), (-3, 9, 10)): ((0, 8, 1),), + ((0, 8, 1), (2, 3, 7)): ((0, 2, 1), (2, 3, 1), (3, 7, 1), (7, 8, 1)), + ((1, 8, 1), (2, 6)): ((1, 3, 1), (3, 7, 1), (7, 8, 1)), + ((1, 8), (2, 7)): ((1, 3), (3, 8)), + ((7, -1, -1), 2): ((7, 5, -1), (5, -1, -1)), + ((7, -1, -1), ()): ((7, -1, -1),), + (((0, 4), (4, 8), (8, 12), (12, 16)), 8): (((0, 4), (4, 8)), ((8, 12), (12, 16))), + } + self._testSplit(pairs, soffset_split) + + def test_shull(self) -> None: + pairs = { + ((0, 5), (5, 8)): (0, 8), + ((8, 7, -1), (7, -1, -1)): (8, -1, -1), + ((5, 8), (0, 5)): (0, 8), + ((8, 7, -1), (9, 8, -1)): (9, 7, -1), + ((0, 7), (5, 8)): (0, 8), + ((5, 8), (0, 7),): (0, 8), + ((8, 6, -1), (7, -1, -1)): (8, -1, -1), + ((7, -1, -1), (8, 6, -1)): (8, -1, -1), + ((0, 9, 1), (10, 12, 1)): (0, 12, 1), + ((10, 12, 1), (0, 9, 1),): (0, 12, 1), + ((8, 7, -1), (4, -1, -1)): (8, -1, -1), + ((9, 10, 1), (0, 12, 1)): (0, 12, 1), + ((10, 12, 1), (0, 9, 1),): (0, 12, 1), + } + + for ctor in (range,): + for initialRanges, expectedResult in pairs.items(): + initialRanges = cnss(ctor, initialRanges) + expectedResult = cnss(ctor, expectedResult) + + with self.subTest(initialRanges=initialRanges): + self.assertEqual(shull(initialRanges), expectedResult) + + def test_sjoin(self) -> None: + pairs = { + #[~~~5~>] + # [~~3~>] + #0.......5.....8 + #[~~~5~~~~~~3~>] + ((0, 5), (5, 8)): ((0, 8),), + + # [<~1~~] + #[<~8~~~] + #0......8.....9 + #[<~8~~~~~~1~~] + ((8, 7, -1), (7, -1, -1)): ((8, -1, -1),), + + # [~~3~>] + #[~~~5~>] + #0......6.....8 + ((5, 8), (0, 5)): ((5, 8), (0, 5),), + + #[<~1~~~] + #.......[<~1~~] + #8......9.....10 + ((8, 7, -1), (9, 8, -1)): ((8, 7, -1), (9, 8, -1),), + + #[~~~7~~~~>] + # [~~2~~>] + #0.......5.7....8 + #[~~~~~~~8~~~~~>] + ((0, 7), (5, 8)): ((0, 8),), + + # [~~2~~>] + #[~~~7~~~~>] + #0.......5.7....8 + ((5, 8), (0, 7),): ((5, 8), (0, 7),), + + #............[<~~~2~~~~~] + #[<~~~~~8~~~~~~] + #.0.....7.......8.......9 + ((8, 6, -1), (7, -1, -1)): ((8, -1, -1),), #!!!! + + + ((0, 5), (5, 10)): ((0, 10),), + ((0, 4), (4, 8), (8, 10)): ((0, 10),), + ((0, 5), (5, 10)): ((0, 10),), + ((0, 4), (4, 8), (8, 10)): ((0, 10),), + + #[<~~~~~8~~~~~~~] + #...........[<~~~2~~~~~] + #0.....7.......8.......9 + ((7, -1, -1), (8, 6, -1)): ((7, -1, -1), (8, 6, -1)), + + + #[~~~9~>]....1...[~~2~>] + #0......9........10....12 + ((0, 9, 1), (10, 12, 1)): ((0, 9, 1), (10, 12, 1)), + ((10, 12, 1), (0, 9, 1),): ((10, 12, 1), (0, 9, 1),), + + #[<~5~~~]...3...[<~1~~] + #0......5.......8.....9 + ((8, 7, -1), (4, -1, -1)): ((8, 7, -1), (4, -1, -1)), + + #.......[~~~1~~~>]...... + #[~~~~~~~12~~~~~~~~~~~~>] + #0......9........10....12 + #[~~~~~~~12~~~~~~~~~~~~>] + ((9, 10, 1), (0, 12, 1)): ((0, 12, 1),), + ((10, 12, 1), (0, 9, 1),): ((10, 12, 1), (0, 9, 1),), + + ((0, 8, 1), (8, 9, 1), (9, 10, 1)): ((0, 10, 1),), + ((0, 8), (8, 9), (9, 10)): ((0, 10),), + ((0, 8, 1), (8, 9, 1), (10, 12, 1), (12, 14, 1)): ((0, 9, 1), (10, 14, 1)), + ((0, 8, 2), (8, 10, 2), (11, 13, 2), (13, 15, 2)): ((0, 10, 2), (11, 15, 2)), + ((0, 8), (8, 9), (10, 12), (12, 14)): ((0, 9), (10, 14)), + ((7, -1, -1), (8, 7, -1)): ((7, -1, -1), (8, 7, -1),), + ((9, 8, -1), (8, 7, -1), (7, -1, -1)): ((9, -1, -1),), + ((7, -1, -1), (8, 7, -1), (9, 8, -1)): ((7, -1, -1), (8, 7, -1), (9, 8, -1),), + (): (), + } + + for ctor in (range,): + for initialRanges, expectedResult in pairs.items(): + initialRanges = cnss(ctor, initialRanges) + expectedResult = cnss(ctor, expectedResult) + + with self.subTest(initialRanges=initialRanges): + self.assertEqual(sjoin(initialRanges), expectedResult) + + + def test_salign(self) -> None: + testMatrix = { + ( + ((9, 8, -1), (8, 7, -1), (7, -1, -1)), + ((19, 15, -1), (15, 13, -1), (13, 9, -1)), + ):( + (( 9, 8, -1), ( 8, 7, -1), ( 7, 5, -1), ( 5, 3, -1), ( 3, -1, -1)), + ((19, 18, -1), (18, 17, -1), (17, 15, -1), (15, 13, -1), (13, 9, -1)), + ), + # my + ( + ((15, -1, -1),), + ((7, -1, -1), (15, 7, -1)), + ):( + ((15, 7, -1), (7, -1, -1)), + ((7, -1, -1), (15, 7, -1)), + ), + # my + ( + ((7, -1, -1), (15, 7, -1)), + (15, -1, -1), + ):( + ((7, -1, -1), (15, 7, -1)), + ((15, 7, -1), (7, -1, -1)), + ), + } + for ctor in isInstArg: + for chall, expectedRes in testMatrix.items(): + + def transformChall(): + for rsIt in chall: + yield cnss(ctor, rsIt) + + chall = tuple(transformChall()) + expectedRes = cnss(ctor, expectedRes) + with self.subTest(chall=chall): + res = salign(chall) + self.assertEqual(res, expectedRes) + + +#@unittest.skip +class DiffTests(unittest.TestCase): + def test_sdiff(self) -> None: + S = SDiffAutomata.State + + pairs = { + # 0 5 7 10 + # |%------->% + # % |%------%------>%% + # |%-en,ne-}%----ee,en----}%% + ((0, 5, 1), (5, 10, 1)): { + (S.entered, S.notEntered): (0, 5, 1), + (S.entered | S.exited, S.entered): (5, 10, 1), + }, + # 0 5 7 10 + #|%--------%------->% + # |%--------%------->%% + #|%-en,ne-}%en,en--}%-ee,en-}%% + ((0, 7, 1), (5, 10, 1)): { + (S.entered, S.notEntered): (0, 5, 1), + (S.entered, S.entered): (5, 7, 1), + (S.entered | S.exited, S.entered): (7, 10, 1), + }, + # 0 5 7 10 + #|%----------------------->%% + # |%------>% + #|%-en,ne-}%en,en-}%en,ee-}%% + ((0, 10, 1), (5, 7, 1)): { + (S.entered, S.notEntered): (0, 5, 1), + (S.entered, S.entered): (5, 7, 1), + (S.entered, S.entered | S.exited): (7, 10, 1), + }, + + # -1 7 15 + # $<-------$$| + # $$<-----------------$$| + # {-ex,en--${-en,en-$$| + ((15, 7, -1), (15, -1, -1)): { + (S.exited | S.entered, S.entered): (7, -1, -1), + (S.entered, S.entered): (15, 7, -1), + }, + + # -1 1 2 7 + # $$<-------$--------$-------$| + # $<-------$| + # $${-en,ee-${-en,en-${-en,ne$| + ((7, -1, -1), (2, 1, -1)): { + (S.entered, S.notEntered): (7, 2, -1), + (S.entered, S.entered): (2, 1, -1), + (S.entered, S.exited | S.entered): (1, -1, -1) + }, + + # mixed direction + + # 0 4 5 7 9 + # |%-------$>% + # % $<%------------$| + # |%-en,ee-$X%----ee,en---$| + ((0, 5, 1), (9, 4, -1)): { + (S.entered, S.entered | S.exited): (0, 5, 1), + (S.entered | S.exited, S.entered): (9, 4, -1), + }, + #-1 4 5 7 10 + #$$<-------$|% + #$$ $|%------%------>%% + #$$(-en,ne-$|%----ne,en----}%% + ((4, -1, -1), (5, 10, 1)): { + (S.entered, S.notEntered): (4, -1, -1), + (S.notEntered, S.entered): (5, 10, 1), + }, + + # 0 4 7 9 + #|%-------$--------->% + # $<---------%------$| + #|%-en,ee-$}-en,en--{%-ee,en$| + ((0, 7, 1), (9, 4, -1)): { + (S.entered, S.entered | S.exited): (0, 5, 1), + (S.entered, S.entered): (5, 7, 1), + (S.entered | S.exited, S.entered): (9, 6, -1), + }, + #-1 5 6 10 + #$$<--------%------$| + #$$ |%------$-------->%% + #$${-en,ne-{%en,en-$|-ne,en-}%% + ((6, -1, -1), (5, 10, 1)): { + (S.entered, S.notEntered): (4, -1, -1), + (S.entered, S.entered): (6, 4, -1), + (S.notEntered, S.entered): (7, 10, 1), + }, + + # 0 4 6 10 + #|%-------$-------$-------->%% + # $<------$| + #|%-en,ee-$X-en,en$|-en,ne-}%% + ((0, 10, 1), (6, 4, -1)): { + (S.entered, S.entered | S.exited): (0, 5, 1), + (S.entered, S.entered): (5, 7, 1), + (S.entered, S.notEntered): (7, 10, 1), + }, + #-1 5 7 9 + #$$<--------%-------%-----$| + #$$ |%------>% + #$${-en,ne-{%en,en-{%en,ee$| + ((9, -1, -1), (5, 7, 1)): { + (S.entered, S.notEntered): (4, -1, -1), + (S.entered, S.entered): (6, 4, -1), + (S.entered, S.entered | S.exited): (9, 6, -1), + }, + } + + for ctor in isInstArg: + for chall, resp in pairs.items(): + chall = cnss(ctor, chall) + with self.subTest(chall=chall): + self.assertEqual( + sdiff(*chall), + { + k: cnss(ctor, v) for k, v in resp.items() + } + ) + + def test_sunion(self) -> None: + pairs = { + ((0, 5, 1), (5, 10, 1)): ((0, 10, 1),), + ((0, 8, 1), (4, 10, 1)): ((0, 10, 1),), + ((5, 10, 1), (0, 5, 1)): ((0, 10, 1),), + ((4, 10, 1), (0, 8, 1)): ((0, 10, 1),), + + ##failed + #((4, -1, -1), (5, 10, 1)): ((4, -1, -1), (9, 4, -1)), + #((0, 8, 1), (9, 3, -1)): ((0, 10, 1),), + #((9, 3, -1), (0, 8, 1)): ((9, -1, -1),), + #((5, 10, 1), (4, -1, -1)): ((0, 10, 1),), + #((4, -1, -1), (9, 4, -1)): ((9, -1, -1),), + #((9, 3, -1), (7, -1, -1)): ((9, -1, -1),), + #((7, -1, -1), (9, 3, -1)): ((9, -1, -1),), + #((9, 4, -1), (4, -1, -1)): ((9, -1, -1),), + } + + for ctor in isInstArg: + for chall, resp in pairs.items(): + chall = cnss(ctor, chall) + with self.subTest(chall=chall): + resp = cnss(ctor, resp) + self.assertEqual( + sunion(*chall), + resp + ) + + def test_ssub(self) -> None: + pairs = { + ((0, 5, 1),): ((0, 5, 1),), + ((0, 7, 1), (5, 10, 1)): ((0, 5, 1),), + ((0, 10, 1), (5, 7, 1)): ((0, 5, 1), (7, 10, 1)), + ((0, 10, 1), (5, 7, 1), (1, 8, 1)): ((0, 1, 1), (8, 10, 1)), + ((15, 7, -1), (15, -1, -1)): (), + ((7, -1, -1), (2, 1, -1)): ((7, 2, -1), (1, -1, -1)), + ((0, 5, 1), (7, 10, 1)): ((0, 5, 1),), + ((7, 10, 1), (0, 5, 1)): ((7, 10, 1),), + ((9, 6, -1), (4, -1, -1)): ((9, 6, -1),), + ((4, -1, -1), (9, 6, -1)): ((4, -1, -1),) + } + + for ctor in isInstArg: + for chall, resp in pairs.items(): + chall = cnss(ctor, chall) + with self.subTest(chall=chall): + resp = cnss(ctor, resp) + self.assertEqual( + resp, + ssub(*chall), + ) + + def test_sgap(self) -> None: + pairs = { + ((0, 5, 1), (5, 10, 1)): None, + ((0, 7, 1), (5, 10, 1)): None, + ((0, 10, 1), (5, 7, 1)): None, + ((15, 7, -1), (15, -1, -1)): None, + ((7, -1, -1), (2, 1, -1)): None, + ((0, 5, 1), (7, 10, 1)): (5, 7, 1), + ((7, 10, 1), (0, 5, 1)): (5, 7, 1), + ((9, 6, -1), (4, -1, -1)): (6, 4, -1), + ((4, -1, -1), (9, 6, -1)): (6, 4, -1), + } + + for ctor in isInstArg: + for chall, resp in pairs.items(): + chall = cnss(ctor, chall) + with self.subTest(chall=chall): + if resp is not None: + resp = cnss(ctor, resp) + self.assertEqual( + resp, + sgap(*chall), + ) + + def test_sdist(self) -> None: + pairs = { + ((0, 5, 1), (5, 10, 1)): 0, + ((0, 7, 1), (5, 10, 1)): 0, + ((0, 10, 1), (5, 7, 1)): 0, + ((15, 7, -1), (15, -1, -1)): 0, + ((7, -1, -1), (2, 1, -1)): 0, + ((0, 5, 1), (7, 10, 1)): 2, + ((7, 10, 1), (0, 5, 1)): 2, + ((9, 6, -1), (4, -1, -1)): 2, + ((4, -1, -1), (9, 6, -1)): 2, + } + + for ctor in isInstArg: + for chall, resp in pairs.items(): + chall = cnss(ctor, chall) + with self.subTest(chall=chall): + self.assertEqual( + sdist(*chall), + resp + ) + + +class IndexTestsProto(unittest.TestCase): + indexerCtor = None + + @staticmethod + def _genResItem(el, ctorSrc: SliceRangeT): + if isinstance(el, ValueLeaf): + return ValueLeaf(index=ctorSrc(*el.index), indexee=ctorSrc(*el.indexee)) + else: + return KeyLeaf(index=ctorSrc(*el)) + + @classmethod + def _genResult(cls, res: typing.Any, ctorSrc: SliceRangeT) -> typing.Iterator[KeyLeaf]: + for el in res: + yield cls._genResItem(el, ctorSrc) + + def _testIndex(self, index: typing.Optional[typing.Tuple[int, int, int]], testMatrix: typing.Dict[typing.Tuple[int, int, int], typing.Any], src: typing.List[typing.Tuple[int, int, int]] = None) -> None: + for ctorSrc in isInstArg: + t = self.__class__.indexerCtor(index=cnss(ctorSrc, index), data=(cnss(ctorSrc, src) if src is not None else None)) + #ic("t", t) + + for ctorQuery in isInstArg: + for q, expectedRes in testMatrix.items(): + q = ctorQuery(*q) + #ic("q", q) + with self.subTest(q=q): + expectedRes = tuple(self.__class__._genResult(expectedRes, ctorSrc)) + res = tuple(t[q]) + #ic("expectedRes", expectedRes) + #ic("res", res) + self.assertEqual(res, expectedRes) + + +#@unittest.skip +class TreeTests(IndexTestsProto): + indexerCtor = RangesTree.build + + def testTreeTrivial(self) -> None: + rng = (7, -1, -1) + src = [rng] + index = rng + + def genTests(): + for i in range(*rng): + for j in range(i, *rng[1:]): + q = (i, j, rng[2]) + yield (q, (ValueLeaf(rng, rng),)) + + matrix = dict(genTests()) + self._testIndex(index, matrix, src) + + def testsTreeDumb(self) -> None: + index = ((0, 4, 1), (4, 8, 1), (8, 12, 1), (12, 16, 1)) + matrix = { + (0, 8, 1): ((0, 4, 1), (4, 8, 1)), + (8, 16, 1): ((8, 12, 1), (12, 16, 1)), + + (4, 12, 1): ((4, 8, 1), (8, 12, 1)), + (4, 8, 1): ((4, 8, 1),), + + (1, 15, 1): ((0, 4, 1), (4, 8, 1), (8, 12, 1), (12, 16, 1)), + (5, 15, 1): ((4, 8, 1), (8, 12, 1), (12, 16, 1)), + (9, 15, 1): ((8, 12, 1), (12, 16, 1)), + + (12, 15, 1): ((12, 16, 1),), + (3, 4, 1): ((0, 4, 1),), + (0, 1, 1): ((0, 4, 1),), + (7, 9, 1): ((4, 8, 1), (8, 12, 1)), + } + self._testIndex(index, matrix) + + def testsTreeDisjoint(self) -> None: + index = ((0, 9, 1), (14, 32, 1), (127, 255, 1)) + matrix = { + (-1, 0, 1): (), + (0, 1, 1): ((0, 9, 1),), + (9, 10, 1): (), + (-1, 10, 1): ((0, 9, 1),), + (-1, 5, 1): ((0, 9, 1),), + (5, 10, 1): ((0, 9, 1),), + + (13, 14, 1): (), + (14, 15, 1): ((14, 32, 1),), + (32, 33, 1): (), + (13, 33, 1): ((14, 32, 1),), + (13, 23, 1): ((14, 32, 1),), + (23, 33, 1): ((14, 32, 1),), + + (126, 127, 1): (), + (127, 128, 1): ((127, 255, 1),), + (32, 256, 1): (), + (126, 256, 1): ((127, 255, 1),), + (126, 165, 1): ((127, 255, 1),), + (32, 256, 1): ((127, 255, 1),), + } + self._testIndex(index, matrix) + + def testsTreeDumbNegatives(self) -> None: + index = ((0, 4, 1), (4, 8, 1), (8, 12, 1), (12, 16, 1)) + + matrix = { + (7, -1, -1): ((0, 4, 1), (4, 8, 1)), + (15, 7, -1): ((8, 12, 1), (12, 16, 1)), + + (11, 3, -1): ((4, 8, 1), (8, 12, 1)), + (7, 3, -1): ((4, 8, 1),), + + (14, 0, -1): ((0, 4, 1), (4, 8, 1), (8, 12, 1), (12, 16, 1)), + (14, 4, -1): ((4, 8, 1), (8, 12, 1), (12, 16, 1)), + (14, 8, -1): ((8, 12, 1), (12, 16, 1)), + + (14, 11, -1): ((12, 16, 1),), + (3, 2, -1): ((0, 4, 1),), + (0, -1, -1): ((0, 4, 1),), + (8, 6, -1): ((4, 8, 1), (8, 12, 1)), + } + self._testIndex(index, matrix) + + def testsTreeReversedDumb(self) -> None: + index = ( + (3, 2, -1), + (2, 1, -1), + (1, 0, -1), + (0, -1, -1), + ) + #ic(t) + + matrix = { + (0, 2, 1): ((1, 0, -1), (0, -1, -1)), + (2, 4, 1): ((3, 2, -1), (2, 1, -1)), + + (1, 3, 1): ((2, 1, -1), (1, 0, -1)), + (1, 2, 1): ((1, 0, -1),), + } + self._testIndex(index, matrix) + + def testsTreeIndexedPositiveDumb(self) -> None: + src = ((0, 4, 1), (4, 8, 1), (8, 12, 1), (12, 16, 1)) + index = (-16, 0, 1) + + matrix = { + (-16, -8, 1): (ValueLeaf((-16, -12, 1), (0, 4, 1)), ValueLeaf((-12, -8, 1), (4, 8, 1))), + (-8, 0, 1): (ValueLeaf((-8, -4, 1), (8, 12, 1)), ValueLeaf((-4, 0, 1), (12, 16, 1))), + + (-12, -4, 1): (ValueLeaf((-12, -8, 1), (4, 8, 1)), ValueLeaf((-8, -4, 1), (8, 12, 1))), + (-12, -8, 1): (ValueLeaf((-12, -8, 1), (4, 8, 1)),), + + (-15, -1, 1): (ValueLeaf((-16, -12, 1), (0, 4, 1)), ValueLeaf((-12, -8, 1), (4, 8, 1)), ValueLeaf((-8, -4, 1), (8, 12, 1)), ValueLeaf((-4, 0, 1), (12, 16, 1))), + (-11, -1, 1): (ValueLeaf((-12, -8, 1), (4, 8, 1)), ValueLeaf((-8, -4, 1), (8, 12, 1)), ValueLeaf((-4, 0, 1), (12, 16, 1))), + (-7, -1, 1): (ValueLeaf((-8, -4, 1), (8, 12, 1)), ValueLeaf((-4, 0, 1), (12, 16, 1))), + + (-4, -1, 1): (ValueLeaf((-4, 0, 1), (12, 16, 1)),), + (-13, -12, 1): (ValueLeaf((-16, -12, 1), (0, 4, 1)),), + (-16, -15, 1): (ValueLeaf((-16, -12, 1), (0, 4, 1)),), + (-9, -7, 1): (ValueLeaf((-12, -8, 1), (4, 8, 1)), ValueLeaf((-8, -4, 1), (8, 12, 1))), + } + self._testIndex(index, matrix, src) + + def testsClosest(self) -> None: + src = ((0, 3, 1), (6, 7, 1), (12, 16, 1), (16, 20, 1)) + testMatrix = { + (-10, -5, 1): ((0, 3, 1), (0, 0), 5), + (100, 101, 1): ((16, 20, 1), (1, 1), 80), + (5, 6, 1): ((6, 7, 1), (0, 1), 0), + (4, 5, 1): ((0, 3, 1), (0, 0), 1), + (4, 6, 1): ((6, 7, 1), (0, 1), 0), # depends on python impl of `min`, may be (0, 3, 1) + } + + for ctorSrc in isInstArg: + t = self.__class__.indexerCtor([ctorSrc(*el) for el in src], None) + + for ctorQuery in isInstArg: + for q, expectedRes in testMatrix.items(): + q = ctorQuery(*q) + with self.subTest(q=q): + expectedRes = [FuzzySingleLookupResult(self.__class__._genResItem(expectedRes[0], ctorSrc), *expectedRes[1:3], q)] + res = t.get_closest(q) + self.assertEqual(res, expectedRes) + + @classmethod + def _genTestLenCombs(cls, initTree): + l = len(initTree) + for i in range(l - 1): + for comb in itertools.combinations(range(l), i): + selector = [True] * len(initTree) + for el in comb: + selector[el] = False + yield (initTree.__class__(itertools.compress(initTree, selector)), l - i) + + def testsLen(self) -> None: + initTree = ((0, 3, 1), (6, 7, 1), (12, 16, 1), (16, 20, 1)) + testMatrix = dict(self.__class__._genTestLenCombs(initTree)) + + for challenge, response in testMatrix.items(): + with self.subTest(challenge=challenge, response=response): + self.assertEqual(response, len(self.__class__.indexerCtor(cnss(range, challenge)))) + + def testsSetAttr(self): + setElProto = (18, 22, 1) + treeProto = ((0, 4, 1), (4, 8, 1), (8, 12, 1), (12, 16, 1)) + matrix = { + ((-16, -12, 1), ): ( + ((-16, -12, 1), (-12, -8, 1), (-8, -4, 1), (-4, 0, 1)), + (setElProto, (4, 8, 1), (8, 12, 1), (12, 16, 1)) + ), + ((-12, -8, 1), ): ( + ((-16, -12, 1), (-12, -8, 1), (-8, -4, 1), (-4, 0, 1)), + ((0, 4, 1), setElProto, (8, 12, 1), (12, 16, 1)) + ), + ((-8, -4, 1), ): ( + ((-16, -12, 1), (-12, -8, 1), (-8, -4, 1), (-4, 0, 1)), + ((0, 4, 1), (4, 8, 1), setElProto, (12, 16, 1)) + ), + ((-4, 0, 1), ): ( + ((-16, -12, 1), (-12, -8, 1), (-8, -4, 1), (-4, 0, 1)), + ((0, 4, 1), (4, 8, 1), (8, 12, 1), setElProto) + ), + ((-20, -16, 1), ): ( + ((-20, -16, 1), (-16, -12, 1), (-12, -8, 1), (-8, -4, 1), (-4, 0, 1)), + (setElProto, (0, 4, 1), (4, 8, 1), (8, 12, 1), (12, 16, 1)) + ), + ((16, 20, 1), ): ( + ((-16, -12, 1), (-12, -8, 1), (-8, -4, 1), (-4, 0, 1), (16, 20, 1)), + ((0, 4, 1), (4, 8, 1), (8, 12, 1), (12, 16, 1), setElProto) + ), + } + + for ctor in isInstArg: + src = cnss(ctor, treeProto) + #ic(src) + index = ctor(-16, 0, 1) + setEl = ctor(*setElProto) + for indices2set, etalonFlatStructure in matrix.items(): + t = RangesTree.build(index=index, data=src) + etalonFlatStructure = cnss(ctor, etalonFlatStructure) + for index2Set in indices2set: + index2Set = ctor(*index2Set) + t[index2Set] = setEl + self.assertEqual(list(t[index2Set])[0].indexee, setEl) + + self.assertEqual((tuple(el.index for el in t), tuple(el.indexee for el in t)), etalonFlatStructure) + + +#@unittest.skip +class SeqTests(IndexTestsProto): + indexerCtor = SliceSequence + + def testSequenceTrivial(self) -> None: + rng = (7, -1, -1) + src = [rng] + index = rng + + def genTests(): + for i in range(*rng): + for j in range(i, *rng[1:]): + q = (i, j, rng[2]) + yield (q, (ValueLeaf(q, q),)) + + #matrix = dict(genTests()) + matrix = { + (2, 0, -1): (ValueLeaf((2, 0, -1), (2, 0, -1)),) + } + self._testIndex(index, matrix, src) + + def testSequenceDumb(self) -> None: + src = [(0, 4, 1), (4, 8, 1), (8, 12, 1), (12, 16, 1)] + index = (0, 16, 1) + + matrix = { + (0, 8, 1): (ValueLeaf((0, 4, 1), (0, 4, 1)), ValueLeaf((4, 8, 1), (4, 8, 1))), + + (8, 16, 1): (ValueLeaf((8, 12, 1), (8, 12, 1)), ValueLeaf((12, 16, 1), (12, 16, 1))), + + (4, 12, 1): (ValueLeaf((4, 8, 1), (4, 8, 1)), ValueLeaf((8, 12, 1), (8, 12, 1))), + (4, 8, 1): (ValueLeaf((4, 8, 1), (4, 8, 1)),), + + + (1, 15, 1): (ValueLeaf((1, 4, 1), (1, 4, 1)), ValueLeaf((4, 8, 1), (4, 8, 1)), ValueLeaf((8, 12, 1), (8, 12, 1)), ValueLeaf((12, 15, 1), (12, 15, 1))), + (5, 15, 1): (ValueLeaf((5, 8, 1), (5, 8, 1)), ValueLeaf((8, 12, 1), (8, 12, 1)), ValueLeaf((12, 15, 1), (12, 15, 1))), + (9, 15, 1): (ValueLeaf((9, 12, 1), (9, 12, 1)), ValueLeaf((12, 15, 1), (12, 15, 1))), + + + (12, 15, 1): (ValueLeaf((12, 15, 1), (12, 15, 1)),), + (3, 4, 1): (ValueLeaf((3, 4, 1), (3, 4, 1)),), + (0, 1, 1): (ValueLeaf((0, 1, 1), (0, 1, 1)),), + (7, 9, 1): (ValueLeaf((7, 8, 1), (7, 8, 1)), ValueLeaf((8, 9, 1), (8, 9, 1))), + } + self._testIndex(index, matrix, src) + + def testSequenceEndianness(self) -> None: + src = [(7, -1, -1), (15, 7, -1)] + index = (15, -1, -1) + + matrix = { + (15, -1, -1): (ValueLeaf((15, 7, -1), (7, -1, -1)), ValueLeaf((7, -1, -1), (15, 7, -1))), + } + self._testIndex(index, matrix, src) + + +if __name__ == "__main__": + unittest.main()