ab-split 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +30 -0
- data/.csslintrc +2 -0
- data/.eslintignore +1 -0
- data/.eslintrc +213 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- data/.rspec +1 -0
- data/.rubocop.yml +7 -0
- data/.rubocop_todo.yml +679 -0
- data/.travis.yml +60 -0
- data/Appraisals +19 -0
- data/CHANGELOG.md +696 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +62 -0
- data/Gemfile +7 -0
- data/LICENSE +22 -0
- data/README.md +955 -0
- data/Rakefile +9 -0
- data/ab-split.gemspec +44 -0
- data/gemfiles/4.2.gemfile +9 -0
- data/gemfiles/5.0.gemfile +9 -0
- data/gemfiles/5.1.gemfile +9 -0
- data/gemfiles/5.2.gemfile +9 -0
- data/gemfiles/6.0.gemfile +9 -0
- data/lib/split.rb +76 -0
- data/lib/split/algorithms/block_randomization.rb +23 -0
- data/lib/split/algorithms/weighted_sample.rb +18 -0
- data/lib/split/algorithms/whiplash.rb +38 -0
- data/lib/split/alternative.rb +191 -0
- data/lib/split/combined_experiments_helper.rb +37 -0
- data/lib/split/configuration.rb +255 -0
- data/lib/split/dashboard.rb +74 -0
- data/lib/split/dashboard/helpers.rb +45 -0
- data/lib/split/dashboard/pagination_helpers.rb +86 -0
- data/lib/split/dashboard/paginator.rb +16 -0
- data/lib/split/dashboard/public/dashboard-filtering.js +43 -0
- data/lib/split/dashboard/public/dashboard.js +24 -0
- data/lib/split/dashboard/public/jquery-1.11.1.min.js +4 -0
- data/lib/split/dashboard/public/reset.css +48 -0
- data/lib/split/dashboard/public/style.css +328 -0
- data/lib/split/dashboard/views/_controls.erb +18 -0
- data/lib/split/dashboard/views/_experiment.erb +155 -0
- data/lib/split/dashboard/views/_experiment_with_goal_header.erb +8 -0
- data/lib/split/dashboard/views/index.erb +26 -0
- data/lib/split/dashboard/views/layout.erb +27 -0
- data/lib/split/encapsulated_helper.rb +42 -0
- data/lib/split/engine.rb +15 -0
- data/lib/split/exceptions.rb +6 -0
- data/lib/split/experiment.rb +486 -0
- data/lib/split/experiment_catalog.rb +51 -0
- data/lib/split/extensions/string.rb +16 -0
- data/lib/split/goals_collection.rb +45 -0
- data/lib/split/helper.rb +165 -0
- data/lib/split/metric.rb +101 -0
- data/lib/split/persistence.rb +28 -0
- data/lib/split/persistence/cookie_adapter.rb +94 -0
- data/lib/split/persistence/dual_adapter.rb +85 -0
- data/lib/split/persistence/redis_adapter.rb +57 -0
- data/lib/split/persistence/session_adapter.rb +29 -0
- data/lib/split/redis_interface.rb +50 -0
- data/lib/split/trial.rb +117 -0
- data/lib/split/user.rb +69 -0
- data/lib/split/version.rb +7 -0
- data/lib/split/zscore.rb +57 -0
- data/spec/algorithms/block_randomization_spec.rb +32 -0
- data/spec/algorithms/weighted_sample_spec.rb +19 -0
- data/spec/algorithms/whiplash_spec.rb +24 -0
- data/spec/alternative_spec.rb +320 -0
- data/spec/combined_experiments_helper_spec.rb +57 -0
- data/spec/configuration_spec.rb +258 -0
- data/spec/dashboard/pagination_helpers_spec.rb +200 -0
- data/spec/dashboard/paginator_spec.rb +37 -0
- data/spec/dashboard_helpers_spec.rb +42 -0
- data/spec/dashboard_spec.rb +210 -0
- data/spec/encapsulated_helper_spec.rb +52 -0
- data/spec/experiment_catalog_spec.rb +53 -0
- data/spec/experiment_spec.rb +533 -0
- data/spec/goals_collection_spec.rb +80 -0
- data/spec/helper_spec.rb +1111 -0
- data/spec/metric_spec.rb +31 -0
- data/spec/persistence/cookie_adapter_spec.rb +106 -0
- data/spec/persistence/dual_adapter_spec.rb +194 -0
- data/spec/persistence/redis_adapter_spec.rb +90 -0
- data/spec/persistence/session_adapter_spec.rb +32 -0
- data/spec/persistence_spec.rb +34 -0
- data/spec/redis_interface_spec.rb +111 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/split_spec.rb +43 -0
- data/spec/support/cookies_mock.rb +20 -0
- data/spec/trial_spec.rb +299 -0
- data/spec/user_spec.rb +87 -0
- metadata +322 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
module CombinedExperimentsHelper
|
4
|
+
def ab_combined_test(metric_descriptor, control = nil, *alternatives)
|
5
|
+
return nil unless experiment = find_combined_experiment(metric_descriptor)
|
6
|
+
raise(Split::InvalidExperimentsFormatError, "Unable to find experiment #{metric_descriptor} in configuration") if experiment[:combined_experiments].nil?
|
7
|
+
|
8
|
+
alternative = nil
|
9
|
+
weighted_alternatives = nil
|
10
|
+
experiment[:combined_experiments].each do |combined_experiment|
|
11
|
+
if alternative.nil?
|
12
|
+
if control
|
13
|
+
alternative = ab_test(combined_experiment, control, alternatives)
|
14
|
+
else
|
15
|
+
normalized_alternatives = Split::Configuration.new.normalize_alternatives(experiment[:alternatives])
|
16
|
+
alternative = ab_test(combined_experiment, normalized_alternatives[0], *normalized_alternatives[1])
|
17
|
+
end
|
18
|
+
else
|
19
|
+
weighted_alternatives ||= experiment[:alternatives].each_with_object({}) do |alt, memo|
|
20
|
+
alt = Alternative.new(alt, experiment[:name]).name
|
21
|
+
memo[alt] = (alt == alternative ? 1 : 0)
|
22
|
+
end
|
23
|
+
|
24
|
+
ab_test(combined_experiment, [weighted_alternatives])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
alternative
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_combined_experiment(metric_descriptor)
|
31
|
+
raise(Split::InvalidExperimentsFormatError, 'Invalid descriptor class (String or Symbol required)') unless metric_descriptor.class == String || metric_descriptor.class == Symbol
|
32
|
+
raise(Split::InvalidExperimentsFormatError, 'Enable configuration') unless Split.configuration.enabled
|
33
|
+
raise(Split::InvalidExperimentsFormatError, 'Enable `allow_multiple_experiments`') unless Split.configuration.allow_multiple_experiments
|
34
|
+
Split::configuration.experiments[metric_descriptor.to_sym]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,255 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
class Configuration
|
4
|
+
attr_accessor :ignore_ip_addresses
|
5
|
+
attr_accessor :ignore_filter
|
6
|
+
attr_accessor :db_failover
|
7
|
+
attr_accessor :db_failover_on_db_error
|
8
|
+
attr_accessor :db_failover_allow_parameter_override
|
9
|
+
attr_accessor :allow_multiple_experiments
|
10
|
+
attr_accessor :enabled
|
11
|
+
attr_accessor :persistence
|
12
|
+
attr_accessor :persistence_cookie_length
|
13
|
+
attr_accessor :algorithm
|
14
|
+
attr_accessor :store_override
|
15
|
+
attr_accessor :start_manually
|
16
|
+
attr_accessor :reset_manually
|
17
|
+
attr_accessor :on_trial
|
18
|
+
attr_accessor :on_trial_choose
|
19
|
+
attr_accessor :on_trial_complete
|
20
|
+
attr_accessor :on_experiment_reset
|
21
|
+
attr_accessor :on_experiment_delete
|
22
|
+
attr_accessor :on_before_experiment_reset
|
23
|
+
attr_accessor :on_before_experiment_delete
|
24
|
+
attr_accessor :include_rails_helper
|
25
|
+
attr_accessor :beta_probability_simulations
|
26
|
+
attr_accessor :winning_alternative_recalculation_interval
|
27
|
+
attr_accessor :redis
|
28
|
+
attr_accessor :dashboard_pagination_default_per_page
|
29
|
+
|
30
|
+
attr_reader :experiments
|
31
|
+
|
32
|
+
attr_writer :bots
|
33
|
+
attr_writer :robot_regex
|
34
|
+
|
35
|
+
def bots
|
36
|
+
@bots ||= {
|
37
|
+
# Indexers
|
38
|
+
'AdsBot-Google' => 'Google Adwords',
|
39
|
+
'Baidu' => 'Chinese search engine',
|
40
|
+
'Baiduspider' => 'Chinese search engine',
|
41
|
+
'bingbot' => 'Microsoft bing bot',
|
42
|
+
'Butterfly' => 'Topsy Labs',
|
43
|
+
'Gigabot' => 'Gigabot spider',
|
44
|
+
'Googlebot' => 'Google spider',
|
45
|
+
'MJ12bot' => 'Majestic-12 spider',
|
46
|
+
'msnbot' => 'Microsoft bot',
|
47
|
+
'rogerbot' => 'SeoMoz spider',
|
48
|
+
'PaperLiBot' => 'PaperLi is another content curation service',
|
49
|
+
'Slurp' => 'Yahoo spider',
|
50
|
+
'Sogou' => 'Chinese search engine',
|
51
|
+
'spider' => 'generic web spider',
|
52
|
+
'UnwindFetchor' => 'Gnip crawler',
|
53
|
+
'WordPress' => 'WordPress spider',
|
54
|
+
'YandexAccessibilityBot' => 'Yandex accessibility spider',
|
55
|
+
'YandexBot' => 'Yandex spider',
|
56
|
+
'YandexMobileBot' => 'Yandex mobile spider',
|
57
|
+
'ZIBB' => 'ZIBB spider',
|
58
|
+
|
59
|
+
# HTTP libraries
|
60
|
+
'Apache-HttpClient' => 'Java http library',
|
61
|
+
'AppEngine-Google' => 'Google App Engine',
|
62
|
+
'curl' => 'curl unix CLI http client',
|
63
|
+
'ColdFusion' => 'ColdFusion http library',
|
64
|
+
'EventMachine HttpClient' => 'Ruby http library',
|
65
|
+
'Go http package' => 'Go http library',
|
66
|
+
'Go-http-client' => 'Go http library',
|
67
|
+
'Java' => 'Generic Java http library',
|
68
|
+
'libwww-perl' => 'Perl client-server library loved by script kids',
|
69
|
+
'lwp-trivial' => 'Another Perl library loved by script kids',
|
70
|
+
'Python-urllib' => 'Python http library',
|
71
|
+
'PycURL' => 'Python http library',
|
72
|
+
'Test Certificate Info' => 'C http library?',
|
73
|
+
'Typhoeus' => 'Ruby http library',
|
74
|
+
'Wget' => 'wget unix CLI http client',
|
75
|
+
|
76
|
+
# URL expanders / previewers
|
77
|
+
'awe.sm' => 'Awe.sm URL expander',
|
78
|
+
'bitlybot' => 'bit.ly bot',
|
79
|
+
'bot@linkfluence.net' => 'Linkfluence bot',
|
80
|
+
'facebookexternalhit' => 'facebook bot',
|
81
|
+
'Facebot' => 'Facebook crawler',
|
82
|
+
'Feedfetcher-Google' => 'Google Feedfetcher',
|
83
|
+
'https://developers.google.com/+/web/snippet' => 'Google+ Snippet Fetcher',
|
84
|
+
'LinkedInBot' => 'LinkedIn bot',
|
85
|
+
'LongURL' => 'URL expander service',
|
86
|
+
'NING' => 'NING - Yet Another Twitter Swarmer',
|
87
|
+
'Pinterest' => 'Pinterest Bot',
|
88
|
+
'redditbot' => 'Reddit Bot',
|
89
|
+
'ShortLinkTranslate' => 'Link shortener',
|
90
|
+
'Slackbot' => 'Slackbot link expander',
|
91
|
+
'TweetmemeBot' => 'TweetMeMe Crawler',
|
92
|
+
'Twitterbot' => 'Twitter URL expander',
|
93
|
+
'UnwindFetch' => 'Gnip URL expander',
|
94
|
+
'vkShare' => 'VKontake Sharer',
|
95
|
+
|
96
|
+
# Uptime monitoring
|
97
|
+
'check_http' => 'Nagios monitor',
|
98
|
+
'GoogleStackdriverMonitoring' => 'Google Cloud monitor',
|
99
|
+
'NewRelicPinger' => 'NewRelic monitor',
|
100
|
+
'Panopta' => 'Monitoring service',
|
101
|
+
'Pingdom' => 'Pingdom monitoring',
|
102
|
+
'SiteUptime' => 'Site monitoring services',
|
103
|
+
'UptimeRobot' => 'Monitoring service',
|
104
|
+
|
105
|
+
# ???
|
106
|
+
'DigitalPersona Fingerprint Software' => 'HP Fingerprint scanner',
|
107
|
+
'ShowyouBot' => 'Showyou iOS app spider',
|
108
|
+
'ZyBorg' => 'Zyborg? Hmmm....',
|
109
|
+
'ELB-HealthChecker' => 'ELB Health Check'
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
def experiments= experiments
|
114
|
+
raise InvalidExperimentsFormatError.new('Experiments must be a Hash') unless experiments.respond_to?(:keys)
|
115
|
+
@experiments = experiments
|
116
|
+
end
|
117
|
+
|
118
|
+
def disabled?
|
119
|
+
!enabled
|
120
|
+
end
|
121
|
+
|
122
|
+
def experiment_for(name)
|
123
|
+
if normalized_experiments
|
124
|
+
# TODO symbols
|
125
|
+
normalized_experiments[name.to_sym]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def metrics
|
130
|
+
return @metrics if defined?(@metrics)
|
131
|
+
@metrics = {}
|
132
|
+
if self.experiments
|
133
|
+
self.experiments.each do |key, value|
|
134
|
+
metrics = value_for(value, :metric) rescue nil
|
135
|
+
Array(metrics).each do |metric_name|
|
136
|
+
if metric_name
|
137
|
+
@metrics[metric_name.to_sym] ||= []
|
138
|
+
@metrics[metric_name.to_sym] << Split::Experiment.new(key)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
@metrics
|
144
|
+
end
|
145
|
+
|
146
|
+
def normalized_experiments
|
147
|
+
return nil if @experiments.nil?
|
148
|
+
|
149
|
+
experiment_config = {}
|
150
|
+
@experiments.keys.each do |name|
|
151
|
+
experiment_config[name.to_sym] = {}
|
152
|
+
end
|
153
|
+
|
154
|
+
@experiments.each do |experiment_name, settings|
|
155
|
+
alternatives = if (alts = value_for(settings, :alternatives))
|
156
|
+
normalize_alternatives(alts)
|
157
|
+
end
|
158
|
+
|
159
|
+
experiment_data = {
|
160
|
+
alternatives: alternatives,
|
161
|
+
goals: value_for(settings, :goals),
|
162
|
+
metadata: value_for(settings, :metadata),
|
163
|
+
algorithm: value_for(settings, :algorithm),
|
164
|
+
resettable: value_for(settings, :resettable)
|
165
|
+
}
|
166
|
+
|
167
|
+
experiment_data.each do |name, value|
|
168
|
+
experiment_config[experiment_name.to_sym][name] = value if value != nil
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
experiment_config
|
173
|
+
end
|
174
|
+
|
175
|
+
def normalize_alternatives(alternatives)
|
176
|
+
given_probability, num_with_probability = alternatives.inject([0,0]) do |a,v|
|
177
|
+
p, n = a
|
178
|
+
if percent = value_for(v, :percent)
|
179
|
+
[p + percent, n + 1]
|
180
|
+
else
|
181
|
+
a
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
num_without_probability = alternatives.length - num_with_probability
|
186
|
+
unassigned_probability = ((100.0 - given_probability) / num_without_probability / 100.0)
|
187
|
+
|
188
|
+
if num_with_probability.nonzero?
|
189
|
+
alternatives = alternatives.map do |v|
|
190
|
+
if (name = value_for(v, :name)) && (percent = value_for(v, :percent))
|
191
|
+
{ name => percent / 100.0 }
|
192
|
+
elsif name = value_for(v, :name)
|
193
|
+
{ name => unassigned_probability }
|
194
|
+
else
|
195
|
+
{ v => unassigned_probability }
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
[alternatives.shift, alternatives]
|
200
|
+
else
|
201
|
+
alternatives = alternatives.dup
|
202
|
+
[alternatives.shift, alternatives]
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def robot_regex
|
207
|
+
@robot_regex ||= /\b(?:#{escaped_bots.join('|')})\b|\A\W*\z/i
|
208
|
+
end
|
209
|
+
|
210
|
+
def initialize
|
211
|
+
@ignore_ip_addresses = []
|
212
|
+
@ignore_filter = proc{ |request| is_robot? || is_ignored_ip_address? }
|
213
|
+
@db_failover = false
|
214
|
+
@db_failover_on_db_error = proc{|error|} # e.g. use Rails logger here
|
215
|
+
@on_experiment_reset = proc{|experiment|}
|
216
|
+
@on_experiment_delete = proc{|experiment|}
|
217
|
+
@on_before_experiment_reset = proc{|experiment|}
|
218
|
+
@on_before_experiment_delete = proc{|experiment|}
|
219
|
+
@db_failover_allow_parameter_override = false
|
220
|
+
@allow_multiple_experiments = false
|
221
|
+
@enabled = true
|
222
|
+
@experiments = {}
|
223
|
+
@persistence = Split::Persistence::SessionAdapter
|
224
|
+
@persistence_cookie_length = 31536000 # One year from now
|
225
|
+
@algorithm = Split::Algorithms::WeightedSample
|
226
|
+
@include_rails_helper = true
|
227
|
+
@beta_probability_simulations = 10000
|
228
|
+
@winning_alternative_recalculation_interval = 60 * 60 * 24 # 1 day
|
229
|
+
@redis = ENV.fetch(ENV.fetch('REDIS_PROVIDER', 'REDIS_URL'), 'redis://localhost:6379')
|
230
|
+
@dashboard_pagination_default_per_page = 10
|
231
|
+
end
|
232
|
+
|
233
|
+
def redis_url=(value)
|
234
|
+
warn '[DEPRECATED] `redis_url=` is deprecated in favor of `redis=`'
|
235
|
+
self.redis = value
|
236
|
+
end
|
237
|
+
|
238
|
+
def redis_url
|
239
|
+
warn '[DEPRECATED] `redis_url` is deprecated in favor of `redis`'
|
240
|
+
self.redis
|
241
|
+
end
|
242
|
+
|
243
|
+
private
|
244
|
+
|
245
|
+
def value_for(hash, key)
|
246
|
+
if hash.kind_of?(Hash)
|
247
|
+
hash.has_key?(key.to_s) ? hash[key.to_s] : hash[key.to_sym]
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def escaped_bots
|
252
|
+
bots.map { |key, _| Regexp.escape(key) }
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'split'
|
4
|
+
require 'bigdecimal'
|
5
|
+
require 'split/dashboard/helpers'
|
6
|
+
require 'split/dashboard/pagination_helpers'
|
7
|
+
|
8
|
+
module Split
|
9
|
+
class Dashboard < Sinatra::Base
|
10
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
11
|
+
|
12
|
+
set :views, "#{dir}/dashboard/views"
|
13
|
+
set :public_folder, "#{dir}/dashboard/public"
|
14
|
+
set :static, true
|
15
|
+
set :method_override, true
|
16
|
+
|
17
|
+
helpers Split::DashboardHelpers
|
18
|
+
helpers Split::DashboardPaginationHelpers
|
19
|
+
|
20
|
+
get '/' do
|
21
|
+
# Display experiments without a winner at the top of the dashboard
|
22
|
+
@experiments = Split::ExperimentCatalog.all_active_first
|
23
|
+
|
24
|
+
@metrics = Split::Metric.all
|
25
|
+
|
26
|
+
# Display Rails Environment mode (or Rack version if not using Rails)
|
27
|
+
if Object.const_defined?('Rails')
|
28
|
+
@current_env = Rails.env.titlecase
|
29
|
+
else
|
30
|
+
@current_env = "Rack: #{Rack.version}"
|
31
|
+
end
|
32
|
+
erb :index
|
33
|
+
end
|
34
|
+
|
35
|
+
post '/force_alternative' do
|
36
|
+
experiment = Split::ExperimentCatalog.find(params[:experiment])
|
37
|
+
alternative = Split::Alternative.new(params[:alternative], experiment.name)
|
38
|
+
alternative.increment_participation
|
39
|
+
Split::User.new(self)[experiment.key] = alternative.name
|
40
|
+
redirect url('/')
|
41
|
+
end
|
42
|
+
|
43
|
+
post '/experiment' do
|
44
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
45
|
+
@alternative = Split::Alternative.new(params[:alternative], params[:experiment])
|
46
|
+
@experiment.winner = @alternative.name
|
47
|
+
redirect url('/')
|
48
|
+
end
|
49
|
+
|
50
|
+
post '/start' do
|
51
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
52
|
+
@experiment.start
|
53
|
+
redirect url('/')
|
54
|
+
end
|
55
|
+
|
56
|
+
post '/reset' do
|
57
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
58
|
+
@experiment.reset
|
59
|
+
redirect url('/')
|
60
|
+
end
|
61
|
+
|
62
|
+
post '/reopen' do
|
63
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
64
|
+
@experiment.reset_winner
|
65
|
+
redirect url('/')
|
66
|
+
end
|
67
|
+
|
68
|
+
delete '/experiment' do
|
69
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
70
|
+
@experiment.delete
|
71
|
+
redirect url('/')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
module DashboardHelpers
|
4
|
+
def h(text)
|
5
|
+
Rack::Utils.escape_html(text)
|
6
|
+
end
|
7
|
+
|
8
|
+
def url(*path_parts)
|
9
|
+
[ path_prefix, path_parts ].join("/").squeeze('/')
|
10
|
+
end
|
11
|
+
|
12
|
+
def path_prefix
|
13
|
+
request.env['SCRIPT_NAME']
|
14
|
+
end
|
15
|
+
|
16
|
+
def number_to_percentage(number, precision = 2)
|
17
|
+
round(number * 100)
|
18
|
+
end
|
19
|
+
|
20
|
+
def round(number, precision = 2)
|
21
|
+
begin
|
22
|
+
BigDecimal(number.to_s)
|
23
|
+
rescue ArgumentError
|
24
|
+
BigDecimal(0)
|
25
|
+
end.round(precision).to_f
|
26
|
+
end
|
27
|
+
|
28
|
+
def confidence_level(z_score)
|
29
|
+
return z_score if z_score.is_a? String
|
30
|
+
|
31
|
+
z = round(z_score.to_s.to_f, 3).abs
|
32
|
+
|
33
|
+
if z >= 2.58
|
34
|
+
'99% confidence'
|
35
|
+
elsif z >= 1.96
|
36
|
+
'95% confidence'
|
37
|
+
elsif z >= 1.65
|
38
|
+
'90% confidence'
|
39
|
+
else
|
40
|
+
'Insufficient confidence'
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'split/dashboard/paginator'
|
3
|
+
|
4
|
+
module Split
|
5
|
+
module DashboardPaginationHelpers
|
6
|
+
def pagination_per
|
7
|
+
default_per_page = Split.configuration.dashboard_pagination_default_per_page
|
8
|
+
@pagination_per ||= (params[:per] || default_per_page).to_i
|
9
|
+
end
|
10
|
+
|
11
|
+
def page_number
|
12
|
+
@page_number ||= (params[:page] || 1).to_i
|
13
|
+
end
|
14
|
+
|
15
|
+
def paginated(collection)
|
16
|
+
Split::DashboardPaginator.new(collection, page_number, pagination_per).paginate
|
17
|
+
end
|
18
|
+
|
19
|
+
def pagination(collection)
|
20
|
+
html = []
|
21
|
+
html << first_page_tag if show_first_page_tag?
|
22
|
+
html << ellipsis_tag if show_first_ellipsis_tag?
|
23
|
+
html << prev_page_tag if show_prev_page_tag?
|
24
|
+
html << current_page_tag
|
25
|
+
html << next_page_tag if show_next_page_tag?(collection)
|
26
|
+
html << ellipsis_tag if show_last_ellipsis_tag?(collection)
|
27
|
+
html << last_page_tag(collection) if show_last_page_tag?(collection)
|
28
|
+
html.join
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def show_first_page_tag?
|
34
|
+
page_number > 2
|
35
|
+
end
|
36
|
+
|
37
|
+
def first_page_tag
|
38
|
+
%Q(<a href="#{url.chop}?page=1&per=#{pagination_per}">1</a>)
|
39
|
+
end
|
40
|
+
|
41
|
+
def show_first_ellipsis_tag?
|
42
|
+
page_number >= 4
|
43
|
+
end
|
44
|
+
|
45
|
+
def ellipsis_tag
|
46
|
+
'<span>...</span>'
|
47
|
+
end
|
48
|
+
|
49
|
+
def show_prev_page_tag?
|
50
|
+
page_number > 1
|
51
|
+
end
|
52
|
+
|
53
|
+
def prev_page_tag
|
54
|
+
%Q(<a href="#{url.chop}?page=#{page_number - 1}&per=#{pagination_per}">#{page_number - 1}</a>)
|
55
|
+
end
|
56
|
+
|
57
|
+
def current_page_tag
|
58
|
+
"<span><b>#{page_number}</b></span>"
|
59
|
+
end
|
60
|
+
|
61
|
+
def show_next_page_tag?(collection)
|
62
|
+
(page_number * pagination_per) < collection.count
|
63
|
+
end
|
64
|
+
|
65
|
+
def next_page_tag
|
66
|
+
%Q(<a href="#{url.chop}?page=#{page_number + 1}&per=#{pagination_per}">#{page_number + 1}</a>)
|
67
|
+
end
|
68
|
+
|
69
|
+
def show_last_ellipsis_tag?(collection)
|
70
|
+
(total_pages(collection) - page_number) >= 3
|
71
|
+
end
|
72
|
+
|
73
|
+
def total_pages(collection)
|
74
|
+
collection.count / pagination_per + ((collection.count % pagination_per).zero? ? 0 : 1)
|
75
|
+
end
|
76
|
+
|
77
|
+
def show_last_page_tag?(collection)
|
78
|
+
page_number < (total_pages(collection) - 1)
|
79
|
+
end
|
80
|
+
|
81
|
+
def last_page_tag(collection)
|
82
|
+
total = total_pages(collection)
|
83
|
+
%Q(<a href="#{url.chop}?page=#{total}&per=#{pagination_per}">#{total}</a>)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|