gecoder 0.5.0 → 0.6.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.
Files changed (61) hide show
  1. data/CHANGES +16 -3
  2. data/example/magic_sequence.rb +1 -1
  3. data/example/queens.rb +1 -1
  4. data/example/send_more_money.rb +1 -1
  5. data/example/sudoku.rb +1 -1
  6. data/ext/missing.cpp +18 -4
  7. data/ext/missing.h +8 -0
  8. data/lib/gecoder/bindings.rb +30 -3
  9. data/lib/gecoder/bindings/bindings.rb +22 -0
  10. data/lib/gecoder/interface/binding_changes.rb +81 -107
  11. data/lib/gecoder/interface/branch.rb +65 -14
  12. data/lib/gecoder/interface/constraints.rb +1 -0
  13. data/lib/gecoder/interface/constraints/bool_enum/boolean.rb +16 -12
  14. data/lib/gecoder/interface/constraints/int/arithmetic.rb +7 -3
  15. data/lib/gecoder/interface/constraints/int/linear.rb +19 -16
  16. data/lib/gecoder/interface/constraints/int_enum/arithmetic.rb +8 -4
  17. data/lib/gecoder/interface/constraints/int_enum/channel.rb +14 -6
  18. data/lib/gecoder/interface/constraints/int_enum/element.rb +7 -5
  19. data/lib/gecoder/interface/constraints/int_enum/sort.rb +1 -4
  20. data/lib/gecoder/interface/constraints/set/cardinality.rb +6 -3
  21. data/lib/gecoder/interface/constraints/set/connection.rb +136 -0
  22. data/lib/gecoder/interface/constraints/set_enum/channel.rb +18 -0
  23. data/lib/gecoder/interface/constraints/set_enum/distinct.rb +61 -0
  24. data/lib/gecoder/interface/constraints/set_enum_constraints.rb +32 -0
  25. data/lib/gecoder/interface/constraints/set_var_constraints.rb +1 -0
  26. data/lib/gecoder/interface/enum_wrapper.rb +12 -3
  27. data/lib/gecoder/interface/model.rb +77 -56
  28. data/lib/gecoder/interface/search.rb +74 -5
  29. data/lib/gecoder/interface/variables.rb +117 -15
  30. data/lib/gecoder/version.rb +1 -1
  31. data/specs/binding_changes.rb +9 -5
  32. data/specs/bool_var.rb +8 -12
  33. data/specs/branch.rb +85 -19
  34. data/specs/constraints/arithmetic.rb +99 -71
  35. data/specs/constraints/bool_enum.rb +26 -18
  36. data/specs/constraints/boolean.rb +53 -49
  37. data/specs/constraints/cardinality.rb +33 -26
  38. data/specs/constraints/channel.rb +77 -6
  39. data/specs/constraints/connection.rb +352 -0
  40. data/specs/constraints/constraints.rb +10 -1
  41. data/specs/constraints/count.rb +79 -39
  42. data/specs/constraints/distinct.rb +128 -9
  43. data/specs/constraints/element.rb +26 -19
  44. data/specs/constraints/equality.rb +2 -1
  45. data/specs/constraints/int_domain.rb +19 -12
  46. data/specs/constraints/int_relation.rb +12 -6
  47. data/specs/constraints/linear.rb +30 -30
  48. data/specs/constraints/reification_sugar.rb +8 -4
  49. data/specs/constraints/set_domain.rb +24 -18
  50. data/specs/constraints/set_relation.rb +38 -23
  51. data/specs/constraints/sort.rb +12 -10
  52. data/specs/enum_wrapper.rb +9 -3
  53. data/specs/int_var.rb +8 -4
  54. data/specs/logging.rb +24 -0
  55. data/specs/model.rb +25 -7
  56. data/specs/search.rb +41 -1
  57. data/specs/set_var.rb +36 -7
  58. data/specs/spec_helper.rb +3 -10
  59. data/vendor/rust/rust/templates/FunctionDefinition.rusttpl +1 -1
  60. metadata +12 -3
  61. data/specs/tmp +0 -22
@@ -0,0 +1,32 @@
1
+ module Gecode
2
+ module SetEnumMethods
3
+ include Gecode::Constraints::LeftHandSideMethods
4
+
5
+ private
6
+
7
+ # Produces an expression for the lhs module.
8
+ def expression(params)
9
+ params.update(:lhs => self)
10
+ Constraints::SetEnum::Expression.new(@model, params)
11
+ end
12
+ end
13
+
14
+ # A module containing constraints that have enumerations of set variables as
15
+ # left hand side.
16
+ module Constraints::SetEnum
17
+ # Expressions with set enums as left hand sides.
18
+ class Expression < Gecode::Constraints::Expression
19
+ # Raises TypeError unless the left hand side is a set enum.
20
+ def initialize(model, params)
21
+ super
22
+
23
+ unless params[:lhs].respond_to? :to_set_var_array
24
+ raise TypeError, 'Must have set enum as left hand side.'
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ require 'gecoder/interface/constraints/set_enum/channel'
32
+ require 'gecoder/interface/constraints/set_enum/distinct'
@@ -35,3 +35,4 @@ end
35
35
  require 'gecoder/interface/constraints/set/domain'
36
36
  require 'gecoder/interface/constraints/set/relation'
37
37
  require 'gecoder/interface/constraints/set/cardinality'
38
+ require 'gecoder/interface/constraints/set/connection'
@@ -45,10 +45,19 @@ module Gecode
45
45
  end
46
46
  end
47
47
 
48
+ module VariableEnumMethods
49
+ include EnumMethods
50
+
51
+ # Gets the values of all the variables in the enum.
52
+ def values
53
+ map{ |var| var.value }
54
+ end
55
+ end
56
+
48
57
  # A module containing the methods needed by enumerations containing int
49
58
  # variables. Requires that it's included in an enumerable.
50
59
  module IntEnumMethods
51
- include EnumMethods
60
+ include VariableEnumMethods
52
61
 
53
62
  # Returns an int variable array with all the bound variables.
54
63
  def to_int_var_array
@@ -80,7 +89,7 @@ module Gecode
80
89
  # A module containing the methods needed by enumerations containing boolean
81
90
  # variables. Requires that it's included in an enumerable.
82
91
  module BoolEnumMethods
83
- include EnumMethods
92
+ include VariableEnumMethods
84
93
 
85
94
  # Returns a bool variable array with all the bound variables.
86
95
  def to_bool_var_array
@@ -99,7 +108,7 @@ module Gecode
99
108
  # A module containing the methods needed by enumerations containing set
100
109
  # variables. Requires that it's included in an enumerable.
101
110
  module SetEnumMethods
102
- include EnumMethods
111
+ include VariableEnumMethods
103
112
 
104
113
  # Returns a set variable array with all the bound variables.
105
114
  def to_set_var_array
@@ -1,15 +1,12 @@
1
1
  module Gecode
2
2
  # Model is the base class that all models must inherit from.
3
3
  class Model
4
- attr :constraints
5
- protected :constraints
6
-
7
4
  # Creates a new integer variable with the specified domain. The domain can
8
5
  # either be a range or a number of elements.
9
6
  def int_var(*domain_args)
10
- range = domain_range(*domain_args)
11
- index = active_space.new_int_vars(range.begin, range.end).first
12
- construct_int_var(index, *domain_args)
7
+ enum = domain_enum(*domain_args)
8
+ index = selected_space.new_int_vars(enum).first
9
+ FreeIntVar.new(self, index)
13
10
  end
14
11
 
15
12
  # Creates an array containing the specified number of integer variables
@@ -18,10 +15,10 @@ module Gecode
18
15
  def int_var_array(count, *domain_args)
19
16
  # TODO: Maybe the custom domain should be specified as an array instead?
20
17
 
21
- range = domain_range(*domain_args)
18
+ enum = domain_enum(*domain_args)
22
19
  variables = []
23
- active_space.new_int_vars(range.begin, range.end, count).each do |index|
24
- variables << construct_int_var(index, *domain_args)
20
+ selected_space.new_int_vars(enum, count).each do |index|
21
+ variables << FreeIntVar.new(self, index)
25
22
  end
26
23
  return wrap_enum(variables)
27
24
  end
@@ -32,13 +29,12 @@ module Gecode
32
29
  def int_var_matrix(row_count, col_count, *domain_args)
33
30
  # TODO: Maybe the custom domain should be specified as an array instead?
34
31
 
35
- range = domain_range(*domain_args)
36
- indices = active_space.new_int_vars(range.begin, range.end,
37
- row_count*col_count)
32
+ enum = domain_enum(*domain_args)
33
+ indices = selected_space.new_int_vars(enum, row_count*col_count)
38
34
  rows = []
39
35
  row_count.times do |i|
40
36
  rows << indices[(i*col_count)...(i.succ*col_count)].map! do |index|
41
- construct_int_var(index, *domain_args)
37
+ FreeIntVar.new(self, index)
42
38
  end
43
39
  end
44
40
  return wrap_enum(Util::EnumMatrix.rows(rows, false))
@@ -46,14 +42,14 @@ module Gecode
46
42
 
47
43
  # Creates a new boolean variable.
48
44
  def bool_var(*domain_args)
49
- index = active_space.new_bool_vars.first
45
+ index = selected_space.new_bool_vars.first
50
46
  FreeBoolVar.new(self, index)
51
47
  end
52
48
 
53
49
  # Creates an array containing the specified number of boolean variables.
54
50
  def bool_var_array(count)
55
51
  variables = []
56
- active_space.new_bool_vars(count).each do |index|
52
+ selected_space.new_bool_vars(count).each do |index|
57
53
  variables << FreeBoolVar.new(self, index)
58
54
  end
59
55
  return wrap_enum(variables)
@@ -62,7 +58,7 @@ module Gecode
62
58
  # Creates a matrix containing the specified number rows and columns of
63
59
  # boolean variables.
64
60
  def bool_var_matrix(row_count, col_count)
65
- indices = active_space.new_bool_vars(row_count*col_count)
61
+ indices = selected_space.new_bool_vars(row_count*col_count)
66
62
  rows = []
67
63
  row_count.times do |i|
68
64
  rows << indices[(i*col_count)...(i.succ*col_count)].map! do |index|
@@ -81,7 +77,7 @@ module Gecode
81
77
  def set_var(glb_domain, lub_domain, cardinality_range = nil)
82
78
  check_set_bounds(glb_domain, lub_domain)
83
79
 
84
- index = active_space.new_set_vars(glb_domain, lub_domain,
80
+ index = selected_space.new_set_vars(glb_domain, lub_domain,
85
81
  to_set_cardinality_range(cardinality_range)).first
86
82
  FreeSetVar.new(self, index)
87
83
  end
@@ -92,7 +88,7 @@ module Gecode
92
88
  check_set_bounds(glb_domain, lub_domain)
93
89
 
94
90
  variables = []
95
- active_space.new_set_vars(glb_domain, lub_domain,
91
+ selected_space.new_set_vars(glb_domain, lub_domain,
96
92
  to_set_cardinality_range(cardinality_range), count).each do |index|
97
93
  variables << FreeSetVar.new(self, index)
98
94
  end
@@ -106,7 +102,7 @@ module Gecode
106
102
  cardinality_range = nil)
107
103
  check_set_bounds(glb_domain, lub_domain)
108
104
 
109
- indices = active_space.new_set_vars(glb_domain, lub_domain,
105
+ indices = selected_space.new_set_vars(glb_domain, lub_domain,
110
106
  to_set_cardinality_range(cardinality_range), row_count*col_count)
111
107
  rows = []
112
108
  row_count.times do |i|
@@ -117,53 +113,83 @@ module Gecode
117
113
  return wrap_enum(Util::EnumMatrix.rows(rows, false))
118
114
  end
119
115
 
120
- # Retrieves the currently active space (the one which variables refer to).
116
+ # Retrieves the currently used space. Calling this method is only allowed
117
+ # when sanctioned by the model beforehand, e.g. when the model asks a
118
+ # constraint to post itself. Otherwise an RuntimeError is raised.
119
+ #
120
+ # The space returned by this method should never be stored, it should be
121
+ # rerequested from the model every time that it's needed.
121
122
  def active_space
122
- @active_space ||= base_space
123
- end
124
-
125
- # Retrieves the base from which searches are made.
126
- def base_space
127
- @base_space ||= Gecode::Raw::Space.new
123
+ unless @allow_space_access
124
+ raise 'Space access is restricted and the permission to access the ' +
125
+ 'space has not been given.'
126
+ end
127
+ selected_space
128
128
  end
129
129
 
130
130
  # Adds the specified constraint to the model. Returns the newly added
131
131
  # constraint.
132
132
  def add_constraint(constraint)
133
- constraints << constraint
133
+ add_interaction do
134
+ constraint.post
135
+ end
134
136
  return constraint
135
137
  end
136
138
 
139
+ # Adds a block containing something that interacts with Gecode to a queue
140
+ # where it is potentially executed.
141
+ def add_interaction(&block)
142
+ gecode_interaction_queue << block
143
+ end
144
+
145
+ # Allows the model's active space to be accessed while the block is
146
+ # executed. Don't use this unless you know what you're doing. Anything that
147
+ # the space is used for (such as bound variables) must be released before
148
+ # the block ends.
149
+ #
150
+ # Returns the result of the block.
151
+ def allow_space_access(&block)
152
+ # We store the old value so that nested calls don't become a problem, i.e.
153
+ # access is allowed as long as one call to this method is still on the
154
+ # stack.
155
+ old = @allow_space_access
156
+ @allow_space_access = true
157
+ res = yield
158
+ @allow_space_access = old
159
+ return res
160
+ end
161
+
137
162
  protected
138
163
 
139
- def constraints
140
- @constraints ||= []
164
+ # Gets a queue of objects that can be posted to the model's active_space
165
+ # (by calling their post method).
166
+ def gecode_interaction_queue
167
+ @gecode_interaction_queue ||= []
141
168
  end
142
169
 
143
170
  private
144
171
 
145
- # Returns the range of the specified domain arguments, which can either be
146
- # given as a range or a number of elements. Raises ArgumentError if no
147
- # arguments have been specified.
148
- def domain_range(*domain_args)
149
- min = max = nil
172
+ # Returns an enumeration of the specified domain arguments, which can
173
+ # either be given as a range or a number of elements. Raises ArgumentError
174
+ # if no arguments have been specified.
175
+ def domain_enum(*domain_args)
150
176
  if domain_args.empty?
151
177
  raise ArgumentError, 'A domain has to be specified.'
152
178
  elsif domain_args.size > 1
153
- min = domain_args.min
154
- max = domain_args.max
179
+ return domain_args
155
180
  else
156
181
  element = domain_args.first
157
- if element.respond_to?(:begin) and element.respond_to?(:end) and
182
+ if element.respond_to?(:first) and element.respond_to?(:last) and
158
183
  element.respond_to?(:exclude_end?)
159
- min = element.begin
160
- max = element.end
161
- max -= 1 if element.exclude_end?
184
+ if element.exclude_end?
185
+ return element.first..(element.last - 1)
186
+ else
187
+ return element
188
+ end
162
189
  else
163
- min = max = element
190
+ return element..element
164
191
  end
165
192
  end
166
- return min..max
167
193
  end
168
194
 
169
195
  # Transforms the argument to a set cardinality range, returns nil if the
@@ -196,20 +222,15 @@ module Gecode
196
222
  end
197
223
  end
198
224
 
199
- # Creates an integer variable from the specified index and domain. The
200
- # domain can either be given as a range or as a number of elements.
201
- def construct_int_var(index, *domain_args)
202
- var = FreeIntVar.new(self, index)
203
-
204
- if domain_args.size > 1
205
- # Place an additional domain constraint on the variable with the
206
- # arguments as domain. We post it directly since there's no reason not
207
- # to and the user might otherwise get unexpected domains when inspecting
208
- # the variable before solving.
209
- constraint = var.must_be.in domain_args
210
- @constraints.delete(constraint).post
211
- end
212
- return var
225
+ # Retrieves the base from which searches are made.
226
+ def base_space
227
+ @base_space ||= Gecode::Raw::Space.new
228
+ end
229
+
230
+ # Retrieves the currently selected space, the one which constraints and
231
+ # variables should be bound to.
232
+ def selected_space
233
+ @active_space ||= base_space
213
234
  end
214
235
  end
215
236
  end
@@ -36,20 +36,89 @@ module Gecode
36
36
  self.reset!
37
37
  end
38
38
 
39
+ # Finds the optimal solution. Optimality is defined by the provided block
40
+ # which is given one parameter, a solution to the problem. The block should
41
+ # constrain the solution so that that only "better" solutions can be new
42
+ # solutions. For instance if one wants to optimize a variable named price
43
+ # (accessible from the model) to be as low as possible then one should write
44
+ # the following.
45
+ #
46
+ # model.optimize! do |model, best_so_far|
47
+ # model.price.must < best_so_far.price.val
48
+ # end
49
+ #
50
+ # Returns nil if there is no solution.
51
+ def optimize!(&block)
52
+ next_space = nil
53
+ best_space = nil
54
+ bab = bab_engine
55
+
56
+ Model.constrain_proc = lambda do |home_space, best_space|
57
+ @active_space = best_space
58
+ yield(self, self)
59
+ @active_space = home_space
60
+ perform_queued_gecode_interactions
61
+ end
62
+
63
+ while not (next_space = bab.next).nil?
64
+ best_space = next_space
65
+ end
66
+ Model.constrain_proc = nil
67
+ return nil if best_space.nil?
68
+ return self
69
+ end
70
+
71
+ class <<self
72
+ def constrain_proc=(proc)
73
+ @constrain_proc = proc
74
+ end
75
+
76
+ def constrain(home, best)
77
+ if @constrain_proc.nil?
78
+ raise NotImplementedError, 'Constrain method not implemented.'
79
+ else
80
+ @constrain_proc.call(home, best)
81
+ end
82
+ end
83
+ end
84
+
39
85
  private
40
86
 
41
- # Creates an DFS engine for the search, executing any unexecuted
42
- # constraints first.
87
+ # Creates a depth first search engine for search, executing any
88
+ # unexecuted constraints first.
43
89
  def dfs_engine
44
90
  # Execute constraints.
45
- constraints.each{ |con| con.post }
46
- constraints.clear # Empty the queue.
91
+ perform_queued_gecode_interactions
92
+
93
+ # Construct the engine.
94
+ stop = Gecode::Raw::Search::Stop.new
95
+ Gecode::Raw::DFS.new(selected_space,
96
+ Gecode::Raw::Search::Config::MINIMAL_DISTANCE,
97
+ Gecode::Raw::Search::Config::ADAPTIVE_DISTANCE,
98
+ stop)
99
+ end
47
100
 
101
+ # Creates a branch and bound engine for optimization search, executing any
102
+ # unexecuted constraints first.
103
+ def bab_engine
104
+ # Execute constraints.
105
+ perform_queued_gecode_interactions
106
+
107
+ # Construct the engine.
48
108
  stop = Gecode::Raw::Search::Stop.new
49
- Gecode::Raw::DFS.new(active_space,
109
+ Gecode::Raw::BAB.new(selected_space,
50
110
  Gecode::Raw::Search::Config::MINIMAL_DISTANCE,
51
111
  Gecode::Raw::Search::Config::ADAPTIVE_DISTANCE,
52
112
  stop)
53
113
  end
114
+
115
+ # Executes any interactions with Gecode still waiting in the queue
116
+ # (emptying the queue) in the process.
117
+ def perform_queued_gecode_interactions
118
+ allow_space_access do
119
+ gecode_interaction_queue.each{ |con| con.call }
120
+ gecode_interaction_queue.clear # Empty the queue.
121
+ end
122
+ end
54
123
  end
55
124
  end
@@ -22,16 +22,7 @@ module Gecode
22
22
  # class using the specified method in a space.
23
23
  def Gecode::FreeVar(bound_class, space_bind_method)
24
24
  clazz = Class.new(FreeVarBase)
25
- clazz.class_eval <<-"end_method_definitions"
26
- # Delegate methods we can't handle to the bound int variable if possible.
27
- def method_missing(name, *args)
28
- if #{bound_class}.instance_methods.include? name.to_s
29
- bind.send(name, *args)
30
- else
31
- super
32
- end
33
- end
34
-
25
+ clazz.class_eval <<-"end_method_definitions"
35
26
  # Binds the int variable to the currently active space of the model,
36
27
  # returning the bound int variable.
37
28
  def bind
@@ -51,6 +42,29 @@ module Gecode
51
42
  "#<\#{self.class} \#{domain}>"
52
43
  end
53
44
  end
45
+
46
+ private
47
+
48
+ # Delegates the method with the specified name to a method with the
49
+ # specified name when the variable is bound. If the bound method's name
50
+ # is nil then the same name as the new method's name is assumed.
51
+ def self.delegate(method_name, bound_method_name = nil)
52
+ bound_method_name = method_name if bound_method_name.nil?
53
+ module_eval <<-"end_code"
54
+ def \#{method_name}(*args)
55
+ @model.allow_space_access do
56
+ bind.method(:\#{bound_method_name}).call(*args)
57
+ end
58
+ end
59
+ end_code
60
+ end
61
+
62
+ # Sends the specified method name and arguments to the bound variable.
63
+ def send_bound(method_name, *args)
64
+ @model.allow_space_access do
65
+ bind.send(method_name, *args)
66
+ end
67
+ end
54
68
  end_method_definitions
55
69
  return clazz
56
70
  end
@@ -58,10 +72,25 @@ module Gecode
58
72
  # Int variables.
59
73
  FreeIntVar = FreeVar(Gecode::Raw::IntVar, :int_var)
60
74
  class FreeIntVar
75
+ delegate :min
76
+ delegate :max
77
+ delegate :size
78
+ delegate :width
79
+ delegate :degree
80
+ delegate :range?, :range
81
+ delegate :assigned?, :assigned
82
+ delegate :include?, :in
83
+
84
+ # Gets the value of the assigned integer variable (a fixnum).
85
+ def value
86
+ raise 'No value is assigned.' unless assigned?
87
+ send_bound(:val)
88
+ end
89
+
61
90
  # Returns a string representation of the the range of the variable's domain.
62
91
  def domain
63
92
  if assigned?
64
- "range: #{val.to_s}"
93
+ "range: #{value.to_s}"
65
94
  else
66
95
  "range: #{min}..#{max}"
67
96
  end
@@ -71,25 +100,98 @@ module Gecode
71
100
  # Bool variables.
72
101
  FreeBoolVar = FreeVar(Gecode::Raw::BoolVar, :bool_var)
73
102
  class FreeBoolVar
103
+ delegate :assigned?, :assigned
104
+
105
+ # Gets the values in the assigned boolean variable (true or false).
106
+ def value
107
+ raise 'No value is assigned.' unless assigned?
108
+ send_bound(:val) == 1
109
+ end
110
+
74
111
  # Returns a string representation of the the variable's domain.
75
112
  def domain
76
113
  if assigned?
77
- true?.to_s
114
+ value.to_s
78
115
  else
79
116
  'unassigned'
80
117
  end
81
118
  end
82
119
  end
83
-
120
+
84
121
  # Set variables.
85
122
  FreeSetVar = FreeVar(Gecode::Raw::SetVar, :set_var)
86
123
  class FreeSetVar
124
+ delegate :assigned?, :assigned
125
+
126
+ # Gets all the elements located in the greatest lower bound of the set.
127
+ def lower_bound
128
+ min = send_bound(:glbMin)
129
+ max = send_bound(:glbMax)
130
+ EnumerableView.new(min, max, send_bound(:glbSize)) do
131
+ (min..max).to_a.delete_if{ |e| not send_bound(:contains, e) }
132
+ end
133
+ end
134
+
135
+ # Gets all the elements located in the least upper bound of the set.
136
+ def upper_bound
137
+ min = send_bound(:lubMin)
138
+ max = send_bound(:lubMax)
139
+ EnumerableView.new(min, max, send_bound(:lubSize)) do
140
+ (min..max).to_a.delete_if{ |e| send_bound(:notContains, e) }
141
+ end
142
+ end
143
+
144
+ # Gets the values in the assigned set variable (an enumerable).
145
+ def value
146
+ raise 'No value is assigned.' unless assigned?
147
+ lower_bound
148
+ end
149
+
150
+ # Returns a range containing the allowed values for the set's cardinality.
151
+ def cardinality
152
+ send_bound(:cardMin)..send_bound(:cardMax)
153
+ end
154
+
87
155
  # Returns a string representation of the the variable's domain.
88
156
  def domain
89
157
  if assigned?
90
- "#{glb_min}..#{lub_min}"
158
+ lower_bound.to_a.inspect
159
+ else
160
+ "glb-range: #{lower_bound.to_a.inspect}, lub-range: #{upper_bound.to_a.inspect}"
161
+ end
162
+ end
163
+ end
164
+
165
+ # Describes an immutable view of an enumerable.
166
+ class EnumerableView
167
+ attr :size
168
+ attr :min
169
+ attr :max
170
+ include Enumerable
171
+
172
+ # Constructs a view with the specified minimum, maximum and size. The block
173
+ # should construct an enumerable containing the elements of the set.
174
+ def initialize(min, max, size, &enum_constructor)
175
+ @min = min
176
+ @max = max
177
+ @size = size
178
+ @constructor = enum_constructor
179
+ @enum = nil
180
+ end
181
+
182
+ # Used by Enumerable.
183
+ def each(&block)
184
+ enum.each(&block)
185
+ end
186
+
187
+ private
188
+
189
+ # Gets the enumeration being viewed.
190
+ def enum
191
+ if @enum.nil?
192
+ @enum = @constructor.call
91
193
  else
92
- "glb-range: #{glb_min}..#{glb_max}, lub-range: #{lub_min}..#{lub_max}"
194
+ return @enum
93
195
  end
94
196
  end
95
197
  end