enumerable_fu 0.1.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.
- data/.gitignore +4 -0
- data/Gemfile +9 -0
- data/README.markdown +50 -0
- data/Rakefile +13 -0
- data/benchmarks/pipeline_bench.rb +71 -0
- data/enumerable_fu.gemspec +21 -0
- data/lib/enumerable_fu/filtering.rb +68 -0
- data/lib/enumerable_fu/merging.rb +70 -0
- data/lib/enumerable_fu/mixing.rb +2 -0
- data/lib/enumerable_fu/version.rb +3 -0
- data/lib/enumerable_fu/zipping.rb +36 -0
- data/lib/enumerable_fu.rb +2 -0
- data/spec/enumerable_fu/filtering_spec.rb +62 -0
- data/spec/enumerable_fu/merging_spec.rb +35 -0
- data/spec/enumerable_fu/zipping_spec.rb +22 -0
- data/spec/spec_helper.rb +37 -0
- metadata +74 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
EnumerableFu
|
2
|
+
============
|
3
|
+
|
4
|
+
"EnumerableFu" extends `Enumerable` with "lazy" versions of various operations,
|
5
|
+
allowing streamed processing of large (or even infinite) collections.
|
6
|
+
|
7
|
+
It also provides some interesting ways of combining Enumerables.
|
8
|
+
|
9
|
+
Filters
|
10
|
+
-------
|
11
|
+
|
12
|
+
`selecting` (cf. `select`, or `find_all`) returns each element for which the given block is true
|
13
|
+
|
14
|
+
(1..6).selecting { |x| x.even? } # generates: 2, 4, 6
|
15
|
+
|
16
|
+
`rejecting` (cf. `reject`) returns each element for which the given block is false:
|
17
|
+
|
18
|
+
(1..6).rejecting { |x| x.even? } # generates: 1, 3, 5
|
19
|
+
|
20
|
+
`collecting` (cf. `collect`, or `map`) applies a block to each element:
|
21
|
+
|
22
|
+
[1,2,3].collecting { |x| x*2 } # generates: 2, 4, 6
|
23
|
+
|
24
|
+
`uniqing` (cf. `uniq`) returns unique elements:
|
25
|
+
|
26
|
+
[2,1,2,3,2,1].uniqing # generates: 2, 1, 3
|
27
|
+
|
28
|
+
Unlike the original methods, the "...ing" variants each return an immutable Enumerable object, rather than an Array. The actual processing is deferred until the result Enumerable is enumerated (e.g. with `each`), and elements are produced "just in time".
|
29
|
+
|
30
|
+
Mixers
|
31
|
+
------
|
32
|
+
|
33
|
+
`Enumerable.zipping` pulls elements from a number of Enumerables in parallel, yielding each group.
|
34
|
+
|
35
|
+
array1 = [1,3,6]
|
36
|
+
array2 = [2,4,7]
|
37
|
+
Enumerable.zipping(array1, array2) # generates: [1,2], [3,4], [6,7]
|
38
|
+
|
39
|
+
`Enumerable.merging` merges multiple Enumerables, preserving sort-order. The inputs are assumed to be sorted already.
|
40
|
+
|
41
|
+
array1 = [1,4,5]
|
42
|
+
array2 = [2,3,6]
|
43
|
+
Enumerable.merging(array1, array2) # generates: 1, 2, 3, 4, 5, 6
|
44
|
+
|
45
|
+
Variant `Enumerable.merging_by` uses a block to determine sort-order.
|
46
|
+
|
47
|
+
array1 = %w(a dd cccc)
|
48
|
+
array2 = %w(eee bbbbb)
|
49
|
+
Enumerable.merging_by(array1, array2) { |x| x.length }
|
50
|
+
# generates: %w(a dd eee cccc bbbbb)
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "rake"
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
Bundler::GemHelper.install_tasks
|
5
|
+
|
6
|
+
require "rspec/core/rake_task"
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new do |t|
|
9
|
+
t.pattern = 'spec/**/*_spec.rb'
|
10
|
+
t.rspec_opts = ["--colour", "--format", "nested"]
|
11
|
+
end
|
12
|
+
|
13
|
+
task "default" => "spec"
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
|
3
|
+
$: << File.expand_path("../../lib", __FILE__)
|
4
|
+
|
5
|
+
RUBY19 = RUBY_VERSION =~ /^1\.9/
|
6
|
+
|
7
|
+
if RUBY19
|
8
|
+
require "lazing"
|
9
|
+
module Enumerable
|
10
|
+
alias :lazing_select :selecting
|
11
|
+
alias :lazing_collect :collecting
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
require "enumerable_fu"
|
16
|
+
|
17
|
+
require 'facets'
|
18
|
+
|
19
|
+
array = (1..100000).to_a
|
20
|
+
|
21
|
+
# Test scenario:
|
22
|
+
# - filter out even numbers
|
23
|
+
# - square them
|
24
|
+
# - grab the first thousand
|
25
|
+
|
26
|
+
printf "%-30s", "IMPLEMENTATION"
|
27
|
+
printf "%12s", "take(10)"
|
28
|
+
printf "%12s", "take(100)"
|
29
|
+
printf "%12s", "take(1000)"
|
30
|
+
printf "%12s", "to_a"
|
31
|
+
puts ""
|
32
|
+
|
33
|
+
def measure(&block)
|
34
|
+
begin
|
35
|
+
printf "%12.5f", Benchmark.realtime(&block)
|
36
|
+
rescue
|
37
|
+
printf "%12s", "n/a"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def benchmark(description, control_result = nil)
|
42
|
+
result = nil
|
43
|
+
printf "%-30s", description
|
44
|
+
measure { yield.take(10).to_a }
|
45
|
+
measure { yield.take(100).to_a }
|
46
|
+
measure { result = yield.take(1000).to_a }
|
47
|
+
measure { yield.to_a }
|
48
|
+
puts ""
|
49
|
+
unless control_result.nil? || result == control_result
|
50
|
+
raise "unexpected result from '#{description}'"
|
51
|
+
end
|
52
|
+
result
|
53
|
+
end
|
54
|
+
|
55
|
+
@control = benchmark "conventional" do
|
56
|
+
array.select { |x| x.even? }.collect { |x| x*x }
|
57
|
+
end
|
58
|
+
|
59
|
+
benchmark "enumerable_fu", @control do
|
60
|
+
array.selecting { |x| x.even? }.collecting { |x| x*x }
|
61
|
+
end
|
62
|
+
|
63
|
+
if RUBY19
|
64
|
+
benchmark "lazing", @control do
|
65
|
+
array.lazing_select { |x| x.even? }.lazing_collect { |x| x*x }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
benchmark "facets Enumerable#defer", @control do
|
70
|
+
array.defer.select { |x| x.even? }.collect { |x| x*x }
|
71
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "enumerable_fu/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
|
7
|
+
s.name = "enumerable_fu"
|
8
|
+
s.version = EnumerableFu::VERSION.dup
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ["Mike Williams"]
|
11
|
+
s.email = "mdub@dogbiscuit.org"
|
12
|
+
s.homepage = "http://github.com/mdub/enumerable_fu"
|
13
|
+
|
14
|
+
s.summary = %{Lazy extensions to Enumerable}
|
15
|
+
s.description = %{EnumerableFu extends Enumerable with "lazy" versions of various operations, allowing streamed processing of large (or even infinite) collections.}
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module EnumerableFu
|
4
|
+
|
5
|
+
class Filter
|
6
|
+
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
def initialize(&generator)
|
10
|
+
@generator = generator
|
11
|
+
end
|
12
|
+
|
13
|
+
def each
|
14
|
+
return to_enum unless block_given?
|
15
|
+
yielder = proc { |x| yield x }
|
16
|
+
@generator.call(yielder)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
module Enumerable
|
24
|
+
|
25
|
+
def collecting
|
26
|
+
EnumerableFu::Filter.new do |output|
|
27
|
+
each do |element|
|
28
|
+
output.call yield(element)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def selecting
|
34
|
+
EnumerableFu::Filter.new do |output|
|
35
|
+
each do |element|
|
36
|
+
output.call(element) if yield(element)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def rejecting
|
42
|
+
EnumerableFu::Filter.new do |output|
|
43
|
+
each do |element|
|
44
|
+
output.call(element) unless yield(element)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def uniqing
|
50
|
+
EnumerableFu::Filter.new do |output|
|
51
|
+
seen = Set.new
|
52
|
+
each do |element|
|
53
|
+
output.call(element) if seen.add?(element)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def uniqing_by
|
59
|
+
EnumerableFu::Filter.new do |output|
|
60
|
+
seen = Set.new
|
61
|
+
each do |element|
|
62
|
+
output.call(element) if seen.add?(yield element)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module EnumerableFu
|
2
|
+
|
3
|
+
class Merger
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(enumerables, &transformer)
|
8
|
+
@enumerables = enumerables
|
9
|
+
@transformer = transformer
|
10
|
+
end
|
11
|
+
|
12
|
+
def each(&block)
|
13
|
+
return to_enum unless block_given?
|
14
|
+
Generator.new(@enumerables.map(&:to_enum), @transformer).each(&block)
|
15
|
+
end
|
16
|
+
|
17
|
+
class Generator
|
18
|
+
|
19
|
+
def initialize(enumerators, transformer)
|
20
|
+
@enumerators = enumerators
|
21
|
+
@transformer = transformer
|
22
|
+
end
|
23
|
+
|
24
|
+
def each
|
25
|
+
while true do
|
26
|
+
discard_empty_enumerators
|
27
|
+
break if @enumerators.empty?
|
28
|
+
yield next_enumerator.next
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def discard_empty_enumerators
|
35
|
+
@enumerators.delete_if do |e|
|
36
|
+
begin
|
37
|
+
e.peek
|
38
|
+
false
|
39
|
+
rescue StopIteration
|
40
|
+
true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def next_enumerator
|
46
|
+
@enumerators.min_by { |enumerator| transform(enumerator.peek) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def transform(item)
|
50
|
+
return item unless @transformer
|
51
|
+
@transformer.call(item)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
class << Enumerable
|
61
|
+
|
62
|
+
def merging(*enumerables)
|
63
|
+
EnumerableFu::Merger.new(enumerables)
|
64
|
+
end
|
65
|
+
|
66
|
+
def merging_by(*enumerables, &block)
|
67
|
+
EnumerableFu::Merger.new(enumerables, &block)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module EnumerableFu
|
2
|
+
|
3
|
+
class Zipper
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(enumerables)
|
8
|
+
@enumerables = enumerables
|
9
|
+
end
|
10
|
+
|
11
|
+
def each
|
12
|
+
enumerators = @enumerables.map(&:to_enum)
|
13
|
+
while true
|
14
|
+
chunk = enumerators.map do |enumerator|
|
15
|
+
begin
|
16
|
+
enumerator.next
|
17
|
+
rescue StopIteration
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
break if chunk.all?(&:nil?)
|
22
|
+
yield chunk
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
class << Enumerable
|
31
|
+
|
32
|
+
def zipping(*enumerables)
|
33
|
+
EnumerableFu::Zipper.new(enumerables)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Enumerable do
|
4
|
+
|
5
|
+
describe "#collecting" do
|
6
|
+
|
7
|
+
it "transforms items" do
|
8
|
+
[1,2,3].collecting { |x| x * 2 }.to_a.should == [2,4,6]
|
9
|
+
end
|
10
|
+
|
11
|
+
it "is lazy" do
|
12
|
+
[1,2,3].with_time_bomb.collecting { |x| x * 2 }.first.should == 2
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#selecting" do
|
18
|
+
|
19
|
+
it "excludes items that don't pass the predicate" do
|
20
|
+
(1..6).selecting { |x| x.even? }.to_a.should == [2,4,6]
|
21
|
+
end
|
22
|
+
|
23
|
+
it "is lazy" do
|
24
|
+
(1..6).with_time_bomb.selecting { |x| x.even? }.first == 2
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#rejecting" do
|
30
|
+
|
31
|
+
it "excludes items that do pass the predicate" do
|
32
|
+
(1..6).rejecting { |x| x.even? }.to_a.should == [1,3,5]
|
33
|
+
end
|
34
|
+
|
35
|
+
it "is lazy" do
|
36
|
+
(1..6).with_time_bomb.rejecting { |x| x.even? }.first == 1
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#uniqing" do
|
42
|
+
|
43
|
+
it "removes duplicates" do
|
44
|
+
[1,3,2,4,3,5,4,6].uniqing.to_a.should == [1,3,2,4,5,6]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "is lazy" do
|
48
|
+
[1,2,3].with_time_bomb.uniqing.first.should == 1
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "#uniqing_by" do
|
54
|
+
|
55
|
+
it "uses the block to derive identity" do
|
56
|
+
@array = %w(A1 A2 B1 A3 C1 B2 C2)
|
57
|
+
@array.uniqing_by { |s| s[0,1] }.to_a.should == %w(A1 B1 C1)
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Enumerable, :needs_enumerators => true do
|
4
|
+
|
5
|
+
describe ".merging" do
|
6
|
+
|
7
|
+
it "merges multiple Enumerators" do
|
8
|
+
@array1 = [1,3,6]
|
9
|
+
@array2 = [2,4,7]
|
10
|
+
@array3 = [5,8]
|
11
|
+
@merge = Enumerable.merging(@array1, @array2, @array3)
|
12
|
+
@merge.to_a.should == [1,2,3,4,5,6,7,8]
|
13
|
+
end
|
14
|
+
|
15
|
+
it "is lazy" do
|
16
|
+
@enum1 = [1,3]
|
17
|
+
@enum2 = [2,4].with_time_bomb
|
18
|
+
@merge = Enumerable.merging(@enum1, @enum2)
|
19
|
+
@merge.take(4).should == [1,2,3,4]
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
describe ".merging_by" do
|
25
|
+
|
26
|
+
it "uses the block to determine order" do
|
27
|
+
@array1 = %w(cccc dd a)
|
28
|
+
@array2 = %w(eeeee bbb)
|
29
|
+
@merge = Enumerable.merging_by(@array1, @array2) { |s| -s.length }
|
30
|
+
@merge.to_a.should == %w(eeeee cccc bbb dd a)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Enumerable, :needs_enumerators => true do
|
4
|
+
|
5
|
+
describe "#zipping" do
|
6
|
+
|
7
|
+
it "zips together multiple Enumerables" do
|
8
|
+
@array1 = [1,3,6]
|
9
|
+
@array2 = [2,4,7]
|
10
|
+
@array3 = [5,8]
|
11
|
+
@zip = Enumerable.zipping(@array1, @array2, @array3)
|
12
|
+
@zip.to_a.should == [[1,2,5], [3,4,8], [6,7,nil]]
|
13
|
+
end
|
14
|
+
|
15
|
+
it "is lazy" do
|
16
|
+
@zip = Enumerable.zipping(%w(a b c), [1,2].with_time_bomb)
|
17
|
+
@zip.take(2).should == [["a", 1], ["b", 2]]
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
|
3
|
+
require 'rspec'
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
unless defined?(::Enumerator)
|
7
|
+
config.filter_run_excluding :needs_enumerators => true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Boom < StandardError; end
|
12
|
+
|
13
|
+
class WithTimeBomb
|
14
|
+
|
15
|
+
include Enumerable
|
16
|
+
|
17
|
+
def initialize(source)
|
18
|
+
@source = source
|
19
|
+
end
|
20
|
+
|
21
|
+
def each(&block)
|
22
|
+
@source.each(&block)
|
23
|
+
raise Boom
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
module Enumerable
|
29
|
+
|
30
|
+
# extend an Enumerable to throw an exception after last element
|
31
|
+
def with_time_bomb
|
32
|
+
WithTimeBomb.new(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
require "enumerable_fu"
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: enumerable_fu
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.1.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mike Williams
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-04-14 00:00:00 +10:00
|
14
|
+
default_executable:
|
15
|
+
dependencies: []
|
16
|
+
|
17
|
+
description: EnumerableFu extends Enumerable with "lazy" versions of various operations, allowing streamed processing of large (or even infinite) collections.
|
18
|
+
email: mdub@dogbiscuit.org
|
19
|
+
executables: []
|
20
|
+
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files: []
|
24
|
+
|
25
|
+
files:
|
26
|
+
- .gitignore
|
27
|
+
- Gemfile
|
28
|
+
- README.markdown
|
29
|
+
- Rakefile
|
30
|
+
- benchmarks/pipeline_bench.rb
|
31
|
+
- enumerable_fu.gemspec
|
32
|
+
- lib/enumerable_fu.rb
|
33
|
+
- lib/enumerable_fu/filtering.rb
|
34
|
+
- lib/enumerable_fu/merging.rb
|
35
|
+
- lib/enumerable_fu/mixing.rb
|
36
|
+
- lib/enumerable_fu/version.rb
|
37
|
+
- lib/enumerable_fu/zipping.rb
|
38
|
+
- spec/enumerable_fu/filtering_spec.rb
|
39
|
+
- spec/enumerable_fu/merging_spec.rb
|
40
|
+
- spec/enumerable_fu/zipping_spec.rb
|
41
|
+
- spec/spec_helper.rb
|
42
|
+
has_rdoc: true
|
43
|
+
homepage: http://github.com/mdub/enumerable_fu
|
44
|
+
licenses: []
|
45
|
+
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: "0"
|
63
|
+
requirements: []
|
64
|
+
|
65
|
+
rubyforge_project:
|
66
|
+
rubygems_version: 1.6.2
|
67
|
+
signing_key:
|
68
|
+
specification_version: 3
|
69
|
+
summary: Lazy extensions to Enumerable
|
70
|
+
test_files:
|
71
|
+
- spec/enumerable_fu/filtering_spec.rb
|
72
|
+
- spec/enumerable_fu/merging_spec.rb
|
73
|
+
- spec/enumerable_fu/zipping_spec.rb
|
74
|
+
- spec/spec_helper.rb
|