Class: ABMeter::Core::Utils::NumUtils

Inherits:
Object
  • Object
show all
Defined in:
lib/abmeter/core/utils/num_utils.rb

Overview

NumUtils provides deterministic percentage assignment for A/B testing

This implementation uses SHA256 + multiply-and-shift algorithm:

  • Zero external dependencies (uses Ruby’s built-in Digest)

  • Fast execution (< 2 microseconds per assignment)

  • Cryptographically secure randomness

  • Distribution quality sufficient for A/B testing:

    • 10K samples: ~3% average deviation (normal for this sample size)

    • 100K samples: ~0.9% average deviation (good for statistical significance)

    • 1M samples: ~0.7% average deviation (excellent uniformity)

The multiply-and-shift method avoids modulo bias by scaling the hash value proportionally across the entire 64-bit space before mapping to the 1-100 range.

Class Method Summary collapse

Class Method Details

.percentages_to_ranges(percentages) ⇒ Object

10, 20, 30, 40

-> [(1..10) (11..30), (31..60), (61..100)]



42
43
44
45
46
47
48
49
50
51
# File 'lib/abmeter/core/utils/num_utils.rb', line 42

def self.percentages_to_ranges(percentages)
  ranges = []
  percentages.each do |percentage|
    last_end = ranges.last&.end || 0
    start_val = last_end + 1
    end_val = last_end + percentage
    ranges << (start_val..end_val)
  end
  ranges
end

.to_percentage(salt, id) ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/abmeter/core/utils/num_utils.rb', line 23

def self.to_percentage(salt, id)
  # Use a hash function to generate a deterministic but random-looking number
  hash = Digest::SHA256.hexdigest("#{salt}:#{id}")

  # Convert first 16 characters (64 bits) to integer for better distribution
  # This gives us a number between 0 and 2^64-1
  num = hash[0..15].to_i(16)

  # Industry-standard multiply-and-shift method for uniform distribution
  # This avoids modulo bias by scaling the 64-bit space proportionally
  # Formula: (num * range) >> bits = (num * 100) >> 64
  # This maps [0, 2^64) uniformly to [0, 100)
  percentage = (num * 100) >> 64

  # Add 1 to get 1-100 range instead of 0-99
  percentage + 1
end