weightedpicker 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,6 +1,12 @@
1
1
  = weightedpicker changelog
2
2
 
3
- == Master (for 0.1.1)
3
+ == Master (for 0.1.3)
4
+
5
+ == Version 0.1.2 [2014-09-01] released
6
+ Add WeightedPicker::total_weight.
7
+ Change test method from rspec to test-unit
8
+
9
+ == Version 0.1.1 [2013-04-18] released
4
10
  Add bin/weight to deal with weight yaml.
5
11
  Add WeightedPicker::names.
6
12
  Add WeightedPicker::dump_histgram.
data/Gemfile CHANGED
@@ -6,10 +6,11 @@ source "http://rubygems.org"
6
6
  # Add dependencies to develop your gem here.
7
7
  # Include everything needed to run rake, tests, features, etc.
8
8
  group :development do
9
- gem "rspec", "~> 2.13.0"
10
- gem "rdoc", "~> 3.12"
11
- gem "bundler", "~> 1.3.4"
12
- gem "jeweler", "~> 1.8.3"
9
+ #gem "rspec", "~> 2.13.0"
10
+ gem "test-unit", "~> 3.0.1"
11
+ gem "rdoc", "~> 4.0.1"
12
+ gem "bundler", "~> 1.7.2"
13
+ gem "jeweler", "~> 2.0.1"
13
14
  gem "simplecov", ">= 0"
14
15
  #gem "psych", ">= 0"
15
16
  end
data/Rakefile CHANGED
@@ -3,50 +3,58 @@
3
3
  require 'rubygems'
4
4
  require 'bundler'
5
5
  begin
6
- Bundler.setup(:default, :development)
6
+ Bundler.setup(:default, :development)
7
7
  rescue Bundler::BundlerError => e
8
- $stderr.puts e.message
9
- $stderr.puts "Run `bundle install` to install missing gems"
10
- exit e.status_code
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
11
  end
12
12
  require 'rake'
13
13
 
14
14
  require 'jeweler'
15
15
  Jeweler::Tasks.new do |gem|
16
- # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
- gem.name = "weightedpicker"
18
- gem.homepage = "http://github.com/ippei94da/weightedpicker"
19
- gem.license = "MIT"
20
- gem.summary = %Q{Picking one item from list at the rate of its weight.}
21
- gem.description = %Q{This library enables to pick out items at the rate of their weight.
22
- Weight data is storaged as a YAML file.
23
- You can use this library for music player, wallpaper changer, language training.
24
- }
25
- gem.email = "ippei94da@gmail.com"
26
- gem.authors = ["ippei94da"]
27
- # dependencies defined in Gemfile
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "weightedpicker"
18
+ gem.homepage = "http://github.com/ippei94da/weightedpicker"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Picking one item from list at the rate of its weight.}
21
+ gem.description = %Q{This library enables to pick out items at the rate of their weight.
22
+ Weight data is storaged as a YAML file.
23
+ You can use this library for music player, wallpaper changer, language training.
24
+ }
25
+ gem.email = "ippei94da@gmail.com"
26
+ gem.authors = ["ippei94da"]
27
+ # dependencies defined in Gemfile
28
28
  end
29
29
  Jeweler::RubygemsDotOrgTasks.new
30
30
 
31
- require 'rspec/core'
32
- require 'rspec/core/rake_task'
33
- RSpec::Core::RakeTask.new(:spec) do |spec|
34
- spec.pattern = FileList['spec/**/*_spec.rb']
35
- end
31
+ #require 'rspec/core'
32
+ #require 'rspec/core/rake_task'
33
+ #RSpec::Core::RakeTask.new(:spec) do |spec|
34
+ # spec.pattern = FileList['spec/**/*_spec.rb']
35
+ #end
36
+ #
37
+ #RSpec::Core::RakeTask.new(:rcov) do |spec|
38
+ # spec.pattern = 'spec/**/*_spec.rb'
39
+ # spec.rcov = true
40
+ #end
41
+ #
42
+ #task :default => :spec
36
43
 
37
- RSpec::Core::RakeTask.new(:rcov) do |spec|
38
- spec.pattern = 'spec/**/*_spec.rb'
39
- spec.rcov = true
44
+ require 'rake/testtask'
45
+ Rake::TestTask.new(:test) do |test|
46
+ test.libs << 'lib' << 'test'
47
+ test.pattern = 'test/**/test_*.rb'
48
+ test.verbose = true
40
49
  end
41
-
42
- task :default => :spec
50
+ task :default => :test
43
51
 
44
52
  require 'rdoc/task'
45
53
  Rake::RDocTask.new do |rdoc|
46
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
54
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
47
55
 
48
- rdoc.rdoc_dir = 'rdoc'
49
- rdoc.title = "weightedpicker #{version}"
50
- rdoc.rdoc_files.include('README*')
51
- rdoc.rdoc_files.include('lib/**/*.rb')
56
+ rdoc.rdoc_dir = 'rdoc'
57
+ rdoc.title = "weightedpicker #{version}"
58
+ rdoc.rdoc_files.include('README*')
59
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
60
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.2
@@ -1,3 +1,4 @@
1
+ # coding: utf-8
1
2
  require 'yaml'
2
3
 
3
4
  class WeightedPicker; end
@@ -5,18 +6,18 @@ class WeightedPicker; end
5
6
  require 'weightedpicker/tree.rb'
6
7
 
7
8
  # TODO
8
- # initialize.指定したファイル内のデータが WeightedPicker 的に
9
- # 解釈できなければ例外。
9
+ # initialize.指定したファイル内のデータが WeightedPicker 的に
10
+ # 解釈できなければ例外。
10
11
 
11
12
  #= 概要
12
13
  # 要素群の中から、優先度に応じた重み付きランダムで、
13
14
  # どれか1つを選択する。
14
15
  # たとえば、以下の用途に使える。
15
16
  #* 音楽ファイルの再生(好みのものは多い頻度で、
16
- # そうでないものは少ない頻度で)
17
+ # そうでないものは少ない頻度で)
17
18
  #* クイズゲームの問題選択
18
19
  #* 画像
19
- # * 写真で壁紙, スライドショー的な用途。
20
+ # * 写真で壁紙, スライドショー的な用途。
20
21
  #
21
22
  # 対象の要素はファイルであることを前提としない。
22
23
  # 文字列であることが多いかもしれない。
@@ -95,124 +96,128 @@ require 'weightedpicker/tree.rb'
95
96
  #
96
97
  # ヒストリ関係はこのクラスの外に出した方が良いと判断。
97
98
  class WeightedPicker
98
- MAX_WEIGHT = 2**16
99
- INI_WEIGHT = 2** 8
100
- MIN_WEIGHT = 2** 0
101
-
102
- HISTGRAM_WIDTH = 50
103
-
104
- class InvalidFilenameError < Exception; end
105
- class NoEntryError < Exception; end
106
- class NotExistKeyError < Exception; end
107
- class InvalidWeightError < Exception; end
108
-
109
- # Initialization.
110
- def initialize(data)
111
- data = sanity_data(data)
112
- @tree = WeightedPicker::Tree.new(data)
113
- end
114
-
115
- # Argument 'file' indicates a strage file name for data
116
- # which this class manages.
117
- # If the 'file' does not exist, this file is used to data strage.
118
- #
119
- # Argument 'items' is an array of items to be managed.
120
- # Not all the items in the 'file' survive.
121
- # A and B in the file and B and C in items,
122
- # then B and C is the items which this class manage.
123
- # A is discarded from record.
124
- def self.load_file(filename)
125
- weights = YAML.load_file(filename)
126
- self.new(weights)
127
- end
128
-
129
- def dump(io)
130
- YAML.dump(@tree.names_weights, io)
131
- end
132
-
133
- def dump_histgram(io)
134
- encounters = {}
135
- names_weights.each do |key, weight|
136
- #val_log2 = (Math::log(weight)/Math::log(2.0)).to_i
137
- power = Math::log2(weight).ceil
138
- encounters[power] ||= 0
139
- encounters[power] += 1
99
+ MAX_WEIGHT = 2**16
100
+ INI_WEIGHT = 2** 8
101
+ MIN_WEIGHT = 2** 0
102
+
103
+ HISTGRAM_WIDTH = 50
104
+
105
+ class InvalidFilenameError < Exception; end
106
+ class NoEntryError < Exception; end
107
+ class NotExistKeyError < Exception; end
108
+ class InvalidWeightError < Exception; end
109
+
110
+ # Initialization.
111
+ def initialize(data)
112
+ data = sanity_data(data)
113
+ @tree = WeightedPicker::Tree.new(data)
140
114
  end
141
- max = encounters.values.max
142
- 0.upto(16) do |power|
143
- num = encounters[power] || 0
144
- stars = "*" * (HISTGRAM_WIDTH.to_f * num / max).ceil
145
- io.printf("%6d(%4d)|#{stars}\n", 2**power, num)
115
+
116
+ # Argument 'file' indicates a strage file name for data
117
+ # which this class manages.
118
+ # If the 'file' does not exist, this file is used to data strage.
119
+ #
120
+ # Argument 'items' is an array of items to be managed.
121
+ # Not all the items in the 'file' survive.
122
+ # A and B in the file and B and C in items,
123
+ # then B and C is the items which this class manage.
124
+ # A is discarded from record.
125
+ def self.load_file(filename)
126
+ weights = YAML.load_file(filename)
127
+ self.new(weights)
146
128
  end
147
- end
148
-
149
- def names_weights
150
- @tree.names_weights
151
- end
152
-
153
- def names
154
- names_weights.keys
155
- end
156
-
157
- # 乱数を利用して優先度で重み付けして要素を選び、要素を返す。
158
- # num is only for test. User should not use this argument.
159
- def pick
160
- @tree.pick
161
- end
162
-
163
- #重みを重くする。(優先度が上がる)
164
- def weigh(item)
165
- @tree.weigh(item)
166
- end
167
-
168
- #重みを軽くする。(優先度が下がる)
169
- def lighten(item)
170
- @tree.lighten(item)
171
- end
172
-
173
- # 引数 keys で示したものと、
174
- # 内部的に管理しているデータが整合しているかチェックし、
175
- # keys に合わせる。
176
- # 追加されたデータの重みは、データ内に存在する最大値と
177
- # 同じになる。
178
- # This affects destructively.
179
- def merge(keys)
180
- new_weights = {}
181
- new_keys = []
182
- max = 0
183
- data = @tree.names_weights
184
- keys.each do |key|
185
- new_weights[key] = data[key]
186
-
187
- if data[key] == nil
188
- #substitute max among exist values afterward
189
- new_keys << key unless data[key]
190
- next
191
- end
192
-
193
- max = data[key] if max < data[key]
129
+
130
+ def dump(io)
131
+ YAML.dump(@tree.names_weights, io)
194
132
  end
195
133
 
196
- max = INI_WEIGHT if max < INI_WEIGHT
197
- new_keys.each do |key|
198
- new_weights[key] = max
134
+ def dump_histgram(io)
135
+ encounters = {}
136
+ names_weights.each do |key, weight|
137
+ #val_log2 = (Math::log(weight)/Math::log(2.0)).to_i
138
+ power = Math::log2(weight).ceil
139
+ encounters[power] ||= 0
140
+ encounters[power] += 1
141
+ end
142
+ max = encounters.values.max
143
+ 0.upto(16) do |power|
144
+ num = encounters[power] || 0
145
+ stars = "*" * (HISTGRAM_WIDTH.to_f * num / max).ceil
146
+ io.printf("%6d(%4d)|#{stars}\n", 2**power, num)
147
+ end
199
148
  end
200
149
 
201
- data = new_weights
150
+ def names_weights
151
+ @tree.names_weights
152
+ end
153
+
154
+ def names
155
+ names_weights.keys
156
+ end
202
157
 
203
- @tree = WeightedPicker::Tree.new(data)
204
- end
158
+ # 乱数を利用して優先度で重み付けして要素を選び、要素を返す。
159
+ # num is only for test. User should not use this argument.
160
+ def pick
161
+ @tree.pick
162
+ end
163
+
164
+ #重みを重くする。(優先度が上がる)
165
+ def weigh(item)
166
+ @tree.weigh(item)
167
+ end
168
+
169
+ #重みを軽くする。(優先度が下がる)
170
+ def lighten(item)
171
+ @tree.lighten(item)
172
+ end
173
+
174
+ # 引数 keys で示したものと、
175
+ # 内部的に管理しているデータが整合しているかチェックし、
176
+ # keys に合わせる。
177
+ # 追加されたデータの重みは、データ内に存在する最大値と
178
+ # 同じになる。
179
+ # This affects destructively.
180
+ def merge(keys)
181
+ new_weights = {}
182
+ new_keys = []
183
+ max = 0
184
+ data = @tree.names_weights
185
+ keys.each do |key|
186
+ new_weights[key] = data[key]
187
+
188
+ if data[key] == nil
189
+ #substitute max among exist values afterward
190
+ new_keys << key unless data[key]
191
+ next
192
+ end
193
+
194
+ max = data[key] if max < data[key]
195
+ end
196
+
197
+ max = INI_WEIGHT if max < INI_WEIGHT
198
+ new_keys.each do |key|
199
+ new_weights[key] = max
200
+ end
201
+
202
+ data = new_weights
203
+
204
+ @tree = WeightedPicker::Tree.new(data)
205
+ end
206
+
207
+ def total_weight
208
+ @tree.total_weight
209
+ end
205
210
 
206
- private
211
+ private
207
212
 
208
- def sanity_data(data)
209
- data.each do |key, val|
210
- data[key] = 0 if val < MIN_WEIGHT
211
- data[key] = MAX_WEIGHT if MAX_WEIGHT < val
213
+ def sanity_data(data)
214
+ data.each do |key, val|
215
+ data[key] = 0 if val < MIN_WEIGHT
216
+ data[key] = MAX_WEIGHT if MAX_WEIGHT < val
212
217
 
213
- raise InvalidWeightError, "#{val.inspect}, not integer." unless val.is_a? Integer
218
+ raise InvalidWeightError, "#{val.inspect}, not integer." unless val.is_a? Integer
219
+ end
220
+ data
214
221
  end
215
- data
216
- end
217
222
 
218
223
  end
@@ -6,118 +6,122 @@
6
6
  #
7
7
  class WeightedPicker::Tree
8
8
 
9
- attr_reader :size
9
+ attr_reader :size
10
10
 
11
- class NoEntryError < Exception; end
11
+ class NoEntryError < Exception; end
12
12
 
13
- #
14
- def initialize(data)
15
- @size = data.size #for return hash.
13
+ #
14
+ def initialize(data)
15
+ @size = data.size #for return hash.
16
16
 
17
- @names = data.keys
18
- @weights = []
19
- @weights[0] = data.values
17
+ @names = data.keys
18
+ @weights = []
19
+ @weights[0] = data.values
20
20
 
21
- #Fill 0 to 2**n
22
- @size.upto ((2 ** depth) - 1) do |i|
23
- @weights[0][i] = 0
24
- end
21
+ #Fill 0 to 2**n
22
+ @size.upto ((2 ** depth) - 1) do |i|
23
+ @weights[0][i] = 0
24
+ end
25
+
26
+ depth.times do |i|
27
+ @weights[i+1] = []
28
+ num = @weights[i].size
29
+ (num - num / 2).times do |j|
30
+ @weights[i+1] << @weights[i][2*j] + @weights[i][2*j + 1]
31
+ end
32
+ end
25
33
 
26
- depth.times do |i|
27
- @weights[i+1] = []
28
- num = @weights[i].size
29
- (num - num / 2).times do |j|
30
- @weights[i+1] << @weights[i][2*j] + @weights[i][2*j + 1]
31
- end
34
+ @weights.reverse!
32
35
  end
33
36
 
34
- @weights.reverse!
35
- end
37
+ # Return internal data as a Hash.
38
+ def names_weights
39
+ results = {}
40
+ @size.times do |i|
41
+ results[@names[i]] = @weights[-1][i]
42
+ end
43
+ results
44
+ end
36
45
 
37
- # Return internal data as a Hash.
38
- def names_weights
39
- results = {}
40
- @size.times do |i|
41
- results[@names[i]] = @weights[-1][i]
46
+ def pick
47
+ raise NoEntryError if @weights[0][0] == 0
48
+
49
+ current_index = 0
50
+ depth.times do |i|
51
+ next_id0 = 2 * current_index
52
+ next_id1 = 2 * current_index + 1
53
+ #puts
54
+ choise = choose( @weights[i+1][next_id0], @weights[i+1][next_id1])
55
+ current_index = 2 * current_index + choise
56
+ end
57
+ return @names[current_index]
42
58
  end
43
- results
44
- end
45
-
46
- def pick
47
- raise NoEntryError if @weights[0][0] == 0
48
-
49
- current_index = 0
50
- depth.times do |i|
51
- next_id0 = 2 * current_index
52
- next_id1 = 2 * current_index + 1
53
- #puts
54
- choise = choose( @weights[i+1][next_id0], @weights[i+1][next_id1])
55
- current_index = 2 * current_index + choise
59
+
60
+ def weigh(item)
61
+ raise NoEntryError unless @names.include?(item)
62
+ id = index(item)
63
+ old_weight = @weights[-1][id]
64
+ if (WeightedPicker::MAX_WEIGHT < old_weight * 2)
65
+ add_weight = WeightedPicker::MAX_WEIGHT - old_weight
66
+ else
67
+ add_weight = old_weight
68
+ end
69
+ return if add_weight == 0
70
+ add_ancestors(id, add_weight)
56
71
  end
57
- return @names[current_index]
58
- end
59
-
60
- def weigh(item)
61
- raise NoEntryError unless @names.include?(item)
62
- id = index(item)
63
- old_weight = @weights[-1][id]
64
- if (WeightedPicker::MAX_WEIGHT < old_weight * 2)
65
- add_weight = WeightedPicker::MAX_WEIGHT - old_weight
66
- else
67
- add_weight = old_weight
72
+
73
+ def lighten(item)
74
+ raise NoEntryError unless @names.include?(item)
75
+ id = index(item)
76
+ old_weight = @weights[-1][id]
77
+ if (old_weight / 2 < WeightedPicker::MIN_WEIGHT)
78
+ add_weight = 0
79
+ else
80
+ add_weight = - old_weight / 2
81
+ end
82
+ return if add_weight == 0
83
+ add_ancestors(id, add_weight)
68
84
  end
69
- return if add_weight == 0
70
- add_ancestors(id, add_weight)
71
- end
72
-
73
- def lighten(item)
74
- raise NoEntryError unless @names.include?(item)
75
- id = index(item)
76
- old_weight = @weights[-1][id]
77
- if (old_weight / 2 < WeightedPicker::MIN_WEIGHT)
78
- add_weight = 0
79
- else
80
- add_weight = - old_weight / 2
85
+
86
+ def total_weight
87
+ @weights[0][0]
81
88
  end
82
- return if add_weight == 0
83
- add_ancestors(id, add_weight)
84
- end
85
89
 
86
- private
90
+ private
87
91
 
88
- def add_ancestors(id, val)
89
- (depth+1).times do |d|
90
- divisor = 2 ** (depth - d)
91
- x = id / divisor
92
- @weights[d][x] += val
92
+ def add_ancestors(id, val)
93
+ (depth+1).times do |d|
94
+ divisor = 2 ** (depth - d)
95
+ x = id / divisor
96
+ @weights[d][x] += val
97
+ end
93
98
  end
94
- end
95
99
 
96
- def log2_ceil(num)
97
- result = 0
98
- while (num > 1)
99
- result += 1
100
- num -= num/2
100
+ def log2_ceil(num)
101
+ result = 0
102
+ while (num > 1)
103
+ result += 1
104
+ num -= num/2
105
+ end
106
+ result
101
107
  end
102
- result
103
- end
104
108
 
105
- def depth
106
- log2_ceil(@size)
107
- end
109
+ def depth
110
+ log2_ceil(@size)
111
+ end
108
112
 
109
- def choose(num0, num1)
110
- sum = num0 + num1
113
+ def choose(num0, num1)
114
+ sum = num0 + num1
111
115
 
112
- # 0, 1, 2
113
- return 0 if rand(sum) < num0
114
- return 1
115
- end
116
+ # 0, 1, 2
117
+ return 0 if rand(sum) < num0
118
+ return 1
119
+ end
116
120
 
117
- def index(item)
118
- return @names.index(item)
119
- #raise WeightedPicker::Tree::NoEntryError
120
- end
121
+ def index(item)
122
+ return @names.index(item)
123
+ #raise WeightedPicker::Tree::NoEntryError
124
+ end
121
125
 
122
126
  end
123
127