cantor 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # Cantor
2
+
3
+ Fast implementation of finite and complement sets in Ruby
4
+
5
+ ## Constructors
6
+
7
+ * `Cantor.empty`
8
+ Finite set that contains no elements
9
+
10
+ * `Cantor.build(enum)`
11
+ Finite set containing each element in `enum`, whose domain of discourse is
12
+ unrestricted
13
+
14
+ * `Cantor.absolute(enum, universe)`
15
+ Finite set containing each element in `enum`, whose domain of discourse is
16
+ `universe`
17
+
18
+ * `Cantor.universal`
19
+ Infinite set containing every value in the universe
20
+
21
+ * `Cantor.complement(enum)`
22
+ Set containing every value except those in `enum`. Finite when `enum` is
23
+ infinite. Infinite when `enum` is finite
24
+
25
+ ## Operations
26
+
27
+ * `xs.include?(x)`
28
+ * `xs.exclude?(x)`
29
+ * `xs.finite?`
30
+ * `xs.infinite?`
31
+ * `xs.empty?`
32
+ * `xs.size`
33
+ * `xs.replace(ys)`
34
+ * `~xs`
35
+ * `xs.complement`
36
+ * `xs + xs`
37
+ * `xs | ys`
38
+ * `xs.union(ys)`
39
+ * `xs - ys`
40
+ * `xs.difference(ys)`
41
+ * `xs ^ ys`
42
+ * `xs.symmetric_difference(ys)`
43
+ * `xs & ys`
44
+ * `xs.intersection(ys)`
45
+ * `xs <= ys`
46
+ * `xs.subset?(ys)`
47
+ * `xs < ys`
48
+ * `xs.proper_subset?(ys)`
49
+ * `xs >= ys`
50
+ * `xs.superset?(ys)`
51
+ * `xs > ys`
52
+ * `xs.proper_superset?(ys)`
53
+ * `xs.disjoint?(ys)`
54
+ * `xs == ys`
55
+
56
+ ## Performance
57
+
58
+ Sets with a finite domain of discourse are represented using a bit string of
59
+ 2<sup>|U|</sup> bits, where |U| is the size of the domain. This provides nearly
60
+ O(1) constant-time implementation using bitwise operations for all of the above
61
+ set operations.
62
+
63
+ The bit string is represented as an Integer, but as the domain grows larger
64
+ than `0.size * 8 - 2` items, the type is automatically expanded to a Bignum.
65
+ Bitwise operations on Bignums are O(|U|), which is still be significantly
66
+ faster than using the default Set library.
67
+
68
+ Sets with an unrestricted domain of discourse are implemented using a Hash.
69
+ Unary operations and membership tests are O(1) constant-time. Binary operations
70
+ on these sets is close to that of the default Set library.
71
+
72
+ ### Benchmarks
73
+
74
+ These benchmarks aren't intended to be useful. While they indicate the
75
+ worst-case performance for Cantor, they probably don't show the worst
76
+ case for the standard Set library.
77
+
78
+ <table>
79
+ <tbody>
80
+ <tr>
81
+ <td><img title="intersection" src="/kputnam/cantor/raw/master/benchmark/intersection.png"/></td>
82
+ <td><img title="difference" src="/kputnam/cantor/raw/master/benchmark/difference.png"/></td>
83
+ </tr>
84
+ <tr>
85
+ <td><img title="union" src="/kputnam/cantor/raw/master/benchmark/union.png"/></td>
86
+ <td><img title="symmetric difference" src="/kputnam/cantor/raw/master/benchmark/sdifference.png"/></td>
87
+ </tr>
88
+ <tr>
89
+ <td><img title="subset" src="/kputnam/cantor/raw/master/benchmark/subset.png"/></td>
90
+ <td><img title="superset" src="/kputnam/cantor/raw/master/benchmark/superset.png"/></td>
91
+ </tr>
92
+ <tr>
93
+ <td><img title="equality" src="/kputnam/cantor/raw/master/benchmark/equality.png"/></td>
94
+ <td><img title="membership" src="/kputnam/cantor/raw/master/benchmark/membership.png"/></td>
95
+ </tr>
96
+ </tbody>
97
+ </table>
data/Rakefile ADDED
@@ -0,0 +1,81 @@
1
+ require "pathname"
2
+ abspath = Pathname.new(File.dirname(__FILE__)).expand_path
3
+ relpath = abspath.relative_path_from(Pathname.pwd)
4
+
5
+ begin
6
+ require "rubygems"
7
+ require "bundler/setup"
8
+ rescue LoadError
9
+ warn "couldn't load bundler:"
10
+ warn " #{$!}"
11
+ end
12
+
13
+ task :console do
14
+ exec *%w(irb -I lib -r cantor)
15
+ end
16
+
17
+ begin
18
+ require "rspec/core/rake_task"
19
+ RSpec::Core::RakeTask.new do |t|
20
+ t.verbose = false
21
+ t.pattern = "#{relpath}/spec/examples/**/*.example"
22
+
23
+ t.rspec_opts = %w(--color --format p)
24
+ t.rspec_opts << "-I#{abspath}/spec"
25
+ end
26
+ rescue LoadError
27
+ task :spec do
28
+ warn "couldn't load rspec"
29
+ warn " #{$!}"
30
+ exit 1
31
+ end
32
+ end
33
+
34
+ begin
35
+ require "rcov"
36
+ begin
37
+ require "rspec/core/rake_task"
38
+ RSpec::Core::RakeTask.new(:rcov) do |t|
39
+ t.rcov = true
40
+ t.rcov_opts = "--exclude spec/,gems/,00401"
41
+
42
+ t.verbose = false
43
+ t.pattern = "#{relpath}/spec/examples/**/*.example"
44
+
45
+ t.rspec_opts = %w(--color --format p)
46
+ t.rspec_opts << "-I#{abspath}/spec"
47
+ end
48
+ rescue LoadError
49
+ task :rcov do
50
+ warn "couldn't load rspec"
51
+ warn " #{$!}"
52
+ exit 1
53
+ end
54
+ end
55
+ rescue LoadError
56
+ task :rcov do
57
+ warn "couldn't load rcov:"
58
+ warn " #{$!}"
59
+ exit 1
60
+ end
61
+ end
62
+
63
+ begin
64
+ require "yard"
65
+
66
+ # Note options are loaded from .yardopts
67
+ YARD::Rake::YardocTask.new(:yard => :clobber_yard)
68
+
69
+ task :clobber_yard do
70
+ rm_rf "#{relpath}/doc/generated"
71
+ mkdir_p "#{relpath}/doc/generated/images"
72
+ end
73
+ rescue LoadError
74
+ task :yard do
75
+ warn "couldn't load yard:"
76
+ warn " #{$!}"
77
+ exit 1
78
+ end
79
+ end
80
+
81
+ task :default => :spec
data/lib/cantor.rb ADDED
@@ -0,0 +1,54 @@
1
+ module Cantor
2
+ autoload :AbstractSet, "cantor/abstract_set"
3
+ autoload :AbsoluteSet, "cantor/absolute_set"
4
+ autoload :NullSet, "cantor/null_set"
5
+ autoload :RelativeComplement, "cantor/relative_complement"
6
+ autoload :RelativeSet, "cantor/relative_set"
7
+ autoload :UniversalSet, "cantor/universal_set"
8
+ end
9
+
10
+ class << Cantor
11
+ # @group Constructors
12
+ ###########################################################################
13
+
14
+ # @return [Cantor::AbstractSet]
15
+ def build(object)
16
+ if object.is_a?(Cantor::AbstractSet)
17
+ object
18
+ elsif object.is_a?(Enumerable)
19
+ Cantor::RelativeSet.build(object)
20
+ else
21
+ raise TypeError,
22
+ "argument must be an AbstractSet or Enumerable"
23
+ end
24
+ end
25
+
26
+ # @return [Cantor::AbstractSet]
27
+ def complement(other)
28
+ build(other).complement
29
+ end
30
+
31
+ # @return [Cantor::UniversalSet]
32
+ def universal
33
+ Cantor::UniversalSet.build
34
+ end
35
+
36
+ # @return [Cantor::NullSet]
37
+ def empty
38
+ Cantor::NullSet.build
39
+ end
40
+
41
+ # @return [Cantor::AbsoluteSet]
42
+ def absolute(other, universe = other)
43
+ if universe == Cantor::UniversalSet
44
+ build(other)
45
+ elsif universe.eql?(other)
46
+ Cantor::AbsoluteSet.build(universe)
47
+ else
48
+ Cantor::AbsoluteSet.build(universe).intersection(other)
49
+ end
50
+ end
51
+
52
+ # @endgroup
53
+ ###########################################################################
54
+ end
@@ -0,0 +1,295 @@
1
+ module Cantor
2
+
3
+ #
4
+ # {AbsoluteSet} is a subset of a finite, fully-enumerated universe set. This
5
+ # means every possible value that can belong to the {AbsoluteSet} must
6
+ # already belong to the universe set, which is a _finite_ collection.
7
+ #
8
+ # This implementation is fairly efficient when computing set operations on
9
+ # two sets from the same universe, especially compared to {RelativeSet}.
10
+ # Efficiency is achieved by encoding each element in the universe's
11
+ # membership to the specific subset as a bitmask. Operations can then be
12
+ # performed using bitwise operations, instead of using operations on a Hash.
13
+ #
14
+ # This data type is not suitable for sets whose elements belong to an
15
+ # huge universe of possible values, as each set requires `2**|U|` bits of
16
+ # storage where `|U|` is the size of the universe. Operations on sets that
17
+ # belong to different universes do not currently attempt to merge the two
18
+ # universe sets, as this probably a better use case for {RelativeSet}.
19
+ #
20
+ class AbsoluteSet < AbstractSet
21
+ include Enumerable
22
+
23
+ # @return [Integer]
24
+ attr_reader :mask
25
+
26
+ # @return [Hash]
27
+ attr_reader :universe
28
+
29
+ def initialize(mask, universe)
30
+ @mask, @universe = mask, universe.freeze
31
+ end
32
+
33
+ # @private
34
+ # @return [AbsoluteSet]
35
+ def copy(changes = {})
36
+ AbsoluteSet.new \
37
+ changes.fetch(:mask, @mask),
38
+ changes.fetch(:universe, @universe)
39
+ end
40
+
41
+ # Returns a single element from the set, with no guarantees about which
42
+ # element. If the set is {#empty?}, the return value is undefined.
43
+ def first
44
+ @universe.each do |value, n|
45
+ unless @mask[n].zero?
46
+ return value
47
+ end
48
+ end
49
+ end
50
+
51
+ # Yields each element in the set to the implicit block argument
52
+ #
53
+ # @return [void]
54
+ def each
55
+ @universe.each do |value, n|
56
+ unless @mask[n].zero?
57
+ yield(value)
58
+ end
59
+ end
60
+ end
61
+
62
+ # (see AbstractSet#finite?)
63
+ def finite?
64
+ true
65
+ end
66
+
67
+ # (see AbstractSet#empty?)
68
+ def empty?
69
+ @mask.zero?
70
+ end
71
+
72
+ # (see AbstractSet#size)
73
+ def size
74
+ @universe.inject(0){|size, (value, n)| size + @mask[n] }
75
+ end
76
+
77
+ # (see AbstractSet#replace)
78
+ def replace(other)
79
+ if other.is_a?(AbstractSet)
80
+ other
81
+ else
82
+ copy(:mask => as_mask(other, true))
83
+ end
84
+ end
85
+
86
+ # (see AbstractSet#include?)
87
+ def include?(element)
88
+ if n = @universe.fetch(element, false)
89
+ # Same as (@mask & (1 << n)).zero? but potentially eliminates
90
+ # converting the intermediate computation to a Ruby value
91
+ not @mask[n].zero?
92
+ end
93
+ end
94
+
95
+ # @group Set Operations
96
+ #########################################################################
97
+
98
+ # @return [AbsoluteSet]
99
+ def map
100
+ mask = 0
101
+
102
+ @universe.each do |value, n|
103
+ unless @mask[n].zero?
104
+ value = yield(value)
105
+
106
+ if m = @universe.fetch(value, false)
107
+ mask |= (1 << m)
108
+ else
109
+ raise "universe does not contain element #{value.inspect}"
110
+ end
111
+ end
112
+ end
113
+
114
+ copy(:mask => mask)
115
+ end
116
+
117
+ # @return [AbsoluteSet]
118
+ def select
119
+ mask = 0
120
+
121
+ @universe.each do |value, n|
122
+ unless @mask[n].zero? or not yield(value)
123
+ mask |= (1 << n)
124
+ end
125
+ end
126
+
127
+ copy(:mask => mask)
128
+ end
129
+
130
+ # @return [AbsoluteSet]
131
+ def reject
132
+ mask = 0
133
+
134
+ @universe.each do |value, n|
135
+ unless @mask[n].zero? or yield(value)
136
+ mask |= (1 << n)
137
+ end
138
+ end
139
+
140
+ copy(:mask => mask)
141
+ end
142
+
143
+ # (see AbstractSet#complement)
144
+ def complement
145
+ copy(:mask => ~@mask & ((1 << @universe.size) - 1))
146
+ end
147
+
148
+ # (see AbstractSet#union)
149
+ def union(other)
150
+ if other.is_a?(AbsoluteSet) and other.universe.eql?(@universe)
151
+ copy(:mask => @mask | other.mask)
152
+ elsif other.is_a?(AbstractSet) and other.infinite?
153
+ other.union(self)
154
+ else
155
+ copy(:mask => @mask | as_mask(other, true))
156
+ end
157
+ end
158
+
159
+ # (see AbstractSet#intersection)
160
+ def intersection(other)
161
+ if other.is_a?(AbsoluteSet) and other.universe.eql?(@universe)
162
+ copy(:mask => @mask & other.mask)
163
+ elsif other.is_a?(AbstractSet) and other.infinite?
164
+ other.intersection(self)
165
+ else
166
+ copy(:mask => @mask & as_mask(other, false))
167
+ end
168
+ end
169
+
170
+ # (see AbstractSet#difference)
171
+ def difference(other)
172
+ if other.is_a?(AbsoluteSet) and other.universe.eql?(@universe)
173
+ copy(:mask => @mask & ~other.mask)
174
+ elsif other.is_a?(AbstractSet) and other.infinite?
175
+ intersection(other.complement)
176
+ else
177
+ copy(:mask => @mask & ~as_mask(other, false))
178
+ end
179
+ end
180
+
181
+ # (see AbstractSet#symmetric_difference)
182
+ def symmetric_difference(other)
183
+ if other.is_a?(AbsoluteSet) and other.universe.eql?(@universe)
184
+ copy(:mask => @mask ^ other.mask)
185
+ elsif other.is_a?(AbstractSet) and other.infinite?
186
+ other.symmetric_difference(self)
187
+ else
188
+ copy(:mask => @mask ^ as_mask(other, true))
189
+ end
190
+ end
191
+
192
+ # @group Set Ordering
193
+ #########################################################################
194
+
195
+ # (see AbstractSet#==)
196
+ def ==(other)
197
+ if other.is_a?(AbsoluteSet) and other.universe.eql?(@universe)
198
+ @mask == other.mask
199
+ elsif other.is_a?(AbstractSet) and other.infinite?
200
+ false
201
+ elsif other.is_a?(Enumerable)
202
+ @mask == as_mask(other, false) and size == other.size
203
+ end
204
+ end
205
+
206
+ # @group Pretty Printing
207
+ #########################################################################
208
+
209
+ # @return [void]
210
+ def pretty_print(q)
211
+ q.text("AbsoluteSet[#{size}/#{@universe.size}]")
212
+ q.group(2, "(", ")") do
213
+ q.breakable ""
214
+
215
+ elements = to_a
216
+ elements.take(5).each do |e|
217
+ unless q.current_group.first?
218
+ q.text ","
219
+ q.breakable
220
+ end
221
+ q.pp e
222
+ end
223
+
224
+ if elements.length > 5
225
+ q.text ","
226
+ q.breakable
227
+ q.text "..."
228
+ end
229
+ end
230
+ end
231
+
232
+ # @return [String]
233
+ def inspect
234
+ "AbsoluteSet(#{to_a.map(&:inspect).join(', ')})"
235
+ end
236
+
237
+ # @endgroup
238
+ #########################################################################
239
+
240
+ private
241
+
242
+ # @return [Integer]
243
+ def as_mask(other, strict)
244
+ mask = 0
245
+ size = 0
246
+
247
+ if other.is_a?(AbstractSet) and @universe.size < other.size
248
+ @universe.each do |value, n|
249
+ if other.include?(value)
250
+ mask |= (1 << n)
251
+ size += 1
252
+ end
253
+ end
254
+
255
+ if strict and size < other.size
256
+ # other is not a subset of @universe
257
+ raise ArgumentError,
258
+ "universe does not contain all elements from #{other.inspect}"
259
+ end
260
+ else
261
+ # We might land here if other is an Array, since its probably
262
+ # much worse to repeatedly call Array#include? than it is to
263
+ # iterate the entire Array only once
264
+ other.each do |x|
265
+ if n = @universe.fetch(x, false)
266
+ mask |= (1 << n)
267
+ elsif strict
268
+ raise ArgumentError,
269
+ "universe does not contain element #{x.inspect}"
270
+ end
271
+ end
272
+ end
273
+
274
+ mask
275
+ end
276
+
277
+ end
278
+
279
+ class << AbsoluteSet
280
+ # @group Constructors
281
+ #########################################################################
282
+
283
+ # @return [AbsoluteSet]
284
+ def build(values)
285
+ count = -1
286
+ universe = values.inject({}){|hash, v| hash.update(v => (count += 1)) }
287
+
288
+ new((1 << (count + 1)) - 1, universe)
289
+ end
290
+
291
+ # @endgroup
292
+ #########################################################################
293
+ end
294
+
295
+ end