JavaScriptでカレンダーを実装する – Dateオブジェクトの基礎から学ぶ

プログラミング

今回は、JavaScriptを使ってカレンダーを一から実装する方法を解説します。

カレンダーを作る過程で、Dateオブジェクトの扱い方、日付計算のロジック、動的なHTML生成など、多くのことを学びました。特にDateオブジェクトのメソッドには癖があり、最初は戸惑うことが多かったです。

この記事では、カレンダー実装の基本から、私が実際につまずいたポイントまで詳しく解説します。

完成イメージ

まず、作るカレンダーの完成形を見てみましょう。

月単位でカレンダーを表示し、前月・次月への移動ができます。今日の日付はハイライト表示されます。

Dateオブジェクトの基礎

カレンダーを作る前に、JavaScriptのDateオブジェクトについて理解する必要があります。

Dateオブジェクトとは

Dateオブジェクトは、JavaScriptで日付や時刻を扱うためのオブジェクトです。

// 現在の日時
let now = new Date();
console.log(now);  // Mon Dec 15 2024 10:30:00 GMT+0900

// 特定の日付
let someDay = new Date(2024, 11, 15);  // 2024年12月15日

重要なメソッド

カレンダー実装に必要な主なメソッドを紹介します。

getFullYear()

指定された日時の年に相当する絶対的な値を返します。

let date = new Date(2024, 11, 15);
console.log(date.getFullYear());  // 2024

getMonth()

指定された日時の月を返します。0からスタートするため、1月は0になることに注意が必要です。

let date = new Date(2024, 0, 15);  // 2024年1月15日
console.log(date.getMonth());  // 0(1月)

let date2 = new Date(2024, 11, 15);  // 2024年12月15日
console.log(date2.getMonth());  // 11(12月)

最初、これを知らなくて1ヶ月ズレたカレンダーができてしまいました。月を扱うときは常に「実際の月 – 1」を意識する必要があります。

getDate()

1から31までの日付を返します。月の何日目かを取得するメソッドです。

let date = new Date(2024, 11, 15);
console.log(date.getDate());  // 15

getDay()

指定した日時の曜日に対応した数値を返します。日曜日は0からスタートします。

let date = new Date(2024, 11, 1);  // 2024年12月1日(日曜日)
console.log(date.getDay());  // 0(日曜日)

// 曜日の対応
// 0 = 日曜日
// 1 = 月曜日
// 2 = 火曜日
// 3 = 水曜日
// 4 = 木曜日
// 5 = 金曜日
// 6 = 土曜日


カレンダー実装に必要な情報の計算

カレンダーを生成するには、以下の4つの情報が必要です。これらは全て generateCalendar() 関数の冒頭で計算しています。

// 必要な情報を計算
const startDay = new Date(year, month - 1, 1).getDay();          // 月の最初の曜日
const endDate = new Date(year, month, 0).getDate();              // 月の日数
const today = new Date();                                         // 今日の日付
const lastMonthEndDate = new Date(year, month - 1, 0).getDate(); // 前月の最終日

1. 月の最初の曜日 (startDay)

その月の1日が何曜日から始まるかを知る必要があります。これによって、カレンダーの最初の行をどこから埋めるかが決まります。

let startDay = new Date(year, month - 1, 1).getDay();
// 例: 2024年12月1日 → 0(日曜日)

2. 月の日数 (endDate)

その月が何日まであるかを知る必要があります。ここで使っているのが、Dateオブジェクトのトリックです。new Date(2024, 12, 0) は「2024年12月の0日」、つまり「2024年11月の最終日」を意味します。

let endDate = new Date(year, month, 0).getDate();
// 例: new Date(2024, 12, 0) → 2024年12月31日 → 31

このように、日付に0を指定することで前月の最終日を取得できます。最初、この書き方を見たときは「なぜ12月なのに13月?」と混乱しました。仕組みとしては、month + 1 の月を指定して、日を 0 にすることで、その前月の最終日が取得できるということです。

3. 前月の最終日 (lastMonthEndDate)

カレンダーの余白を埋めるために、前月の最終日も必要です。

let lastMonthEndDate = new Date(year, month - 1, 0).getDate();
// 例: new Date(2024, 11, 0) → 2024年11月30日 → 30

4. 今日の日付 (today)

今日の日付をハイライト表示するために使います。

let today = new Date();



カレンダー生成の完全なコード

まず、完全なコードを見てから、各部分を詳しく解説します。

HTML

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>カレンダー</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="calendar-container">
        <button id="prevMonth">前の月</button>
        <button id="nextMonth">次の月</button>
        <div id="calendar"></div>
    </div>
    <script src="script.js"></script>
</body>
</html>

CSS (style.css)

.calendar-container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
}

button {
    padding: 10px 20px;
    margin: 10px 5px;
    cursor: pointer;
}

table {
    width: 100%;
    border-collapse: collapse;
    font-weight: bold;
    margin-top: 20px;
}

td, th {
    border: 2px solid #ddd;
    padding: 10px;
    text-align: center;
}

th {
    background-color: #f2f2f2;
}

/* 日曜日(最初の列)*/
td:first-child {
    color: #ff0000;
}

/* 土曜日(最後の列)*/
td:last-child {
    color: #0011ff;
}

/* 無効な日付(前月・次月)*/
td.is-disabled {
    color: #787878;
}

/* 今日の日付 */
td.is-today {
    background-color: #ffff99;
}

JavaScript (script.js)

// 現在の年月を取得
let year = new Date().getFullYear();
let month = new Date().getMonth() + 1;

// カレンダーを生成する関数
function generateCalendar(year, month) {
    // 必要な情報を計算
    const startDay = new Date(year, month - 1, 1).getDay();
    const endDate = new Date(year, month, 0).getDate();
    const today = new Date();
    const lastMonthEndDate = new Date(year, month - 1, 0).getDate();
    
    let dayCount = 1;
    let calendarHtml = '<table>';
    
    // タイトル行
    calendarHtml += `<tr><th colspan="7">${year}年 ${month}月</th></tr>`;
    
    // 曜日ヘッダー
    calendarHtml += '<tr>';
    const weeks = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
    for (let i = 0; i < weeks.length; i++) {
        calendarHtml += `<th>${weeks[i]}</th>`;
    }
    calendarHtml += '</tr>';
    
    // カレンダー本体(6週分)
    for (let i = 0; i < 6; i++) {
        calendarHtml += '<tr>';
        for (let j = 0; j < 7; j++) {
            if (i === 0 && j < startDay) {
                // パターン1: 前月の余白
                let num = lastMonthEndDate - startDay + j + 1;
                calendarHtml += `<td class="is-disabled">${num}</td>`;
            } else if (dayCount > endDate) {
                // パターン2: 次月の余白
                let num = dayCount - endDate;
                calendarHtml += `<td class="is-disabled">${num}</td>`;
                dayCount++;
            } else {
                // パターン3: 当月の日付
                const isToday = (year === today.getFullYear() && 
                                 month === (today.getMonth() + 1) && 
                                 dayCount === today.getDate());
                
                let classNames = isToday ? 'is-today' : '';
                calendarHtml += `<td class="${classNames}">${dayCount}</td>`;
                dayCount++;
            }
        }
        calendarHtml += '</tr>';
    }
    
    calendarHtml += '</table>';
    
    // DOMに挿入
    document.getElementById('calendar').innerHTML = calendarHtml;
}

// 初期表示
generateCalendar(year, month);

// 前月ボタン
document.getElementById('prevMonth').addEventListener('click', () => {
    month--;
    if (month < 1) {
        month = 12;
        year--;
    }
    generateCalendar(year, month);
});

// 次月ボタン
document.getElementById('nextMonth').addEventListener('click', () => {
    month++;
    if (month > 12) {
        month = 1;
        year++;
    }
    generateCalendar(year, month);
});


コードの詳細解説

それでは、各部分を詳しく見ていきましょう。

カレンダー生成のロジック

カレンダーを生成する generateCalendar() 関数が、このコードの核心部分です。

3パターンのセル生成の解説

カレンダーの各セルは、完全なコードの中で3つのパターンに分かれて処理されています。

// カレンダー本体(6週分)
for (let i = 0; i < 6; i++) {
    calendarHtml += '<tr>';
    for (let j = 0; j < 7; j++) {
        if (i === 0 && j < startDay) {
            // パターン1: 前月の余白
        } else if (dayCount > endDate) {
            // パターン2: 次月の余白
        } else {
            // パターン3: 当月の日付
        }
    }
    calendarHtml += '</tr>';
}

パターン1: 前月の余白

月の最初の曜日より前のセルには、前月の日付を薄く表示します。

if (i === 0 && j < startDay) {
    // 1行目で、月の開始曜日より前
    let num = lastMonthEndDate - startDay + j + 1;
    calendarHtml += `<td class="is-disabled">${num}</td>`;
}

この計算式が最初は全く理解できませんでした。例えば、12月1日が水曜日(startDay = 3)、11月が30日までの場合を考えてみます。

日曜(j=0)の計算: 30 - 3 + 0 + 1 = 28
月曜(j=1)の計算: 30 - 3 + 1 + 1 = 29
火曜(j=2)の計算: 30 - 3 + 2 + 1 = 30

結果:
日 月 火 水 木 金 土
28 29 30  1  2  3  4

前月(11月)の最終日が30日なので、28、29、30が正しく表示されます。

パターン2: 次月の余白

月の日数を超えた後のセルには、次月の日付を薄く表示します。

else if (dayCount > endDate) {
    // 月の日数を超えた
    let num = dayCount - endDate;
    calendarHtml += `<td class="is-disabled">${num}</td>`;
    dayCount++;
}

例えば、12月は31日までなので、32日目のセルには次月(1月)の1日が表示されます。

パターン3: 当月の日付

実際の当月の日付を表示します。今日の日付は特別なクラスを付けます。

else {
    // 今日かどうか判定
    const isToday = (year === today.getFullYear() && 
                     month === (today.getMonth() + 1) && 
                     dayCount === today.getDate());
    
    let classNames = isToday ? 'is-today' : '';
    
    calendarHtml += `<td class="${classNames}">${dayCount}</td>`;
    dayCount++;
}


前月・次月の移動

カレンダーに前月・次月ボタンを追加して、月を切り替えられるようにします。

HTML

<button id="prevMonth">前の月</button>
<button id="nextMonth">次の月</button>
<div id="calendar"></div>

JavaScript

let year = new Date().getFullYear();
let month = new Date().getMonth() + 1;

// 初期表示
generateCalendar(year, month);

// 前月ボタン
document.getElementById('prevMonth').addEventListener('click', () => {
    month--;
    if (month < 1) {
        month = 12;
        year--;
    }
    generateCalendar(year, month);
});

// 次月ボタン
document.getElementById('nextMonth').addEventListener('click', () => {
    month++;
    if (month > 12) {
        month = 1;
        year++;
    }
    generateCalendar(year, month);
});

ポイントは、1月から前月に行くと12月(前年)に、12月から次月に行くと1月(翌年)になるように制御することです。

// 1月 → 前月 → 12月(前年)
year = 2024, month = 1
↓ 前月ボタン
month = 0 → month = 12, year = 2023

// 12月 → 次月 → 1月(翌年)
year = 2024, month = 12
↓ 次月ボタン
month = 13 → month = 1, year = 2025


CSSでのスタイリング

カレンダーに見た目を整えるCSSを追加します。

基本的なスタイル

table {
    width: 100%;
    border-collapse: collapse;
    font-weight: bold;
}

td, th {
    border: 2px solid #ddd;
    padding: 10px;
    text-align: center;
}

th {
    background-color: #f2f2f2;
}

曜日ごとの色分け

/* 日曜日(最初の列)*/
td:first-child {
    color: #ff0000;
}

/* 土曜日(最後の列)*/
td:last-child {
    color: #0011ff;
}

無効な日付(前月・次月)

td.is-disabled {
    color: #787878;
}

今日の日付

td.is-today {
    background-color: #ffff99;
}


つまずいたポイント

1. 月が0から始まる

Dateオブジェクトの月は0から始まるため、常に実際の月より1小さい値を指定する必要があります。

// ❌ 間違い
let month = 12;  // 12月のつもり
new Date(2024, 12, 1)  // → 2025年1月1日になる

// ✅ 正解
let month = 12;  // 12月
new Date(2024, month - 1, 1)  // → 2024年12月1日

最初、これに気づかずに1ヶ月ズレたカレンダーができて、何度もデバッグしました。

2. 前月の日付計算

前月の余白の日付を計算する式が、最初は全く理解できませんでした。

let num = lastMonthEndDate - startDay + j + 1;

この式を理解するには、実際に数値を当てはめて何度も計算してみることが必要でした。紙に書いて確認したことで、ようやく理解できました。

3. getDate()とgetDay()の混同

getDate() は日付(1-31)を返し、getDay() は曜日(0-6)を返します。似た名前なので、最初はよく混同していました。

let date = new Date(2024, 11, 15);

date.getDate();  // 15(日付)
date.getDay();   // 0(日曜日)

4. 月末日の取得方法

月の日数を取得する new Date(year, month, 0) のトリックが、最初は理解できませんでした。「なぜ次の月を指定するのか」「なぜ0日なのか」が分からず、調べるまで時間がかかりました。

// 2024年12月は何日まで?
let endDate = new Date(2024, 12, 0).getDate();
// → new Date(2024, 12, 0) = 2024年12月の0日 = 2024年11月の最終日


まとめ

今回、カレンダーを実装して学んだポイントです。

JavaScriptのDateオブジェクトには独特な癖があります。特に月が0から始まることは、常に意識する必要があります。また、日付に0を指定することで前月の最終日を取得できるトリックは、カレンダー実装では必須のテクニックです。

カレンダー生成のロジックは、3パターンのセルを正しく処理することが重要です。前月の余白、当月の日付、次月の余白をそれぞれ適切に計算して表示する必要があります。

前月の日付計算の式は複雑に見えますが、実際に数値を当てはめて計算してみることで理解できます。分からないときは、紙に書いて確認するのが一番です。

Dateオブジェクトの主要メソッドを理解すれば、カレンダー以外にも様々な日付処理ができるようになります。getFullYear()で年、getMonth()で月(0始まり)、getDate()で日、getDay()で曜日(日曜が0)を取得できることを覚えておきましょう。


関連記事:

  • 現在準備中

カテゴリー: 開発記録
タグ: JavaScript, カレンダー, Date, 日付計算, DOM操作

コメント

タイトルとURLをコピーしました