hash_sample 0.8.7 → 1.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbaa5ac645a6128e616c5d2b33494ee8784c78836d341f15189016627f58d2e0
4
- data.tar.gz: 5273af277526e13aea1999606c5208a444fb9916baa9169e2247c1f2fbbb8848
3
+ metadata.gz: 39d81a45676bb796c3e4b12f107783954bdd4209a597a3f019affd2054999a8b
4
+ data.tar.gz: dba23546f29b23d004de850f163dd8bc873bf21bbc0307665b539e2ea8f97f04
5
5
  SHA512:
6
- metadata.gz: a111c5c5eb58d2f20df8506ff7c6d3ed6e8ccfb3aa6642fbf6800bf6e090017d5cc1f140d0613460e578156b9f253df58882cefaaca0c729aa76fc20de8666c0
7
- data.tar.gz: 047c8aa18ea0f5c1abc746eafecf3077519562b66ece4de47d38fedf5e3754231ebc570c3c39cf3c1ce55489c9935edb4c1712148739e10b50231897880df007
6
+ metadata.gz: eb909e73f6e90502e3d4e759d6aab56cd837b4e19079b8508c8fe0b9b4380c328f5b5abb8ed472dcaa3f34adab9b9c54d49624fd038ddde538c9b1ac55595b54
7
+ data.tar.gz: 32c844882181b440c452831ac9756b83bdd1768c41ab7a31e889f6feee7f4f34af567ac1f38071ad7e1470de815894a81c0e624b0e78627defeb116cf69aa935
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in the .gemspec file
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # hash_sample
2
2
 
3
- [![Build Status](https://travis-ci.com/serg123e/hash_sample.svg?branch=master)](https://travis-ci.com/serg123e/hash_sample)
3
+ [![Build Status](https://travis-ci.com/serg123e/hash_sample.svg)](https://travis-ci.com/serg123e/hash_sample)
4
+ [![Test Coverage](https://img.shields.io/codecov/c/github/serg123e/hash_sample.svg)](https://codecov.io/github/serg123e/hash_sample?branch=master)
4
5
 
5
6
  Implements methods for Hash class for getting weighted random samples with and without replacement, as well as regular random samples
6
7
 
@@ -13,14 +14,17 @@ Implements methods for Hash class for getting weighted random samples with and w
13
14
  ```ruby
14
15
  require 'hash_sample'
15
16
  loaded_die = {'1' => 0.1, '2' => 0.1, '3' => 0.1, '4' => 0.1, '5' => 0.1, '6' => 0.5}
16
- p loaded_die.wchoice # "6"
17
- p loaded_die.wchoice(1) # ["6"]
18
- p loaded_die.wchoice(10) # ["4", "6", "3", "3", "2", "2", "1", "6", "4", "6"]
19
- p loaded_die.wsample # 6
20
- p loaded_die.wsample(6) # ["6", "3", "2", "4", "1", "5"]
21
- p loaded_die.wsample(10) # ["2", "6", "1", "3", "4", "5"]
17
+ # weighted random choice of keys, with replacement (elements can repeat)
18
+ p loaded_die.weighted_choice # "6"
19
+ p loaded_die.weighted_choices(1) # ["6"]
20
+ p loaded_die.wchoices(10) # ["4", "6", "3", "3", "2", "2", "1", "6", "4", "6"]
21
+ # weighted random choice of keys, without replacement (elements can NOT repeat)
22
+ p loaded_die.weighted_sample # 6
23
+ p loaded_die.weighted_samples(6) # ["6", "3", "2", "4", "1", "5"]
24
+ p loaded_die.wsamples(10) # ["2", "6", "1", "3", "4", "5"]
25
+ # regular random choice of key-value pairs (pairs can NOT repeat)
22
26
  p loaded_die.sample # { '1' => 0.1 }
23
- p loaded_die.sample(6) # {'1' => 0.1, '2' => 0.1, '3' => 0.1, '4' => 0.1, '5' => 0.1, '6' => 0.5}
27
+ p loaded_die.samples(6) # {'1' => 0.1, '2' => 0.1, '3' => 0.1, '4' => 0.1, '5' => 0.1, '6' => 0.5}
24
28
  ```
25
29
 
26
30
  ## Hash instance methods
@@ -35,8 +39,12 @@ If the hash contains less than n unique keys, the copy of whole hash will be ret
35
39
 
36
40
  Returns new Hash containing sample key=>value pairs
37
41
 
42
+ ### hash.weighted_choice ⇒ Object
38
43
  ### hash.wchoice ⇒ Object
39
- ### hash.wchoice(n) ⇒ Array of n samples.
44
+
45
+ ### hash.weighted_choices(n) ⇒ Array of n samples
46
+
47
+ ### hash.wchoices(n) ⇒ ⇒ Array of n samples
40
48
  Weighted random sampling *with* replacement.
41
49
 
42
50
  Choose a random key or n random keys from the hash, according to weights defined in hash values.
@@ -49,22 +57,28 @@ All weights should be Numeric.
49
57
 
50
58
  Zero or negative weighs will be ignored.
51
59
 
52
- {'_' => 9, 'a' => 1}.wchoice(10) # ["_", "a", "_", "_", "_", "_", "_", "_", "_", "_"]
60
+ {'_' => 9, 'a' => 1}.wchoices(10) # ["_", "a", "_", "_", "_", "_", "_", "_", "_", "_"]
53
61
 
62
+ ### hash.weighted_sample ⇒ Object
54
63
  ### hash.wsample ⇒ Object
55
- ### hash.wsample(n) ⇒ Array of n samples.
64
+
65
+ ### hash.wsamples(n) ⇒ Array of n samples.
66
+
67
+ ### hash.weighted_samples(n) ⇒ Array of n samples.
56
68
  Weighted random sampling *without* replacement.
57
69
 
58
70
  Choose 1 or n *distinct* random keys from the hash, according to weights defined in hash values.
59
- Drawn items are not replaced.
71
+ Drawn items are not put back into the set, so they **can not be repeated in result**.
60
72
 
61
- If the hash is empty the first form returns nil and the second form returns an empty array.
73
+ If the hash is empty, singular form returns nil and plural returns an empty array.
62
74
 
63
75
  All weights should be Numeric.
64
76
 
65
77
  Zero or negative weighs will be ignored.
66
78
 
67
- {'_' => 9, 'a' => 1}.wchoice(10) # ["_", "a"]
79
+ Hash.new.weighted_sample # nil
80
+ Hash.new.weighted_samples # []
81
+ {'_' => 9, 'a' => 1}.weighted_samples(10) # ["_", "a"]
68
82
 
69
83
  ### hash.wchoices(n = 1) ⇒ Object
70
84
  alias for wchoice
data/Rakefile CHANGED
@@ -1,4 +1,6 @@
1
- require 'yard'
1
+ # frozen_string_literal: true
2
+
3
+ require 'yard'
2
4
  require 'rake'
3
5
  require 'date'
4
6
 
@@ -9,7 +11,7 @@ require 'date'
9
11
  #############################################################################
10
12
 
11
13
  def name
12
- "hash_sample"
14
+ 'hash_sample'
13
15
  end
14
16
 
15
17
  def version
@@ -38,7 +40,7 @@ def bump_version
38
40
  end
39
41
 
40
42
  def replace_header(head, header_name)
41
- head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
43
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{Regexp.last_match(1)}#{send(header_name)}'" }
42
44
  end
43
45
 
44
46
  def gemspec_file
@@ -46,11 +48,11 @@ def gemspec_file
46
48
  end
47
49
 
48
50
  def gem_files
49
- ["#{name}-#{version}.gem"]
51
+ ["#{name}-#{version}.gem"]
50
52
  end
51
53
 
52
54
  def gemspecs
53
- ["#{name}.gemspec"]
55
+ ["#{name}.gemspec"]
54
56
  end
55
57
 
56
58
  def date
@@ -65,20 +67,20 @@ end
65
67
  YARD::Rake::YardocTask.new do |t|
66
68
  end
67
69
 
68
- desc "Generate RCov test coverage and open in your browser"
70
+ desc 'Generate RCov test coverage and open in your browser'
69
71
  task :coverage do
70
72
  require 'rcov'
71
- sh "rm -fr coverage"
72
- sh "rcov test/test_*.rb"
73
- sh "open coverage/index.html"
73
+ sh 'rm -fr coverage'
74
+ sh 'rcov test/test_*.rb'
75
+ sh 'open coverage/index.html'
74
76
  end
75
77
 
76
- desc "Open an irb session preloaded with this library"
78
+ desc 'Open an irb session preloaded with this library'
77
79
  task :console do
78
80
  sh "irb -r rubygems -r ./lib/#{name}.rb"
79
81
  end
80
82
 
81
- desc "Update version number and gemspec"
83
+ desc 'Update version number and gemspec'
82
84
  task :bump do
83
85
  puts "Updated version to #{bump_version}"
84
86
  # Execute does not invoke dependencies.
@@ -88,8 +90,8 @@ task :bump do
88
90
  end
89
91
 
90
92
  desc 'Build gem'
91
- task :build => :gemspec do
92
- sh "mkdir pkg"
93
+ task build: :gemspec do
94
+ sh 'mkdir pkg'
93
95
  gemspecs.each do |gemspec|
94
96
  sh "gem build #{gemspec}"
95
97
  end
@@ -98,14 +100,13 @@ task :build => :gemspec do
98
100
  end
99
101
  end
100
102
 
101
-
102
- desc "Build and install"
103
- task :install => :build do
103
+ desc 'Build and install'
104
+ task install: :build do
104
105
  sh "gem install --local --no-document pkg/#{name}-#{version}.gem"
105
106
  end
106
107
 
107
108
  desc 'Update gemspec'
108
- task :gemspec => :validate do
109
+ task gemspec: :validate do
109
110
  # read spec file and split out manifest section
110
111
  spec = File.read(gemspec_file)
111
112
  head, _manifest, tail = spec.split(/\s*# = MANIFEST =\n/)
@@ -114,22 +115,22 @@ task :gemspec => :validate do
114
115
  replace_header(head, :name)
115
116
  replace_header(head, :version)
116
117
  replace_header(head, :date)
117
- #comment this out if your rubyforge_project has a different name
118
- # replace_header(head, :rubyforge_project)
118
+ # comment this out if your rubyforge_project has a different name
119
+ # replace_header(head, :rubyforge_project)
119
120
 
120
121
  # determine file list from git ls-files
121
- files = `git ls-files`.
122
- split("\n").
123
- sort.
124
- reject { |file| file =~ /^\./ }.
125
- reject { |file| file =~ /^(rdoc|pkg|test|Home\.md|\.gitattributes|Guardfile)/ }.
126
- map { |file| " #{file}" }.
127
- join("\n")
122
+ files = `git ls-files`
123
+ .split("\n")
124
+ .sort
125
+ .grep_v(/^\./)
126
+ .grep_v(/^(rdoc|pkg|test|Home\.md|\.gitattributes|Guardfile)/)
127
+ .map { |file| " #{file}" }
128
+ .join("\n")
128
129
 
129
130
  # piece file back together and write
130
- manifest = " s.files = %w(\n#{files}\n )"
131
- spec = [head, manifest, tail].join("\n # = MANIFEST =\n")
132
- File.open(gemspec_file, 'w') { |io| io.write(spec) }
131
+ manifest = " s.files = %w[\n#{files}\n ]"
132
+ spec = [head, manifest, tail].join("\n # = MANIFEST =\n")
133
+ File.write(gemspec_file, spec)
133
134
  puts "Updated #{gemspec_file}"
134
135
  end
135
136
 
@@ -141,7 +142,7 @@ task :validate do
141
142
  exit!
142
143
  end
143
144
  unless Dir['VERSION*'].empty?
144
- puts "A `VERSION` file at root level violates Gem best practices."
145
+ puts 'A `VERSION` file at root level violates Gem best practices.'
145
146
  exit!
146
147
  end
147
148
  end
@@ -149,13 +150,13 @@ end
149
150
  desc 'Tag commit and push it'
150
151
  task :tag do
151
152
  sh "git tag v#{version}"
152
- sh "git push --tags"
153
+ sh 'git push --tags'
153
154
  end
154
155
 
155
156
  begin
156
157
  require 'rspec/core/rake_task'
157
- desc "run rspec tests"
158
+ desc 'run rspec tests'
158
159
  RSpec::Core::RakeTask.new(:spec)
159
- task :default => :spec
160
- rescue LoadError
161
- end
160
+ task default: :spec
161
+ rescue LoadError # rubocop:disable Lint/SuppressedException
162
+ end
data/hash_sample.gemspec CHANGED
@@ -1,42 +1,49 @@
1
- Gem::Specification.new do |s|
2
- s.name = 'hash_sample'
3
- s.platform = Gem::Platform::RUBY
4
- s.authors = ["Sergey Evstegneiev"]
5
- s.email = ["serg123e@gmail.com"]
6
- s.homepage = 'https://github.com/serg123e/hash_sample'
7
- s.summary = %q{Implements multiple sampling methods for Hash class}
8
- s.description = %q{Implements methods for Hash class for getting weighted random samples with and without replacement, as well as regular random samples}
1
+ # frozen_string_literal: true
9
2
 
10
- s.add_development_dependency "rspec", "~> 3.5"
11
- s.add_development_dependency "simplecov", "~> 0.12"
12
- s.add_development_dependency "rspec-simplecov", "~> 0.2"
3
+ Gem::Specification.new do |s|
4
+ s.name = 'hash_sample'
5
+ s.platform = Gem::Platform::RUBY
6
+ s.authors = ["Sergey Evstegneiev"]
7
+ s.email = ["serg123e@gmail.com"]
8
+ s.homepage = 'https://github.com/serg123e/hash_sample'
9
+ s.summary = 'Implements multiple sampling methods for Hash class'
10
+ s.description = 'Implements methods for Hash class for getting weighted random samples with and without ' \
11
+ 'replacement, as well as regular random samples'
13
12
 
14
- s.add_development_dependency "rake", "~> 13"
15
- s.add_development_dependency "yard", "~> 0.9"
13
+ s.add_development_dependency "codecov", "~> 0.2"
14
+ s.add_development_dependency "rspec", "~> 3.5"
15
+ s.add_development_dependency "rspec-simplecov", "~> 0.2"
16
+ s.add_development_dependency "simplecov", "~> 0.12"
16
17
 
18
+ s.add_development_dependency "rake", "~> 13"
19
+ s.add_development_dependency "reek", "~> 6"
20
+ s.add_development_dependency "rubocop", "~> 1.59"
21
+ s.add_development_dependency "rubocop-rake", "~> 0.6"
22
+ s.add_development_dependency "rubocop-rspec", "~> 3"
23
+ s.add_development_dependency "yard", "~> 0.9"
17
24
 
18
- s.require_paths = ["lib"]
25
+ s.require_paths = ["lib"]
19
26
 
20
- s.required_ruby_version = '>= 2.4'
27
+ s.required_ruby_version = '>= 2.4'
21
28
 
22
- s.date = '2020-05-01'
23
- s.version = '0.8.7'
24
- s.license = 'MIT'
29
+ s.date = '2024-11-19'
30
+ s.version = '1.0.2'
31
+ s.license = 'MIT'
25
32
 
26
- s.rdoc_options = ['--charset=UTF-8']
27
- s.extra_rdoc_files = %w(README.md LICENSE)
28
- # = MANIFEST =
29
- s.files = %w(
30
- Gemfile
31
- LICENSE
32
- README.md
33
- Rakefile
34
- hash_sample.gemspec
35
- lib/hash_sample.rb
36
- lib/hash_sample/version.rb
37
- spec/hash_sample_spec.rb
38
- spec/spec_helper.rb
39
- )
40
- # = MANIFEST =
41
- s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
42
- end
33
+ s.rdoc_options = ['--charset=UTF-8']
34
+ s.extra_rdoc_files = %w[README.md LICENSE]
35
+ # = MANIFEST =
36
+ s.files = %w[
37
+ Gemfile
38
+ LICENSE
39
+ README.md
40
+ Rakefile
41
+ hash_sample.gemspec
42
+ lib/hash_sample.rb
43
+ lib/hash_sample/version.rb
44
+ spec/hash_sample_spec.rb
45
+ spec/spec_helper.rb
46
+ ]
47
+ # = MANIFEST =
48
+ s.test_files = s.files.select { |path| path =~ %r{^spec/*\.rb} }
49
+ end
@@ -1,4 +1,6 @@
1
- module HashSample
2
- # gem version
3
- VERSION = '0.8.7'
4
- end
1
+ # frozen_string_literal: true
2
+
3
+ module HashSample
4
+ # gem version
5
+ VERSION = '1.0.2'
6
+ end
data/lib/hash_sample.rb CHANGED
@@ -1,30 +1,33 @@
1
- # monkey-patched Hash module
1
+ # frozen_string_literal: true
2
+
3
+ # Implements methods for getting weighted random samples with and without replacement,
4
+ # as well as regular random samples
2
5
  class Hash
3
6
  ##
4
7
  # Choose a random key=>value pair or *n* random pairs from the hash.
5
8
  #
6
9
  # @return [Hash] new Hash containing sample key=>value pairs
7
10
  #
8
- # The elements are chosen by using random and unique indices in order to ensure that each element doesn't includes more than once.
11
+ # Each element doesn't includes more than once.
12
+ #
9
13
  # If the hash is empty it returns an empty hash.
10
- # If the hash contains less than *n* unique keys, the copy of whole hash will be returned, none of keys will be lost.
11
- def sample(n = 1)
12
- to_a.sample(n).to_h
14
+ #
15
+ # If the hash contains less than *n* unique keys, the copy of whole hash
16
+ # will be returned, none of keys will be lost.
17
+ #
18
+ def sample(number = 1)
19
+ to_a.sample(number).to_h
13
20
  end
14
21
 
15
- ###
16
- # alias for wchoice
17
- def wchoices(*args)
18
- wchoice(*args)
19
- end
22
+ alias samples sample
20
23
 
21
- ##
24
+ ##
22
25
  # Choose 1 or n random keys from the hash, according to weights defined in hash values
23
26
  # (weighted random sampling *with* *replacement*)
24
- #
25
- # @overload wchoice
27
+ #
28
+ # @overload weighted_choice
26
29
  # @return [Object] one sample object
27
- # @overload wchoice(n)
30
+ # @overload weighted_choices(n)
28
31
  # @param n [Integer] number of samples to be returned
29
32
  # @return [Array] Array of n samples
30
33
  #
@@ -35,60 +38,73 @@ class Hash
35
38
  #
36
39
  # ===== Example
37
40
  #
38
- # p {'_' => 9, 'a' => 1}.wchoice(10) # ["_", "a", "_", "_", "_", "_", "_", "_", "_", "_"]
41
+ # p {'_' => 9, 'a' => 1}.weighted_choices(10) # ["_", "a", "_", "_", "_", "_", "_", "_", "_", "_"]
39
42
  #
40
- def wchoice(*args)
41
- _check_weighted_params
42
- n = args.first || 1
43
- res = []
44
- n.times do
45
- tmp = max_by { |_, weight| weight.positive? ? rand**(1.0 / weight) : 0 }
46
- res << tmp.first unless tmp.nil?
47
- end
48
- return args.empty? ? res.first : res
43
+ def weighted_choice
44
+ weighted_choices(1).first
49
45
  end
50
46
 
51
- # internal method to validate parameters
52
- def _check_weighted_params(*_args)
53
- sum_weights = 0
54
- each_value do |v|
55
- raise ArgumentError, "All weights should be numeric unlike #{v}" unless v.is_a? Numeric
47
+ def weighted_choices(number = 1)
48
+ return [] if empty?
56
49
 
57
- sum_weights += v if v.positive?
58
- end
59
-
60
- raise ArgumentError, "At least one weight should be > 0" unless sum_weights.positive? || empty?
50
+ validate_weights
51
+ Array.new(number) { _wrs.first }
61
52
  end
62
53
 
54
+ alias wchoices weighted_choices
55
+ alias wchoice weighted_choice
56
+
63
57
  ##
64
- # Choose 1 or n *distinct* random keys from the hash, according to weights defined in hash values
65
- # (weighted random sampling *without* *replacement*)
58
+ # Choose 1 or _number_ of *distinct* random keys from the hash, according to
59
+ # weights defined in hash values (weighted random sampling *without* *replacement*)
66
60
  #
67
- # @overload wsample
61
+ # @overload weighted_sample
68
62
  # @return [Object] one sample object
69
- # @overload wsample(n)
70
- # @param n [Integer] number of samples to be returned
71
- # @return [Array] Array of n or sometimes less than n samples
63
+ # @overload weighted_samples(number)
64
+ # @param number [Integer] number of samples to be returned
65
+ # @return [Array] Array of specified or sometimes less than specified samples
66
+ #
67
+ # When there are no sufficient distinct samples to return, the result will
68
+ # contain less than specified number of samples
72
69
  #
73
- # When there are no sufficient distinct samples to return, the result will contain less than n samples
74
70
  # If the hash is empty the first form returns nil and the second form returns an empty array.
75
71
  # All weights should be Numeric.
76
- # Zero or negative weighs will be ignored.
72
+ # Objects with zero or negative weighs will be skipped.
77
73
  #
78
74
  # ===== Example
75
+ # {'a' => 98, 'b' => 1, 'c' => 1}.weighted_sample # 'a'
76
+ # {'a' => 98, 'b' => 1, 'c' => 1}.weighted_samples(3) # ['a', 'a', 'a']
77
+ # {'_' => 9, 'a' => 1}.weighted_samples(10) # ['_', 'a']
79
78
  #
80
- # p {'_' => 9, 'a' => 1}.wsample(10) # ["_", "a"]
81
- #
82
- def wsample(*args)
83
- _check_weighted_params
84
- n = args.first || 1
85
- res = max_by(n) { |_, weight| weight.positive? ? rand**(1.0 / weight) : 0 }.map(&:first)
86
- return args.empty? ? res.first : res
79
+ def weighted_sample
80
+ weighted_samples(1).first
81
+ end
82
+
83
+ def weighted_samples(number = 1)
84
+ return [] if empty?
85
+
86
+ validate_weights
87
+ _wrs(number).map(&:first)
88
+ end
89
+
90
+ alias wsamples weighted_samples
91
+ alias wsample weighted_sample
92
+
93
+ private
94
+
95
+ # internal method to validate parameters
96
+ def validate_weights
97
+ sum_weights = 0
98
+ each_value do |weight|
99
+ raise ArgumentError, "Invalid weight: #{weight}. All weights must be numeric." unless weight.is_a? Numeric
100
+
101
+ sum_weights += weight if weight.positive?
102
+ end
103
+ raise ArgumentError, 'At least one weight should be > 0' unless sum_weights.positive?
87
104
  end
88
105
 
89
- ###
90
- # alias for wsample
91
- def wsamples(*args)
92
- wsample(*args)
106
+ # Returns the *number* of key-value pairs, implementing weighted random sampling.
107
+ def _wrs(*number)
108
+ max_by(*number) { |_, weight| weight.positive? ? rand**(1.0 / weight) : 0 }
93
109
  end
94
110
  end
@@ -1,168 +1,162 @@
1
- # require 'lib/core_ext.rb'
2
-
3
- #
4
- # Specs
5
- #
6
- describe 'Hash#sample' do
7
- describe 'when specified parameter n>1' do
8
- it 'returns new Hash with specified number of unique key=>value samples' do
9
- h = { 'a' => 'b', 'b' => 'b', 'c' => 'b' }
10
- expect(h.sample(3)).to eq h
11
- end
12
- end
13
- describe 'when specified parameter n> number of unique keys' do
14
- it 'returns new Hash only with unique key=>value samples' do
15
- h = { 'a' => 'b', 'b' => 'b', 'c' => 'b' }
16
- expect(h.sample(10)).to eq h
17
- end
18
- end
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples 'weighted sampler' do |weighted_method|
4
+ context weighted_method.to_s do
5
+ let(:weighted_methods) { "#{weighted_method}s" }
19
6
 
20
- describe 'when specified parameter n> number of unique keys' do
21
- it 'keys can not be lost because of bad luck' do
22
- h = { 'a' => 'b', 'b' => 'b', 'c' => 'b' }
23
- min = h.keys.length
24
- 100.times do
25
- min = [h.sample(4).keys.length, min].min
7
+ describe 'plural form of method' do
8
+ it 'can be used' do
9
+ expect(described_class.new).to respond_to(weighted_methods)
26
10
  end
27
- expect(min).to be 3
28
- end
29
- end
30
11
 
31
- describe 'when specified parameter n==1' do
32
- it 'returns new Hash with 1 random key=>value sample' do
33
- h = { 'a' => 'b', 'b' => 'b', 'c' => 'b' }
34
- expect(h.sample(1).keys.length).to eq 1
35
- end
36
- end
37
- end
12
+ it 'works as expected without args' do
13
+ expect({ 'a' => 1 }.send(weighted_methods)).to eq ['a']
14
+ end
38
15
 
39
- %w[wchoice wsample].each do |weighted_method|
40
- describe 'plural form of method' do
41
- weighted_methods = weighted_method + "s"
42
- it 'can be used' do
43
- expect({}).to respond_to(weighted_methods)
44
- end
45
- it 'works as expected without args' do
46
- expect({ 'a' => 1 }.send(weighted_methods)).to eq 'a'
47
- end
48
- it 'works as expected with args' do
49
- expect({ 'a' => 1 }.send(weighted_methods, 1)).to eq ['a']
16
+ it 'works as expected with args' do
17
+ expect({ 'a' => 1 }.send(weighted_methods, 1)).to eq ['a']
18
+ end
50
19
  end
51
- end
52
20
 
53
- describe "Hash\##{weighted_method}" do
54
- it 'returns weighted sample key from all keys with respect of its weights' do
55
- s = { 1 => 90, 2 => 10 }
56
- freq = Hash.new(0)
57
- 1000.times { freq[s.send(weighted_method)] += 1 }
58
- expect(freq[1]).to be_between(800, 999)
59
- expect(freq[2]).to be_between(1, 200)
60
- end
21
+ describe "Hash##{weighted_method}" do
22
+ let(:test_hash) { { 1 => 90, 2 => 10 } }
61
23
 
62
- describe 'when weights are equal' do
63
- it 'it should returns equal parts of samples' do
64
- res = 1.upto(100_000).to_a.map { { +1 => 50, -1 => 50 }.send(weighted_method) }
65
- expect(res.sum).to be_between(-1000, 1000) # +-1% bias
24
+ it 'returns weighted sample key from all keys with respect of its weights' do
25
+ freq = described_class.new(0)
26
+ 1000.times { freq[test_hash.send(weighted_method)] += 1 }
27
+ expect(freq[1]).to be_between(850, 950) # +-5% bias
66
28
  end
67
- end
68
29
 
69
- describe 'when Hash is empty' do
70
- it 'returns nil' do
71
- expect({}.send(weighted_method)).to be_nil
30
+ it 'returns equal parts of samples when weights are equal' do
31
+ res = 1.upto(100_000).to_a.map { { +1 => 50, -1 => 50 }.send(weighted_method) }
32
+ expect(res.sum).to be_between(-1000, 1000) # +-1% bias
72
33
  end
73
- end
74
34
 
75
- describe 'when weights are Float' do
76
- it 'returns a value as expected' do
35
+ it 'works with fractional weights' do
77
36
  expect([1, 2].include?({ 1 => 0.1, 2 => 0.9 }.send(weighted_method))).to be true
78
37
  end
79
- end
80
38
 
81
- describe 'when some weights are negative' do
82
- it 'does not sample that key' do
83
- 100.times { expect({ 'a' => -1, 'b' => 2 }.send(weighted_method)).to eq 'b' }
39
+ it 'ignores negative weights' do
40
+ 10.times { expect({ 'a' => -1, 'b' => 2 }.send(weighted_method)).to eq 'b' }
84
41
  end
85
- end
86
42
 
87
- describe 'when weight contains zero' do
88
- it 'returns non-zero weighted element' do
43
+ it 'ignores zero weight' do
89
44
  10.times do
90
45
  expect({ 1 => 0, 2 => 1, 3 => 0 }.send(weighted_method)).to eq 2
91
46
  end
92
47
  end
93
- end
94
48
 
95
- describe 'when weight is non-numeric' do
96
- it 'raises ArgumentError' do
49
+ it 'raises ArgumentError when weight is non-numeric' do
97
50
  expect { { 1 => 'asd', 2 => 2 }.send(weighted_method) }.to raise_error(ArgumentError)
98
51
  end
99
- end
100
52
 
101
- describe 'when all weights are zero' do
102
- it 'raises ArgumentError' do
53
+ it 'raises ArgumentError when all weights are zero' do
103
54
  expect { { 1 => 0, 2 => 0 }.send(weighted_method) }.to raise_error(ArgumentError)
104
55
  end
105
- # @todo do not raise error when all weights are zero
106
- # xit 'returns empty array' do
107
- # h = { 'a'=>0, 'b'=>0, 'c'=>0 }
108
- # expect( h.wchoice(10) ).to eq []
109
- # end
110
- end
111
56
 
112
- describe 'when hash is empty' do
113
- it 'returns empty array or nil' do
114
- expect({}.send(weighted_method, 10)).to eq []
115
- expect({}.send(weighted_method)).to eq nil
57
+ it 'returns [] from {} if param specified' do
58
+ expect({}.send(weighted_methods, 10)).to eq []
116
59
  end
117
- end
118
60
 
119
- describe 'when specified parameter n>1' do
120
- it 'returns array of sample keys 2' do
121
- 100.times { expect({ 1 => 1, 2 => 0.01, 3 => 0.0000001 }.wchoice(3).length).to be 3 }
61
+ it 'returns [] from {} if no param specified' do
62
+ expect({}.send(weighted_methods)).to eq []
122
63
  end
64
+
65
+ it 'returns array for parameter > 1' do
66
+ 100.times { expect({ 1 => 1, 2 => 0.01, 3 => 0.0000001 }.weighted_choices(3).length).to be 3 }
67
+ end
68
+
69
+ context 'when specified parameter n==1' do
70
+ subject(:result) { { '1' => 1, '2' => 1, '3' => 1 }.weighted_samples(1) }
71
+
72
+ it { expect(result).to be_a(Array) }
73
+ it { expect(result.length).to be 1 }
74
+ end
75
+
76
+ describe 'should work with complex Objects as keys' do
77
+ subject(:result) do
78
+ { test_class.new('asd') => 1, test_class.new('bsd') => 1, test_class.new('dsf') => 1 }.weighted_choice
79
+ end
80
+
81
+ let(:test_class) { Struct.new(:foo) }
82
+
83
+ it 'returns array of one key' do
84
+ expect(result).to be_a(test_class)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ RSpec.describe Hash do
92
+ let(:simple_hash) { { 'a' => 'x', 'b' => 'y', 'c' => 'z' } }
93
+ let(:weighted_hash) { { 'a' => 90, 'b' => 10 } }
94
+ let(:empty_hash) { {} }
95
+
96
+ describe '#sample' do
97
+ context 'when n is less than or equal to the number of unique keys' do
98
+ subject(:result) { simple_hash.sample(2) }
99
+
100
+ it { expect(result.keys.size).to eq(2) }
101
+ it { expect(result).to be_a(described_class) }
123
102
  end
124
- describe 'when specified parameter n==1' do
125
- subject { { '1' => 1, '2' => 1, '3' => 1 }.wchoice(1) }
126
- it 'returns array of one key' do
127
- expect(subject).to be_kind_of(Array)
128
- expect(subject.length).to be 1
103
+
104
+ context 'when n is greater than the number of unique keys' do
105
+ subject(:result) { simple_hash.sample(10) }
106
+
107
+ it 'returns the entire hash' do
108
+ expect(result).to eq(simple_hash)
129
109
  end
130
110
  end
131
111
 
132
- describe 'should work with complex Objetcts as keys' do
133
- subject { { %w[asd zxf] => 1, %w[asd bsd] => 1, %w[asd dsf] => 1 }.wchoice }
134
- it 'returns array of one key' do
135
- expect(subject).to be_kind_of(Array)
136
- expect(subject.length).to be 2
137
- expect(subject).to include 'asd'
112
+ context 'when n equals 1' do
113
+ subject(:result) { simple_hash.sample(1) }
114
+
115
+ it { expect(result.keys.size).to eq(1) }
116
+ it { expect(result).to be_a(described_class) }
117
+ end
118
+
119
+ context 'when the hash is empty' do
120
+ it { expect(empty_hash.sample).to eq({}) }
121
+ end
122
+ end
123
+
124
+ describe '#weighted_choices' do
125
+ it_behaves_like 'weighted sampler', :weighted_choice
126
+
127
+ context 'when specified parameter n>1' do
128
+ it 'returns array of n sample keys' do
129
+ expect({ 'a' => 1 }.weighted_choices(2)).to eq %w[a a]
138
130
  end
139
131
  end
140
132
 
141
- describe 'when specified parameter n is greater than number of unique keys' do
133
+ context 'when specified parameter is greater than number of keys' do
142
134
  it 'returns array with exactly n key samples, repeating some of them' do
143
135
  h = { 'a' => 1, 'b' => 1, 'c' => 1 }
144
- expect(h.wchoice(10).length).to eq 10
136
+ expect(h.weighted_choices(10).length).to eq 10
145
137
  end
146
138
  end
147
139
  end
148
- end
149
140
 
150
- describe 'Hash#wchoice' do
151
- describe 'when specified parameter n>1' do
152
- it 'returns array of n sample keys' do
153
- expect({ 'a' => 1 }.wchoice(2)).to eq %w[a a]
141
+ describe '#weighted_samples' do
142
+ it_behaves_like 'weighted sampler', :weighted_sample
143
+
144
+ describe 'when specified parameter n>1' do
145
+ it 'returns array of unique keys' do
146
+ expect({ 'a' => 1 }.weighted_samples(2)).to eq ['a']
147
+ end
154
148
  end
155
- end
156
- end
157
149
 
158
- describe 'Hash#wsample' do
159
- describe 'when specified parameter n>1' do
160
- it 'returns array of unique keys' do
161
- expect({ 'a' => 1 }.wsample(2)).to eq ['a']
150
+ context 'when specified parameter is greater than number of keys' do
151
+ let(:sample_hash) { { 'a' => 1, 'b' => 1, 'c' => 1 } }
152
+
153
+ it 'returns array with all presented keys' do
154
+ expect(sample_hash.weighted_samples(100).length).to eq 3
155
+ end
162
156
  end
163
- end
164
157
 
165
- it 'returned objects are not repeated' do
166
- expect({ '_' => 9, 'a' => 1 }.wsample(10).sort).to eq %w[_ a]
158
+ it 'returned objects are not repeated' do
159
+ expect({ '_' => 9, 'a' => 1 }.weighted_samples(10).sort).to eq %w[_ a]
160
+ end
167
161
  end
168
162
  end
data/spec/spec_helper.rb CHANGED
@@ -1,7 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'simplecov'
2
4
  require 'rspec/simplecov'
3
5
 
4
6
  SimpleCov.minimum_coverage 100
5
7
  SimpleCov.start
6
8
 
9
+ if ENV['CI'] == 'true'
10
+ require 'codecov'
11
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
12
+ end
13
+
7
14
  require 'hash_sample'
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hash_sample
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.7
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Evstegneiev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-01 00:00:00.000000000 Z
11
+ date: 2024-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rspec
14
+ name: codecov
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '3.5'
19
+ version: '0.2'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '3.5'
26
+ version: '0.2'
27
27
  - !ruby/object:Gem::Dependency
28
- name: simplecov
28
+ name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.12'
33
+ version: '3.5'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0.12'
40
+ version: '3.5'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec-simplecov
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.12'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.12'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rake
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +80,62 @@ dependencies:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
82
  version: '13'
83
+ - !ruby/object:Gem::Dependency
84
+ name: reek
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '6'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.59'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.59'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.6'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.6'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3'
69
139
  - !ruby/object:Gem::Dependency
70
140
  name: yard
71
141
  requirement: !ruby/object:Gem::Requirement
@@ -119,8 +189,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
189
  - !ruby/object:Gem::Version
120
190
  version: '0'
121
191
  requirements: []
122
- rubyforge_project:
123
- rubygems_version: 2.7.7
192
+ rubygems_version: 3.1.6
124
193
  signing_key:
125
194
  specification_version: 4
126
195
  summary: Implements multiple sampling methods for Hash class