重み付抽選機能を作る

INDEX PAGE

重み付抽選機能を作る


登録されたデータを重み付けランダム抽選したいという要件は
Webでは意外とあるのではと思うのですが
今回、製作中のWebサイトで投稿された内容を
五段階のユーザーの評価の平均点に対応した頻度で表示する機能を作りました。


基本的な設計としては
■idと重みというセットのデータを複数登録していく
■登録完了後に、idを重みの数だけハッシュにセットしていく。
■このセットの中かidを無作為に抽出します。


具体的にはこんな感じのソースになりました。

class WghtdRndmExtrct

  def initialize(arry = nil)
    @key_cntr = 0
    @rltt_cntr = 0
    @wghtd_rltt = Hash.new

    unless arry ==nil then
       addArry(arry)
    end
  end

  def addArry(arry)
    for elmnt in arry
       addElmnt(elmnt[:wght],elmnt[:id])
    end
  end

  def addElmnt(wght,id)
    wght.times{
      @wghtd_rltt[ @rltt_cntr ] = id
      @rltt_cntr = @rltt_cntr + 1
    }
    @key_cntr = @key_cntr + 1
  end

  def keyCnt
    return @key_cntr
  end

  def rlttCnt
    return @rltt_cntr
  end

  def 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
  end

end

抽選用の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)
      end

    end

    ##バージョンを更新する
    rllt_vrsn = tdy
  end

  ##ユーザーIDと本日日付の数字を元に抽選IDを得る
  return rllt.getRlltValue(usrId,tdy.to_i)

end

あるユーザーにとって、ある日同じものが表示されてほしいという用件だったので
このような実装となりましたが
毎回ことなる結果を表示したいのであれば
引数に乱数などを設定するとよいのではと思います。


INDEX PAGE

オーバーロードは書けるの?

INDEX PAGE


オーバーロードは書けるの?



Javaでは同じ名称のメソッドで引数の型、個数などを変えられる
いわゆるオーバーロードというテクニックが存在します。


Rubyに関しては、オーバーロードという記述方法は存在していないようです。
そもそもスクリプト言語ということもあり
引数に関する型判定が存在していないということが一番大きな理由と思われます。


しかし、実際問題としてオーバーロードを使えないと
プログラムとして使いにくいということも多いはずです。
Rubyではオーバーロードこそできませんが、同じようなことをする方法はいくつか提供されています。


■引数にデフォルト値を設定する

下記のように、引数に対して「=」をつなげて値を設定することでデフォルト値を設定することができます。
デフォルト値が設定できれば引数自体は省略可能になります。

def hoge(key1,key2,key3=nil,key4=nil)
 print(key1)
 print(key2)

 unless key3 ==nil then
  print(key3)
 end

 unless 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

相当ださいのであまり使いたくないですが、一応。
なお、型のチェックについては下に書いておきました。

型(Class)のチェック



INDEX PAGE

Railsで集計処理を実装

INDEX PAGE

Railsで集計処理を実装



蓄積したデータの合計や平均などを求めたいなどというとき
SQLで「Group By」を使用しますが
Railsではこのような場合にはActiveRecord::Calculationsを利用するようです。
http://api.rubyonrails.org/classes/ActiveRecord/Calculations/ClassMethods.html


こちらのモジュールの使い方ですが
まずはこれを利用したいエンティティーに対応したModelクラスに
このモジュールをミックスインします。

class HogeHoge < ActiveRecord::Base
include ActiveRecord::Calculations

end

こうすると、このモデルクラスで下のような関数が利用できるようになります。

  • average :平均値を求める
  • count :カウント
  • maximum :最大値
  • minimum :最小値
  • sum :集計値


これを使って集計を行ってみる例がこんな感じです。
HogeHogeエンティティの「hoge_age」が30以上の「fuga」カラムの集計値が取得できます。

@hoge_sum = HogeHoge.sum(:fuga, :conditions => "hoge_age > 30")

これを利用すればかなり簡単に集計処理がかけます。


INDEX PAGE

ActiveRecordのModelクラスとエンティティ名の関係を自由に設定したい


INDEX PAGE


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」にとらわれることなく自由に設定できるようになります。



INDEX PAGE

DATE_SELECTをCHAR型のカラムに対応させる


INDEX PAGE


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)
  end

  ##スーパークラスコンストラクターを呼ぶ 
  super

 end

end


以上でうまく動くようになりました。
当初Modelクラスのインスタンス変数は作成していなかったのですが
(そんなものがあるのはかっこ悪いなぁと)
この場合、更新処理などにおいて、
インスタンスの保持値をdate_selectで受け取るところでエラーとなってしまうので
この形となりました。

なお、インスタンス変数の取得用メソッドは
実態としては文字列の日付を日付型に変換して戻すように作ります。


毎回記述Modelにしないといけないのがとっても面倒なので
ユーティリティー化したいのですがど、この方式では難しく
date_selectをいじったほうがはやそうです。

そのうち暇があれば挑戦してみます。

INDEX PAGE

(続)Ruby On RailsにLoginEngineのソースを直しました

INDEX PAGE

(続)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
########################################################

INDEX PAGE

バージョンアップ Rails2.0.1→2.1.0

INDEX PAGE


バージョンアップ 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が変わっている?
という印象ですが、問題が無かったのはまだ僕がたいしたものを作ってないからの模様です。

と言うわけで、今後何か動かなくなっているものがあれば報告します。

INDEX PAGE