crosscounter 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/NOTES.txt +70 -0
- data/README.md +21 -0
- data/Rakefile +7 -0
- data/crosscounter.gemspec +22 -0
- data/lib/crosscounter/compute.rb +46 -0
- data/lib/crosscounter/expansion.rb +26 -0
- data/lib/crosscounter/util.rb +11 -0
- data/lib/crosscounter/version.rb +3 -0
- data/lib/crosscounter.rb +6 -0
- data/spec/crosscounter/compute_spec.rb +75 -0
- data/spec/crosscounter/expansion_spec.rb +31 -0
- data/spec/spec_helper.rb +0 -0
- metadata +87 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
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,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
|
data/lib/crosscounter.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
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
|