interval_notation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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