Vue.jsでカレンダーを自作する

# 始めに
カレンダーを実装するにあたって、ライブラリを色々探していましたが、月曜日スタートのものが無く、要件に適するものが中々見つかりませんでした（そもそも月曜日スタートにしたいって日本だけなんですかね？）。
ライブラリを見つけてもスタイルの調整がやりやすいかとか色々懸念があったので最終的には自作したのですが、その時にどんな風に実装したかを記事にしました。

# 実装方法
ざっくりいうと以下の流れで実装しています。詳細は次から説明します。

1. カレンダーリストを生成する
    1. 先月の見えている日付を生成
    2. 今月の日付を生成
    3. 来月の見えている日付の生成
2. カレンダーリストのデータに従って表示する

## 先月の見えている日付を生成
いきなりで申し訳ないですが、ここが一番難所です（汗）。ここをクリアできれば後は比較的簡単ですので頑張りましょう。

### 事前知識
まず最初に結構よく使うので覚えて欲しいのですが、以下のようにすると最初の日付と最後の日付を取得できます。JavaScriptのDate型は日付の変更は意外と融通を効かせて、範囲外の数値を入れると前の月に遡ったり、次の月へ進めて計算してくれます。0にすると1日より1つ前になるので、先月の一番最後の日となります。

今月の最初の日付：`new Date(year, month)` // dayを入れない（1を入れてもいいです）
先月の最後の日付：`new Date(year, month, 0)` // dayに0を入れる

![b01c36d2-ddaf-4006-a348-0e39b86020ef - コピー.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187768/08b19469-cde3-496f-1dee-2f655cb33137.jpeg)

上の図にちゃっかり`getDay()`も書いていますが、曜日については日曜日の0が始めで、その後に月、火、水、木、金、土の順で数値が上がります。図では火曜日なので2が返ってきます。
この仕様はpythonでもありましたし、割と一般的かなと思っているのでこれをそのまま使います。

### 埋めるべき日数を求める
ここから結構難しいのですが、最初に表示する曜日を考慮して、先月で埋めるべき日数を求めます。
まず最初は簡単に日曜日スタートからで考えますが、その場合は今月の始めの日付から求まる`getDay()`の数値がそのまま埋めるべき日数に当たります。火曜日の場合は2と返り、2日分埋めればいいですね。
次に月曜日スタートで考えます。1日ずれたので先ほどの例だと1日分埋めればよさそうです。曜日の表現は0, 1, 2で表すと月曜日は1で、この数だけ減らすと求まりそうな気がします。具体的には開始曜日の変数を`offset`とすると、`getDay() - offset`としたら求まりそうです。
火曜日スタートも同様にすれば求まるので、水曜日スタートについて考えます。`getDay() - offset`で計算すると`-1`になってしまい、上手く求まりません。しかしもう1週増えたと考えて7を足すと結果は6になり埋めるべき日数がちゃんと求まります。

逆サイドの値に移動する実装は`%`を使うと上手くできて、`(7 + getDay() - offset) % 7`とすると全ての曜日に対して対応することができます。

![b01c36d2-ddaf-4006-a348-0e39b86020ef.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187768/691a898e-5da6-83c1-b3c2-54ee47fc0fcc.jpeg)

### まとめ
まとめると、コードは以下のようになります。

```javascript:先月の埋めるべき日付リストを生成する
// 前月の日を埋める
const prevPaddingDays = (() => {
  const firstDay = (new Date(year, month)).getDay();
  // 埋める日数を求める（日曜日スタートがoffset:0で、月曜日からにする場合はoffset:1にする）
  const paddingDayCount = (firstDay + 7 - offset) % 7;
  const prevLastDate = (new Date(year, month, 0)).getDate();
  return _.range(prevLastDate - paddingDayCount + 1, prevLastDate + 1).map((day) => ({
    date: new Date(year, month - 1, day),
    day,
    isPrev: true,
    isNext: false,
  }));
})();
```

## 今月の日付の生成
先月の埋めるべき日付が求められたら後は簡単です。
今月の最終日は先ほどのやり方を使って`new Date(year, month + 1, 0)`とすれば分かります。0の場合は1月前になってしまうので先に+1しておくのには注意してください。後は日数分だけリストを作ればOKです。

```javascript:今月の日付の生成
// 今月の日にちリストを生成する
const currentDays = (() => {
  const lastDate = new Date(year, month + 1, 0);
  const currentDayCount = lastDate.getDate();
  return _.range(1, currentDayCount + 1).map((day) => ({
    date: new Date(year, month, day),
    day,
    isPrev: false,
    isNext: false,
  }));
})();
```

## 来月の見えている日付の生成
来月の日数も曜日から算出してもいいですが、別の方法で少し楽をします。
要は7の倍数になるように最後の日付を埋めればいいので、`%`で余りを求めて埋めるべき日数を算出します。

```javascript:来月の埋めるべき日付リストを生成する
// 来月の日を埋める
const nextPaddingDays = (() => {
  const paddingDayCount = (42 - (prevPaddingDays.length + currentDays.length)) % 7;
  return _.range(1, paddingDayCount + 1).map((day) => ({
    date: new Date(year, month + 1, day),
    day,
    isPrev: false,
    isNext: true,
  }));
})();
```

## カレンダーリストとして整える
最後にこれらの日付リストを結合したら完成です。
gridで表示する場合は1次元のままで問題ないですが、flexboxなどで実装する場合は2次元のほうがやりやすいのでここでは2次元リストに変換をしています。

```javascript:カレンダーリストとして整える
// 結合する
const flatCalendar = [
  ...prevPaddingDays,
  ...currentDays,
  ...nextPaddingDays,
];

// 2次元配列にして返す
return _.range(0, flatCalendar.length / 7).map((i) => {
  return flatCalendar.slice(i * 7, (i + 1) * 7);
});
```

## カレンダーリストのデータに従って表示
後は表示するだけなので説明は割愛します。
最後にVue.jsで実装したサンプルコードを載せますので、詳細はそちらをご参照ください。

# 終わりに
以上がカレンダーの実装方法でした。月曜日スタートのカレンダーにしなくちゃいけなくて困っている方がいましたら、ぜひ参考にしてみてください。こちらだと水曜日スタートとかもできますよ(笑)。

<p class="codepen" data-height="487" data-theme-id="default" data-default-tab="js,result" data-user="wintyo" data-slug-hash="JjoYQLX" style="height: 487px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="カレンダー">
  <span>See the Pen <a href="https://codepen.io/wintyo/pen/JjoYQLX">
  カレンダー</a> by wintyo (<a href="https://codepen.io/wintyo">@wintyo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>