crosscounter 0.2.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 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