ab 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,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: