├── .rspec ├── Gemfile ├── lib ├── interval_tree │ └── version.rb └── interval_tree.rb ├── .document ├── spec ├── spec_helper.rb └── interval_tree_spec.rb ├── Rakefile ├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── LICENSE ├── fast_interval_tree.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | 2 | source 'https://rubygems.org' 3 | 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/interval_tree/version.rb: -------------------------------------------------------------------------------- 1 | module IntervalTree 2 | VERSION = "0.2.2" 3 | end 4 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'interval_tree' 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | ruby-version: ['2.6', '2.7', '3.0'] 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Ruby 12 | uses: ruby/setup-ruby@v1 13 | with: 14 | ruby-version: ${{ matrix.ruby-version }} 15 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 16 | - name: Run tests 17 | run: bundle exec rake 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## RubyMine 5 | .idea 6 | 7 | ## TEXTMATE 8 | *.tmproj 9 | tmtags 10 | 11 | ## EMACS 12 | *~ 13 | \#* 14 | .\#* 15 | 16 | ## VIM 17 | *.swp 18 | 19 | ## PROJECT::GENERAL 20 | coverage 21 | rdoc 22 | pkg 23 | 24 | # for a library or gem, you might want to ignore these files since the code is 25 | # intended to run in multiple environments; otherwise, check them in: 26 | Gemfile.lock 27 | .ruby-version 28 | .ruby-gemset 29 | 30 | ## PROJECT::SPECIFIC 31 | 32 | ## RubyGems 33 | 34 | fast_interval_tree-*.gem 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2017 MISHIMA, Hiroyuki; Simeon Simeonov; Carlos Alonsol; Sam Davies 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /fast_interval_tree.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | require_relative 'lib/interval_tree/version' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = 'fast_interval_tree' 9 | s.version = IntervalTree::VERSION 10 | s.summary = 'A Ruby implementation of the Centered Interval Tree data structure' 11 | s.description = <<-END.gsub(/^ {4}/, '') 12 | A Ruby implementation of the Centered Interval Tree data structure. 13 | See also: http://en.wikipedia.org/wiki/Interval_tree 14 | END 15 | s.authors = ['Hiroyuki Mishima', 'Simeon Simeonov', 'Carlos Alonso', 'Sam Davies', 'Brendan Weibrecht', 'Chris Nankervis', 'Thomas van der Pol'] 16 | s.email = ['brendan@weibrecht.net.au'] 17 | s.files = ['lib/interval_tree.rb'] 18 | s.homepage = 'https://github.com/greensync/interval-tree' 19 | s.metadata['source_code_uri'] = 'https://github.com/greensync/interval-tree' 20 | s.license = 'MIT' 21 | 22 | # Specify which files should be added to the gem when it is released. 23 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 24 | s.files = Dir.chdir(File.expand_path(__dir__)) do 25 | Dir.glob('{bin,lib}/**/*') + %w[README.md Gemfile Rakefile fast_interval_tree.gemspec] 26 | end 27 | s.require_paths = ['lib'] 28 | 29 | s.add_development_dependency 'rspec', '~> 3.10' 30 | s.add_development_dependency 'rake', '~> 13.0.1' 31 | end 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IntervalTree 2 | 3 | ![Tests](https://github.com/greensync/interval-tree/actions/workflows/.github/workflows/ruby.yml/badge.svg) 4 | 5 | An implementation of the centered interval tree algorithm in Ruby. 6 | 7 | ## See also 8 | 9 | * [A description of the algorithm in Wikipedia](http://en.wikipedia.org/wiki/Interval_tree) 10 | 11 | ## Maintainers Wanted! 12 | 13 | We are not currently using `interval-tree` at GreenSync; however, we recognize that it may still be useful to some. With our efforts currently focused elsewhere, we are seeking a new maintainer(s) to be the primary maintainer for the project. Please get in touch with @maddymarkovitz or @ZimbiX if you are interested. 14 | 15 | ## ChangeLog 16 | 17 | ### 2020-11-09, contribution by [Brendan Weibrecht](https://github.com/ZimbiX), [Chris Nankervis](https://github.com/chrisnankervis) and [Thomas van der Pol](https://github.com/tvanderpol) 18 | 19 | * Substantially improved performance when supplied with a large range as the search query. 20 | 21 | ### 2017-05-12, contribution by [Sam Davies](https://github.com/samphilipd) 22 | 23 | * User can specify an option in search `unique: false` if s/he wants multiple matches to be returned. 24 | 25 | ### 2015-11-02, contribution by [Carlos Alonso](https://github.com/calonso) 26 | 27 | * Improved centering 28 | * Fixed searching: With some use cases with very large trees, the library fails to find intervals. 29 | * Added rubygems structure to be able to be pushed as a gem 30 | 31 | ### 2013-04-06, contribution by [Simeon Simeonov](https://github.com/ssimeonov) 32 | 33 | * **Range factory**: The current design allows for Range-compatible elements to be added except for the case where `Tree#ensure_exclusive_end` constructs a Range in a private method. In keeping with good design practices of containers such as Hash, this pull requests allows for a custom range factory to be provided to `Tree#initialize` while maintaining perfect backward compatibility. 34 | Search in empty trees failing 35 | * Adds a nil guard in `Tree#search` to protect against empty tree searches failing. 36 | * **Cosmetic improvements**: Language & whitespace in specs, Gemfile addition, and .gitignore update 37 | 38 | ## Install 39 | 40 | ```bash 41 | $ gem install fast_interval_tree 42 | ``` 43 | 44 | ## Usage 45 | 46 | See spec/interval_tree_spec.rb for details. 47 | 48 | ```ruby 49 | require "interval_tree" 50 | 51 | itv = [(0...3), (1...4), (3...5), (0...3)] 52 | t = IntervalTree::Tree.new(itv) 53 | p t.search(2) #=> [0...3, 1...4] 54 | p t.search(2, unique: false) #=> [0...3, 0...3, 1...4] 55 | p t.search(1...4) #=> [0...3, 1...4, 3...5] 56 | ``` 57 | 58 | ## Note 59 | 60 | Result intervals are always returned 61 | in the "left-closed and right-open" style that can be expressed 62 | by three-dotted Range object literals `(first...last)` 63 | 64 | Two-dotted full-closed intervals `(first..last)` are also accepted and internally 65 | converted to half-closed intervals. 66 | 67 | ## Copyright 68 | 69 | **Author**: MISHIMA, Hiroyuki ( https://github.com/misshie ), Simeon Simeonov ( https://github.com/ssimeonov ), Carlos Alonso ( https://github.com/calonso ), Sam Davies ( https://github.com/samphilipd ), Brendan Weibrecht (https://github.com/ZimbiX), Chris Nankervis (https://github.com/chrisnankervis), Thomas van der Pol (https://github.com/tvanderpol). 70 | 71 | **Copyright**: (c) 2011-2020 MISHIMA, Hiroyuki; Simeon Simeonov; Carlos Alonsol; Sam Davies; Brendan Weibrecht; Chris Nankervis; Thomas van der Pol 72 | 73 | **License**: The MIT/X11 license 74 | -------------------------------------------------------------------------------- /lib/interval_tree.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | module IntervalTree 4 | 5 | class Tree 6 | def initialize(ranges, &range_factory) 7 | range_factory = lambda { |l, r| (l ... r+1) } unless block_given? 8 | ranges_excl = ensure_exclusive_end([ranges].flatten, range_factory) 9 | @top_node = divide_intervals(ranges_excl) 10 | end 11 | attr_reader :top_node 12 | 13 | def divide_intervals(intervals) 14 | return nil if intervals.empty? 15 | x_center = center(intervals) 16 | s_center = Array.new 17 | s_left = Array.new 18 | s_right = Array.new 19 | 20 | intervals.each do |k| 21 | case 22 | when k.last.to_r < x_center 23 | s_left << k 24 | when k.first.to_r > x_center 25 | s_right << k 26 | else 27 | s_center << k 28 | end 29 | end 30 | Node.new(x_center, s_center, 31 | divide_intervals(s_left), divide_intervals(s_right)) 32 | end 33 | 34 | # Search by range or point 35 | DEFAULT_OPTIONS = {unique: true} 36 | def search(query, options = {}) 37 | options = DEFAULT_OPTIONS.merge(options) 38 | 39 | return nil unless @top_node 40 | 41 | if query.respond_to?(:first) 42 | result = top_node.search(query) 43 | options[:unique] ? result.uniq : result 44 | else 45 | point_search(self.top_node, query, [], options[:unique]) 46 | end 47 | .sort_by{|x|[x.first, x.last]} 48 | end 49 | 50 | def ==(other) 51 | top_node == other.top_node 52 | end 53 | 54 | private 55 | 56 | def ensure_exclusive_end(ranges, range_factory) 57 | ranges.map do |range| 58 | case 59 | when !range.respond_to?(:exclude_end?) 60 | range 61 | when range.exclude_end? 62 | range 63 | else 64 | range_factory.call(range.first, range.end) 65 | end 66 | end 67 | end 68 | 69 | def center(intervals) 70 | ( 71 | intervals.map(&:begin).min.to_r + 72 | intervals.map(&:end).max.to_r 73 | ) / 2 74 | end 75 | 76 | def point_search(node, point, result, unique = true) 77 | node.s_center.each do |k| 78 | if k.include?(point) 79 | result << k 80 | end 81 | end 82 | if node.left_node && ( point.to_r < node.x_center ) 83 | point_search(node.left_node, point, []).each{|k|result << k} 84 | end 85 | if node.right_node && ( point.to_r >= node.x_center ) 86 | point_search(node.right_node, point, []).each{|k|result << k} 87 | end 88 | if unique 89 | result.uniq 90 | else 91 | result 92 | end 93 | end 94 | end # class Tree 95 | 96 | class Node 97 | def initialize(x_center, s_center, left_node, right_node) 98 | @x_center = x_center 99 | @s_center = s_center 100 | @left_node = left_node 101 | @right_node = right_node 102 | end 103 | attr_reader :x_center, :s_center, :left_node, :right_node 104 | 105 | def ==(other) 106 | x_center == other.x_center && 107 | s_center == other.s_center && 108 | left_node == other.left_node && 109 | right_node == other.right_node 110 | end 111 | 112 | # Search by range only 113 | def search(query) 114 | search_s_center(query) + 115 | (left_node && query.begin.to_r < x_center && left_node.search(query) || []) + 116 | (right_node && query.end.to_r > x_center && right_node.search(query) || []) 117 | end 118 | 119 | private 120 | 121 | def search_s_center(query) 122 | s_center.select do |k| 123 | ( 124 | # k is entirely contained within the query 125 | (k.begin >= query.begin) && 126 | (k.end <= query.end) 127 | ) || ( 128 | # k's start overlaps with the query 129 | (k.begin >= query.begin) && 130 | (k.begin < query.end) 131 | ) || ( 132 | # k's end overlaps with the query 133 | (k.end > query.begin) && 134 | (k.end <= query.end) 135 | ) || ( 136 | # k is bigger than the query 137 | (k.begin < query.begin) && 138 | (k.end > query.end) 139 | ) 140 | end 141 | end 142 | end # class Node 143 | 144 | end # module IntervalTree 145 | -------------------------------------------------------------------------------- /spec/interval_tree_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "IntervalTree::Node" do 4 | describe '.new' do 5 | context 'given ([], [], [], [])' do 6 | it 'returns a Node object' do 7 | expect(IntervalTree::Node.new([], [], [], [])).to be_a(IntervalTree::Node) 8 | end 9 | end 10 | end 11 | 12 | describe '#search' do 13 | subject(:result) { node.search(-5...3) } 14 | 15 | let(:node) do 16 | IntervalTree::Tree.new( 17 | [ 18 | 10...14, 19 | 2...20, 20 | 0...5, 21 | 0...8, 22 | 3...6, 23 | 15...20, 24 | 16...21, 25 | 17...25, 26 | 21...24, 27 | ], 28 | ).top_node 29 | end 30 | 31 | before do 32 | allow(node.left_node).to receive(:search).and_call_original 33 | allow(node.right_node).to receive(:search).and_call_original 34 | result 35 | end 36 | 37 | it 'returns the matching ranges' do 38 | expect(result).to eq( 39 | [ 40 | 2...20, 41 | 0...5, 42 | 0...8, 43 | ] 44 | ) 45 | end 46 | 47 | context 'only searches the necessary nodes' do 48 | it 'searches the left node' do 49 | expect(node.left_node).to have_received(:search) 50 | end 51 | 52 | it "does not search the right node, since the top node's center (12) is greater than the search's end (3)" do 53 | expect(node.right_node).not_to have_received(:search) 54 | end 55 | end 56 | end 57 | end 58 | 59 | describe "IntervalTree::Tree" do 60 | describe '#center' do 61 | context 'given [(1...5),]' do 62 | it 'returns 3' do 63 | itvs = [(1...5),] 64 | t = IntervalTree::Tree.new([]) 65 | expect(t.__send__(:center, itvs)).to be == 3.0 66 | 67 | end 68 | end 69 | 70 | context 'given [(1...5), (2...6)]' do 71 | it 'returns 3' do 72 | itvs = [(1...5), (2...6),] 73 | t = IntervalTree::Tree.new([]) 74 | expect(t.__send__(:center, itvs)).to be == 3.5 75 | end 76 | end 77 | end 78 | 79 | describe '.new' do 80 | context 'given [(1...5)]' do 81 | it 'returns a Tree' do 82 | itvs = [(1...5)] 83 | expect(IntervalTree::Tree.new(itvs)).to be_an IntervalTree::Tree 84 | end 85 | end 86 | 87 | context 'given [(1...5),(2...6), (3...7)]' do 88 | it 'returns ret.top_node.x_centeran == 4' do 89 | itvs = [(1...5), (2...6), (3...7)] 90 | tree = IntervalTree::Tree.new(itvs) 91 | expect(tree.top_node.x_center).to be == 4.0 92 | end 93 | end 94 | 95 | context 'given [(1..5),(2..6), (3..7)]' do 96 | it 'returns ret.top_node.x_centeran == 4 ' do 97 | itvs = [(1..5), (2..6), (3..7)] 98 | tree = IntervalTree::Tree.new(itvs) 99 | expect(tree.top_node.x_center).to be == 4.5 100 | end 101 | end 102 | 103 | context 'with a custom range factory' do 104 | class ValueRange < Range 105 | attr_accessor :value 106 | def initialize(l, r, value = nil) 107 | super(l, r, true) 108 | @value = value 109 | end 110 | end 111 | context 'given [(1..5)] and a ValueRange factory block' do 112 | it 'constructs a range with a value' do 113 | itvs = [(1..5)] 114 | tree = IntervalTree::Tree.new(itvs) { |l, r| ValueRange.new(l, r, 15) } 115 | result = tree.search(2).first 116 | expect(result).to be_kind_of ValueRange 117 | expect(result.value).to be == 15 118 | end 119 | end 120 | end 121 | 122 | describe 'dividing intervals' do 123 | subject(:tree) do 124 | IntervalTree::Tree.new( 125 | [ 126 | 10...14, 127 | 2...20, 128 | 0...5, 129 | 0...8, 130 | 3...6, 131 | 15...20, 132 | 16...21, 133 | 17...25, 134 | 21...24, 135 | ], 136 | ) 137 | end 138 | 139 | let(:node) do 140 | IntervalTree::Node.new( 141 | 12.5, # x_center 142 | [ 10...14, 2...20 ], # s_center 143 | left_node, # s_left 144 | right_node, # s_right 145 | ) 146 | end 147 | let(:left_node) do 148 | IntervalTree::Node.new( 149 | 4, # x_center 150 | [ 0...5, 0...8, 3...6 ], # s_center 151 | nil, # s_left 152 | nil, # s_right 153 | ) 154 | end 155 | let(:right_node) do 156 | IntervalTree::Node.new( 157 | 20, # x_center 158 | [ 15...20, 16...21, 17...25 ], # s_center 159 | nil, # s_left 160 | right_node_of_right_node, # s_right 161 | ) 162 | end 163 | let(:right_node_of_right_node) do 164 | IntervalTree::Node.new( 165 | 22.5, # x_center 166 | [ 21...24 ], # s_center 167 | nil, # s_left 168 | nil, # s_right 169 | ) 170 | end 171 | 172 | context 'given a set of intervals' do 173 | it 'divides everything correctly' do 174 | expect(tree.top_node).to eq(node) 175 | end 176 | 177 | it 'divides into a left node correctly' do 178 | expect(tree.top_node.left_node).to eq(left_node) 179 | end 180 | 181 | it 'divides into a right node correctly' do 182 | expect(tree.top_node.right_node).to eq(right_node) 183 | end 184 | 185 | it 'divides the right node into a right node correctly' do 186 | expect(tree.top_node.right_node.right_node).to eq(right_node_of_right_node) 187 | end 188 | end 189 | end 190 | end 191 | 192 | describe '#search' do 193 | context 'given []' do 194 | it 'returns nil for all searches' do 195 | itvs = [] 196 | IntervalTree::Tree.new(itvs).tap do |tree| 197 | expect(tree.search(5)).to be_nil 198 | expect(tree.search(1..2)).to be_nil 199 | expect(tree.search(1...2)).to be_nil 200 | end 201 | end 202 | end 203 | 204 | context 'given [(1...5)] and a point query "3"' do 205 | it 'returns an array of intervals (1...5)]' do 206 | expect(IntervalTree::Tree.new([1...5]).search(3)).to be == [1...5] 207 | end 208 | 209 | it 'returns an empty array in the right end corner case' do 210 | expect(IntervalTree::Tree.new([1...5]).search(5)).to be == [] 211 | end 212 | 213 | it 'returns the range in the left end corner case' do 214 | expect(IntervalTree::Tree.new([1...5]).search(1)).to be == [1...5] 215 | end 216 | end 217 | 218 | context 'given non-array full-closed "(1..4)" and a point query "3"' do 219 | it 'returns an array contains a half-open interval (1...5)]' do 220 | expect(IntervalTree::Tree.new(1..4).search(4)).to be == [1...5] 221 | end 222 | 223 | it 'returns an empty array in the right end corner case' do 224 | expect(IntervalTree::Tree.new(1..4).search(5)).to be == [] 225 | end 226 | 227 | it 'returns the range in the left end corner case' do 228 | expect(IntervalTree::Tree.new(1..4).search(1)).to be == [1...5] 229 | end 230 | end 231 | 232 | context 'given [(1...5), (2...6)] and a point query "3"' do 233 | it 'returns [(1...5), (2...6)]' do 234 | itvs = [(1...5), (2...6),] 235 | results = IntervalTree::Tree.new(itvs).search(3) 236 | expect(results).to be == itvs 237 | end 238 | end 239 | 240 | context 'given [(0...8), (1...5), (2...6)] and a point query "3"' do 241 | it 'returns [(0...8), (1...5), (2...6)]' do 242 | itvs = [(0...8), (1...5), (2...6)] 243 | results = IntervalTree::Tree.new(itvs).search(3) 244 | expect(results).to be == itvs 245 | end 246 | end 247 | 248 | context 'given [(0...8), (1...5), (2...6)] and a query by (1...4)' do 249 | it 'returns [(0...8), (1...5), (2...6)]' do 250 | itvs = [(0...8), (1...5), (2...6)] 251 | results = IntervalTree::Tree.new(itvs).search(1...4) 252 | expect(results).to be == itvs 253 | end 254 | end 255 | 256 | context 'given [(1...3), (3...5)] and a query by (3...9)' do 257 | it 'returns [(3...5)]' do 258 | results = IntervalTree::Tree.new([(1...3), (3...5)]).search(3...9) 259 | expect(results).to be == [(3...5)] 260 | end 261 | end 262 | 263 | context 'given [(1...3), (3...5), (4...8)] and a query by (3...5)' do 264 | it 'returns [(3...5), (4...8)]' do 265 | itvs = [(1...3), (3...5), (4...8)] 266 | results = IntervalTree::Tree.new(itvs).search(3...5) 267 | expect(results).to be == [(3...5), (4...8)] 268 | end 269 | end 270 | 271 | context 'given [(1...3), (3...5), (3...9), (4...8)] and a query by (3...5)' do 272 | it 'returns [(3...5), (3...9), (4...8)]' do 273 | itvs = [(1...3), (3...5), (3...9), (4...8)] 274 | results = IntervalTree::Tree.new(itvs).search(3...5) 275 | expect(results).to be == [(3...5), (3...9), (4...8)] 276 | end 277 | end 278 | 279 | context 'with unique defaulting to true' do 280 | context 'given intervals with duplicates' do 281 | it 'returns the duplicates in the result' do 282 | itv = [(0...3), (1...4), (3...5), (0...3)] 283 | t = IntervalTree::Tree.new(itv) 284 | results = t.search(2) 285 | expect(results).to match_array([0...3, 1...4]) 286 | end 287 | end 288 | end 289 | 290 | context 'with unique: false' do 291 | context 'given [(1...3), (1...3), (2...4), (1...3)] and a query by (1)' do 292 | it 'returns [(1...3), (1...3), (1...3)]' do 293 | itvs = [(1...3), (1...3), (2...4), (1...3)] 294 | results = IntervalTree::Tree.new(itvs).search(1, unique: false) 295 | expect(results).to match_array([(1...3), (1...3), (1...3)]) 296 | end 297 | end 298 | 299 | context 'given [(1...3), (1...3), (2...4), (1...3)] and a query by (3)' do 300 | it 'returns [(2..4)]' do 301 | itvs = [(1...3), (1...3), (2...4), (1...3)] 302 | results = IntervalTree::Tree.new(itvs).search(3, unique: false) 303 | expect(results).to match_array([(2...4)]) 304 | end 305 | end 306 | end 307 | 308 | context "when concerned with performance" do 309 | context "with a small search space of very large ranges" do 310 | it "should still be fast" do 311 | itvs = [1...10_000_000] 312 | needle = 5_000_001...15_000_000 313 | 314 | a = Time.now 315 | results = IntervalTree::Tree.new(itvs).search(needle) 316 | b = Time.now 317 | 318 | expect(b - a).to be_within(0.05).of(0) 319 | end 320 | end 321 | end 322 | 323 | context 'when using time ranges' do 324 | it 'still works' do 325 | itvs = [Time.utc(2020, 11, 1)...Time.utc(2020, 11, 20)] 326 | needle = Time.utc(2020, 11, 5)...Time.utc(2020, 11, 6) 327 | results = IntervalTree::Tree.new(itvs).search(needle) 328 | expect(results).to eq(itvs) 329 | end 330 | end 331 | end 332 | end 333 | --------------------------------------------------------------------------------