crosscounter 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ Gemfile.lock
6
+ vendor
7
+ bin
8
+ TODO.*
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in crosscounter.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Parker Selbert
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/NOTES.txt ADDED
@@ -0,0 +1,70 @@
1
+ ---
2
+
3
+ n = 1_000_000
4
+ Benchmark.bm do |x|
5
+ x.report { n.times { friendly_words.include?('mandarin') } }
6
+ x.report { n.times { friendly_string =~ /mandarin/ } }
7
+ x.report { n.times { friendly_string['mandarin'] } }
8
+ x.report { n.times { friendly_string.index('mandarin') } }
9
+ end
10
+
11
+ Matching against values as a string is ~7x faster
12
+
13
+ user system total real
14
+ 5.630000 0.000000 5.630000 ( 5.625129)
15
+ 0.790000 0.000000 0.790000 ( 0.788589)
16
+ 1.190000 0.000000 1.190000 ( 1.192151)
17
+ 1.190000 0.000000 1.190000 ( 1.187852)
18
+
19
+ ---
20
+
21
+ # 1 Total: 194.028314
22
+
23
+ %self total self wait child calls name
24
+ 37.23 164.362 72.237 0.000 92.125 10681477 *Hash#each
25
+ 11.02 21.382 21.380 0.000 0.001 10972331 <Module::Crosscounter::Compute>#regexify
26
+ 10.90 21.147 21.146 0.000 0.000 10972547 String#sub
27
+ 7.86 177.838 15.241 0.000 162.596 10676448 Enumerable#all?
28
+ 7.02 13.617 13.617 0.000 0.000 10974955 String#=~
29
+ 6.78 190.996 13.158 0.000 177.838 13566 Array#count
30
+ 4.73 9.179 9.179 0.000 0.000 10972581 Hash#fetch
31
+ 4.66 9.047 9.047 0.000 0.000 11043313 Symbol#to_s
32
+ 4.47 8.676 8.676 0.000 0.000 10972786 String#to_sym
33
+ 3.02 5.866 5.866 0.000 0.000 9305498 String#to_s
34
+ 0.81 1.577 1.577 0.000 0.000 1706407 Fixnum#to_s
35
+ 0.06 188.146 0.118 0.000 188.028 8381 *Array#each
36
+ 0.05 192.566 0.105 0.000 192.460 4174 *Array#map
37
+ 0.04 0.079 0.079 0.000 0.000 8026 String#gsub
38
+
39
+ # 2 Total: 112.476687 (1.73x Faster)
40
+
41
+ %self total self wait child calls name
42
+ 39.07 84.951 43.949 0.000 41.002 10681477 *Hash#each
43
+ 17.29 19.451 19.450 0.000 0.001 10972331 <Module::Crosscounter::Compute>#regexify
44
+ 12.52 97.285 14.085 0.000 83.200 10676448 Enumerable#all?
45
+ 11.02 12.400 12.400 0.000 0.000 10974955 String#=~
46
+ 10.87 109.508 12.224 0.000 97.285 13566 Array#count
47
+ 4.78 5.379 5.379 0.000 0.000 9305498 String#to_s
48
+ 1.33 1.494 1.494 0.000 0.000 1706407 Fixnum#to_s
49
+ 0.53 0.596 0.596 0.000 0.000 305605 String#sub
50
+ 0.09 110.965 0.104 0.000 110.860 4174 *Array#map
51
+ 0.09 108.632 0.096 0.000 108.536 9168 *Array#each
52
+ 0.08 0.139 0.089 0.000 0.050 8605 ActiveSupport::Callbacks::ClassMethods#__callback_runner_name
53
+ 0.06 0.088 0.068 0.000 0.021 4722 <Class::ActiveSupport::TimeZone>#seconds_to_utc_offset
54
+ 0.04 0.050 0.050 0.000 0.000 64463 Symbol#to_s
55
+ 0.04 0.108 0.049 0.000 0.059 7875 ActiveRecord::AttributeMethods::Read::ClassMethods#type_cast_attribute
56
+ 0.04 0.056 0.047 0.000 0.009 4210 <Class::Hash>#[]
57
+ 0.04 0.326 0.046 0.000 0.281 11530 ActiveRecord::AttributeMethods#respond_to?
58
+ 0.04 0.684 0.044 0.000 0.641 4191 ActiveRecord::Base#init_with
59
+ 0.04 0.041 0.041 0.000 0.000 8026 String#gsub
60
+
61
+ # Total: 92.512639 (1.21x Faster)
62
+
63
+ %self total self wait child calls name
64
+ 50.37 65.501 46.595 0.000 18.906 10681477 *Hash#each
65
+ 14.75 77.401 13.648 0.000 63.753 10676448 Enumerable#all?
66
+ 13.03 89.452 12.051 0.000 77.401 13566 Array#count
67
+ 7.61 7.039 7.039 0.000 0.000 10894304 Kernel#kind_of?
68
+ 2.15 3.004 1.986 0.000 1.018 1670405 String#==
69
+ 2.12 1.959 1.958 0.000 0.000 1040575 Array#join
70
+ 2.08 4.924 1.920 0.000 3.004 1670214 Fixnum#==
data/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # Crosscounter
2
+
3
+ A set of functional tools for generating cross tabulations.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'crosscounter', '~> 0.1.0.beta'
10
+
11
+ ## Usage
12
+
13
+ This is in early development. It isn't intended for general use.
14
+
15
+ ## Contributing
16
+
17
+ 1. Fork it
18
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
19
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
20
+ 4. Push to the branch (`git push origin my-new-feature`)
21
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler/setup'
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new :spec
6
+
7
+ task :default => :spec
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'crosscounter/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'crosscounter'
8
+ gem.version = Crosscounter::VERSION
9
+ gem.authors = ['Parker Selbert']
10
+ gem.email = ['parker@sorentwo.com']
11
+ gem.description = %(Functionally create cross tabulations)
12
+ gem.summary = %(
13
+ Crosscounter allows you to create a simple pipeline for defining cross
14
+ tabulated output)
15
+ gem.homepage = 'https://github.com/sorentwo/crosscounter'
16
+
17
+ gem.files = `git ls-files`.split($/)
18
+ gem.test_files = gem.files.grep(%r{^(spec)/})
19
+ gem.require_paths = ['lib']
20
+
21
+ gem.add_development_dependency 'rspec', '~> 2.12.0'
22
+ end
@@ -0,0 +1,46 @@
1
+ require 'crosscounter/util'
2
+
3
+ module Crosscounter
4
+ module Compute
5
+ @@values = {}
6
+ @@tuples = {}
7
+
8
+ def self.compute(enumerable, properties)
9
+ enumerable.count do |object|
10
+ properties.all? do |key, value|
11
+ extracted = (object[key] || object[key.sub('_', '')])
12
+
13
+ if extracted.kind_of?(Array)
14
+ extracted.join("\t") =~ regexify(value)
15
+ else
16
+ extracted == value
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.compute_all(enumerable, rows, columns)
23
+ enumerable.map! { |object| Crosscounter::Util.stringify_keys(object) }
24
+
25
+ tuplize(rows).map do |tuple|
26
+ initial = [tuple.last, compute(enumerable, tuple.first => tuple.last)]
27
+
28
+ tuplize(columns).inject(initial) do |rows, column|
29
+ rows << compute(enumerable,
30
+ tuple.first => tuple.last,
31
+ "_#{column.first}" => column.last)
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.regexify(value)
37
+ @@values[value] ||= /(\A|\t)#{value}(\Z|\t)/
38
+ end
39
+
40
+ def self.tuplize(hash)
41
+ @@tuples[hash] ||= hash.flat_map do |tuple|
42
+ tuple.last.map { |value| [tuple.first.to_s, value] }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,26 @@
1
+ module Crosscounter
2
+ module Expansion
3
+ def self.expand(keywords, expansions)
4
+ keywords.inject({}) do |hash, keyword|
5
+ hash[keyword] = resolved(expansions, keyword)
6
+ hash
7
+ end
8
+ end
9
+
10
+ def self.replace(enumerable, replacements)
11
+ enumerable.map do |object|
12
+ replacements.inject({}) do |hash, replacement|
13
+ hash[replacement.first] = replacement.last.call(object)
14
+
15
+ hash
16
+ end
17
+ end
18
+ end
19
+
20
+ def self.resolved(expansions, keyword)
21
+ value = expansions.fetch(keyword)
22
+
23
+ value.respond_to?(:call) ? value.call : value
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ module Crosscounter
2
+ module Util
3
+ def self.stringify_keys(hash)
4
+ hash.keys.each do |key|
5
+ hash[key.to_s] = hash.delete(key)
6
+ end
7
+
8
+ hash
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Crosscounter
2
+ VERSION = '0.2.0'
3
+ end
@@ -0,0 +1,6 @@
1
+ require 'crosscounter/version'
2
+ require 'crosscounter/expansion'
3
+ require 'crosscounter/compute'
4
+
5
+ module Crosscounter
6
+ end
@@ -0,0 +1,75 @@
1
+ require 'crosscounter/compute'
2
+
3
+ describe Crosscounter::Compute do
4
+ subject(:computer) { Crosscounter::Compute }
5
+
6
+ describe '.compute' do
7
+ it 'counts the number of cross occurring values between all properties' do
8
+ enumerable = [
9
+ { 'age' => 18, 'gender' => 'male' },
10
+ { 'age' => 19, 'gender' => 'female' },
11
+ { 'age' => 18, 'gender' => 'male' }
12
+ ]
13
+
14
+ computer.compute(enumerable, 'age' => 18).should == 2
15
+ computer.compute(enumerable, 'age' => 18, 'gender' => 'male').should == 2
16
+ computer.compute(enumerable, 'age' => 19, 'gender' => 'male').should == 0
17
+ computer.compute(enumerable, 'age' => 19, 'gender' => 'female').should == 1
18
+ end
19
+
20
+ it 'matches against regular expressions' do
21
+ enumerable = [
22
+ { 'age' => 18, 'tags' => %w[happy sad] },
23
+ { 'age' => 19, 'tags' => %w[happy mad] },
24
+ { 'age' => 18, 'tags' => %w[mad sad] },
25
+ { 'age' => 18, 'tags' => %w[sad] }
26
+ ]
27
+
28
+ computer.compute(enumerable, 'age' => 18, 'tags' => 'sad').should == 3
29
+ computer.compute(enumerable, 'age' => 18, 'tags' => 'happy').should == 1
30
+ end
31
+
32
+ it 'compensates for duplicate keys by normalizing leading underscores' do
33
+ enumerable = [
34
+ { 'age' => '18', 'tags' => %w[happy sad] },
35
+ { 'age' => '19', 'tags' => %w[happy mad] },
36
+ { 'age' => '18', 'tags' => %w[mad sad] },
37
+ { 'age' => '18', 'tags' => %w[sad] }
38
+ ]
39
+
40
+ computer.compute(enumerable, 'tags' => 'sad', '_tags' => 'happy').should == 1
41
+ end
42
+ end
43
+
44
+ describe '.compute_all' do
45
+ it 'generates a list of all x properties against all y properties' do
46
+ enumerable = [
47
+ { age: 18, gender: 'male', tags: %w[happy sad] },
48
+ { age: 19, gender: 'female', tags: %w[happy mad] },
49
+ { age: 18, gender: 'male', tags: %w[mad sad] },
50
+ { age: 19, gender: 'male', tags: %w[sad] }
51
+ ]
52
+
53
+ computed = computer.compute_all(enumerable,
54
+ { age: [18, 19], gender: %w[male female], tags: %w[happy sad mad] },
55
+ { tags: %w[happy sad mad] }
56
+ )
57
+
58
+ computed.should == [
59
+ [18, 2, 1, 2, 1],
60
+ [19, 2, 1, 1, 1],
61
+ ['male', 3, 1, 3, 1],
62
+ ['female', 1, 1, 0, 1],
63
+ ['happy', 2, 2, 1, 1],
64
+ ['sad', 3, 1, 3, 1],
65
+ ['mad', 2, 1, 1, 2]
66
+ ]
67
+ end
68
+ end
69
+
70
+ describe '.tuplize' do
71
+ it 'unzips the hash into key/value tuples' do
72
+ computer.tuplize(age: [18, 19, 20]).should == [['age', 18], ['age', 19], ['age', 20]]
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,31 @@
1
+ require 'crosscounter/expansion'
2
+
3
+ describe Crosscounter::Expansion do
4
+ subject(:expansion) { Crosscounter::Expansion }
5
+
6
+ describe '.expand' do
7
+ it 'replaces a set of keywords with statically defined values' do
8
+ expanded = expansion.expand([:days], days: %w[sunday monday])
9
+
10
+ expanded.should == { days: %w[sunday monday] }
11
+ end
12
+
13
+ it 'replaces a keyword with dynamically defined values' do
14
+ expanded = expansion.expand([:ages], ages: -> { (18..21).map(&:to_s) })
15
+
16
+ expanded.should == { ages: %w[18 19 20 21] }
17
+ end
18
+ end
19
+
20
+ describe '.replace' do
21
+ it 'replaces all objects with mapped values' do
22
+ male_object = mock(gender: 'male')
23
+ female_object = mock(gender: 'female')
24
+
25
+ replaced = expansion.replace([male_object, female_object],
26
+ gender: -> object { object.gender.downcase })
27
+
28
+ replaced.should == [{ gender: 'male' }, { gender: 'female' }]
29
+ end
30
+ end
31
+ end
File without changes
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crosscounter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Parker Selbert
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.12.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 2.12.0
30
+ description: Functionally create cross tabulations
31
+ email:
32
+ - parker@sorentwo.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - .gitignore
38
+ - .travis.yml
39
+ - Gemfile
40
+ - LICENSE.txt
41
+ - NOTES.txt
42
+ - README.md
43
+ - Rakefile
44
+ - crosscounter.gemspec
45
+ - lib/crosscounter.rb
46
+ - lib/crosscounter/compute.rb
47
+ - lib/crosscounter/expansion.rb
48
+ - lib/crosscounter/util.rb
49
+ - lib/crosscounter/version.rb
50
+ - spec/crosscounter/compute_spec.rb
51
+ - spec/crosscounter/expansion_spec.rb
52
+ - spec/spec_helper.rb
53
+ homepage: https://github.com/sorentwo/crosscounter
54
+ licenses: []
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ segments:
66
+ - 0
67
+ hash: 2051450551277427890
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ segments:
75
+ - 0
76
+ hash: 2051450551277427890
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 1.8.23
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: Crosscounter allows you to create a simple pipeline for defining cross tabulated
83
+ output
84
+ test_files:
85
+ - spec/crosscounter/compute_spec.rb
86
+ - spec/crosscounter/expansion_spec.rb
87
+ - spec/spec_helper.rb