├── .ackrc ├── .github └── workflows │ └── manual.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── HACKING.md ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.md ├── intervaltree ├── __init__.py ├── interval.py ├── intervaltree.py └── node.py ├── scripts └── testall.sh ├── setup.cfg ├── setup.py └── test ├── __init__.py ├── data ├── __init__.py ├── issue25_orig.py ├── issue4.py ├── issue41_orig.py ├── issue4_result.py ├── ivs0.py ├── ivs1.py ├── ivs1_float_copy_structure.py ├── ivs2.py └── ivs3.py ├── interval_methods ├── __init__.py ├── binary_test.py ├── sorting_test.py └── unary_test.py ├── intervals.py ├── intervaltree_methods ├── __init__.py ├── copy_test.py ├── debug_test.py ├── delete_test.py ├── init_test.py ├── insert_test.py ├── query_test.py ├── restructure_test.py └── setlike_test.py ├── intervaltrees.py ├── issues ├── __init__.py ├── issue25.txt ├── issue25_test.py ├── issue26_test.py ├── issue27_test.py ├── issue4.py ├── issue41_test.py ├── issue67_test.py └── issue72_test.py ├── match.py ├── optimality ├── __init__.py ├── optimality_test.py └── optimality_test_matrix.py ├── pprint.py └── progress_bar.py /.ackrc: -------------------------------------------------------------------------------- 1 | --ignore-dir=intervaltree.egg-info 2 | --norst 3 | -------------------------------------------------------------------------------- /.github/workflows/manual.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | 8 | env: 9 | TEST_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/intervaltree:test 10 | LATEST_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/intervaltree:latest 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - 17 | name: Checkout 18 | uses: actions/checkout@v3 19 | - 20 | name: Login to Docker Hub 21 | uses: docker/login-action@v2 22 | with: 23 | username: ${{ secrets.DOCKERHUB_USERNAME }} 24 | password: ${{ secrets.DOCKERHUB_TOKEN }} 25 | - 26 | name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v2 28 | - 29 | name: Build and export to Docker 30 | uses: docker/build-push-action@v4 31 | with: 32 | context: . 33 | file: ./Dockerfile 34 | load: true 35 | tags: ${{ env.TEST_TAG }} 36 | - 37 | name: Test 38 | run: docker run --rm ${{ env.TEST_TAG }} 39 | - 40 | name: Build and push 41 | uses: docker/build-push-action@v4 42 | with: 43 | context: . 44 | file: ./Dockerfile 45 | push: true 46 | tags: ${{ env.LATEST_TAG }} 47 | 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE dirs 2 | .idea/ 3 | *.komodoproject 4 | .vscode/ 5 | 6 | # Auto-generated 7 | *.pyc 8 | dist/ 9 | build/ 10 | .cache/ 11 | .pytest_cache/ 12 | 13 | # generated by coverage 14 | .coverage 15 | htmlcov/ 16 | 17 | # Dev dependencies 18 | pyandoc/ 19 | pandoc 20 | docutils/ 21 | bin/ 22 | 23 | # Developer eggs 24 | *.egg-info 25 | *.egg 26 | .eggs/ 27 | 28 | # Mac noise 29 | .DS_Store 30 | 31 | # Other temps 32 | restats 33 | 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## Version 3.1.0 4 | - Dropped support for Python 3.4, added Python 3.8 5 | - Add `__slots__` optimization in Node class, should give performance improvement 6 | - Fixed: 7 | - Restore universal wheels 8 | - Bytes/str type incompatibility in setup.py 9 | - New version of distutils rejects version suffixes of `.postNN`, use `aNN` instead 10 | 11 | ## Version 3.0.2 12 | - Fixed: 13 | - On some systems, setup.py opened README.md with a non-unicode encoding. My fault for leaving the encoding flapping in the breeze. It's been fixed. 14 | 15 | ## Version 3.0.1 16 | - Added: 17 | - Travis testing for 3.7 and 3.8-dev. These needed OpenSSL, sudo and Xenial. 3.8-dev is allowed to fail. 18 | - Fixed: 19 | - PyPI wasn't rendering markdown because I didn't tell it what format to use. 20 | - Python 2 wasn't installing via pip because of a new utils package. It has been zapped. 21 | - Maintainers: 22 | - TestPyPI version strings use `.postN` as the suffix instead of `bN`, and `N` counts from the latest tagged commit, which should be the last release 23 | - Install from TestPyPI works via `make install-testpypi` 24 | 25 | ## Version 3.0.0 26 | - Breaking: 27 | - `search(begin, end, strict)` has been replaced with `at(point)`, `overlap(begin, end)`, and `envelop(begin, end)` 28 | - `extend(items)` has been deleted, use `update(items)` instead 29 | - Methods that take a `strict=True/False` argument now consistently default to `strict=True` 30 | - Dropped support for Python 2.6, 3.2, and 3.3 31 | - Add support for Python 3.5, 3.6, and 3.7 32 | - Faster `Interval` overlap checking (@tuxzz, #56) 33 | - Updated README: 34 | - new restructuring methods from 2.1.0 35 | - example of `from_tuples()` added 36 | - more info about `chop()`, `split_overlaps()`, `merge_overlaps()` and `merge_equals()`. 37 | - Fixes: 38 | - `Node.from_tuples()` will now raise an error if given an empty iterable. This should never happen, and it should error if it does. 39 | - `Interval.distance_to()` gave an incorrect distance when passed the `Interval`'s upper boundary 40 | - `Node.pop_greatest_child()` sometimes forgot to `rotate()` when creating new child nodes. (@escalonn, #41, #42) 41 | - `IntervalTree.begin()` and `end()` are O(1), not O(n). (@ProgVal, #40) 42 | - `intersection_update()` and `symmetric_difference()` and `symmetric_difference_update()` didn't actually work. Now they do. 43 | - `collections.abc` deprecation warning no longer happens 44 | - Maintainers: 45 | - PyPi accepts Markdown! Woohoo! 46 | - reorganize tests 47 | - more tests added to improve code coverage (We're at 96%! Yay!) 48 | - test for issue #4 had a broken import reference 49 | 50 | ## Version 2.1.0 51 | - Added: 52 | - `merge_overlaps()` method and tests 53 | - `merge_equals()` method and tests 54 | - `range()` method 55 | - `span()` method, for returning the difference between `end()` and `begin()` 56 | - Fixes: 57 | - Development version numbering is changing to be compliant with PEP440. Version numbering now contains major, minor and micro release numbers, plus the number of builds following the stable release version, e.g. 2.0.4b34 58 | - Speed improvement: `begin()` and `end()` methods used iterative `min()` and `max()` builtins instead of the more efficient `iloc` member available to `SortedDict` 59 | - `overlaps()` method used to return `True` even if provided null test interval 60 | - Maintainers: 61 | - Added coverage test (`make coverage`) with html report (`htmlcov/index.html`) 62 | - Tests run slightly faster 63 | 64 | ## Version 2.0.4 65 | - Fix: Issue #27: README incorrectly showed using a comma instead of a colon when querying the `IntervalTree`: it showed `tree[begin, end]` instead of `tree[begin:end]` 66 | 67 | ## Version 2.0.3 68 | - Fix: README showed using + operator for setlike union instead of the correct | operator 69 | - Removed tests from release package to speed up installation; to get the tests, download from GitHub 70 | 71 | ## Version 2.0.2 72 | - Fix: Issue #20: performance enhancement for large trees. `IntervalTree.search()` made a copy of the entire `boundary_table` resulting in linear search time. The `sortedcollections` package is now the sole install dependency 73 | 74 | ## Version 2.0.1 75 | - Fix: Issue #26: failed to prune empty `Node` after a rotation promoted contents of `s_center` 76 | 77 | ## Version 2.0.0 78 | - `IntervalTree` now supports the full `collections.MutableSet` API 79 | - Added: 80 | - `__delitem__` to `IntervalTree` 81 | - `Interval` comparison methods `lt()`, `gt()`, `le()` and `ge()` to `Interval`, as an alternative to the comparison operators, which are designed for sorting 82 | - `IntervalTree.from_tuples(iterable)` 83 | - `IntervalTree.clear()` 84 | - `IntervalTree.difference(iterable)` 85 | - `IntervalTree.difference_update(iterable)` 86 | - `IntervalTree.union(iterable)` 87 | - `IntervalTree.intersection(iterable)` 88 | - `IntervalTree.intersection_update(iterable)` 89 | - `IntervalTree.symmetric_difference(iterable)` 90 | - `IntervalTree.symmetric_difference_update(iterable)` 91 | - `IntervalTree.chop(a, b)` 92 | - `IntervalTree.slice(point)` 93 | - Deprecated `IntervalTree.extend()` -- use `update()` instead 94 | - Internal improvements: 95 | - More verbose tests with progress bars 96 | - More tests for comparison and sorting behavior 97 | - Code in the README is included in the unit tests 98 | - Fixes 99 | - BACKWARD INCOMPATIBLE: On ranged queries where `begin >= end`, the query operated on the overlaps of `begin`. This behavior was documented as expected in 1.x; it is now changed to be more consistent with the definition of `Interval`s, which are half-open. 100 | - Issue #25: pruning empty Nodes with staggered descendants could result in invalid trees 101 | - Sorting `Interval`s and numbers in the same list gathered all the numbers at the beginning and the `Interval`s at the end 102 | - `IntervalTree.overlaps()` and friends returned `None` instead of `False` 103 | - Maintainers: `make install-testpypi` failed because the `pip` was missing a `--pre` flag 104 | 105 | ## Version 1.1.1 106 | - Removed requirement for pyandoc in order to run functionality tests. 107 | 108 | ## Version 1.1.0 109 | - Added ability to use `Interval.distance_to()` with points, not just `Intervals` 110 | - Added documentation on return types to `IntervalTree` and `Interval` 111 | - `Interval.__cmp__()` works with points too 112 | - Fix: `IntervalTree.score()` returned maximum score of 0.5 instead of 1.0. Now returns max of subscores instead of avg 113 | - Internal improvements: 114 | - Development version numbering scheme, based on `git describe` the "building towards" release is appended after a hyphen, eg. 1.0.2-37-g2da2ef0-1.10. The previous tagged release is 1.0.2, and there have been 37 commits since then, current tag is g2da2ef0, and we are getting ready for a 1.1.0 release 115 | - Optimality tests added 116 | - `Interval` overlap tests for ranges, `Interval`s and points added 117 | 118 | ## Version 1.0.2 119 | -Bug fixes: 120 | - `Node.depth_score_helper()` raised `AttributeError` 121 | - README formatting 122 | 123 | ## Version 1.0.1 124 | - Fix: pip install failure because of failure to generate README.rst 125 | 126 | ## Version 1.0.0 127 | - Renamed from PyIntervalTree to intervaltree 128 | - Speed improvements for adding and removing Intervals (~70% faster than 0.4) 129 | - Bug fixes: 130 | - BACKWARD INCOMPATIBLE: `len()` of an `Interval` is always 3, reverting to default behavior for `namedtuples`. In Python 3, `len` returning a non-integer raises an exception. Instead, use `Interval.length()`, which returns 0 for null intervals and `end - begin` otherwise. Also, if the `len() === 0`, then `not iv` is `True`. 131 | - When inserting an `Interval` via `__setitem__` and improper parameters given, all errors were transformed to `IndexError` 132 | - `split_overlaps` did not update the `boundary_table` counts 133 | - Internal improvements: 134 | - More robust local testing tools 135 | - Long series of interdependent tests have been separated into sections 136 | 137 | ## Version 0.4 138 | - Faster balancing (~80% faster) 139 | - Bug fixes: 140 | - Double rotations were performed in place of a single rotation when presented an unbalanced Node with a balanced child. 141 | - During single rotation, kept referencing an unrotated Node instead of the new, rotated one 142 | 143 | ## Version 0.3.3 144 | - Made IntervalTree crash if inited with a null Interval (end <= begin) 145 | - IntervalTree raises ValueError instead of AssertionError when a null Interval is inserted 146 | 147 | ## Version 0.3.2 148 | - Support for Python 3.2+ and 2.6+ 149 | - Changed license from LGPL to more permissive Apache license 150 | - Merged changes from https://github.com/konstantint/PyIntervalTree to 151 | https://github.com/chaimleib/PyIntervalTree 152 | - Interval now inherits from a namedtuple. Benefits: should be faster. 153 | Drawbacks: slight behavioural change (Intervals not mutable anymore). 154 | - Added float tests 155 | - Use setup.py for tests 156 | - Automatic testing via travis-ci 157 | - Removed dependency on six 158 | - Interval improvements: 159 | - Intervals without data have a cleaner string representation 160 | - Intervals without data are pickled more compactly 161 | - Better hashing 162 | - Intervals are ordered by begin, then end, then by data. If data is not 163 | orderable, sorts by type(data) 164 | - Bug fixes: 165 | - Fixed crash when querying empty tree 166 | - Fixed missing close parenthesis in examples 167 | - Made IntervalTree crash earlier if a null Interval is added 168 | - Internals: 169 | - New test directory 170 | - Nicer display of data structures for debugging, using custom 171 | test/pprint.py (Python 2.6, 2.7) 172 | - More sensitive exception handling 173 | - Local script to test in all supported versions of Python 174 | - Added IntervalTree.score() to measure how optimally a tree is structured 175 | 176 | ## Version 0.2.3 177 | - Slight changes for inclusion in PyPI. 178 | - Some documentation changes 179 | - Added tests 180 | - Bug fix: interval addition via [] was broken in Python 2.7 (see http://bugs.python.org/issue21785) 181 | - Added intervaltree.bio subpackage, adding some utilities for use in bioinformatics 182 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Modified by chaimleib March 2023 from 2 | # https://github.com/vicamo/docker-pyenv/blob/main/alpine/Dockerfile 3 | # 4 | # Changes: 5 | # * customize the versions of python installed 6 | # * remove the dependency on github.com/momo-lab/xxenv-latest 7 | # * forbid failures when building python 8 | # * add other tools like parallel 9 | # * run intervaltree tests 10 | 11 | FROM alpine:latest AS base 12 | 13 | ENV PYENV_ROOT="/opt/pyenv" 14 | ENV PYENV_SHELL="bash" 15 | ENV PATH="${PYENV_ROOT}/shims:${PYENV_ROOT}/bin:$PATH" 16 | 17 | # http://bugs.python.org/issue19846 18 | # > At the moment, setting "LANG=C" on a Linux system *fundamentally breaks Python 3*, and that's not OK. 19 | ENV LANG C.UTF-8 20 | 21 | # runtime dependencies 22 | RUN set -eux; \ 23 | apk update; \ 24 | apk add --no-cache \ 25 | bash \ 26 | build-base \ 27 | bzip2 \ 28 | ca-certificates \ 29 | curl \ 30 | expat \ 31 | git \ 32 | libffi \ 33 | mpdecimal \ 34 | ncurses-libs \ 35 | openssl \ 36 | parallel \ 37 | readline \ 38 | sqlite-libs \ 39 | tk \ 40 | xz \ 41 | zlib \ 42 | ; 43 | 44 | RUN set -eux; \ 45 | curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash; \ 46 | pyenv update 47 | 48 | FROM base AS builder 49 | 50 | # runtime dependencies 51 | RUN set -eux; \ 52 | apk update; \ 53 | apk add --no-cache \ 54 | bzip2-dev \ 55 | libffi-dev \ 56 | ncurses-dev \ 57 | openssl-dev \ 58 | readline-dev \ 59 | sqlite-dev \ 60 | tk-dev \ 61 | xz-dev \ 62 | zlib-dev \ 63 | ; 64 | 65 | FROM builder AS build-2.7.18 66 | RUN set -eux; pyenv install 2.7.18; \ 67 | find ${PYENV_ROOT}/versions -depth \ 68 | \( \ 69 | \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \ 70 | -o \( -type f -a -name 'wininst-*.exe' \) \ 71 | \) -exec rm -rf '{}' + 72 | 73 | FROM builder AS build-3.6.15 74 | RUN set -eux; pyenv install 3.6.15; \ 75 | find ${PYENV_ROOT}/versions -depth \ 76 | \( \ 77 | \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \ 78 | -o \( -type f -a -name 'wininst-*.exe' \) \ 79 | \) -exec rm -rf '{}' + 80 | 81 | FROM builder AS build-3.7.16 82 | RUN set -eux; pyenv install 3.7.16; \ 83 | find ${PYENV_ROOT}/versions -depth \ 84 | \( \ 85 | \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \ 86 | -o \( -type f -a -name 'wininst-*.exe' \) \ 87 | \) -exec rm -rf '{}' + 88 | 89 | FROM builder AS build-3.8.16 90 | RUN set -eux; pyenv install 3.8.16; \ 91 | find ${PYENV_ROOT}/versions -depth \ 92 | \( \ 93 | \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \ 94 | -o \( -type f -a -name 'wininst-*.exe' \) \ 95 | \) -exec rm -rf '{}' + 96 | 97 | FROM builder AS build-3.9.16 98 | RUN set -eux; pyenv install 3.9.16; \ 99 | find ${PYENV_ROOT}/versions -depth \ 100 | \( \ 101 | \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \ 102 | -o \( -type f -a -name 'wininst-*.exe' \) \ 103 | \) -exec rm -rf '{}' + 104 | 105 | FROM builder AS build-3.10.10 106 | RUN set -eux; pyenv install 3.10.10; \ 107 | find ${PYENV_ROOT}/versions -depth \ 108 | \( \ 109 | \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \ 110 | -o \( -type f -a -name 'wininst-*.exe' \) \ 111 | \) -exec rm -rf '{}' + 112 | 113 | FROM builder AS build-3.11.2 114 | RUN set -eux; pyenv install 3.11.2; \ 115 | find ${PYENV_ROOT}/versions -depth \ 116 | \( \ 117 | \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \ 118 | -o \( -type f -a -name 'wininst-*.exe' \) \ 119 | \) -exec rm -rf '{}' + 120 | 121 | FROM base AS tester 122 | COPY --from=build-2.7.18 /opt/pyenv/versions/2.7.18 /opt/pyenv/versions/2.7.18 123 | COPY --from=build-3.6.15 /opt/pyenv/versions/3.6.15 /opt/pyenv/versions/3.6.15 124 | COPY --from=build-3.7.16 /opt/pyenv/versions/3.7.16 /opt/pyenv/versions/3.7.16 125 | COPY --from=build-3.8.16 /opt/pyenv/versions/3.8.16 /opt/pyenv/versions/3.8.16 126 | COPY --from=build-3.9.16 /opt/pyenv/versions/3.9.16 /opt/pyenv/versions/3.9.16 127 | COPY --from=build-3.10.10 /opt/pyenv/versions/3.10.10 /opt/pyenv/versions/3.10.10 128 | COPY --from=build-3.11.2 /opt/pyenv/versions/3.11.2 /opt/pyenv/versions/3.11.2 129 | 130 | RUN set -eux; \ 131 | pyenv rehash; \ 132 | pyenv versions 133 | 134 | WORKDIR /intervaltree 135 | COPY . . 136 | CMD [ "scripts/testall.sh" ] 137 | 138 | -------------------------------------------------------------------------------- /HACKING.md: -------------------------------------------------------------------------------- 1 | Hacking intervaltree 2 | ==================== 3 | 4 | This is a developer's guide to modifying and maintaining `intervaltree`. 5 | 6 | ## Dependencies 7 | 8 | * On a Mac, you will need [`brew`][brew]. 9 | 10 | * On Linux, you will need `apt-get`. 11 | 12 | On all systems, Python 2.6, 2.7, 3.2, 3.3, 3.4 and 3.5 are needed to run the complete test suite. 13 | 14 | ### Single version of Python 15 | 16 | If you don't have all the above versions of Python, these `make` features will be unavailable to you: 17 | 18 | * `make` and `make test` 19 | * `make pytest` 20 | * `make upload` and `make release upload` 21 | 22 | 23 | ## Project structure 24 | 25 | ### `intervaltree` 26 | 27 | The `intervaltree` directory has three main files: 28 | 29 | * `intervaltree.py` 30 | * `interval.py` 31 | * `node.py` 32 | 33 | `intervaltree.py` and `interval.py` contain the public API to `IntervalTree` and `Interval`. `node.py` contains the internal logic of the tree. For the theory of how this type of tree works, read the following: 34 | 35 | * Wikipedia's [Interval tree][Wiki intervaltree] article 36 | * Eternally Confuzzled's tutorial on [AVL balancing][Confuzzled AVL tree] 37 | * Tyler Kahn's simpler, immutable [interval tree implementation][Kahn intervaltree] in Python 38 | 39 | ### `test` 40 | 41 | All files ending with `_test.py` are detected and run whenever you run `make` or `make quicktest`. In those files, only functions beginning with `test_` are executed. 42 | 43 | #### `test/data` 44 | Some tests depend on having certain lists of `Interval`s. These are stored in the modules of `test/data`. Most of these modules only contain a `data` attribute, which is a list of tuples that is converted to a list of `Interval`s by `test/intervals.py`. You can access them by importing the dict of lists of `Interval`s `test.intervals.ivs`. 45 | 46 | Other tests (like `test/issue25_test.py`) depend on having pre-constructed `IntervalTree`s. These are constructed by `test/intervaltrees.py` and can be accessed by importing `test.intervaltrees.trees`. This is a dict of callables that return `IntervalTree`s. 47 | 48 | ### `scripts` 49 | 50 | Contains `testall.sh`, which runs all tests on all supported versions of Python. 51 | 52 | ### Dependency folders 53 | 54 | * `pyandoc` and `pandoc` give the ability to convert README.md into rst format for PyPI. 55 | * `docutils` and `bin` are created if `pip` could not install them system-wide without permissions. These are used to check the syntax of the converted rst README. 56 | * `*.egg` are created by `PyTest` to fulfill testing dependencies 57 | * `intervaltree.egg-info` is package metadata generated by `setup.py`. 58 | 59 | ### Other documentation files 60 | 61 | * `HACKING.md` is this file. 62 | * `README.md` contains public API documentation and credits. 63 | * `README.rst` is generated from `README.md`. 64 | * `CHANGELOG.md` 65 | * `LICENSE.txt` 66 | 67 | ### Other code files 68 | 69 | * `Makefile` contains convenience routines for managing and testing the project. It installs certain dependencies that are too inconvenient to install on [travis-ci.org][], and gives an easy way to call `test/testall.sh`. 70 | * `setup.py` runs the tests in a single version of Python. Also, packages the project for PyPI. 71 | * `setup.cfg` configures `setup.py`. If you want to permanently skip a folder in testing, do it here. 72 | 73 | 74 | ## Testing 75 | 76 | ### Code 77 | 78 | To run the tests in the `test` directory, run 79 | 80 | make test 81 | 82 | or simply 83 | 84 | make 85 | 86 | The two commands above run all the available tests on all versions of Python supported. You should run `make` first, because it will also detect missing dependencies and install them. 87 | 88 | The first time you run `make`, you may be asked for your password. This is in order to install `pandoc`, a tool used for processing the README file. 89 | 90 | Running all tests requires that you have all the supported versions of Python installed. These are 2.6, 2.7, 3.2, 3.3, 3.4 and 3.5. Try to use your package manager to install them if possible. Otherwise, go to [python.org/downloads][] and install them manually. 91 | 92 | #### Single version of Python 93 | 94 | Run 95 | 96 | make quicktest 97 | 98 | ### README 99 | 100 | To test changes to the README and make sure that they are compatible with PyPI's very restrictive rules, run 101 | 102 | make rst 103 | 104 | `make rst` is also run by `make test`, but `make test` takes longer. 105 | 106 | 107 | ## Cleaning 108 | 109 | To clean up the project directory, run 110 | 111 | make distclean 112 | 113 | That should remove all the locally-installed dependencies and clear out all the temporary files. 114 | 115 | To keep the dependencies, but clean everything else, run 116 | 117 | make clean 118 | 119 | 120 | ## Maintainers: Working with PyPI 121 | 122 | ### README 123 | 124 | To update the README on PyPI, run 125 | 126 | make register 127 | 128 | This will test the README's syntax strictly and push it up to the PyPI test server. 129 | 130 | If you are satisfied with the results, run 131 | 132 | make release register 133 | 134 | to do it for real on the production PyPI server. 135 | 136 | ### Publishing 137 | 138 | To publish a new version to PyPI, run 139 | 140 | make upload 141 | 142 | This will run `make test`, zip up the source distribution and push it to the PyPI test server. 143 | 144 | If this looks like it went well, run 145 | 146 | make release upload 147 | 148 | to push the distribution to the production PyPI server. 149 | 150 | 151 | [brew]: http://brew.sh/ 152 | [python.org/downloads]: http://www.python.org/downloads 153 | [travis-ci.org]: https://travis-ci.org/ 154 | [Confuzzled AVL tree]: http://www.eternallyconfuzzled.com/tuts/datastructures/jsw_tut_avl.aspx 155 | [Wiki intervaltree]: http://en.wikipedia.org/wiki/Interval_tree 156 | [Kahn intervaltree]: http://zurb.com/forrst/posts/Interval_Tree_implementation_in_python-e0K 157 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md CHANGELOG.md LICENSE.txt 2 | recursive-include test * 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=bash 2 | 3 | SCRIPTS_DIR:=$(PWD)/scripts 4 | 5 | # any files ending in .py?, and any folders named __pycache__ 6 | TEMPS=$(shell \ 7 | find intervaltree test \ 8 | \( -type f -name '*.py?' ! -path '*/__pycache__/*' \) \ 9 | -o \( -type d -name '__pycache__' \) \ 10 | ) 11 | 12 | PYTHONS:=2.7.18 3.6.15 3.7.16 3.8.16 3.9.16 3.10.10 3.11.2 13 | PYTHON_MAJORS:=$(shell \ 14 | echo "$(PYTHONS)" | \ 15 | tr ' ' '\n' | cut -d. -f1 | \ 16 | uniq \ 17 | ) 18 | PYTHON_MINORS:=$(shell \ 19 | echo "$(PYTHONS)" | \ 20 | tr ' ' '\n' | cut -d. -f1,2 | \ 21 | uniq \ 22 | ) 23 | 24 | # PyPI server name, as specified in ~/.pypirc 25 | # See http://peterdowns.com/posts/first-time-with-pypi.html 26 | PYPI=pypitest 27 | TWINE=twine 28 | 29 | # default target 30 | all: test 31 | 32 | test: pytest 33 | 34 | quicktest: 35 | PYPI=$(PYPI) python setup.py test 36 | 37 | coverage: 38 | coverage run --source=intervaltree setup.py develop test 39 | coverage report 40 | coverage html 41 | 42 | pytest: deps-dev 43 | PYTHONS="$(PYTHONS)" PYTHON_MINORS="$(PYTHON_MINORS)" "$(SCRIPTS_DIR)/testall.sh" 44 | 45 | clean: clean-build clean-eggs clean-temps 46 | 47 | distclean: clean 48 | 49 | clean-build: 50 | rm -rf dist build 51 | 52 | clean-eggs: 53 | rm -rf *.egg* .eggs/ 54 | 55 | clean-temps: 56 | rm -rf $(TEMPS) 57 | 58 | install-testpypi: 59 | pip install \ 60 | --no-cache-dir \ 61 | --index-url https://test.pypi.org/simple/ \ 62 | --extra-index-url https://pypi.org/simple \ 63 | intervaltree 64 | 65 | install-pypi: 66 | pip install intervaltree 67 | 68 | install-develop: 69 | PYPI=$(PYPI) python setup.py develop 70 | 71 | uninstall: 72 | pip uninstall intervaltree 73 | 74 | # Register at PyPI 75 | register: 76 | PYPI=$(PYPI) python setup.py register -r $(PYPI) 77 | 78 | # Setup for live upload 79 | release: 80 | $(eval PYPI=pypi) 81 | 82 | # Build source distribution 83 | sdist-build: distclean deps-dev 84 | PYPI=$(PYPI) python setup.py sdist 85 | 86 | # Build dist distribution 87 | bdist-build: distclean deps-dev 88 | PYPI=$(PYPI) python setup.py bdist_wheel 89 | 90 | dist-upload: sdist-build bdist-build 91 | if [[ "$(PYPI)" == pypitest ]]; then \ 92 | $(TWINE) upload --repository-url https://test.pypi.org/legacy/ dist/*; \ 93 | else \ 94 | $(TWINE) upload dist/*; \ 95 | fi 96 | 97 | deps-dev: pyenv-install-versions 98 | 99 | # Uploads to test server, unless the release target was run too 100 | upload: test clean dist-upload 101 | 102 | pyenv-is-installed: 103 | pyenv --version &>/dev/null || (echo "ERROR: pyenv not installed" && false) 104 | 105 | pyenv-install-versions: pyenv-is-installed 106 | for pyver in $(PYTHONS); do (echo N | pyenv install $$pyver) || true; done 107 | for pyver in $(PYTHONS); do \ 108 | export PYENV_VERSION=$$pyver; \ 109 | pip install -U pip; \ 110 | pip install -U pytest; \ 111 | done | grep -v 'Requirement already satisfied, skipping upgrade' 112 | # twine and wheel needed only under latest PYTHONS version for uploading to PYPI 113 | export PYENV_VERSION=$(shell \ 114 | echo $(PYTHONS) | \ 115 | tr ' ' '\n' | \ 116 | tail -n1 \ 117 | ) 118 | pip install -U twine 119 | pip install -U wheel 120 | pyenv rehash 121 | 122 | # for debugging the Makefile 123 | env: 124 | @echo 125 | @echo TEMPS="\"$(TEMPS)\"" 126 | @echo PYTHONS="\"$(PYTHONS)\"" 127 | @echo PYTHON_MAJORS="\"$(PYTHON_MAJORS)\"" 128 | @echo PYTHON_MINORS="\"$(PYTHON_MINORS)\"" 129 | @echo PYPI="\"$(PYPI)\"" 130 | 131 | 132 | .PHONY: all \ 133 | test \ 134 | quicktest \ 135 | pytest \ 136 | clean \ 137 | distclean \ 138 | clean-build \ 139 | clean-eggs \ 140 | clean-temps \ 141 | install-testpypi \ 142 | install-pypi \ 143 | install-develop \ 144 | pyenv-install-versions \ 145 | pyenv-is-installed \ 146 | uninstall \ 147 | register \ 148 | release \ 149 | sdist-upload \ 150 | deps-ci \ 151 | deps-dev \ 152 | pm-update \ 153 | upload \ 154 | env 155 | 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status badge][]][build status] 2 | 3 | intervaltree 4 | ============ 5 | 6 | A mutable, self-balancing interval tree for Python 2 and 3. Queries may be by point, by range overlap, or by range envelopment. 7 | 8 | This library was designed to allow tagging text and time intervals, where the intervals include the lower bound but not the upper bound. 9 | 10 | **Version 3 changes!** 11 | 12 | * The `search(begin, end, strict)` method no longer exists. Instead, use one of these: 13 | * `at(point)` 14 | * `overlap(begin, end)` 15 | * `envelop(begin, end)` 16 | * The `extend(items)` method no longer exists. Instead, use `update(items)`. 17 | * Methods like `merge_overlaps()` which took a `strict` argument consistently default to `strict=True`. Before, some methods defaulted to `True` and others to `False`. 18 | 19 | Installing 20 | ---------- 21 | 22 | ```sh 23 | pip install intervaltree 24 | ``` 25 | 26 | Features 27 | -------- 28 | 29 | * Supports Python 2.7 and Python 3.6+ (Tested under 2.7, and 3.6 thru 3.11) 30 | * Initializing 31 | * blank `tree = IntervalTree()` 32 | * from an iterable of `Interval` objects (`tree = IntervalTree(intervals)`) 33 | * from an iterable of tuples (`tree = IntervalTree.from_tuples(interval_tuples)`) 34 | 35 | * Insertions 36 | * `tree[begin:end] = data` 37 | * `tree.add(interval)` 38 | * `tree.addi(begin, end, data)` 39 | 40 | * Deletions 41 | * `tree.remove(interval)` (raises `ValueError` if not present) 42 | * `tree.discard(interval)` (quiet if not present) 43 | * `tree.removei(begin, end, data)` (short for `tree.remove(Interval(begin, end, data))`) 44 | * `tree.discardi(begin, end, data)` (short for `tree.discard(Interval(begin, end, data))`) 45 | * `tree.remove_overlap(point)` 46 | * `tree.remove_overlap(begin, end)` (removes all overlapping the range) 47 | * `tree.remove_envelop(begin, end)` (removes all enveloped in the range) 48 | 49 | * Point queries 50 | * `tree[point]` 51 | * `tree.at(point)` (same as previous) 52 | 53 | * Overlap queries 54 | * `tree[begin:end]` 55 | * `tree.overlap(begin, end)` (same as previous) 56 | 57 | * Envelop queries 58 | * `tree.envelop(begin, end)` 59 | 60 | * Membership queries 61 | * `interval_obj in tree` (this is fastest, O(1)) 62 | * `tree.containsi(begin, end, data)` 63 | * `tree.overlaps(point)` 64 | * `tree.overlaps(begin, end)` 65 | 66 | * Iterable 67 | * `for interval_obj in tree:` 68 | * `tree.items()` 69 | 70 | * Sizing 71 | * `len(tree)` 72 | * `tree.is_empty()` 73 | * `not tree` 74 | * `tree.begin()` (the `begin` coordinate of the leftmost interval) 75 | * `tree.end()` (the `end` coordinate of the rightmost interval) 76 | 77 | * Set-like operations 78 | * union 79 | * `result_tree = tree.union(iterable)` 80 | * `result_tree = tree1 | tree2` 81 | * `tree.update(iterable)` 82 | * `tree |= other_tree` 83 | 84 | * difference 85 | * `result_tree = tree.difference(iterable)` 86 | * `result_tree = tree1 - tree2` 87 | * `tree.difference_update(iterable)` 88 | * `tree -= other_tree` 89 | 90 | * intersection 91 | * `result_tree = tree.intersection(iterable)` 92 | * `result_tree = tree1 & tree2` 93 | * `tree.intersection_update(iterable)` 94 | * `tree &= other_tree` 95 | 96 | * symmetric difference 97 | * `result_tree = tree.symmetric_difference(iterable)` 98 | * `result_tree = tree1 ^ tree2` 99 | * `tree.symmetric_difference_update(iterable)` 100 | * `tree ^= other_tree` 101 | 102 | * comparison 103 | * `tree1.issubset(tree2)` or `tree1 <= tree2` 104 | * `tree1 <= tree2` 105 | * `tree1.issuperset(tree2)` or `tree1 > tree2` 106 | * `tree1 >= tree2` 107 | * `tree1 == tree2` 108 | 109 | * Restructuring 110 | * `chop(begin, end)` (slice intervals and remove everything between `begin` and `end`, optionally modifying the data fields of the chopped-up intervals) 111 | * `slice(point)` (slice intervals at `point`) 112 | * `split_overlaps()` (slice at all interval boundaries, optionally modifying the data field) 113 | * `merge_overlaps()` (joins overlapping intervals into a single interval, optionally merging the data fields) 114 | * `merge_equals()` (joins intervals with matching ranges into a single interval, optionally merging the data fields) 115 | * `merge_neighbors()` (joins adjacent intervals into a single interval if the distance between their range terminals is less than or equal to a given distance. Optionally merges overlapping intervals. Can also merge the data fields.) 116 | 117 | * Copying and typecasting 118 | * `IntervalTree(tree)` (`Interval` objects are same as those in tree) 119 | * `tree.copy()` (`Interval` objects are shallow copies of those in tree) 120 | * `set(tree)` (can later be fed into `IntervalTree()`) 121 | * `list(tree)` (ditto) 122 | 123 | * Pickle-friendly 124 | * Automatic AVL balancing 125 | 126 | Examples 127 | -------- 128 | 129 | * Getting started 130 | 131 | ``` python 132 | >>> from intervaltree import Interval, IntervalTree 133 | >>> t = IntervalTree() 134 | >>> t 135 | IntervalTree() 136 | 137 | ``` 138 | 139 | * Adding intervals - any object works! 140 | 141 | ``` python 142 | >>> t[1:2] = "1-2" 143 | >>> t[4:7] = (4, 7) 144 | >>> t[5:9] = {5: 9} 145 | 146 | ``` 147 | 148 | * Query by point 149 | 150 | The result of a query is a `set` object, so if ordering is important, 151 | you must sort it first. 152 | 153 | ``` python 154 | >>> sorted(t[6]) 155 | [Interval(4, 7, (4, 7)), Interval(5, 9, {5: 9})] 156 | >>> sorted(t[6])[0] 157 | Interval(4, 7, (4, 7)) 158 | 159 | ``` 160 | 161 | * Query by range 162 | 163 | Note that ranges are inclusive of the lower limit, but non-inclusive of the upper limit. So: 164 | 165 | ``` python 166 | >>> sorted(t[2:4]) 167 | [] 168 | 169 | ``` 170 | 171 | Since our search was over `2 ≤ x < 4`, neither `Interval(1, 2)` nor `Interval(4, 7)` 172 | was included. The first interval, `1 ≤ x < 2` does not include `x = 2`. The second 173 | interval, `4 ≤ x < 7`, does include `x = 4`, but our search interval excludes it. So, 174 | there were no overlapping intervals. However: 175 | 176 | ``` python 177 | >>> sorted(t[1:5]) 178 | [Interval(1, 2, '1-2'), Interval(4, 7, (4, 7))] 179 | 180 | ``` 181 | 182 | To only return intervals that are completely enveloped by the search range: 183 | 184 | ``` python 185 | >>> sorted(t.envelop(1, 5)) 186 | [Interval(1, 2, '1-2')] 187 | 188 | ``` 189 | 190 | * Accessing an `Interval` object 191 | 192 | ``` python 193 | >>> iv = Interval(4, 7, (4, 7)) 194 | >>> iv.begin 195 | 4 196 | >>> iv.end 197 | 7 198 | >>> iv.data 199 | (4, 7) 200 | 201 | >>> begin, end, data = iv 202 | >>> begin 203 | 4 204 | >>> end 205 | 7 206 | >>> data 207 | (4, 7) 208 | 209 | ``` 210 | 211 | * Constructing from lists of intervals 212 | 213 | We could have made a similar tree this way: 214 | 215 | ``` python 216 | >>> ivs = [(1, 2), (4, 7), (5, 9)] 217 | >>> t = IntervalTree( 218 | ... Interval(begin, end, "%d-%d" % (begin, end)) for begin, end in ivs 219 | ... ) 220 | 221 | ``` 222 | 223 | Or, if we don't need the data fields: 224 | 225 | ``` python 226 | >>> t2 = IntervalTree(Interval(*iv) for iv in ivs) 227 | 228 | ``` 229 | 230 | Or even: 231 | 232 | ``` python 233 | >>> t2 = IntervalTree.from_tuples(ivs) 234 | 235 | ``` 236 | 237 | * Removing intervals 238 | 239 | ``` python 240 | >>> t.remove(Interval(1, 2, "1-2")) 241 | >>> sorted(t) 242 | [Interval(4, 7, '4-7'), Interval(5, 9, '5-9')] 243 | 244 | >>> t.remove(Interval(500, 1000, "Doesn't exist")) # raises ValueError 245 | Traceback (most recent call last): 246 | ValueError 247 | 248 | >>> t.discard(Interval(500, 1000, "Doesn't exist")) # quietly does nothing 249 | 250 | >>> del t[5] # same as t.remove_overlap(5) 251 | >>> t 252 | IntervalTree() 253 | 254 | ``` 255 | 256 | We could also empty a tree entirely: 257 | 258 | ``` python 259 | >>> t2.clear() 260 | >>> t2 261 | IntervalTree() 262 | 263 | ``` 264 | 265 | Or remove intervals that overlap a range: 266 | 267 | ``` python 268 | >>> t = IntervalTree([ 269 | ... Interval(0, 10), 270 | ... Interval(10, 20), 271 | ... Interval(20, 30), 272 | ... Interval(30, 40)]) 273 | >>> t.remove_overlap(25, 35) 274 | >>> sorted(t) 275 | [Interval(0, 10), Interval(10, 20)] 276 | 277 | ``` 278 | 279 | We can also remove only those intervals completely enveloped in a range: 280 | 281 | ``` python 282 | >>> t.remove_envelop(5, 20) 283 | >>> sorted(t) 284 | [Interval(0, 10)] 285 | 286 | ``` 287 | 288 | * Chopping 289 | 290 | We could also chop out parts of the tree: 291 | 292 | ``` python 293 | >>> t = IntervalTree([Interval(0, 10)]) 294 | >>> t.chop(3, 7) 295 | >>> sorted(t) 296 | [Interval(0, 3), Interval(7, 10)] 297 | 298 | ``` 299 | 300 | To modify the new intervals' data fields based on which side of the interval is being chopped: 301 | 302 | ``` python 303 | >>> def datafunc(iv, islower): 304 | ... oldlimit = iv[islower] 305 | ... return "oldlimit: {0}, islower: {1}".format(oldlimit, islower) 306 | >>> t = IntervalTree([Interval(0, 10)]) 307 | >>> t.chop(3, 7, datafunc) 308 | >>> sorted(t)[0] 309 | Interval(0, 3, 'oldlimit: 10, islower: True') 310 | >>> sorted(t)[1] 311 | Interval(7, 10, 'oldlimit: 0, islower: False') 312 | 313 | ``` 314 | 315 | * Slicing 316 | 317 | You can also slice intervals in the tree without removing them: 318 | 319 | ``` python 320 | >>> t = IntervalTree([Interval(0, 10), Interval(5, 15)]) 321 | >>> t.slice(3) 322 | >>> sorted(t) 323 | [Interval(0, 3), Interval(3, 10), Interval(5, 15)] 324 | 325 | ``` 326 | 327 | You can also set the data fields, for example, re-using `datafunc()` from above: 328 | 329 | ``` python 330 | >>> t = IntervalTree([Interval(5, 15)]) 331 | >>> t.slice(10, datafunc) 332 | >>> sorted(t)[0] 333 | Interval(5, 10, 'oldlimit: 15, islower: True') 334 | >>> sorted(t)[1] 335 | Interval(10, 15, 'oldlimit: 5, islower: False') 336 | 337 | ``` 338 | 339 | Future improvements 340 | ------------------- 341 | 342 | See the [issue tracker][] on GitHub. 343 | 344 | Based on 345 | -------- 346 | 347 | * Eternally Confuzzled's [AVL tree][Confuzzled AVL tree] 348 | * Wikipedia's [Interval Tree][Wiki intervaltree] 349 | * Heavily modified from Tyler Kahn's [Interval Tree implementation in Python][Kahn intervaltree] ([GitHub project][Kahn intervaltree GH]) 350 | * Incorporates contributions from: 351 | * [konstantint/Konstantin Tretyakov][Konstantin intervaltree] of the University of Tartu (Estonia) 352 | * [siniG/Avi Gabay][siniG intervaltree] 353 | * [lmcarril/Luis M. Carril][lmcarril intervaltree] of the Karlsruhe Institute for Technology (Germany) 354 | * [depristo/MarkDePristo][depristo intervaltree] 355 | 356 | Copyright 357 | --------- 358 | 359 | * [Chaim Leib Halbert][GH], 2013-2023 360 | * Modifications, [Konstantin Tretyakov][Konstantin intervaltree], 2014 361 | 362 | Licensed under the [Apache License, version 2.0][Apache]. 363 | 364 | The source code for this project is at https://github.com/chaimleib/intervaltree 365 | 366 | 367 | [build status badge]: https://github.com/chaimleib/intervaltree/workflows/ci/badge.svg 368 | [build status]: https://github.com/chaimleib/intervaltree/actions 369 | [GH]: https://github.com/chaimleib/intervaltree 370 | [issue tracker]: https://github.com/chaimleib/intervaltree/issues 371 | [Konstantin intervaltree]: https://github.com/konstantint/PyIntervalTree 372 | [siniG intervaltree]: https://github.com/siniG/intervaltree 373 | [lmcarril intervaltree]: https://github.com/lmcarril/intervaltree 374 | [depristo intervaltree]: https://github.com/depristo/intervaltree 375 | [Confuzzled AVL tree]: http://www.eternallyconfuzzled.com/tuts/datastructures/jsw_tut_avl.aspx 376 | [Wiki intervaltree]: http://en.wikipedia.org/wiki/Interval_tree 377 | [Kahn intervaltree]: http://zurb.com/forrst/posts/Interval_Tree_implementation_in_python-e0K 378 | [Kahn intervaltree GH]: https://github.com/tylerkahn/intervaltree-python 379 | [Apache]: http://www.apache.org/licenses/LICENSE-2.0 380 | -------------------------------------------------------------------------------- /intervaltree/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 6 | Queries may be by point, by range overlap, or by range envelopment. 7 | 8 | Root package. 9 | 10 | Copyright 2013-2018 Chaim Leib Halbert 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | """ 24 | from .interval import Interval 25 | from .intervaltree import IntervalTree 26 | -------------------------------------------------------------------------------- /intervaltree/interval.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 6 | Queries may be by point, by range overlap, or by range envelopment. 7 | 8 | Interval class 9 | 10 | Copyright 2013-2018 Chaim Leib Halbert 11 | Modifications copyright 2014 Konstantin Tretyakov 12 | 13 | Licensed under the Apache License, Version 2.0 (the "License"); 14 | you may not use this file except in compliance with the License. 15 | You may obtain a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unless required by applicable law or agreed to in writing, software 20 | distributed under the License is distributed on an "AS IS" BASIS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions and 23 | limitations under the License. 24 | """ 25 | from numbers import Number 26 | from collections import namedtuple 27 | 28 | 29 | # noinspection PyBroadException 30 | class Interval(namedtuple('IntervalBase', ['begin', 'end', 'data'])): 31 | __slots__ = () # Saves memory, avoiding the need to create __dict__ for each interval 32 | 33 | def __new__(cls, begin, end, data=None): 34 | return super(Interval, cls).__new__(cls, begin, end, data) 35 | 36 | def overlaps(self, begin, end=None): 37 | """ 38 | Whether the interval overlaps the given point, range or Interval. 39 | :param begin: beginning point of the range, or the point, or an Interval 40 | :param end: end point of the range. Optional if not testing ranges. 41 | :return: True or False 42 | :rtype: bool 43 | """ 44 | if end is not None: 45 | # An overlap means that some C exists that is inside both ranges: 46 | # begin <= C < end 47 | # and 48 | # self.begin <= C < self.end 49 | # See https://stackoverflow.com/questions/3269434/whats-the-most-efficient-way-to-test-two-integer-ranges-for-overlap/3269471#3269471 50 | return begin < self.end and end > self.begin 51 | try: 52 | return self.overlaps(begin.begin, begin.end) 53 | except: 54 | return self.contains_point(begin) 55 | 56 | def overlap_size(self, begin, end=None): 57 | """ 58 | Return the overlap size between two intervals or a point 59 | :param begin: beginning point of the range, or the point, or an Interval 60 | :param end: end point of the range. Optional if not testing ranges. 61 | :return: Return the overlap size, None if not overlap is found 62 | :rtype: depends on the given input (e.g., int will be returned for int interval and timedelta for 63 | datetime intervals) 64 | """ 65 | overlaps = self.overlaps(begin, end) 66 | if not overlaps: 67 | return 0 68 | 69 | if end is not None: 70 | # case end is given 71 | i0 = max(self.begin, begin) 72 | i1 = min(self.end, end) 73 | return i1 - i0 74 | # assume the type is interval, in other cases, an exception will be thrown 75 | i0 = max(self.begin, begin.begin) 76 | i1 = min(self.end, begin.end) 77 | return i1 - i0 78 | 79 | def contains_point(self, p): 80 | """ 81 | Whether the Interval contains p. 82 | :param p: a point 83 | :return: True or False 84 | :rtype: bool 85 | """ 86 | return self.begin <= p < self.end 87 | 88 | def range_matches(self, other): 89 | """ 90 | Whether the begins equal and the ends equal. Compare __eq__(). 91 | :param other: Interval 92 | :return: True or False 93 | :rtype: bool 94 | """ 95 | return ( 96 | self.begin == other.begin and 97 | self.end == other.end 98 | ) 99 | 100 | def contains_interval(self, other): 101 | """ 102 | Whether other is contained in this Interval. 103 | :param other: Interval 104 | :return: True or False 105 | :rtype: bool 106 | """ 107 | return ( 108 | self.begin <= other.begin and 109 | self.end >= other.end 110 | ) 111 | 112 | def distance_to(self, other): 113 | """ 114 | Returns the size of the gap between intervals, or 0 115 | if they touch or overlap. 116 | :param other: Interval or point 117 | :return: distance 118 | :rtype: Number 119 | """ 120 | if self.overlaps(other): 121 | return 0 122 | try: 123 | if self.begin < other.begin: 124 | return other.begin - self.end 125 | else: 126 | return self.begin - other.end 127 | except: 128 | if self.end <= other: 129 | return other - self.end 130 | else: 131 | return self.begin - other 132 | 133 | def is_null(self): 134 | """ 135 | Whether this equals the null interval. 136 | :return: True if end <= begin else False 137 | :rtype: bool 138 | """ 139 | return self.begin >= self.end 140 | 141 | def length(self): 142 | """ 143 | The distance covered by this Interval. 144 | :return: length 145 | :type: Number 146 | """ 147 | if self.is_null(): 148 | return 0 149 | return self.end - self.begin 150 | 151 | def __hash__(self): 152 | """ 153 | Depends on begin and end only. 154 | :return: hash 155 | :rtype: Number 156 | """ 157 | return hash((self.begin, self.end)) 158 | 159 | def __eq__(self, other): 160 | """ 161 | Whether the begins equal, the ends equal, and the data fields 162 | equal. Compare range_matches(). 163 | :param other: Interval 164 | :return: True or False 165 | :rtype: bool 166 | """ 167 | return ( 168 | self.begin == other.begin and 169 | self.end == other.end and 170 | self.data == other.data 171 | ) 172 | 173 | def __cmp__(self, other): 174 | """ 175 | Tells whether other sorts before, after or equal to this 176 | Interval. 177 | 178 | Sorting is by begins, then by ends, then by data fields. 179 | 180 | If data fields are not both sortable types, data fields are 181 | compared alphabetically by type name. 182 | :param other: Interval 183 | :return: -1, 0, 1 184 | :rtype: int 185 | """ 186 | s = self[0:2] 187 | try: 188 | o = other[0:2] 189 | except: 190 | o = (other,) 191 | if s != o: 192 | return -1 if s < o else 1 193 | try: 194 | if self.data == other.data: 195 | return 0 196 | return -1 if self.data < other.data else 1 197 | except TypeError: 198 | s = type(self.data).__name__ 199 | o = type(other.data).__name__ 200 | if s == o: 201 | return 0 202 | return -1 if s < o else 1 203 | 204 | def __lt__(self, other): 205 | """ 206 | Less than operator. Parrots __cmp__() 207 | :param other: Interval or point 208 | :return: True or False 209 | :rtype: bool 210 | """ 211 | return self.__cmp__(other) < 0 212 | 213 | def __gt__(self, other): 214 | """ 215 | Greater than operator. Parrots __cmp__() 216 | :param other: Interval or point 217 | :return: True or False 218 | :rtype: bool 219 | """ 220 | return self.__cmp__(other) > 0 221 | 222 | def _raise_if_null(self, other): 223 | """ 224 | :raises ValueError: if either self or other is a null Interval 225 | """ 226 | if self.is_null(): 227 | raise ValueError("Cannot compare null Intervals!") 228 | if hasattr(other, 'is_null') and other.is_null(): 229 | raise ValueError("Cannot compare null Intervals!") 230 | 231 | def lt(self, other): 232 | """ 233 | Strictly less than. Returns True if no part of this Interval 234 | extends higher than or into other. 235 | :raises ValueError: if either self or other is a null Interval 236 | :param other: Interval or point 237 | :return: True or False 238 | :rtype: bool 239 | """ 240 | self._raise_if_null(other) 241 | return self.end <= getattr(other, 'begin', other) 242 | 243 | def le(self, other): 244 | """ 245 | Less than or overlaps. Returns True if no part of this Interval 246 | extends higher than other. 247 | :raises ValueError: if either self or other is a null Interval 248 | :param other: Interval or point 249 | :return: True or False 250 | :rtype: bool 251 | """ 252 | self._raise_if_null(other) 253 | return self.end <= getattr(other, 'end', other) 254 | 255 | def gt(self, other): 256 | """ 257 | Strictly greater than. Returns True if no part of this Interval 258 | extends lower than or into other. 259 | :raises ValueError: if either self or other is a null Interval 260 | :param other: Interval or point 261 | :return: True or False 262 | :rtype: bool 263 | """ 264 | self._raise_if_null(other) 265 | if hasattr(other, 'end'): 266 | return self.begin >= other.end 267 | else: 268 | return self.begin > other 269 | 270 | def ge(self, other): 271 | """ 272 | Greater than or overlaps. Returns True if no part of this Interval 273 | extends lower than other. 274 | :raises ValueError: if either self or other is a null Interval 275 | :param other: Interval or point 276 | :return: True or False 277 | :rtype: bool 278 | """ 279 | self._raise_if_null(other) 280 | return self.begin >= getattr(other, 'begin', other) 281 | 282 | def _get_fields(self): 283 | """ 284 | Used by str, unicode, repr and __reduce__. 285 | 286 | Returns only the fields necessary to reconstruct the Interval. 287 | :return: reconstruction info 288 | :rtype: tuple 289 | """ 290 | if self.data is not None: 291 | return self.begin, self.end, self.data 292 | else: 293 | return self.begin, self.end 294 | 295 | def __repr__(self): 296 | """ 297 | Executable string representation of this Interval. 298 | :return: string representation 299 | :rtype: str 300 | """ 301 | if isinstance(self.begin, Number): 302 | s_begin = str(self.begin) 303 | s_end = str(self.end) 304 | else: 305 | s_begin = repr(self.begin) 306 | s_end = repr(self.end) 307 | if self.data is None: 308 | return "Interval({0}, {1})".format(s_begin, s_end) 309 | else: 310 | return "Interval({0}, {1}, {2})".format(s_begin, s_end, repr(self.data)) 311 | 312 | __str__ = __repr__ 313 | 314 | def copy(self): 315 | """ 316 | Shallow copy. 317 | :return: copy of self 318 | :rtype: Interval 319 | """ 320 | return Interval(self.begin, self.end, self.data) 321 | 322 | def __reduce__(self): 323 | """ 324 | For pickle-ing. 325 | :return: pickle data 326 | :rtype: tuple 327 | """ 328 | return Interval, self._get_fields() 329 | -------------------------------------------------------------------------------- /scripts/testall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Tests using `python setup.py test` using different versions of python. 3 | 4 | this_dir="$(dirname "$0")" 5 | export base_dir="$(dirname "$this_dir")" 6 | 7 | set -x 8 | code=0 9 | for ver in $(pyenv versions --bare | sort -V); do 10 | pyenv global "$ver" 11 | python --version 12 | python "$base_dir/setup.py" test || code=1 13 | done 14 | set +x 15 | exit "$code" 16 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = 3 | tag_svn_revision = false 4 | 5 | [tool:pytest] 6 | addopts = --doctest-modules --doctest-glob='README.md' --ignore=setup.py --ignore=*.pyc 7 | norecursedirs=*.egg* *doc* .* _* htmlcov scripts dist bin test/data 8 | 9 | [bdist_wheel] 10 | universal = 1 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 6 | Queries may be by point, by range overlap, or by range envelopment. 7 | 8 | Distribution logic 9 | 10 | Note that "python setup.py test" invokes pytest on the package. With appropriately 11 | configured setup.cfg, this will check both xxx_test modules and docstrings. 12 | 13 | Copyright 2013-2023 Chaim Leib Halbert 14 | 15 | Licensed under the Apache License, Version 2.0 (the "License"); 16 | you may not use this file except in compliance with the License. 17 | You may obtain a copy of the License at 18 | 19 | http://www.apache.org/licenses/LICENSE-2.0 20 | 21 | Unless required by applicable law or agreed to in writing, software 22 | distributed under the License is distributed on an "AS IS" BASIS, 23 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | See the License for the specific language governing permissions and 25 | limitations under the License. 26 | """ 27 | from __future__ import absolute_import 28 | import io 29 | import os 30 | import sys 31 | from sys import exit 32 | from setuptools import setup 33 | from setuptools.command.test import test as TestCommand 34 | import subprocess 35 | 36 | ## CONFIG 37 | target_version = '3.2.0' 38 | 39 | 40 | def version_info(target_version): 41 | is_dev_version = 'PYPI' in os.environ and os.environ['PYPI'] == 'pypitest' 42 | if is_dev_version: 43 | p = subprocess.Popen('git describe --tag'.split(), stdout=subprocess.PIPE) 44 | git_describe = str(p.communicate()[0]).strip() 45 | release, build, commitish = git_describe.split('-') 46 | version = "{0}a{1}".format(target_version, build) 47 | else: # This is a RELEASE version 48 | version = target_version 49 | return { 50 | 'is_dev_version': is_dev_version, 51 | 'version': version, 52 | 'target_version': target_version 53 | } 54 | 55 | 56 | vinfo = version_info(target_version) 57 | if vinfo['is_dev_version']: 58 | print("This is a DEV version") 59 | print("Target: {target_version}\n".format(**vinfo)) 60 | else: 61 | print("!!!>>> This is a RELEASE version << 8 | Interval(8.65, 13.65) 9 | <: Node<5.66, depth=1, balance=0> 10 | Interval(3.57, 9.47) 11 | Interval(5.38, 10.38) 12 | Interval(5.66, 9.66) 13 | >: Node<16.49, depth=2, balance=-1> 14 | Interval(16.49, 20.83) 15 | <: Node<11.42, depth=1, balance=0> 16 | Interval(11.42, 16.42) 17 | """ 18 | from intervaltree import IntervalTree, Interval 19 | from intervaltree.node import Node 20 | 21 | data = [ 22 | (8.65, 13.65), #0 23 | (3.57, 9.47), #1 24 | (5.38, 10.38), #2 25 | (5.66, 9.66), #3 26 | (16.49, 20.83), #4 27 | (11.42, 16.42), #5 28 | ] 29 | 30 | def tree(): 31 | t = IntervalTree.from_tuples(data) 32 | # Node<10.58, depth=3, balance=1> 33 | # Interval(8.65, 13.65) 34 | root = Node() 35 | root.x_center = 10.58 36 | root.s_center = set([Interval(*data[0])]) 37 | root.depth = 3 38 | root.balance = 1 39 | 40 | # <: Node<5.66, depth=1, balance=0> 41 | # Interval(3.57, 9.47) 42 | # Interval(5.38, 10.38) 43 | # Interval(5.66, 9.66) 44 | n = root.left_node = Node() 45 | n.x_center = 5.66 46 | n.s_center = set(Interval(*tup) for tup in data[1:4]) 47 | n.depth = 1 48 | n.balance = 0 49 | 50 | # >: Node<16.49, depth=2, balance=-1> 51 | # Interval(16.49, 20.83) 52 | n = root.right_node = Node() 53 | n.x_center = 16.49 54 | n.s_center = set([Interval(*data[4])]) 55 | n.depth = 2 56 | n.balance = -1 57 | 58 | # <: Node<11.42, depth=1, balance=0> 59 | # Interval(11.42, 16.42) 60 | n.left_node = Node() 61 | n = n.left_node 62 | n.x_center = 11.42 63 | n.s_center = set([Interval(*data[5])]) 64 | n.depth = 1 65 | n.balance = 0 66 | 67 | structure = root.print_structure(tostring=True) 68 | # root.print_structure() 69 | assert structure == """\ 70 | Node<10.58, depth=3, balance=1> 71 | Interval(8.65, 13.65) 72 | <: Node<5.66, depth=1, balance=0> 73 | Interval(3.57, 9.47) 74 | Interval(5.38, 10.38) 75 | Interval(5.66, 9.66) 76 | >: Node<16.49, depth=2, balance=-1> 77 | Interval(16.49, 20.83) 78 | <: Node<11.42, depth=1, balance=0> 79 | Interval(11.42, 16.42) 80 | """ 81 | t.top_node = root 82 | t.verify() 83 | return t 84 | 85 | if __name__ == "__main__": 86 | tree().print_structure() 87 | -------------------------------------------------------------------------------- /test/data/issue41_orig.py: -------------------------------------------------------------------------------- 1 | """ 2 | Manually create the original tree that revealed the bug. I have to 3 | do this, since with later code changes, the original sequence of 4 | insertions and removals might not recreate that tree exactly, with 5 | the same internal structure. 6 | 7 | Node<961, depth=2, balance=0> 8 | Interval(961, 986, 1) 9 | <: Node<871, depth=1, balance=0> 10 | Interval(860, 917, 1) 11 | Interval(860, 917, 2) 12 | Interval(860, 917, 3) 13 | Interval(860, 917, 4) 14 | Interval(871, 917, 1) 15 | Interval(871, 917, 2) 16 | Interval(871, 917, 3) 17 | >: Node<1047, depth=1, balance=0> 18 | Interval(1047, 1064, 1) 19 | Interval(1047, 1064, 2) 20 | """ 21 | from intervaltree import IntervalTree, Interval 22 | from intervaltree.node import Node 23 | 24 | data = [ 25 | (860, 917, 1), #0 26 | (860, 917, 2), #1 27 | (860, 917, 3), #2 28 | (860, 917, 4), #3 29 | (871, 917, 1), #4 30 | (871, 917, 2), #5 31 | (871, 917, 3), #6 32 | (961, 986, 1), #7 33 | (1047, 1064, 1), #8 34 | (1047, 1064, 2), #9 35 | ] 36 | 37 | def tree(): 38 | t = IntervalTree.from_tuples(data) 39 | # Node<961, depth=2, balance=0> 40 | # Interval(961, 986, 1) 41 | root = Node() 42 | root.x_center = 961 43 | root.s_center = set([Interval(*data[7])]) 44 | root.depth = 2 45 | root.balance = 0 46 | 47 | # <: Node<871, depth=1, balance=0> 48 | # Interval(860, 917, 1) 49 | # Interval(860, 917, 2) 50 | # Interval(860, 917, 3) 51 | # Interval(860, 917, 4) 52 | # Interval(871, 917, 1) 53 | # Interval(871, 917, 2) 54 | # Interval(871, 917, 3) 55 | n = root.left_node = Node() 56 | n.x_center = 871 57 | n.s_center = set(Interval(*tup) for tup in data[:7]) 58 | n.depth = 1 59 | n.balance = 0 60 | 61 | # >: Node<1047, depth=1, balance=0> 62 | # Interval(1047, 1064, 1) 63 | # Interval(1047, 1064, 2) 64 | n = root.right_node = Node() 65 | n.x_center = 1047 66 | n.s_center = set(Interval(*tup) for tup in data[8:]) 67 | n.depth = 1 68 | n.balance = 0 69 | 70 | structure = root.print_structure(tostring=True) 71 | # root.print_structure() 72 | assert structure == """\ 73 | Node<961, depth=2, balance=0> 74 | Interval(961, 986, 1) 75 | <: Node<871, depth=1, balance=0> 76 | Interval(860, 917, 1) 77 | Interval(860, 917, 2) 78 | Interval(860, 917, 3) 79 | Interval(860, 917, 4) 80 | Interval(871, 917, 1) 81 | Interval(871, 917, 2) 82 | Interval(871, 917, 3) 83 | >: Node<1047, depth=1, balance=0> 84 | Interval(1047, 1064, 1) 85 | Interval(1047, 1064, 2) 86 | """ 87 | t.top_node = root 88 | t.verify() 89 | return t 90 | 91 | if __name__ == "__main__": 92 | tree().print_structure() 93 | -------------------------------------------------------------------------------- /test/data/ivs0.py: -------------------------------------------------------------------------------- 1 | """ 2 | Empty IntervalTree data. 3 | """ 4 | data = [] 5 | -------------------------------------------------------------------------------- /test/data/ivs1.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tiny IntervalTree data. For tiny testing. 3 | """ 4 | data = \ 5 | [(1, 2, '[1,2)'), 6 | (4, 7, '[4,7)'), 7 | (5, 9, '[5,9)'), 8 | (6, 10, '[6,10)'), 9 | (8, 10, '[8,10)'), 10 | (8, 15, '[8,15)'), 11 | (10, 12, '[10,12)'), 12 | (12, 14, '[12,14)'), 13 | (14, 15, '[14,15)')] 14 | -------------------------------------------------------------------------------- /test/data/ivs1_float_copy_structure.py: -------------------------------------------------------------------------------- 1 | """ 2 | Same as ivs1, but with floats. 3 | """ 4 | data = \ 5 | [(0.1, 0.2, '[1,2)'), 6 | (0.4, 0.7, '[4,7)'), 7 | (0.5, 0.9, '[5,9)'), 8 | (0.6, 1.0, '[6,10)'), 9 | (0.8, 1.0, '[8,10)'), 10 | (0.8, 1.5, '[8,15)'), 11 | (1.0, 1.2, '[10,12)'), 12 | (1.2, 1.4, '[12,14)'), 13 | (1.4, 1.5, '[14,15)')] 14 | -------------------------------------------------------------------------------- /test/data/ivs2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Random integer ranges, no gaps. 3 | """ 4 | data = \ 5 | [(-50, -42, None), 6 | (-42, -35, None), 7 | (-35, -29, None), 8 | (-29, -25, None), 9 | (-25, -24, None), 10 | (-24, -16, None), 11 | (-16, -13, None), 12 | (-13, -10, None), 13 | (-10, -7, None), 14 | (-7, -1, None), 15 | (-1, 0, None), 16 | (0, 1, None), 17 | (1, 5, None), 18 | (5, 13, None), 19 | (13, 23, None), 20 | (23, 25, None), 21 | (25, 27, None), 22 | (27, 31, None), 23 | (31, 32, None), 24 | (32, 42, None), 25 | (42, 51, None), 26 | (51, 52, None), 27 | (52, 54, None), 28 | (54, 55, None), 29 | (55, 59, None), 30 | (59, 64, None), 31 | (64, 71, None), 32 | (71, 73, None), 33 | (73, 83, None), 34 | (83, 88, None), 35 | (88, 89, None), 36 | (89, 91, None), 37 | (91, 95, None), 38 | (95, 104, None), 39 | (104, 108, None), 40 | (108, 110, None), 41 | (110, 118, None), 42 | (118, 127, None), 43 | (127, 136, None), 44 | (136, 140, None), 45 | (140, 148, None), 46 | (148, 151, None), 47 | (151, 158, None), 48 | (158, 163, None), 49 | (163, 172, None), 50 | (172, 175, None), 51 | (175, 182, None), 52 | (182, 183, None), 53 | (183, 192, None), 54 | (192, 195, None), 55 | (195, 203, None), 56 | (203, 209, None), 57 | (209, 212, None), 58 | (212, 214, None), 59 | (214, 217, None), 60 | (217, 220, None), 61 | (220, 223, None), 62 | (223, 232, None), 63 | (232, 242, None), 64 | (242, 248, None), 65 | (248, 257, None), 66 | (257, 258, None), 67 | (258, 266, None), 68 | (266, 267, None), 69 | (267, 272, None), 70 | (272, 280, None), 71 | (280, 286, None), 72 | (286, 287, None), 73 | (287, 288, None), 74 | (288, 296, None), 75 | (296, 305, None), 76 | (305, 315, None), 77 | (315, 325, None), 78 | (325, 334, None), 79 | (334, 340, None), 80 | (340, 350, None), 81 | (350, 358, None), 82 | (358, 362, None), 83 | (362, 371, None), 84 | (371, 381, None), 85 | (381, 388, None), 86 | (388, 391, None), 87 | (391, 392, None), 88 | (392, 400, None), 89 | (400, 407, None), 90 | (407, 414, None), 91 | (414, 420, None), 92 | (420, 429, None), 93 | (429, 434, None), 94 | (434, 443, None), 95 | (443, 447, None), 96 | (447, 448, None), 97 | (448, 452, None), 98 | (452, 454, None), 99 | (454, 462, None), 100 | (462, 470, None), 101 | (470, 480, None), 102 | (480, 488, None), 103 | (488, 493, None), 104 | (493, 501, None)] 105 | -------------------------------------------------------------------------------- /test/data/ivs3.py: -------------------------------------------------------------------------------- 1 | """ 2 | Random integer ranges, with gaps. 3 | """ 4 | data = \ 5 | [(-50, -42, None), 6 | (-36, -27, None), 7 | (-27, -21, None), 8 | (-21, -17, None), 9 | (-17, -16, None), 10 | (-14, -4, None), 11 | (-4, 0, None), 12 | (4, 12, None), 13 | (13, 23, None), 14 | (23, 32, None), 15 | (41, 46, None), 16 | (46, 55, None), 17 | (55, 58, None), 18 | (68, 69, None), 19 | (69, 75, None), 20 | (75, 80, None), 21 | (80, 82, None), 22 | (82, 92, None), 23 | (99, 108, None), 24 | (108, 111, None), 25 | (111, 113, None), 26 | (117, 122, None), 27 | (130, 133, None), 28 | (138, 144, None), 29 | (144, 145, None), 30 | (154, 157, None), 31 | (157, 162, None), 32 | (171, 173, None), 33 | (173, 180, None), 34 | (180, 188, None), 35 | (191, 197, None), 36 | (197, 200, None), 37 | (209, 215, None), 38 | (221, 225, None), 39 | (226, 231, None), 40 | (234, 241, None), 41 | (241, 244, None), 42 | (244, 253, None), 43 | (253, 258, None), 44 | (258, 262, None), 45 | (262, 271, None), 46 | (271, 273, None), 47 | (273, 279, None), 48 | (284, 293, None), 49 | (299, 307, None), 50 | (309, 318, None), 51 | (321, 328, None), 52 | (332, 338, None), 53 | (338, 340, None), 54 | (350, 353, None), 55 | (353, 363, None), 56 | (368, 375, None), 57 | (375, 384, None), 58 | (384, 390, None), 59 | (399, 407, None), 60 | (407, 413, None), 61 | (413, 422, None), 62 | (424, 427, None), 63 | (432, 434, None), 64 | (441, 450, None), 65 | (457, 463, None), 66 | (463, 473, None), 67 | (480, 490, None), 68 | (490, 500, None), 69 | (500, 506, None), 70 | (511, 514, None), 71 | (514, 524, None), 72 | (524, 533, None), 73 | (533, 543, None), 74 | (547, 554, None), 75 | (554, 564, None), 76 | (564, 573, None), 77 | (577, 578, None), 78 | (588, 593, None), 79 | (603, 607, None), 80 | (609, 613, None), 81 | (620, 630, None), 82 | (630, 632, None), 83 | (633, 635, None), 84 | (635, 636, None), 85 | (640, 648, None), 86 | (648, 658, None), 87 | (658, 661, None), 88 | (661, 671, None), 89 | (672, 677, None), 90 | (685, 695, None), 91 | (695, 705, None), 92 | (705, 710, None), 93 | (715, 716, None), 94 | (719, 724, None), 95 | (724, 726, None), 96 | (731, 741, None), 97 | (746, 749, None), 98 | (754, 762, None), 99 | (762, 769, None), 100 | (774, 775, None), 101 | (785, 795, None), 102 | (803, 804, None), 103 | (814, 819, None), 104 | (819, 828, None)] 105 | -------------------------------------------------------------------------------- /test/interval_methods/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: Interval methods 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | -------------------------------------------------------------------------------- /test/interval_methods/binary_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: Intervals, methods on two intervals 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | 22 | from intervaltree import Interval 23 | from pprint import pprint 24 | import pickle 25 | 26 | iv0 = Interval(0, 10) 27 | iv1 = Interval(-10, -5) 28 | iv2 = Interval(-10, 0) 29 | iv3 = Interval(-10, 5) 30 | iv4 = Interval(-10, 10) 31 | iv5 = Interval(-10, 20) 32 | iv6 = Interval(0, 20) 33 | iv7 = Interval(5, 20) 34 | iv8 = Interval(10, 20) 35 | iv9 = Interval(15, 20) 36 | iv10 = Interval(-5, 0) 37 | 38 | 39 | def test_interval_overlaps_size_interval(): 40 | assert iv0.overlap_size(iv0) == 10 41 | assert not iv0.overlap_size(iv1) 42 | assert not iv0.overlap_size(iv2) 43 | assert iv0.overlap_size(iv3) == 5 44 | assert iv0.overlap_size(iv4) == 10 45 | assert iv0.overlap_size(iv5) == 10 46 | assert iv0.overlap_size(iv6) == 10 47 | assert iv0.overlap_size(iv7) == 5 48 | assert not iv0.overlap_size(iv8) 49 | assert not iv0.overlap_size(iv9) 50 | 51 | 52 | def test_interval_overlap_interval(): 53 | assert iv0.overlaps(iv0) 54 | assert not iv0.overlaps(iv1) 55 | assert not iv0.overlaps(iv2) 56 | assert iv0.overlaps(iv3) 57 | assert iv0.overlaps(iv4) 58 | assert iv0.overlaps(iv5) 59 | assert iv0.overlaps(iv6) 60 | assert iv0.overlaps(iv7) 61 | assert not iv0.overlaps(iv8) 62 | assert not iv0.overlaps(iv9) 63 | 64 | 65 | def test_contains_interval(): 66 | assert iv0.contains_interval(iv0) 67 | assert not iv0.contains_interval(iv1) 68 | assert not iv0.contains_interval(iv2) 69 | assert not iv0.contains_interval(iv3) 70 | assert not iv0.contains_interval(iv4) 71 | assert not iv0.contains_interval(iv5) 72 | assert not iv0.contains_interval(iv6) 73 | assert not iv0.contains_interval(iv7) 74 | assert not iv0.contains_interval(iv8) 75 | assert not iv0.contains_interval(iv9) 76 | assert not iv0.contains_interval(iv10) 77 | 78 | assert not iv2.contains_interval(iv0) 79 | assert iv2.contains_interval(iv1) 80 | assert iv2.contains_interval(iv2) 81 | assert not iv2.contains_interval(iv3) 82 | assert not iv2.contains_interval(iv4) 83 | assert not iv2.contains_interval(iv5) 84 | assert not iv2.contains_interval(iv6) 85 | assert not iv2.contains_interval(iv7) 86 | assert not iv2.contains_interval(iv8) 87 | assert not iv2.contains_interval(iv9) 88 | assert iv2.contains_interval(iv10) 89 | 90 | 91 | def test_distance_to_interval(): 92 | assert iv0.distance_to(iv0) == 0 93 | assert iv0.distance_to(iv1) == 5 94 | assert iv0.distance_to(iv2) == 0 95 | assert iv0.distance_to(iv3) == 0 96 | assert iv0.distance_to(iv4) == 0 97 | assert iv0.distance_to(iv5) == 0 98 | assert iv0.distance_to(iv6) == 0 99 | assert iv0.distance_to(iv7) == 0 100 | assert iv0.distance_to(iv8) == 0 101 | assert iv0.distance_to(iv9) == 5 102 | assert iv0.distance_to(iv10) == 0 103 | 104 | 105 | def test_distance_to_point(): 106 | assert iv0.distance_to(-5) == 5 107 | assert iv0.distance_to(0) == 0 108 | assert iv0.distance_to(5) == 0 109 | assert iv0.distance_to(10) == 0 110 | assert iv0.distance_to(15) == 5 111 | 112 | 113 | if __name__ == "__main__": 114 | import pytest 115 | pytest.main([__file__, '-v']) 116 | -------------------------------------------------------------------------------- /test/interval_methods/sorting_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: Intervals, sorting methods 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | 22 | from intervaltree import Interval 23 | import pytest 24 | 25 | 26 | def test_interval_overlaps_point(): 27 | iv = Interval(0, 10) 28 | 29 | assert not iv.overlaps(-5) 30 | assert iv.overlaps(0) 31 | assert iv.overlaps(5) 32 | assert not iv.overlaps(10) 33 | assert not iv.overlaps(15) 34 | 35 | 36 | def test_interval_overlaps_range(): 37 | iv0 = Interval(0, 10) 38 | iv1 = (-10, -5) 39 | iv2 = (-10, 0) 40 | iv3 = (-10, 5) 41 | iv4 = (-10, 10) 42 | iv5 = (-10, 20) 43 | iv6 = (0, 20) 44 | iv7 = (5, 20) 45 | iv8 = (10, 20) 46 | iv9 = (15, 20) 47 | 48 | assert iv0.overlaps(*iv0[0:1]) 49 | assert not iv0.overlaps(*iv1) 50 | assert not iv0.overlaps(*iv2) 51 | assert iv0.overlaps(*iv3) 52 | assert iv0.overlaps(*iv4) 53 | assert iv0.overlaps(*iv5) 54 | assert iv0.overlaps(*iv6) 55 | assert iv0.overlaps(*iv7) 56 | assert not iv0.overlaps(*iv8) 57 | assert not iv0.overlaps(*iv9) 58 | 59 | assert iv0.overlaps(Interval(*iv0)) 60 | assert not iv0.overlaps(Interval(*iv1)) 61 | assert not iv0.overlaps(Interval(*iv2)) 62 | assert iv0.overlaps(Interval(*iv3)) 63 | assert iv0.overlaps(Interval(*iv4)) 64 | assert iv0.overlaps(Interval(*iv5)) 65 | assert iv0.overlaps(Interval(*iv6)) 66 | assert iv0.overlaps(Interval(*iv7)) 67 | assert not iv0.overlaps(Interval(*iv8)) 68 | assert not iv0.overlaps(Interval(*iv9)) 69 | 70 | 71 | def test_interval_int_comparison_operators(): 72 | """ 73 | Test comparisons with integers using < and > 74 | """ 75 | iv = Interval(0, 10) 76 | 77 | assert (iv > -5) 78 | assert (-5 < iv) 79 | assert not (iv < -5) 80 | assert not (-5 > iv) 81 | 82 | assert (iv > 0) # special for sorting 83 | assert (0 < iv) # special for sorting 84 | assert not (iv < 0) 85 | assert not (0 > iv) 86 | 87 | assert not (iv > 5) 88 | assert not (5 < iv) 89 | assert (iv < 5) # special for sorting 90 | assert (5 > iv) # special for sorting 91 | 92 | assert not (iv > 10) 93 | assert not (10 < iv) 94 | assert (iv < 10) 95 | assert (10 > iv) 96 | 97 | assert not (iv > 15) 98 | assert not (15 < iv) 99 | assert (iv < 15) 100 | assert (15 > iv) 101 | 102 | 103 | def test_interval_int_comparison_methods(): 104 | """ 105 | Test comparisons with integers using gt(), ge(), lt() and le() 106 | """ 107 | iv = Interval(0, 10) 108 | 109 | assert iv.gt(-5) 110 | assert iv.ge(-5) 111 | assert not iv.lt(-5) 112 | assert not iv.le(-5) 113 | 114 | assert not iv.gt(0) 115 | assert iv.ge(0) 116 | assert not iv.lt(0) 117 | assert not iv.le(0) 118 | 119 | assert not iv.gt(5) 120 | assert not iv.ge(5) 121 | assert not iv.lt(5) 122 | assert not iv.le(5) 123 | 124 | assert not iv.gt(10) 125 | assert not iv.ge(10) 126 | assert iv.lt(10) 127 | assert iv.le(10) 128 | 129 | assert not iv.gt(15) 130 | assert not iv.ge(15) 131 | assert iv.lt(15) 132 | assert iv.le(15) 133 | 134 | 135 | def test_interval_interval_comparison_methods(): 136 | """ 137 | Test comparisons with other Intervals using gt(), ge(), lt() and 138 | le() 139 | """ 140 | iv0 = Interval(0, 10) 141 | iv1 = Interval(-10, -5) 142 | iv2 = Interval(-10, 0) 143 | iv3 = Interval(-10, 5) 144 | iv4 = Interval(-10, 10) 145 | iv5 = Interval(-10, 20) 146 | iv6 = Interval(0, 20) 147 | iv7 = Interval(5, 20) 148 | iv8 = Interval(10, 20) 149 | iv9 = Interval(15, 20) 150 | 151 | assert not iv0.gt(iv0) 152 | assert iv0.gt(iv1) 153 | assert iv0.gt(iv2) 154 | assert not iv0.gt(iv3) 155 | assert not iv0.gt(iv4) 156 | assert not iv0.gt(iv5) 157 | assert not iv0.gt(iv6) 158 | assert not iv0.gt(iv7) 159 | assert not iv0.gt(iv8) 160 | assert not iv0.gt(iv9) 161 | 162 | assert iv0.ge(iv0) 163 | assert iv0.ge(iv1) 164 | assert iv0.ge(iv2) 165 | assert iv0.ge(iv3) 166 | assert iv0.ge(iv4) 167 | assert iv0.ge(iv5) 168 | assert iv0.ge(iv6) 169 | assert not iv0.ge(iv7) 170 | assert not iv0.ge(iv8) 171 | assert not iv0.ge(iv9) 172 | 173 | assert not iv0.lt(iv0) 174 | assert not iv0.lt(iv1) 175 | assert not iv0.lt(iv2) 176 | assert not iv0.lt(iv3) 177 | assert not iv0.lt(iv4) 178 | assert not iv0.lt(iv5) 179 | assert not iv0.lt(iv6) 180 | assert not iv0.lt(iv7) 181 | assert iv0.lt(iv8) 182 | assert iv0.lt(iv9) 183 | 184 | assert iv0.le(iv0) 185 | assert not iv0.le(iv1) 186 | assert not iv0.le(iv2) 187 | assert not iv0.le(iv3) 188 | assert iv0.le(iv4) 189 | assert iv0.le(iv5) 190 | assert iv0.le(iv6) 191 | assert iv0.le(iv7) 192 | assert iv0.le(iv8) 193 | assert iv0.le(iv9) 194 | 195 | 196 | def test_interval_null_interval_comparison_methods(): 197 | """ 198 | Test comparisons with other Intervals using gt(), ge(), lt() and 199 | le() 200 | """ 201 | iv0 = Interval(0, 10) 202 | ivn = Interval(0, 0) 203 | 204 | with pytest.raises(ValueError): 205 | iv0.gt(ivn) 206 | 207 | with pytest.raises(ValueError): 208 | ivn.gt(iv0) 209 | 210 | with pytest.raises(ValueError): 211 | iv0.ge(ivn) 212 | 213 | with pytest.raises(ValueError): 214 | ivn.ge(iv0) 215 | 216 | with pytest.raises(ValueError): 217 | iv0.lt(ivn) 218 | 219 | with pytest.raises(ValueError): 220 | ivn.lt(iv0) 221 | 222 | with pytest.raises(ValueError): 223 | iv0.le(ivn) 224 | 225 | with pytest.raises(ValueError): 226 | ivn.le(iv0) 227 | 228 | 229 | def test_interval_interval_cmp(): 230 | """ 231 | Test comparisons with other Intervals using __cmp__() 232 | """ 233 | iv0 = Interval(0, 10) 234 | iv1 = Interval(-10, -5) 235 | iv2 = Interval(-10, 0) 236 | iv3 = Interval(-10, 5) 237 | iv4 = Interval(-10, 10) 238 | iv5 = Interval(-10, 20) 239 | iv6 = Interval(0, 20) 240 | iv7 = Interval(5, 20) 241 | iv8 = Interval(10, 20) 242 | iv9 = Interval(15, 20) 243 | 244 | assert iv0.__cmp__(iv0) == 0 245 | assert iv0.__cmp__(iv1) == 1 246 | assert iv0.__cmp__(iv2) == 1 247 | assert iv0.__cmp__(iv3) == 1 248 | assert iv0.__cmp__(iv4) == 1 249 | assert iv0.__cmp__(iv5) == 1 250 | assert iv0.__cmp__(iv6) == -1 251 | assert iv0.__cmp__(iv7) == -1 252 | assert iv0.__cmp__(iv8) == -1 253 | assert iv0.__cmp__(iv9) == -1 254 | 255 | 256 | def test_interval_int_cmp(): 257 | """ 258 | Test comparisons with ints using __cmp__() 259 | """ 260 | iv = Interval(0, 10) 261 | 262 | assert iv.__cmp__(-5) == 1 263 | assert iv.__cmp__(0) == 1 264 | assert iv.__cmp__(5) == -1 265 | assert iv.__cmp__(10) == -1 266 | assert iv.__cmp__(15) == -1 267 | 268 | 269 | def test_interval_sort_interval(): 270 | base = Interval(0, 10) 271 | ivs = [ 272 | Interval(-10, -5), 273 | Interval(-10, 0), 274 | Interval(-10, 5), 275 | Interval(-10, 10), 276 | Interval(-10, 20), 277 | Interval(0, 20), 278 | Interval(5, 20), 279 | Interval(10, 20), 280 | Interval(15, 20), 281 | ] 282 | 283 | for iv in ivs: 284 | sort = sorted([base, iv]) 285 | assert sort[0].__cmp__(sort[1]) in (-1, 0) 286 | 287 | sort = sorted([iv, base]) 288 | assert sort[0].__cmp__(sort[1]) in (-1, 0) 289 | 290 | 291 | if __name__ == "__main__": 292 | import pytest 293 | pytest.main([__file__, '-v']) 294 | -------------------------------------------------------------------------------- /test/interval_methods/unary_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: Intervals, methods on self only 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | 22 | from intervaltree import Interval 23 | from pprint import pprint 24 | import pickle 25 | 26 | 27 | def test_isnull(): 28 | iv = Interval(0, 0) 29 | assert iv.is_null() 30 | 31 | iv = Interval(1, 0) 32 | assert iv.is_null() 33 | 34 | 35 | def test_copy(): 36 | iv0 = Interval(1, 2, 3) 37 | iv1 = iv0.copy() 38 | assert iv1.begin == iv0.begin 39 | assert iv1.end == iv0.end 40 | assert iv1.data == iv0.data 41 | assert iv1 == iv0 42 | 43 | iv2 = pickle.loads(pickle.dumps(iv0)) 44 | assert iv2.begin == iv0.begin 45 | assert iv2.end == iv0.end 46 | assert iv2.data == iv0.data 47 | assert iv2 == iv0 48 | 49 | 50 | def test_len(): 51 | iv = Interval(0, 0) 52 | assert len(iv) == 3 53 | 54 | iv = Interval(0, 1, 2) 55 | assert len(iv) == 3 56 | 57 | iv = Interval(1.3, 2.2) 58 | assert len(iv) == 3 59 | 60 | 61 | def test_length(): 62 | iv = Interval(0, 0) 63 | assert iv.length() == 0 64 | 65 | iv = Interval(0, 3) 66 | assert iv.length() == 3 67 | 68 | iv = Interval(-1, 1, 'data') 69 | assert iv.length() == 2 70 | 71 | iv = Interval(0.1, 3) 72 | assert iv.length() == 2.9 73 | 74 | 75 | def test_str(): 76 | iv = Interval(0, 1) 77 | s = str(iv) 78 | assert s == 'Interval(0, 1)' 79 | assert repr(iv) == s 80 | 81 | iv = Interval(0, 1, '[0,1)') 82 | s = str(iv) 83 | assert s == "Interval(0, 1, '[0,1)')" 84 | assert repr(iv) == s 85 | 86 | iv = Interval((1,2), (3,4)) 87 | s = str(iv) 88 | assert s == 'Interval((1, 2), (3, 4))' 89 | assert repr(iv) == s 90 | 91 | iv = Interval((1,2), (3,4), (5, 6)) 92 | s = str(iv) 93 | assert s == 'Interval((1, 2), (3, 4), (5, 6))' 94 | assert repr(iv) == s 95 | 96 | 97 | def test_get_fields(): 98 | ivn = Interval(0, 1) 99 | ivo = Interval(0, 1, 'hello') 100 | 101 | assert ivn._get_fields() == (0, 1) 102 | assert ivo._get_fields() == (0, 1, 'hello') 103 | 104 | 105 | if __name__ == "__main__": 106 | import pytest 107 | pytest.main([__file__, '-v']) 108 | -------------------------------------------------------------------------------- /test/intervals.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: utilities to generate intervals 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import absolute_import 22 | from intervaltree import Interval 23 | from pprint import pprint 24 | from random import randint, choice 25 | from test.progress_bar import ProgressBar 26 | import os 27 | try: 28 | xrange 29 | except NameError: 30 | xrange = range 31 | 32 | try: 33 | unicode 34 | except NameError: 35 | unicode = str 36 | 37 | def make_iv(begin, end, label=False): 38 | if label: 39 | return Interval(begin, end, "[{0},{1})".format(begin, end)) 40 | else: 41 | return Interval(begin, end) 42 | 43 | 44 | def nogaps_rand(size=100, labels=False): 45 | """ 46 | Create a random list of Intervals with no gaps or overlaps 47 | between the intervals. 48 | :rtype: list of Intervals 49 | """ 50 | cur = -50 51 | result = [] 52 | for i in xrange(size): 53 | length = randint(1, 10) 54 | result.append(make_iv(cur, cur + length, labels)) 55 | cur += length 56 | return result 57 | 58 | 59 | def gaps_rand(size=100, labels=False): 60 | """ 61 | Create a random list of intervals with random gaps, but no 62 | overlaps between the intervals. 63 | 64 | :rtype: list of Intervals 65 | """ 66 | cur = -50 67 | result = [] 68 | for i in xrange(size): 69 | length = randint(1, 10) 70 | if choice([True, False]): 71 | cur += length 72 | length = randint(1, 10) 73 | result.append(make_iv(cur, cur + length, labels)) 74 | cur += length 75 | return result 76 | 77 | 78 | def overlaps_nogaps_rand(size=100, labels=False): 79 | l1 = nogaps_rand(size, labels) 80 | l2 = nogaps_rand(size, labels) 81 | result = set(l1) | set(l2) 82 | return list(result) 83 | 84 | 85 | def write_ivs_data(name, ivs, docstring='', imports=None): 86 | """ 87 | Write the provided ivs to test/name.py. 88 | :param name: file name, minus the extension 89 | :type name: str 90 | :param ivs: an iterable of Intervals 91 | :type ivs: collections.i 92 | :param docstring: a string to be inserted at the head of the file 93 | :param imports: executable code to be inserted before data=... 94 | """ 95 | def trepr(s): 96 | """ 97 | Like repr, but triple-quoted. NOT perfect! 98 | 99 | Taken from http://compgroups.net/comp.lang.python/re-triple-quoted-repr/1635367 100 | """ 101 | text = '\n'.join([repr(line)[1:-1] for line in s.split('\n')]) 102 | squotes, dquotes = "'''", '"""' 103 | my_quotes, other_quotes = dquotes, squotes 104 | if my_quotes in text: 105 | if other_quotes in text: 106 | escaped_quotes = 3*('\\' + other_quotes[0]) 107 | text = text.replace(other_quotes, escaped_quotes) 108 | else: 109 | my_quotes = other_quotes 110 | return "%s%s%s" % (my_quotes, text, my_quotes) 111 | 112 | data = [tuple(iv) for iv in ivs] 113 | with open('test/data/{0}.py'.format(name), 'w') as f: 114 | if docstring: 115 | f.write(trepr(docstring)) 116 | f.write('\n') 117 | if isinstance(imports, (str, unicode)): 118 | f.write(imports) 119 | f.write('\n\n') 120 | elif isinstance(imports, (list, tuple, set)): 121 | for line in imports: 122 | f.write(line + '\n') 123 | f.write('\n') 124 | 125 | f.write('data = \\\n') 126 | pprint(data, f) 127 | 128 | 129 | if __name__ == '__main__': 130 | # ivs = gaps_rand() 131 | # write_ivs_data('ivs3', ivs, docstring=""" 132 | # Random integer ranges, with gaps. 133 | # """ 134 | # ) 135 | pprint(ivs) 136 | -------------------------------------------------------------------------------- /test/intervaltree_methods/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: IntervalTree methods 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | -------------------------------------------------------------------------------- /test/intervaltree_methods/copy_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: IntervalTree, Copying 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import absolute_import 22 | from intervaltree import Interval, IntervalTree 23 | from test import data 24 | try: 25 | import cPickle as pickle 26 | except ImportError: 27 | import pickle 28 | 29 | 30 | def test_copy(): 31 | itree = IntervalTree([Interval(0, 1, "x"), Interval(1, 2, ["x"])]) 32 | itree.verify() 33 | 34 | itree2 = IntervalTree(itree) # Shares Interval objects 35 | itree2.verify() 36 | 37 | itree3 = itree.copy() # Shallow copy (same as above, as Intervals are singletons) 38 | itree3.verify() 39 | 40 | itree4 = pickle.loads(pickle.dumps(itree)) # Deep copy 41 | itree4.verify() 42 | 43 | list(itree[1])[0].data[0] = "y" 44 | assert sorted(itree) == [Interval(0, 1, 'x'), Interval(1, 2, ['y'])] 45 | assert sorted(itree2) == [Interval(0, 1, 'x'), Interval(1, 2, ['y'])] 46 | assert sorted(itree3) == [Interval(0, 1, 'x'), Interval(1, 2, ['y'])] 47 | assert sorted(itree4) == [Interval(0, 1, 'x'), Interval(1, 2, ['x'])] 48 | 49 | 50 | def test_copy_cast(): 51 | t = IntervalTree.from_tuples(data.ivs1.data) 52 | 53 | tcopy = IntervalTree(t) 54 | tcopy.verify() 55 | assert t == tcopy 56 | 57 | tlist = list(t) 58 | for iv in tlist: 59 | assert iv in t 60 | for iv in t: 61 | assert iv in tlist 62 | 63 | tset = set(t) 64 | assert tset == t.items() 65 | 66 | 67 | if __name__ == "__main__": 68 | import pytest 69 | pytest.main([__file__, '-v']) 70 | -------------------------------------------------------------------------------- /test/intervaltree_methods/debug_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: IntervalTree, Basic query methods (read-only) 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import absolute_import 22 | from intervaltree import Interval, IntervalTree 23 | import pytest 24 | from test import data 25 | from pprint import pprint, pformat 26 | try: 27 | import cPickle as pickle 28 | except ImportError: 29 | import pickle 30 | 31 | 32 | def test_print_empty(): 33 | t = IntervalTree() 34 | assert t.print_structure(True) == "" 35 | t.print_structure() 36 | 37 | t.verify() 38 | 39 | 40 | def test_mismatched_tree_and_membership_set(): 41 | t = IntervalTree.from_tuples(data.ivs1.data) 42 | members = set(t.all_intervals) 43 | assert t.all_intervals == members 44 | t.removei(1, 2, '[1,2)') 45 | assert t.all_intervals != members 46 | t.all_intervals = members # intentionally introduce error 47 | with pytest.raises(AssertionError): 48 | t.verify() 49 | 50 | 51 | def test_small_tree_score(): 52 | # inefficiency score for trees of len() <= 2 should be 0.0 53 | t = IntervalTree() 54 | assert t.score() == 0.0 55 | 56 | t.addi(1, 4) 57 | assert t.score() == 0.0 58 | 59 | t.addi(2, 5) 60 | assert t.score() == 0.0 61 | 62 | t.addi(1, 100) # introduces inefficiency, b/c len(s_center) > 1 63 | assert t.score() != 0.0 64 | 65 | 66 | def test_score_no_report(): 67 | t = IntervalTree.from_tuples(data.ivs1.data) 68 | score = t.score(False) 69 | assert isinstance(score, (int, float)) 70 | 71 | 72 | if __name__ == "__main__": 73 | pytest.main([__file__, '-v']) 74 | -------------------------------------------------------------------------------- /test/intervaltree_methods/delete_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: IntervalTree, Basic deletion methods 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import absolute_import 22 | from intervaltree import Interval, IntervalTree 23 | import pytest 24 | from test import data, match 25 | try: 26 | import cPickle as pickle 27 | except ImportError: 28 | import pickle 29 | 30 | 31 | def test_delete(): 32 | t = IntervalTree.from_tuples(data.ivs1.data) 33 | try: 34 | t.remove(Interval(1, 3, "Doesn't exist")) 35 | except ValueError: 36 | pass 37 | else: 38 | raise AssertionError("Expected ValueError") 39 | 40 | try: 41 | t.remove(Interval(500, 1000, "Doesn't exist")) 42 | except ValueError: 43 | pass 44 | else: 45 | raise AssertionError("Expected ValueError") 46 | 47 | orig = t.print_structure(True) 48 | t.discard(Interval(1, 3, "Doesn't exist")) 49 | t.discard(Interval(500, 1000, "Doesn't exist")) 50 | assert orig == t.print_structure(True) 51 | 52 | assert match.set_data(t[14]) == set(['[8,15)', '[14,15)']) 53 | t.remove(Interval(14, 15, '[14,15)')) 54 | assert match.set_data(t[14]) == set(['[8,15)']) 55 | t.verify() 56 | 57 | t.discard(Interval(8, 15, '[8,15)')) 58 | assert match.set_data(t[14]) == set() 59 | t.verify() 60 | 61 | assert t[5] 62 | t.remove_overlap(5) 63 | t.verify() 64 | assert not t[5] 65 | 66 | 67 | def test_removei(): 68 | # Empty tree 69 | e = IntervalTree() 70 | with pytest.raises(ValueError): 71 | e.removei(-1000, -999, "Doesn't exist") 72 | e.verify() 73 | assert len(e) == 0 74 | 75 | # Non-existent member should raise ValueError 76 | t = IntervalTree.from_tuples(data.ivs1.data) 77 | oldlen = len(t) 78 | with pytest.raises(ValueError): 79 | t.removei(-1000, -999, "Doesn't exist") 80 | t.verify() 81 | assert len(t) == oldlen 82 | 83 | # Should remove existing member 84 | assert Interval(1, 2, '[1,2)') in t 85 | t.removei(1, 2, '[1,2)') 86 | assert len(t) == oldlen - 1 87 | assert Interval(1, 2, '[1,2)') not in t 88 | 89 | 90 | def test_discardi(): 91 | # Empty tree 92 | e = IntervalTree() 93 | e.discardi(-1000, -999, "Doesn't exist") 94 | e.verify() 95 | assert len(e) == 0 96 | 97 | # Non-existent member should do nothing quietly 98 | t = IntervalTree.from_tuples(data.ivs1.data) 99 | oldlen = len(t) 100 | t.discardi(-1000, -999, "Doesn't exist") 101 | t.verify() 102 | assert len(t) == oldlen 103 | 104 | # Should discard existing member 105 | assert Interval(1, 2, '[1,2)') in t 106 | t.discardi(1, 2, '[1,2)') 107 | assert len(t) == oldlen - 1 108 | assert Interval(1, 2, '[1,2)') not in t 109 | 110 | 111 | def test_emptying_iteration(): 112 | t = IntervalTree.from_tuples(data.ivs1.data) 113 | 114 | for iv in sorted(iter(t)): 115 | t.remove(iv) 116 | t.verify() 117 | assert len(t) == 0 118 | assert t.is_empty() 119 | assert not t 120 | 121 | 122 | def test_emptying_clear(): 123 | t = IntervalTree.from_tuples(data.ivs1.data) 124 | assert t 125 | t.clear() 126 | assert len(t) == 0 127 | assert t.is_empty() 128 | assert not t 129 | 130 | # make sure emptying an empty tree does not crash 131 | t.clear() 132 | 133 | 134 | if __name__ == "__main__": 135 | pytest.main([__file__, '-v']) 136 | -------------------------------------------------------------------------------- /test/intervaltree_methods/init_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: IntervalTree, initialization methods 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import absolute_import 22 | from intervaltree import Interval, IntervalTree 23 | import pytest 24 | 25 | try: 26 | import cPickle as pickle 27 | except ImportError: 28 | import pickle 29 | 30 | 31 | def test_empty_init(): 32 | tree = IntervalTree() 33 | tree.verify() 34 | assert not tree 35 | assert len(tree) == 0 36 | assert list(tree) == [] 37 | assert tree.is_empty() 38 | 39 | 40 | def test_list_init(): 41 | tree = IntervalTree([Interval(-10, 10), Interval(-20.0, -10.0)]) 42 | tree.verify() 43 | assert tree 44 | assert len(tree) == 2 45 | assert tree.items() == set([Interval(-10, 10), Interval(-20.0, -10.0)]) 46 | assert tree.begin() == -20 47 | assert tree.end() == 10 48 | 49 | 50 | def test_generator_init(): 51 | tree = IntervalTree( 52 | Interval(begin, end) for begin, end in 53 | [(-10, 10), (-20, -10), (10, 20)] 54 | ) 55 | tree.verify() 56 | assert tree 57 | assert len(tree) == 3 58 | assert tree.items() == set([ 59 | Interval(-20, -10), 60 | Interval(-10, 10), 61 | Interval(10, 20), 62 | ]) 63 | assert tree.begin() == -20 64 | assert tree.end() == 20 65 | 66 | 67 | def test_invalid_interval_init(): 68 | """ 69 | Ensure that begin < end. 70 | """ 71 | with pytest.raises(ValueError): 72 | IntervalTree([Interval(-1, -2)]) 73 | 74 | with pytest.raises(ValueError): 75 | IntervalTree([Interval(0, 0)]) 76 | 77 | with pytest.raises(ValueError): 78 | IntervalTree(Interval(b, e) for b, e in [(1, 2), (1, 0)]) 79 | 80 | with pytest.raises(ValueError): 81 | IntervalTree(Interval(b, e) for b, e in [(1, 2), (1, 1)]) 82 | 83 | 84 | if __name__ == "__main__": 85 | pytest.main([__file__, '-v']) 86 | -------------------------------------------------------------------------------- /test/intervaltree_methods/insert_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: IntervalTree, Basic insertion methods 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import absolute_import 22 | from intervaltree import Interval, IntervalTree 23 | import pytest 24 | from test import data, match 25 | try: 26 | import cPickle as pickle 27 | except ImportError: 28 | import pickle 29 | 30 | 31 | def test_insert(): 32 | tree = IntervalTree() 33 | 34 | tree[0:1] = "data" 35 | assert len(tree) == 1 36 | assert tree.items() == set([Interval(0, 1, "data")]) 37 | 38 | tree.add(Interval(10, 20)) 39 | assert len(tree) == 2 40 | assert tree.items() == set([Interval(0, 1, "data"), Interval(10, 20)]) 41 | 42 | tree.addi(19.9, 20) 43 | assert len(tree) == 3 44 | assert tree.items() == set([ 45 | Interval(0, 1, "data"), 46 | Interval(19.9, 20), 47 | Interval(10, 20), 48 | ]) 49 | 50 | tree.update([Interval(19.9, 20.1), Interval(20.1, 30)]) 51 | assert len(tree) == 5 52 | assert tree.items() == set([ 53 | Interval(0, 1, "data"), 54 | Interval(19.9, 20), 55 | Interval(10, 20), 56 | Interval(19.9, 20.1), 57 | Interval(20.1, 30), 58 | ]) 59 | 60 | 61 | def test_duplicate_insert(): 62 | tree = IntervalTree() 63 | 64 | # string data 65 | tree[-10:20] = "arbitrary data" 66 | contents = frozenset([Interval(-10, 20, "arbitrary data")]) 67 | 68 | assert len(tree) == 1 69 | assert tree.items() == contents 70 | 71 | tree.addi(-10, 20, "arbitrary data") 72 | assert len(tree) == 1 73 | assert tree.items() == contents 74 | 75 | tree.add(Interval(-10, 20, "arbitrary data")) 76 | assert len(tree) == 1 77 | assert tree.items() == contents 78 | 79 | tree.update([Interval(-10, 20, "arbitrary data")]) 80 | assert len(tree) == 1 81 | assert tree.items() == contents 82 | 83 | # None data 84 | tree[-10:20] = None 85 | contents = frozenset([ 86 | Interval(-10, 20), 87 | Interval(-10, 20, "arbitrary data"), 88 | ]) 89 | 90 | assert len(tree) == 2 91 | assert tree.items() == contents 92 | 93 | tree.addi(-10, 20) 94 | assert len(tree) == 2 95 | assert tree.items() == contents 96 | 97 | tree.add(Interval(-10, 20)) 98 | assert len(tree) == 2 99 | assert tree.items() == contents 100 | 101 | tree.update([Interval(-10, 20), Interval(-10, 20, "arbitrary data")]) 102 | assert len(tree) == 2 103 | assert tree.items() == contents 104 | 105 | 106 | def test_same_range_insert(): 107 | t = IntervalTree.from_tuples(data.ivs1.data) 108 | 109 | t.add(Interval(14, 15, '[14,15)####')) 110 | assert match.set_data(t[14]) == set(['[8,15)', '[14,15)', '[14,15)####']) 111 | t.verify() 112 | 113 | 114 | def test_add_invalid_interval(): 115 | """ 116 | Ensure that begin < end. 117 | """ 118 | itree = IntervalTree() 119 | with pytest.raises(ValueError): 120 | itree.addi(1, 0) 121 | 122 | with pytest.raises(ValueError): 123 | itree.addi(1, 1) 124 | 125 | with pytest.raises(ValueError): 126 | itree[1:0] = "value" 127 | 128 | with pytest.raises(ValueError): 129 | itree[1:1] = "value" 130 | 131 | with pytest.raises(ValueError): 132 | itree[1.1:1.05] = "value" 133 | 134 | with pytest.raises(ValueError): 135 | itree[1.1:1.1] = "value" 136 | 137 | 138 | def test_insert_to_filled_tree(): 139 | t = IntervalTree.from_tuples(data.ivs1.data) 140 | orig = t.print_structure(True) # original structure record 141 | 142 | assert match.set_data(t[1]) == set(['[1,2)']) 143 | t.add(Interval(1, 2, '[1,2)')) # adding duplicate should do nothing 144 | assert match.set_data(t[1]) == set(['[1,2)']) 145 | assert orig == t.print_structure(True) 146 | 147 | t[1:2] = '[1,2)' # adding duplicate should do nothing 148 | assert match.set_data(t[1]) == set(['[1,2)']) 149 | assert orig == t.print_structure(True) 150 | 151 | assert Interval(2, 4, '[2,4)') not in t 152 | t.add(Interval(2, 4, '[2,4)')) 153 | assert match.set_data(t[2]) == set(['[2,4)']) 154 | t.verify() 155 | 156 | t[13:15] = '[13,15)' 157 | assert match.set_data(t[14]) == set(['[8,15)', '[13,15)', '[14,15)']) 158 | t.verify() 159 | 160 | 161 | if __name__ == "__main__": 162 | pytest.main([__file__, '-v']) 163 | -------------------------------------------------------------------------------- /test/intervaltree_methods/query_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: IntervalTree, Basic query methods (read-only) 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import absolute_import 22 | from intervaltree import Interval, IntervalTree 23 | import pytest 24 | from test import data, match 25 | try: 26 | import cPickle as pickle 27 | except ImportError: 28 | import pickle 29 | 30 | 31 | def test_empty_queries(): 32 | t = IntervalTree() 33 | e = set() 34 | 35 | assert len(t) == 0 36 | assert t.is_empty() 37 | assert t[3] == e 38 | assert t[4:6] == e 39 | assert t.begin() == 0 40 | assert t.end() == 0 41 | assert t[t.begin():t.end()] == e 42 | assert t.overlap(t.begin(), t.end()) == e 43 | assert t.envelop(t.begin(), t.end()) == e 44 | assert t.items() == e 45 | assert set(t) == e 46 | assert set(t.copy()) == e 47 | assert t.find_nested() == {} 48 | assert t.range().is_null() 49 | assert t.range().length() == 0 50 | t.verify() 51 | 52 | 53 | def test_point_queries(): 54 | t = IntervalTree.from_tuples(data.ivs1.data) 55 | assert match.set_data(t[4]) == set(['[4,7)']) 56 | assert match.set_data(t.at(4)) == set(['[4,7)']) 57 | assert match.set_data(t[9]) == set(['[6,10)', '[8,10)', '[8,15)']) 58 | assert match.set_data(t.at(9)) == set(['[6,10)', '[8,10)', '[8,15)']) 59 | assert match.set_data(t[15]) == set() 60 | assert match.set_data(t.at(15)) == set() 61 | assert match.set_data(t[5]) == set(['[4,7)', '[5,9)']) 62 | assert match.set_data(t.at(5)) == set(['[4,7)', '[5,9)']) 63 | assert match.set_data(t[4:5]) == set(['[4,7)']) 64 | 65 | 66 | def test_envelop_vs_overlap_queries(): 67 | t = IntervalTree.from_tuples(data.ivs1.data) 68 | assert match.set_data(t.envelop(4, 5)) == set() 69 | assert match.set_data(t.overlap(4, 5)) == set(['[4,7)']) 70 | assert match.set_data(t.envelop(4, 6)) == set() 71 | assert match.set_data(t.overlap(4, 6)) == set(['[4,7)', '[5,9)']) 72 | assert match.set_data(t.envelop(6, 10)) == set(['[6,10)', '[8,10)']) 73 | assert match.set_data(t.overlap(6, 10)) == set([ 74 | '[4,7)', '[5,9)', '[6,10)', '[8,10)', '[8,15)']) 75 | assert match.set_data(t.envelop(6, 11)) == set(['[6,10)', '[8,10)']) 76 | assert match.set_data(t.overlap(6, 11)) == set([ 77 | '[4,7)', '[5,9)', '[6,10)', '[8,10)', '[8,15)', '[10,12)']) 78 | 79 | 80 | def test_partial_get_query(): 81 | def assert_get(t, limit): 82 | s = set(t) 83 | assert t[:] == s 84 | 85 | s = set(iv for iv in t if iv.begin < limit) 86 | assert t[:limit] == s 87 | 88 | s = set(iv for iv in t if iv.end > limit) 89 | assert t[limit:] == s 90 | 91 | assert_get(IntervalTree.from_tuples(data.ivs1.data), 7) 92 | assert_get(IntervalTree.from_tuples(data.ivs2.data), -3) 93 | 94 | 95 | def test_tree_bounds(): 96 | def assert_tree_bounds(t): 97 | begin, end, _ = set(t).pop() 98 | for iv in t: 99 | if iv.begin < begin: begin = iv.begin 100 | if iv.end > end: end = iv.end 101 | assert t.begin() == begin 102 | assert t.end() == end 103 | 104 | assert_tree_bounds(IntervalTree.from_tuples(data.ivs1.data)) 105 | assert_tree_bounds(IntervalTree.from_tuples(data.ivs2.data)) 106 | 107 | 108 | def test_membership(): 109 | t = IntervalTree.from_tuples(data.ivs1.data) 110 | assert Interval(1, 2, '[1,2)') in t 111 | assert t.containsi(1, 2, '[1,2)') 112 | assert Interval(1, 3, '[1,3)') not in t 113 | assert not t.containsi(1, 3, '[1,3)') 114 | assert t.overlaps(4) 115 | assert t.overlaps(9) 116 | assert not t.overlaps(15) 117 | assert t.overlaps(0, 4) 118 | assert t.overlaps(1, 2) 119 | assert t.overlaps(1, 3) 120 | assert t.overlaps(8, 15) 121 | assert not t.overlaps(15, 16) 122 | assert not t.overlaps(-1, 0) 123 | assert not t.overlaps(2, 4) 124 | 125 | def test_overlaps_empty(): 126 | # Empty tree 127 | t = IntervalTree() 128 | assert not t.overlaps(-1) 129 | assert not t.overlaps(0) 130 | 131 | assert not t.overlaps(-1, 1) 132 | assert not t.overlaps(-1, 0) 133 | assert not t.overlaps(0, 0) 134 | assert not t.overlaps(0, 1) 135 | assert not t.overlaps(1, 0) 136 | assert not t.overlaps(1, -1) 137 | assert not t.overlaps(0, -1) 138 | 139 | assert not t.overlaps(Interval(-1, 1)) 140 | assert not t.overlaps(Interval(-1, 0)) 141 | assert not t.overlaps(Interval(0, 0)) 142 | assert not t.overlaps(Interval(0, 1)) 143 | assert not t.overlaps(Interval(1, 0)) 144 | assert not t.overlaps(Interval(1, -1)) 145 | assert not t.overlaps(Interval(0, -1)) 146 | 147 | 148 | def test_overlaps(): 149 | t = IntervalTree.from_tuples(data.ivs1.data) 150 | assert not t.overlaps(-3.2) 151 | assert t.overlaps(1) 152 | assert t.overlaps(1.5) 153 | assert t.overlaps(0, 3) 154 | assert not t.overlaps(0, 1) 155 | assert not t.overlaps(2, 4) 156 | assert not t.overlaps(4, 2) 157 | assert not t.overlaps(3, 0) 158 | 159 | 160 | def test_span(): 161 | e = IntervalTree() 162 | assert e.span() == 0 163 | 164 | t = IntervalTree.from_tuples(data.ivs1.data) 165 | assert t.span() == t.end() - t.begin() 166 | assert t.span() == 14 167 | 168 | 169 | if __name__ == "__main__": 170 | pytest.main([__file__, '-v']) 171 | -------------------------------------------------------------------------------- /test/intervaltree_methods/setlike_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: IntervalTree, Special methods 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import absolute_import 22 | from intervaltree import Interval, IntervalTree 23 | import pytest 24 | from test import data 25 | try: 26 | import cPickle as pickle 27 | except ImportError: 28 | import pickle 29 | 30 | 31 | def test_update(): 32 | t = IntervalTree() 33 | interval = Interval(0, 1) 34 | s = set([interval]) 35 | 36 | t.update(s) 37 | assert isinstance(t, IntervalTree) 38 | assert len(t) == 1 39 | assert set(t).pop() == interval 40 | 41 | interval = Interval(2, 3) 42 | t.update([interval]) 43 | assert isinstance(t, IntervalTree) 44 | assert len(t) == 2 45 | assert sorted(t)[1] == interval 46 | 47 | 48 | def test_invalid_update(): 49 | t = IntervalTree() 50 | 51 | with pytest.raises(ValueError): 52 | t.update([Interval(1, 0)]) 53 | 54 | with pytest.raises(ValueError): 55 | t.update([Interval(1, 1)]) 56 | 57 | 58 | def test_union(): 59 | t = IntervalTree() 60 | interval = Interval(0, 1) 61 | s = set([interval]) 62 | 63 | # union with empty 64 | r = t.union(s) 65 | assert len(r) == 1 66 | assert set(r).pop() == interval 67 | 68 | # update with duplicates 69 | t.update(s) 70 | t.update(s) 71 | assert len(t) == 1 72 | assert set(t).pop() == interval 73 | 74 | # update with non-dupe 75 | interval = Interval(2, 3) 76 | t.update([interval]) 77 | assert len(t) == 2 78 | assert sorted(t)[1] == interval 79 | 80 | # commutativity with full overlaps, then no overlaps 81 | a = IntervalTree.from_tuples(data.ivs1.data) 82 | b = IntervalTree.from_tuples(data.ivs2.data) 83 | e = IntervalTree() 84 | 85 | aa = a.union(a) 86 | ae = a.union(e) 87 | ea = e.union(a) 88 | ee = e.union(e) 89 | aa.verify() 90 | ae.verify() 91 | ea.verify() 92 | ee.verify() 93 | assert aa == a 94 | assert ae == a 95 | assert ea == a 96 | assert ee == e 97 | 98 | ab = a.union(b) 99 | ba = b.union(a) 100 | ab.verify() 101 | ba.verify() 102 | assert ab == ba 103 | assert len(ab) == 109 104 | 105 | # commutativity with strict subset overlap 106 | aba = ab.union(a) 107 | abb = ab.union(b) 108 | bab = ba.union(b) 109 | baa = ba.union(a) 110 | aba.verify() 111 | abb.verify() 112 | bab.verify() 113 | baa.verify() 114 | assert aba == abb 115 | assert abb == bab 116 | assert bab == baa 117 | 118 | assert aba == ab 119 | 120 | # commutativity with partial overlap 121 | c = IntervalTree.from_tuples(data.ivs3.data) 122 | bc = b.union(c) 123 | cb = c.union(b) 124 | bc.verify() 125 | cb.verify() 126 | assert bc == cb 127 | assert len(bc) > len(b) 128 | assert len(bc) > len(c) 129 | assert len(bc) < len(b) + len(c) 130 | for iv in b: 131 | assert iv in bc 132 | for iv in c: 133 | assert iv in bc 134 | 135 | 136 | def test_union_operator(): 137 | t = IntervalTree() 138 | interval = Interval(0, 1) 139 | s = set([interval]) 140 | 141 | # currently runs fine 142 | # with pytest.raises(TypeError): 143 | # t | list(s) 144 | r = t | IntervalTree(s) 145 | assert len(r) == 1 146 | assert sorted(r)[0] == interval 147 | 148 | # also currently runs fine 149 | # with pytest.raises(TypeError): 150 | # t |= s 151 | t |= IntervalTree(s) 152 | assert len(t) == 1 153 | assert sorted(t)[0] == interval 154 | 155 | 156 | def test_invalid_union(): 157 | t = IntervalTree() 158 | 159 | with pytest.raises(ValueError): 160 | t.union([Interval(1, 0)]) 161 | 162 | 163 | def test_difference(): 164 | minuend = IntervalTree.from_tuples(data.ivs1.data) 165 | assert isinstance(minuend, IntervalTree) 166 | subtrahend = minuend.copy() 167 | expected_difference = IntervalTree([subtrahend.pop()]) 168 | expected_difference.add(subtrahend.pop()) 169 | 170 | minuend.verify() 171 | subtrahend.verify() 172 | expected_difference.verify() 173 | 174 | assert len(expected_difference) == len(minuend) - len(subtrahend) 175 | 176 | for iv in expected_difference: 177 | assert iv not in subtrahend 178 | assert iv in minuend 179 | 180 | difference = minuend.difference(subtrahend) 181 | difference.verify() 182 | 183 | for iv in difference: 184 | assert iv not in subtrahend 185 | assert iv in minuend 186 | assert iv in expected_difference 187 | 188 | assert difference == expected_difference 189 | 190 | 191 | def test_difference_operator(): 192 | minuend = IntervalTree.from_tuples(data.ivs1.data) 193 | assert isinstance(minuend, IntervalTree) 194 | subtrahend = minuend.copy() 195 | expected_difference = IntervalTree([subtrahend.pop()]) 196 | expected_difference.add(subtrahend.pop()) 197 | 198 | minuend.verify() 199 | subtrahend.verify() 200 | expected_difference.verify() 201 | 202 | assert len(expected_difference) == len(minuend) - len(subtrahend) 203 | 204 | for iv in expected_difference: 205 | assert iv not in subtrahend 206 | assert iv in minuend 207 | 208 | difference = minuend - subtrahend 209 | difference.verify() 210 | 211 | for iv in difference: 212 | assert iv not in subtrahend 213 | assert iv in minuend 214 | assert iv in expected_difference 215 | 216 | assert difference == expected_difference 217 | 218 | 219 | def test_intersection(): 220 | a = IntervalTree.from_tuples(data.ivs1.data) 221 | b = IntervalTree.from_tuples(data.ivs2.data) 222 | e = IntervalTree() 223 | 224 | # intersections with e 225 | assert a.intersection(e) == e 226 | ae = a.copy() 227 | ae.intersection_update(e) 228 | assert ae == e 229 | 230 | assert b.intersection(e) == e 231 | be = b.copy() 232 | be.intersection_update(e) 233 | assert be == e 234 | 235 | assert e.intersection(e) == e 236 | ee = e.copy() 237 | ee.intersection_update(e) 238 | assert ee == e 239 | 240 | # intersections with self 241 | assert a.intersection(a) == a 242 | aa = a.copy() 243 | aa.intersection_update(a) 244 | assert aa == a 245 | 246 | assert b.intersection(b) == b 247 | bb = b.copy() 248 | bb.intersection(b) == b 249 | assert bb == b 250 | 251 | # commutativity resulting in empty 252 | ab = a.intersection(b) 253 | ba = b.intersection(a) 254 | ab.verify() 255 | ba.verify() 256 | assert ab == ba 257 | assert len(ab) == 0 # no overlaps, so empty tree 258 | 259 | ab = a.copy() 260 | ab.intersection_update(b) 261 | ba = b.copy() 262 | ba.intersection_update(a) 263 | ab.verify() 264 | ba.verify() 265 | assert ab == ba 266 | assert len(ab) == 0 # no overlaps, so empty tree 267 | 268 | # commutativity on non-overlapping sets 269 | ab = a.union(b) 270 | ba = b.union(a) 271 | aba = ab.intersection(a) # these should yield no change 272 | abb = ab.intersection(b) 273 | bab = ba.intersection(b) 274 | baa = ba.intersection(a) 275 | aba.verify() 276 | abb.verify() 277 | bab.verify() 278 | baa.verify() 279 | assert aba == a 280 | assert abb == b 281 | assert bab == b 282 | assert baa == a 283 | 284 | ab = a.union(b) 285 | ba = b.union(a) 286 | aba = ab.copy() 287 | aba.intersection_update(a) # these should yield no change 288 | abb = ab.copy() 289 | abb.intersection_update(b) 290 | bab = ba.copy() 291 | bab.intersection_update(b) 292 | baa = ba.copy() 293 | baa.intersection_update(a) 294 | aba.verify() 295 | abb.verify() 296 | bab.verify() 297 | baa.verify() 298 | assert aba == a 299 | assert abb == b 300 | assert bab == b 301 | assert baa == a 302 | 303 | # commutativity with overlapping sets 304 | c = IntervalTree.from_tuples(data.ivs3.data) 305 | bc = b.intersection(c) 306 | cb = c.intersection(b) 307 | bc.verify() 308 | cb.verify() 309 | assert bc == cb 310 | assert len(bc) < len(b) 311 | assert len(bc) < len(c) 312 | assert len(bc) > 0 313 | assert b.containsi(13, 23) 314 | assert c.containsi(13, 23) 315 | assert bc.containsi(13, 23) 316 | assert not b.containsi(819, 828) 317 | assert not c.containsi(0, 1) 318 | assert not bc.containsi(819, 828) 319 | assert not bc.containsi(0, 1) 320 | 321 | bc = b.copy() 322 | bc.intersection_update(c) 323 | cb = c.copy() 324 | cb.intersection_update(b) 325 | bc.verify() 326 | cb.verify() 327 | assert bc == cb 328 | assert len(bc) < len(b) 329 | assert len(bc) < len(c) 330 | assert len(bc) > 0 331 | assert b.containsi(13, 23) 332 | assert c.containsi(13, 23) 333 | assert bc.containsi(13, 23) 334 | assert not b.containsi(819, 828) 335 | assert not c.containsi(0, 1) 336 | assert not bc.containsi(819, 828) 337 | assert not bc.containsi(0, 1) 338 | 339 | 340 | def test_symmetric_difference(): 341 | a = IntervalTree.from_tuples(data.ivs1.data) 342 | b = IntervalTree.from_tuples(data.ivs2.data) 343 | e = IntervalTree() 344 | 345 | # symdiffs with e 346 | assert a.symmetric_difference(e) == a 347 | ae = a.copy() 348 | ae.symmetric_difference_update(e) 349 | assert ae == a 350 | 351 | assert b.symmetric_difference(e) == b 352 | be = b.copy() 353 | be.symmetric_difference_update(e) 354 | assert be == b 355 | 356 | assert e.symmetric_difference(e) == e 357 | ee = e.copy() 358 | ee.symmetric_difference_update(e) 359 | assert ee == e 360 | 361 | # symdiff with self 362 | assert a.symmetric_difference(a) == e 363 | aa = a.copy() 364 | aa.symmetric_difference_update(a) 365 | assert aa == e 366 | 367 | assert b.symmetric_difference(b) == e 368 | bb = b.copy() 369 | bb.symmetric_difference_update(b) == e 370 | assert bb == e 371 | 372 | # commutativity resulting in empty 373 | ab = a.symmetric_difference(b) 374 | ba = b.symmetric_difference(a) 375 | ab.verify() 376 | ba.verify() 377 | assert ab == ba 378 | assert len(ab) == len(a) + len(b) # no overlaps, so sum 379 | 380 | ab = a.copy() 381 | ab.symmetric_difference_update(b) 382 | ba = b.copy() 383 | ba.symmetric_difference_update(a) 384 | ab.verify() 385 | ba.verify() 386 | assert ab == ba 387 | assert len(ab) == len(a) + len(b) # no overlaps, so sum 388 | 389 | # commutativity on non-overlapping sets 390 | ab = a.union(b) 391 | ba = b.union(a) 392 | aba = ab.symmetric_difference(a) 393 | abb = ab.symmetric_difference(b) 394 | bab = ba.symmetric_difference(b) 395 | baa = ba.symmetric_difference(a) 396 | aba.verify() 397 | abb.verify() 398 | bab.verify() 399 | baa.verify() 400 | assert aba == b 401 | assert abb == a 402 | assert bab == a 403 | assert baa == b 404 | 405 | ab = a.union(b) 406 | ba = b.union(a) 407 | aba = ab.copy() 408 | aba.symmetric_difference_update(a) 409 | abb = ab.copy() 410 | abb.symmetric_difference_update(b) 411 | bab = ba.copy() 412 | bab.symmetric_difference_update(b) 413 | baa = ba.copy() 414 | baa.symmetric_difference_update(a) 415 | aba.verify() 416 | abb.verify() 417 | bab.verify() 418 | baa.verify() 419 | assert aba == b 420 | assert abb == a 421 | assert bab == a 422 | assert baa == b 423 | 424 | # commutativity with overlapping sets 425 | c = IntervalTree.from_tuples(data.ivs3.data) 426 | bc = b.symmetric_difference(c) 427 | cb = c.symmetric_difference(b) 428 | bc.verify() 429 | cb.verify() 430 | assert bc == cb 431 | assert len(bc) > 0 432 | assert len(bc) < len(b) + len(c) 433 | assert b.containsi(13, 23) 434 | assert c.containsi(13, 23) 435 | assert not bc.containsi(13, 23) 436 | assert c.containsi(819, 828) 437 | assert not b.containsi(819, 828) 438 | assert b.containsi(0, 1) 439 | assert not c.containsi(0, 1) 440 | assert bc.containsi(819, 828) 441 | assert bc.containsi(0, 1) 442 | 443 | bc = b.copy() 444 | bc.symmetric_difference_update(c) 445 | cb = c.copy() 446 | cb.symmetric_difference_update(b) 447 | bc.verify() 448 | cb.verify() 449 | assert bc == cb 450 | assert len(bc) > 0 451 | assert len(bc) < len(b) + len(c) 452 | assert b.containsi(13, 23) 453 | assert c.containsi(13, 23) 454 | assert not bc.containsi(13, 23) 455 | assert c.containsi(819, 828) 456 | assert not b.containsi(819, 828) 457 | assert b.containsi(0, 1) 458 | assert not c.containsi(0, 1) 459 | assert bc.containsi(819, 828) 460 | assert bc.containsi(0, 1) 461 | 462 | if __name__ == "__main__": 463 | pytest.main([__file__, '-v']) 464 | -------------------------------------------------------------------------------- /test/intervaltrees.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: utilities to generate test trees 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import absolute_import 22 | from intervaltree import IntervalTree 23 | from pprint import pprint 24 | from test import intervals, data 25 | from test.progress_bar import ProgressBar 26 | try: 27 | xrange 28 | except NameError: 29 | xrange = range 30 | 31 | def nogaps_rand(size=100, labels=False): 32 | """ 33 | Create a random IntervalTree with no gaps or overlaps between 34 | the intervals. 35 | """ 36 | return IntervalTree(intervals.nogaps_rand(size, labels)) 37 | 38 | 39 | def gaps_rand(size=100, labels=False): 40 | """ 41 | Create a random IntervalTree with random gaps, but no overlaps 42 | between the intervals. 43 | """ 44 | return IntervalTree(intervals.gaps_rand(size, labels)) 45 | -------------------------------------------------------------------------------- /test/issues/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: issues by tracking number 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | -------------------------------------------------------------------------------- /test/issues/issue25.txt: -------------------------------------------------------------------------------- 1 | Just before the final removei(), the tree looks like this: 2 | 3 | Node<10.58, depth=3, balance=1> 4 | Interval(8.65, 13.65) 5 | <: Node<5.66, depth=1, balance=0> 6 | Interval(3.57, 9.47) 7 | Interval(5.38, 10.38) 8 | Interval(5.66, 9.66) 9 | >: Node<16.49, depth=2, balance=-1> 10 | Interval(16.49, 20.83) 11 | <: Node<11.42, depth=1, balance=0> 12 | Interval(11.42, 16.42) 13 | 14 | verify() confirms that this is valid. Then the final call is: 15 | 16 | t.removei(8.65,13.65) 17 | 18 | Which should remove the root of the tree. This would leave a root Node with an empty s_center: 19 | 20 | Node<10.58, depth=3, balance=1> 21 | <: Node<5.66, depth=1, balance=0> 22 | Interval(3.57, 9.47) 23 | Interval(5.38, 10.38) 24 | Interval(5.66, 9.66) 25 | >: Node<16.49, depth=2, balance=-1> 26 | Interval(16.49, 20.83) 27 | <: Node<11.42, depth=1, balance=0> 28 | Interval(11.42, 16.42) 29 | 30 | To prune it, we rotate from the heavy side, but this is not possible because the heavy side has a child on the left. We must rotate it first: 31 | 32 | Node<10.58, depth=3, balance=1> 33 | <: Node<5.66, depth=1, balance=0> 34 | Interval(3.57, 9.47) 35 | Interval(5.38, 10.38) 36 | Interval(5.66, 9.66) 37 | >: Node<11.42, depth=2, balance=1> 38 | Interval(11.42, 16.42) 39 | >: Node<16.49, depth=1, balance=0> 40 | Interval(16.49, 20.83) 41 | 42 | Then we can prune the root: 43 | 44 | Node<11.42, depth=2, balance=0> 45 | Interval(11.42, 16.42) 46 | <: Node<5.66, depth=1, balance=0> 47 | Interval(3.57, 9.47) 48 | Interval(5.38, 10.38) 49 | Interval(5.66, 9.66) 50 | >: Node<16.49, depth=1, balance=0> 51 | Interval(16.49, 20.83) 52 | 53 | Manual verification shows that this is a valid tree. 54 | 55 | Instead, what happens is: 56 | 57 | 1) The IntervalTree object realizes correctly that it contains Interval(8.65,13.65). 58 | 2) It calls self.top_node.remove(Interval(8.65,13.65)). 59 | 3) The root Node checks for a center_hit(interval). As you recall, the tree looks like this: 60 | 61 | Node<10.58, depth=3, balance=1> 62 | Interval(8.65, 13.65) 63 | <: Node<5.66, depth=1, balance=0> 64 | Interval(3.57, 9.47) 65 | Interval(5.38, 10.38) 66 | Interval(5.66, 9.66) 67 | >: Node<16.49, depth=2, balance=-1> 68 | Interval(16.49, 20.83) 69 | <: Node<11.42, depth=1, balance=0> 70 | Interval(11.42, 16.42) 71 | 72 | 4) Interval(8.65,13.65) indeed does center_hit() with 10.58. 73 | 5) The Node calls self.s_center.remove(interval). 74 | 6) self.s_center is now empty. The Node now returns self.prune(). The tree looks like this: 75 | 76 | Node<10.58, depth=3, balance=1> 77 | <: Node<5.66, depth=1, balance=0> 78 | Interval(3.57, 9.47) 79 | Interval(5.38, 10.38) 80 | Interval(5.66, 9.66) 81 | >: Node<16.49, depth=2, balance=-1> 82 | Interval(16.49, 20.83) 83 | <: Node<11.42, depth=1, balance=0> 84 | Interval(11.42, 16.42) 85 | 86 | 7) prune() checks for an empty child. None exists. 87 | 8) prune() now calls (heir, self[0]) = self[0].pop_greatest_child(). (This is not optimal, because this is not the heavy side.) heir looks like this: 88 | 89 | Node<9.38, depth=2, balance=1> 90 | Interval(3.57, 9.47) 91 | Interval(5.38, 10.38) 92 | >: Node<5.66, depth=1, balance=0> 93 | Interval(5.66, 9.66) 94 | 95 | This is baffling, since there has been no Interval added with 9.38 as a bound. However, this is because the old code calculated the new x_center by subtracting 1 from the upper bound of the highest interval, and verifying that this is a valid x_center. 96 | 97 | The new code chooses the max of the current x_center and all the end boundaries of the intervals in s_center, except for the highest interval. This is less likely to make overlaps, I believe. 98 | -------------------------------------------------------------------------------- /test/issues/issue25_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: IntervalTree, insertion and removal of float intervals 6 | Submitted as issue #25 (Incorrect KeyError) by sciencectn 7 | 8 | Copyright 2013-2018 Chaim Leib Halbert 9 | 10 | Licensed under the Apache License, Version 2.0 (the "License"); 11 | you may not use this file except in compliance with the License. 12 | You may obtain a copy of the License at 13 | 14 | http://www.apache.org/licenses/LICENSE-2.0 15 | 16 | Unless required by applicable law or agreed to in writing, software 17 | distributed under the License is distributed on an "AS IS" BASIS, 18 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | See the License for the specific language governing permissions and 20 | limitations under the License. 21 | """ 22 | from __future__ import absolute_import 23 | from intervaltree import IntervalTree 24 | from test import data 25 | import pytest 26 | 27 | 28 | def test_sequence(): 29 | t = IntervalTree() 30 | t.addi(6.37,11.37) 31 | t.verify() 32 | t.addi(12.09,17.09) 33 | t.verify() 34 | t.addi(5.68,11.58) 35 | t.verify() 36 | t.removei(6.37,11.37) 37 | t.verify() 38 | t.addi(13.23,18.23) 39 | t.verify() 40 | t.removei(12.09,17.09) 41 | t.verify() 42 | t.addi(4.29,8.29) 43 | t.verify() 44 | t.removei(13.23,18.23) 45 | t.verify() 46 | t.addi(12.04,17.04) 47 | t.verify() 48 | t.addi(9.39,13.39) 49 | t.verify() 50 | t.removei(5.68,11.58) 51 | t.verify() 52 | t.removei(4.29,8.29) 53 | t.verify() 54 | t.removei(12.04,17.04) 55 | t.verify() 56 | t.addi(5.66,9.66) # Value inserted here 57 | t.verify() 58 | t.addi(8.65,13.65) 59 | t.verify() 60 | t.removei(9.39,13.39) 61 | t.verify() 62 | t.addi(16.49,20.83) 63 | t.verify() 64 | t.addi(11.42,16.42) 65 | t.verify() 66 | t.addi(5.38,10.38) 67 | t.verify() 68 | t.addi(3.57,9.47) 69 | t.verify() 70 | t.removei(8.65,13.65) 71 | t.verify() 72 | t.removei(5.66,9.66) # Deleted here 73 | t.verify() 74 | 75 | 76 | def test_structure(): 77 | """ 78 | Reconstruct the original tree just before the final removals, 79 | then perform the removals. This is needed because with future 80 | code changes, the above sequences may not exactly reproduce the 81 | internal structure of the tree. 82 | """ 83 | t = IntervalTree.from_tuples(data.issue25_orig.data) 84 | # t.print_structure() 85 | t.verify() 86 | 87 | t.removei(8.65, 13.65) # remove root node 88 | # t.print_structure() 89 | t.verify() 90 | 91 | t.removei(5.66, 9.66) 92 | # t.print_structure() 93 | t.verify() 94 | 95 | t.removei(5.38, 10.38) # try removing root node again 96 | # t.print_structure() 97 | t.verify() 98 | 99 | 100 | if __name__ == "__main__": 101 | # pytest.main([__file__, '-v']) 102 | test_structure() 103 | -------------------------------------------------------------------------------- /test/issues/issue26_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: IntervalTree, insertion and removal of float intervals 6 | Submitted as issue #26 (Pop from empty list error) by sciencectn 7 | Ensure that rotations that promote Intervals prune when necessary 8 | 9 | Copyright 2013-2018 Chaim Leib Halbert 10 | 11 | Licensed under the Apache License, Version 2.0 (the "License"); 12 | you may not use this file except in compliance with the License. 13 | You may obtain a copy of the License at 14 | 15 | http://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, 19 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | See the License for the specific language governing permissions and 21 | limitations under the License. 22 | """ 23 | from __future__ import absolute_import 24 | from intervaltree import IntervalTree, Interval 25 | # from test.intervaltrees import trees 26 | import pytest 27 | 28 | 29 | def test_original_sequence(): 30 | t = IntervalTree() 31 | t.addi(17.89,21.89) 32 | t.addi(11.53,16.53) 33 | t.removei(11.53,16.53) 34 | t.removei(17.89,21.89) 35 | t.addi(-0.62,4.38) 36 | t.addi(9.24,14.24) 37 | t.addi(4.0,9.0) 38 | t.removei(-0.62,4.38) 39 | t.removei(9.24,14.24) 40 | t.removei(4.0,9.0) 41 | t.addi(12.86,17.86) 42 | t.addi(16.65,21.65) 43 | t.removei(12.86,17.86) 44 | 45 | 46 | def test_debug_sequence(): 47 | t = IntervalTree() 48 | t.verify() 49 | t.addi(17.89,21.89) 50 | t.verify() 51 | t.addi(11.53,16.53) 52 | t.verify() 53 | t.removei(11.53,16.53) 54 | t.verify() 55 | t.removei(17.89,21.89) 56 | t.verify() 57 | t.addi(-0.62,4.38) 58 | t.verify() 59 | t.addi(9.24,14.24) 60 | # t.print_structure() 61 | # Node<-0.62, depth=2, balance=1> 62 | # Interval(-0.62, 4.38) 63 | # >: Node<9.24, depth=1, balance=0> 64 | # Interval(9.24, 14.24) 65 | t.verify() 66 | 67 | t.addi(4.0,9.0) # This line breaks the invariants, leaving an empty node 68 | # t.print_structure() 69 | t.verify() 70 | t.removei(-0.62,4.38) 71 | t.verify() 72 | t.removei(9.24,14.24) 73 | t.verify() 74 | t.removei(4.0,9.0) 75 | t.verify() 76 | t.addi(12.86,17.86) 77 | t.verify() 78 | t.addi(16.65,21.65) 79 | t.verify() 80 | t.removei(12.86,17.86) 81 | 82 | 83 | def test_minimal_sequence(): 84 | t = IntervalTree() 85 | t.addi(-0.62, 4.38) # becomes root 86 | t.addi(9.24, 14.24) # right child 87 | 88 | ## Check that the tree structure is like this: 89 | # t.print_structure() 90 | # Node<-0.62, depth=2, balance=1> 91 | # Interval(-0.62, 4.38) 92 | # >: Node<9.24, depth=1, balance=0> 93 | # Interval(9.24, 14.24) 94 | root = t.top_node 95 | assert root.s_center == set([Interval(-0.62, 4.38)]) 96 | assert root.right_node.s_center == set([Interval(9.24, 14.24)]) 97 | assert not root.left_node 98 | 99 | t.verify() 100 | 101 | # This line left an empty node when drotate() failed to promote 102 | # Intervals properly: 103 | t.addi(4.0, 9.0) 104 | t.print_structure() 105 | t.verify() 106 | 107 | 108 | if __name__ == "__main__": 109 | pytest.main([__file__, '-v']) 110 | -------------------------------------------------------------------------------- /test/issues/issue27_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: IntervalTree, insertion and removal of float intervals 6 | Submitted as issue #26 (Pop from empty list error) by sciencectn 7 | Ensure that rotations that promote Intervals prune when necessary 8 | 9 | Copyright 2013-2018 Chaim Leib Halbert 10 | 11 | Licensed under the Apache License, Version 2.0 (the "License"); 12 | you may not use this file except in compliance with the License. 13 | You may obtain a copy of the License at 14 | 15 | http://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, 19 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | See the License for the specific language governing permissions and 21 | limitations under the License. 22 | """ 23 | from __future__ import absolute_import 24 | from intervaltree import IntervalTree, Interval 25 | import pytest 26 | 27 | 28 | def original_print(): 29 | it = IntervalTree() 30 | it.addi(1, 3, "dude") 31 | it.addi(2, 4, "sweet") 32 | it.addi(6, 9, "rad") 33 | for iobj in it: 34 | print(it[iobj.begin, iobj.end]) # set(), should be using : 35 | 36 | for iobj in it: 37 | print(it.envelop(iobj.begin, iobj.end)) 38 | 39 | # set([Interval(6, 9, 'rad')]) 40 | # set([Interval(1, 3, 'dude'), Interval(2, 4, 'sweet')]) 41 | # set([Interval(1, 3, 'dude'), Interval(2, 4, 'sweet')]) 42 | 43 | 44 | def test_brackets_vs_overlap(): 45 | it = IntervalTree() 46 | it.addi(1, 3, "dude") 47 | it.addi(2, 4, "sweet") 48 | it.addi(6, 9, "rad") 49 | for iobj in it: 50 | assert it[iobj.begin:iobj.end] == it.overlap(iobj.begin, iobj.end) 51 | 52 | # set([Interval(6, 9, 'rad')]) 53 | # set([Interval(1, 3, 'dude'), Interval(2, 4, 'sweet')]) 54 | # set([Interval(1, 3, 'dude'), Interval(2, 4, 'sweet')]) 55 | 56 | 57 | if __name__ == "__main__": 58 | pytest.main([__file__, '-v']) 59 | 60 | -------------------------------------------------------------------------------- /test/issues/issue4.py: -------------------------------------------------------------------------------- 1 | ''' 2 | PyIntervalTree issue #4 test 3 | https://github.com/konstantint/PyIntervalTree/issues/4 4 | 5 | Test contributed by jacekt 6 | ''' 7 | from __future__ import absolute_import 8 | from intervaltree import IntervalTree 9 | from test.progress_bar import ProgressBar 10 | from test import data 11 | items = data.issue4.data 12 | MAX = data.issue4.MAX 13 | from test.intervals import write_ivs_data 14 | from test.optimality.optimality_test_matrix import OptimalityTestMatrix 15 | from pprint import pprint 16 | import cProfile 17 | import pstats 18 | 19 | 20 | def test_build_tree(): 21 | pbar = ProgressBar(len(items)) 22 | 23 | tree = IntervalTree() 24 | tree[0:MAX] = None 25 | for b, e, alloc in items: 26 | if alloc: 27 | ivs = tree[b:e] 28 | assert len(ivs)==1 29 | iv = ivs.pop() 30 | assert iv.begin<=b and e<=iv.end 31 | tree.remove(iv) 32 | if iv.begin worst: 46 | worst = score 47 | 48 | # make sure we did at least as well as before's worst-case 49 | assert score <= prev_score 50 | assert 0.0 == report[test]['depth'] 51 | 52 | if worst < prev_score: # worst-case has improved! 53 | warn(IntervalTree.from_tuples(data.ivs1.data).print_structure(True)) 54 | warn("ivs1 scored {0} < {1} worst-case, better than expected!".format( 55 | score, 56 | prev_score 57 | )) 58 | 59 | 60 | def test_ivs2(): 61 | """ 62 | No gaps, no overlaps. 63 | """ 64 | report = matrix.result_matrix['ivs name']['ivs2'] 65 | assert 0.0 == report['add ascending']['_cumulative'] 66 | assert 0.0 == report['add descending']['_cumulative'] 67 | assert 0.0 == report['init']['_cumulative'] 68 | 69 | def test_ivs3(): 70 | """ 71 | Gaps, no overlaps. 72 | """ 73 | report = matrix.result_matrix['ivs name']['ivs2'] 74 | assert 0.0 == report['add ascending']['_cumulative'] 75 | assert 0.0 == report['add descending']['_cumulative'] 76 | assert 0.0 == report['init']['_cumulative'] 77 | 78 | 79 | if __name__ == "__main__": 80 | test_ivs1() 81 | test_ivs2() 82 | test_ivs3() 83 | pprint(matrix.summary_matrix) 84 | pprint(matrix.result_matrix) 85 | -------------------------------------------------------------------------------- /test/optimality/optimality_test_matrix.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: IntervalTree optimality 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import absolute_import 22 | from intervaltree import IntervalTree, Interval 23 | from test import data 24 | from copy import deepcopy 25 | from pprint import pprint 26 | from test.progress_bar import ProgressBar 27 | 28 | try: 29 | xrange 30 | except NameError: 31 | xrange = range 32 | 33 | 34 | class OptimalityTestMatrix(object): 35 | def __init__(self, ivs=None, verbose=False): 36 | """ 37 | Initilize a test matrix. To run it, see run(). 38 | :param ivs: A dictionary mapping each test name to its 39 | iterable of Intervals. 40 | :type ivs: None or dict of [str, list of Interval] 41 | :param verbose: Whether to print the structure of the trees 42 | :type verbose: bool 43 | """ 44 | self.verbose = verbose 45 | 46 | # set test_tupes 47 | self.test_types = {} 48 | # all methods beginning with "test_" 49 | test_names = [ 50 | name for name in self.__class__.__dict__ 51 | if 52 | callable(getattr(self, name)) and 53 | name.startswith('test_') 54 | ] 55 | for test_name in test_names: 56 | key = test_name[len('test_'):] 57 | key = ' '.join(key.split('_')) 58 | test_function = getattr(self, test_name) 59 | self.test_types[key] = test_function 60 | 61 | # set ivs 62 | self.ivs = { 63 | key: [Interval(*tup) for tup in value.data] 64 | for key, value in data.__dict__.items() 65 | if 'copy_structure' not in key and hasattr(value, 'data') 66 | } 67 | 68 | # initialize result matrix 69 | self.result_matrix = { 70 | 'ivs name': {}, 71 | 'test type': {} 72 | } 73 | for name in self.ivs: 74 | self.result_matrix['ivs name'][name] = {} 75 | for name in self.test_types: 76 | self.result_matrix['test type'][name] = {} 77 | self.summary_matrix = deepcopy(self.result_matrix) 78 | 79 | def test_init(self, ivs): 80 | t = IntervalTree(ivs) 81 | return t 82 | 83 | def test_add_ascending(self, ivs): 84 | if self.verbose: 85 | pbar = ProgressBar(len(ivs)) 86 | t = IntervalTree() 87 | for iv in sorted(ivs): 88 | t.add(iv) 89 | if self.verbose: pbar() 90 | return t 91 | 92 | def test_add_descending(self, ivs): 93 | if self.verbose: 94 | pbar = ProgressBar(len(ivs)) 95 | t = IntervalTree() 96 | for iv in sorted(ivs, reverse=True): 97 | t.add(iv) 98 | if self.verbose: pbar() 99 | return t 100 | 101 | def test_prebuilt(self, tree): 102 | if isinstance(tree, IntervalTree): 103 | return tree 104 | return None # N/A 105 | 106 | def summarize(self): 107 | def stats(report): 108 | assert isinstance(report, dict) 109 | cumulatives = [test['_cumulative'] for test in report.values()] 110 | return { 111 | 'mean': sum(cumulatives) / len(cumulatives), 112 | 'worst': max(cumulatives), 113 | 'best': min(cumulatives), 114 | } 115 | for report_type in self.result_matrix: 116 | for name, report in self.result_matrix[report_type].items(): 117 | if report: 118 | self.summary_matrix[report_type][name] = stats(report) 119 | elif report_type == "test type": 120 | del self.test_types[name] 121 | return self.result_matrix 122 | 123 | def tabulate(self): 124 | """ 125 | Store all the score results in self.result_matrix. 126 | """ 127 | for test_name, test in self.test_types.items(): 128 | for ivs_name, ivs in self.ivs.items(): 129 | if self.verbose: 130 | print("{0}: {1}".format(ivs_name, test_name)) 131 | tree = test(ivs) 132 | if not tree: 133 | continue 134 | score = tree.score(True) 135 | if self.verbose > 1: 136 | tree.print_structure() 137 | 138 | self.result_matrix['ivs name'][ivs_name][test_name] = score 139 | self.result_matrix['test type'][test_name][ivs_name] = score 140 | 141 | def run(self): 142 | self.tabulate() 143 | self.summarize() 144 | results = { 145 | 'summary': self.summary_matrix, 146 | 'results': self.result_matrix, 147 | } 148 | return results 149 | 150 | if __name__ == "__main__": 151 | from test.intervaltrees import trees 152 | matrix = OptimalityTestMatrix() 153 | matrix.run() 154 | pprint(matrix.summary_matrix) 155 | 156 | matrix = OptimalityTestMatrix({ 157 | 'ivs': trees['ivs1'](), 158 | }) 159 | matrix.run() 160 | pprint(matrix.summary_matrix) 161 | # pprint(matrix.result_matrix) 162 | -------------------------------------------------------------------------------- /test/pprint.py: -------------------------------------------------------------------------------- 1 | # Author: Fred L. Drake, Jr. 2 | # fdrake@acm.org 3 | # 4 | # This is a simple little module I wrote to make life easier. I didn't 5 | # see anything quite like it in the library, though I may have overlooked 6 | # something. I wrote this when I was trying to read some heavily nested 7 | # tuples with fairly non-descriptive content. This is modeled very much 8 | # after Lisp/Scheme - style pretty-printing of lists. If you find it 9 | # useful, thank small children who sleep at night. 10 | 11 | """Support to pretty-print lists, tuples, & dictionaries recursively. 12 | 13 | ## Modified by Chaim-Leib Halbert 03/06/14: 14 | ## Added support for objects defining simple __reduce__() methods. 15 | ## For this to function, __reduce__ must returns a tuple of a 16 | ## constructor and arguments for the constructor ONLY. No attribute 17 | ## dictionary allowed, or we default to old pprint behavior. 18 | ## 19 | ## To use old pprint behavior and ignore __reduce__, pass pprint and 20 | ## friends enable_pickle=False. 21 | 22 | Very simple, but useful, especially in debugging data structures. 23 | 24 | Classes 25 | ------- 26 | 27 | PrettyPrinter() 28 | Handle pretty-printing operations onto a stream using a configured 29 | set of formatting parameters. 30 | 31 | Functions 32 | --------- 33 | 34 | pformat() 35 | Format a Python object into a pretty-printed representation. 36 | 37 | pprint() 38 | Pretty-print a Python object to a stream [default is sys.stdout]. 39 | 40 | saferepr() 41 | Generate a 'standard' repr()-like value, but protect against recursive 42 | data structures. 43 | 44 | """ 45 | 46 | #print('Using custom pprint!') 47 | 48 | import sys as _sys 49 | import warnings 50 | 51 | ENABLE_PICKLE_DEFAULT = True 52 | from numbers import Number 53 | 54 | try: 55 | from cStringIO import StringIO as _StringIO 56 | except ImportError: 57 | try: 58 | from StringIO import StringIO as _StringIO 59 | except ImportError: 60 | from io import StringIO as _StringIO 61 | 62 | try: 63 | basestring 64 | except NameError: 65 | basestring = str 66 | 67 | __all__ = ["pprint","pformat","isreadable","isrecursive","saferepr", 68 | "PrettyPrinter"] 69 | 70 | # cache these for faster access: 71 | _commajoin = ", ".join 72 | _id = id 73 | _len = len 74 | _type = type 75 | 76 | 77 | def pprint(object, stream=None, indent=1, width=80, depth=None, 78 | enable_pickle=ENABLE_PICKLE_DEFAULT): 79 | """Pretty-print a Python object to a stream [default is sys.stdout].""" 80 | printer = PrettyPrinter( 81 | stream=stream, indent=indent, width=width, depth=depth, 82 | enable_pickle=enable_pickle) 83 | printer.pprint(object) 84 | 85 | def pformat(object, indent=1, width=80, depth=None, 86 | enable_pickle=ENABLE_PICKLE_DEFAULT): 87 | """Format a Python object into a pretty-printed representation.""" 88 | printer = PrettyPrinter( 89 | indent=indent, width=width, depth=depth, 90 | enable_pickle=enable_pickle) 91 | return printer.pformat(object) 92 | 93 | def saferepr(object, enable_pickle=ENABLE_PICKLE_DEFAULT): 94 | """Version of repr() which can handle recursive data structures.""" 95 | return _safe_repr(object, {}, None, 0, enable_pickle=enable_pickle)[0] 96 | 97 | def isreadable(object, enable_pickle=ENABLE_PICKLE_DEFAULT): 98 | """Determine if saferepr(object) is readable by eval().""" 99 | return _safe_repr(object, {}, None, 0, enable_pickle=enable_pickle)[1] 100 | 101 | def isrecursive(object, enable_pickle=ENABLE_PICKLE_DEFAULT): 102 | """Determine if object requires a recursive representation.""" 103 | return _safe_repr(object, {}, None, 0, enable_pickle=enable_pickle)[2] 104 | 105 | def _sorted(iterable): 106 | with warnings.catch_warnings(): 107 | if _sys.py3kwarning: 108 | warnings.filterwarnings("ignore", "comparing unequal types " 109 | "not supported", DeprecationWarning) 110 | iterable = list(iterable) 111 | if iterable and hasattr(iterable[0], '__key__'): 112 | key = iterable[0].__key__ 113 | return sorted(iterable, key=key) 114 | return sorted(iterable) 115 | 116 | class PrettyPrinter: 117 | def __init__(self, indent=1, width=80, depth=None, stream=None, enable_pickle=ENABLE_PICKLE_DEFAULT): 118 | """Handle pretty printing operations onto a stream using a set of 119 | configured parameters. 120 | 121 | indent 122 | Number of spaces to indent for each level of nesting. 123 | 124 | width 125 | Attempted maximum number of columns in the output. 126 | 127 | depth 128 | The maximum depth to print out nested structures. 129 | 130 | stream 131 | The desired output stream. If omitted (or false), the standard 132 | output stream available at construction will be used. 133 | 134 | """ 135 | indent = int(indent) 136 | width = int(width) 137 | assert indent >= 0, "indent must be >= 0" 138 | assert depth is None or depth > 0, "depth must be > 0" 139 | assert width, "width must be != 0" 140 | self._depth = depth 141 | self._indent_per_level = indent 142 | self._width = width 143 | if stream is not None: 144 | self._stream = stream 145 | else: 146 | self._stream = _sys.stdout 147 | self.enable_pickle = enable_pickle 148 | 149 | def pprint(self, object, enable_pickle=None): 150 | if enable_pickle is None: 151 | enable_pickle = self.enable_pickle 152 | self._format(object, self._stream, 0, 0, {}, 0, enable_pickle) 153 | self._stream.write("\n") 154 | 155 | def pformat(self, object, enable_pickle=None): 156 | if enable_pickle is None: 157 | enable_pickle = self.enable_pickle 158 | sio = _StringIO() 159 | self._format(object, sio, 0, 0, {}, 0, enable_pickle) 160 | return sio.getvalue() 161 | 162 | def isrecursive(self, object): 163 | return self.format(object, {}, 0, 0)[2] 164 | 165 | def isreadable(self, object): 166 | s, readable, recursive = self.format(object, {}, 0, 0) 167 | return readable and not recursive 168 | 169 | def _format(self, object, stream, indent, allowance, context, level, enable_pickle=None): 170 | if enable_pickle is None: 171 | enable_pickle = self.enable_pickle 172 | level = level + 1 173 | objid = _id(object) 174 | if objid in context: 175 | stream.write(_recursion(object)) 176 | self._recursive = True 177 | self._readable = False 178 | return 179 | rep = self._repr(object, context, level - 1) 180 | typ = _type(object) 181 | sepLines = _len(rep) > (self._width - 1 - indent - allowance) 182 | write = stream.write 183 | 184 | if self._depth and level > self._depth: 185 | write(rep) 186 | return 187 | 188 | r = getattr(typ, "__repr__", None) 189 | if issubclass(typ, dict) and r is dict.__repr__: 190 | write('{') 191 | if self._indent_per_level > 1: 192 | write((self._indent_per_level - 1) * ' ') 193 | length = _len(object) 194 | if length: 195 | context[objid] = 1 196 | indent = indent + self._indent_per_level 197 | items = _sorted(object.items()) 198 | key, ent = items[0] 199 | rep = self._repr(key, context, level) 200 | write(rep) 201 | write(': ') 202 | self._format(ent, stream, indent + _len(rep) + 2, 203 | allowance + 1, context, level) 204 | if length > 1: 205 | for key, ent in items[1:]: 206 | rep = self._repr(key, context, level) 207 | if sepLines: 208 | write(',\n%s%s: ' % (' '*indent, rep)) 209 | else: 210 | write(', %s: ' % rep) 211 | self._format(ent, stream, indent + _len(rep) + 2, 212 | allowance + 1, context, level) 213 | indent = indent - self._indent_per_level 214 | del context[objid] 215 | write('}') 216 | return 217 | 218 | if ((issubclass(typ, list) and r is list.__repr__) or 219 | (issubclass(typ, tuple) and r is tuple.__repr__) or 220 | (issubclass(typ, set) and r is set.__repr__) or 221 | (issubclass(typ, frozenset) and r is frozenset.__repr__) 222 | ): 223 | length = _len(object) 224 | if issubclass(typ, list): 225 | write('[') 226 | endchar = ']' 227 | elif issubclass(typ, set): 228 | if not length: 229 | write('set()') 230 | return 231 | write('set([') 232 | endchar = '])' 233 | object = _sorted(object) 234 | indent += 4 235 | elif issubclass(typ, frozenset): 236 | if not length: 237 | write('frozenset()') 238 | return 239 | write('frozenset([') 240 | endchar = '])' 241 | object = _sorted(object) 242 | indent += 10 243 | else: 244 | write('(') 245 | endchar = ')' 246 | if self._indent_per_level > 1 and sepLines: 247 | write((self._indent_per_level - 1) * ' ') 248 | if length: 249 | context[objid] = 1 250 | indent = indent + self._indent_per_level 251 | self._format(object[0], stream, indent, allowance + 1, 252 | context, level) 253 | if length > 1: 254 | for ent in object[1:]: 255 | if sepLines: 256 | write(',\n' + ' '*indent) 257 | else: 258 | write(', ') 259 | self._format(ent, stream, indent, 260 | allowance + 1, context, level) 261 | indent = indent - self._indent_per_level 262 | del context[objid] 263 | if issubclass(typ, tuple) and length == 1: 264 | write(',') 265 | write(endchar) 266 | return 267 | 268 | ## use pickle data 269 | if enable_pickle and _pickleable(typ, object): 270 | reduce_data = object.__reduce__() 271 | constructor, args = reduce_data 272 | constructor = constructor.__name__ 273 | length = _len(args) 274 | if not length: 275 | write(constructor + '()') 276 | return 277 | write(constructor + '(') 278 | endchar = ')' 279 | indent += len(constructor) 280 | 281 | if self._indent_per_level > 1 and sepLines: 282 | write((self._indent_per_level - 1) * ' ') 283 | 284 | context[objid] = 1 285 | indent = indent + self._indent_per_level 286 | self._format(args[0], stream, indent, allowance + 1, 287 | context, level) 288 | if length > 1: 289 | for ent in args[1:]: 290 | if sepLines: 291 | write(',\n' + ' '*indent) 292 | else: 293 | write(', ') 294 | self._format(ent, stream, indent, 295 | allowance + 1, context, level) 296 | indent = indent - self._indent_per_level 297 | del context[objid] 298 | write(endchar) 299 | return 300 | 301 | write(rep) 302 | 303 | def _repr(self, object, context, level): 304 | repr, readable, recursive = self.format(object, context.copy(), 305 | self._depth, level) 306 | if not readable: 307 | self._readable = False 308 | if recursive: 309 | self._recursive = True 310 | return repr 311 | 312 | def format(self, object, context, maxlevels, level, enable_pickle=None): 313 | if enable_pickle is None: 314 | enable_pickle = self.enable_pickle 315 | """Format object for a specific context, returning a string 316 | and flags indicating whether the representation is 'readable' 317 | and whether the object represents a recursive construct. 318 | """ 319 | return _safe_repr(object, context, maxlevels, level, enable_pickle=enable_pickle) 320 | 321 | 322 | # Return triple (repr_string, isreadable, isrecursive). 323 | 324 | def _safe_repr(object, context, maxlevels, level, enable_pickle=None): 325 | if enable_pickle is None: 326 | enable_pickle = ENABLE_PICKLE_DEFAULT 327 | typ = _type(object) 328 | if typ is str: 329 | if 'locale' not in _sys.modules: 330 | return repr(object), True, False 331 | if "'" in object and '"' not in object: 332 | closure = '"' 333 | quotes = {'"': '\\"'} 334 | else: 335 | closure = "'" 336 | quotes = {"'": "\\'"} 337 | qget = quotes.get 338 | sio = _StringIO() 339 | write = sio.write 340 | for char in object: 341 | if char.isalpha(): 342 | write(char) 343 | else: 344 | write(qget(char, repr(char)[1:-1])) 345 | return ("%s%s%s" % (closure, sio.getvalue(), closure)), True, False 346 | 347 | r = getattr(typ, "__repr__", None) 348 | if issubclass(typ, dict) and r is dict.__repr__: 349 | if not object: 350 | return "{}", True, False 351 | objid = _id(object) 352 | if maxlevels and level >= maxlevels: 353 | return "{...}", False, objid in context 354 | if objid in context: 355 | return _recursion(object), False, True 356 | context[objid] = 1 357 | readable = True 358 | recursive = False 359 | components = [] 360 | append = components.append 361 | level += 1 362 | saferepr = _safe_repr 363 | for k, v in _sorted(object.items()): 364 | krepr, kreadable, krecur = saferepr(k, context, maxlevels, level) 365 | vrepr, vreadable, vrecur = saferepr(v, context, maxlevels, level) 366 | append("%s: %s" % (krepr, vrepr)) 367 | readable = readable and kreadable and vreadable 368 | if krecur or vrecur: 369 | recursive = True 370 | del context[objid] 371 | return "{%s}" % _commajoin(components), readable, recursive 372 | 373 | if (issubclass(typ, list) and r is list.__repr__) or \ 374 | (issubclass(typ, tuple) and r is tuple.__repr__): 375 | if issubclass(typ, list): 376 | if not object: 377 | return "[]", True, False 378 | format = "[%s]" 379 | elif _len(object) == 1: 380 | format = "(%s,)" 381 | else: 382 | if not object: 383 | return "()", True, False 384 | format = "(%s)" 385 | objid = _id(object) 386 | if maxlevels and level >= maxlevels: 387 | return format % "...", False, objid in context 388 | if objid in context: 389 | return _recursion(object), False, True 390 | context[objid] = 1 391 | readable = True 392 | recursive = False 393 | components = [] 394 | append = components.append 395 | level += 1 396 | for o in object: 397 | orepr, oreadable, orecur = _safe_repr( 398 | o, context, maxlevels, level, enable_pickle) 399 | append(orepr) 400 | if not oreadable: 401 | readable = False 402 | if orecur: 403 | recursive = True 404 | del context[objid] 405 | return format % _commajoin(components), readable, recursive 406 | 407 | ## Use pickle data 408 | if enable_pickle and _pickleable(typ, object): 409 | reduce_data = object.__reduce__() 410 | constructor, args = reduce_data 411 | constructor = constructor.__name__ 412 | if not args: 413 | return constructor+"()", True, False 414 | objid = _id(object) 415 | if maxlevels and level >= maxlevels: 416 | return constructor+"(...)", False, objid in context 417 | if objid in context: 418 | return _recursion(object), False, True 419 | context[objid] = 1 420 | readable = True 421 | recursive = False 422 | components = [] 423 | append = components.append 424 | level += 1 425 | saferepr = _safe_repr 426 | for arg in args: 427 | arepr, areadable, arecur = saferepr(arg, context, maxlevels, level) 428 | append(arepr) 429 | readable = readable and areadable 430 | if arecur: 431 | recursive = True 432 | del context[objid] 433 | return constructor + "(%s)" % _commajoin(components), readable, recursive 434 | 435 | rep = repr(object) 436 | return rep, (rep and not rep.startswith('<')), False 437 | 438 | def _pickleable(typ, obj): 439 | """ 440 | Whether we can use __reduce__ to pprint. 441 | """ 442 | if (issubclass(typ, Number) or 443 | issubclass(typ, basestring) 444 | ): return False 445 | try: 446 | reduce_data = obj.__reduce__() 447 | except TypeError: 448 | return False 449 | return len(reduce_data) == 2 450 | 451 | def _recursion(object): 452 | return ("" 453 | % (_type(object).__name__, _id(object))) 454 | 455 | 456 | def _perfcheck(object=None, enable_pickle=None): 457 | if enable_pickle is None: 458 | enable_pickle = ENABLE_PICKLE_DEFAULT 459 | import time 460 | if object is None: 461 | object = [("string", (1, 2), [3, 4], {5: 6, 7: 8})] * 100000 462 | p = PrettyPrinter(enable_pickle=enable_pickle) 463 | t1 = time.time() 464 | _safe_repr(object, {}, None, 0, enable_pickle=enable_pickle) 465 | t2 = time.time() 466 | p.pformat(object) 467 | t3 = time.time() 468 | print ("_safe_repr:", t2 - t1) 469 | print ("pformat:", t3 - t2) 470 | 471 | if __name__ == "__main__": 472 | print("With enable_pickle:") 473 | _perfcheck(enable_pickle=True) 474 | 475 | print("") 476 | print("Without enable_pickle:") 477 | _perfcheck(enable_pickle=False) 478 | -------------------------------------------------------------------------------- /test/progress_bar.py: -------------------------------------------------------------------------------- 1 | """ 2 | intervaltree: A mutable, self-balancing interval tree for Python 2 and 3. 3 | Queries may be by point, by range overlap, or by range envelopment. 4 | 5 | Test module: progress bar 6 | 7 | Copyright 2013-2018 Chaim Leib Halbert 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import print_function 22 | from __future__ import division 23 | from time import time 24 | from functools import partial 25 | import sys 26 | 27 | try: 28 | xrange 29 | except NameError: 30 | xrange = range 31 | 32 | 33 | def write(s): 34 | sys.stdout.write(s) 35 | sys.stdout.flush() 36 | 37 | 38 | class ProgressBar(object): 39 | """ 40 | See how far a process has gone. 41 | 42 | Example: 43 | from progress_bar import ProgressBar 44 | 45 | big_list = ... 46 | 47 | pbar = ProgressBar(len(big_list)) 48 | for item in big_list: 49 | process_item(item) 50 | pbar() 51 | 52 | You can increment by different amounts by calling 53 | `pbar(increment)` instead of just `pbar()`. 54 | 55 | ## Creating a `ProgressBar` 56 | You can customize the format for the output using arguments to 57 | the initializer. 58 | 59 | `width`: 60 | how many characters wide 61 | `fmt`: 62 | a format string. Choose from ProgressBar.Formats.* or create 63 | your own. Available predesigned formats: 64 | 65 | class Formats(object): 66 | basic = '[%=] %p%%' 67 | fraction = '|%=| %i/%t' 68 | timer = basic + ' %es' 69 | predictive = '(%=) %p%% %es elapsed, ETA %Es' 70 | speed = '[%=] %R/s %p%%' 71 | advanced = '%r/%t remaining [%=] %p%% ETA %Es @ %R/s %es elapsed' 72 | items_remaining = '{%=} %r/%t remaining' 73 | stats_only = '%p%% %r/%t remaining %_ ETA %Es @ %R/s %es elapsed' 74 | 75 | `time_throttle`: 76 | minimum time in seconds before updating the display 77 | `custom_outputter`: 78 | a dict mapping character strings to strings or callables that 79 | generate strings. The callable will be invoked with the 80 | `ProgressBar` instance as its first argument. Example: 81 | 82 | def kbytes_copied(pbar): 83 | kbytes = pbar.i / 1024 84 | return pbar.float_formatter(kbytes) + 'KB' 85 | 86 | pbar = ProgressBar(max_val, 87 | fmt='%p%% %[%=] %k copied', # '%k' turns into kbytes_copied() 88 | custom_outputters={'k': kbytes_copied}) 89 | 90 | `custom_expanding_outputters`: 91 | Like `custom_outputter`, but for output that expands to fill 92 | available space. These are invoked with two arguments: the 93 | `ProgressBar` instance, and the calculated remaining width 94 | for your outputter. Example: 95 | 96 | def hash_progressbar(pbar, size): 97 | segs = pbar.fraction * size 98 | return segs*'#' + (size-segs)*' ' 99 | 100 | pbar = ProgressBar(max_val, 101 | fmt='%p%% [%#]', # '%#' turns into my hash_progressbar 102 | custom_expanding_outputters={'#': hash_progressbar}) 103 | 104 | 105 | ## Predefined outputters 106 | All these are preceded with a percent symbol in the `fmt` string 107 | provided when creating a `ProgressBar`. 108 | 109 | `t`: 110 | Maximum progress count. 111 | `i`: 112 | Current count. 113 | `=`: 114 | Progress bar, without endcaps. Expands to fill available width. 115 | `_` (underscore): 116 | Expanding space. 117 | `p`: 118 | Percent progress, without percent symbol. To add that, use '%%' 119 | `r`: 120 | Count remaining. 121 | `e`: 122 | Time elapsed. Counted from first `pbar()` call, where `pbar` is 123 | your `ProgressBar` instance. 124 | `E`: 125 | ETA (estimated time of arrival). Based on current rate, linearly 126 | extrapolate to predict how much time in seconds is left. 127 | `R`: 128 | Count rate. In units of counts per second. 129 | `%`: 130 | A literal percent symbol. 131 | 132 | ## Fields available for your outputter 133 | When writing custom outputters, it is sometimes useful to access the 134 | internal fields of the `ProgressBar` instance. The following are 135 | available: 136 | 137 | `i`: 138 | Current count. 139 | `total`: 140 | Maximum progress count. 141 | `start_time`: 142 | Time of first display update. (from `time.time()`) 143 | `last_output_time`: 144 | Time of most recent display update. (from `time.time()`) 145 | `time_throttle`: 146 | Minimum time between display updates. 147 | `width`: 148 | Total available width for entire `ProgressBar` line. 149 | `last_output_width`: 150 | Width of last output, minus trailing spaces on the right. 151 | `rate`: 152 | How many counts per second. 153 | `fraction`: 154 | A float of what amount is done. A value between 0 and 1. 155 | `remaining: 156 | How many counts left. 157 | `elapsed`: 158 | Total time elapsed since first count. 159 | `eta`: 160 | ETA (estimated time of arrival) of final count, based on 161 | current count rate. 162 | """ 163 | class Formats(object): 164 | basic = '[%=] %p%%' 165 | fraction = '|%=| %i/%t' 166 | timer = basic + ' %es' 167 | predictive = '(%=) %p%% %es elapsed, ETA %Es' 168 | speed = '[%=] %R/s %p%%' 169 | advanced = '%r/%t remaining [%=] %p%% ETA %Es @ %R/s %es elapsed' 170 | items_remaining = '{%=} %r/%t remaining' 171 | stats_only = '%p%% %r/%t remaining %_ ETA %Es @ %R/s %es elapsed' 172 | 173 | def __init__(self, 174 | total, 175 | width=80, 176 | fmt=Formats.advanced, 177 | time_throttle=0.05, 178 | custom_outputters=None, 179 | custom_expanding_outputters=None 180 | ): 181 | # cache the values calculated during the self.i-th iteration 182 | self.i = 0 183 | self.total = total 184 | 185 | # throttling 186 | self.time_throttle = time_throttle 187 | self.should_output = self.should_output_first 188 | 189 | # time 190 | # clock starts ticking on first update call 191 | self.start_time = None 192 | self.last_output_time = None 193 | 194 | # output string 195 | self.format = fmt 196 | self.width = width 197 | self.last_output_width = 0 198 | self.outputters = { 199 | 't': str(self.total), 200 | 'i': lambda: str(self.i), 201 | '=': self.make_progress_bar, 202 | '_': self.make_expanding_space, 203 | 'p': lambda: self.float_formatter(100 * self.fraction), 204 | 'r': lambda: str(self.remaining), 205 | 'e': lambda: self.float_formatter(self.elapsed), 206 | 'E': lambda: self.float_formatter(self.eta), 207 | 'R': lambda: self.float_formatter(self.rate), 208 | } 209 | if custom_outputters or custom_expanding_outputters: 210 | custom_outputters = custom_outputters or {} 211 | if custom_expanding_outputters: 212 | custom_outputters.update(custom_expanding_outputters) 213 | bound_outputters = {} 214 | for k, v in custom_outputters.items(): 215 | if not callable(v): 216 | # test concatenation early, let it raise if it fails 217 | '' + v 218 | else: 219 | v = partial(v, self) 220 | bound_outputters[k] = v 221 | self.outputters.update(bound_outputters) 222 | 223 | # stuff that fills the rest of the space 224 | self.expanding_outputters = [ 225 | self.make_progress_bar, 226 | self.make_expanding_space, 227 | ] 228 | if custom_expanding_outputters: 229 | self.expanding_outputters.extend(custom_expanding_outputters.items()) 230 | self.tokens = [] 231 | self.update_format(fmt) 232 | 233 | def __call__(self, increment=1): 234 | """ 235 | Update the ProgressBar state. 236 | :param increment: how much to increase self.i 237 | """ 238 | self.i += increment 239 | if self.should_output(): 240 | self.last_output_time = time() 241 | self.write() 242 | 243 | def update_format(self, fmt): 244 | self.format = fmt 245 | self.parse_format(fmt) 246 | 247 | def should_output_first(self): 248 | """ 249 | Initializes throttle control 250 | :return: bool 251 | """ 252 | self.last_output_time = self.start_time = time() 253 | self.should_output = self.should_output_middle 254 | return True 255 | 256 | def should_output_middle(self): 257 | """ 258 | Implements throttle control 259 | :return: bool 260 | """ 261 | systems_go = ( 262 | time() - self.last_output_time > self.time_throttle or 263 | self.i == self.total 264 | ) 265 | return systems_go 266 | 267 | def parse_format(self, fmt): 268 | tokens = self.tokenize(fmt) 269 | self.tokens = self.join_tokens(tokens) 270 | 271 | def tokenize(self, fmt): 272 | """ 273 | Splits format string into tokens. Used by parse_format. 274 | :returns: list of characters and outputter functions 275 | :rtype: list of (str or callable) 276 | """ 277 | output = [] 278 | hot_char = '%' 279 | in_code = [] 280 | for c in fmt: 281 | if not in_code: 282 | if c == hot_char: 283 | in_code.append(c) 284 | else: 285 | output.append(c) 286 | continue 287 | #else: in_code: 288 | if c == hot_char: 289 | output.append(c) 290 | in_code = [] 291 | continue 292 | if c in self.outputters: 293 | func_or_str = self.outputters[c] 294 | output.append(func_or_str) 295 | in_code = [] 296 | continue 297 | 298 | output.append(c) 299 | in_code = [] 300 | 301 | return output 302 | 303 | @staticmethod 304 | def join_tokens(tokens): 305 | """ 306 | Join sequential strings among the tokens. 307 | :param tokens: list of (str or callable) 308 | :return: list of (str or callable) 309 | """ 310 | output = [] 311 | raw = [] 312 | for s in tokens: 313 | if callable(s): 314 | if raw: 315 | raw = ''.join(raw) 316 | output.append(raw) 317 | raw = [] 318 | output.append(s) 319 | continue 320 | raw.append(s) 321 | if raw: 322 | raw = ''.join(raw) 323 | output.append(raw) 324 | return output 325 | 326 | def make_output_string(self): 327 | tokens = self.make_output_string_static(self.tokens) 328 | tokens = self.make_output_string_resized(tokens) 329 | output = ''.join(tokens) 330 | return output 331 | 332 | def make_output_string_static(self, tokens): 333 | output = [] 334 | for token in tokens: 335 | if callable(token) and token not in self.expanding_outputters: 336 | generated = token() 337 | output.append(generated) 338 | else: 339 | output.append(token) 340 | output = self.join_tokens(output) 341 | return output 342 | 343 | def make_output_string_resized(self, tokens): 344 | output = [] 345 | static_size = sum(len(s) for s in tokens if hasattr(s, '__len__')) 346 | remaining_width = self.width - static_size 347 | for token in tokens: 348 | if callable(token): 349 | token = token(remaining_width) 350 | output.append(token) 351 | output = self.join_tokens(output) 352 | return output 353 | 354 | def write(self): 355 | output = self.make_output_string() 356 | underrun = self.last_output_width - len(output) 357 | self.last_output_width = len(output.rstrip()) 358 | output += underrun*' ' # completely erase last line with spaces 359 | 360 | write('\r' + output) 361 | if self.i >= self.total: 362 | print() 363 | 364 | def make_progress_bar(self, size): 365 | frac = self.i / self.total 366 | num_segs = int(frac * size) 367 | 368 | output = ''.join([ 369 | num_segs * '=', 370 | (size - num_segs) * ' ', 371 | ]) 372 | return output 373 | 374 | def make_expanding_space(self, size): 375 | return size*' ' 376 | 377 | @staticmethod 378 | def float_formatter(f): 379 | if f > 10: 380 | return str(int(f)) 381 | if f > 1: 382 | return "%0.1f" % f 383 | return "%0.2f" % f 384 | 385 | @property 386 | def fraction(self): 387 | return self.i / self.total 388 | 389 | @property 390 | def elapsed(self): 391 | return time() - self.start_time 392 | 393 | @property 394 | def rate(self): 395 | if not self.elapsed: 396 | return 0 397 | return self.i / self.elapsed 398 | 399 | @property 400 | def remaining(self): 401 | return self.total - self.i 402 | 403 | @property 404 | def eta(self): 405 | if not self.rate: 406 | return 1 407 | return self.remaining / self.rate 408 | 409 | def __str__(self): 410 | return self.make_output_string() 411 | 412 | 413 | def _slow_test(): 414 | from time import sleep 415 | total = 10 416 | pbar = ProgressBar(total) 417 | for i in xrange(total): 418 | pbar() 419 | sleep(0.5) 420 | 421 | 422 | def _fast_test(): 423 | total = 5 * 10**7 424 | pbar = ProgressBar( 425 | total, 426 | fmt=ProgressBar.Formats.stats_only, 427 | ) 428 | for i in xrange(total): 429 | pbar() 430 | 431 | 432 | def _profile(): 433 | import cProfile 434 | cProfile.run('_fast_test()', sort='cumulative') 435 | 436 | if __name__ == "__main__": 437 | # _slow_test() 438 | _profile() 439 | --------------------------------------------------------------------------------