【Rails】ビット列を活用した、Todoアプリを作ってみませんか？

# はじめに
DMM WEBCAMPのAdvent Calendarの **15** 日目の担当することになりました、@takakouと申します🏙️✨

今回は、「**ビット列を活用した、Todoアプリを作ってみませんか？**」というテーマで記事をシェアします！📝

記事執筆は未熟者で、至らない点もあるかと思いますが、皆さんのコメントやフィードバックをお待ちしています！🚀💬
# 目次
<!-- タイトルとアンカー名を編集 -->
1. [目的 🎯](#1-目的)
1. [対象者 👨](#2-対象者)
1. [動作環境 🖥️](#3-動作環境)
1. [アプリ概要 🛠️](#4-アプリ概要)
1. [実装手順 🚀](#5-実装手順)
1. [参考文献 📚](#6-参考文献)
1. [おわりに 🎉](#7-おわりに)
<!-- 各チャプター -->

# 1. 目的
最初に本記事の目的を説明しておきます。
- タスクを管理するアプリで設定する曜日指定の定業タスクの発生周期や、カレンダーアプリなどの予定の発生周期を設定するのって内部的にどんな動きをしているのか理解する。
- ビット列を使った情報の管理を理解する。
- データを保持する際の効率性を重視できるようになる。


# 2. 対象者
本記事では下記の方々を対象としています。
- Railsのアプリケーションの作成経験がある方
- Todo管理アプリケーションを作ってみたい方
- ビット列を使った設計に興味がある方
- 一つのカラムにいくつかの情報を保存してみようと思ったことがある方。

# 3. 動作環境

この章では、本システムの動作環境について詳細に説明します。以下の環境で開発およびテストを行っています。

## ハードウェア
- **PC**: MacBook Air (M1, 2020モデル)
- **RAM**: 8GB

## ソフトウェア
- **OS**: macOS Monterey (バージョン 12.1)

## 開発環境
- **エディタ** : Visual Studio Code
- **言語**: Ruby (バージョン 3.1.2)
- **フレームワーク**: Rails (バージョン 6.1.7)


# 4. アプリ概要
この章では簡単にどんなアプリを作るのか、**ビット列**とは何で、なぜ使うのか、等々を説明していこうと思います。

## どんなアプリを作るのか

週次のタスク管理アプリみたいなものを作ろうと思っています。
機能としてはこんな感じ

- タスクを登録しておける
- タスクは 「月~日」 まで複数曜日選ぶことができる。
- 利用者は自分が登録したタスクを曜日ごとに一覧で確認できる。

## ビット列とは何か
ビット列とは、ビット単位の論理演算で特定のビットだけを操作対象とするために用いられるビットパターンのことです。特定のビットだけを取り出したり変更することができる。これだとイメージが湧かないと思うので簡単に例を挙げてみます。

Wifi,Bluetooth,AirDropの機能のON/OFFで例えてみましょう。
機能がONの場合は「**1**」をOFFの場合は「**0**」をフラグとして立てるとしましょう。

フラグを立てた時の例を下記に挙げておきます。

```
Wifiが「ON」,Bluetoothが「OFF」,AirDropが「ON」とします。
=>101

Wifiが「OFF」,Bluetoothが「OFF」,AirDropが「ON」とします。
=>001
```
というふうになります。
なので、このビット列を一つのカラムに挿入することで、三つの状態を一つのカラムで管理することができるという感じです。

## どの部分で使うのか
今回作成するタスク管理アプリでは、「周期」というカラムを使用して、タスクが行われる曜日を設定します。例えば、月曜日が「1」、火曜日が「2」として、それぞれの曜日に対応する2のべき乗の値を持ちます。タスクの設定で複数の曜日を選択した場合、それぞれの曜日の値をビット単位で「OR」演算します。例えば、月曜と水曜のタスクは 1 | 4 = 5 として表現されます。この方法により、一つのカラムで複数の曜日情報を効率的に管理できます。


## なぜ使うのか
ビット列を使用する主な利点は、データの効率化にあります。ビット列を使わない場合、タスクごとに複数の曜日情報を別々に保存する必要がありますが、ビット列を使うことで、これらの情報を1つのカラムに集約できます。
文字だと伝わりづらいので、使わなかった時と、使った時に分けて、説明してER図で確認してみましょう。

### ビット列無
ビット列を使わない設計だとER図はこんな感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/c8996955-c666-b4bc-21bd-9ad5f3429d21.png)

わざわざタスク一つに対して、いくつかタスクの詳細のデータを作成しなくてはならず、かなり冗長な処理になってしまうなと感じます。


### ビット列有
ビット列を使う設計だとER図はこんな感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/5c1587ae-4db1-49ae-bc89-6ac31df36da1.png)

「周期」のカラムにはビットフラグを用いて何曜日のタスクなのか登録していく方式になります。
月〜日の順番で、例えば土日のタスクなら`0000011`で、平日のタスクなら`1111100`という感じになります。これで別モデルを用意しなくなったのでめちゃくちゃ便利です!



### 比較
ということで、ビット列が無い方はテーブルが一つ増え、例えば、1タスクにつき最大7つのデータが作成されてしまいます。これって曜日を参照するだけなのにこんなことするの無駄だと思いませんか..?
ということで二つ目のビット列有りの設計を用いて一つのカラムに複数のデータを入れるような方式を使ってアプリの制作をしてみます。



# 5. 実装手順

この章では、実際にタスク管理アプリを作る手順について説明してみます。

:::note warn
**注意点**
初心者向けですし、わざわざページをスクロールして戻るのも面倒だと思いますので、モデルに処理をまとめすぎないようにしています。かなりviewファイルが汚れていますがご了承ください。
:::


## アプリケーション作成
まずはアプリケーションを作成したいディレクトリに移動し、下記コマンドを打ちます。

```sh:Terminal
~ $ rails new bitMaskTodoApp
```

作成したアプリにディレクトリを移動し、下記コマンドを打ちます。

```sh:Terminal
~ $ yarn add @babel/plugin-proposal-private-methods @babel/plugin-proposal-private-property-in-object
```

念のため起動できるか確認しておきます
```sh:Terminal
~ $ rails s
```
下記のURLにアクセスし画像のように表示されていたらとりあえず問題ありません。

http://localhost:3000/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/837afd7e-7c72-757c-930c-b00e2674bfd1.png)

## Bootstrap導入
今回はCDNで導入します。
ついでに表示するページも全体の半分にし、少しpaddingを入れておきます。
```diff_erb:views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>BitMaskTodoApp</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  </head>

  <body>
+   <div class="p-5 w-50">
      <%= yield %>
+    </div>
  </body>
</html>

```

## Model作成
Taskのモデルを作成していきます

### Taskモデル
```sh:Terminal
~ $ rails g model Task 
```

Taskモデルのmigrationファイルができると思うので、内容を追加しておきます。

```diff_ruby
class CreateTasks < ActiveRecord::Migration[6.1]
  def change
    create_table :tasks do |t|
      # タスク名
      t.string :name
      # 内容
      t.string :body
      # 周期
      t.integer :is_loop
      # 作成日時 更新日時
      t.timestamps
    end
  end
end
```

migrationが通しておきます。
```sh:Terminal
~ $ rails db:migrate
```

Taskのモデルファイルも変更をしておきます。

```diff_ruby:task.rb
class Task < ApplicationRecord
    
  validates :name, presence: true
  validates :body, presence: true
  validates :is_loop, presence: true
  
  # ヘルパーメソッドとして定義
  def display_is_loop(is_loop_value)
    options = [
      "月",
      "火",
      "水",
      "木",
      "金",
      "土",
      "日",
    ]

    selected_values = []

    options.each_with_index do |option, index|
      bit = 2**index
      selected_values << option if is_loop_value & bit == bit
    end

    selected_values.join(", ")
  end
end
```

## Controller作成

tasks_controllerを作成します。

```sh:Terminal
~ $ rails g controller tasks
```


コントローラーの内容を記述しています。
```diff_ruby:tasks_controller
class TasksController < ApplicationController
  # 一覧
  def index
    # タスクの一覧を取得
    @tasks = Task.all
  end

  # 新規作成ページ
  def new
    # タスクの空インスタンスを定義
    @task = Task.new
  end

  # 新規作成処理
  def create
    # is_loopが送られているかを確認
    unless task_params[:is_loop].nil?
      # 選択された値を取得
      selected_values = task_params[:is_loop].map(&:to_i)

      # 選択された値をビット列に変換
      is_loop_mask = selected_values.reduce(0) { |sum, value| sum | value }
    end

    # @taskに登録用のデータをまとめて挿入
    @task = Task.new(task_params.merge(is_loop: is_loop_mask))

    # タスクを保存
    if @task.save
        # タスクの一覧ページへ遷移
        redirect_to tasks_path,notice: "タスクを削除しました。"
    else
        # newページを再レンダリング
        render :new
    end
  end

  # 削除処理
  def destroy
    # URLに含まれてるidから削除するタスクを検索
    @task = Task.find(params[:id])
    # タスクを削除
    @task.destroy
    # タスクの一覧ページに遷移
    redirect_to tasks_path,notice: "タスクを削除しました。"
  end

  protected

  # パラメータ取得用のメソッド
  def task_params
    params.require(:task).permit(:name, :body, is_loop: [])
  end
end
```

## routes.rbの変更
routes.rbの下記の記述を追加しておきます。
```diff_ruby:routes.rb
Rails.application.routes.draw do

  #タスクのRouting
  resources :tasks,only: [:index, :new, :create, :destroy]

end
```



## View作成
`views/tasks`配下に`new.html.erb`と`index.html.erb`を作成します。
それぞれのページにて処理、レイアウトを記述していきます。

```erb:views/tasks/new.html.erb
<%= form_with model: @task do |form| %>
  <% if @task.errors.any? %>
    <div id="error_explanation" class="alert alert-danger">
      <h2><%= pluralize(@task.errors.count, "error") %> prohibited this task from being saved:</h2>

      <ul>
        <% @task.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="mb-3">
    <%= form.label :name,"タイトル",class: 'form-label' %>
    <%= form.text_field :name, class: 'form-control' %>
  </div>

  <div class="mb-3">
    <%= form.label :body,"内容", class: 'form-label' %>
    <%= form.text_area :body, class: 'form-control' %>
  </div>

  <div class="mb-3">
    <%= form.label :is_loop, "実行曜日", class: 'form-label' %><br>
    <% options = [
        ["月", 1],
        ["火", 2],
        ["水", 4],
        ["木", 8],
        ["金", 16],
        ["土", 32],
        ["日", 64]
      ] 
    %>
    <% options.each do |option| %>
      <div class="form-check form-check-inline">
        <%= form.check_box :is_loop, { multiple: true, checked: (@task.is_loop & option[1]) == option[1], class: 'form-check-input' }, option[1], nil %>
        <%= form.label :is_loop, option[0], value: option[1], class: 'form-check-label' %>
      </div>
    <% end %>
  </div>

  <div class="actions">
    <%= form.submit class: 'btn btn-primary' %>
  </div>
<% end %>
```

```erb:views/tasks/index.html.erb
<p id="notice" class="text-success"><%= notice %></p>

<h1>Tasks</h1>
<%= link_to 'New Task', new_task_path %>
<table class="table">
  <thead>
    <tr>
      <th>タイトル</th>
      <th>内容</th>
      <th>実行曜日</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @tasks.each do |task| %>
      <tr>
        <td><%= task.name %></td>
        <td><%= task.body %></td>
        <td><%= task.display_is_loop(task.is_loop) %></td>
        <td><%= link_to 'Destroy', task, method: :delete, data: { confirm: '本当に削除しますか?' },class:"btn btn-sm btn-danger" %></td>
      </tr>
    <% end %>
  </tbody>
</table>
```




## 検索機能の追加
viewページで月〜日のボタンを用意し、クリックしたらその曜日のタスクが出てくるようにしてみます。
```diff_erb:views/tasks/index.html.erb
<p id="notice" class="text-success"><%= notice %></p>

<h1>Tasks</h1>
<%= link_to 'New Task', new_task_path %>

+ <div class="mt-2 mb-2">
+   <% [["月", 1], ["火", 2], ["水", 4], ["木", 8], ["金", 16], ["土", 32], ["日", 64], ["全体", 127]].each do |day, bit_value| %>
+    <%= link_to day, tasks_path(is_loop: bit_value), class: "btn #{params[:is_loop].to_i == bit_value ? "btn-primary" : "btn-secondary"} btn-sm mr-2 d-inline-block" %>
+   <% end %>
+ </div>


<table class="table">
  <thead>
    <tr>
      <th>タイトル</th>
      <th>内容</th>
      <th>実行曜日</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @tasks.each do |task| %>
      <tr>
        <td><%= task.name %></td>
        <td><%= task.body %></td>
        <td><%= task.display_is_loop(task.is_loop) %></td>
        <td><%= link_to 'Destroy', task, method: :delete, data: { confirm: '本当に削除しますか?' },class:"btn btn-sm btn-danger" %></td>
      </tr>
    <% end %>
  </tbody>
</table>
```



indexアクションの内容を変更します。
```diff_ruby:tasks_controller
def index
+    # クエリストリングにis_loopがあるか確認
+    if params[:is_loop]
+      # ANDを用いて検索
+      @tasks = Task.where('is_loop & ? > 0',params[:is_loop].to_i)
+    else
      # タスクの一覧を取得
      @tasks = Task.all
+    end
end
```

## テストデータの作成
seedを使って初期を入れて見ましょう。
```rb:db/seeds.rb
# タスクを作る
Task.create!(name:"タスク1",body:"内容1",is_loop: 1)
Task.create!(name:"タスク2",body:"内容2",is_loop: 1+2)
Task.create!(name:"タスク3",body:"内容3",is_loop: 1+2+4)
Task.create!(name:"タスク4",body:"内容4",is_loop: 1+2+4+8)
Task.create!(name:"タスク5",body:"内容5",is_loop: 1+2+4+8+16)
Task.create!(name:"タスク6",body:"内容6",is_loop: 1+2+4+8+16+32)
Task.create!(name:"タスク7",body:"内容7",is_loop: 1+2+4+8+16+32+64)
```

入力が終わったら下記のコマンドでseedでデータを作成しましょう。
```sh
~ $ rails db:seed
```

## 動作検証
`rails s`でアプリケーションを起動後、下記のURLにアクセスして動作検証をして見ましょう

http://localhost:3000/tasks

### タスク一覧
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/8e74402e-9e4d-fa08-6850-2a2dd1dc1ee7.png)

### タスク一覧(曜日指定)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/5b1b11ec-0bfc-2d46-2254-56543e407d44.png)


### タスク作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/a83c9fbe-3381-8eb1-a8c8-0733ce596e07.png)

作成、削除、一覧でのデータ確認、一覧での曜日指定等の操作が上手くいったら、実装は終了になります。
ここまで長かったと思いますが、お疲れ様でした。

# 6. 参考文献

https://e-words.jp/w/%E3%83%93%E3%83%83%E3%83%88%E3%83%9E%E3%82%B9%E3%82%AF.html#google_vignette

# 7. おわりに

本記事では、ビット列を用いたタスク管理アプリ制作についてまとめてみました！

本記事を通して、ビット列を用いた設計や普段使っているアプリで周期を設定する際に内部的にどのようにデータを保存しているのかを理解いただければとても嬉しいです。

Advent Calendarはまだまだありますので、楽しんでいただけますと幸いです！

最後に、最近、自分のブログをNext.jsとmicroCMSを使って作成することにハマっているので、暇な方や興味がある方がいらっしゃればそちらも見ていただますと泣いて喜びます！

https://kt-tech.blog/