重み付抽選機能を作る
重み付抽選機能を作る
登録されたデータを重み付けランダム抽選したいという要件は
Webでは意外とあるのではと思うのですが
今回、製作中のWebサイトで投稿された内容を
五段階のユーザーの評価の平均点に対応した頻度で表示する機能を作りました。
基本的な設計としては
■idと重みというセットのデータを複数登録していく
■登録完了後に、idを重みの数だけハッシュにセットしていく。
■このセットの中かidを無作為に抽出します。
具体的にはこんな感じのソースになりました。
class WghtdRndmExtrct
def initialize(arry = nil)
@key_cntr = 0
@rltt_cntr = 0
@wghtd_rltt = Hash.newunless arry ==nil then
addArry(arry)
end
enddef addArry(arry)
for elmnt in arry
addElmnt(elmnt[:wght],elmnt[:id])
end
enddef addElmnt(wght,id)
wght.times{
@wghtd_rltt[ @rltt_cntr ] = id
@rltt_cntr = @rltt_cntr + 1
}
@key_cntr = @key_cntr + 1
enddef keyCnt
return @key_cntr
enddef rlttCnt
return @rltt_cntr
enddef getRlltValue(key1,key2=1,key3=1,key4=1)
unless @wghtd_rltt == nil then
id = (key1.to_i * key2.to_i * key3.to_i + key4.to_i) % @rltt_cntr
return @wghtd_rltt[id]
end
endend
抽選用のHashが異様に大きくならない状態(100万レコード以内)であれば
パフォーマンス的にも問題なく動作します。
実際に抽選値を取得するgetRlltValueは今回それほど厳密さを求められる要件ではないので
それほどまじめに実装していませんが、
掛け算の結果が要素数(rltt_cntr)に比べて大きくなるようにしないと
HASHの最初のほうのIDに抽選結果が偏るので、その点を気をつけることを推奨します。
実際にこのユーティリティーを利用している例も書いておくと
def wrdChsr(usrId)
##本日日付を8桁キャラで取得する
t = Time.now
tdy = t.strftime('%Y%m%d')##パフォーマンスを考慮して抽選用HASHは一日に一度だけ再作成する。
if @@rllt_vrsn == nil || @@rllt_vrsn != tdy then##公開中のワードを取得する
wrds = MWrd.find(:all, :conditions => "pblc_dvsn = '01'")##抽選オブジェクトを初期化する。
rllt = WghtdRndmExtrct.new##抽選オブジェクトにワードを設定していく
for wrd in wrds##投票集計値(vote_sum_pnt)投票数(vote_cnt )から平均点を得るて、重みとする。
if wrd.vote_sum_pnt != nil || wrd.vote_cnt != nil then
rllt.addElmnt(( wrd.vote_sum_pnt / wrd.vote_cnt ).to_i + 1, wrd.id)
else
rllt.addElmnt(1, wrd.id)
endend
##バージョンを更新する
rllt_vrsn = tdy
end##ユーザーIDと本日日付の数字を元に抽選IDを得る
return rllt.getRlltValue(usrId,tdy.to_i)end
あるユーザーにとって、ある日同じものが表示されてほしいという用件だったので
このような実装となりましたが
毎回ことなる結果を表示したいのであれば
引数に乱数などを設定するとよいのではと思います。
オーバーロードは書けるの?
オーバーロードは書けるの?
Javaでは同じ名称のメソッドで引数の型、個数などを変えられる
いわゆるオーバーロードというテクニックが存在します。
Rubyに関しては、オーバーロードという記述方法は存在していないようです。
そもそもスクリプト言語ということもあり
引数に関する型判定が存在していないということが一番大きな理由と思われます。
しかし、実際問題としてオーバーロードを使えないと
プログラムとして使いにくいということも多いはずです。
Rubyではオーバーロードこそできませんが、同じようなことをする方法はいくつか提供されています。
■引数にデフォルト値を設定する
下記のように、引数に対して「=」をつなげて値を設定することでデフォルト値を設定することができます。
デフォルト値が設定できれば引数自体は省略可能になります。
def hoge(key1,key2,key3=nil,key4=nil)
print(key1)
print(key2)unless key3 ==nil then
print(key3)
endunless key4 ==nil then
print(key4)
end
end
勿論、実際に引数が渡されてくればそちらがデフォルト値よりも優先されます。
http://www.rubylife.jp/ini/method/index3.html
引数の順番の意味が固定になることは避けられないのですが、
個人的には、これで大方の要件は満たせました。
■引数をハッシュや配列で渡す
引数で受け取ったハッシュや配列を、メソッドのロジックの中でばらして
使用するという方法もあります。
配列を渡す方法として
def hoge(*keys)
end
というように「*」を利用することもできるようです。
http://www.ruby-lang.org/ja/man/html/FAQ_CAD1BFF4A1A2C4EABFF4A1A2B0FABFF4.html#a2.2e8.20.2a.a4.ac.a4.c4.a4.a4.a4.bf.b0.fa.bf.f4.a4.cf.b2.bf.a4.c7.a4.b9.a4.ab
どちらにしても、引数に対するチェックをしっかりしないと
若干恐いかもしれないなぁと感じます。
■引数の型を判定してCase文で書く
とてもべたな方法ですが、引数の型によって処理を変えたいならこの方法です。
def hogePrint(hoge)
case hoge
when hoge.class.to_s = "String" then
print hoge
when hoge.class.to_s = "Integer" then
print hoge.to_s
else
print "hoge is't String or Numeric"
end
end
相当ださいのであまり使いたくないですが、一応。
なお、型のチェックについては下に書いておきました。
Railsで集計処理を実装
Railsで集計処理を実装
蓄積したデータの合計や平均などを求めたいなどというとき
SQLで「Group By」を使用しますが
Railsではこのような場合にはActiveRecord::Calculationsを利用するようです。
http://api.rubyonrails.org/classes/ActiveRecord/Calculations/ClassMethods.html
こちらのモジュールの使い方ですが
まずはこれを利用したいエンティティーに対応したModelクラスに
このモジュールをミックスインします。
class HogeHoge < ActiveRecord::Base
include ActiveRecord::Calculationsend
こうすると、このモデルクラスで下のような関数が利用できるようになります。
- average :平均値を求める
- count :カウント
- maximum :最大値
- minimum :最小値
- sum :集計値
これを使って集計を行ってみる例がこんな感じです。
HogeHogeエンティティの「hoge_age」が30以上の「fuga」カラムの集計値が取得できます。
@hoge_sum = HogeHoge.sum(:fuga, :conditions => "hoge_age > 30")
これを利用すればかなり簡単に集計処理がかけます。
ActiveRecordのModelクラスとエンティティ名の関係を自由に設定したい
ActiveRecordのModelクラスとエンティティ名の関係を自由に設定したい
RailsではジェネレーターによってModelクラスを自動生成でき
テーブル名の複数形のModelクラスがデータベースのエンティティと対応するという
暗黙のルールが存在します。
しかし、どうしてもテーブル名とModelクラスの名前を分離したい場合もあります。
例えば
・複数形のModelクラスが気に食わない場合
・複数の画面で異なるリレーションでModelクラスを利用しなければいけない場合
・エンティティ名とModelクラス名をリンクさせたくない場合
一応こういう時に、エンティティ名とModelクラスの癒着を解消する方法がありました。
■単純に複数形を解除したいとき
「/config/environment.rb」の中で
ActiveRecord::Base.pluralize_table_names = false
と記述することで、アプリケーション全体的に複数形にしなくなります。
ただしこれをやると、全てのアプリケーションに適用されてしまうのでご注意を。
■エンティティ名を独自に定義したいとき
Modelクラス内に次のように定義することで、
自由にModelクラスとエンティティの関係を構築できます。
class Hoge < ActiveRecord::Base
set_table_name "hoge_mst"
end
さらに「set_primary_key」という設定を行えば
PKも「id」にとらわれることなく自由に設定できるようになります。
DATE_SELECTをCHAR型のカラムに対応させる
DATE_SELECTをCHAR型のカラムに対応させる
Rubyには日付形式の入力フォームをサポートする
date_selectという機能が存在します。
こちら年・月・日を別々のプルダウンで表現してくれて便利な機能です。
ERBファイルの中に下のように書き込んで利用することが出来ます。
誕生日:<%= send(:date_select, "user", "brthday",:start_year => 1850, :end_year => Time.now.year, :use_month_numbers => true) %>
2月31日のようなありえない日付が入力できてしまうなど問題もありますが
問題を是正するプラグインもでているようです。
さて、このdate_selectですがちょっと残念な問題を抱えている
対応するmodelがRubyのDate型に対応するカラムでなければエラーで落ちてしまいます。
日付系の入力項目であっても、後の検索しやすさなどの為にCHAR型で保持したいと言うことはよくあります。
Date型系のカラムはDB上でもIndex評価がほとんどうまく行かないので
正直DBが専門の僕としてはまったく推薦できない状況です。
というわけで、どうしてもCHAR型のフィールドにdate_selectフィールドの検索結果を格納したかったので
Modelクラスに次のような補正を行うことで対応しました。
(1)MODELクラスのインスタンスフィールドにDATE型の値格納用変数を用意する。
class User < ActiveRecord::Base
attr_accessor :brthday_timestmp ##誕生日(デート型格納用)
end
(2)ERB中のDATE_SELECTに対応するフィールドの名称を(1)の変数に変更する。
誕生日:<%= send(:date_select, "user", "brthday_timestmp",:start_year => 1850, :end_year => Time.now.year, :use_month_numbers => true) %>
(3)MODELクラスにinitializeメソッドを作成する
class User < ActiveRecord::Base
##日付型ダミー変数の取得用関数、実態としては文字列の日付を変換して返す
def brthday_timestmp
if brthday != nil then
@brthday_timestmp = Date::new(brthday[0,4].to_i, brthday[4,2].to_i, brthday[6,2].to_i)
end
end
##日付型ダミー変数の設定用関数
def brthday_timestmp=(value)
@brthday_timestmp = value##文字列方の変数に格納
frmtStrng = '%Y%m%d'
attributes["brthday"] = @brthday_timestmp.strftime(frmtStrng)end
##属性の設定
def attributes=(attributes)if attributes != nil && attributes.has_key?("brthday_timestmp(1i)") then
if attributes["brthday_timestmp(1i)"].length > 0 then
##年・月・日の項目別にローカル変数に格納
year = attributes["brthday_timestmp(1i)"]
mnth = attributes["brthday_timestmp(2i)"]
day = attributes["brthday_timestmp(3i)"]##日付型の変数に格納
@brthday_timestmp = Date::new(year.to_i, mnth.to_i, day.to_i)##文字列方の変数に格納
frmtStrng = '%Y%m%d'
attributes["brthday"] = @brthday_timestmp.strftime(frmtStrng)
end
##パラメーターから日付関連KEYを削除
attributes.delete("brthday_timestmp(1i)")
attributes.delete("brthday_timestmp(2i)")
attributes.delete("brthday_timestmp(3i)")end
##スーパークラスを呼ぶ
super
end
##モデルクラス初期化用メソッド
def initialize(attributes = nil)##画面からの属性がわたってきて初期化されるときのみ
##日付型の変換処理を実行する
if attributes != nil then
attributes=(attributes)
endend
end
以上でうまく動くようになりました。
当初Modelクラスのインスタンス変数は作成していなかったのですが
(そんなものがあるのはかっこ悪いなぁと)
この場合、更新処理などにおいて、
インスタンスの保持値をdate_selectで受け取るところでエラーとなってしまうので
この形となりました。
なお、インスタンス変数の取得用メソッドは
実態としては文字列の日付を日付型に変換して戻すように作ります。
毎回記述Modelにしないといけないのがとっても面倒なので
ユーティリティー化したいのですがど、この方式では難しく
date_selectをいじったほうがはやそうです。
そのうち暇があれば挑戦してみます。
(続)Ruby On RailsにLoginEngineのソースを直しました
(続)Ruby On RailsにLoginEngineのソースを直しました
実は続きがありました。
LoginEngineではWebページにリクエストがあったときに
一旦リクエストのURLをセッションに格納して
ログインページに飛ばした後に、
ログインに成功すると、セッションに格納してあるURLを取得してリダイレクトする仕組みになっています。
ところが、この処理がうまく行かずにエラーが発生していることが判明しました。
(いや、結構前から知っていて直していたけど書き忘れていました。)
原因は
/vender/plugins/login_engines/lib/login_engine/authenticated_system.rb
の80行目
def redirect_to_stored_or_default(default=nil)
if session['return-to'].nil?
redirect_to default
else
redirect_to_url session['return-to']
session['return-to'] = nil
end
end
どうもここがエラーになります。
どうもredirect_to_urlはRails2.0で消滅したメソッドのようです。
と言うわけで、これをredirect_toに置き換えます。
def redirect_to_stored_or_default(default=nil)
if session['return-to'].nil?
redirect_to default
else
redirect_to session['return-to']
session['return-to'] = nil
end
end
これで動くようになりました。
########################################################
◎LoginEngineの導入
其の壱 http://d.hatena.ne.jp/sai-ou89/20080401
其の弐 http://d.hatena.ne.jp/sai-ou89/20080402
其の参 http://d.hatena.ne.jp/sai-ou89/20080403
其の四 http://d.hatena.ne.jp/sai-ou89/20080404
其の五 http://d.hatena.ne.jp/sai-ou89/20080604
########################################################
バージョンアップ Rails2.0.1→2.1.0
バージョンアップ Rails2.0.1→2.1.0
Railsがバージョンアップされたそうです。
http://itpro.nikkeibp.co.jp/article/NEWS/20080602/305646/
これを当てたら動かなくなるソースとかあるのかなと
ちょっとおっかなびっくりしましたが
アップデートを実行
gem update rails
更新適用後、アプリケーションを動かしてみると
なんと、特に動かなくなっているものは無い模様。
下記のページを見ると、おお色々変わっている感じ。
http://docs.google.com/View?docid=ddn3rmd_12ddzcw4q3
特にActiveSupportあたりはAPIが変わっている?
という印象ですが、問題が無かったのはまだ僕がたいしたものを作ってないからの模様です。
と言うわけで、今後何か動かなくなっているものがあれば報告します。