prop_check 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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +4 -0
- data/.tool-versions +1 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +0 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +48 -0
- data/LICENSE.txt +21 -0
- data/README.md +182 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/prop_check.rb +17 -0
- data/lib/prop_check/generator.rb +95 -0
- data/lib/prop_check/generators.rb +415 -0
- data/lib/prop_check/helper.rb +27 -0
- data/lib/prop_check/lazy_tree.rb +142 -0
- data/lib/prop_check/property.rb +207 -0
- data/lib/prop_check/property/check_evaluator.rb +45 -0
- data/lib/prop_check/property/configuration.rb +14 -0
- data/lib/prop_check/rspec.rb +14 -0
- data/lib/prop_check/version.rb +3 -0
- data/prop_check.gemspec +40 -0
- metadata +119 -0
@@ -0,0 +1,415 @@
|
|
1
|
+
require 'prop_check/generator'
|
2
|
+
require 'prop_check/lazy_tree'
|
3
|
+
module PropCheck
|
4
|
+
##
|
5
|
+
# Contains common generators.
|
6
|
+
# Use this module by including it in the class (e.g. in your test suite)
|
7
|
+
# where you want to use them.
|
8
|
+
module Generators
|
9
|
+
extend self
|
10
|
+
##
|
11
|
+
# Always returns the same value, regardless of `size` or `rng` (random number generator state)
|
12
|
+
#
|
13
|
+
# No shrinking (only considers the current single value `val`).
|
14
|
+
#
|
15
|
+
# >> Generators.constant("pie").sample(5, size: 10, rng: Random.new(42))
|
16
|
+
# => ["pie", "pie", "pie", "pie", "pie"]
|
17
|
+
def constant(val)
|
18
|
+
Generator.wrap(val)
|
19
|
+
end
|
20
|
+
|
21
|
+
private def integer_shrink(val)
|
22
|
+
# 0 cannot shrink further; base case
|
23
|
+
return [] if val.zero?
|
24
|
+
|
25
|
+
# Numbers are shrunken by
|
26
|
+
# subtracting themselves, their half, quarter, eight, ... (rounded towards zero!)
|
27
|
+
# from themselves, until the number itself is reached.
|
28
|
+
# So: for 20 we have [0, 10, 15, 18, 19, 20]
|
29
|
+
halvings =
|
30
|
+
Helper
|
31
|
+
.scanl(val) { |x| (x / 2.0).truncate }
|
32
|
+
.take_while { |x| !x.zero? }
|
33
|
+
.map { |x| val - x }
|
34
|
+
.map { |x| LazyTree.new(x, integer_shrink(x)) }
|
35
|
+
|
36
|
+
# For negative numbers, we also attempt if the positive number has the same result.
|
37
|
+
if val.abs > val
|
38
|
+
[LazyTree.new(val.abs, halvings)].lazy
|
39
|
+
else
|
40
|
+
halvings
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Returns a random integer in the given range (if a range is given)
|
46
|
+
# or between 0..num (if a single integer is given).
|
47
|
+
#
|
48
|
+
# Does not scale when `size` changes.
|
49
|
+
# This means `choose` is useful for e.g. picking an element out of multiple possibilities,
|
50
|
+
# but for other purposes you probably want to use `integer` et co.
|
51
|
+
#
|
52
|
+
# Shrinks to integers closer to zero.
|
53
|
+
#
|
54
|
+
# >> r = Random.new(42); Generators.choose(0..5).sample(size: 10, rng: r)
|
55
|
+
# => [3, 4, 2, 4, 4, 1, 2, 2, 2, 4]
|
56
|
+
# >> r = Random.new(42); Generators.choose(0..5).sample(size: 20000, rng: r)
|
57
|
+
# => [3, 4, 2, 4, 4, 1, 2, 2, 2, 4]
|
58
|
+
def choose(range)
|
59
|
+
Generator.new do |_size, rng|
|
60
|
+
val = rng.rand(range)
|
61
|
+
LazyTree.new(val, integer_shrink(val))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# A random integer which scales with `size`.
|
67
|
+
# Integers start small (around 0)
|
68
|
+
# and become more extreme (both higher and lower, negative) when `size` increases.
|
69
|
+
#
|
70
|
+
#
|
71
|
+
# Shrinks to integers closer to zero.
|
72
|
+
#
|
73
|
+
# >> Generators.integer.call(2, Random.new(42))
|
74
|
+
# => 1
|
75
|
+
# >> Generators.integer.call(10000, Random.new(42))
|
76
|
+
# => 5795
|
77
|
+
# >> r = Random.new(42); Generators.integer.sample(size: 20000, rng: r)
|
78
|
+
# => [-4205, -19140, 18158, -8716, -13735, -3150, 17194, 1962, -3977, -18315]
|
79
|
+
def integer
|
80
|
+
Generator.new do |size, rng|
|
81
|
+
val = rng.rand(-size..size)
|
82
|
+
LazyTree.new(val, integer_shrink(val))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Only returns integers that are zero or larger.
|
88
|
+
# See `integer` for more information.
|
89
|
+
def nonnegative_integer
|
90
|
+
integer.map(&:abs)
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Only returns integers that are larger than zero.
|
95
|
+
# See `integer` for more information.
|
96
|
+
def positive_integer
|
97
|
+
nonnegative_integer.map { |x| x + 1 }
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Only returns integers that are zero or smaller.
|
102
|
+
# See `integer` for more information.
|
103
|
+
def nonpositive_integer
|
104
|
+
nonnegative_integer.map(&:-@)
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Only returns integers that are smaller than zero.
|
109
|
+
# See `integer` for more information.
|
110
|
+
def negative_integer
|
111
|
+
positive_integer.map(&:-@)
|
112
|
+
end
|
113
|
+
|
114
|
+
private def fraction(num_a, num_b, num_c)
|
115
|
+
num_a.to_f + num_b.to_f / (num_c.to_f.abs + 1.0)
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Generates floating point numbers
|
120
|
+
# These start small (around 0)
|
121
|
+
# and become more extreme (large positive and large negative numbers)
|
122
|
+
#
|
123
|
+
#
|
124
|
+
# Shrinks to numbers closer to zero.
|
125
|
+
#
|
126
|
+
# TODO testing for NaN, Infinity?
|
127
|
+
def float
|
128
|
+
# integer.bind do |a|
|
129
|
+
# integer.bind do |b|
|
130
|
+
# integer.bind do |c|
|
131
|
+
# Generator.wrap(fraction(a, b, c))
|
132
|
+
# end
|
133
|
+
# end
|
134
|
+
# end
|
135
|
+
tuple(integer, integer, integer).map do |a, b, c|
|
136
|
+
fraction(a, b, c)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# Picks one of the given generators in `choices` at random uniformly every time.
|
142
|
+
#
|
143
|
+
# Shrinks to values earlier in the list of `choices`.
|
144
|
+
#
|
145
|
+
# >> Generators.one_of(Generators.constant(true), Generators.constant(false)).sample(5, size: 10, rng: Random.new(42))
|
146
|
+
# => [true, false, true, true, true]
|
147
|
+
def one_of(*choices)
|
148
|
+
choose(choices.length).bind do |index|
|
149
|
+
choices[index]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# Picks one of the choices given in `frequencies` at random every time.
|
155
|
+
# `frequencies` expects keys to be numbers
|
156
|
+
# (representing the relative frequency of this generator)
|
157
|
+
# and values to be generators.
|
158
|
+
#
|
159
|
+
# Shrinks to arbitrary elements (since hashes are not ordered).
|
160
|
+
#
|
161
|
+
# >> Generators.frequency(5 => Generators.integer, 1 => Generators.printable_ascii_char).sample(size: 10, rng: Random.new(42))
|
162
|
+
# => [4, -3, 10, 8, 0, -7, 10, 1, "E", 10]
|
163
|
+
def frequency(frequencies)
|
164
|
+
choices = frequencies.reduce([]) do |acc, elem|
|
165
|
+
freq, val = elem
|
166
|
+
acc + ([val] * freq)
|
167
|
+
end
|
168
|
+
one_of(*choices)
|
169
|
+
end
|
170
|
+
|
171
|
+
##
|
172
|
+
# Generates an array containing always exactly one value from each of the passed generators,
|
173
|
+
# in the same order as specified:
|
174
|
+
#
|
175
|
+
# Shrinks element generators, one at a time (trying last one first).
|
176
|
+
#
|
177
|
+
# >> Generators.tuple(Generators.integer, Generators.float).call(10, Random.new(42))
|
178
|
+
# => [-4, 13.0]
|
179
|
+
def tuple(*generators)
|
180
|
+
Generator.new do |size, rng|
|
181
|
+
LazyTree.zip(generators.map do |generator|
|
182
|
+
generator.generate(size, rng)
|
183
|
+
end)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
##
|
188
|
+
# Given a `hash` where the values are generators,
|
189
|
+
# creates a generator that returns hashes
|
190
|
+
# with the same keys, and their corresponding values from their corresponding generators.
|
191
|
+
#
|
192
|
+
# Shrinks element generators.
|
193
|
+
#
|
194
|
+
# >> Generators.fixed_hash(a: Generators.integer(), b: Generators.float(), c: Generators.integer()).call(10, Random.new(42))
|
195
|
+
# => {:a=>-4, :b=>13.0, :c=>-3}
|
196
|
+
def fixed_hash(hash)
|
197
|
+
keypair_generators =
|
198
|
+
hash.map do |key, generator|
|
199
|
+
generator.map { |val| [key, val] }
|
200
|
+
end
|
201
|
+
|
202
|
+
tuple(*keypair_generators)
|
203
|
+
.map(&:to_h)
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# Generates an array of elements, where each of the elements
|
208
|
+
# is generated by `element_generator`.
|
209
|
+
#
|
210
|
+
# Shrinks to shorter arrays (with shrunken elements).
|
211
|
+
#
|
212
|
+
# >> Generators.array(Generators.positive_integer).sample(5, size: 10, rng: Random.new(42))
|
213
|
+
# => [[10, 5, 1, 4], [5, 9, 1, 1, 11, 8, 4, 9, 11, 10], [6], [11, 11, 2, 2, 7, 2, 6, 5, 5], [2, 10, 9, 7, 9, 5, 11, 3]]
|
214
|
+
def array(element_generator)
|
215
|
+
nonnegative_integer.bind do |generator|
|
216
|
+
generators = (0...generator).map do
|
217
|
+
element_generator.clone
|
218
|
+
end
|
219
|
+
|
220
|
+
tuple(*generators)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
##
|
225
|
+
# Generates a hash of key->values,
|
226
|
+
# where each of the keys is made using the `key_generator`
|
227
|
+
# and each of the values using the `value_generator`.
|
228
|
+
#
|
229
|
+
# Shrinks to hashes with less key/value pairs.
|
230
|
+
#
|
231
|
+
# >> Generators.hash(Generators.printable_ascii_string, Generators.positive_integer).sample(5, size: 3, rng: Random.new(42))
|
232
|
+
# => [{""=>2, "g\\4"=>4, "rv"=>2}, {"7"=>2}, {"!"=>1, "E!"=>1}, {"kY5"=>2}, {}]
|
233
|
+
def hash(key_generator, value_generator)
|
234
|
+
array(tuple(key_generator, value_generator))
|
235
|
+
.map(&:to_h)
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
@alphanumeric_chars = [('a'..'z'), ('A'..'Z'), ('0'..'9')].flat_map(&:to_a).freeze
|
240
|
+
##
|
241
|
+
# Generates a single-character string
|
242
|
+
# containing one of a..z, A..Z, 0..9
|
243
|
+
#
|
244
|
+
# Shrinks towards lowercase 'a'.
|
245
|
+
#
|
246
|
+
def alphanumeric_char
|
247
|
+
one_of(*@alphanumeric_chars.map(&method(:constant)))
|
248
|
+
end
|
249
|
+
|
250
|
+
##
|
251
|
+
# Generates a string
|
252
|
+
# containing only the characters a..z, A..Z, 0..9
|
253
|
+
# Shrinks towards fewer characters, and towards lowercase 'a'.
|
254
|
+
def alphanumeric_string
|
255
|
+
array(alphanumeric_char).map(&:join)
|
256
|
+
end
|
257
|
+
|
258
|
+
@printable_ascii_chars = (' '..'~').to_a.freeze
|
259
|
+
|
260
|
+
##
|
261
|
+
# Generates a single-character string
|
262
|
+
# from the printable ASCII character set.
|
263
|
+
#
|
264
|
+
# Shrinks towards ' '.
|
265
|
+
#
|
266
|
+
# >> Generators.printable_ascii_char.sample(size: 10, rng: Random.new(42))
|
267
|
+
# => ["S", "|", ".", "g", "\\", "4", "r", "v", "j", "j"]
|
268
|
+
def printable_ascii_char
|
269
|
+
one_of(*@printable_ascii_chars.map(&method(:constant)))
|
270
|
+
end
|
271
|
+
|
272
|
+
##
|
273
|
+
# Generates strings
|
274
|
+
# from the printable ASCII character set.
|
275
|
+
# Shrinks towards fewer characters, and towards ' '.
|
276
|
+
def printable_ascii_string
|
277
|
+
array(printable_ascii_char).map(&:join)
|
278
|
+
end
|
279
|
+
|
280
|
+
@ascii_chars = [
|
281
|
+
@printable_ascii_chars,
|
282
|
+
[
|
283
|
+
"\n",
|
284
|
+
"\r",
|
285
|
+
"\t",
|
286
|
+
"\v",
|
287
|
+
"\b",
|
288
|
+
"\f",
|
289
|
+
"\e",
|
290
|
+
"\d",
|
291
|
+
"\a"
|
292
|
+
]
|
293
|
+
].flat_map(&:to_a).freeze
|
294
|
+
|
295
|
+
##
|
296
|
+
# Generates a single-character string
|
297
|
+
# from the printable ASCII character set.
|
298
|
+
# Shrinks towards '\n'.
|
299
|
+
def ascii_char
|
300
|
+
one_of(*@ascii_chars.map(&method(:constant)))
|
301
|
+
end
|
302
|
+
|
303
|
+
##
|
304
|
+
# Generates strings
|
305
|
+
# from the printable ASCII character set.
|
306
|
+
# Shrinks towards fewer characters, and towards '\n'.
|
307
|
+
def ascii_string
|
308
|
+
array(ascii_char).map(&:join)
|
309
|
+
end
|
310
|
+
|
311
|
+
@printable_chars = [
|
312
|
+
@ascii_chars,
|
313
|
+
"\u{A0}".."\u{D7FF}",
|
314
|
+
"\u{E000}".."\u{FFFD}",
|
315
|
+
"\u{10000}".."\u{10FFFF}"
|
316
|
+
].flat_map(&:to_a).freeze
|
317
|
+
|
318
|
+
##
|
319
|
+
# Generates a single-character printable string
|
320
|
+
# both ASCII characters and Unicode.
|
321
|
+
#
|
322
|
+
# Shrinks towards characters with lower codepoints, e.g. ASCII
|
323
|
+
#
|
324
|
+
def printable_char
|
325
|
+
one_of(*@printable_chars.map(&method(:constant)))
|
326
|
+
end
|
327
|
+
|
328
|
+
##
|
329
|
+
# Generates a printable string
|
330
|
+
# both ASCII characters and Unicode.
|
331
|
+
#
|
332
|
+
# Shrinks towards shorter strings, and towards characters with lower codepoints, e.g. ASCII
|
333
|
+
#
|
334
|
+
def printable_string
|
335
|
+
array(printable_char).map(&:join)
|
336
|
+
end
|
337
|
+
|
338
|
+
##
|
339
|
+
# Generates a single unicode character
|
340
|
+
# (both printable and non-printable).
|
341
|
+
#
|
342
|
+
# Shrinks towards characters with lower codepoints, e.g. ASCII
|
343
|
+
#
|
344
|
+
def char
|
345
|
+
choose(0..0x10FFFF).map do |num|
|
346
|
+
[num].pack('U')
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
##
|
351
|
+
# Generates a string of unicode characters
|
352
|
+
# (which might contain both printable and non-printable characters).
|
353
|
+
#
|
354
|
+
# Shrinks towards characters with lower codepoints, e.g. ASCII
|
355
|
+
#
|
356
|
+
def string
|
357
|
+
array(char).map(&:join)
|
358
|
+
end
|
359
|
+
|
360
|
+
##
|
361
|
+
# Generates either `true` or `false`
|
362
|
+
#
|
363
|
+
# Shrinks towards `false`
|
364
|
+
#
|
365
|
+
def boolean
|
366
|
+
one_of(constant(false), constant(true))
|
367
|
+
end
|
368
|
+
|
369
|
+
##
|
370
|
+
# Generates always `nil`.
|
371
|
+
#
|
372
|
+
# Does not shrink.
|
373
|
+
def nil
|
374
|
+
constant(nil)
|
375
|
+
end
|
376
|
+
|
377
|
+
##
|
378
|
+
# Generates `nil` or `false`.
|
379
|
+
#
|
380
|
+
# Shrinks towards `nil`.
|
381
|
+
#
|
382
|
+
def falsey
|
383
|
+
one_of(constant(nil), constant(false))
|
384
|
+
end
|
385
|
+
|
386
|
+
##
|
387
|
+
# Generates common terms that are not `nil` or `false`.
|
388
|
+
#
|
389
|
+
# Shrinks towards simpler terms, like `true`, an empty array, a single character or an integer.
|
390
|
+
#
|
391
|
+
def truthy
|
392
|
+
one_of(constant(true),
|
393
|
+
constant([]),
|
394
|
+
char,
|
395
|
+
integer,
|
396
|
+
float,
|
397
|
+
string,
|
398
|
+
array(integer),
|
399
|
+
array(float),
|
400
|
+
array(char),
|
401
|
+
array(string),
|
402
|
+
hash(symbol, integer),
|
403
|
+
hash(string, integer),
|
404
|
+
hash(string, string)
|
405
|
+
)
|
406
|
+
end
|
407
|
+
|
408
|
+
##
|
409
|
+
# Generates whatever `other_generator` generates
|
410
|
+
# but sometimes instead `nil`.`
|
411
|
+
def nillable(other_generator)
|
412
|
+
frequency(9 => other_generator, 1 => constant(nil))
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module PropCheck
|
2
|
+
##
|
3
|
+
# Helper functions that have no other place to live
|
4
|
+
module Helper
|
5
|
+
extend self
|
6
|
+
##
|
7
|
+
# Creates a (potentially lazy) Enumerator
|
8
|
+
# starting with `elem`
|
9
|
+
# with each consecutive element obtained
|
10
|
+
# by calling `operation` on the previous element.
|
11
|
+
#
|
12
|
+
# >> Helper.scanl(0, &:next).take(10).force
|
13
|
+
# => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
14
|
+
# >> Helper.scanl([0, 1]) { |curr, next_elem| [next_elem, curr + next_elem] }.map(&:first).take(10).force
|
15
|
+
# => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
|
16
|
+
def scanl(elem, &operation)
|
17
|
+
Enumerator.new do |yielder|
|
18
|
+
acc = elem
|
19
|
+
loop do
|
20
|
+
# p acc
|
21
|
+
yielder << acc
|
22
|
+
acc = operation.call(acc)
|
23
|
+
end
|
24
|
+
end.lazy
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
##
|
2
|
+
# A refinement for enumerators
|
3
|
+
# to allow lazy appending of two (potentially lazy) enumerators:
|
4
|
+
module LazyAppend
|
5
|
+
refine Enumerable do
|
6
|
+
## >> [1,2,3].lazy_append([4,5.6]).to_a
|
7
|
+
## => [1,2,3,4,5,6]
|
8
|
+
def lazy_append(other_enumerator)
|
9
|
+
[self, other_enumerator].lazy.flat_map(&:lazy)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module PropCheck
|
15
|
+
##
|
16
|
+
# A Rose tree with the root being eager,
|
17
|
+
# and the children computed lazily, on demand.
|
18
|
+
class LazyTree
|
19
|
+
using LazyAppend
|
20
|
+
|
21
|
+
attr_accessor :root, :children
|
22
|
+
def initialize(root, children = [].lazy)
|
23
|
+
@root = root
|
24
|
+
@children = children
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Maps `block` eagerly over `root` and lazily over `children`, returning a new LazyTree as result.
|
29
|
+
#
|
30
|
+
# >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).map(&:next).to_a
|
31
|
+
# => LazyTree.new(2, [LazyTree.new(3, [LazyTree.new(4)]), LazyTree.new(5)]).to_a
|
32
|
+
def map(&block)
|
33
|
+
new_root = block.call(root)
|
34
|
+
new_children = children.map { |child_tree| child_tree.map(&block) }
|
35
|
+
LazyTree.new(new_root, new_children)
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Turns a tree of trees
|
40
|
+
# in a single flattened tree, with subtrees that are closer to the root
|
41
|
+
# and the left subtree earlier in the list of children.
|
42
|
+
# TODO: Check for correctness
|
43
|
+
# def flatten
|
44
|
+
# root_tree = root
|
45
|
+
# root_root = root_tree.root
|
46
|
+
|
47
|
+
# root_children = root_tree.children
|
48
|
+
# flattened_children = children.map(&:flatten)
|
49
|
+
|
50
|
+
# combined_children = root_children.lazy_append(flattened_children)
|
51
|
+
|
52
|
+
# LazyTree.new(root_root, combined_children)
|
53
|
+
# end
|
54
|
+
|
55
|
+
def self.wrap(val)
|
56
|
+
LazyTree.new(val)
|
57
|
+
end
|
58
|
+
|
59
|
+
def bind(&fun)
|
60
|
+
inner_tree = fun.call(root)
|
61
|
+
inner_root = inner_tree.root
|
62
|
+
inner_children = inner_tree.children
|
63
|
+
mapped_children = children.map { |child| child.bind(&fun) }
|
64
|
+
|
65
|
+
combined_children = inner_children.lazy_append(mapped_children)
|
66
|
+
|
67
|
+
LazyTree.new(inner_root, combined_children)
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Turns a LazyTree in a long lazy enumerable, with the root first followed by its children
|
72
|
+
# (and the first children's result before later children; i.e. a depth-first traversal.)
|
73
|
+
#
|
74
|
+
# Be aware that this lazy enumerable is potentially infinite,
|
75
|
+
# possibly uncountably so.
|
76
|
+
#
|
77
|
+
# >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).each.force
|
78
|
+
# => [1, 4, 2, 3]
|
79
|
+
def each(&block)
|
80
|
+
squish = lambda do |tree, list|
|
81
|
+
new_children = tree.children.reduce(list) { |acc, elem| squish.call(elem, acc) }
|
82
|
+
[tree.root].lazy_append(new_children)
|
83
|
+
end
|
84
|
+
|
85
|
+
squish.call(self, [])
|
86
|
+
|
87
|
+
# base = [root]
|
88
|
+
# recursive = children.map(&:each)
|
89
|
+
# res = base.lazy_append(recursive)
|
90
|
+
|
91
|
+
# return res.each(&block) if block_given?
|
92
|
+
|
93
|
+
# res
|
94
|
+
|
95
|
+
# res = [[root], children.flat_map(&:each)].lazy.flat_map(&:lazy)
|
96
|
+
# res = res.map(&block) if block_given?
|
97
|
+
# res
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Fully evaluate the LazyTree into an eager array, with the root first followed by its children
|
102
|
+
# (and the first children's result before later children; i.e. a depth-first traversal.)
|
103
|
+
#
|
104
|
+
# Be aware that calling this might make Ruby attempt to evaluate an infinite collection.
|
105
|
+
# Therefore, it is mostly useful for debugging; in production you probably want to use
|
106
|
+
# the other mechanisms this class provides..
|
107
|
+
#
|
108
|
+
# >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).to_a
|
109
|
+
# => [1, 4, 2, 3]
|
110
|
+
def to_a
|
111
|
+
each.force
|
112
|
+
end
|
113
|
+
|
114
|
+
# TODO: fix implementation
|
115
|
+
def self.zip(trees)
|
116
|
+
# p "TREES: "
|
117
|
+
# p trees.to_a
|
118
|
+
# p "END TREES"
|
119
|
+
# raise "Boom!" unless trees.to_a.is_a?(Array) && trees.to_a.first.is_a?(LazyTree)
|
120
|
+
# p self
|
121
|
+
new_root = trees.to_a.map(&:root)
|
122
|
+
# p new_root
|
123
|
+
# new_children = trees.permutations.flat_map(&:children)
|
124
|
+
new_children = permutations(trees).map { |children| LazyTree.zip(children) }
|
125
|
+
# p new_children
|
126
|
+
LazyTree.new(new_root, new_children)
|
127
|
+
end
|
128
|
+
|
129
|
+
private_class_method def self.permutations(trees)
|
130
|
+
# p trees
|
131
|
+
trees.lazy.each_with_index.flat_map do |tree, index|
|
132
|
+
tree.children.map do |child|
|
133
|
+
child_trees = trees.to_a.clone
|
134
|
+
child_trees[index] = child
|
135
|
+
# p "CHILD TREES:"
|
136
|
+
# p child_trees
|
137
|
+
child_trees.lazy
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|