totally_lazy 0.0.4 → 0.0.5
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/.travis.yml +3 -0
- data/Gemfile +3 -0
- data/Guardfile +24 -0
- data/README.md +31 -0
- data/Rakefile +12 -0
- data/VERSION +1 -1
- data/lib/any.rb +13 -0
- data/lib/generators.rb +161 -0
- data/lib/option.rb +75 -10
- data/lib/pair.rb +6 -0
- data/lib/parallel/parallel.rb +442 -0
- data/lib/parallel/processor_count.rb +85 -0
- data/lib/predicates/compare.rb +32 -0
- data/lib/{predicates.rb → predicates/conversions.rb} +0 -13
- data/lib/predicates/numbers.rb +25 -0
- data/lib/predicates/predicates.rb +57 -0
- data/lib/predicates/where.rb +34 -0
- data/lib/predicates/where_processor.rb +13 -0
- data/lib/sequence.rb +217 -21
- data/lib/totally_lazy.rb +124 -1
- data/spec/functor_spec.rb +35 -0
- data/spec/generators_spec.rb +37 -0
- data/spec/option_spec.rb +73 -2
- data/spec/pair_spec.rb +5 -2
- data/spec/predicate_spec.rb +37 -5
- data/spec/sequence_spec.rb +144 -4
- data/spec/serialization_spec.rb +56 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/type_check_spec.rb +1 -2
- data/totally_lazy.gemspec +28 -4
- metadata +60 -3
@@ -0,0 +1,442 @@
|
|
1
|
+
require 'rbconfig'
|
2
|
+
require_relative 'processor_count'
|
3
|
+
|
4
|
+
module Parallel
|
5
|
+
extend Parallel::ProcessorCount
|
6
|
+
|
7
|
+
class DeadWorker < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
class Break < StandardError
|
11
|
+
end
|
12
|
+
|
13
|
+
class Kill < StandardError
|
14
|
+
end
|
15
|
+
|
16
|
+
Stop = Object.new
|
17
|
+
|
18
|
+
INTERRUPT_SIGNAL = :SIGINT
|
19
|
+
|
20
|
+
class ExceptionWrapper
|
21
|
+
attr_reader :exception
|
22
|
+
def initialize(exception)
|
23
|
+
dumpable = Marshal.dump(exception) rescue nil
|
24
|
+
unless dumpable
|
25
|
+
exception = RuntimeError.new("Undumpable Exception -- #{exception.inspect}")
|
26
|
+
end
|
27
|
+
|
28
|
+
@exception = exception
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Worker
|
33
|
+
attr_reader :pid, :read, :write
|
34
|
+
attr_accessor :thread
|
35
|
+
def initialize(read, write, pid)
|
36
|
+
@read, @write, @pid = read, write, pid
|
37
|
+
end
|
38
|
+
|
39
|
+
def close_pipes
|
40
|
+
read.close
|
41
|
+
write.close
|
42
|
+
end
|
43
|
+
|
44
|
+
def wait
|
45
|
+
Process.wait(pid)
|
46
|
+
rescue Interrupt
|
47
|
+
# process died
|
48
|
+
end
|
49
|
+
|
50
|
+
def work(data)
|
51
|
+
begin
|
52
|
+
Marshal.dump(data, write)
|
53
|
+
rescue Errno::EPIPE
|
54
|
+
raise DeadWorker
|
55
|
+
end
|
56
|
+
|
57
|
+
begin
|
58
|
+
Marshal.load(read)
|
59
|
+
rescue EOFError
|
60
|
+
raise DeadWorker
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class ItemWrapper
|
66
|
+
def initialize(array, mutex)
|
67
|
+
@lambda = (array.respond_to?(:call) && array) || queue_wrapper(array)
|
68
|
+
@items = array.to_a unless @lambda # turn Range and other Enumerable-s into an Array
|
69
|
+
@mutex = mutex
|
70
|
+
@index = -1
|
71
|
+
end
|
72
|
+
|
73
|
+
def producer?
|
74
|
+
@lambda
|
75
|
+
end
|
76
|
+
|
77
|
+
def each_with_index(&block)
|
78
|
+
if producer?
|
79
|
+
loop do
|
80
|
+
item, index = self.next
|
81
|
+
break unless index
|
82
|
+
yield(item, index)
|
83
|
+
end
|
84
|
+
else
|
85
|
+
@items.each_with_index(&block)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def next
|
90
|
+
if producer?
|
91
|
+
# - index and item stay in sync
|
92
|
+
# - do not call lambda after it has returned Stop
|
93
|
+
item, index = @mutex.synchronize do
|
94
|
+
return if @stopped
|
95
|
+
item = @lambda.call
|
96
|
+
@stopped = (item == Parallel::Stop)
|
97
|
+
return if @stopped
|
98
|
+
[item, @index += 1]
|
99
|
+
end
|
100
|
+
else
|
101
|
+
index = @mutex.synchronize { @index += 1 }
|
102
|
+
return if index >= size
|
103
|
+
item = @items[index]
|
104
|
+
end
|
105
|
+
[item, index]
|
106
|
+
end
|
107
|
+
|
108
|
+
def size
|
109
|
+
@items.size
|
110
|
+
end
|
111
|
+
|
112
|
+
def pack(item, index)
|
113
|
+
producer? ? [item, index] : index
|
114
|
+
end
|
115
|
+
|
116
|
+
def unpack(data)
|
117
|
+
producer? ? data : [@items[data], data]
|
118
|
+
end
|
119
|
+
|
120
|
+
def queue_wrapper(array)
|
121
|
+
array.respond_to?(:num_waiting) && array.respond_to?(:pop) && lambda { array.pop(false) }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class << self
|
126
|
+
def in_threads(options={:count => 2})
|
127
|
+
count, options = extract_count_from_options(options)
|
128
|
+
|
129
|
+
out = []
|
130
|
+
threads = []
|
131
|
+
|
132
|
+
count.times do |i|
|
133
|
+
threads[i] = Thread.new do
|
134
|
+
out[i] = yield(i)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
kill_on_ctrl_c(threads) { wait_for_threads(threads) }
|
139
|
+
|
140
|
+
out
|
141
|
+
end
|
142
|
+
|
143
|
+
def in_processes(options = {}, &block)
|
144
|
+
count, options = extract_count_from_options(options)
|
145
|
+
count ||= processor_count
|
146
|
+
map(0...count, options.merge(:in_processes => count), &block)
|
147
|
+
end
|
148
|
+
|
149
|
+
def each(array, options={}, &block)
|
150
|
+
map(array, options.merge(:preserve_results => false), &block)
|
151
|
+
array
|
152
|
+
end
|
153
|
+
|
154
|
+
def each_with_index(array, options={}, &block)
|
155
|
+
each(array, options.merge(:with_index => true), &block)
|
156
|
+
end
|
157
|
+
|
158
|
+
def map(array, options = {}, &block)
|
159
|
+
options[:mutex] = Mutex.new
|
160
|
+
|
161
|
+
if RUBY_PLATFORM =~ /java/ and not options[:in_processes]
|
162
|
+
method = :in_threads
|
163
|
+
size = options[method] || processor_count
|
164
|
+
elsif options[:in_threads]
|
165
|
+
method = :in_threads
|
166
|
+
size = options[method]
|
167
|
+
else
|
168
|
+
method = :in_processes
|
169
|
+
if Process.respond_to?(:fork)
|
170
|
+
size = options[method] || processor_count
|
171
|
+
else
|
172
|
+
$stderr.puts "Warning: Process.fork is not supported by this Ruby"
|
173
|
+
size = 0
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
items = ItemWrapper.new(array, options[:mutex])
|
178
|
+
|
179
|
+
size = [items.producer? ? size : items.size, size].min
|
180
|
+
|
181
|
+
options[:return_results] = (options[:preserve_results] != false || !!options[:finish])
|
182
|
+
add_progress_bar!(items, options)
|
183
|
+
|
184
|
+
if size == 0
|
185
|
+
work_direct(items, options, &block)
|
186
|
+
elsif method == :in_threads
|
187
|
+
work_in_threads(items, options.merge(:count => size), &block)
|
188
|
+
else
|
189
|
+
work_in_processes(items, options.merge(:count => size), &block)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def map_with_index(array, options={}, &block)
|
194
|
+
map(array, options.merge(:with_index => true), &block)
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
def add_progress_bar!(items, options)
|
200
|
+
if title = options[:progress]
|
201
|
+
raise "Progressbar and producers don't mix" if items.producer?
|
202
|
+
require 'ruby-progressbar'
|
203
|
+
progress = ProgressBar.create(
|
204
|
+
:title => title,
|
205
|
+
:total => items.size,
|
206
|
+
:format => '%t |%E | %B | %a'
|
207
|
+
)
|
208
|
+
old_finish = options[:finish]
|
209
|
+
options[:finish] = lambda do |item, i, result|
|
210
|
+
old_finish.call(item, i, result) if old_finish
|
211
|
+
progress.increment
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
def work_direct(items, options)
|
218
|
+
results = []
|
219
|
+
items.each_with_index do |e,i|
|
220
|
+
results << (options[:with_index] ? yield(e,i) : yield(e))
|
221
|
+
end
|
222
|
+
results
|
223
|
+
end
|
224
|
+
|
225
|
+
def work_in_threads(items, options, &block)
|
226
|
+
results = []
|
227
|
+
exception = nil
|
228
|
+
|
229
|
+
in_threads(options[:count]) do
|
230
|
+
# as long as there are more items, work on one of them
|
231
|
+
loop do
|
232
|
+
break if exception
|
233
|
+
item, index = items.next
|
234
|
+
break unless index
|
235
|
+
|
236
|
+
begin
|
237
|
+
results[index] = with_instrumentation item, index, options do
|
238
|
+
call_with_index(item, index, options, &block)
|
239
|
+
end
|
240
|
+
rescue StandardError => e
|
241
|
+
exception = e
|
242
|
+
break
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
handle_exception(exception, results)
|
248
|
+
end
|
249
|
+
|
250
|
+
def work_in_processes(items, options, &blk)
|
251
|
+
workers = create_workers(items, options, &blk)
|
252
|
+
results = []
|
253
|
+
exception = nil
|
254
|
+
|
255
|
+
kill_on_ctrl_c(workers.map(&:pid)) do
|
256
|
+
in_threads(options[:count]) do |i|
|
257
|
+
worker = workers[i]
|
258
|
+
worker.thread = Thread.current
|
259
|
+
|
260
|
+
begin
|
261
|
+
loop do
|
262
|
+
break if exception
|
263
|
+
item, index = items.next
|
264
|
+
break unless index
|
265
|
+
|
266
|
+
output = with_instrumentation item, index, options do
|
267
|
+
worker.work(items.pack(item, index))
|
268
|
+
end
|
269
|
+
|
270
|
+
if ExceptionWrapper === output
|
271
|
+
exception = output.exception
|
272
|
+
if Parallel::Kill === exception
|
273
|
+
(workers - [worker]).each do |w|
|
274
|
+
kill_that_thing!(w.thread)
|
275
|
+
kill_that_thing!(w.pid)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
else
|
279
|
+
results[index] = output
|
280
|
+
end
|
281
|
+
end
|
282
|
+
ensure
|
283
|
+
worker.close_pipes
|
284
|
+
worker.wait # if it goes zombie, rather wait here to be able to debug
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
handle_exception(exception, results)
|
290
|
+
end
|
291
|
+
|
292
|
+
def create_workers(items, options, &block)
|
293
|
+
workers = []
|
294
|
+
Array.new(options[:count]).each do
|
295
|
+
workers << worker(items, options.merge(:started_workers => workers), &block)
|
296
|
+
end
|
297
|
+
workers
|
298
|
+
end
|
299
|
+
|
300
|
+
def worker(items, options, &block)
|
301
|
+
# use less memory on REE
|
302
|
+
GC.copy_on_write_friendly = true if GC.respond_to?(:copy_on_write_friendly=)
|
303
|
+
|
304
|
+
child_read, parent_write = IO.pipe
|
305
|
+
parent_read, child_write = IO.pipe
|
306
|
+
|
307
|
+
pid = Process.fork do
|
308
|
+
begin
|
309
|
+
options.delete(:started_workers).each(&:close_pipes)
|
310
|
+
|
311
|
+
parent_write.close
|
312
|
+
parent_read.close
|
313
|
+
|
314
|
+
process_incoming_jobs(child_read, child_write, items, options, &block)
|
315
|
+
ensure
|
316
|
+
child_read.close
|
317
|
+
child_write.close
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
child_read.close
|
322
|
+
child_write.close
|
323
|
+
|
324
|
+
Worker.new(parent_read, parent_write, pid)
|
325
|
+
end
|
326
|
+
|
327
|
+
def process_incoming_jobs(read, write, items, options, &block)
|
328
|
+
while !read.eof?
|
329
|
+
data = Marshal.load(read)
|
330
|
+
item, index = items.unpack(data)
|
331
|
+
result = begin
|
332
|
+
call_with_index(item, index, options, &block)
|
333
|
+
rescue StandardError => e
|
334
|
+
ExceptionWrapper.new(e)
|
335
|
+
end
|
336
|
+
Marshal.dump(result, write)
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def wait_for_threads(threads)
|
341
|
+
interrupted = threads.compact.map do |t|
|
342
|
+
begin
|
343
|
+
t.join
|
344
|
+
nil
|
345
|
+
rescue Interrupt => e
|
346
|
+
e # thread died, do not stop other threads
|
347
|
+
end
|
348
|
+
end.compact
|
349
|
+
raise interrupted.first if interrupted.first
|
350
|
+
end
|
351
|
+
|
352
|
+
def handle_exception(exception, results)
|
353
|
+
return nil if [Parallel::Break, Parallel::Kill].include? exception.class
|
354
|
+
raise exception if exception
|
355
|
+
results
|
356
|
+
end
|
357
|
+
|
358
|
+
# options is either a Integer or a Hash with :count
|
359
|
+
def extract_count_from_options(options)
|
360
|
+
if options.is_a?(Hash)
|
361
|
+
count = options[:count]
|
362
|
+
else
|
363
|
+
count = options
|
364
|
+
options = {}
|
365
|
+
end
|
366
|
+
[count, options]
|
367
|
+
end
|
368
|
+
|
369
|
+
# kill all these pids or threads if user presses Ctrl+c
|
370
|
+
def kill_on_ctrl_c(things)
|
371
|
+
@to_be_killed ||= []
|
372
|
+
old_interrupt = nil
|
373
|
+
|
374
|
+
if @to_be_killed.empty?
|
375
|
+
old_interrupt = trap_interrupt do
|
376
|
+
$stderr.puts 'Parallel execution interrupted, exiting ...'
|
377
|
+
@to_be_killed.flatten.compact.each { |thing| kill_that_thing!(thing) }
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
@to_be_killed << things
|
382
|
+
|
383
|
+
yield
|
384
|
+
ensure
|
385
|
+
@to_be_killed.pop # free threads for GC and do not kill pids that could be used for new processes
|
386
|
+
restore_interrupt(old_interrupt) if @to_be_killed.empty?
|
387
|
+
end
|
388
|
+
|
389
|
+
def trap_interrupt
|
390
|
+
old = Signal.trap INTERRUPT_SIGNAL, 'IGNORE'
|
391
|
+
|
392
|
+
Signal.trap INTERRUPT_SIGNAL do
|
393
|
+
yield
|
394
|
+
if old == "DEFAULT"
|
395
|
+
raise Interrupt
|
396
|
+
else
|
397
|
+
old.call
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
old
|
402
|
+
end
|
403
|
+
|
404
|
+
def restore_interrupt(old)
|
405
|
+
Signal.trap INTERRUPT_SIGNAL, old
|
406
|
+
end
|
407
|
+
|
408
|
+
def kill_that_thing!(thing)
|
409
|
+
if thing.is_a?(Thread)
|
410
|
+
thing.kill
|
411
|
+
else
|
412
|
+
begin
|
413
|
+
Process.kill(:KILL, thing)
|
414
|
+
rescue Errno::ESRCH
|
415
|
+
# some linux systems already automatically killed the children at this point
|
416
|
+
# so we just ignore them not being there
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
def call_with_index(item, index, options, &block)
|
422
|
+
args = [item]
|
423
|
+
args << index if options[:with_index]
|
424
|
+
if options[:return_results]
|
425
|
+
block.call(*args)
|
426
|
+
else
|
427
|
+
block.call(*args)
|
428
|
+
nil # avoid GC overhead of passing large results around
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
def with_instrumentation(item, index, options)
|
433
|
+
on_start = options[:start]
|
434
|
+
on_finish = options[:finish]
|
435
|
+
options[:mutex].synchronize { on_start.call(item, index) } if on_start
|
436
|
+
result = yield
|
437
|
+
result unless options[:preserve_results] == false
|
438
|
+
ensure
|
439
|
+
options[:mutex].synchronize { on_finish.call(item, index, result) } if on_finish
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Parallel
|
2
|
+
module ProcessorCount
|
3
|
+
# Number of processors seen by the OS and used for process scheduling.
|
4
|
+
#
|
5
|
+
# * AIX: /usr/sbin/pmcycles (AIX 5+), /usr/sbin/lsdev
|
6
|
+
# * BSD: /sbin/sysctl
|
7
|
+
# * Cygwin: /proc/cpuinfo
|
8
|
+
# * Darwin: /usr/bin/hwprefs, /usr/sbin/sysctl
|
9
|
+
# * HP-UX: /usr/sbin/ioscan
|
10
|
+
# * IRIX: /usr/sbin/sysconf
|
11
|
+
# * Linux: /proc/cpuinfo
|
12
|
+
# * Minix 3+: /proc/cpuinfo
|
13
|
+
# * Solaris: /usr/sbin/psrinfo
|
14
|
+
# * Tru64 UNIX: /usr/sbin/psrinfo
|
15
|
+
# * UnixWare: /usr/sbin/psrinfo
|
16
|
+
#
|
17
|
+
def processor_count
|
18
|
+
@processor_count ||= begin
|
19
|
+
os_name = RbConfig::CONFIG["target_os"]
|
20
|
+
if os_name =~ /mingw|mswin/
|
21
|
+
require 'win32ole'
|
22
|
+
result = WIN32OLE.connect("winmgmts://").ExecQuery(
|
23
|
+
"select NumberOfLogicalProcessors from Win32_Processor")
|
24
|
+
result.to_enum.collect(&:NumberOfLogicalProcessors).reduce(:+)
|
25
|
+
elsif File.readable?("/proc/cpuinfo")
|
26
|
+
IO.read("/proc/cpuinfo").scan(/^processor/).size
|
27
|
+
elsif File.executable?("/usr/bin/hwprefs")
|
28
|
+
IO.popen("/usr/bin/hwprefs thread_count").read.to_i
|
29
|
+
elsif File.executable?("/usr/sbin/psrinfo")
|
30
|
+
IO.popen("/usr/sbin/psrinfo").read.scan(/^.*on-*line/).size
|
31
|
+
elsif File.executable?("/usr/sbin/ioscan")
|
32
|
+
IO.popen("/usr/sbin/ioscan -kC processor") do |out|
|
33
|
+
out.read.scan(/^.*processor/).size
|
34
|
+
end
|
35
|
+
elsif File.executable?("/usr/sbin/pmcycles")
|
36
|
+
IO.popen("/usr/sbin/pmcycles -m").read.count("\n")
|
37
|
+
elsif File.executable?("/usr/sbin/lsdev")
|
38
|
+
IO.popen("/usr/sbin/lsdev -Cc processor -S 1").read.count("\n")
|
39
|
+
elsif File.executable?("/usr/sbin/sysconf") and os_name =~ /irix/i
|
40
|
+
IO.popen("/usr/sbin/sysconf NPROC_ONLN").read.to_i
|
41
|
+
elsif File.executable?("/usr/sbin/sysctl")
|
42
|
+
IO.popen("/usr/sbin/sysctl -n hw.ncpu").read.to_i
|
43
|
+
elsif File.executable?("/sbin/sysctl")
|
44
|
+
IO.popen("/sbin/sysctl -n hw.ncpu").read.to_i
|
45
|
+
else
|
46
|
+
$stderr.puts "Unknown platform: " + RbConfig::CONFIG["target_os"]
|
47
|
+
$stderr.puts "Assuming 1 processor."
|
48
|
+
1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Number of physical processor cores on the current system.
|
54
|
+
#
|
55
|
+
def physical_processor_count
|
56
|
+
@physical_processor_count ||= begin
|
57
|
+
ppc = case RbConfig::CONFIG["target_os"]
|
58
|
+
when /darwin1/
|
59
|
+
IO.popen("/usr/sbin/sysctl -n hw.physicalcpu").read.to_i
|
60
|
+
when /linux/
|
61
|
+
cores = {} # unique physical ID / core ID combinations
|
62
|
+
phy = 0
|
63
|
+
IO.read("/proc/cpuinfo").scan(/^physical id.*|^core id.*/) do |ln|
|
64
|
+
if ln.start_with?("physical")
|
65
|
+
phy = ln[/\d+/]
|
66
|
+
elsif ln.start_with?("core")
|
67
|
+
cid = phy + ":" + ln[/\d+/]
|
68
|
+
cores[cid] = true if not cores[cid]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
cores.count
|
72
|
+
when /mswin|mingw/
|
73
|
+
require 'win32ole'
|
74
|
+
result_set = WIN32OLE.connect("winmgmts://").ExecQuery(
|
75
|
+
"select NumberOfCores from Win32_Processor")
|
76
|
+
result_set.to_enum.collect(&:NumberOfCores).reduce(:+)
|
77
|
+
else
|
78
|
+
processor_count
|
79
|
+
end
|
80
|
+
# fall back to logical count if physical info is invalid
|
81
|
+
ppc > 0 ? ppc : processor_count
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Predicates
|
2
|
+
|
3
|
+
module Compare
|
4
|
+
|
5
|
+
def equals(value)
|
6
|
+
-> (v, meth=:self, invert=false) do
|
7
|
+
invert ? inverted_value(v, value, meth, :==) : regular_value(v, value, meth, :==)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
alias equal_to equals
|
12
|
+
|
13
|
+
def greater_than(value)
|
14
|
+
-> (v, meth=:self, invert=false) do
|
15
|
+
invert ? inverted_value(v, value, meth, :>) : regular_value(v, value, meth, :>)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def less_than(value)
|
20
|
+
-> (v, meth=:self, invert=false) do
|
21
|
+
invert ? inverted_value(v, value, meth, :<) : regular_value(v, value, meth, :<)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def matches(regex)
|
26
|
+
-> (v, meth=:self, invert=false) do
|
27
|
+
invert ? inverted_regex(v, regex, meth) : regular_regex(v, regex, meth)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -1,17 +1,5 @@
|
|
1
1
|
module Predicates
|
2
2
|
|
3
|
-
module Numbers
|
4
|
-
|
5
|
-
def even
|
6
|
-
-> (v) { Type.responds(v, :even?); v if v.even? }
|
7
|
-
end
|
8
|
-
|
9
|
-
def odd
|
10
|
-
-> (v) { Type.responds(v, :odd?); v if v.odd? }
|
11
|
-
end
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
3
|
module Conversions
|
16
4
|
|
17
5
|
def as_string
|
@@ -31,5 +19,4 @@ module Predicates
|
|
31
19
|
end
|
32
20
|
|
33
21
|
end
|
34
|
-
|
35
22
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Predicates
|
2
|
+
|
3
|
+
module Numbers
|
4
|
+
|
5
|
+
def even
|
6
|
+
-> (v, meth=:self, invert=false) do
|
7
|
+
invert ? inverted(v, meth, :even?) : regular(v, meth, :even?)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def odd
|
12
|
+
-> (v, meth=:self, invert=false) do
|
13
|
+
invert ? inverted(v, meth, :odd?) : regular(v, meth, :odd?)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
#
|
17
|
+
# def between(lower, higher)
|
18
|
+
# -> (v, meth=:self, invert=false) do
|
19
|
+
# invert ? inverted(v, meth, :between?) : regular(v, meth, :between?)
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Predicates
|
2
|
+
|
3
|
+
def inverted(v, meth, pred)
|
4
|
+
if meth == :self
|
5
|
+
Type.responds(v, pred)
|
6
|
+
v unless v.send(pred)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def regular(v, meth, pred)
|
11
|
+
if meth == :self
|
12
|
+
Type.responds(v, pred)
|
13
|
+
v if v.send(pred)
|
14
|
+
else
|
15
|
+
r = v.send(meth)
|
16
|
+
Type.responds(r, pred)
|
17
|
+
v if r.send(pred)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def inverted_value(v, value, meth, pred)
|
22
|
+
if meth == :self
|
23
|
+
Type.responds(v, pred)
|
24
|
+
v unless v.send(pred, value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def regular_value(v, value, meth, pred)
|
29
|
+
if meth == :self
|
30
|
+
Type.responds(v, pred)
|
31
|
+
v if v.send(pred, value)
|
32
|
+
else
|
33
|
+
r = v.send(meth)
|
34
|
+
Type.responds(r, pred)
|
35
|
+
v if r.send(pred, value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def regular_regex(v, regex, meth)
|
40
|
+
if meth == :self
|
41
|
+
v if v.to_s.match(regex)
|
42
|
+
else
|
43
|
+
r = v.send(meth)
|
44
|
+
v if r.to_s.match(regex)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def inverted_regex(v, regex, meth)
|
49
|
+
if meth == :self
|
50
|
+
v unless v.to_s.match(regex)
|
51
|
+
else
|
52
|
+
r = v.send(meth)
|
53
|
+
v unless r.to_s.match(regex)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Predicates
|
2
|
+
|
3
|
+
module Where
|
4
|
+
class WherePredicate
|
5
|
+
|
6
|
+
attr_reader :predicates
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@predicates = empty
|
10
|
+
end
|
11
|
+
|
12
|
+
def where(predicates)
|
13
|
+
@predicates = predicates.is_a?(Pair::Pair) ? @predicates.append(predicates) : @predicates.join(Pair.from_map(predicates))
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def and(predicates)
|
18
|
+
@predicates = predicates.is_a?(Pair::Pair) ? @predicates.append(predicates) : @predicates.join(Pair.from_map(predicates))
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def where(predicate_map)
|
25
|
+
WherePredicate.new.where(predicate_map)
|
26
|
+
end
|
27
|
+
|
28
|
+
def is(single_predicate)
|
29
|
+
pair(:self, single_predicate)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class WhereProcessor
|
2
|
+
def initialize(value)
|
3
|
+
@value = value
|
4
|
+
end
|
5
|
+
|
6
|
+
def apply(predicates, invert=false)
|
7
|
+
if invert
|
8
|
+
@value if predicates.map { |x| x.value.call(@value, x.key) }.contains?(nil)
|
9
|
+
else
|
10
|
+
@value unless predicates.map { |x| x.value.call(@value, x.key) }.contains?(nil)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|