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 +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
|