ab 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in olery_ab.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 sparkboxx
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.
@@ -0,0 +1,29 @@
1
+ # OleryAb
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'olery_ab'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install olery_ab
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ab/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "ab"
8
+ gem.version = Ab::VERSION
9
+ gem.authors = ["sparkboxx", "giannismelidis"]
10
+ gem.email = ["wilcovanduinkerken@olery.com", "giannismelidis@olery.com"]
11
+ gem.description = %q{Basic AB testing gem}
12
+ gem.summary = %q{The gem provides a basic class Tester or view helper function ab that allows you to deterministically show a certain option (a/b/c/etc) to a user based on a certain chance (e.g. 1/5)}
13
+ gem.homepage = "http://github.com/olery/ab"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(spec)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_development_dependency("rspec")
21
+ end
@@ -0,0 +1,23 @@
1
+ require "digest/sha1"
2
+
3
+ require "ab/version"
4
+ require "ab/tester"
5
+ require "ab/indexer"
6
+ require "ab/chance"
7
+ require 'ab/view_helpers'
8
+
9
+ require 'ab/railtie' if defined?(Rails)
10
+
11
+ #
12
+ # Overall Initial Idea:
13
+ #
14
+
15
+ #options = []
16
+ #options << link_to("some kind of url")
17
+ #options << link_to("the other url")
18
+
19
+ # = ab_test(:options=>options, :chance=>"1/1", :input=>user.id, :name=>"announcements") # returns an option
20
+
21
+ module Ab
22
+ end
23
+
@@ -0,0 +1,36 @@
1
+ module Ab
2
+ class Chance
3
+ attr_reader :chances
4
+
5
+ def initialize(chance_string)
6
+ raw_chances = self.class.split_chance(chance_string)
7
+ validate_raw_chances(raw_chances)
8
+
9
+ @chances = self.class.normalize(raw_chances)
10
+ end
11
+
12
+ def self.normalize(array)
13
+ sum = array.reduce(&:+)
14
+ factor = 100.to_f / sum
15
+ array.map{|e| e * factor}.map(&:to_i)
16
+ end
17
+
18
+ def self.split_chance(string)
19
+ string.split("/").map(&:to_i)
20
+ end
21
+
22
+ def validate_raw_chances(chances)
23
+ chances.each do |chance|
24
+ raise ArgumentError.new("chance should be > 0") if chance < 0
25
+ end
26
+ end
27
+
28
+ def count
29
+ chances.count
30
+ end
31
+
32
+ def to_a
33
+ chances
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,75 @@
1
+ module Ab
2
+ class Indexer
3
+ def self.call(opts={})
4
+ validate_options(opts)
5
+
6
+ value = opts[:value].to_s
7
+ chances = opts[:chances]
8
+ salt = opts[:seed].to_s
9
+
10
+ seed = Digest::SHA1.hexdigest(salt + value).to_i
11
+ random_number = Randomizer.new(seed).rand(1..100)
12
+
13
+ sum = 0
14
+ chances.each_index do |index|
15
+ min = sum
16
+ max = chances[index] + sum
17
+
18
+ if (min..max).include?(random_number)
19
+ return index
20
+ end
21
+
22
+ sum = max
23
+ end
24
+ raise IndexError.new("Could not resolve any index for the given chances")
25
+ end
26
+
27
+
28
+ private
29
+
30
+ def self.validate_options(opts)
31
+ keys = [:value, :chances, :seed]
32
+ keys.each do |key|
33
+ raise ArgumentError.new("Missing option: :#{key}") if opts[key].nil?
34
+ end
35
+
36
+ if opts[:chances].reduce(&:+)==0
37
+ raise ArgumentError.new("Chances does not actually contain any chance")
38
+ end
39
+ end
40
+
41
+ class Randomizer
42
+ attr_reader :randomizer
43
+
44
+ def initialize(seed)
45
+ if RUBY_VERSION =~ /^1.8/
46
+ @randomizer = OneEight.new(seed)
47
+ else
48
+ @randomizer = Random.new(seed)
49
+ end
50
+ end
51
+
52
+ def rand(value)
53
+ randomizer.rand(value)
54
+ end
55
+
56
+ private
57
+
58
+ class OneEight
59
+ def initialize(seed)
60
+ srand(seed)
61
+ end
62
+
63
+ def rand(value)
64
+ if value.kind_of?(Range)
65
+ first = value.first
66
+ last = value.last - first
67
+ return Kernel::rand(last) + first
68
+ else
69
+ Kernel::rand(value)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,9 @@
1
+ require 'ab/view_helpers'
2
+
3
+ module Ab
4
+ class Railtie < Rails::Railtie
5
+ initializer "ab.view_helpers" do
6
+ ActionView::Base.send :include, ViewHelpers
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,42 @@
1
+ module Ab
2
+ class Tester
3
+ attr_reader :options, :chances, :input, :name
4
+
5
+ def initialize(opts={})
6
+ opts = defaults.merge!(opts)
7
+
8
+ @options = opts[:options]
9
+ @chances = Chance.new(opts[:chances])
10
+ @input = opts[:input]
11
+ @indexer = opts[:indexer]
12
+ @name = opts[:name]
13
+ end
14
+
15
+ def valid?
16
+ chances.count == options.count
17
+ end
18
+
19
+ def validate!
20
+ raise ArgumentError unless valid?
21
+ end
22
+
23
+ def call(id)
24
+ validate!
25
+ index = @indexer.call(:value=>id,:chances=>chances.to_a, :seed=>name)
26
+ if block_given?
27
+ yield options[index]
28
+ else
29
+ return options[index]
30
+ end
31
+ end
32
+ alias_method :for, :call
33
+
34
+ private
35
+ def defaults
36
+ { :options=>[true,false],
37
+ :chances=>"50/50",
38
+ :name=>"Please provide me a name",
39
+ :indexer=>Indexer }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module Ab
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,7 @@
1
+ module Ab
2
+ module ViewHelpers
3
+ def ab(options={})
4
+ Ab::Tester.new(options)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+ require 'ab/view_helpers.rb'
3
+
4
+ describe "Helper Method" do
5
+ class View
6
+ include Ab::ViewHelpers
7
+ end
8
+
9
+ let(:view){ View.new}
10
+
11
+ it "should do nothing on empty calls" do
12
+ expect{view.ab}.to_not raise_error
13
+ end
14
+
15
+ it "should take multiple options" do
16
+ expect{view.ab(:options=>[1,2,3], :chance=>"1/2/2", :input=>50)}.to_not raise_error
17
+ end
18
+
19
+ end
20
+
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ab::Chance do
4
+ it "should split a string into a an array of integers" do
5
+ Chance.split_chance("10/20/30").should eql([10,20,30])
6
+ end
7
+
8
+ it "should not accept minus chances" do
9
+ expect{Chance.new("-1/0/0")}.to raise_error(ArgumentError)
10
+ end
11
+
12
+ it "should return normalized chances" do
13
+ Chance.new("1/0/0").to_a.should eql([100,0,0])
14
+ Chance.new("0/1/0").to_a.should eql([0,100,0])
15
+ Chance.new("0/0/1").to_a.should eql([0,0,100])
16
+ end
17
+
18
+ context "normalizing" do
19
+ it "should normalize tens" do
20
+ Chance.normalize([10,15,25]).should eql([20,30,50])
21
+ end
22
+
23
+ it "should normalize thousands" do
24
+ Chance.normalize([1000,1500,2500]).should eql([20,30,50])
25
+ end
26
+
27
+ it "should normalize decimals" do
28
+ Chance.normalize([0.1,0.15,0.25]).should eql([20,30,50])
29
+ end
30
+
31
+ it "should cope with zero's" do
32
+ Chance.normalize([0,0,1]).should eql([0,0,100])
33
+ end
34
+
35
+ end
36
+ end
37
+
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ab::Indexer do
4
+ let(:indexer){ Ab::Indexer }
5
+
6
+
7
+ it "should process a bunch of options" do
8
+ options = [
9
+ {:chances=>[100,0], :value=>123, :result=>0, :seed=>123},
10
+ {:chances=>[0,100], :value=>123, :result=>1, :seed=>123},
11
+ {:chances=>[-10,100], :value=>123, :result=>1, :seed=>123},
12
+ {:chances=>[100,0,0], :value=>123, :result=>0, :seed=>123},
13
+ {:chances=>[0,100,0], :value=>123, :result=>1, :seed=>123},
14
+ {:chances=>[0,0,100], :value=>123, :result=>2, :seed=>123},
15
+ ]
16
+
17
+ options.each do |option|
18
+ indexer.call(option).should eql(option[:result])
19
+ end
20
+
21
+ end
22
+
23
+ it "raises ArgumentError when arguments are missing" do
24
+ expect{indexer.call()}.to raise_error(ArgumentError)
25
+ end
26
+
27
+ it "raises ArgumentError when chances are 0" do
28
+ expect{indexer.call(:chances=>[0,0], :value=>123, :seed=>123)}.to raise_error(ArgumentError)
29
+ end
30
+
31
+ it "raises Error when it can't resolve an index" do
32
+ expect{indexer.call(:chances=>[-10, -20], :value=>123, :seed=>123)}.to raise_error(IndexError)
33
+ end
34
+
35
+ end
@@ -0,0 +1,2 @@
1
+ require 'ab'
2
+ include Ab
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ab::Tester do
4
+
5
+ it "should initialize with no options" do
6
+ expect{Tester.new}.to_not raise_error
7
+ end
8
+
9
+ it "should initialize with options" do
10
+ expect{Tester.new(:options=>[1,2,3], :chances=>"1/2/2", :input=>50)}.to_not raise_error
11
+ end
12
+
13
+ it "should remember the options" do
14
+ Tester.new(:options=>[1,2,3]).options.should eql([1,2,3])
15
+ end
16
+
17
+ it "should remember the chances" do
18
+ Tester.new(:chances=>"10/10/10").chances.should_not be_nil
19
+ end
20
+
21
+ it "should remember the input" do
22
+ Tester.new(:input=>10).input.should eql(10)
23
+ end
24
+
25
+ it "should check if the amount of options equals the amount of chances" do
26
+ expect{Tester.new(:options=>[1,2,3], :chances=>"10/30").call}.to raise_error(ArgumentError)
27
+ end
28
+
29
+ it "should be able to set it's own indexer" do
30
+ Tester.new(:options=>[1,2,3],
31
+ :chances=>"0/0/1",
32
+ :indexer=>Proc.new{|a,b| 1}).call(1235).should eql(2)
33
+ end
34
+
35
+ it "uses a sane default indexer" do
36
+ Tester.new(:chances=>"1/0/0", :options=>[1,2,3]).call(1235).should eql(1)
37
+ Tester.new(:chances=>"0/1/0", :options=>[1,2,3]).call(1235).should eql(2)
38
+ Tester.new(:chances=>"0/0/1", :options=>[1,2,3]).call(1235).should eql(3)
39
+ end
40
+
41
+ it "should take a block and provide the result to the block" do
42
+ expect do |b|
43
+ Tester.new(:chances=>"1/0/0", :options=>[1,2,3])\
44
+ .call(1235, &b)
45
+ end.to yield_with_args(1)
46
+ end
47
+
48
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ab
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - sparkboxx
9
+ - giannismelidis
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-02-21 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ description: Basic AB testing gem
32
+ email:
33
+ - wilcovanduinkerken@olery.com
34
+ - giannismelidis@olery.com
35
+ executables: []
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - .gitignore
40
+ - Gemfile
41
+ - LICENSE.txt
42
+ - README.md
43
+ - Rakefile
44
+ - ab.gemspec
45
+ - lib/ab.rb
46
+ - lib/ab/chance.rb
47
+ - lib/ab/indexer.rb
48
+ - lib/ab/railtie.rb
49
+ - lib/ab/tester.rb
50
+ - lib/ab/version.rb
51
+ - lib/ab/view_helpers.rb
52
+ - spec/ab_test_spec.rb
53
+ - spec/chance_spec.rb
54
+ - spec/int_indexer_spec.rb
55
+ - spec/spec_helper.rb
56
+ - spec/tester_spec.rb
57
+ homepage: http://github.com/olery/ab
58
+ licenses: []
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubyforge_project:
77
+ rubygems_version: 1.8.24
78
+ signing_key:
79
+ specification_version: 3
80
+ summary: The gem provides a basic class Tester or view helper function ab that allows
81
+ you to deterministically show a certain option (a/b/c/etc) to a user based on a
82
+ certain chance (e.g. 1/5)
83
+ test_files:
84
+ - spec/ab_test_spec.rb
85
+ - spec/chance_spec.rb
86
+ - spec/int_indexer_spec.rb
87
+ - spec/spec_helper.rb
88
+ - spec/tester_spec.rb
89
+ has_rdoc: