[Rails] Bitwise Operator(ビット演算)を用いた曜日の管理

Cronのように曜日を指定してジョブを発動するスケジューラを作成していて、外部にこれを保持するテーブルを作るのはセンスがないし、mondayからsundayのカラムを作るのも無駄に感じたので、ビットを使って曜日を表現し、これを用いて曜日の判別をしようと思いました。 Railsでビット演算を用いたチェックボックスのフラグ管理をしてみたを多いに参考にさせていただきました。

具体的には下記のように表現します。

  • 月曜日 0b1
  • 火曜日 0b10
  • 水曜日 0b100
  • 木曜日 0b1000
  • 金曜日 0b10000
  • 土曜日 0b100000
  • 日曜日 0b1000000

1100101の場合、月曜日、水曜日、土曜日、日曜日が選択されている状態です。 1バイト以下の7bitで表現できるのでメモリにもストレージにも優しいですね。

Migrationファイルを作り、DBカラムwday作成。

$ rails g migration AddWdayToTimer wday:integer
$ vi db/migrate/20180818*
# limitを追加し、最小限1バイトのtinyint(dbによりますが)を作成
 t.integer :wday, limit: 1

View/Controllerは下記のようになりDBには整数で保存されます。

  <div class="field">
  <% %w(Mon Tue Wed Thu Fri Sat Sun).each_with_index do |v, i| %>
    <%= form.label :wday  do %>
      <%= form.check_box :wday, {multiple: true, checked: !form.object.wday.nil? && (form.object.wday & 2**i != 0)}, 2**i %>
      <%= v %>
      &nbsp
    <% end %>
  <% end %>
  </div>
  def create
    @timer = Timer.new(timer_params)
    @timer.wday = timer_params[:wday].map(&:to_i).inject(&:+)

これを使う時に、&し判定します。 0でない場合は、曜日フラグが立っているということです。  僕の遠い昔のアセンブリ言語の記憶が正しければbitのAND計算は鬼早いはずです。

[2] pry(main)> Timer.last.wday
  Timer Load (0.2ms)  SELECT  "timers".* FROM "timers" ORDER BY "timers"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> 21
[3] pry(main)> Timer.last.wday & 0b1 #月曜日
  Timer Load (0.1ms)  SELECT  "timers".* FROM "timers" ORDER BY "timers"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> 1
[4] pry(main)> Timer.last.wday & 0b10 #火曜日
  Timer Load (0.1ms)  SELECT  "timers".* FROM "timers" ORDER BY "timers"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> 0
[5] pry(main)> Timer.last.wday & 0b100 #水曜日
  Timer Load (0.1ms)  SELECT  "timers".* FROM "timers" ORDER BY "timers"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> 4

DBに対しては、月曜日にフラグがたってるRowを取り出すには、

Timer.where('wday & ? > 0', 0b0000001)

ビットフラグの管理はアプリケーション側にその管理を要求するのしっかりドキュメントしておく必要がありますので、忘れずにコメントを書いておきましょう。

Related Posts