interval_notation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,190 @@
1
+ require_relative 'basic_intervals'
2
+ require_relative 'error'
3
+ require_relative 'operations'
4
+
5
+ module IntervalNotation
6
+ class IntervalSet
7
+ attr_reader :intervals
8
+
9
+ # +IntervalSet.new+ accepts an ordered list of intervals.
10
+ # Intervals should be sorted from leftmost to rightmost and should not overlap.
11
+ # It's not recommended to use this constructor directly. Instead take a look at +IntervalNotation::Syntax+ module.
12
+ #
13
+ # Example:
14
+ # # don't use this
15
+ # IntervalSet.new([OpenOpenInterval.new(1,3), Point.new(5)])
16
+ # # instead use
17
+ # oo(1,3) | pt(5)
18
+
19
+ def initialize(intervals)
20
+ unless IntervalSet.check_valid?(intervals)
21
+ raise Error, "IntervalSet.new accepts non-overlapping, sorted regions.\n" +
22
+ "Try to use IntervalNotation.union(...) to create interval from overlapping or not-sorted intervals"
23
+ end
24
+ @intervals = intervals.freeze
25
+ end
26
+
27
+ # Method to create an interval set from string.
28
+ # It accepts strings obtained by +#to_s+ and many other formats.
29
+ # Spaces inside are ignored
30
+ # Intervals can be joined with +u+ or +U+ letters and unicode union character +∪+
31
+ # Points can go separately or be combined inside of single curly braces. Single point can go without braces at all
32
+ # +,+ and +;+ are both valid value separators.
33
+ # Infinity can be represented by +inf+, +infty+, +\infty+, +infinity+, +∞+ (each can go with or without sign)
34
+ # Empty set is empty string, word +Empty+ or unicode character +∅+
35
+ # +R+ represents whole 1-D line (-∞, ∞)
36
+ def self.from_string(str)
37
+ intervals = str.split(/[uU∪]/).flat_map{|interval_str|
38
+ BasicIntervals.from_string(interval_str)
39
+ }.map(&:to_interval_set)
40
+ Operations.union(intervals)
41
+ end
42
+
43
+ # Output standard mathematical notation of interval set in left-to-right order.
44
+ # Each singular point is listed separately in curly braces.
45
+ def to_s
46
+ @intervals.empty? ? EMPTY_SET_SYMBOL : intervals.map(&:to_s).join(UNION_SYMBOL)
47
+ end
48
+
49
+ def inspect # :nodoc:
50
+ to_s
51
+ end
52
+
53
+ # Checks whether an interval set contains certain position.
54
+ # Operation complexity is O(ln N), where N is a number of contiguous regions in an interval set
55
+ def include_position?(value)
56
+ interval = @intervals.bsearch{|interv| interv.to >= value }
57
+ interval && interval.include_position?(value)
58
+ end
59
+
60
+ # Checks whether an interval set contains another interval set. Alias: +#include?+
61
+ def contain?(other)
62
+ self.intersection(other) == other
63
+ end
64
+ alias include? contain?
65
+
66
+ # Checks whether an interval set is covered by another interval set. Alias: +#covered_by?+
67
+ def contained_by?(other)
68
+ self.intersection(other) == self
69
+ end
70
+ alias covered_by? contained_by?
71
+
72
+ # TODO: optimize if possible.
73
+ # Checks whether an interval set intersects another interval set. Alias: +#intersect?+
74
+ def intersect?(other)
75
+ ! intersection(other).empty?
76
+ end
77
+ alias overlap? intersect?
78
+
79
+ # Checks whether an interval set is empty
80
+ def empty?
81
+ @intervals.empty?
82
+ end
83
+
84
+ # Checks whether an interval set is contiguous (empty set treated contiguous)
85
+ def contiguous?
86
+ @intervals.size <= 1
87
+ end
88
+
89
+ # Total length of all intervals in set
90
+ def total_length
91
+ @intervals.map(&:length).inject(0, &:+)
92
+ end
93
+
94
+ # Number of connected components
95
+ def num_connected_components
96
+ @intervals.size
97
+ end
98
+
99
+ # TODO: optimize.
100
+ # Closure of an interval set
101
+ def closure
102
+ Operations.union(@intervals.map(&:closure).map(&:to_interval_set))
103
+ end
104
+
105
+ # Minimal contiguous interval, covering interval set
106
+ def covering_interval
107
+ if @intervals.size == 0
108
+ Empty
109
+ elsif @intervals.size == 1
110
+ self
111
+ else
112
+ BasicIntervals.interval_by_boundary_inclusion(@intervals.first.include_from?, @intervals.first.from,
113
+ @intervals.last.include_to?, @intervals.last.to).to_interval_set
114
+ end
115
+ end
116
+
117
+ def hash # :nodoc:
118
+ @intervals.hash
119
+ end
120
+
121
+ def eql?(other) # :nodoc:
122
+ other.class.equal?(self.class) && intervals == other.intervals
123
+ end
124
+
125
+ # Intervals are equal only if they contain exactly the same intervals.
126
+ # Point inclusion is also considered
127
+ def ==(other)
128
+ other.is_a?(IntervalSet) && intervals == other.intervals
129
+ end
130
+
131
+ # Union of an interval set with another interval set +other+. Alias: +|+
132
+ # To unite many (tens of thousands intervals) intervals use +IntervalNotation::Operations.unite+ method.
133
+ # (+Operations.unite+ is dramatically faster than sequentially uniting intervals one-by-one)
134
+ def union(other)
135
+ Operations.union([self, other])
136
+ end
137
+
138
+ # Intersection of an interval set with another interval set +other+. Alias: +&+
139
+ # To unite many (tens of thousands intervals) intervals use +IntervalNotation::Operations.intersection+ method.
140
+ # (+Operations.intersection+ is dramatically faster than sequentially intersecting intervals one-by-one)
141
+ def intersection(other)
142
+ Operations.intersection([self, other])
143
+ end
144
+
145
+ # Difference between an interval set and another interval set +other+. Alias: +-+
146
+ def subtract(other)
147
+ Operations.combine([self, other], SubtractCombiner.new)
148
+ end
149
+
150
+ # Symmetric difference between an interval set and another interval set +other+. Alias: +^+
151
+ def symmetric_difference(other)
152
+ Operations.combine([self, other], SymmetricDifferenceCombiner.new)
153
+ end
154
+
155
+ # Complement of an interval set in R. Alias: +~+
156
+ def complement
157
+ R.subtract(self)
158
+ end
159
+
160
+ alias :& :intersection
161
+ alias :| :union
162
+ alias :- :subtract
163
+ alias :^ :symmetric_difference
164
+ alias :~ :complement
165
+
166
+
167
+ # Auxiliary method to share part of common interface with basic intervals
168
+ def to_interval_set # :nodoc:
169
+ self
170
+ end
171
+
172
+ class << self
173
+ # auxiliary method to check that intervals are sorted and don't overlap
174
+ def check_valid?(intervals)
175
+ intervals.each_cons(2).all? do |interval_1, interval_2|
176
+ interval_1.to <= interval_2.from && !(interval_1.to == interval_2.from && (interval_1.include_to? || interval_2.include_from?))
177
+ end
178
+ end
179
+
180
+ # An +IntervalSet.new_unsafe+ is a constructor which skips validation. It's designed mostly for internal use.
181
+ # It can be used when you are absolutely sure, that intervals are ordered and don't overlap.
182
+ def new_unsafe(intervals)
183
+ obj = allocate
184
+ obj.instance_variable_set(:@intervals, intervals.freeze)
185
+ obj
186
+ end
187
+ end
188
+ end
189
+ extend Operations
190
+ end
@@ -0,0 +1,62 @@
1
+ require_relative 'interval_set'
2
+ require_relative 'basic_intervals'
3
+ require_relative 'combiners'
4
+
5
+ module IntervalNotation
6
+ module Operations
7
+ # Internal method which combines intervals according to an algorithm given by a combiner.
8
+ # Combiner tells whether current section or point should be included to a new interval.
9
+ def combine(interval_sets, combiner)
10
+ points = interval_sets.each_with_index.flat_map{|interval_set, interval_set_index|
11
+ interval_set.intervals.flat_map{|interval|
12
+ interval.interval_boundaries(interval_set_index)
13
+ }
14
+ }.sort_by(&:value)
15
+
16
+ intervals = []
17
+
18
+ incl_from = nil
19
+ from = nil
20
+
21
+ points.chunk(&:value).each do |point_value, points_on_place|
22
+ combiner.pass(points_on_place)
23
+
24
+ if combiner.previous_state
25
+ if combiner.state
26
+ unless combiner.include_last_point
27
+ intervals << BasicIntervals.interval_by_boundary_inclusion(incl_from, from, false, point_value)
28
+ incl_from = false
29
+ from = point_value
30
+ end
31
+ else
32
+ to = point_value
33
+ incl_to = combiner.include_last_point
34
+ intervals << BasicIntervals.interval_by_boundary_inclusion(incl_from, from, incl_to, to)
35
+ from = nil # easier to find an error (but not necessary code)
36
+ incl_from = nil # ditto
37
+ end
38
+ else
39
+ if combiner.state
40
+ from = point_value
41
+ incl_from = combiner.include_last_point
42
+ else
43
+ intervals << BasicIntervals::Point.new(point_value) if combiner.include_last_point
44
+ end
45
+ end
46
+ end
47
+ IntervalSet.new_unsafe(intervals)
48
+ end
49
+
50
+ # Union of multiple intervals.
51
+ def union(intervals)
52
+ combine(intervals, UnionCombiner.new(intervals.size))
53
+ end
54
+
55
+ # Intersection of multiple intervals
56
+ def intersection(intervals)
57
+ combine(intervals, IntersectCombiner.new(intervals.size))
58
+ end
59
+
60
+ module_function :combine, :union, :intersection
61
+ end
62
+ end
@@ -0,0 +1,3 @@
1
+ module IntervalNotation
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,117 @@
1
+ require_relative 'interval_notation/version'
2
+
3
+ require_relative 'interval_notation/error'
4
+ require_relative 'interval_notation/basic_intervals'
5
+ require_relative 'interval_notation/combiners'
6
+ require_relative 'interval_notation/interval_set'
7
+ require_relative 'interval_notation/operations'
8
+
9
+ module IntervalNotation
10
+ UNION_SYMBOL = '∪'.freeze
11
+ PLUS_INFINITY_SYMBOL = '+∞'.freeze
12
+ MINUS_INFINITY_SYMBOL = '-∞'.freeze
13
+ EMPTY_SET_SYMBOL = '∅'.freeze
14
+
15
+ R = IntervalSet.new_unsafe( [BasicIntervals::OpenOpenInterval.new(-Float::INFINITY, Float::INFINITY)] )
16
+ Empty = IntervalSet.new_unsafe([])
17
+
18
+ module Syntax
19
+ # Long syntax for interval factory methods
20
+ module Short
21
+ R = ::IntervalNotation::R
22
+ Empty = ::IntervalNotation::Empty
23
+
24
+ def int(str)
25
+ IntervalSet.from_string(str)
26
+ end
27
+
28
+ def oo(from, to)
29
+ IntervalSet.new_unsafe( [BasicIntervals::OpenOpenInterval.new(from, to)] )
30
+ end
31
+
32
+ def co(from, to)
33
+ IntervalSet.new_unsafe( [BasicIntervals::ClosedOpenInterval.new(from, to)] )
34
+ end
35
+
36
+ def oc(from, to)
37
+ IntervalSet.new_unsafe( [BasicIntervals::OpenClosedInterval.new(from, to)] )
38
+ end
39
+
40
+ def cc(from, to)
41
+ IntervalSet.new_unsafe( [BasicIntervals::ClosedClosedInterval.new(from, to)] )
42
+ end
43
+
44
+ def pt(value)
45
+ IntervalSet.new_unsafe( [BasicIntervals::Point.new(value)] )
46
+ end
47
+
48
+ def lt(value)
49
+ IntervalSet.new_unsafe( [BasicIntervals::OpenOpenInterval.new(-Float::INFINITY, value)] )
50
+ end
51
+
52
+ def le(value)
53
+ IntervalSet.new_unsafe( [BasicIntervals::OpenClosedInterval.new(-Float::INFINITY, value)] )
54
+ end
55
+
56
+ def gt(value)
57
+ IntervalSet.new_unsafe( [BasicIntervals::OpenOpenInterval.new(value, Float::INFINITY)] )
58
+ end
59
+
60
+ def ge(value)
61
+ IntervalSet.new_unsafe( [BasicIntervals::ClosedOpenInterval.new(value, Float::INFINITY)] )
62
+ end
63
+
64
+ module_function :oo, :co, :oc, :cc, :pt, :lt, :le, :gt, :ge, :int
65
+ end
66
+
67
+ # Long syntax for interval factory methods
68
+ module Long
69
+ R = ::IntervalNotation::R
70
+ Empty = ::IntervalNotation::Empty
71
+
72
+ def interval(str)
73
+ IntervalSet.from_string(str)
74
+ end
75
+
76
+ def open_open(from, to)
77
+ IntervalSet.new_unsafe( [BasicIntervals::OpenOpenInterval.new(from, to)] )
78
+ end
79
+
80
+ def closed_open(from, to)
81
+ IntervalSet.new_unsafe( [BasicIntervals::ClosedOpenInterval.new(from, to)] )
82
+ end
83
+
84
+ def open_closed(from, to)
85
+ IntervalSet.new_unsafe( [BasicIntervals::OpenClosedInterval.new(from, to)] )
86
+ end
87
+
88
+ def closed_closed(from, to)
89
+ IntervalSet.new_unsafe( [BasicIntervals::ClosedClosedInterval.new(from, to)] )
90
+ end
91
+
92
+ def point(value)
93
+ IntervalSet.new_unsafe( [BasicIntervals::Point.new(value)] )
94
+ end
95
+
96
+ def less_than(value)
97
+ IntervalSet.new_unsafe( [BasicIntervals::OpenOpenInterval.new(-Float::INFINITY, value)] )
98
+ end
99
+
100
+ def less_than_or_equal_to(value)
101
+ IntervalSet.new_unsafe( [BasicIntervals::OpenClosedInterval.new(-Float::INFINITY, value)] )
102
+ end
103
+
104
+ def greater_than(value)
105
+ IntervalSet.new_unsafe( [BasicIntervals::OpenOpenInterval.new(value, Float::INFINITY)] )
106
+ end
107
+
108
+ def greater_than_or_equal_to(value)
109
+ IntervalSet.new_unsafe( [BasicIntervals::ClosedOpenInterval.new(value, Float::INFINITY)] )
110
+ end
111
+
112
+ module_function :open_open, :closed_open, :open_closed, :closed_closed, :point,
113
+ :less_than, :less_than_or_equal_to, :greater_than, :greater_than_or_equal_to,
114
+ :interval
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,140 @@
1
+ require 'interval_notation'
2
+
3
+ include IntervalNotation
4
+ include IntervalNotation::BasicIntervals
5
+ include IntervalNotation::Syntax::Short
6
+
7
+ describe IntervalNotation do
8
+ describe OpenOpenInterval do
9
+ describe '.new' do
10
+ { [1,3] => :ok,
11
+ [1, Float::INFINITY] => :ok,
12
+ [-Float::INFINITY, 1] => :ok,
13
+ [-Float::INFINITY, Float::INFINITY] => :ok,
14
+
15
+ [1,1] => :fail,
16
+ [3,1] => :fail,
17
+ [1,-Float::INFINITY] => :fail,
18
+ [Float::INFINITY, 1] => :fail,
19
+ [-Float::INFINITY, -Float::INFINITY] => :fail,
20
+ [Float::INFINITY, Float::INFINITY] => :fail,
21
+ [Float::INFINITY, -Float::INFINITY] => :fail,
22
+ }.each do |(from, to), result|
23
+ if result == :ok
24
+ it "OpenOpenInterval.new(#{from}, #{to}) should not raise" do
25
+ expect{ OpenOpenInterval.new(from, to) }.not_to raise_error
26
+ end
27
+ elsif result == :fail
28
+ it "OpenOpenInterval.OpenOpenInterval(#{from}, #{to}) should raise" do
29
+ expect{ OpenOpenInterval.new(from, to) }.to raise_error Error
30
+ end
31
+ else
32
+ raise 'Incorrect test'
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+
39
+ describe OpenClosedInterval do
40
+ describe '.new' do
41
+ { [1,3] => :ok,
42
+ [-Float::INFINITY, 1] => :ok,
43
+
44
+ [1, Float::INFINITY] => :fail,
45
+ [-Float::INFINITY, Float::INFINITY] => :fail,
46
+ [1,1] => :fail,
47
+ [3,1] => :fail,
48
+ [1,-Float::INFINITY] => :fail,
49
+ [Float::INFINITY, 1] => :fail,
50
+ [-Float::INFINITY, -Float::INFINITY] => :fail,
51
+ [Float::INFINITY, Float::INFINITY] => :fail,
52
+ [Float::INFINITY, -Float::INFINITY] => :fail,
53
+ }.each do |(from, to), result|
54
+ if result == :ok
55
+ it "OpenClosedInterval.new(#{from}, #{to}) should not raise" do
56
+ expect{ OpenClosedInterval.new(from, to) }.not_to raise_error
57
+ end
58
+ elsif result == :fail
59
+ it "OpenOpenInterval.new(#{from}, #{to}) should raise" do
60
+ expect{ OpenClosedInterval.new(from, to) }.to raise_error Error
61
+ end
62
+ else
63
+ raise 'Incorrect test'
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+
70
+ describe ClosedOpenInterval do
71
+ describe '.new' do
72
+ { [1,3] => :ok,
73
+ [1, Float::INFINITY] => :ok,
74
+
75
+ [-Float::INFINITY, 1] => :fail,
76
+ [-Float::INFINITY, Float::INFINITY] => :fail,
77
+ [1,1] => :fail,
78
+ [3,1] => :fail,
79
+ [1,-Float::INFINITY] => :fail,
80
+ [Float::INFINITY, 1] => :fail,
81
+ [-Float::INFINITY, -Float::INFINITY] => :fail,
82
+ [Float::INFINITY, Float::INFINITY] => :fail,
83
+ [Float::INFINITY, -Float::INFINITY] => :fail,
84
+ }.each do |(from, to), result|
85
+ if result == :ok
86
+ it "ClosedOpenInterval.new(#{from}, #{to}) should not raise" do
87
+ expect{ ClosedOpenInterval.new(from, to) }.not_to raise_error
88
+ end
89
+ elsif result == :fail
90
+ it "OpenOpenInterval.new(#{from}, #{to}) should raise" do
91
+ expect{ ClosedOpenInterval.new(from, to) }.to raise_error Error
92
+ end
93
+ else
94
+ raise 'Incorrect test'
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+
101
+ describe ClosedClosedInterval do
102
+ describe '.new' do
103
+ { [1,3] => :ok,
104
+
105
+ [1, Float::INFINITY] => :fail,
106
+ [-Float::INFINITY, 1] => :fail,
107
+ [-Float::INFINITY, Float::INFINITY] => :fail,
108
+ [1,1] => :fail,
109
+ [3,1] => :fail,
110
+ [1,-Float::INFINITY] => :fail,
111
+ [Float::INFINITY, 1] => :fail,
112
+ [-Float::INFINITY, -Float::INFINITY] => :fail,
113
+ [Float::INFINITY, Float::INFINITY] => :fail,
114
+ [Float::INFINITY, -Float::INFINITY] => :fail,
115
+ }.each do |(from, to), result|
116
+ if result == :ok
117
+ it "ClosedClosedInterval.new(#{from}, #{to}) should not raise" do
118
+ expect{ ClosedClosedInterval.new(from, to) }.not_to raise_error
119
+ end
120
+ elsif result == :fail
121
+ it "OpenOpenInterval.new(#{from}, #{to}) should raise" do
122
+ expect{ ClosedClosedInterval.new(from, to) }.to raise_error Error
123
+ end
124
+ else
125
+ raise 'Incorrect test'
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ describe Point do
132
+ it 'should not raise if finite' do
133
+ expect{ Point.new(3) }.not_to raise_error
134
+ end
135
+ it 'should raise if infinite' do
136
+ expect{ Point.new(-Float::INFINITY) }.to raise_error
137
+ expect{ Point.new(Float::INFINITY) }.to raise_error
138
+ end
139
+ end
140
+ end