From fb69cff9c4390e21602b46482861efae7478fac5 Mon Sep 17 00:00:00 2001 From: IMGillusion Date: Wed, 9 Sep 2026 12:07:42 +0800 Subject: [PATCH] fix(overlaps): scan only query-range boundary points (#127) overlaps_range() tested every boundary point in the tree (O(n) pure-python comparisons per query) instead of just the points inside (begin, end). Restrict the scan to the bisect slice, mirroring the indexing overlap() already uses. Unfixed: ~4s for 200 queries on a 300k-interval tree; fixed: ~0.01s. --- CHANGELOG.md | 2 ++ intervaltree/intervaltree.py | 10 ++++++++-- test/issues/issue127_test.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 test/issues/issue127_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a71113f..720b7f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Better subclassing support: Determine classes dynamically, so that methods like str() are aware when our types are subclassed. - Removed no-op check and extra list allocation during chop() + - overlaps() on a range no longer scans the entire boundary table; + only boundary points inside the queried range are tested (#127) - Maintainers: - Load issue4 test data via JSON to save on compilation and collection time. diff --git a/intervaltree/intervaltree.py b/intervaltree/intervaltree.py index e273127..8e39b0a 100644 --- a/intervaltree/intervaltree.py +++ b/intervaltree/intervaltree.py @@ -619,10 +619,16 @@ def overlaps_range(self, begin, end): return False elif self.overlaps_point(begin): return True + # only test boundary points inside (begin, end), not the whole + # boundary table (#127); boundaries exactly at begin are already + # covered by the overlaps_point(begin) check above + boundary_table = self.boundary_table + bound_begin = boundary_table.bisect_left(begin) + bound_end = boundary_table.bisect_left(end) # up to, but not including end return any( self.overlaps_point(bound) - for bound in self.boundary_table - if begin < bound < end + for index in xrange(bound_begin, bound_end) + for bound in (boundary_table.keys()[index],) ) def split_overlaps(self): diff --git a/test/issues/issue127_test.py b/test/issues/issue127_test.py new file mode 100644 index 0000000..e8dc063 --- /dev/null +++ b/test/issues/issue127_test.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" +Regression test for issue #127: overlaps(begin, end) on a range scanned +every boundary point in the tree (O(n) pure-python comparisons per query) +instead of only the points inside (begin, end). + +300k disjoint intervals -> 600k boundary points. A miss-case query in the +unfixed code takes ~4s for 200 queries; with the boundary scan restricted +to the query range it should be under a tenth of a second. +""" +import time + +from intervaltree import IntervalTree + + +def test_overlaps_range_scans_only_query_range(): + tree = IntervalTree.from_tuples((i * 10, i * 10 + 4) for i in range(300000)) + + # sanity: semantics must be unchanged + assert tree.overlaps(500006, 500009) is False # range inside a pure gap + assert tree.overlaps(500006, 500012) is True # range reaches (500010, 500014) + + t0 = time.time() + for _ in range(200): + tree.overlaps(500006, 500009) + # unfixed: ~4s (600k boundary comparisons per query) + # fixed: ~0.01s (only the 0-2 boundaries inside the range) + assert time.time() - t0 < 1.0