【個人開発】予約表の日時表示

# 本記事の概要
以前、予約管理アプリを開発したので、その内容を記した記事を投稿してみました。

https://qiita.com/Sakita0514/items/6c9c22a1f743fa0bfb51

今回は、このアプリで実装した機能の備忘録として、予約機能および予約確認機能における予約表の表示方法について、自分でも忘れた部分があるので備忘録として残したいと思います。
# 使用技術
使用技術は上記記事の内容と変わらないです。
|種類|使用技術名|
|---|---|
フロントエンド|HTML, CSS
バックエンド|Java（サーブレット、JSP）
開発環境|Eclipse IDE(4.6.3)
インフラ|Apache Tomcat v8.0
DB|MySQL(5.7.33)
DB連携|Hibernate(5.2.13final)

# 予約表の概要
予約表は下図のように、列に年月日がシステム日付から10日分表記されます。そして、行には時刻が9～20時まで1時間間隔で表記されており、その日時が予約可であれば「◎」が表示され、予約不可であれば「×」が表示されます。予約可「◎」の日時であれば、「◎」をクリックすると予約確定画面へと遷移し、DBに予約データが登録される仕組みになっております。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563748/334b08a2-5ade-f215-9f7e-03727f015cec.png)
# 予約表実装における難点
予約表は概要でも説明した通り、システム日付から10日分の期間を表示しています。この10日分の期間において月をまたいでいると年月の表示部分が1ブロック（Fig.1）から2ブロック（Fig.2）に増えます。Fig.2だと9月から10月かけての表示になっているため、「2023年9月」と「2023年10月」の表示が必要です。この表示方法を動的に変更できるよう実装しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563748/5f62fa90-bc26-1c40-95b2-8e3b70543a95.png)
　　　　　　　　　　　　　Fig.1　表示期間が月を跨がない場合
        
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563748/334b08a2-5ade-f215-9f7e-03727f015cec.png)
　　　　　　　　　　　　　Fig.2　表示期間が月を跨ぐ場合（9月→10月）

# ソースコードの解説
実際のソースコードについて解説します。
### 画面遷移フロー
本件にの予約表おける画面遷移フローは下図のようになっています。toppage/idnex.jspはトップページとなっており、この画面で「予約する」を選択すると、ReserveIndexServletを経由して予約日時選択画面であるreserve/index.jspに遷移します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563748/c035db54-b538-c334-ec13-8ac00ed2992c.png)
### JSPの解説
まず、reserve/index.jspのソースコードについて解説します。
下記のソースコードはreserve/index.jspの見出しから予約表における年月表示部分（Fig.3）のコードを示しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563748/ab19aa37-9478-1e64-c99c-79a276aa9b87.png)
　　　　　　　　　　　　Fig.3　赤枠部分が予約表の年月表示部分

```
<h2>予約管理システムへようこそ</h2>
        <h3>【予約状況】　◎：予約可　×：予約不可</h3>
        <table>
            <thead>
                <tr>
                    <td class="monthCell"></td>
                    <th class="dateCellNonborder" colspan="${firstColspan}"
                        style="width:${firstPx}px;">${yearList[0]}年${monthList[0]}月</th>
                    <c:if test="${secondColspan > 0}">
                        <th class="dateCellNonborder" colspan="${secondColspan}"
                            style="width:${secondPx}px;">${yearList[1]}年${monthList[1]}月</th>
                    </c:if>
                </tr>
```


ソースコードについて抜粋して上から順に説明します。
まず、下記ソースコードのh2,h3タグは見出しと表の説明のようなものです。
```
<h2>予約管理システムへようこそ</h2>
        <h3>【予約状況】　◎：予約可　×：予約不可</h3>
```
下記ソースコードのように次行のtableタグが予約表全体を構成しています。そして、theadタグ内のtrタグはこのタグ内のコードがこの予約表における最上段の行に実装されることを示しており、本アプリではこのtrタグ内で年月を表示しています。
そして、trタグ内のthタグで年月の表示を実装していますが、ここでポイントが2つあります。
#### ポイント①「今月のみ」と「今月かつ翌月」の表示切替
ポイントの1つ目は、年月表示における「今月のみ」の表記と「今月かつ翌月」の表記の切り替え方です。この予約表における表示期間はシステム日付から10日分と定めているので、年月の表示は、月を跨がず今月表示分のセル1つ分のみ必要なパターンと月を跨ぎ今月と翌月の表示分のセル2つ分必要なパターンの2通りがあります。この2通りのパターンに対して動的に対応可能なように、セル1つ分はデフォルトで表記し、セル2つ目はサーブレットから受け取った値による条件分岐によって実装しました。よって、始めのthタグが今月分の表記、c:ifタグから始まる部分が条件分岐による翌月の表記になっており、2通りを切り替えられるようになっています。この条件分岐における条件は、EL式で記述したsecondColspanの値によって決定しますが、この値はサーブレットから渡されるため、サーブレットの説明部分で後述します。
#### ポイント②年月表示セル数の動的対応
ポイントの2つ目はthタグ内の属性であるcolspanによってその年月表示がセル何個分必要かを動的に対応している点です。もし、年月表示部分において「今月かつ翌月」の表示が必要だった場合、システム日付によって「今月」「翌月」の表示日数が異なります。例えば、システム日付が2023/9/25だった場合、Fig.4のように「2023年9月」は6日分、つまりセル6個分がcolspanの値となり、「2023年10月」は4日分つまりセル4個分がcolspanの値となるので、このcolspanの値に対して動的な対応を実現するため、JSPではfirstColspan,secondColspanでサーブレットから値を受け取っています。この2つの変数に関してもサーブレット説明部分で後述します。
```
<thead>
                <tr>
                    <td class="monthCell"></td>
                    <th class="dateCellNonborder" colspan="${firstColspan}"
                        style="width:${firstPx}px;">${yearList[0]}年${monthList[0]}月</th>
                    <c:if test="${secondColspan > 0}">
                        <th class="dateCellNonborder" colspan="${secondColspan}"
                            style="width:${secondPx}px;">${yearList[1]}年${monthList[1]}月</th>
                    </c:if>
                </tr>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563748/ba57c71e-7792-b027-f5c3-815c433ce0a4.png)
　　　　　　　　　Fig.4　システム日付が2023/9/25だった場合の例
### サーブレットの解説
reserve/index.jspに遷移する際のReserveIndexServletのソースコードについて解説します。
doGetメソッドのソースコードを下記に一部抜粋しました。それについて解説します。
①NumofDaysで予約表の表示日数（10日）を定めており、JSPに渡す年月日と曜日のリストを宣言しています。
②システム日付の値を取得しています。
③変数sameMonthCountで10日分の日付を取得した際に重複する月が何日分あるかカウントしています。このsameMonthCountが10であれば、予約表にて表示する月は「今月」のみです。値が10未満であれば、10日の間で月を跨いでいるので予約表の表示は「今月かつ翌月」になります。
④②で取得したシステム日付は年月日が連結された状態でインスタンスに代入されるので、そのインスタンスから「年」「月」「日」「曜日」をそれぞれ抽出するためのSimpleDateFormat型のインスタンスを生成しています。
⑤このfor文にて本日から10日分の日付を全て確認し、①で宣言した各リストに「年」「月」「日」「曜日」を代入すると共に③で宣言したsameMonthCountのカウンタを起動しています。
⑥このif文で始めは空である「年」と「月」のデータを格納するリストにシステム日付でいう本日の値をそれぞれ格納します。
⑦このif文で本日から10日間の日付のうち重複している「月」の値をカウントしています。すなわち③で宣言したSameMonthCountのカウンタの起動です。カウンタはシステム日付でいう本日の月と⑤のfor文で回している本日からn日目の月が一致していればインクリメントされます。そのインクリメントされる際のif文の条件は、(j >= 1 && monthList.size() < 2)なっています。j >= 1は⑤で実行したfor文のループの2回目以降はtrueにするという意です。ループの1回目は本日のシステム日付同士の一致確認となってしまい、ここでインクリメントされてしまうと、カウンタの数が1個多くなってしまいます。monthList.size() < 2は「月」を格納するリストの最大要素数を2個にするための条件です。JSPの解説で説明した通り、予約表の表示期間は10日分なので、表示する月は多くても「今月かつ翌月」の2個です。このif文におけるelseにおいて、「月」のリストへの値格納も兼ねているため、「月」リストの最大要素数を超過しないようこの条件を指定しています。
⑧ここでは日付と曜日をそれぞれ日付から抽出して各リストへ格納しています。
⑨ここでシステム日付のインスタンスを1日進めて⑤のfor文の条件式へと戻っています。
```
        //①テーブルに表示する日付のカラム数
        int NumofDays = 10;
        List<String> yearList = new ArrayList<String>();
        List<String> yearListAll = new ArrayList<String>();
        List<String> monthList = new ArrayList<String>();
        List<String> monthListAll = new ArrayList<String>();
        List<String> dateList = new ArrayList<String>();
        List<String> DayOfWeekList = new ArrayList<String>();

        //②日付の取得
        Calendar cal = Calendar.getInstance();
        //cal.set(2023, Calendar.SEPTEMBER, 25);

        //③同月カウンタ
        int sameMonthCount = 0;
        String month_0;

        //④日付の表示形式の設定
        SimpleDateFormat sdf_year = new SimpleDateFormat("Y");
        SimpleDateFormat sdf_month = new SimpleDateFormat("M");
        SimpleDateFormat sdf_day = new SimpleDateFormat("d");
        SimpleDateFormat sdf_DayOfWeek = new SimpleDateFormat("(E)");

        //⑤本日から指定日数分の日付を取得
        //月を跨ぐ場合の処理含む
        for (int j = 0; j < NumofDays; j++) {
            //⑥月のリストが空であれば月の値を挿入
            if (monthList == null || monthList.isEmpty()) {
                yearList.add(sdf_year.format(cal.getTime()));
                monthList.add(sdf_month.format(cal.getTime()));
            }

            //⑦for文のループ2回目以降(月の重複挿入防止)かつ月リストの最大要素数未満であれば月を挿入
            if (j >= 1 && monthList.size() < 2) {
                month_0 = monthList.get(0);
                if (month_0.equals(sdf_month.format(cal.getTime()))) {
                    ++sameMonthCount;
                } else {
                    yearList.add(sdf_year.format(cal.getTime()));
                    monthList.add(sdf_month.format(cal.getTime()));
                }
            }

            //⑧日付、曜日の格納
            dateList.add(sdf_day.format(cal.getTime()));
            DayOfWeekList.add(sdf_DayOfWeek.format(cal.getTime()));
            //⑨システム日付を1日進める
            cal.add(Calendar.DATE, 1);
        }
```
以上のようにSameMonthcountのカウンタを用いて本日から10日間の日付のうち重複している「月」がいくつあるかカウントし、下記ソースコードのようにリクエストスコープを利用し、JSP側へ受け渡すパラメータを設定しています。firstColspanが今月の重複数、secondColspanが翌月の重複数となっており、
```
        request.setAttribute("firstColspan", sameMonthCount + 1);
        request.setAttribute("secondColspan", NumofDays - (sameMonthCount + 1));
```
JSPのポイント①で解説した通り、secondColspanが0であればJSPで解説した下記のif文の条件は満たさず、予約表には「今月」のみの表記となります。
また、JSPのポイント②で解説した通り、firstColspanは10日間の日付のうち「月」が重複した個数、すなわちSameMonthCountです。よって重複した個数分だけ表示セルが連結され、表示セルの大きさに動的に対応して予約表に表示されます。
```
<td class="monthCell"></td>
    <th class="dateCellNonborder" colspan="${firstColspan}"
        style="width:${firstPx}px;">${yearList[0]}年${monthList[0]}月</th>
            <c:if test="${secondColspan > 0}">
                <th class="dateCellNonborder" colspan="${secondColspan}"
                    style="width:${secondPx}px;">${yearList[1]}年${monthList[1]}月</th>
            </c:if>
```
# 総括
今回は、動的な予約表の表示について解説しました。本件のアプリ開発において最も実装が難しかった部分だったので解説記事を残してみました。振り返ってソースコード自体は効率性や冗長性に優れていたのではないかと思っています。現状、できる限りの知識を用いて実装できたと思います。ただ、説明は難しかったため、本記事の内容が読者に伝わるかどうか不安ではあります。。記事中のソースコードの埋め込み方や画像を用いた説明については、これから色んな記事を参考にさせていただき、改善していきたいと思っています。
# 次回の内容
次回は、サーブレット間のパラメータの受け渡しについて、記事を作成する予定です。[本アプリ開発の記事](https://qiita.com/Sakita0514/items/6c9c22a1f743fa0bfb51)の課題にも残しましたがパラメータを①セッションスコープで渡すのか②URLの乗せて渡すのか、について両方の特徴や使用機会に合わせた場合分けなどを調査して記事に残したいと思います。
# あとがき
この記事についてのご質問やご意見、アドバイス等があれば些細なことでも良いのでお問い合わせください！
お待ちしております！