crunchr 0.0.1

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.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec", "~> 2.12"
10
+ gem "mocha"
11
+ gem "rdoc", "~> 3.12"
12
+ gem "bundler", "1.2.1"
13
+ gem "jeweler", "~> 1.8.4"
14
+ gem "rcov", ">= 0"
15
+ end
@@ -0,0 +1,37 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.1.3)
5
+ git (1.2.5)
6
+ jeweler (1.8.4)
7
+ bundler (~> 1.0)
8
+ git (>= 1.2.5)
9
+ rake
10
+ rdoc
11
+ json (1.7.6)
12
+ metaclass (0.0.1)
13
+ mocha (0.13.1)
14
+ metaclass (~> 0.0.1)
15
+ rake (10.0.3)
16
+ rcov (0.9.10)
17
+ rdoc (3.12)
18
+ json (~> 1.4)
19
+ rspec (2.12.0)
20
+ rspec-core (~> 2.12.0)
21
+ rspec-expectations (~> 2.12.0)
22
+ rspec-mocks (~> 2.12.0)
23
+ rspec-core (2.12.2)
24
+ rspec-expectations (2.12.1)
25
+ diff-lcs (~> 1.1.3)
26
+ rspec-mocks (2.12.1)
27
+
28
+ PLATFORMS
29
+ ruby
30
+
31
+ DEPENDENCIES
32
+ bundler (= 1.2.1)
33
+ jeweler (~> 1.8.4)
34
+ mocha
35
+ rcov
36
+ rdoc (~> 3.12)
37
+ rspec (~> 2.12)
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Hartog C. de Mik
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,82 @@
1
+ = crunchr
2
+
3
+ Given an ORM-model that makes snapshots with counts off your data
4
+ When I include Crunchr
5
+ Then I can do all kinds of nifty calculations
6
+
7
+ == Synopsis
8
+
9
+ # Given an ORM-mode that makes snapshots
10
+ class Statistic < ActiveRecord::Base
11
+ serialize :data
12
+
13
+ def self.snapshot
14
+ Statistic.create(
15
+ data: {
16
+ keys: Key.count,
17
+ doors: Door.count,
18
+ rooms: {
19
+ count: Room.count,
20
+ occupied: Room.occupied.count
21
+ }
22
+ }
23
+ )
24
+ end
25
+
26
+ # When I include Crunchr
27
+ include Crunchr
28
+ end
29
+
30
+ # Then I can doo all kinds of nifty things
31
+ s = Statistic.last
32
+
33
+ # like, fetch data
34
+ s.fetch('keys') # 10
35
+ s.fetch('doors') # 8
36
+ s.fetch('rooms/count') # 7
37
+
38
+ # do calculations
39
+ s.fetch('keys x doors') # 80
40
+ s.fetch('keys - doors') # 2 spare keys...
41
+ s.fetch('doors / rooms/count') # 1.1428 doors per room
42
+
43
+ # make deltas
44
+ delta = s.delta(Statistic.first)
45
+ delta.fetch('keys') # 10 (now) - 5 (then) = 5
46
+ delta.fetch('keys - doors') # 5 (see above) - 8 = -3
47
+ delta.fetch('rooms/occupied') # 0 (did not change)
48
+
49
+ # make tables
50
+ rows = Statistic.where( "created_at > ?", 1.week.ago )
51
+ Statistic.as_table( rows, keys: ['keys', 'doors', 'keys / doors'] )
52
+ # => [
53
+ # [ 9, 8, 1.125 ]
54
+ # [ 9, 8, 1.125 ]
55
+ # [ 7, 8, 0.875 ]
56
+ # [ 10, 8, 1.25 ]
57
+ # ]
58
+
59
+ == TODO
60
+
61
+ * Use created_at as a way to group data for ActiveModel (alike) includers
62
+
63
+ == Contributing to crunchr
64
+
65
+ * Check out the latest master to make sure the feature hasn't been
66
+ implemented or the bug hasn't been fixed yet.
67
+ * Check out the issue tracker to make sure someone already hasn't requested
68
+ it and/or contributed it.
69
+ * Fork the project.
70
+ * Start a feature/bugfix branch.
71
+ * Commit and push until you are happy with your contribution.
72
+ * Make sure to add tests for it. This is important so I don't break it in a
73
+ future version unintentionally.
74
+ * Please try not to mess with the Rakefile, version, or history. If you want
75
+ to have your own version, or is otherwise necessary, that is fine, but
76
+ please isolate to its own commit so I can cherry-pick around it.
77
+
78
+ == Copyright
79
+
80
+ Copyright (c) 2013 Hartog C. de Mik. See LICENSE.txt for
81
+ further details.
82
+
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "crunchr"
18
+ gem.homepage = "http://github.com/coffeeaddict/crunchr"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Statistics crunching}
21
+ gem.description = %Q{Crunch statistics}
22
+ gem.email = "hartog@organisedminds.com"
23
+ gem.authors = ["Hartog C. de Mik"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ task :default => :spec
40
+
41
+ require 'rdoc/task'
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "crunchr #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,68 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "crunchr"
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Hartog C. de Mik"]
12
+ s.date = "2013-02-08"
13
+ s.description = "Crunch statistics"
14
+ s.email = "hartog@organisedminds.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rspec",
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "LICENSE.txt",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "crunchr.gemspec",
29
+ "lib/crunchr.rb",
30
+ "lib/crunchr/active_record.rb",
31
+ "lib/crunchr/core_ext.rb",
32
+ "spec/crunchr_spec.rb",
33
+ "spec/spec_helper.rb"
34
+ ]
35
+ s.homepage = "http://github.com/coffeeaddict/crunchr"
36
+ s.licenses = ["MIT"]
37
+ s.require_paths = ["lib"]
38
+ s.rubygems_version = "1.8.11"
39
+ s.summary = "Statistics crunching"
40
+
41
+ if s.respond_to? :specification_version then
42
+ s.specification_version = 3
43
+
44
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
45
+ s.add_development_dependency(%q<rspec>, ["~> 2.12"])
46
+ s.add_development_dependency(%q<mocha>, [">= 0"])
47
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
48
+ s.add_development_dependency(%q<bundler>, ["= 1.2.1"])
49
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.4"])
50
+ s.add_development_dependency(%q<rcov>, [">= 0"])
51
+ else
52
+ s.add_dependency(%q<rspec>, ["~> 2.12"])
53
+ s.add_dependency(%q<mocha>, [">= 0"])
54
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
55
+ s.add_dependency(%q<bundler>, ["= 1.2.1"])
56
+ s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
57
+ s.add_dependency(%q<rcov>, [">= 0"])
58
+ end
59
+ else
60
+ s.add_dependency(%q<rspec>, ["~> 2.12"])
61
+ s.add_dependency(%q<mocha>, [">= 0"])
62
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
63
+ s.add_dependency(%q<bundler>, ["= 1.2.1"])
64
+ s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
65
+ s.add_dependency(%q<rcov>, [">= 0"])
66
+ end
67
+ end
68
+
@@ -0,0 +1,246 @@
1
+ require 'crunchr/core_ext'
2
+ require 'bigdecimal'
3
+
4
+ # Crunch statistics with fun
5
+ #
6
+ # @author Harotg de Mik
7
+ #
8
+ module Crunchr
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ def zero; self.class.zero; end
14
+ def checked(val); self.class.checked(val); end
15
+
16
+ def delta(other)
17
+ return nil unless other.respond_to?(:data) && !other.data.is_a?(Hash)
18
+ return nil unless self.data.is_a?(Hash)
19
+
20
+ delta = self.class.new( :data => self.data.delta(other.data) )
21
+
22
+ # make it read-only
23
+ delta.readonly! if delta.respond_to?(:readonly!)
24
+
25
+ return delta
26
+ end
27
+
28
+ # Get the value from the data
29
+ #
30
+ # # Given a data tree that looks like
31
+ # { number: 1
32
+ # collection: {
33
+ # depth: 2
34
+ # }
35
+ # list: [ 1, 2, 3 ]
36
+ # }
37
+ #
38
+ # fetch("number") # => 1
39
+ # fetch("collection/depth") # => 2
40
+ # fetch("n'existe pas") # => nil
41
+ # fetch("collection") # => { depth: 2 }
42
+ # fetch("list") # => nil - NaN && !Hash
43
+ #
44
+ # When you supply a calculation to fetch, it will delegate to calculate
45
+ # fetch("number : collection") # => 0.5 (1 / 2)
46
+ #
47
+ def fetch(key)
48
+ return calculate(key) if key =~ / [*\/:x+-] /
49
+
50
+ key = key.split(/\//).collect(&:to_sym) if key =~ /\//
51
+ value = nil
52
+
53
+ # fetch directly
54
+ if [String, Symbol].include?(key.class)
55
+ if self.data.has_key?(key)
56
+ value = self.data.fetch(key)
57
+ else
58
+ value = self.data.fetch(key.to_sym) rescue nil
59
+ end
60
+
61
+ else
62
+ value = self.data
63
+ key.each do |sub|
64
+ value = value.fetch(sub) rescue nil
65
+ end
66
+ end
67
+
68
+ if value.is_a?(Numeric) || value.is_a?(Hash)
69
+ return value
70
+ end
71
+
72
+ rescue => ex
73
+ if self.class.respond_to?(:logger) && !self.class.logger.nil?
74
+ self.class.logger.error "Error in #{self.class}.fetch(#{key}) for #{data}"
75
+ end
76
+ end
77
+
78
+ # given a string like 'keys - doors', returns the amount of spare keys
79
+ #
80
+ def calculate(key)
81
+ (left, op, right) = key.split(/\s/)
82
+
83
+
84
+ left = (
85
+ left =~ /[^\d.]/ ? self.fetch(left) : BigDecimal.new(left)
86
+ ) || zero()
87
+
88
+ right = (
89
+ right =~ /[^\d.]/ ? self.fetch(right) : BigDecimal.new(right)
90
+ ) || zero()
91
+
92
+ op = op == ":" ? "/" : op
93
+ op = op == "x" ? "*" : op
94
+
95
+ # make sure at least 1 hand is a float
96
+ left *= 1.0 if [left.class, right.class].include?(Fixnum)
97
+
98
+ value = ( left.send(op, right) ) rescue zero()
99
+ return checked(value)
100
+ end
101
+
102
+ module ClassMethods
103
+ # pass in a list off data-objects with and get a nice table
104
+ # list = [ Object.data({ doors: 1, keys: 2}), Object.data({ doors: 1, keys: 3 }, ... ]
105
+ #
106
+ # table = Object.as_table(list, keys: %w[doors keys])
107
+ # # => [ [ 1, 2 ], [ 1, 3 ], [ 1, 4 ], [ 3, 8 ] ]
108
+ #
109
+ # Or use lists in lists
110
+ # deep_list = [ list, list list ]
111
+ # table = Object.as_table(deep_list, keys: %[doors keys], list_operator: delta)
112
+ # # => [ [ 2, 6 ] ] (differnece of mac and min for both doors and keys)
113
+ #
114
+ # @param [Array] list List (1d or 2d) of data objects
115
+ # @param [Hash] opts Options
116
+ # @option opts [Array] keys List of keys to fetch, may contain
117
+ # calculations, eg: ['doors', 'keys', 'doors / keys']
118
+ # @option opts [Symbol] list_operator With a 2d list, what operator to
119
+ # apply to each given list to determine the 1d value, any of
120
+ # - :mean
121
+ # - :stddev
122
+ # - :median
123
+ # - :range
124
+ # - :mode
125
+ # - :sum
126
+ # - :min
127
+ # - ;max
128
+ def as_table(list, opts = {})
129
+ keys = opts[:keys] || raise("Need keys")
130
+
131
+ table = []
132
+
133
+ list.each do |statistic|
134
+ iteration_keys = keys.dup
135
+
136
+ if statistic.is_a?(Array)
137
+ (iteration_keys, statistic) = flatten(statistic, opts)
138
+ end
139
+
140
+ row = []
141
+
142
+ iteration_keys.each do |key|
143
+ value = zero()
144
+
145
+ if key == :date
146
+ value = opts[:date_fmt] ?
147
+ statistic.created_at.strftime(opts[:date_fmt]) :
148
+ statistic.created_at.to_date
149
+
150
+ else
151
+ value = statistic.fetch(key)
152
+
153
+ if value.respond_to? :round
154
+ value = case opts[:round]
155
+ when nil
156
+ value
157
+ when 0
158
+ value.round rescue value
159
+ else
160
+ value.round(opts[:round])
161
+ end
162
+ end
163
+
164
+ value = opts[:str_fmt] % value if opts[:str_fmt]
165
+
166
+ value = value.to_f if value.is_a?(BigDecimal)
167
+ end
168
+
169
+ row << checked(value)
170
+ end
171
+
172
+ if opts[:delta] && table.any?
173
+ new_row = []
174
+ row.each_with_index do |value, idx|
175
+ next unless value.kind_of?(Numeric)
176
+ new_row[idx] = checked(row[idx] - @prev[idx])
177
+ end
178
+
179
+ @prev = row.dup
180
+ row = new_row
181
+ else
182
+ @prev = row
183
+ end
184
+
185
+ table << row
186
+ end
187
+
188
+ return table
189
+ end
190
+
191
+ def flatten(array, opts)
192
+ keys = opts[:keys].dup
193
+
194
+ # this must be an interval period : find the mean, sum, max, whatever
195
+ opts[:list_operator] ||= :mean
196
+
197
+ collection = self.new( :data => {} )
198
+
199
+ keys.each_with_index do |key, idx|
200
+ if key == :date
201
+ collection.created_at = array.first.created_at
202
+ next
203
+ end
204
+
205
+ collection_key = key.to_s.gsub(/[\s*\/:x+-]+/, '_')
206
+ keys[idx] = collection_key if collection_key != key
207
+
208
+ array.each do |item|
209
+ collection.data[collection_key] ||= []
210
+ collection.data[collection_key] << (item.fetch(key) || 0)
211
+ end
212
+
213
+ # turn the collection into a single value
214
+ value = if opts[:list_operator] == :delta
215
+ collection.data[collection_key].max -
216
+ collection.data[collection_key].min
217
+
218
+ else
219
+ collection.data[collection_key].send(opts[:list_operator])
220
+
221
+ end
222
+
223
+ collection.data[collection_key] = value
224
+ end
225
+
226
+ collection
227
+ collection.readonly! if collection.respond_to?(:readonly!)
228
+
229
+ return [keys, collection]
230
+ end
231
+
232
+
233
+ def zero
234
+ BigDecimal.new("0.0")
235
+ end
236
+
237
+ def checked(value)
238
+ value = zero() if value.respond_to?(:nan?) && value.nan?
239
+ value = zero() if value.respond_to?(:infinity?) && value.infinity?
240
+ value = zero() if value.nil?
241
+ value = value.to_f if value.is_a? BigDecimal
242
+
243
+ value
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,68 @@
1
+ # = stuff for dealing with statistics
2
+ #
3
+ # monkey patches Hash, Array, ActiveRecord::Base and ActiveRecord::Relation
4
+ #
5
+
6
+ class Array
7
+ # return an array of arrays where each inner array represents a period of
8
+ # time
9
+ #
10
+ # == Synopsis
11
+ # timed_interval(:week, 2) # two week periods
12
+ # timed_interval(:month, 6) # half year periods
13
+ # timed_interval(:day, 1) # per day
14
+ #
15
+ # == Prerequisites
16
+ # The items in the list must respond_to interval_time and it must return
17
+ # something that responds to < or >
18
+ #
19
+ def timed_interval(length, amount=1)
20
+ list = self.sort_by(&:created_at)
21
+ current = list.first.interval_time(length)
22
+
23
+
24
+ records = []
25
+ period = []
26
+
27
+ list.each do |record|
28
+ if record.interval_time(length) > (current + (amount - 1))
29
+ records << period
30
+ period = []
31
+ current = record.interval_time(length)
32
+ end
33
+
34
+ period << record
35
+ end
36
+
37
+ records
38
+ end
39
+ end
40
+
41
+ class ActiveRecord::Base
42
+ def interval_time(length)
43
+ self.created_at.strftime(interval_fmt(length)).to_i
44
+ end
45
+
46
+ def interval_fmt(length)
47
+ case length
48
+ when :hour
49
+ "%Y%j%H"
50
+ when :day
51
+ "%Y%j"
52
+ when :week
53
+ "%Y%W"
54
+ when :month
55
+ "%Y%m"
56
+ when :year
57
+ "%Y"
58
+ else
59
+ raise "Invalid interval length: #{length}"
60
+ end
61
+ end
62
+ end
63
+
64
+ class ActiveRecord::Relation
65
+ def timed_interval(length, amount=1)
66
+ @records = self.to_a.timed_interval(length, amount)
67
+ end
68
+ end
@@ -0,0 +1,57 @@
1
+ # = stuff for dealing with statistics
2
+ #
3
+ # monkey patches Hash, Array, ActiveRecord::Base and ActiveRecord::Relation
4
+ #
5
+
6
+ class Hash
7
+ def delta(other)
8
+ return nil unless other.is_a?(Hash)
9
+
10
+ delta = {}
11
+
12
+ self.keys.each do |key|
13
+ next if !other.has_key? key
14
+ if self[key].is_a?(Hash)
15
+ delta[key] = self[key].delta(other[key])
16
+ else
17
+ delta[key] = (self[key] - other[key]) rescue nil
18
+ end
19
+ end
20
+
21
+ delta
22
+ end
23
+ end
24
+
25
+ class Array
26
+ def sum
27
+ self.inject(0.0) { |s, i| s += i }
28
+ end
29
+
30
+ def mean
31
+ (self.sum * 1.0) / self.count
32
+ end
33
+
34
+ def stddev
35
+ Math.sqrt(self.collect{ |i| (i - self.mean) ** 2 }.sum) / Math.sqrt(self.count - 1)
36
+ end
37
+
38
+ def median
39
+ center = (self.count + (self.count % 2)) / 2
40
+ list = self.sort
41
+
42
+ self.count % 2 == 0 ?
43
+ [ list[center - 1], list[center] ].mean :
44
+ list[center - 1]
45
+ end
46
+
47
+ def range
48
+ list = self.sort
49
+ list.last - list.first
50
+ end
51
+
52
+ def mode
53
+ counts = {}
54
+ self.collect { |i| counts[i] ||= 0; counts[i] += 1 }
55
+ counts.key(counts.values.sort.last)
56
+ end
57
+ end
@@ -0,0 +1,167 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ class TestClass
4
+ attr_accessor :data
5
+ include Crunchr
6
+
7
+ def initialize(data={})
8
+ @data = data
9
+ end
10
+ end
11
+
12
+ deep_hash = {
13
+ users: { count: 14, active: 2},
14
+ loans: {
15
+ requested: { EUR: 781.284599, GBP: 0.65395, USD: 0.65395, AWG: 0.65395 },
16
+ payed: { EUR: 130.42, GBP: 145.23 }
17
+ },
18
+ orders: { count: 8, lines: 14 },
19
+ commission: {
20
+ approved: { EUR: 32.56 },
21
+ pending: { EUR: 0.8492, GBP: 1.3079, USD: 1.3079, AWG: 1.3079, JPY: 1.3079 }
22
+ },
23
+ }
24
+
25
+ def gen_table_list
26
+ list = []
27
+ prev_rabbit = rand(10)
28
+ 3.times do
29
+ list << TestClass.new(
30
+ rabbits: prev_rabbit += rand(10), dogs: rand(20), cats: rand(40)
31
+ )
32
+ end
33
+ list
34
+ end
35
+
36
+ describe "Crunchr" do
37
+ subject {
38
+ TestClass.new
39
+ }
40
+
41
+ context "The basics" do
42
+ before(:each) do
43
+ subject.data = { doors: 10, keys: 8 }
44
+ end
45
+
46
+ it "fetches door" do
47
+ subject.fetch("doors").should == 10
48
+ end
49
+
50
+ it "calls calculate when a stmt is present" do
51
+ subject.expects(:calculate)
52
+ subject.fetch("keys - doors")
53
+ end
54
+
55
+ it "calculates the right value" do
56
+ subject.fetch("keys - doors").should == -2
57
+ end
58
+
59
+ it "returns nil for non existing" do
60
+ subject.fetch("n'existe pas").should be_nil
61
+ end
62
+
63
+ it "return nil for non-numeric/hash values" do
64
+ subject.data[:hash] = { :depth => 2 }
65
+ subject.data[:arrray] = [ 1, 2 ]
66
+
67
+ subject.fetch("hash").should == { :depth => 2 }
68
+ subject.fetch("array").should be_nil
69
+ end
70
+ end
71
+
72
+ context "Deep hashes" do
73
+ before(:each) do
74
+ subject.data = deep_hash
75
+ end
76
+
77
+ it "fetches deep keys" do
78
+ subject.fetch("loans/requested/GBP").should == 0.65395
79
+ end
80
+
81
+ it "calculates deep keys" do
82
+ subject.fetch("loans/requested/GBP + loans/payed/GBP").should == 0.65395 + 145.23
83
+ end
84
+ end
85
+
86
+ context "1d tables" do
87
+ before(:each) do
88
+ @list = gen_table_list
89
+ end
90
+
91
+ it "should make a nice table" do
92
+ expected = [
93
+ [ @list[0].data[:dogs], @list[0].data[:cats] ],
94
+ [ @list[1].data[:dogs], @list[1].data[:cats] ],
95
+ [ @list[2].data[:dogs], @list[2].data[:cats] ],
96
+ ]
97
+
98
+ TestClass.as_table(@list, keys: %w[dogs cats]).should == expected
99
+ end
100
+
101
+ it "should make a calculated table" do
102
+ expected = [
103
+ [ @list[0].data[:dogs], @list[0].data[:cats] - @list[0].data[:dogs] ],
104
+ [ @list[1].data[:dogs], @list[1].data[:cats] - @list[1].data[:dogs] ],
105
+ [ @list[2].data[:dogs], @list[2].data[:cats] - @list[2].data[:dogs] ],
106
+ ]
107
+
108
+ TestClass.as_table(@list, keys: ['dogs', 'cats - dogs']).should == expected
109
+ end
110
+
111
+ it "should make delta tables" do
112
+ expected = [
113
+ [ @list[0].data[:rabbits] ],
114
+ [ @list[1].data[:rabbits] - @list[0].data[:rabbits] ],
115
+ [ @list[2].data[:rabbits] - @list[1].data[:rabbits] ],
116
+ ]
117
+
118
+ TestClass.as_table(@list, keys: ['rabbits'], delta: true).should == expected
119
+ end
120
+ end
121
+
122
+ context "2d tables" do
123
+ before(:each) do
124
+ @list = []
125
+ 3.times do
126
+ @list << gen_table_list
127
+ end
128
+ end
129
+
130
+ it "should flatten the list with the sum operator" do
131
+ res = TestClass.as_table(@list, keys: ['dogs - cats'], list_operator: :sum)
132
+ res.should == [
133
+ [ @list[0][0].data[:dogs] - @list[0][0].data[:cats] +
134
+ @list[0][1].data[:dogs] - @list[0][1].data[:cats] +
135
+ @list[0][2].data[:dogs] - @list[0][2].data[:cats]
136
+ ],
137
+ [ @list[1][0].data[:dogs] - @list[1][0].data[:cats] +
138
+ @list[1][1].data[:dogs] - @list[1][1].data[:cats] +
139
+ @list[1][2].data[:dogs] - @list[1][2].data[:cats]
140
+ ],
141
+ [ @list[2][0].data[:dogs] - @list[2][0].data[:cats] +
142
+ @list[2][1].data[:dogs] - @list[2][1].data[:cats] +
143
+ @list[2][2].data[:dogs] - @list[2][2].data[:cats]
144
+ ],
145
+ ]
146
+ end
147
+
148
+ it "should flatten the list with the mean operator" do
149
+ res = TestClass.as_table(@list, keys: ['dogs - cats'], list_operator: :mean)
150
+ res.should == [
151
+ [ ( @list[0][0].data[:dogs] - @list[0][0].data[:cats] +
152
+ @list[0][1].data[:dogs] - @list[0][1].data[:cats] +
153
+ @list[0][2].data[:dogs] - @list[0][2].data[:cats] ) / 3.0
154
+ ],
155
+ [ ( @list[1][0].data[:dogs] - @list[1][0].data[:cats] +
156
+ @list[1][1].data[:dogs] - @list[1][1].data[:cats] +
157
+ @list[1][2].data[:dogs] - @list[1][2].data[:cats] ) / 3.0
158
+ ],
159
+ [ ( @list[2][0].data[:dogs] - @list[2][0].data[:cats] +
160
+ @list[2][1].data[:dogs] - @list[2][1].data[:cats] +
161
+ @list[2][2].data[:dogs] - @list[2][2].data[:cats] ) / 3.0
162
+ ],
163
+ ]
164
+ end
165
+
166
+ end
167
+ end
@@ -0,0 +1,13 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'crunchr'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+ config.tty = true
12
+ config.mock_framework = :mocha
13
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crunchr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Hartog C. de Mik
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &16830960 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '2.12'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *16830960
25
+ - !ruby/object:Gem::Dependency
26
+ name: mocha
27
+ requirement: &12972500 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *12972500
36
+ - !ruby/object:Gem::Dependency
37
+ name: rdoc
38
+ requirement: &12974440 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '3.12'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *12974440
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: &12969640 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - =
53
+ - !ruby/object:Gem::Version
54
+ version: 1.2.1
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *12969640
58
+ - !ruby/object:Gem::Dependency
59
+ name: jeweler
60
+ requirement: &12977560 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 1.8.4
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *12977560
69
+ - !ruby/object:Gem::Dependency
70
+ name: rcov
71
+ requirement: &12997100 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *12997100
80
+ description: Crunch statistics
81
+ email: hartog@organisedminds.com
82
+ executables: []
83
+ extensions: []
84
+ extra_rdoc_files:
85
+ - LICENSE.txt
86
+ - README.rdoc
87
+ files:
88
+ - .document
89
+ - .rspec
90
+ - Gemfile
91
+ - Gemfile.lock
92
+ - LICENSE.txt
93
+ - README.rdoc
94
+ - Rakefile
95
+ - VERSION
96
+ - crunchr.gemspec
97
+ - lib/crunchr.rb
98
+ - lib/crunchr/active_record.rb
99
+ - lib/crunchr/core_ext.rb
100
+ - spec/crunchr_spec.rb
101
+ - spec/spec_helper.rb
102
+ homepage: http://github.com/coffeeaddict/crunchr
103
+ licenses:
104
+ - MIT
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ! '>='
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ segments:
116
+ - 0
117
+ hash: 1841846266649628700
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ! '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 1.8.11
127
+ signing_key:
128
+ specification_version: 3
129
+ summary: Statistics crunching
130
+ test_files: []