目次
サンプル・プログラム
カレンダーを作るのに必要な関数
今回は、上図のように、指定した年月の1ヵ月カレンダーを表示することを目的にする。
次に、手作業でカレンダーを作ることを想像しながら、必要なプロシージャを洗い出していく。
まず、6月1日の曜日を調べる。身近にあるカレンダーやネットで調べれば、6月1日は火曜日(列番号2)であることがわかる。
そこで、0行2列を1日として、右方向へ数字を並べていく。土曜日(列番号6)になったら折り返し、次の行に書いてゆく。
6月は30日まであるので、30まで書き続ける。
- 1日の曜日(番号)を求める
- 月の日数を求める
- 上記1から2まで、ループ文を使って日数を加算しながらカレンダー・フレームを埋めてゆく
曜日を求める関数
ネットを検索すると「ツェラーの公式」という計算式が見つかる。Wikipediaにある公式を参考に、「日曜日=0~土曜日=6」となる下記の計算式を選んだ。
\( \displaystyle C = \lfloor \frac{y}{10} \rfloor \)
\( \displaystyle Y = y \quad mod \quad 7 \)
\( \displaystyle h = (5C + Y + \lfloor \frac{y}{4} \rfloor + \lfloor \frac{C}{4} \rfloor + \lfloor \frac{26(m + 1)}{10} \rfloor + d) \quad mod \quad 7 \)
\( y \):西暦年,\( m \):月,\( d \):日
※ただし、\( m \)が1,2の時は、13月、14月として扱い、\( y \)を1だけ減じる。
\( \displaystyle \lfloor x \rfloor \):xを超えない最大の整数(Math.floorメソッド)
これを関数に書くと、次のようになる。
前半で、引数が計算可能な値であることを調べるバリデーションを行っている。計算可能な値でなければ、曜日番号としてはあり得ない -1 を返す。
79: /**
80: * 指定した年月日の曜日番号を求める.
81: * ツェラーの公式を利用する.
82: * @param Number year 西暦年
83: * @param Number month 月
84: * @param Number day 日
85: * @return Number 曜日番号(0:日曜日, 1:月曜日...6:土曜日)/(-1):引数エラー
86: */
87: function getWeekNumber(year, month, day) {
88: //引数のバリデーション
89: if (! Number.isInteger(year) || ! Number.isInteger(month) ||
90: ! Number.isInteger(day) ||
91: month < 1 || month > 12) {
92: return (-1);
93: }
94: maxDay = getDaysInMonth(year, month);
95: if (day < 1 || day > maxDay) {
96: return (-1);
97: }
98:
99: //ツェラーの公式の変形
100: if (month <= 2) {
101: month += 12;
102: year--;
103: }
104: c = Math.floor(year / 100);
105: y = year % 100;
106: h = (5 * c + y + Math.floor(y / 4) + Math.floor(c / 4) + Math.floor(26 * (month + 1) / 10) + day - 1) % 7;
107:
108: return h;
109: }
月の日数を求める関数
「3.2 switch~case文」で学んだプロシージャをベースに、うるう年対応にして関数化する。
2月の月の日数が可変となるため、switch~case文では扱いが難しい。
そこで、配列を利用する。
ここでは添字を月とみなして、あらかじめ月の日数を代入しておく。
70: //月の日数
71: let daysInMoth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
x = [a, b, ...];のように書く。配列xの添字0に要素aが、添字1に要素bが代入されてゆく。
配列xの添字0の要素を参照するには x[0] と書けばいい。添字には変数を利用することができる。
配列xは変数であるから、うるう年の時は、2月に29を代入すればいい。うるう年の計算は、前回作ったユーザー定義関数 isleap を用いる。
57: /**
58: * 指定した年月の日数(末日までの日数)を求める.
59: * @param Number year 西暦年
60: * @param Number month 月
61: * @return Number 月の日数/(-1):引数エラー
62: */
63: function getDaysInMonth(year, month) {
64: //引数のバリデーション
65: if (! Number.isInteger(year) || ! Number.isInteger(month) ||
66: month < 1 || month > 12) {
67: return (-1);
68: }
69:
70: //月の日数
71: let daysInMoth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
72:
73: //うるう年判定
74: daysInMoth[2] = isLeapYear(year) ? 29 : 28;
75:
76: return daysInMoth[month];
77: }
カレンダー・フレームを日付で埋める
このフレームをいきなり画面に表示するのではなく、7列×5行の配列に日付を代入していくことを考えることにする。
添字が列・行の2方向に及ぶが、こういう配列を2次元配列を呼ぶ。たとえば、2021年6月1日は火曜日なので、0行目の2列目に1を代入する。以後、3列目に2を、4列目に3を‥‥6列目に5を代入したら折り返して、1行目の0列目に6を代入していく。
JavaScriptでは、2次元配列を x[a][b] のように書く。
138: //カレンダー配列の初期化
139: let calendarArray = new Array(MAX_ROW);
140: for (let row = 0; row < MAX_ROW; row++) {
141: calendarArray[row] = new Array(MAX_COL);
142: for (let col = 0; col < MAX_COL; col++) {
143: calendarArray[row][col] = 0;
144: }
145: }
先ほどは、days = [0, 31, 28, 31, 30,... のようにして1次元配列を初期化したが、今回のようにループ文を使って値を代入していく場合には Array オブジェクトを生成する必要がある。詳しいことはコラム欄に譲るが、ここでは、配列の初期化のお作法として、こういう書き方があると覚えておいてほしい。
167: //配列calendarArrayに日付を格納
168: day = 1;
169: col = startWeekNumber;
170: row = 0;
171: do {
172: calendarArray[row][col] = day;
173: day++;
174: if (++col >= MAX_COL) {
175: col = 0;
176: row++;
177: }
178: } while (day <= goalDay);
これを doループ で回し、配列 calendar に日付を代入していく。
表示用HTMLタグを生成する
180: //配列calendarArrayを表形式に展開
181: for (let row = 0; row < MAX_ROW; row++) {
182: if ((row >= (MAX_ROW - 1)) && (calendarArray[row][0] === 0)) break; //最後が空行ならbreak
183: html +="<tr>\n";
184: for (let col = 0; col < MAX_COL; col++) {
185: if (calendarArray[row][col] > 0) {
186: html +=`
187: <td class="${WEEK_LIST[col]}">${calendarArray[row][col]}</td>
188: `;
189: } else {
190: html +=`
191: <td> </td>
192: `;
193: }
194: }
195: html +="</tr>\n";
196: }
197: //結果を表示
198: document.getElementById('calendar').innerHTML = html;
最後に、カレンダー作成を行う関数 dispMonthlyCalendar を通して見ておこう。
111: /**
112: * テキストボックスから年月を取得し,1ヵ月カレンダーを表示する.
113: * @param なし
114: * @return なし
115: */
116: function dispMonthlyCalendar() {
117: //カレンダーの行列サイズ
118: const MAX_ROW = 6;
119: const MAX_COL = 7;
120: //曜日の種類
121: const SUNDAY = 'sunday';
122: const SATURDAY = 'saturday';
123: const WEEKDAY = 'weekday';
124: //曜日表
125: const WEEK_LIST = [SUNDAY, WEEKDAY, WEEKDAY, WEEKDAY, WEEKDAY, WEEKDAY, SATURDAY];
126:
127: //年月を取得
128: let year = parseInt(document.getElementById('year').value, 10);
129: let month = parseInt(document.getElementById('month').value, 10);
130:
131: //月の日数,1日の曜日番号
132: goalDay = getDaysInMonth(year, month);
133: startWeekNumber = getWeekNumber(year, month, 1);
134:
135: //エラーメッセージをクリア
136: document.getElementById('error').innerHTML = '';
137:
138: //カレンダー配列の初期化
139: let calendarArray = new Array(MAX_ROW);
140: for (let row = 0; row < MAX_ROW; row++) {
141: calendarArray[row] = new Array(MAX_COL);
142: for (let col = 0; col < MAX_COL; col++) {
143: calendarArray[row][col] = 0;
144: }
145: }
146:
147: //カレンダーの1行目
148: html =`
149: <tr>
150: <th class="sunday">日</th>
151: <th class="weekday">月</th>
152: <th class="weekday">火</th>
153: <th class="weekday">水</th>
154: <th class="weekday">木</th>
155: <th class="weekday">金</th>
156: <th class="saturday">土</th>
157: </tr>
158: `;
159:
160: //バリデーション
161: if ((goalDay === (-1)) || (startWeekNumber === (-1))) {
162: //エラーを表示
163: document.getElementById('error').innerHTML = 'エラー:正しい月を入力してください.';
164:
165: //カレンダー作成
166: } else {
167: //配列calendarArrayに日付を格納
168: day = 1;
169: col = startWeekNumber;
170: row = 0;
171: do {
172: calendarArray[row][col] = day;
173: day++;
174: if (++col >= MAX_COL) {
175: col = 0;
176: row++;
177: }
178: } while (day <= goalDay);
179:
180: //配列calendarArrayを表形式に展開
181: for (let row = 0; row < MAX_ROW; row++) {
182: if ((row >= (MAX_ROW - 1)) && (calendarArray[row][0] === 0)) break; //最後が空行ならbreak
183: html +="<tr>\n";
184: for (let col = 0; col < MAX_COL; col++) {
185: if (calendarArray[row][col] > 0) {
186: html +=`
187: <td class="${WEEK_LIST[col]}">${calendarArray[row][col]}</td>
188: `;
189: } else {
190: html +=`
191: <td> </td>
192: `;
193: }
194: }
195: html +="</tr>\n";
196: }
197: //結果を表示
198: document.getElementById('calendar').innerHTML = html;
199: }
200: calendarArray = null;
201: }
読みやすいプログラム:変数名
今回は、これらの関数の中で使っている変数の命名規則を紹介する。
●変数の命名規則
- 変数の内容を名詞で表現する。
- 並べる順序は形容詞+名詞
- 発音しやすい英単語を並べる。
- 単語はローワーキャメルケースで並べる。
- 単語は最大3つ程度まで。
- 配列は複数形またはListやArrayを負荷する。
getDaysInMonth の中で定義している、月の日数を表す配列は、daysInMoth と複数形にした。
カレンダー配列は、カレンダーが複数あるわけではなくカレンダーの構造を配列で表していることから、calendarArray と命名した。
関数名と変数名は、ともにローワーキャメルケースだ。
PHPでは、変数名の頭文字はドルマーク $ ではじまるので区別しやすいが、JavaScriptではそういうわけにはいかない。動詞ではじまるのが関数という命名規則によって両者を識別する。
読みやすいプログラム:定数名
117: //カレンダーの行列サイズ
118: const MAX_ROW = 6;
119: const MAX_COL = 7;
120: //曜日の種類
121: const SUNDAY = 'sunday';
122: const SATURDAY = 'saturday';
123: const WEEKDAY = 'weekday';
124: //曜日表
125: const WEEK_LIST = [SUNDAY, WEEKDAY, WEEKDAY, WEEKDAY, WEEKDAY, WEEKDAY, SATURDAY];
126:
●定数の命名規則
- 変数の内容を名詞で表現する。
- 並べる順序は形容詞+名詞
- 発音しやすい英単語を並べる。
- 単語はアッパースネークケースで並べる。
- 単語は最大3つ程度まで。
読みやすいプログラム:マジックナンバーを使わない
読みやすいプログラムでは、マジックナンバーを使わず、定数などに置き換える。
こうすることで、プログラムの仕様変更の時、定数の変更だけで対応できるようになる。
コラム:JavaScriptの配列
例題で紹介した days = [0, 31, 28, 31, 30,... は、じつは days というオブジェクトを生成している。
2次元配列を初期化するのに calendar = new Array(ROW_MAX) と書いているのは、配列がオブジェクトであることを端的に表している。
JavaScriptの配列は、添字に0から始まる整数しか使えないという制約がある。PHPやPythonのような連想配列には対応していない。しかし、要素として代入できるデータ型に制約はない。
そして、JavaScriptの配列は1次元しか実在しておらず、2次元配列は calendar[0]に別の1次元配列(オブジェクト)を代入している。
前述の2次元配列を初期化の際、2次元目の要素を初期化するのに calendar[row] = new Array(COL_MAX); と書いているのが、そのことをよく表している。
これにより、3次元、4次元‥‥という多次元配列も実現できる。