interval_notation 0.1.3 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +92 -4
- data/TODO.md +1 -0
- data/lib/interval_notation.rb +72 -48
- data/lib/interval_notation/basic_intervals.rb +4 -1
- data/lib/interval_notation/interval_set.rb +22 -2
- data/lib/interval_notation/operations.rb +3 -3
- data/lib/interval_notation/segmentation.rb +113 -0
- data/lib/interval_notation/sweep_line.rb +3 -0
- data/lib/interval_notation/sweep_line/sweep_line.rb +97 -0
- data/lib/interval_notation/sweep_line/trace_state/intersection.rb +56 -0
- data/lib/interval_notation/sweep_line/trace_state/multiple_state.rb +46 -0
- data/lib/interval_notation/sweep_line/trace_state/subtract.rb +20 -0
- data/lib/interval_notation/sweep_line/trace_state/symmetric_difference.rb +20 -0
- data/lib/interval_notation/sweep_line/trace_state/tagging.rb +72 -0
- data/lib/interval_notation/sweep_line/trace_state/union.rb +57 -0
- data/lib/interval_notation/sweep_line/trace_states.rb +6 -0
- data/lib/interval_notation/version.rb +1 -1
- data/spec/interval_notation_spec.rb +42 -0
- data/spec/segmentation_spec.rb +177 -0
- data/spec/tagging_spec.rb +69 -0
- metadata +16 -3
- data/lib/interval_notation/combiners.rb +0 -166
@@ -0,0 +1,97 @@
|
|
1
|
+
require_relative '../segmentation'
|
2
|
+
|
3
|
+
module IntervalNotation
|
4
|
+
# SweepLine is an internal helper module for combining interval sets using sweep line.
|
5
|
+
# It starts moving from -∞ to +∞ and keep which intervals are crossed by sweep line in trace-state variable.
|
6
|
+
# State object have methods which recalculate state when sweep line meets a group of boundary points
|
7
|
+
# and a method to convolve state underlying representation into surface state value.
|
8
|
+
# E.g. `Union` trace state can store information about which intervals are intersected by a sweep line,
|
9
|
+
# but on surface one knows only whether it intersects any interval.
|
10
|
+
#
|
11
|
+
# Boundary points for state recalculation are grouped by the same coordinate. As sweep lines goes left-to-right,
|
12
|
+
# initial state is a state at -∞.
|
13
|
+
# See `TraceState` module for an examples of trace-states.
|
14
|
+
#
|
15
|
+
# Principially sweep line method helps to effectively recalculate number of crossed intervals without rechecking
|
16
|
+
# all intervals each time, and dramatically reduces speed of operations on large number of intervals.
|
17
|
+
#
|
18
|
+
# Usage example:
|
19
|
+
# SweepLine.make_interval_set([interval_1, interval_2, interval_3], SweepLine::TraceState::Union.initial_state(3))
|
20
|
+
#
|
21
|
+
module SweepLine
|
22
|
+
# Make segmentation by state of a list of interval sets.
|
23
|
+
# Accepts a list of pairs (interval, tag)
|
24
|
+
def self.make_segmentation(indexed_interval_sets, initial_state)
|
25
|
+
points = interval_boundaries(indexed_interval_sets)
|
26
|
+
segmentation_by_boundary_points(points, initial_state).map_state{|segment|
|
27
|
+
segment.state.state_convolution
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
# Make segmentation by state of a list of interval sets.
|
32
|
+
# Accepts a list of intervals without tags.
|
33
|
+
def self.make_interval_set(interval_sets, initial_state)
|
34
|
+
make_segmentation(interval_sets.each_with_index, initial_state).make_interval_set
|
35
|
+
end
|
36
|
+
|
37
|
+
# Make tagging segmentation of interval sets.
|
38
|
+
# Each segment's state in a resulting segmentation is a set of tags lying against a segment.
|
39
|
+
def self.make_tagging(indexed_interval_sets)
|
40
|
+
make_segmentation(indexed_interval_sets, SweepLine::TraceState::SingleTagging.initial_state)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Make multi-tagging segmentation of interval sets. Multi-tagging means that segment store
|
44
|
+
# not only tag but number of times this tag was met.
|
45
|
+
# Each segment's state in a resulting segmentation is a hash {tag => count} for tags lying against a segment.
|
46
|
+
def self.make_multitagging(indexed_interval_sets)
|
47
|
+
make_segmentation(indexed_interval_sets, SweepLine::TraceState::MultiTagging.initial_state)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Extracts interval boundaries marked by interval indices or tags
|
51
|
+
# Accepts a list of pairs (interval, tag)
|
52
|
+
def self.interval_boundaries(tagged_interval_sets)
|
53
|
+
tagged_interval_sets.flat_map{|interval_set, interval_set_tag|
|
54
|
+
interval_set.intervals.flat_map{|interval|
|
55
|
+
interval.interval_boundaries(interval_set_tag)
|
56
|
+
}
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Make a segmentation using sweep line along boundary points.
|
61
|
+
def self.segmentation_by_boundary_points(boundary_points, initial_state)
|
62
|
+
if boundary_points.empty?
|
63
|
+
segment = Segmentation::Segment.new(BasicIntervals::OpenOpenInterval.new(-Float::INFINITY, Float::INFINITY), initial_state)
|
64
|
+
return Segmentation.new([segment])
|
65
|
+
end
|
66
|
+
|
67
|
+
point_chunks = boundary_points.sort_by(&:value).chunk(&:value).to_a
|
68
|
+
state = initial_state
|
69
|
+
|
70
|
+
# Process minus-infinity points which can change initial state
|
71
|
+
if point_chunks.first.first == -Float::INFINITY
|
72
|
+
point_value, points_at_minus_infinity = point_chunks.shift
|
73
|
+
state = state.state_after_point(points_at_minus_infinity)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Remove plus-infinity points as they can change state but this won't be reflected by any interval
|
77
|
+
if point_chunks.last.first == Float::INFINITY
|
78
|
+
point_value, points_at_plus_infinity = point_chunks.pop
|
79
|
+
end
|
80
|
+
|
81
|
+
prev_point_value = -Float::INFINITY
|
82
|
+
segments = []
|
83
|
+
# We removed points at plus or minus infinity so now we process inner points
|
84
|
+
point_chunks.each do |point_value, points_on_place|
|
85
|
+
segments << Segmentation::Segment.new(BasicIntervals::OpenOpenInterval.new(prev_point_value, point_value), state)
|
86
|
+
segments << Segmentation::Segment.new(BasicIntervals::Point.new(point_value), state.state_at_point(points_on_place))
|
87
|
+
state = state.state_after_point(points_on_place)
|
88
|
+
prev_point_value = point_value
|
89
|
+
end
|
90
|
+
|
91
|
+
# add fictive segment up to plus-infinity point (may be fictive)
|
92
|
+
segments << Segmentation::Segment.new(BasicIntervals::OpenOpenInterval.new(prev_point_value, Float::INFINITY), state)
|
93
|
+
Segmentation.new(segments, skip_validation: true) # here we can skip validation but not normalization
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module IntervalNotation
|
2
|
+
module SweepLine
|
3
|
+
module TraceState
|
4
|
+
|
5
|
+
# Class allows to observe whether sweep line is inside of interval sets intersection or outside
|
6
|
+
Intersection = Struct.new(:num_uncovered) do
|
7
|
+
def self.initial_state(num_interval_sets)
|
8
|
+
self.new(num_interval_sets)
|
9
|
+
end
|
10
|
+
|
11
|
+
# map state before point into state at point
|
12
|
+
def state_at_point(points_on_place)
|
13
|
+
new_state = num_uncovered
|
14
|
+
points_on_place.each{|point|
|
15
|
+
if point.singular_point?
|
16
|
+
new_state -= 1
|
17
|
+
else
|
18
|
+
if point.closing && !point.included
|
19
|
+
new_state += 1
|
20
|
+
elsif point.opening && point.included
|
21
|
+
new_state -= 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
}
|
25
|
+
self.class.new(new_state)
|
26
|
+
end
|
27
|
+
|
28
|
+
# map state at point into state after point
|
29
|
+
def state_after_point(points_on_place)
|
30
|
+
new_state = num_uncovered
|
31
|
+
points_on_place.reject(&:singular_point?).each{|point|
|
32
|
+
new_state += point.opening ? -1 : +1
|
33
|
+
}
|
34
|
+
|
35
|
+
self.class.new(new_state)
|
36
|
+
end
|
37
|
+
|
38
|
+
def state_convolution
|
39
|
+
num_uncovered == 0
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# # More generic but less efficient version of Intersection state
|
44
|
+
# class IntersectionMultiState < MultipleState
|
45
|
+
# def self.initial_state(num_interval_sets)
|
46
|
+
# self.new( Array.new(num_interval_sets, false) )
|
47
|
+
# end
|
48
|
+
|
49
|
+
# def state_convolution
|
50
|
+
# inclusion_state.all?
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module IntervalNotation
|
2
|
+
module SweepLine
|
3
|
+
module TraceState
|
4
|
+
|
5
|
+
# MultipleState is a simple abstract class to store and manage state of intersection with sweep line of several interval sets
|
6
|
+
#
|
7
|
+
# In order to use it one should define subclass with `#state_convolution` method.
|
8
|
+
# If specific convolution can be defined in easier terms, `#state_at_point` and `#state_after_point`
|
9
|
+
# also can be redefined in a subclass for perfomance and clarity reasons
|
10
|
+
MultipleState = Struct.new(:inclusion_state) do
|
11
|
+
# map state before point into state at point
|
12
|
+
def state_at_point(points_on_place)
|
13
|
+
new_state = inclusion_state.dup
|
14
|
+
points_on_place.each{|point|
|
15
|
+
new_state[point.interval_index] = point.included
|
16
|
+
}
|
17
|
+
self.class.new(new_state)
|
18
|
+
end
|
19
|
+
|
20
|
+
# map state before point (not at point!) into state after point
|
21
|
+
def state_after_point(points_on_place)
|
22
|
+
new_state = inclusion_state.dup
|
23
|
+
|
24
|
+
interval_boundary_points = points_on_place.reject(&:singular_point?)
|
25
|
+
points_by_closing = interval_boundary_points.group_by(&:closing)
|
26
|
+
closing_points = points_by_closing.fetch(true){ [] }
|
27
|
+
opening_points = points_by_closing.fetch(false){ [] }
|
28
|
+
|
29
|
+
closing_points.each{|point|
|
30
|
+
new_state[point.interval_index] = false
|
31
|
+
}
|
32
|
+
opening_points.each{|point|
|
33
|
+
new_state[point.interval_index] = true
|
34
|
+
}
|
35
|
+
self.class.new(new_state)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Convolve state inner state into state result
|
39
|
+
def state_convolution
|
40
|
+
raise NotImplementedError, '#state_convolution should be redefined in a superclass'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative 'multiple_state'
|
2
|
+
|
3
|
+
module IntervalNotation
|
4
|
+
module SweepLine
|
5
|
+
module TraceState
|
6
|
+
|
7
|
+
# Class allows to observe whether sweep line is inside of first and outside of second interval set
|
8
|
+
class Subtract < MultipleState
|
9
|
+
def self.initial_state
|
10
|
+
self.new([false, false])
|
11
|
+
end
|
12
|
+
|
13
|
+
def state_convolution
|
14
|
+
inclusion_state[0] && !inclusion_state[1]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative 'multiple_state'
|
2
|
+
|
3
|
+
module IntervalNotation
|
4
|
+
module SweepLine
|
5
|
+
module TraceState
|
6
|
+
|
7
|
+
# Class allows to observe whether sweep line is inside of exactly one of two intervals
|
8
|
+
class SymmetricDifference < MultipleState
|
9
|
+
def self.initial_state
|
10
|
+
self.new([false, false])
|
11
|
+
end
|
12
|
+
|
13
|
+
def state_convolution
|
14
|
+
inclusion_state[0] ^ inclusion_state[1]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module IntervalNotation
|
4
|
+
module SweepLine
|
5
|
+
module TraceState
|
6
|
+
|
7
|
+
# Tagging is an abstract state class which stores an overlay markup of several tagged interval-sets.
|
8
|
+
# Look at SingleTagging and MultiTagging which implement different convolution strategies (see below).
|
9
|
+
#
|
10
|
+
# Each interval set can be marked with the only tag but for an overlay each individual point
|
11
|
+
# can be marked with zero to many tags.
|
12
|
+
# Tag name is taken from boundary point's interval index.
|
13
|
+
# Tag names needn't be unique.
|
14
|
+
#
|
15
|
+
# When a point is overlapped by several tags with the same name, there are two strategies:
|
16
|
+
# * SingleTagging takes into account only the fact, that the point/interval was tagged
|
17
|
+
# * MultiTagging count the number of times each tag was assigned to a point/interval
|
18
|
+
#
|
19
|
+
Tagging = Struct.new(:tag_count) do
|
20
|
+
def self.initial_state
|
21
|
+
self.new(Hash.new(0))
|
22
|
+
end
|
23
|
+
|
24
|
+
# map state before point into state at point
|
25
|
+
def state_at_point(points_on_place)
|
26
|
+
new_state = tag_count.dup
|
27
|
+
points_on_place.each{|point|
|
28
|
+
if point.singular_point?
|
29
|
+
new_state[point.interval_index] += 1
|
30
|
+
else
|
31
|
+
if point.closing && !point.included
|
32
|
+
new_state[point.interval_index] -= 1
|
33
|
+
elsif point.opening && point.included
|
34
|
+
new_state[point.interval_index] += 1
|
35
|
+
end
|
36
|
+
end
|
37
|
+
}
|
38
|
+
new_state.reject!{|tag, count| count.zero?}
|
39
|
+
self.class.new(new_state)
|
40
|
+
end
|
41
|
+
|
42
|
+
# map state before point (not at point!) into state after point
|
43
|
+
def state_after_point(points_on_place)
|
44
|
+
new_state = tag_count.dup
|
45
|
+
points_on_place.reject(&:singular_point?).each{|point|
|
46
|
+
new_state[point.interval_index] += point.opening ? 1 : -1
|
47
|
+
}
|
48
|
+
new_state.reject!{|tag, count| count.zero?}
|
49
|
+
self.class.new(new_state)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Convolve state inner state into state result
|
53
|
+
def state_convolution
|
54
|
+
raise NotImplementedError, 'Tagging is an abstract state class. Use SingleTagging or MultiTagging instead'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class SingleTagging < Tagging
|
59
|
+
def state_convolution; tag_count.keys.to_set; end
|
60
|
+
def to_s; "{#{tag_count.keys.join(', ')}}"; end
|
61
|
+
def inspect; to_s; end
|
62
|
+
end
|
63
|
+
|
64
|
+
class MultiTagging < Tagging
|
65
|
+
def state_convolution; tag_count; end
|
66
|
+
def to_s; tag_count.to_s; end
|
67
|
+
def inspect; to_s; end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module IntervalNotation
|
2
|
+
module SweepLine
|
3
|
+
module TraceState
|
4
|
+
|
5
|
+
# Class allows to observe whether sweep line is inside of interval sets union or outside
|
6
|
+
Union = Struct.new(:num_covered) do
|
7
|
+
def self.initial_state(num_interval_sets)
|
8
|
+
self.new(0)
|
9
|
+
end
|
10
|
+
|
11
|
+
# map state before point into state at point
|
12
|
+
def state_at_point(points_on_place)
|
13
|
+
new_state = num_covered
|
14
|
+
points_on_place.each{|point|
|
15
|
+
if point.singular_point?
|
16
|
+
new_state += 1
|
17
|
+
else
|
18
|
+
if point.closing && !point.included
|
19
|
+
new_state -= 1
|
20
|
+
elsif point.opening && point.included
|
21
|
+
new_state += 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
}
|
25
|
+
self.class.new(new_state)
|
26
|
+
end
|
27
|
+
|
28
|
+
# map state at point into state after point
|
29
|
+
def state_after_point(points_on_place)
|
30
|
+
new_state = num_covered
|
31
|
+
points_on_place.reject(&:singular_point?).each{|point|
|
32
|
+
new_state += point.opening ? 1 : -1
|
33
|
+
}
|
34
|
+
|
35
|
+
self.class.new(new_state)
|
36
|
+
end
|
37
|
+
|
38
|
+
def state_convolution
|
39
|
+
num_covered > 0
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# # More generic but less efficient version of Union state
|
44
|
+
# require_relative 'multiple_state'
|
45
|
+
# class Union < MultipleState
|
46
|
+
# def self.initial_state(num_interval_sets)
|
47
|
+
# self.new( Array.new(num_interval_sets, false) )
|
48
|
+
# end
|
49
|
+
|
50
|
+
# def state_convolution
|
51
|
+
# inclusion_state.any?
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -736,5 +736,47 @@ describe IntervalNotation do
|
|
736
736
|
expect(interval.closure).to eq answer
|
737
737
|
end
|
738
738
|
end
|
739
|
+
|
740
|
+
describe '#connected_components' do
|
741
|
+
{
|
742
|
+
Empty => [],
|
743
|
+
oo(1,3) => [oo(1,3)],
|
744
|
+
cc(5,6) => [cc(5,6)],
|
745
|
+
pt(4) => [pt(4)],
|
746
|
+
oo(1,3) | pt(4) => [oo(1,3), pt(4)],
|
747
|
+
oo(1,3) | cc(5,6) => [oo(1,3), cc(5,6)],
|
748
|
+
oo(1,3) | pt(4) | cc(5,6) => [oo(1,3), pt(4), cc(5,6)],
|
749
|
+
}.each do |interval, answer|
|
750
|
+
it "#{interval}.connected_components should equal #{answer}" do
|
751
|
+
expect(interval.connected_components).to eq answer
|
752
|
+
end
|
753
|
+
end
|
754
|
+
end
|
755
|
+
|
756
|
+
describe '#interval_covering_point' do
|
757
|
+
{
|
758
|
+
[Empty, 2] => nil,
|
759
|
+
[oo(1,3), 2] => OpenOpenInterval.new(1,3),
|
760
|
+
[cc(1,3), 2] => ClosedClosedInterval.new(1,3),
|
761
|
+
[oo(1,3), 4] => nil,
|
762
|
+
[oo(1,3), 3] => nil,
|
763
|
+
[oo(1,3), 1] => nil,
|
764
|
+
[cc(1,3), 1] => ClosedClosedInterval.new(1,3),
|
765
|
+
[cc(1,3), 3] => ClosedClosedInterval.new(1,3),
|
766
|
+
[pt(4), 5] => nil,
|
767
|
+
[pt(4), 4] => Point.new(4),
|
768
|
+
[oo(1,3) | pt(4), 4] => Point.new(4),
|
769
|
+
[oo(1,3) | oo(5,6), 4] => nil,
|
770
|
+
[oo(1,3) | oo(5,6), 2] => OpenOpenInterval.new(1,3),
|
771
|
+
[oo(1,3) | oo(5,6), 5.5] => OpenOpenInterval.new(5,6),
|
772
|
+
[oo(1,3) | pt(4) | oo(5,6), 4] => Point.new(4),
|
773
|
+
[oo(1,3) | pt(4) | oo(5,6), 2] => OpenOpenInterval.new(1,3),
|
774
|
+
[oo(1,3) | pt(4) | oo(5,6), 5.5] => OpenOpenInterval.new(5,6),
|
775
|
+
}.each do |(interval, point), answer|
|
776
|
+
it "#{interval}.interval_covering_point(#{point}) should equal #{answer}" do
|
777
|
+
expect(interval.interval_covering_point(point)).to eq answer
|
778
|
+
end
|
779
|
+
end
|
780
|
+
end
|
739
781
|
end
|
740
782
|
end
|