├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .rspec ├── .ruby-version ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── benchmark ├── benchmark.rb └── profile.rb ├── lib ├── segment_tree.rb └── segment_tree │ └── version.rb ├── segment_tree.gemspec └── spec ├── segment_tree_spec.rb └── spec_helper.rb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby 9 | 10 | on: 11 | push: 12 | branches: [ "master" ] 13 | pull_request: 14 | branches: [ "master" ] 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | test: 21 | 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | ruby-version: ['2.7', '3.0', '3.1', '3.2'] 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Set up Ruby 30 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 31 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 32 | # uses: ruby/setup-ruby@v1 33 | uses: ruby/setup-ruby@ee2113536afb7f793eed4ce60e8d3b26db912da4 # v1.127.0 34 | with: 35 | ruby-version: ${{ matrix.ruby-version }} 36 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 37 | - name: Run tests 38 | run: bundle exec rake spec 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .rbx/ 19 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.0.4 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in segment_tree.gemspec 4 | gemspec 5 | 6 | gem 'simplecov' 7 | 8 | gem 'ruby-prof' if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Alexei Mikhailov 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SegmentTree [![Build Status](https://github.com/take-five/segment_tree/actions/workflows/ruby.yml/badge.svg?branch=master)](https://github.com/take-five/segment_tree/actions/workflows/ruby.yml) 2 | 3 | Ruby implementation of [segment tree](http://en.wikipedia.org/wiki/Segment_tree) data structure. 4 | Segment tree is a tree data structure for storing intervals, or segments. It allows querying which of the stored segments contain a given point. It is, in principle, a static structure; that is, its content cannot be modified once the structure is built. 5 | 6 | Segment tree storage has the complexity of O(n). 7 | Segment tree querying has the complexity of O(log n). 8 | 9 | It's pretty fast on querying trees with ~ 10 millions segments, though building of such big tree will take long. 10 | Internally it is not a tree - it is just a sorted array, and querying the tree is just a simple binary search (it was implemented as real tree in versions before 0.1.0, but these trees consumed a lot of memory). 11 | 12 | ## Installation 13 | 14 | Add this line to your application's Gemfile: 15 | 16 | gem 'segment_tree' 17 | 18 | And then execute: 19 | 20 | $ bundle 21 | 22 | Or install it yourself as: 23 | 24 | $ gem install segment_tree 25 | 26 | ## Usage 27 | 28 | Segment tree consists of segments (in Ruby it's Range objects) and corresponding values. The easiest way to build a segment tree is to create it from hash where segments are keys: 29 | ```ruby 30 | tree = SegmentTree.new(1..10 => "a", 11..20 => "b", 21..30 => "c") # => #> 31 | ``` 32 | 33 | After that you can query the tree of which segment contains a given point: 34 | ```ruby 35 | tree.find(5) # => # 36 | ``` 37 | 38 | ## Real world example 39 | 40 | Segment tree can be used in applications where IP-address geocoding is needed. 41 | 42 | ```ruby 43 | data = [ 44 | [IPAddr.new('87.224.241.0/24').to_range, {:city => "YEKT"}], 45 | [IPAddr.new('195.58.18.0/24').to_range, {:city => "MSK"}] 46 | # and so on 47 | ] 48 | ip_tree = SegmentTree.new(data) 49 | 50 | client_ip = IPAddr.new("87.224.241.66") 51 | ip_tree.find(client_ip).value # => {:city=>"YEKT"} 52 | ``` 53 | 54 | ## Some benchmarks 55 | ``` 56 | Building a tree of N intervals 57 | 58 | user system total real 59 | 100 0.000000 0.000000 0.000000 ( 0.000143) 60 | 1000 0.000000 0.000000 0.000000 ( 0.001094) 61 | 10000 0.010000 0.000000 0.010000 ( 0.011446) 62 | 100000 0.110000 0.000000 0.110000 ( 0.115025) 63 | 1000000 1.390000 0.000000 1.390000 ( 1.387665) 64 | 65 | Finding matching interval in tree of N intervals 66 | 67 | user system total real 68 | 100 0.000000 0.000000 0.000000 ( 0.000030) 69 | 1000 0.000000 0.000000 0.000000 ( 0.000017) 70 | 10000 0.000000 0.000000 0.000000 ( 0.000025) 71 | 100000 0.000000 0.000000 0.000000 ( 0.000033) 72 | 1000000 0.000000 0.000000 0.000000 ( 0.000028) 73 | 74 | Finding matching interval in list of N intervals using Array.find 75 | 76 | user system total real 77 | 100 0.000000 0.000000 0.000000 ( 0.000055) 78 | 1000 0.000000 0.000000 0.000000 ( 0.000401) 79 | 10000 0.010000 0.000000 0.010000 ( 0.003971) 80 | 100000 0.010000 0.000000 0.010000 ( 0.003029) 81 | 1000000 0.040000 0.000000 0.040000 ( 0.038484) 82 | ``` 83 | 84 | ## Contributing 85 | 86 | 1. Fork it 87 | 2. Create your feature branch (`git checkout -b my-new-feature`) 88 | 3. Commit your changes (`git commit -am 'Added some feature'`) 89 | 4. Push to the branch (`git push origin my-new-feature`) 90 | 5. Create new Pull Request 91 | 92 | ## TODO 93 | 1. Fix README typos and grammatical errors (english speaking contributors are welcomed) 94 | 2. Implement C binding for MRI. 95 | 96 | ## LICENSE 97 | Copyright (c) 2012 Alexei Mikhailov 98 | 99 | MIT License 100 | 101 | Permission is hereby granted, free of charge, to any person obtaining 102 | a copy of this software and associated documentation files (the 103 | "Software"), to deal in the Software without restriction, including 104 | without limitation the rights to use, copy, modify, merge, publish, 105 | distribute, sublicense, and/or sell copies of the Software, and to 106 | permit persons to whom the Software is furnished to do so, subject to 107 | the following conditions: 108 | 109 | The above copyright notice and this permission notice shall be 110 | included in all copies or substantial portions of the Software. 111 | 112 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 113 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 114 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 115 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 116 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 117 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 118 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 119 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | require "rspec/core/rake_task" 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | -------------------------------------------------------------------------------- /benchmark/benchmark.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | require "benchmark" 5 | require "segment_tree" 6 | 7 | # generate a tree with +n+ number of intervals 8 | def tree(n) 9 | SegmentTree.new list(n) 10 | end 11 | def list(n) 12 | (0..n).map { |num| [num * 10..(num + 1) * 10 - 1, num] } 13 | end 14 | 15 | puts "Pregenerating data..." 16 | tests = [100, 1000, 10_000, 100_000, 1_000_000] 17 | 18 | lists = Hash[tests.map { |n| [n, list(n)] }] 19 | trees = Hash[tests.map { |n| [n, tree(n)] }] 20 | 21 | puts "Done" 22 | puts 23 | 24 | puts "Building a tree of N intervals" 25 | Benchmark.bmbm do |x| 26 | tests.each do |n| 27 | x.report(n.to_s) { tree(n) } 28 | end 29 | end 30 | 31 | puts 32 | puts "Finding matching interval in tree of N intervals" 33 | Benchmark.bmbm do |x| 34 | tests.each do |n| 35 | t = trees[n] 36 | 37 | x.report(n.to_s) { t.find(rand(n)) } 38 | end 39 | end 40 | 41 | puts 42 | puts "Finding matching interval in list of N intervals" 43 | Benchmark.bmbm do |x| 44 | tests.each do |n| 45 | data = lists[n] 46 | 47 | x.report(n.to_s) { data.find { |range, _| range.include?(rand(n)) } } 48 | end 49 | end -------------------------------------------------------------------------------- /benchmark/profile.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | require "segment_tree" 5 | require 'ruby-prof' 6 | 7 | # generate a tree with +n+ number of intervals 8 | def tree(n) 9 | SegmentTree.new list(n) 10 | end 11 | def list(n) 12 | (0..n).map { |num| [(num * 10)..(num + 1) * 10 - 1, num] } 13 | end 14 | 15 | t = tree(100_000) 16 | n = rand(100_000) 17 | 18 | RubyProf.start 19 | t.find(n) 20 | result = RubyProf.stop 21 | 22 | # Print a flat profile to text 23 | printer = RubyProf::FlatPrinter.new(result) 24 | printer.print(STDOUT) -------------------------------------------------------------------------------- /lib/segment_tree.rb: -------------------------------------------------------------------------------- 1 | # == Synopsys 2 | # Segment tree is a tree data structure for storing intervals, or segments. 3 | # It allows querying which of the stored segments contain a given point. 4 | # It is, in principle, a static structure; that is, its content cannot be modified once the structure is built. 5 | # 6 | # == Example 7 | # data = [ 8 | # [IPAddr.new('87.224.241.0/24').to_range, {:city => "YEKT"}], 9 | # [IPAddr.new('195.58.18.0/24').to_range, {:city => "MSK"}] 10 | # # and so on 11 | # ] 12 | # ip_tree = SegmentTree.new(data) 13 | # 14 | # client_ip = IPAddr.new("87.224.241.66") 15 | # ip_tree.find(client_ip).value # => {:city=>"YEKT"} 16 | class SegmentTree 17 | # An elementary interval 18 | class Segment #:nodoc:all: 19 | attr_reader :range, :value 20 | 21 | def initialize(range, value) 22 | raise ArgumentError, 'Range expected, %s given' % range.class.name unless range.is_a?(Range) 23 | 24 | @range, @value = range, value 25 | end 26 | 27 | # segments are sorted from left to right, from shortest to longest 28 | def <=>(other) 29 | case cmp = @range.begin <=> other.range.begin 30 | when 0 then @range.end <=> other.range.end 31 | else cmp 32 | end 33 | end 34 | 35 | def ==(other) 36 | other.is_a?(self.class) && 37 | @range == other.range && 38 | @value == other.value 39 | end 40 | 41 | def eql?(other) 42 | other.is_a?(self.class) && 43 | @range.eql?(other.range) && 44 | @value.eql?(other.value) 45 | end 46 | 47 | def hash 48 | [@range, @value].hash 49 | end 50 | 51 | def marshal_dump 52 | { 53 | range: @range, 54 | value: @value, 55 | } 56 | end 57 | 58 | def marshal_load(serialized_tree) 59 | @range = serialized_tree[:range] 60 | @value = serialized_tree[:value] 61 | end 62 | end 63 | 64 | attr_reader :segments 65 | 66 | # Build a segment tree from +data+. 67 | # 68 | # Data can be one of the following: 69 | # 1. Hash - a hash, where ranges are keys, 70 | # i.e. {(0..3) => some_value1, (4..6) => some_value2, ...} 71 | # 2. 2-dimensional array - an array of arrays where first element of 72 | # each element is range, and second is value: 73 | # [[(0..3), some_value1], [(4..6), some_value2] ...] 74 | # 75 | # You can pass optional argument +sorted+. 76 | # If +sorted+ is true then tree consider that data already sorted in proper order. 77 | # Use it at your own risk! 78 | def initialize(data, sorted = false) 79 | # build elementary segments 80 | @segments = case data 81 | when Hash, Array, Enumerable then 82 | data.collect { |range, value| Segment.new(range, value) } 83 | else raise ArgumentError, '2-dim Array or Hash expected' 84 | end 85 | 86 | @segments.sort! unless sorted 87 | end 88 | 89 | # Find first interval containing point +x+. 90 | # @return [Segment|NilClass] 91 | def find(x) 92 | return nil if x.nil? 93 | low = 0 94 | high = @segments.size - 1 95 | while low <= high 96 | mid = (low + high) / 2 97 | 98 | case matches?(x, low, high, mid) 99 | when -1 then high = mid - 1 100 | when 1 then low = mid + 1 101 | when 0 then return @segments[mid] 102 | else return nil 103 | end 104 | end 105 | nil 106 | end 107 | 108 | def inspect 109 | if @segments.size > 0 110 | "SegmentTree(#{@segments.first.range.begin}..#{@segments.last.range.end})" 111 | else 112 | "SegmentTree(empty)" 113 | end 114 | end 115 | 116 | def ==(other) 117 | other.is_a?(self.class) && @segments == other.segments 118 | end 119 | 120 | def eql?(other) 121 | other.is_a?(self.class) && @segments.eql?(other.segments) 122 | end 123 | 124 | def hash 125 | @segments.hash 126 | end 127 | 128 | def marshal_dump 129 | { 130 | segments: @segments, 131 | } 132 | end 133 | 134 | def marshal_load(serialized_tree) 135 | @segments = serialized_tree[:segments] 136 | end 137 | 138 | private 139 | def matches?(x, low_idx, high_idx, idx) #:nodoc: 140 | low, high = @segments[low_idx], @segments[high_idx] 141 | segment = @segments[idx] 142 | left = idx > 0 && @segments[idx - 1] 143 | right = idx < @segments.size - 1 && @segments[idx + 1] 144 | 145 | case 146 | when left && low.range.begin <= x && x <= left.range.end then -1 147 | when segment.range.begin <=x && x <= segment.range.end then 0 148 | when right && right.range.begin <=x && x <= high.range.end then 1 149 | else nil 150 | end 151 | end 152 | end -------------------------------------------------------------------------------- /lib/segment_tree/version.rb: -------------------------------------------------------------------------------- 1 | class SegmentTree 2 | VERSION = '0.2.0' 3 | end 4 | -------------------------------------------------------------------------------- /segment_tree.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/segment_tree/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.author = 'Alexei Mikhailov' 6 | gem.email = 'amikhailov83@gmail.com' 7 | gem.description = %q{Tree data structure for storing segments. It allows querying which of the stored segments contain a given point.} 8 | gem.summary = %q{Tree data structure for storing segments. It allows querying which of the stored segments contain a given point.} 9 | gem.homepage = 'https://github.com/take-five/segment_tree' 10 | 11 | gem.files = `git ls-files`.split($\).grep(/lib|spec/) 12 | gem.test_files = gem.files.grep(/spec/) 13 | gem.name = 'segment_tree' 14 | gem.require_paths = %W(lib) 15 | gem.version = SegmentTree::VERSION 16 | 17 | gem.add_development_dependency 'bundler', '>= 1.0' 18 | gem.add_development_dependency 'rspec', '>= 3.1.0' 19 | gem.add_development_dependency 'rake' 20 | end 21 | -------------------------------------------------------------------------------- /spec/segment_tree_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'segment_tree' 3 | 4 | # subject { tree } 5 | # it { should query(12).and_return("b") } 6 | RSpec::Matchers.define :query do |key| 7 | chain :and_return do |expected| 8 | @expected = expected 9 | @expected = nil if @expected == :nothing 10 | end 11 | 12 | match do |tree| 13 | result = tree.find(key) 14 | result &&= result.value 15 | 16 | expect(result).to eq @expected 17 | end 18 | 19 | failure_message do |tree| 20 | result = tree.find(key) 21 | result &&= result.value 22 | 23 | "expected that #{tree.inspect} would return #{@expected.inspect} when querying #{key.inspect}, " + 24 | "but #{result.inspect} returned instead" 25 | end 26 | end 27 | 28 | describe SegmentTree do 29 | # some fixtures 30 | # [[0..9, "a"], [10..19, "b"], ..., [90..99, "j"]] - spanned intervals 31 | let(:sample_spanned) { (0..9).zip('a'..'j').map { |num, letter| [(num * 10)..(num + 1) * 10 - 1, letter] }.shuffle } 32 | # [[0..10, "a"], [10..20, "b"], ..., [90..100, "j"]] - partially overlapping intervals 33 | let(:sample_overlapping) { (0..9).zip('a'..'j').map { |num, letter| [(num * 10)..(num + 1) * 10 + 2, letter] }.shuffle } 34 | # [[0..5, "a"], [10..15, "b"], ..., [90..95, "j"]] - sparsed intervals 35 | let(:sample_sparsed) { (0..9).zip('a'..'j').map { |num, letter| [(num * 10)..(num + 1) * 10 - 5, letter] }.shuffle } 36 | 37 | # [[0..5, "a"], [0..7, "aa"], [10..15, "b"], [10..17, "bb"], ..., [90..97, "jj"]] 38 | let(:sample_overlapping2) do 39 | (0..9).zip('a'..'j').map do |num, letter| 40 | [(num * 10)..(num + 1) * 10 - 5, letter, 41 | (num * 10)..(num + 1) * 10 - 3, letter * 2] 42 | end. 43 | flatten. 44 | each_slice(2). 45 | to_a. 46 | shuffle 47 | end 48 | 49 | describe '.new' do 50 | context 'given a hash with ranges as keys' do 51 | let :data do 52 | {7..9 => 'a', 53 | 4..6 => 'b', 54 | 0..3 => 'c', 55 | 10..12 => 'd'} 56 | end 57 | 58 | subject(:tree) { SegmentTree.new(data) } 59 | 60 | it { is_expected.to be_a SegmentTree } 61 | end 62 | 63 | context 'given an array of arrays' do 64 | let :data do 65 | [[0..3, 'a'], 66 | [4..6, 'b'], 67 | [7..9, 'c'], 68 | [10..12, 'd']].shuffle 69 | end 70 | 71 | subject(:tree) { SegmentTree.new(data) } 72 | 73 | it { is_expected.to be_a SegmentTree } 74 | end 75 | 76 | context 'given preordered data' do 77 | let :data do 78 | [[0..3, 'a'], 79 | [4..6, 'b'], 80 | [7..9, 'c'], 81 | [10..12, 'd']] 82 | end 83 | 84 | subject(:tree) { SegmentTree.new(data, true) } 85 | 86 | it { is_expected.to be_a SegmentTree } 87 | it { is_expected.to query(8).and_return('c') } 88 | end 89 | 90 | context 'given nor hash neither array' do 91 | it { expect{ SegmentTree.new(Object.new) }.to raise_error(ArgumentError) } 92 | end 93 | 94 | context 'given 1-dimensional array' do 95 | let :data do 96 | [0..3, 'a', 97 | 4..6, 'b', 98 | 7..9, 'c', 99 | 10..12, 'd'] 100 | end 101 | 102 | it { expect{ SegmentTree.new(data) }.to raise_error(ArgumentError) } 103 | end 104 | end 105 | 106 | describe 'querying' do 107 | context 'given spanned intervals' do 108 | subject { SegmentTree.new(sample_spanned) } 109 | 110 | it { is_expected.to query(12).and_return('b') } 111 | it { is_expected.to query(101).and_return(:nothing) } 112 | end 113 | 114 | context 'given partially overlapping intervals' do 115 | subject { SegmentTree.new(sample_overlapping) } 116 | 117 | it { is_expected.to query(11).and_return('a') } 118 | end 119 | 120 | context 'given sparsed intervals' do 121 | subject { SegmentTree.new(sample_sparsed) } 122 | 123 | it { is_expected.to query(12).and_return('b') } 124 | it { is_expected.to query(8).and_return(:nothing) } 125 | end 126 | 127 | context 'given hardly overlapping intervals' do 128 | subject { SegmentTree.new(sample_overlapping2) } 129 | 130 | it { is_expected.to query(12).and_return('b') } 131 | it { is_expected.to query(8).and_return(:nothing) } 132 | end 133 | end 134 | 135 | describe '#==' do 136 | subject { SegmentTree.new(sample_overlapping) } 137 | 138 | it { is_expected.to eq(SegmentTree.new(sample_overlapping)) } 139 | it { is_expected.not_to eq(SegmentTree.new(sample_overlapping2)) } 140 | 141 | it 'is equal when a range coerces' do 142 | expect(SegmentTree.new((1..2) => "a")).to eq(SegmentTree.new(((1.0)..(2.0)) => "a")) 143 | end 144 | 145 | it 'is equal when a value coerces' do 146 | expect(SegmentTree.new((1..2) => 1)).to eq(SegmentTree.new((1..2) => 1.0)) 147 | end 148 | 149 | it "isn't equal when only a range is different" do 150 | expect(SegmentTree.new((1..2) => "a")).not_to eq(SegmentTree.new((1..3) => "a")) 151 | end 152 | 153 | it "isn't equal when only a value is different" do 154 | expect(SegmentTree.new((1..2) => "a")).not_to eq(SegmentTree.new((1..2) => "b")) 155 | end 156 | end 157 | 158 | describe '#eql?' do 159 | subject { SegmentTree.new(sample_overlapping) } 160 | 161 | it { is_expected.to be_eql(SegmentTree.new(sample_overlapping)) } 162 | it { is_expected.not_to be_eql(SegmentTree.new(sample_overlapping2)) } 163 | 164 | it "isn't equal when a range coerces" do 165 | expect(SegmentTree.new((1..2) => "a")).not_to be_eql(SegmentTree.new(((1.0)..(2.0)) => "a")) 166 | end 167 | 168 | it "isn't equal when a value coerces" do 169 | expect(SegmentTree.new((1..2) => 1)).not_to be_eql(SegmentTree.new((1..2) => 1.0)) 170 | end 171 | 172 | it "isn't equal when only a range is different" do 173 | expect(SegmentTree.new((1..2) => "a")).not_to be_eql(SegmentTree.new((1..3) => "a")) 174 | end 175 | 176 | it "isn't equal when only a value is different" do 177 | expect(SegmentTree.new((1..2) => "a")).not_to be_eql(SegmentTree.new((1..2) => "b")) 178 | end 179 | end 180 | 181 | describe '#hash' do 182 | subject { SegmentTree.new(sample_overlapping).hash } 183 | 184 | it { is_expected.to eq(SegmentTree.new(sample_overlapping).hash) } 185 | it { is_expected.not_to eq(SegmentTree.new(sample_overlapping2).hash) } 186 | 187 | it "isn't equal when only a range is different" do 188 | expect(SegmentTree.new((1..2) => "a").hash).not_to eq(SegmentTree.new((1..3) => "a").hash) 189 | end 190 | 191 | it "isn't equal when only a value is different" do 192 | expect(SegmentTree.new((1..2) => "a").hash).not_to eq(SegmentTree.new((1..2) => "b").hash) 193 | end 194 | end 195 | 196 | describe 'marshaling' do 197 | it 'dumps and loads successfully' do 198 | aggregate_failures do 199 | [ 200 | sample_spanned, 201 | sample_sparsed, 202 | sample_overlapping, 203 | sample_overlapping2, 204 | ].each do |sample| 205 | tree = SegmentTree.new(sample) 206 | dumped = Marshal.dump(tree) 207 | expect(Marshal.load(dumped)).to eq(tree) 208 | end 209 | end 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | 3 | RSpec.configure do |config| 4 | # Run specs in random order to surface order dependencies. If you find an 5 | # order dependency and want to debug it, you can fix the order by providing 6 | # the seed, which is printed after each run. 7 | # --seed 1234 8 | config.order = 'random' 9 | end 10 | 11 | if defined?(RUBY_ENGINE) && 12 | RUBY_ENGINE == 'ruby' && 13 | RUBY_VERSION > '1.9' 14 | require 'simplecov' 15 | SimpleCov.start 16 | end --------------------------------------------------------------------------------