├── .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 [](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
--------------------------------------------------------------------------------