weightedpicker 0.1.1 → 0.1.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.
- data/CHANGES +7 -1
- data/Gemfile +5 -4
- data/Rakefile +39 -31
- data/VERSION +1 -1
- data/lib/weightedpicker.rb +117 -112
- data/lib/weightedpicker/tree.rb +95 -91
- data/{spec → test}/a-1b256.yaml +0 -0
- data/{spec → test}/a256b0.yaml +0 -0
- data/{spec → test}/a256b1.yaml +0 -0
- data/{spec → test}/a256b128.yaml +0 -0
- data/{spec → test}/a512b64.yaml +0 -0
- data/{spec → test}/a99999b64.yaml +0 -0
- data/{spec → test}/float.yaml +0 -0
- data/test/helper.rb +17 -0
- data/test/test_tree.rb +120 -0
- data/test/test_weightedpicker.rb +239 -0
- metadata +56 -32
- data/spec/spec_helper.rb +0 -12
- data/spec/tree_spec.rb +0 -123
- data/spec/weightedpicker_spec.rb +0 -238
- data/weightedpicker.gemspec +0 -74
data/CHANGES
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
= weightedpicker changelog
|
2
2
|
|
3
|
-
== Master (for 0.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 "
|
11
|
-
gem "
|
12
|
-
gem "
|
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
|
-
|
6
|
+
Bundler.setup(:default, :development)
|
7
7
|
rescue Bundler::BundlerError => e
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
54
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
47
55
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
+
0.1.2
|
data/lib/weightedpicker.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
150
|
+
def names_weights
|
151
|
+
@tree.names_weights
|
152
|
+
end
|
153
|
+
|
154
|
+
def names
|
155
|
+
names_weights.keys
|
156
|
+
end
|
202
157
|
|
203
|
-
|
204
|
-
|
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
|
-
|
211
|
+
private
|
207
212
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
-
|
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
|
data/lib/weightedpicker/tree.rb
CHANGED
@@ -6,118 +6,122 @@
|
|
6
6
|
#
|
7
7
|
class WeightedPicker::Tree
|
8
8
|
|
9
|
-
|
9
|
+
attr_reader :size
|
10
10
|
|
11
|
-
|
11
|
+
class NoEntryError < Exception; end
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
#
|
14
|
+
def initialize(data)
|
15
|
+
@size = data.size #for return hash.
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
@names = data.keys
|
18
|
+
@weights = []
|
19
|
+
@weights[0] = data.values
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
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
|
-
|
35
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
90
|
+
private
|
87
91
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
109
|
+
def depth
|
110
|
+
log2_ceil(@size)
|
111
|
+
end
|
108
112
|
|
109
|
-
|
110
|
-
|
113
|
+
def choose(num0, num1)
|
114
|
+
sum = num0 + num1
|
111
115
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
+
# 0, 1, 2
|
117
|
+
return 0 if rand(sum) < num0
|
118
|
+
return 1
|
119
|
+
end
|
116
120
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
+
def index(item)
|
122
|
+
return @names.index(item)
|
123
|
+
#raise WeightedPicker::Tree::NoEntryError
|
124
|
+
end
|
121
125
|
|
122
126
|
end
|
123
127
|
|