gecoder 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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